##// END OF EJS Templates
notifications: support real-time notifications with websockets via channelstream
ergo -
r526:1b57d2ee default
parent child Browse files
Show More
@@ -0,0 +1,79 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22
23 from pyramid.settings import asbool
24
25 from rhodecode.config.routing import ADMIN_PREFIX
26 from rhodecode.lib.ext_json import json
27
28
29 def url_gen(request):
30 urls = {
31 'connect': request.route_url('channelstream_connect'),
32 'subscribe': request.route_url('channelstream_subscribe')
33 }
34 return json.dumps(urls)
35
36
37 PLUGIN_DEFINITION = {
38 'name': 'channelstream',
39 'config': {
40 'javascript': [],
41 'css': [],
42 'template_hooks': {
43 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.html'
44 },
45 'url_gen': url_gen,
46 'static': None,
47 'enabled': False,
48 'server': '',
49 'secret': ''
50 }
51 }
52
53
54 def includeme(config):
55 settings = config.registry.settings
56 PLUGIN_DEFINITION['config']['enabled'] = asbool(
57 settings.get('channelstream.enabled'))
58 PLUGIN_DEFINITION['config']['server'] = settings.get(
59 'channelstream.server', '')
60 PLUGIN_DEFINITION['config']['secret'] = settings.get(
61 'channelstream.secret', '')
62 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
63 'channelstream.history.location', '')
64 config.register_rhodecode_plugin(
65 PLUGIN_DEFINITION['name'],
66 PLUGIN_DEFINITION['config']
67 )
68 # create plugin history location
69 history_dir = PLUGIN_DEFINITION['config']['history.location']
70 if history_dir and not os.path.exists(history_dir):
71 os.makedirs(history_dir, 0750)
72
73 config.add_route(
74 name='channelstream_connect',
75 pattern=ADMIN_PREFIX + '/channelstream/connect')
76 config.add_route(
77 name='channelstream_subscribe',
78 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
79 config.scan('rhodecode.channelstream')
@@ -0,0 +1,177 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 """
22 Channel Stream controller for rhodecode
23
24 :created_on: Oct 10, 2015
25 :author: marcinl
26 :copyright: (c) 2013-2015 RhodeCode GmbH.
27 :license: Commercial License, see LICENSE for more details.
28 """
29
30 import logging
31 import uuid
32
33 from pylons import tmpl_context as c
34 from pyramid.settings import asbool
35 from pyramid.view import view_config
36 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPBadGateway
37
38 from rhodecode.lib.channelstream import (
39 channelstream_request,
40 ChannelstreamConnectionException,
41 ChannelstreamPermissionException,
42 check_channel_permissions,
43 get_connection_validators,
44 get_user_data,
45 parse_channels_info,
46 update_history_from_logs,
47 STATE_PUBLIC_KEYS)
48 from rhodecode.lib.auth import NotAnonymous
49 from rhodecode.lib.utils2 import str2bool
50
51 log = logging.getLogger(__name__)
52
53
54 class ChannelstreamView(object):
55 def __init__(self, context, request):
56 self.context = context
57 self.request = request
58
59 # Some of the decorators rely on this attribute to be present
60 # on the class of the decorated method.
61 self._rhodecode_user = request.user
62 registry = request.registry
63 self.channelstream_config = registry.rhodecode_plugins['channelstream']
64 if not self.channelstream_config.get('enabled'):
65 log.exception('Channelstream plugin is disabled')
66 raise HTTPBadRequest()
67
68 @NotAnonymous()
69 @view_config(route_name='channelstream_connect', renderer='json')
70 def connect(self):
71 """ handle authorization of users trying to connect """
72 try:
73 json_body = self.request.json_body
74 except Exception:
75 log.exception('Failed to decode json from request')
76 raise HTTPBadRequest()
77 try:
78 channels = check_channel_permissions(
79 json_body.get('channels'),
80 get_connection_validators(self.request.registry))
81 except ChannelstreamPermissionException:
82 log.error('Incorrect permissions for requested channels')
83 raise HTTPForbidden()
84
85 user = c.rhodecode_user
86 if user.user_id:
87 user_data = get_user_data(user.user_id)
88 else:
89 user_data = {
90 'id': None,
91 'username': None,
92 'first_name': None,
93 'last_name': None,
94 'icon_link': None,
95 'display_name': None,
96 'display_link': None,
97 }
98 payload = {
99 'username': user.username,
100 'user_state': user_data,
101 'conn_id': str(uuid.uuid4()),
102 'channels': channels,
103 'channel_configs': {},
104 'state_public_keys': STATE_PUBLIC_KEYS,
105 'info': {
106 'exclude_channels': ['broadcast']
107 }
108 }
109 filtered_channels = [channel for channel in channels
110 if channel != 'broadcast']
111 for channel in filtered_channels:
112 payload['channel_configs'][channel] = {
113 'notify_presence': True,
114 'history_size': 100,
115 'store_history': True,
116 'broadcast_presence_with_user_lists': True
117 }
118 # connect user to server
119 try:
120 connect_result = channelstream_request(self.channelstream_config,
121 payload, '/connect')
122 except ChannelstreamConnectionException:
123 log.exception('Channelstream service is down')
124 return HTTPBadGateway()
125
126 connect_result['channels'] = channels
127 connect_result['channels_info'] = parse_channels_info(
128 connect_result['channels_info'],
129 include_channel_info=filtered_channels)
130 update_history_from_logs(self.channelstream_config,
131 filtered_channels, connect_result)
132 return connect_result
133
134 @NotAnonymous()
135 @view_config(route_name='channelstream_subscribe', renderer='json')
136 def subscribe(self):
137 """ can be used to subscribe specific connection to other channels """
138 try:
139 json_body = self.request.json_body
140 except Exception:
141 log.exception('Failed to decode json from request')
142 raise HTTPBadRequest()
143 try:
144 channels = check_channel_permissions(
145 json_body.get('channels'),
146 get_connection_validators(self.request.registry))
147 except ChannelstreamPermissionException:
148 log.error('Incorrect permissions for requested channels')
149 raise HTTPForbidden()
150 payload = {'conn_id': json_body.get('conn_id', ''),
151 'channels': channels,
152 'channel_configs': {},
153 'info': {
154 'exclude_channels': ['broadcast']}
155 }
156 filtered_channels = [chan for chan in channels if chan != 'broadcast']
157 for channel in filtered_channels:
158 payload['channel_configs'][channel] = {
159 'notify_presence': True,
160 'history_size': 100,
161 'store_history': True,
162 'broadcast_presence_with_user_lists': True
163 }
164 try:
165 connect_result = channelstream_request(
166 self.channelstream_config, payload, '/subscribe')
167 except ChannelstreamConnectionException:
168 log.exception('Channelstream service is down')
169 return HTTPBadGateway()
170 # include_channel_info will limit history only to new channel
171 # to not overwrite histories on other channels in client
172 connect_result['channels_info'] = parse_channels_info(
173 connect_result['channels_info'],
174 include_channel_info=filtered_channels)
175 update_history_from_logs(self.channelstream_config,
176 filtered_channels, connect_result)
177 return connect_result
@@ -0,0 +1,219 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22 import os
23
24 import itsdangerous
25 import requests
26
27 from dogpile.core import ReadWriteMutex
28
29 import rhodecode.lib.helpers as h
30
31 from rhodecode.lib.auth import HasRepoPermissionAny
32 from rhodecode.lib.ext_json import json
33 from rhodecode.model.db import User
34
35 log = logging.getLogger(__name__)
36
37 LOCK = ReadWriteMutex()
38
39 STATE_PUBLIC_KEYS = ['id', 'username', 'first_name', 'last_name',
40 'icon_link', 'display_name', 'display_link']
41
42
43 class ChannelstreamException(Exception):
44 pass
45
46
47 class ChannelstreamConnectionException(Exception):
48 pass
49
50
51 class ChannelstreamPermissionException(Exception):
52 pass
53
54
55 def channelstream_request(config, payload, endpoint, raise_exc=True):
56 signer = itsdangerous.TimestampSigner(config['secret'])
57 sig_for_server = signer.sign(endpoint)
58 secret_headers = {'x-channelstream-secret': sig_for_server,
59 'x-channelstream-endpoint': endpoint,
60 'Content-Type': 'application/json'}
61 req_url = 'http://{}{}'.format(config['server'], endpoint)
62 response = None
63 try:
64 response = requests.post(req_url, data=json.dumps(payload),
65 headers=secret_headers).json()
66 except requests.ConnectionError:
67 log.exception('ConnectionError happened')
68 if raise_exc:
69 raise ChannelstreamConnectionException()
70 except Exception:
71 log.exception('Exception related to channelstream happened')
72 if raise_exc:
73 raise ChannelstreamConnectionException()
74 return response
75
76
77 def get_user_data(user_id):
78 user = User.get(user_id)
79 return {
80 'id': user.user_id,
81 'username': user.username,
82 'first_name': user.name,
83 'last_name': user.lastname,
84 'icon_link': h.gravatar_url(user.email, 14),
85 'display_name': h.person(user, 'username_or_name_or_email'),
86 'display_link': h.link_to_user(user),
87 }
88
89
90 def broadcast_validator(channel_name):
91 """ checks if user can access the broadcast channel """
92 if channel_name == 'broadcast':
93 return True
94
95
96 def repo_validator(channel_name):
97 """ checks if user can access the broadcast channel """
98 channel_prefix = '/repo$'
99 if channel_name.startswith(channel_prefix):
100 elements = channel_name[len(channel_prefix):].split('$')
101 repo_name = elements[0]
102 can_access = HasRepoPermissionAny(
103 'repository.read',
104 'repository.write',
105 'repository.admin')(repo_name)
106 log.debug('permission check for {} channel '
107 'resulted in {}'.format(repo_name, can_access))
108 if can_access:
109 return True
110 return False
111
112
113 def check_channel_permissions(channels, plugin_validators, should_raise=True):
114 valid_channels = []
115
116 validators = [broadcast_validator, repo_validator]
117 if plugin_validators:
118 validators.extend(plugin_validators)
119 for channel_name in channels:
120 is_valid = False
121 for validator in validators:
122 if validator(channel_name):
123 is_valid = True
124 break
125 if is_valid:
126 valid_channels.append(channel_name)
127 else:
128 if should_raise:
129 raise ChannelstreamPermissionException()
130 return valid_channels
131
132
133 def get_channels_info(self, channels):
134 payload = {'channels': channels}
135 # gather persistence info
136 return channelstream_request(self._config(), payload, '/info')
137
138
139 def parse_channels_info(info_result, include_channel_info=None):
140 """
141 Returns data that contains only secure information that can be
142 presented to clients
143 """
144 include_channel_info = include_channel_info or []
145
146 user_state_dict = {}
147 for userinfo in info_result['users']:
148 user_state_dict[userinfo['user']] = {
149 k: v for k, v in userinfo['state'].items()
150 if k in STATE_PUBLIC_KEYS
151 }
152
153 channels_info = {}
154
155 for c_name, c_info in info_result['channels'].items():
156 if c_name not in include_channel_info:
157 continue
158 connected_list = []
159 for userinfo in c_info['users']:
160 connected_list.append({
161 'user': userinfo['user'],
162 'state': user_state_dict[userinfo['user']]
163 })
164 channels_info[c_name] = {'users': connected_list,
165 'history': c_info['history']}
166
167 return channels_info
168
169
170 def log_filepath(history_location, channel_name):
171 filename = '{}.log'.format(channel_name.encode('hex'))
172 filepath = os.path.join(history_location, filename)
173 return filepath
174
175
176 def read_history(history_location, channel_name):
177 filepath = log_filepath(history_location, channel_name)
178 if not os.path.exists(filepath):
179 return []
180 history_lines_limit = -100
181 history = []
182 with open(filepath, 'rb') as f:
183 for line in f.readlines()[history_lines_limit:]:
184 try:
185 history.append(json.loads(line))
186 except Exception:
187 log.exception('Failed to load history')
188 return history
189
190
191 def update_history_from_logs(config, channels, payload):
192 history_location = config.get('history.location')
193 for channel in channels:
194 history = read_history(history_location, channel)
195 payload['channels_info'][channel]['history'] = history
196
197
198 def write_history(config, message):
199 """ writes a messge to a base64encoded filename """
200 history_location = config.get('history.location')
201 if not os.path.exists(history_location):
202 return
203 try:
204 LOCK.acquire_write_lock()
205 filepath = log_filepath(history_location, message['channel'])
206 with open(filepath, 'ab') as f:
207 json.dump(message, f)
208 f.write('\n')
209 finally:
210 LOCK.release_write_lock()
211
212
213 def get_connection_validators(registry):
214 validators = []
215 for k, config in registry.rhodecode_plugins.iteritems():
216 validator = config.get('channelstream', {}).get('connect_validator')
217 if validator:
218 validators.append(validator)
219 return validators
@@ -0,0 +1,268 b''
1 // Mix-ins
2 .borderRadius(@radius) {
3 -moz-border-radius: @radius;
4 -webkit-border-radius: @radius;
5 border-radius: @radius;
6 }
7
8 .boxShadow(@boxShadow) {
9 -moz-box-shadow: @boxShadow;
10 -webkit-box-shadow: @boxShadow;
11 box-shadow: @boxShadow;
12 }
13
14 .opacity(@opacity) {
15 @opacityPercent: @opacity * 100;
16 opacity: @opacity;
17 -ms-filter: ~"progid:DXImageTransform.Microsoft.Alpha(Opacity=@{opacityPercent})";
18 filter: ~"alpha(opacity=@{opacityPercent})";
19 }
20
21 .wordWrap(@wordWrap: break-word) {
22 -ms-word-wrap: @wordWrap;
23 word-wrap: @wordWrap;
24 }
25
26 // Variables
27 @black: #000000;
28 @grey: #999999;
29 @light-grey: #CCCCCC;
30 @white: #FFFFFF;
31 @near-black: #030303;
32 @green: #51A351;
33 @red: #BD362F;
34 @blue: #2F96B4;
35 @orange: #F89406;
36 @default-container-opacity: .8;
37
38 // Styles
39 .toast-title {
40 font-weight: bold;
41 }
42
43 .toast-message {
44 .wordWrap();
45
46 a,
47 label {
48 color: @near-black;
49 }
50
51 a:hover {
52 color: @light-grey;
53 text-decoration: none;
54 }
55 }
56
57 .toast-close-button {
58 position: relative;
59 right: -0.3em;
60 top: -0.3em;
61 float: right;
62 font-size: 20px;
63 font-weight: bold;
64 color: @black;
65 -webkit-text-shadow: 0 1px 0 rgba(255,255,255,1);
66 text-shadow: 0 1px 0 rgba(255,255,255,1);
67 .opacity(0.8);
68
69 &:hover,
70 &:focus {
71 color: @black;
72 text-decoration: none;
73 cursor: pointer;
74 .opacity(0.4);
75 }
76 }
77
78 /*Additional properties for button version
79 iOS requires the button element instead of an anchor tag.
80 If you want the anchor version, it requires `href="#"`.*/
81 button.toast-close-button {
82 padding: 0;
83 cursor: pointer;
84 background: transparent;
85 border: 0;
86 -webkit-appearance: none;
87 }
88
89 //#endregion
90
91 .toast-top-center {
92 top: 0;
93 right: 0;
94 width: 100%;
95 }
96
97 .toast-bottom-center {
98 bottom: 0;
99 right: 0;
100 width: 100%;
101 }
102
103 .toast-top-full-width {
104 top: 0;
105 right: 0;
106 width: 100%;
107 }
108
109 .toast-bottom-full-width {
110 bottom: 0;
111 right: 0;
112 width: 100%;
113 }
114
115 .toast-top-left {
116 top: 12px;
117 left: 12px;
118 }
119
120 .toast-top-right {
121 top: 12px;
122 right: 12px;
123 }
124
125 .toast-bottom-right {
126 right: 12px;
127 bottom: 12px;
128 }
129
130 .toast-bottom-left {
131 bottom: 12px;
132 left: 12px;
133 }
134
135 #toast-container {
136 position: fixed;
137 z-index: 999999;
138 // The container should not be clickable.
139 pointer-events: none;
140 * {
141 -moz-box-sizing: border-box;
142 -webkit-box-sizing: border-box;
143 box-sizing: border-box;
144 }
145
146 > div {
147 position: relative;
148 // The toast itself should be clickable.
149 pointer-events: auto;
150 overflow: hidden;
151 margin: 0 0 6px;
152 padding: 15px;
153 width: 300px;
154 .borderRadius(1px 1px 1px 1px);
155 background-position: 15px center;
156 background-repeat: no-repeat;
157 color: @near-black;
158 .opacity(@default-container-opacity);
159 }
160
161 > :hover {
162 .opacity(1);
163 cursor: pointer;
164 }
165
166 > .toast-info {
167 //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important;
168 }
169
170 > .toast-error {
171 //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important;
172 }
173
174 > .toast-success {
175 //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important;
176 }
177
178 > .toast-warning {
179 //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important;
180 }
181
182 /*overrides*/
183 &.toast-top-center > div,
184 &.toast-bottom-center > div {
185 width: 400px;
186 margin-left: auto;
187 margin-right: auto;
188 }
189
190 &.toast-top-full-width > div,
191 &.toast-bottom-full-width > div {
192 width: 96%;
193 margin-left: auto;
194 margin-right: auto;
195 }
196 }
197
198 .toast {
199 border-color: @near-black;
200 border-style: solid;
201 border-width: 2px 2px 2px 25px;
202 background-color: @white;
203 }
204
205 .toast-success {
206 border-color: @green;
207 }
208
209 .toast-error {
210 border-color: @red;
211 }
212
213 .toast-info {
214 border-color: @blue;
215 }
216
217 .toast-warning {
218 border-color: @orange;
219 }
220
221 .toast-progress {
222 position: absolute;
223 left: 0;
224 bottom: 0;
225 height: 4px;
226 background-color: @black;
227 .opacity(0.4);
228 }
229
230 /*Responsive Design*/
231
232 @media all and (max-width: 240px) {
233 #toast-container {
234
235 > div {
236 padding: 8px;
237 width: 11em;
238 }
239
240 & .toast-close-button {
241 right: -0.2em;
242 top: -0.2em;
243 }
244 }
245 }
246
247 @media all and (min-width: 241px) and (max-width: 480px) {
248 #toast-container {
249 > div {
250 padding: 8px;
251 width: 18em;
252 }
253
254 & .toast-close-button {
255 right: -0.2em;
256 top: -0.2em;
257 }
258 }
259 }
260
261 @media all and (min-width: 481px) and (max-width: 768px) {
262 #toast-container {
263 > div {
264 padding: 15px;
265 width: 25em;
266 }
267 }
268 }
@@ -0,0 +1,435 b''
1 /*
2 * Toastr
3 * Copyright 2012-2015
4 * Authors: John Papa, Hans FjΓ€llemark, and Tim Ferrell.
5 * All Rights Reserved.
6 * Use, reproduction, distribution, and modification of this code is subject to the terms and
7 * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
8 *
9 * ARIA Support: Greta Krafsig
10 *
11 * Project: https://github.com/CodeSeven/toastr
12 */
13 /* global define */
14 (function (define) {
15 define(['jquery'], function ($) {
16 return (function () {
17 var $container;
18 var listener;
19 var toastId = 0;
20 var toastType = {
21 error: 'error',
22 info: 'info',
23 success: 'success',
24 warning: 'warning'
25 };
26
27 var toastr = {
28 clear: clear,
29 remove: remove,
30 error: error,
31 getContainer: getContainer,
32 info: info,
33 options: {},
34 subscribe: subscribe,
35 success: success,
36 version: '2.1.2',
37 warning: warning
38 };
39
40 var previousToast;
41
42 return toastr;
43
44 ////////////////
45
46 function error(message, title, optionsOverride) {
47 return notify({
48 type: toastType.error,
49 iconClass: getOptions().iconClasses.error,
50 message: message,
51 optionsOverride: optionsOverride,
52 title: title
53 });
54 }
55
56 function getContainer(options, create) {
57 if (!options) { options = getOptions(); }
58 $container = $('#' + options.containerId);
59 if ($container.length) {
60 return $container;
61 }
62 if (create) {
63 $container = createContainer(options);
64 }
65 return $container;
66 }
67
68 function info(message, title, optionsOverride) {
69 return notify({
70 type: toastType.info,
71 iconClass: getOptions().iconClasses.info,
72 message: message,
73 optionsOverride: optionsOverride,
74 title: title
75 });
76 }
77
78 function subscribe(callback) {
79 listener = callback;
80 }
81
82 function success(message, title, optionsOverride) {
83 return notify({
84 type: toastType.success,
85 iconClass: getOptions().iconClasses.success,
86 message: message,
87 optionsOverride: optionsOverride,
88 title: title
89 });
90 }
91
92 function warning(message, title, optionsOverride) {
93 return notify({
94 type: toastType.warning,
95 iconClass: getOptions().iconClasses.warning,
96 message: message,
97 optionsOverride: optionsOverride,
98 title: title
99 });
100 }
101
102 function clear($toastElement, clearOptions) {
103 var options = getOptions();
104 if (!$container) { getContainer(options); }
105 if (!clearToast($toastElement, options, clearOptions)) {
106 clearContainer(options);
107 }
108 }
109
110 function remove($toastElement) {
111 var options = getOptions();
112 if (!$container) { getContainer(options); }
113 if ($toastElement && $(':focus', $toastElement).length === 0) {
114 removeToast($toastElement);
115 return;
116 }
117 if ($container.children().length) {
118 $container.remove();
119 }
120 }
121
122 // internal functions
123
124 function clearContainer (options) {
125 var toastsToClear = $container.children();
126 for (var i = toastsToClear.length - 1; i >= 0; i--) {
127 clearToast($(toastsToClear[i]), options);
128 }
129 }
130
131 function clearToast ($toastElement, options, clearOptions) {
132 var force = clearOptions && clearOptions.force ? clearOptions.force : false;
133 if ($toastElement && (force || $(':focus', $toastElement).length === 0)) {
134 $toastElement[options.hideMethod]({
135 duration: options.hideDuration,
136 easing: options.hideEasing,
137 complete: function () { removeToast($toastElement); }
138 });
139 return true;
140 }
141 return false;
142 }
143
144 function createContainer(options) {
145 $container = $('<div/>')
146 .attr('id', options.containerId)
147 .addClass(options.positionClass)
148 .attr('aria-live', 'polite')
149 .attr('role', 'alert');
150
151 $container.appendTo($(options.target));
152 return $container;
153 }
154
155 function getDefaults() {
156 return {
157 tapToDismiss: true,
158 toastClass: 'toast',
159 containerId: 'toast-container',
160 debug: false,
161
162 showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery
163 showDuration: 300,
164 showEasing: 'swing', //swing and linear are built into jQuery
165 onShown: undefined,
166 hideMethod: 'fadeOut',
167 hideDuration: 1000,
168 hideEasing: 'swing',
169 onHidden: undefined,
170 closeMethod: false,
171 closeDuration: false,
172 closeEasing: false,
173
174 extendedTimeOut: 1000,
175 iconClasses: {
176 error: 'toast-error',
177 info: 'toast-info',
178 success: 'toast-success',
179 warning: 'toast-warning'
180 },
181 iconClass: 'toast-info',
182 positionClass: 'toast-top-right',
183 timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky
184 titleClass: 'toast-title',
185 messageClass: 'toast-message',
186 escapeHtml: false,
187 target: 'body',
188 closeHtml: '<button type="button">&times;</button>',
189 newestOnTop: true,
190 preventDuplicates: false,
191 progressBar: false
192 };
193 }
194
195 function publish(args) {
196 if (!listener) { return; }
197 listener(args);
198 }
199
200 function notify(map) {
201 var options = getOptions();
202 var iconClass = map.iconClass || options.iconClass;
203
204 if (typeof (map.optionsOverride) !== 'undefined') {
205 options = $.extend(options, map.optionsOverride);
206 iconClass = map.optionsOverride.iconClass || iconClass;
207 }
208
209 if (shouldExit(options, map)) { return; }
210
211 toastId++;
212
213 $container = getContainer(options, true);
214
215 var intervalId = null;
216 var $toastElement = $('<div/>');
217 var $titleElement = $('<div/>');
218 var $messageElement = $('<div/>');
219 var $progressElement = $('<div/>');
220 var $closeElement = $(options.closeHtml);
221 var progressBar = {
222 intervalId: null,
223 hideEta: null,
224 maxHideTime: null
225 };
226 var response = {
227 toastId: toastId,
228 state: 'visible',
229 startTime: new Date(),
230 options: options,
231 map: map
232 };
233
234 personalizeToast();
235
236 displayToast();
237
238 handleEvents();
239
240 publish(response);
241
242 if (options.debug && console) {
243 console.log(response);
244 }
245
246 return $toastElement;
247
248 function escapeHtml(source) {
249 if (source == null)
250 source = "";
251
252 return new String(source)
253 .replace(/&/g, '&amp;')
254 .replace(/"/g, '&quot;')
255 .replace(/'/g, '&#39;')
256 .replace(/</g, '&lt;')
257 .replace(/>/g, '&gt;');
258 }
259
260 function personalizeToast() {
261 setIcon();
262 setTitle();
263 setMessage();
264 setCloseButton();
265 setProgressBar();
266 setSequence();
267 }
268
269 function handleEvents() {
270 $toastElement.hover(stickAround, delayedHideToast);
271 if (!options.onclick && options.tapToDismiss) {
272 $toastElement.click(hideToast);
273 }
274
275 if (options.closeButton && $closeElement) {
276 $closeElement.click(function (event) {
277 if (event.stopPropagation) {
278 event.stopPropagation();
279 } else if (event.cancelBubble !== undefined && event.cancelBubble !== true) {
280 event.cancelBubble = true;
281 }
282 hideToast(true);
283 });
284 }
285
286 if (options.onclick) {
287 $toastElement.click(function (event) {
288 options.onclick(event);
289 hideToast();
290 });
291 }
292 }
293
294 function displayToast() {
295 $toastElement.hide();
296
297 $toastElement[options.showMethod](
298 {duration: options.showDuration, easing: options.showEasing, complete: options.onShown}
299 );
300
301 if (options.timeOut > 0) {
302 intervalId = setTimeout(hideToast, options.timeOut);
303 progressBar.maxHideTime = parseFloat(options.timeOut);
304 progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime;
305 if (options.progressBar) {
306 progressBar.intervalId = setInterval(updateProgress, 10);
307 }
308 }
309 }
310
311 function setIcon() {
312 if (map.iconClass) {
313 $toastElement.addClass(options.toastClass).addClass(iconClass);
314 }
315 }
316
317 function setSequence() {
318 if (options.newestOnTop) {
319 $container.prepend($toastElement);
320 } else {
321 $container.append($toastElement);
322 }
323 }
324
325 function setTitle() {
326 if (map.title) {
327 $titleElement.append(!options.escapeHtml ? map.title : escapeHtml(map.title)).addClass(options.titleClass);
328 $toastElement.append($titleElement);
329 }
330 }
331
332 function setMessage() {
333 if (map.message) {
334 $messageElement.append(!options.escapeHtml ? map.message : escapeHtml(map.message)).addClass(options.messageClass);
335 $toastElement.append($messageElement);
336 }
337 }
338
339 function setCloseButton() {
340 if (options.closeButton) {
341 $closeElement.addClass('toast-close-button').attr('role', 'button');
342 $toastElement.prepend($closeElement);
343 }
344 }
345
346 function setProgressBar() {
347 if (options.progressBar) {
348 $progressElement.addClass('toast-progress');
349 $toastElement.prepend($progressElement);
350 }
351 }
352
353 function shouldExit(options, map) {
354 if (options.preventDuplicates) {
355 if (map.message === previousToast) {
356 return true;
357 } else {
358 previousToast = map.message;
359 }
360 }
361 return false;
362 }
363
364 function hideToast(override) {
365 var method = override && options.closeMethod !== false ? options.closeMethod : options.hideMethod;
366 var duration = override && options.closeDuration !== false ?
367 options.closeDuration : options.hideDuration;
368 var easing = override && options.closeEasing !== false ? options.closeEasing : options.hideEasing;
369 if ($(':focus', $toastElement).length && !override) {
370 return;
371 }
372 clearTimeout(progressBar.intervalId);
373 return $toastElement[method]({
374 duration: duration,
375 easing: easing,
376 complete: function () {
377 removeToast($toastElement);
378 if (options.onHidden && response.state !== 'hidden') {
379 options.onHidden();
380 }
381 response.state = 'hidden';
382 response.endTime = new Date();
383 publish(response);
384 }
385 });
386 }
387
388 function delayedHideToast() {
389 if (options.timeOut > 0 || options.extendedTimeOut > 0) {
390 intervalId = setTimeout(hideToast, options.extendedTimeOut);
391 progressBar.maxHideTime = parseFloat(options.extendedTimeOut);
392 progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime;
393 }
394 }
395
396 function stickAround() {
397 clearTimeout(intervalId);
398 progressBar.hideEta = 0;
399 $toastElement.stop(true, true)[options.showMethod](
400 {duration: options.showDuration, easing: options.showEasing}
401 );
402 }
403
404 function updateProgress() {
405 var percentage = ((progressBar.hideEta - (new Date().getTime())) / progressBar.maxHideTime) * 100;
406 $progressElement.width(percentage + '%');
407 }
408 }
409
410 function getOptions() {
411 return $.extend({}, getDefaults(), toastr.options);
412 }
413
414 function removeToast($toastElement) {
415 if (!$container) { $container = getContainer(); }
416 if ($toastElement.is(':visible')) {
417 return;
418 }
419 $toastElement.remove();
420 $toastElement = null;
421 if ($container.children().length === 0) {
422 $container.remove();
423 previousToast = undefined;
424 }
425 }
426
427 })();
428 });
429 }(typeof define === 'function' && define.amd ? define : function (deps, factory) {
430 if (typeof module !== 'undefined' && module.exports) { //Node
431 module.exports = factory(require('jquery'));
432 } else {
433 window.toastr = factory(window.jQuery);
434 }
435 }));
@@ -0,0 +1,219 b''
1 "use strict";
2 /** leak object to top level scope **/
3 var ccLog = undefined;
4 // global code-mirror logger;, to enable run
5 // Logger.get('ConnectionController').setLevel(Logger.DEBUG)
6 ccLog = Logger.get('ConnectionController');
7 ccLog.setLevel(Logger.OFF);
8
9 var ConnectionController;
10 var connCtrlr;
11 var registerViewChannels;
12
13 (function () {
14 ConnectionController = function (webappUrl, serverUrl, urls) {
15 var self = this;
16
17 var channels = ['broadcast'];
18 this.state = {
19 open: false,
20 webappUrl: webappUrl,
21 serverUrl: serverUrl,
22 connId: null,
23 socket: null,
24 channels: channels,
25 heartbeat: null,
26 channelsInfo: {},
27 urls: urls
28 };
29 this.channelNameParsers = [];
30
31 this.addChannelNameParser = function (fn) {
32 if (this.channelNameParsers.indexOf(fn) === -1) {
33 this.channelNameParsers.push(fn);
34 }
35 };
36
37 this.listen = function () {
38 if (window.WebSocket) {
39 ccLog.debug('attempting to create socket');
40 var socket_url = self.state.serverUrl + "/ws?conn_id=" + self.state.connId;
41 var socket_conf = {
42 url: socket_url,
43 handleAs: 'json',
44 headers: {
45 "Accept": "application/json",
46 "Content-Type": "application/json"
47 }
48 };
49 self.state.socket = new WebSocket(socket_conf.url);
50
51 self.state.socket.onopen = function (event) {
52 ccLog.debug('open event', event);
53 if (self.state.heartbeat === null) {
54 self.state.heartbeat = setInterval(function () {
55 if (self.state.socket.readyState === WebSocket.OPEN) {
56 self.state.socket.send('heartbeat');
57 }
58 }, 10000)
59 }
60 };
61 self.state.socket.onmessage = function (event) {
62 var data = $.parseJSON(event.data);
63 for (var i = 0; i < data.length; i++) {
64 if (data[i].message.topic) {
65 ccLog.debug('publishing',
66 data[i].message.topic, data[i]);
67 $.Topic(data[i].message.topic).publish(data[i])
68 }
69 else {
70 cclog.warning('unhandled message', data);
71 }
72 }
73 };
74 self.state.socket.onclose = function (event) {
75 ccLog.debug('closed event', event);
76 setTimeout(function () {
77 self.connect(true);
78 }, 5000);
79 };
80
81 self.state.socket.onerror = function (event) {
82 ccLog.debug('error event', event);
83 };
84 }
85 else {
86 ccLog.debug('attempting to create long polling connection');
87 var poolUrl = self.state.serverUrl + "/listen?conn_id=" + self.state.connId;
88 self.state.socket = $.ajax({
89 url: poolUrl
90 }).done(function (data) {
91 ccLog.debug('data', data);
92 var data = $.parseJSON(data);
93 for (var i = 0; i < data.length; i++) {
94 if (data[i].message.topic) {
95 ccLog.info('publishing',
96 data[i].message.topic, data[i]);
97 $.Topic(data[i].message.topic).publish(data[i])
98 }
99 else {
100 cclog.warning('unhandled message', data);
101 }
102 }
103 self.listen();
104 }).fail(function () {
105 ccLog.debug('longpoll error');
106 setTimeout(function () {
107 self.connect(true);
108 }, 5000);
109 });
110 }
111
112 };
113
114 this.connect = function (create_new_socket) {
115 var connReq = {'channels': self.state.channels};
116 ccLog.debug('try obtaining connection info', connReq);
117 $.ajax({
118 url: self.state.urls.connect,
119 type: "POST",
120 contentType: "application/json",
121 data: JSON.stringify(connReq),
122 dataType: "json"
123 }).done(function (data) {
124 ccLog.debug('Got connection:', data.conn_id);
125 self.state.channels = data.channels;
126 self.state.channelsInfo = data.channels_info;
127 self.state.connId = data.conn_id;
128 if (create_new_socket) {
129 self.listen();
130 }
131 self.update();
132 }).fail(function () {
133 setTimeout(function () {
134 self.connect(create_new_socket);
135 }, 5000);
136 });
137 self.update();
138 };
139
140 this.subscribeToChannels = function (channels) {
141 var new_channels = [];
142 for (var i = 0; i < channels.length; i++) {
143 var channel = channels[i];
144 if (self.state.channels.indexOf(channel)) {
145 self.state.channels.push(channel);
146 new_channels.push(channel)
147 }
148 }
149 /**
150 * only execute the request if socket is present because subscribe
151 * can actually add channels before initial app connection
152 **/
153 if (new_channels && self.state.socket !== null) {
154 var connReq = {
155 'channels': self.state.channels,
156 'conn_id': self.state.connId
157 };
158 $.ajax({
159 url: self.state.urls.subscribe,
160 type: "POST",
161 contentType: "application/json",
162 data: JSON.stringify(connReq),
163 dataType: "json"
164 }).done(function (data) {
165 self.state.channels = data.channels;
166 self.state.channelsInfo = data.channels_info;
167 self.update();
168 });
169 }
170 self.update();
171 };
172
173 this.update = function () {
174 for (var key in this.state.channelsInfo) {
175 if (this.state.channelsInfo.hasOwnProperty(key)) {
176 // update channels with latest info
177 $.Topic('/connection_controller/channel_update').publish(
178 {channel: key, state: this.state.channelsInfo[key]});
179 }
180 }
181 /**
182 * checks current channel list in state and if channel is not present
183 * converts them into executable "commands" and pushes them on topics
184 */
185 for (var i = 0; i < this.state.channels.length; i++) {
186 var channel = this.state.channels[i];
187 for (var j = 0; j < this.channelNameParsers.length; j++) {
188 this.channelNameParsers[j](channel);
189 }
190 }
191 };
192
193 this.run = function () {
194 this.connect(true);
195 };
196
197 $.Topic('/connection_controller/subscribe').subscribe(
198 self.subscribeToChannels);
199 };
200
201 $.Topic('/plugins/__REGISTER__').subscribe(function (data) {
202 // enable chat controller
203 if (window.CHANNELSTREAM_SETTINGS && window.CHANNELSTREAM_SETTINGS.enabled) {
204 $(document).ready(function () {
205 connCtrlr.run();
206 });
207 }
208 });
209
210 registerViewChannels = function (){
211 // subscribe to PR repo channel for PR's'
212 if (templateContext.pull_request_data.pull_request_id) {
213 var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
214 String(templateContext.pull_request_data.pull_request_id);
215 connCtrlr.state.channels.push(channelName);
216 }
217 }
218
219 })();
@@ -0,0 +1,60 b''
1 "use strict";
2
3 toastr.options = {
4 "closeButton": true,
5 "debug": false,
6 "newestOnTop": false,
7 "progressBar": false,
8 "positionClass": "toast-top-center",
9 "preventDuplicates": false,
10 "onclick": null,
11 "showDuration": "300",
12 "hideDuration": "300",
13 "timeOut": "0",
14 "extendedTimeOut": "0",
15 "showEasing": "swing",
16 "hideEasing": "linear",
17 "showMethod": "fadeIn",
18 "hideMethod": "fadeOut"
19 };
20
21 function notifySystem(data) {
22 var notification = new Notification(data.message.level + ': ' + data.message.message);
23 };
24
25 function notifyToaster(data){
26 toastr[data.message.level](data.message.message);
27 }
28
29 function handleNotifications(data) {
30
31 if (!templateContext.rhodecode_user.notification_status && !data.testMessage) {
32 // do not act if notifications are disabled
33 return
34 }
35 // use only js notifications for now
36 var onlyJS = true;
37 if (!("Notification" in window) || onlyJS) {
38 // use legacy notificartion
39 notifyToaster(data);
40 }
41 else {
42 // Let's check whether notification permissions have already been granted
43 if (Notification.permission === "granted") {
44 notifySystem(data);
45 }
46 // Otherwise, we need to ask the user for permission
47 else if (Notification.permission !== 'denied') {
48 Notification.requestPermission(function (permission) {
49 if (permission === "granted") {
50 notifySystem(data);
51 }
52 });
53 }
54 else{
55 notifyToaster(data);
56 }
57 }
58 };
59
60 $.Topic('/notifications').subscribe(handleNotifications);
@@ -0,0 +1,24 b''
1 <script>
2 var CHANNELSTREAM_URLS = ${config['url_gen'](request)|n};
3 %if request.registry.rhodecode_plugins['channelstream']['enabled'] and c.rhodecode_user.username != h.DEFAULT_USER:
4 var CHANNELSTREAM_SETTINGS = {
5 'enabled': true,
6 'ws_location': '${request.registry.settings.get('channelstream.ws_url')}',
7 'webapp_location': '${h.url('/', qualified=True)[:-1]}'
8 };
9 %else:
10 var CHANNELSTREAM_SETTINGS = {
11 'enabled':false,
12 'ws_location': '',
13 'webapp_location': ''};
14 %endif
15
16 if (CHANNELSTREAM_SETTINGS.enabled) {
17 connCtrlr = new ConnectionController(
18 CHANNELSTREAM_SETTINGS.webapp_location,
19 CHANNELSTREAM_SETTINGS.ws_location,
20 CHANNELSTREAM_URLS
21 );
22 registerViewChannels();
23 }
24 </script>
@@ -1,141 +1,144 b''
1 1 module.exports = function(grunt) {
2 2 grunt.initConfig({
3 3
4 4 dirs: {
5 5 css: "rhodecode/public/css",
6 6 js: {
7 7 "src": "rhodecode/public/js/src",
8 8 "dest": "rhodecode/public/js"
9 9 }
10 10 },
11 11
12 12 concat: {
13 13 dist: {
14 14 src: [
15 15 // Base libraries
16 16 '<%= dirs.js.src %>/jquery-1.11.1.min.js',
17 17 '<%= dirs.js.src %>/logging.js',
18 18 '<%= dirs.js.src %>/bootstrap.js',
19 19 '<%= dirs.js.src %>/mousetrap.js',
20 20 '<%= dirs.js.src %>/moment.js',
21 21 '<%= dirs.js.src %>/appenlight-client-0.4.1.min.js',
22 22 '<%= dirs.js.src %>/i18n_utils.js',
23 23 '<%= dirs.js.src %>/deform.js',
24 24
25 25 // Plugins
26 26 '<%= dirs.js.src %>/plugins/jquery.pjax.js',
27 27 '<%= dirs.js.src %>/plugins/jquery.dataTables.js',
28 28 '<%= dirs.js.src %>/plugins/flavoured_checkbox.js',
29 29 '<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js',
30 30 '<%= dirs.js.src %>/plugins/jquery.autocomplete.js',
31 31 '<%= dirs.js.src %>/plugins/jquery.debounce.js',
32 32 '<%= dirs.js.src %>/plugins/jquery.mark.js',
33 33 '<%= dirs.js.src %>/plugins/jquery.timeago.js',
34 34 '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js',
35 '<%= dirs.js.src %>/plugins/toastr.js',
35 36
36 37 // Select2
37 38 '<%= dirs.js.src %>/select2/select2.js',
38 39
39 40 // Code-mirror
40 41 '<%= dirs.js.src %>/codemirror/codemirror.js',
41 42 '<%= dirs.js.src %>/codemirror/codemirror_loadmode.js',
42 43 '<%= dirs.js.src %>/codemirror/codemirror_hint.js',
43 44 '<%= dirs.js.src %>/codemirror/codemirror_overlay.js',
44 45 '<%= dirs.js.src %>/codemirror/codemirror_placeholder.js',
45 46 // TODO: mikhail: this is an exception. Since the code mirror modes
46 47 // are loaded "on the fly", we need to keep them in a public folder
47 48 '<%= dirs.js.dest %>/mode/meta.js',
48 49 '<%= dirs.js.dest %>/mode/meta_ext.js',
49 50 '<%= dirs.js.dest %>/rhodecode/i18n/select2/translations.js',
50 51
51 52 // Rhodecode utilities
52 53 '<%= dirs.js.src %>/rhodecode/utils/array.js',
53 54 '<%= dirs.js.src %>/rhodecode/utils/string.js',
54 55 '<%= dirs.js.src %>/rhodecode/utils/pyroutes.js',
55 56 '<%= dirs.js.src %>/rhodecode/utils/ajax.js',
56 57 '<%= dirs.js.src %>/rhodecode/utils/autocomplete.js',
57 58 '<%= dirs.js.src %>/rhodecode/utils/colorgenerator.js',
58 59 '<%= dirs.js.src %>/rhodecode/utils/ie.js',
59 60 '<%= dirs.js.src %>/rhodecode/utils/os.js',
60 61 '<%= dirs.js.src %>/rhodecode/utils/topics.js',
61 62
62 63 // Rhodecode widgets
63 64 '<%= dirs.js.src %>/rhodecode/widgets/multiselect.js',
64 65
65 66 // Rhodecode components
66 67 '<%= dirs.js.src %>/rhodecode/init.js',
68 '<%= dirs.js.src %>/rhodecode/connection_controller.js',
67 69 '<%= dirs.js.src %>/rhodecode/codemirror.js',
68 70 '<%= dirs.js.src %>/rhodecode/comments.js',
69 71 '<%= dirs.js.src %>/rhodecode/constants.js',
70 72 '<%= dirs.js.src %>/rhodecode/files.js',
71 73 '<%= dirs.js.src %>/rhodecode/followers.js',
72 74 '<%= dirs.js.src %>/rhodecode/menus.js',
73 75 '<%= dirs.js.src %>/rhodecode/notifications.js',
74 76 '<%= dirs.js.src %>/rhodecode/permissions.js',
75 77 '<%= dirs.js.src %>/rhodecode/pjax.js',
76 78 '<%= dirs.js.src %>/rhodecode/pullrequests.js',
77 79 '<%= dirs.js.src %>/rhodecode/settings.js',
78 80 '<%= dirs.js.src %>/rhodecode/select2_widgets.js',
79 81 '<%= dirs.js.src %>/rhodecode/tooltips.js',
80 82 '<%= dirs.js.src %>/rhodecode/users.js',
83 '<%= dirs.js.src %>/rhodecode/utils/notifications.js',
81 84 '<%= dirs.js.src %>/rhodecode/appenlight.js',
82 85
83 86 // Rhodecode main module
84 87 '<%= dirs.js.src %>/rhodecode.js'
85 88 ],
86 89 dest: '<%= dirs.js.dest %>/scripts.js',
87 90 nonull: true
88 91 }
89 92 },
90 93
91 94 less: {
92 95 development: {
93 96 options: {
94 97 compress: false,
95 98 yuicompress: false,
96 99 optimization: 0
97 100 },
98 101 files: {
99 102 "<%= dirs.css %>/style.css": "<%= dirs.css %>/main.less"
100 103 }
101 104 },
102 105 production: {
103 106 options: {
104 107 compress: true,
105 108 yuicompress: true,
106 109 optimization: 2
107 110 },
108 111 files: {
109 112 "<%= dirs.css %>/style.css": "<%= dirs.css %>/main.less"
110 113 }
111 114 }
112 115 },
113 116
114 117 watch: {
115 118 less: {
116 119 files: ["<%= dirs.css %>/*.less"],
117 120 tasks: ["less:production"]
118 121 },
119 122 js: {
120 123 files: ["<%= dirs.js.src %>/**/*.js"],
121 124 tasks: ["concat:dist"]
122 125 }
123 126 },
124 127
125 128 jshint: {
126 129 rhodecode: {
127 130 src: '<%= dirs.js.src %>/rhodecode/**/*.js',
128 131 options: {
129 132 jshintrc: '.jshintrc'
130 133 }
131 134 }
132 135 }
133 136 });
134 137
135 138 grunt.loadNpmTasks('grunt-contrib-less');
136 139 grunt.loadNpmTasks('grunt-contrib-concat');
137 140 grunt.loadNpmTasks('grunt-contrib-watch');
138 141 grunt.loadNpmTasks('grunt-contrib-jshint');
139 142
140 143 grunt.registerTask('default', ['less:production', 'concat:dist']);
141 144 };
@@ -1,612 +1,623 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode Enterprise - configuration file #
4 4 # Built-in functions and variables #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 # #
7 7 ################################################################################
8 8
9 9 [DEFAULT]
10 10 debug = true
11 11 ################################################################################
12 12 ## Uncomment and replace with the email address which should receive ##
13 13 ## any error reports after an application crash ##
14 14 ## Additionally these settings will be used by the RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 20 #email_prefix = [RhodeCode]
21 21
22 22 #smtp_server = mail.server.com
23 23 #smtp_username =
24 24 #smtp_password =
25 25 #smtp_port =
26 26 #smtp_use_tls = false
27 27 #smtp_use_ssl = true
28 28 ## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
29 29 #smtp_auth =
30 30
31 31 [server:main]
32 32 ## COMMON ##
33 33 host = 127.0.0.1
34 34 port = 5000
35 35
36 36 ##################################
37 37 ## WAITRESS WSGI SERVER ##
38 38 ## Recommended for Development ##
39 39 ##################################
40 40 use = egg:waitress#main
41 41 ## number of worker threads
42 42 threads = 5
43 43 ## MAX BODY SIZE 100GB
44 44 max_request_body_size = 107374182400
45 45 ## Use poll instead of select, fixes file descriptors limits problems.
46 46 ## May not work on old windows systems.
47 47 asyncore_use_poll = true
48 48
49 49
50 50 ##########################
51 51 ## GUNICORN WSGI SERVER ##
52 52 ##########################
53 53 ## run with gunicorn --log-config <inifile.ini> --paste <inifile.ini>
54 54 #use = egg:gunicorn#main
55 55 ## Sets the number of process workers. You must set `instance_id = *`
56 56 ## when this option is set to more than one worker, recommended
57 57 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
58 58 ## The `instance_id = *` must be set in the [app:main] section below
59 59 #workers = 2
60 60 ## number of threads for each of the worker, must be set to 1 for gevent
61 61 ## generally recommened to be at 1
62 62 #threads = 1
63 63 ## process name
64 64 #proc_name = rhodecode
65 65 ## type of worker class, one of sync, gevent
66 66 ## recommended for bigger setup is using of of other than sync one
67 67 #worker_class = sync
68 68 ## The maximum number of simultaneous clients. Valid only for Gevent
69 69 #worker_connections = 10
70 70 ## max number of requests that worker will handle before being gracefully
71 71 ## restarted, could prevent memory leaks
72 72 #max_requests = 1000
73 73 #max_requests_jitter = 30
74 74 ## amount of time a worker can spend with handling a request before it
75 75 ## gets killed and restarted. Set to 6hrs
76 76 #timeout = 21600
77 77
78 78
79 79 ## prefix middleware for RhodeCode, disables force_https flag.
80 80 ## allows to set RhodeCode under a prefix in server.
81 81 ## eg https://server.com/<prefix>. Enable `filter-with =` option below as well.
82 82 #[filter:proxy-prefix]
83 83 #use = egg:PasteDeploy#prefix
84 84 #prefix = /<your-prefix>
85 85
86 86 [app:main]
87 87 use = egg:rhodecode-enterprise-ce
88 88 ## enable proxy prefix middleware, defined below
89 89 #filter-with = proxy-prefix
90 90
91 91 # During development the we want to have the debug toolbar enabled
92 92 pyramid.includes =
93 93 pyramid_debugtoolbar
94 94 rhodecode.utils.debugtoolbar
95 95 rhodecode.lib.middleware.request_wrapper
96 96
97 97 pyramid.reload_templates = true
98 98
99 99 debugtoolbar.hosts = 0.0.0.0/0
100 100 debugtoolbar.exclude_prefixes =
101 101 /css
102 102 /fonts
103 103 /images
104 104 /js
105 105
106 106 ## RHODECODE PLUGINS ##
107 107 rhodecode.includes =
108 108 rhodecode.api
109 109
110 110
111 111 # api prefix url
112 112 rhodecode.api.url = /_admin/api
113 113
114 114
115 115 ## END RHODECODE PLUGINS ##
116 116
117 117 ## encryption key used to encrypt social plugin tokens,
118 118 ## remote_urls with credentials etc, if not set it defaults to
119 119 ## `beaker.session.secret`
120 120 #rhodecode.encrypted_values.secret =
121 121
122 122 ## decryption strict mode (enabled by default). It controls if decryption raises
123 123 ## `SignatureVerificationError` in case of wrong key, or damaged encryption data.
124 124 #rhodecode.encrypted_values.strict = false
125 125
126 126 full_stack = true
127 127
128 128 ## return gzipped responses from Rhodecode (static files/application)
129 129 gzip_responses = true
130 130
131 131 # autogenerate javascript routes file on startup
132 132 generate_js_files = false
133 133
134 134 ## Optional Languages
135 135 ## en(default), be, de, es, fr, it, ja, pl, pt, ru, zh
136 136 lang = en
137 137
138 138 ## perform a full repository scan on each server start, this should be
139 139 ## set to false after first startup, to allow faster server restarts.
140 140 startup.import_repos = false
141 141
142 142 ## Uncomment and set this path to use archive download cache.
143 143 ## Once enabled, generated archives will be cached at this location
144 144 ## and served from the cache during subsequent requests for the same archive of
145 145 ## the repository.
146 146 #archive_cache_dir = /tmp/tarballcache
147 147
148 148 ## change this to unique ID for security
149 149 app_instance_uuid = rc-production
150 150
151 151 ## cut off limit for large diffs (size in bytes)
152 152 cut_off_limit_diff = 1024000
153 153 cut_off_limit_file = 256000
154 154
155 155 ## use cache version of scm repo everywhere
156 156 vcs_full_cache = true
157 157
158 158 ## force https in RhodeCode, fixes https redirects, assumes it's always https
159 159 ## Normally this is controlled by proper http flags sent from http server
160 160 force_https = false
161 161
162 162 ## use Strict-Transport-Security headers
163 163 use_htsts = false
164 164
165 165 ## number of commits stats will parse on each iteration
166 166 commit_parse_limit = 25
167 167
168 168 ## git rev filter option, --all is the default filter, if you need to
169 169 ## hide all refs in changelog switch this to --branches --tags
170 170 git_rev_filter = --branches --tags
171 171
172 172 # Set to true if your repos are exposed using the dumb protocol
173 173 git_update_server_info = false
174 174
175 175 ## RSS/ATOM feed options
176 176 rss_cut_off_limit = 256000
177 177 rss_items_per_page = 10
178 178 rss_include_diff = false
179 179
180 180 ## gist URL alias, used to create nicer urls for gist. This should be an
181 181 ## url that does rewrites to _admin/gists/<gistid>.
182 182 ## example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
183 183 ## RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/<gistid>
184 184 gist_alias_url =
185 185
186 186 ## List of controllers (using glob pattern syntax) that AUTH TOKENS could be
187 187 ## used for access.
188 188 ## Adding ?auth_token = <token> to the url authenticates this request as if it
189 189 ## came from the the logged in user who own this authentication token.
190 190 ##
191 191 ## Syntax is <ControllerClass>:<function_pattern>.
192 192 ## To enable access to raw_files put `FilesController:raw`.
193 193 ## To enable access to patches add `ChangesetController:changeset_patch`.
194 194 ## The list should be "," separated and on a single line.
195 195 ##
196 196 ## Recommended controllers to enable:
197 197 # ChangesetController:changeset_patch,
198 198 # ChangesetController:changeset_raw,
199 199 # FilesController:raw,
200 200 # FilesController:archivefile,
201 201 # GistsController:*,
202 202 api_access_controllers_whitelist =
203 203
204 204 ## default encoding used to convert from and to unicode
205 205 ## can be also a comma separated list of encoding in case of mixed encodings
206 206 default_encoding = UTF-8
207 207
208 208 ## instance-id prefix
209 209 ## a prefix key for this instance used for cache invalidation when running
210 210 ## multiple instances of rhodecode, make sure it's globally unique for
211 211 ## all running rhodecode instances. Leave empty if you don't use it
212 212 instance_id =
213 213
214 214 ## Fallback authentication plugin. Set this to a plugin ID to force the usage
215 215 ## of an authentication plugin also if it is disabled by it's settings.
216 216 ## This could be useful if you are unable to log in to the system due to broken
217 217 ## authentication settings. Then you can enable e.g. the internal rhodecode auth
218 218 ## module to log in again and fix the settings.
219 219 ##
220 220 ## Available builtin plugin IDs (hash is part of the ID):
221 221 ## egg:rhodecode-enterprise-ce#rhodecode
222 222 ## egg:rhodecode-enterprise-ce#pam
223 223 ## egg:rhodecode-enterprise-ce#ldap
224 224 ## egg:rhodecode-enterprise-ce#jasig_cas
225 225 ## egg:rhodecode-enterprise-ce#headers
226 226 ## egg:rhodecode-enterprise-ce#crowd
227 227 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
228 228
229 229 ## alternative return HTTP header for failed authentication. Default HTTP
230 230 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
231 231 ## handling that causing a series of failed authentication calls.
232 232 ## Set this variable to 403 to return HTTPForbidden, or any other HTTP code
233 233 ## This will be served instead of default 401 on bad authnetication
234 234 auth_ret_code =
235 235
236 236 ## use special detection method when serving auth_ret_code, instead of serving
237 237 ## ret_code directly, use 401 initially (Which triggers credentials prompt)
238 238 ## and then serve auth_ret_code to clients
239 239 auth_ret_code_detection = false
240 240
241 241 ## locking return code. When repository is locked return this HTTP code. 2XX
242 242 ## codes don't break the transactions while 4XX codes do
243 243 lock_ret_code = 423
244 244
245 245 ## allows to change the repository location in settings page
246 246 allow_repo_location_change = true
247 247
248 248 ## allows to setup custom hooks in settings page
249 249 allow_custom_hooks_settings = true
250 250
251 251 ## generated license token, goto license page in RhodeCode settings to obtain
252 252 ## new token
253 253 license_token =
254 254
255 255 ## supervisor connection uri, for managing supervisor and logs.
256 256 supervisor.uri =
257 257 ## supervisord group name/id we only want this RC instance to handle
258 258 supervisor.group_id = dev
259 259
260 260 ## Display extended labs settings
261 261 labs_settings_active = true
262 262
263 263 ####################################
264 264 ### CELERY CONFIG ####
265 265 ####################################
266 266 use_celery = false
267 267 broker.host = localhost
268 268 broker.vhost = rabbitmqhost
269 269 broker.port = 5672
270 270 broker.user = rabbitmq
271 271 broker.password = qweqwe
272 272
273 273 celery.imports = rhodecode.lib.celerylib.tasks
274 274
275 275 celery.result.backend = amqp
276 276 celery.result.dburi = amqp://
277 277 celery.result.serialier = json
278 278
279 279 #celery.send.task.error.emails = true
280 280 #celery.amqp.task.result.expires = 18000
281 281
282 282 celeryd.concurrency = 2
283 283 #celeryd.log.file = celeryd.log
284 284 celeryd.log.level = debug
285 285 celeryd.max.tasks.per.child = 1
286 286
287 287 ## tasks will never be sent to the queue, but executed locally instead.
288 288 celery.always.eager = false
289 289
290 290 ####################################
291 291 ### BEAKER CACHE ####
292 292 ####################################
293 293 # default cache dir for templates. Putting this into a ramdisk
294 294 ## can boost performance, eg. %(here)s/data_ramdisk
295 295 cache_dir = %(here)s/data
296 296
297 297 ## locking and default file storage for Beaker. Putting this into a ramdisk
298 298 ## can boost performance, eg. %(here)s/data_ramdisk/cache/beaker_data
299 299 beaker.cache.data_dir = %(here)s/data/cache/beaker_data
300 300 beaker.cache.lock_dir = %(here)s/data/cache/beaker_lock
301 301
302 302 beaker.cache.regions = super_short_term, short_term, long_term, sql_cache_short, auth_plugins, repo_cache_long
303 303
304 304 beaker.cache.super_short_term.type = memory
305 305 beaker.cache.super_short_term.expire = 10
306 306 beaker.cache.super_short_term.key_length = 256
307 307
308 308 beaker.cache.short_term.type = memory
309 309 beaker.cache.short_term.expire = 60
310 310 beaker.cache.short_term.key_length = 256
311 311
312 312 beaker.cache.long_term.type = memory
313 313 beaker.cache.long_term.expire = 36000
314 314 beaker.cache.long_term.key_length = 256
315 315
316 316 beaker.cache.sql_cache_short.type = memory
317 317 beaker.cache.sql_cache_short.expire = 10
318 318 beaker.cache.sql_cache_short.key_length = 256
319 319
320 320 # default is memory cache, configure only if required
321 321 # using multi-node or multi-worker setup
322 322 #beaker.cache.auth_plugins.type = ext:database
323 323 #beaker.cache.auth_plugins.lock_dir = %(here)s/data/cache/auth_plugin_lock
324 324 #beaker.cache.auth_plugins.url = postgresql://postgres:secret@localhost/rhodecode
325 325 #beaker.cache.auth_plugins.url = mysql://root:secret@127.0.0.1/rhodecode
326 326 #beaker.cache.auth_plugins.sa.pool_recycle = 3600
327 327 #beaker.cache.auth_plugins.sa.pool_size = 10
328 328 #beaker.cache.auth_plugins.sa.max_overflow = 0
329 329
330 330 beaker.cache.repo_cache_long.type = memorylru_base
331 331 beaker.cache.repo_cache_long.max_items = 4096
332 332 beaker.cache.repo_cache_long.expire = 2592000
333 333
334 334 # default is memorylru_base cache, configure only if required
335 335 # using multi-node or multi-worker setup
336 336 #beaker.cache.repo_cache_long.type = ext:memcached
337 337 #beaker.cache.repo_cache_long.url = localhost:11211
338 338 #beaker.cache.repo_cache_long.expire = 1209600
339 339 #beaker.cache.repo_cache_long.key_length = 256
340 340
341 341 ####################################
342 342 ### BEAKER SESSION ####
343 343 ####################################
344 344
345 345 ## .session.type is type of storage options for the session, current allowed
346 346 ## types are file, ext:memcached, ext:database, and memory (default).
347 347 beaker.session.type = file
348 348 beaker.session.data_dir = %(here)s/data/sessions/data
349 349
350 350 ## db based session, fast, and allows easy management over logged in users ##
351 351 #beaker.session.type = ext:database
352 352 #beaker.session.table_name = db_session
353 353 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
354 354 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
355 355 #beaker.session.sa.pool_recycle = 3600
356 356 #beaker.session.sa.echo = false
357 357
358 358 beaker.session.key = rhodecode
359 359 beaker.session.secret = develop-rc-uytcxaz
360 360 beaker.session.lock_dir = %(here)s/data/sessions/lock
361 361
362 362 ## Secure encrypted cookie. Requires AES and AES python libraries
363 363 ## you must disable beaker.session.secret to use this
364 364 #beaker.session.encrypt_key = <key_for_encryption>
365 365 #beaker.session.validate_key = <validation_key>
366 366
367 367 ## sets session as invalid(also logging out user) if it haven not been
368 368 ## accessed for given amount of time in seconds
369 369 beaker.session.timeout = 2592000
370 370 beaker.session.httponly = true
371 371 #beaker.session.cookie_path = /<your-prefix>
372 372
373 373 ## uncomment for https secure cookie
374 374 beaker.session.secure = false
375 375
376 376 ## auto save the session to not to use .save()
377 377 beaker.session.auto = false
378 378
379 379 ## default cookie expiration time in seconds, set to `true` to set expire
380 380 ## at browser close
381 381 #beaker.session.cookie_expires = 3600
382 382
383 383 ###################################
384 384 ## SEARCH INDEXING CONFIGURATION ##
385 385 ###################################
386 386 ## Full text search indexer is available in rhodecode-tools under
387 387 ## `rhodecode-tools index` command
388 388
389 389 # WHOOSH Backend, doesn't require additional services to run
390 390 # it works good with few dozen repos
391 391 search.module = rhodecode.lib.index.whoosh
392 392 search.location = %(here)s/data/index
393 393
394 ########################################
395 ### CHANNELSTREAM CONFIG ####
396 ########################################
397
398 channelstream.enabled = true
399 # location of channelstream server on the backend
400 channelstream.server = 127.0.0.1:9800
401 # location of the channelstream server from outside world
402 channelstream.ws_url = ws://127.0.0.1:9800
403 channelstream.secret = secret
404
394 405 ###################################
395 406 ## APPENLIGHT CONFIG ##
396 407 ###################################
397 408
398 409 ## Appenlight is tailored to work with RhodeCode, see
399 410 ## http://appenlight.com for details how to obtain an account
400 411
401 412 ## appenlight integration enabled
402 413 appenlight = false
403 414
404 415 appenlight.server_url = https://api.appenlight.com
405 416 appenlight.api_key = YOUR_API_KEY
406 417 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
407 418
408 419 # used for JS client
409 420 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
410 421
411 422 ## TWEAK AMOUNT OF INFO SENT HERE
412 423
413 424 ## enables 404 error logging (default False)
414 425 appenlight.report_404 = false
415 426
416 427 ## time in seconds after request is considered being slow (default 1)
417 428 appenlight.slow_request_time = 1
418 429
419 430 ## record slow requests in application
420 431 ## (needs to be enabled for slow datastore recording and time tracking)
421 432 appenlight.slow_requests = true
422 433
423 434 ## enable hooking to application loggers
424 435 appenlight.logging = true
425 436
426 437 ## minimum log level for log capture
427 438 appenlight.logging.level = WARNING
428 439
429 440 ## send logs only from erroneous/slow requests
430 441 ## (saves API quota for intensive logging)
431 442 appenlight.logging_on_error = false
432 443
433 444 ## list of additonal keywords that should be grabbed from environ object
434 445 ## can be string with comma separated list of words in lowercase
435 446 ## (by default client will always send following info:
436 447 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
437 448 ## start with HTTP* this list be extended with additional keywords here
438 449 appenlight.environ_keys_whitelist =
439 450
440 451 ## list of keywords that should be blanked from request object
441 452 ## can be string with comma separated list of words in lowercase
442 453 ## (by default client will always blank keys that contain following words
443 454 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
444 455 ## this list be extended with additional keywords set here
445 456 appenlight.request_keys_blacklist =
446 457
447 458 ## list of namespaces that should be ignores when gathering log entries
448 459 ## can be string with comma separated list of namespaces
449 460 ## (by default the client ignores own entries: appenlight_client.client)
450 461 appenlight.log_namespace_blacklist =
451 462
452 463
453 464 ################################################################################
454 465 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
455 466 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
456 467 ## execute malicious code after an exception is raised. ##
457 468 ################################################################################
458 469 #set debug = false
459 470
460 471
461 472 ##############
462 473 ## STYLING ##
463 474 ##############
464 475 debug_style = true
465 476
466 477 #########################################################
467 478 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
468 479 #########################################################
469 480 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
470 481 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
471 482 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode
472 483
473 484 # see sqlalchemy docs for other advanced settings
474 485
475 486 ## print the sql statements to output
476 487 sqlalchemy.db1.echo = false
477 488 ## recycle the connections after this ammount of seconds
478 489 sqlalchemy.db1.pool_recycle = 3600
479 490 sqlalchemy.db1.convert_unicode = true
480 491
481 492 ## the number of connections to keep open inside the connection pool.
482 493 ## 0 indicates no limit
483 494 #sqlalchemy.db1.pool_size = 5
484 495
485 496 ## the number of connections to allow in connection pool "overflow", that is
486 497 ## connections that can be opened above and beyond the pool_size setting,
487 498 ## which defaults to five.
488 499 #sqlalchemy.db1.max_overflow = 10
489 500
490 501
491 502 ##################
492 503 ### VCS CONFIG ###
493 504 ##################
494 505 vcs.server.enable = true
495 506 vcs.server = localhost:9900
496 507
497 508 ## Web server connectivity protocol, responsible for web based VCS operatations
498 509 ## Available protocols are:
499 510 ## `pyro4` - using pyro4 server
500 511 ## `http` - using http-rpc backend
501 512 vcs.server.protocol = http
502 513
503 514 ## Push/Pull operations protocol, available options are:
504 515 ## `pyro4` - using pyro4 server
505 516 ## `rhodecode.lib.middleware.utils.scm_app_http` - Http based, recommended
506 517 ## `vcsserver.scm_app` - internal app (EE only)
507 518 vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http
508 519
509 520 ## Push/Pull operations hooks protocol, available options are:
510 521 ## `pyro4` - using pyro4 server
511 522 ## `http` - using http-rpc backend
512 523 vcs.hooks.protocol = http
513 524
514 525 vcs.server.log_level = debug
515 526 ## Start VCSServer with this instance as a subprocess, usefull for development
516 527 vcs.start_server = true
517 528 vcs.backends = hg, git, svn
518 529 vcs.connection_timeout = 3600
519 530 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
520 531 ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible
521 532 #vcs.svn.compatible_version = pre-1.8-compatible
522 533
523 534 ################################
524 535 ### LOGGING CONFIGURATION ####
525 536 ################################
526 537 [loggers]
527 538 keys = root, routes, rhodecode, sqlalchemy, beaker, pyro4, templates
528 539
529 540 [handlers]
530 541 keys = console, console_sql
531 542
532 543 [formatters]
533 544 keys = generic, color_formatter, color_formatter_sql
534 545
535 546 #############
536 547 ## LOGGERS ##
537 548 #############
538 549 [logger_root]
539 550 level = NOTSET
540 551 handlers = console
541 552
542 553 [logger_routes]
543 554 level = DEBUG
544 555 handlers =
545 556 qualname = routes.middleware
546 557 ## "level = DEBUG" logs the route matched and routing variables.
547 558 propagate = 1
548 559
549 560 [logger_beaker]
550 561 level = DEBUG
551 562 handlers =
552 563 qualname = beaker.container
553 564 propagate = 1
554 565
555 566 [logger_pyro4]
556 567 level = DEBUG
557 568 handlers =
558 569 qualname = Pyro4
559 570 propagate = 1
560 571
561 572 [logger_templates]
562 573 level = INFO
563 574 handlers =
564 575 qualname = pylons.templating
565 576 propagate = 1
566 577
567 578 [logger_rhodecode]
568 579 level = DEBUG
569 580 handlers =
570 581 qualname = rhodecode
571 582 propagate = 1
572 583
573 584 [logger_sqlalchemy]
574 585 level = INFO
575 586 handlers = console_sql
576 587 qualname = sqlalchemy.engine
577 588 propagate = 0
578 589
579 590 ##############
580 591 ## HANDLERS ##
581 592 ##############
582 593
583 594 [handler_console]
584 595 class = StreamHandler
585 596 args = (sys.stderr,)
586 597 level = DEBUG
587 598 formatter = color_formatter
588 599
589 600 [handler_console_sql]
590 601 class = StreamHandler
591 602 args = (sys.stderr,)
592 603 level = DEBUG
593 604 formatter = color_formatter_sql
594 605
595 606 ################
596 607 ## FORMATTERS ##
597 608 ################
598 609
599 610 [formatter_generic]
600 611 class = rhodecode.lib.logging_formatter.Pyro4AwareFormatter
601 612 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
602 613 datefmt = %Y-%m-%d %H:%M:%S
603 614
604 615 [formatter_color_formatter]
605 616 class = rhodecode.lib.logging_formatter.ColorFormatter
606 617 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
607 618 datefmt = %Y-%m-%d %H:%M:%S
608 619
609 620 [formatter_color_formatter_sql]
610 621 class = rhodecode.lib.logging_formatter.ColorFormatterSql
611 622 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
612 623 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,581 +1,592 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode Enterprise - configuration file #
4 4 # Built-in functions and variables #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 # #
7 7 ################################################################################
8 8
9 9 [DEFAULT]
10 10 debug = true
11 11 ################################################################################
12 12 ## Uncomment and replace with the email address which should receive ##
13 13 ## any error reports after an application crash ##
14 14 ## Additionally these settings will be used by the RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 20 #email_prefix = [RhodeCode]
21 21
22 22 #smtp_server = mail.server.com
23 23 #smtp_username =
24 24 #smtp_password =
25 25 #smtp_port =
26 26 #smtp_use_tls = false
27 27 #smtp_use_ssl = true
28 28 ## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
29 29 #smtp_auth =
30 30
31 31 [server:main]
32 32 ## COMMON ##
33 33 host = 127.0.0.1
34 34 port = 5000
35 35
36 36 ##################################
37 37 ## WAITRESS WSGI SERVER ##
38 38 ## Recommended for Development ##
39 39 ##################################
40 40 #use = egg:waitress#main
41 41 ## number of worker threads
42 42 #threads = 5
43 43 ## MAX BODY SIZE 100GB
44 44 #max_request_body_size = 107374182400
45 45 ## Use poll instead of select, fixes file descriptors limits problems.
46 46 ## May not work on old windows systems.
47 47 #asyncore_use_poll = true
48 48
49 49
50 50 ##########################
51 51 ## GUNICORN WSGI SERVER ##
52 52 ##########################
53 53 ## run with gunicorn --log-config <inifile.ini> --paste <inifile.ini>
54 54 use = egg:gunicorn#main
55 55 ## Sets the number of process workers. You must set `instance_id = *`
56 56 ## when this option is set to more than one worker, recommended
57 57 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
58 58 ## The `instance_id = *` must be set in the [app:main] section below
59 59 workers = 2
60 60 ## number of threads for each of the worker, must be set to 1 for gevent
61 61 ## generally recommened to be at 1
62 62 #threads = 1
63 63 ## process name
64 64 proc_name = rhodecode
65 65 ## type of worker class, one of sync, gevent
66 66 ## recommended for bigger setup is using of of other than sync one
67 67 worker_class = sync
68 68 ## The maximum number of simultaneous clients. Valid only for Gevent
69 69 #worker_connections = 10
70 70 ## max number of requests that worker will handle before being gracefully
71 71 ## restarted, could prevent memory leaks
72 72 max_requests = 1000
73 73 max_requests_jitter = 30
74 74 ## amount of time a worker can spend with handling a request before it
75 75 ## gets killed and restarted. Set to 6hrs
76 76 timeout = 21600
77 77
78 78
79 79 ## prefix middleware for RhodeCode, disables force_https flag.
80 80 ## allows to set RhodeCode under a prefix in server.
81 81 ## eg https://server.com/<prefix>. Enable `filter-with =` option below as well.
82 82 #[filter:proxy-prefix]
83 83 #use = egg:PasteDeploy#prefix
84 84 #prefix = /<your-prefix>
85 85
86 86 [app:main]
87 87 use = egg:rhodecode-enterprise-ce
88 88 ## enable proxy prefix middleware, defined below
89 89 #filter-with = proxy-prefix
90 90
91 91 ## encryption key used to encrypt social plugin tokens,
92 92 ## remote_urls with credentials etc, if not set it defaults to
93 93 ## `beaker.session.secret`
94 94 #rhodecode.encrypted_values.secret =
95 95
96 96 ## decryption strict mode (enabled by default). It controls if decryption raises
97 97 ## `SignatureVerificationError` in case of wrong key, or damaged encryption data.
98 98 #rhodecode.encrypted_values.strict = false
99 99
100 100 full_stack = true
101 101
102 102 ## return gzipped responses from Rhodecode (static files/application)
103 103 gzip_responses = true
104 104
105 105 # autogenerate javascript routes file on startup
106 106 generate_js_files = false
107 107
108 108 ## Optional Languages
109 109 ## en(default), be, de, es, fr, it, ja, pl, pt, ru, zh
110 110 lang = en
111 111
112 112 ## perform a full repository scan on each server start, this should be
113 113 ## set to false after first startup, to allow faster server restarts.
114 114 startup.import_repos = false
115 115
116 116 ## Uncomment and set this path to use archive download cache.
117 117 ## Once enabled, generated archives will be cached at this location
118 118 ## and served from the cache during subsequent requests for the same archive of
119 119 ## the repository.
120 120 #archive_cache_dir = /tmp/tarballcache
121 121
122 122 ## change this to unique ID for security
123 123 app_instance_uuid = rc-production
124 124
125 125 ## cut off limit for large diffs (size in bytes)
126 126 cut_off_limit_diff = 1024000
127 127 cut_off_limit_file = 256000
128 128
129 129 ## use cache version of scm repo everywhere
130 130 vcs_full_cache = true
131 131
132 132 ## force https in RhodeCode, fixes https redirects, assumes it's always https
133 133 ## Normally this is controlled by proper http flags sent from http server
134 134 force_https = false
135 135
136 136 ## use Strict-Transport-Security headers
137 137 use_htsts = false
138 138
139 139 ## number of commits stats will parse on each iteration
140 140 commit_parse_limit = 25
141 141
142 142 ## git rev filter option, --all is the default filter, if you need to
143 143 ## hide all refs in changelog switch this to --branches --tags
144 144 git_rev_filter = --branches --tags
145 145
146 146 # Set to true if your repos are exposed using the dumb protocol
147 147 git_update_server_info = false
148 148
149 149 ## RSS/ATOM feed options
150 150 rss_cut_off_limit = 256000
151 151 rss_items_per_page = 10
152 152 rss_include_diff = false
153 153
154 154 ## gist URL alias, used to create nicer urls for gist. This should be an
155 155 ## url that does rewrites to _admin/gists/<gistid>.
156 156 ## example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
157 157 ## RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/<gistid>
158 158 gist_alias_url =
159 159
160 160 ## List of controllers (using glob pattern syntax) that AUTH TOKENS could be
161 161 ## used for access.
162 162 ## Adding ?auth_token = <token> to the url authenticates this request as if it
163 163 ## came from the the logged in user who own this authentication token.
164 164 ##
165 165 ## Syntax is <ControllerClass>:<function_pattern>.
166 166 ## To enable access to raw_files put `FilesController:raw`.
167 167 ## To enable access to patches add `ChangesetController:changeset_patch`.
168 168 ## The list should be "," separated and on a single line.
169 169 ##
170 170 ## Recommended controllers to enable:
171 171 # ChangesetController:changeset_patch,
172 172 # ChangesetController:changeset_raw,
173 173 # FilesController:raw,
174 174 # FilesController:archivefile,
175 175 # GistsController:*,
176 176 api_access_controllers_whitelist =
177 177
178 178 ## default encoding used to convert from and to unicode
179 179 ## can be also a comma separated list of encoding in case of mixed encodings
180 180 default_encoding = UTF-8
181 181
182 182 ## instance-id prefix
183 183 ## a prefix key for this instance used for cache invalidation when running
184 184 ## multiple instances of rhodecode, make sure it's globally unique for
185 185 ## all running rhodecode instances. Leave empty if you don't use it
186 186 instance_id =
187 187
188 188 ## Fallback authentication plugin. Set this to a plugin ID to force the usage
189 189 ## of an authentication plugin also if it is disabled by it's settings.
190 190 ## This could be useful if you are unable to log in to the system due to broken
191 191 ## authentication settings. Then you can enable e.g. the internal rhodecode auth
192 192 ## module to log in again and fix the settings.
193 193 ##
194 194 ## Available builtin plugin IDs (hash is part of the ID):
195 195 ## egg:rhodecode-enterprise-ce#rhodecode
196 196 ## egg:rhodecode-enterprise-ce#pam
197 197 ## egg:rhodecode-enterprise-ce#ldap
198 198 ## egg:rhodecode-enterprise-ce#jasig_cas
199 199 ## egg:rhodecode-enterprise-ce#headers
200 200 ## egg:rhodecode-enterprise-ce#crowd
201 201 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
202 202
203 203 ## alternative return HTTP header for failed authentication. Default HTTP
204 204 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
205 205 ## handling that causing a series of failed authentication calls.
206 206 ## Set this variable to 403 to return HTTPForbidden, or any other HTTP code
207 207 ## This will be served instead of default 401 on bad authnetication
208 208 auth_ret_code =
209 209
210 210 ## use special detection method when serving auth_ret_code, instead of serving
211 211 ## ret_code directly, use 401 initially (Which triggers credentials prompt)
212 212 ## and then serve auth_ret_code to clients
213 213 auth_ret_code_detection = false
214 214
215 215 ## locking return code. When repository is locked return this HTTP code. 2XX
216 216 ## codes don't break the transactions while 4XX codes do
217 217 lock_ret_code = 423
218 218
219 219 ## allows to change the repository location in settings page
220 220 allow_repo_location_change = true
221 221
222 222 ## allows to setup custom hooks in settings page
223 223 allow_custom_hooks_settings = true
224 224
225 225 ## generated license token, goto license page in RhodeCode settings to obtain
226 226 ## new token
227 227 license_token =
228 228
229 229 ## supervisor connection uri, for managing supervisor and logs.
230 230 supervisor.uri =
231 231 ## supervisord group name/id we only want this RC instance to handle
232 232 supervisor.group_id = prod
233 233
234 234 ## Display extended labs settings
235 235 labs_settings_active = true
236 236
237 237 ####################################
238 238 ### CELERY CONFIG ####
239 239 ####################################
240 240 use_celery = false
241 241 broker.host = localhost
242 242 broker.vhost = rabbitmqhost
243 243 broker.port = 5672
244 244 broker.user = rabbitmq
245 245 broker.password = qweqwe
246 246
247 247 celery.imports = rhodecode.lib.celerylib.tasks
248 248
249 249 celery.result.backend = amqp
250 250 celery.result.dburi = amqp://
251 251 celery.result.serialier = json
252 252
253 253 #celery.send.task.error.emails = true
254 254 #celery.amqp.task.result.expires = 18000
255 255
256 256 celeryd.concurrency = 2
257 257 #celeryd.log.file = celeryd.log
258 258 celeryd.log.level = debug
259 259 celeryd.max.tasks.per.child = 1
260 260
261 261 ## tasks will never be sent to the queue, but executed locally instead.
262 262 celery.always.eager = false
263 263
264 264 ####################################
265 265 ### BEAKER CACHE ####
266 266 ####################################
267 267 # default cache dir for templates. Putting this into a ramdisk
268 268 ## can boost performance, eg. %(here)s/data_ramdisk
269 269 cache_dir = %(here)s/data
270 270
271 271 ## locking and default file storage for Beaker. Putting this into a ramdisk
272 272 ## can boost performance, eg. %(here)s/data_ramdisk/cache/beaker_data
273 273 beaker.cache.data_dir = %(here)s/data/cache/beaker_data
274 274 beaker.cache.lock_dir = %(here)s/data/cache/beaker_lock
275 275
276 276 beaker.cache.regions = super_short_term, short_term, long_term, sql_cache_short, auth_plugins, repo_cache_long
277 277
278 278 beaker.cache.super_short_term.type = memory
279 279 beaker.cache.super_short_term.expire = 10
280 280 beaker.cache.super_short_term.key_length = 256
281 281
282 282 beaker.cache.short_term.type = memory
283 283 beaker.cache.short_term.expire = 60
284 284 beaker.cache.short_term.key_length = 256
285 285
286 286 beaker.cache.long_term.type = memory
287 287 beaker.cache.long_term.expire = 36000
288 288 beaker.cache.long_term.key_length = 256
289 289
290 290 beaker.cache.sql_cache_short.type = memory
291 291 beaker.cache.sql_cache_short.expire = 10
292 292 beaker.cache.sql_cache_short.key_length = 256
293 293
294 294 # default is memory cache, configure only if required
295 295 # using multi-node or multi-worker setup
296 296 #beaker.cache.auth_plugins.type = ext:database
297 297 #beaker.cache.auth_plugins.lock_dir = %(here)s/data/cache/auth_plugin_lock
298 298 #beaker.cache.auth_plugins.url = postgresql://postgres:secret@localhost/rhodecode
299 299 #beaker.cache.auth_plugins.url = mysql://root:secret@127.0.0.1/rhodecode
300 300 #beaker.cache.auth_plugins.sa.pool_recycle = 3600
301 301 #beaker.cache.auth_plugins.sa.pool_size = 10
302 302 #beaker.cache.auth_plugins.sa.max_overflow = 0
303 303
304 304 beaker.cache.repo_cache_long.type = memorylru_base
305 305 beaker.cache.repo_cache_long.max_items = 4096
306 306 beaker.cache.repo_cache_long.expire = 2592000
307 307
308 308 # default is memorylru_base cache, configure only if required
309 309 # using multi-node or multi-worker setup
310 310 #beaker.cache.repo_cache_long.type = ext:memcached
311 311 #beaker.cache.repo_cache_long.url = localhost:11211
312 312 #beaker.cache.repo_cache_long.expire = 1209600
313 313 #beaker.cache.repo_cache_long.key_length = 256
314 314
315 315 ####################################
316 316 ### BEAKER SESSION ####
317 317 ####################################
318 318
319 319 ## .session.type is type of storage options for the session, current allowed
320 320 ## types are file, ext:memcached, ext:database, and memory (default).
321 321 beaker.session.type = file
322 322 beaker.session.data_dir = %(here)s/data/sessions/data
323 323
324 324 ## db based session, fast, and allows easy management over logged in users ##
325 325 #beaker.session.type = ext:database
326 326 #beaker.session.table_name = db_session
327 327 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
328 328 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
329 329 #beaker.session.sa.pool_recycle = 3600
330 330 #beaker.session.sa.echo = false
331 331
332 332 beaker.session.key = rhodecode
333 333 beaker.session.secret = production-rc-uytcxaz
334 334 beaker.session.lock_dir = %(here)s/data/sessions/lock
335 335
336 336 ## Secure encrypted cookie. Requires AES and AES python libraries
337 337 ## you must disable beaker.session.secret to use this
338 338 #beaker.session.encrypt_key = <key_for_encryption>
339 339 #beaker.session.validate_key = <validation_key>
340 340
341 341 ## sets session as invalid(also logging out user) if it haven not been
342 342 ## accessed for given amount of time in seconds
343 343 beaker.session.timeout = 2592000
344 344 beaker.session.httponly = true
345 345 #beaker.session.cookie_path = /<your-prefix>
346 346
347 347 ## uncomment for https secure cookie
348 348 beaker.session.secure = false
349 349
350 350 ## auto save the session to not to use .save()
351 351 beaker.session.auto = false
352 352
353 353 ## default cookie expiration time in seconds, set to `true` to set expire
354 354 ## at browser close
355 355 #beaker.session.cookie_expires = 3600
356 356
357 357 ###################################
358 358 ## SEARCH INDEXING CONFIGURATION ##
359 359 ###################################
360 360 ## Full text search indexer is available in rhodecode-tools under
361 361 ## `rhodecode-tools index` command
362 362
363 363 # WHOOSH Backend, doesn't require additional services to run
364 364 # it works good with few dozen repos
365 365 search.module = rhodecode.lib.index.whoosh
366 366 search.location = %(here)s/data/index
367 367
368 ########################################
369 ### CHANNELSTREAM CONFIG ####
370 ########################################
371
372 channelstream.enabled = true
373 # location of channelstream server on the backend
374 channelstream.server = 127.0.0.1:9800
375 # location of the channelstream server from outside world
376 channelstream.ws_url = ws://127.0.0.1:9800
377 channelstream.secret = secret
378
368 379 ###################################
369 380 ## APPENLIGHT CONFIG ##
370 381 ###################################
371 382
372 383 ## Appenlight is tailored to work with RhodeCode, see
373 384 ## http://appenlight.com for details how to obtain an account
374 385
375 386 ## appenlight integration enabled
376 387 appenlight = false
377 388
378 389 appenlight.server_url = https://api.appenlight.com
379 390 appenlight.api_key = YOUR_API_KEY
380 391 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
381 392
382 393 # used for JS client
383 394 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
384 395
385 396 ## TWEAK AMOUNT OF INFO SENT HERE
386 397
387 398 ## enables 404 error logging (default False)
388 399 appenlight.report_404 = false
389 400
390 401 ## time in seconds after request is considered being slow (default 1)
391 402 appenlight.slow_request_time = 1
392 403
393 404 ## record slow requests in application
394 405 ## (needs to be enabled for slow datastore recording and time tracking)
395 406 appenlight.slow_requests = true
396 407
397 408 ## enable hooking to application loggers
398 409 appenlight.logging = true
399 410
400 411 ## minimum log level for log capture
401 412 appenlight.logging.level = WARNING
402 413
403 414 ## send logs only from erroneous/slow requests
404 415 ## (saves API quota for intensive logging)
405 416 appenlight.logging_on_error = false
406 417
407 418 ## list of additonal keywords that should be grabbed from environ object
408 419 ## can be string with comma separated list of words in lowercase
409 420 ## (by default client will always send following info:
410 421 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
411 422 ## start with HTTP* this list be extended with additional keywords here
412 423 appenlight.environ_keys_whitelist =
413 424
414 425 ## list of keywords that should be blanked from request object
415 426 ## can be string with comma separated list of words in lowercase
416 427 ## (by default client will always blank keys that contain following words
417 428 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
418 429 ## this list be extended with additional keywords set here
419 430 appenlight.request_keys_blacklist =
420 431
421 432 ## list of namespaces that should be ignores when gathering log entries
422 433 ## can be string with comma separated list of namespaces
423 434 ## (by default the client ignores own entries: appenlight_client.client)
424 435 appenlight.log_namespace_blacklist =
425 436
426 437
427 438 ################################################################################
428 439 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
429 440 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
430 441 ## execute malicious code after an exception is raised. ##
431 442 ################################################################################
432 443 set debug = false
433 444
434 445
435 446 #########################################################
436 447 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
437 448 #########################################################
438 449 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
439 450 sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
440 451 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode
441 452
442 453 # see sqlalchemy docs for other advanced settings
443 454
444 455 ## print the sql statements to output
445 456 sqlalchemy.db1.echo = false
446 457 ## recycle the connections after this ammount of seconds
447 458 sqlalchemy.db1.pool_recycle = 3600
448 459 sqlalchemy.db1.convert_unicode = true
449 460
450 461 ## the number of connections to keep open inside the connection pool.
451 462 ## 0 indicates no limit
452 463 #sqlalchemy.db1.pool_size = 5
453 464
454 465 ## the number of connections to allow in connection pool "overflow", that is
455 466 ## connections that can be opened above and beyond the pool_size setting,
456 467 ## which defaults to five.
457 468 #sqlalchemy.db1.max_overflow = 10
458 469
459 470
460 471 ##################
461 472 ### VCS CONFIG ###
462 473 ##################
463 474 vcs.server.enable = true
464 475 vcs.server = localhost:9900
465 476
466 477 ## Web server connectivity protocol, responsible for web based VCS operatations
467 478 ## Available protocols are:
468 479 ## `pyro4` - using pyro4 server
469 480 ## `http` - using http-rpc backend
470 481 #vcs.server.protocol = http
471 482
472 483 ## Push/Pull operations protocol, available options are:
473 484 ## `pyro4` - using pyro4 server
474 485 ## `rhodecode.lib.middleware.utils.scm_app_http` - Http based, recommended
475 486 ## `vcsserver.scm_app` - internal app (EE only)
476 487 #vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http
477 488
478 489 ## Push/Pull operations hooks protocol, available options are:
479 490 ## `pyro4` - using pyro4 server
480 491 ## `http` - using http-rpc backend
481 492 #vcs.hooks.protocol = http
482 493
483 494 vcs.server.log_level = info
484 495 ## Start VCSServer with this instance as a subprocess, usefull for development
485 496 vcs.start_server = false
486 497 vcs.backends = hg, git, svn
487 498 vcs.connection_timeout = 3600
488 499 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
489 500 ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible
490 501 #vcs.svn.compatible_version = pre-1.8-compatible
491 502
492 503 ################################
493 504 ### LOGGING CONFIGURATION ####
494 505 ################################
495 506 [loggers]
496 507 keys = root, routes, rhodecode, sqlalchemy, beaker, pyro4, templates
497 508
498 509 [handlers]
499 510 keys = console, console_sql
500 511
501 512 [formatters]
502 513 keys = generic, color_formatter, color_formatter_sql
503 514
504 515 #############
505 516 ## LOGGERS ##
506 517 #############
507 518 [logger_root]
508 519 level = NOTSET
509 520 handlers = console
510 521
511 522 [logger_routes]
512 523 level = DEBUG
513 524 handlers =
514 525 qualname = routes.middleware
515 526 ## "level = DEBUG" logs the route matched and routing variables.
516 527 propagate = 1
517 528
518 529 [logger_beaker]
519 530 level = DEBUG
520 531 handlers =
521 532 qualname = beaker.container
522 533 propagate = 1
523 534
524 535 [logger_pyro4]
525 536 level = DEBUG
526 537 handlers =
527 538 qualname = Pyro4
528 539 propagate = 1
529 540
530 541 [logger_templates]
531 542 level = INFO
532 543 handlers =
533 544 qualname = pylons.templating
534 545 propagate = 1
535 546
536 547 [logger_rhodecode]
537 548 level = DEBUG
538 549 handlers =
539 550 qualname = rhodecode
540 551 propagate = 1
541 552
542 553 [logger_sqlalchemy]
543 554 level = INFO
544 555 handlers = console_sql
545 556 qualname = sqlalchemy.engine
546 557 propagate = 0
547 558
548 559 ##############
549 560 ## HANDLERS ##
550 561 ##############
551 562
552 563 [handler_console]
553 564 class = StreamHandler
554 565 args = (sys.stderr,)
555 566 level = INFO
556 567 formatter = generic
557 568
558 569 [handler_console_sql]
559 570 class = StreamHandler
560 571 args = (sys.stderr,)
561 572 level = WARN
562 573 formatter = generic
563 574
564 575 ################
565 576 ## FORMATTERS ##
566 577 ################
567 578
568 579 [formatter_generic]
569 580 class = rhodecode.lib.logging_formatter.Pyro4AwareFormatter
570 581 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
571 582 datefmt = %Y-%m-%d %H:%M:%S
572 583
573 584 [formatter_color_formatter]
574 585 class = rhodecode.lib.logging_formatter.ColorFormatter
575 586 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
576 587 datefmt = %Y-%m-%d %H:%M:%S
577 588
578 589 [formatter_color_formatter_sql]
579 590 class = rhodecode.lib.logging_formatter.ColorFormatterSql
580 591 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
581 592 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,228 +1,229 b''
1 1 # Nix environment for the community edition
2 2 #
3 3 # This shall be as lean as possible, just producing the Enterprise
4 4 # derivation. For advanced tweaks to pimp up the development environment we use
5 5 # "shell.nix" so that it does not have to clutter this file.
6 6
7 7 { pkgs ? (import <nixpkgs> {})
8 8 , pythonPackages ? "python27Packages"
9 9 , pythonExternalOverrides ? self: super: {}
10 10 , doCheck ? true
11 11 }:
12 12
13 13 let pkgs_ = pkgs; in
14 14
15 15 let
16 16 pkgs = pkgs_.overridePackages (self: super: {
17 17 # Override subversion derivation to
18 18 # - activate python bindings
19 19 # - set version to 1.8
20 20 subversion = super.subversion18.override {
21 21 httpSupport = true;
22 22 pythonBindings = true;
23 23 python = self.python27Packages.python;
24 24 };
25 25 });
26 26
27 27 inherit (pkgs.lib) fix extends;
28 28
29 29 basePythonPackages = with builtins; if isAttrs pythonPackages
30 30 then pythonPackages
31 31 else getAttr pythonPackages pkgs;
32 32
33 33 elem = builtins.elem;
34 34 basename = path: with pkgs.lib; last (splitString "/" path);
35 35 startsWith = prefix: full: let
36 36 actualPrefix = builtins.substring 0 (builtins.stringLength prefix) full;
37 37 in actualPrefix == prefix;
38 38
39 39 src-filter = path: type: with pkgs.lib;
40 40 let
41 41 ext = last (splitString "." path);
42 42 in
43 43 !elem (basename path) [
44 44 ".git" ".hg" "__pycache__" ".eggs" "node_modules"
45 45 "build" "data" "tmp"] &&
46 46 !elem ext ["egg-info" "pyc"] &&
47 47 !startsWith "result" path;
48 48
49 49 sources = pkgs.config.rc.sources or {};
50 50 rhodecode-enterprise-ce-src = builtins.filterSource src-filter ./.;
51 51
52 52 # Load the generated node packages
53 53 nodePackages = pkgs.callPackage "${pkgs.path}/pkgs/top-level/node-packages.nix" rec {
54 54 self = nodePackages;
55 55 generated = pkgs.callPackage ./pkgs/node-packages.nix { inherit self; };
56 56 };
57 57
58 58 # TODO: Should be taken automatically out of the generates packages.
59 59 # apps.nix has one solution for this, although I'd prefer to have the deps
60 60 # from package.json mapped in here.
61 61 nodeDependencies = with nodePackages; [
62 62 grunt
63 63 grunt-contrib-concat
64 64 grunt-contrib-jshint
65 65 grunt-contrib-less
66 66 grunt-contrib-watch
67 67 jshint
68 68 ];
69 69
70 70 pythonGeneratedPackages = self: basePythonPackages.override (a: {
71 71 inherit self;
72 72 })
73 73 // (scopedImport {
74 74 self = self;
75 75 super = basePythonPackages;
76 76 inherit pkgs;
77 77 inherit (pkgs) fetchurl fetchgit;
78 78 } ./pkgs/python-packages.nix);
79 79
80 80 pythonOverrides = import ./pkgs/python-packages-overrides.nix {
81 81 inherit
82 82 basePythonPackages
83 83 pkgs;
84 84 };
85 85
86 86 pythonLocalOverrides = self: super: {
87 87 rhodecode-enterprise-ce =
88 88 let
89 89 version = builtins.readFile ./rhodecode/VERSION;
90 90 linkNodeModules = ''
91 91 echo "Link node packages"
92 92 # TODO: check if this adds stuff as a dependency, closure size
93 93 rm -fr node_modules
94 94 mkdir -p node_modules
95 95 ${pkgs.lib.concatMapStrings (dep: ''
96 96 ln -sfv ${dep}/lib/node_modules/${dep.pkgName} node_modules/
97 97 '') nodeDependencies}
98 98 echo "DONE: Link node packages"
99 99 '';
100 100 in super.rhodecode-enterprise-ce.override (attrs: {
101 101
102 102 inherit
103 103 doCheck
104 104 version;
105 105 name = "rhodecode-enterprise-ce-${version}";
106 106 releaseName = "RhodeCodeEnterpriseCE-${version}";
107 107 src = rhodecode-enterprise-ce-src;
108 108
109 109 buildInputs =
110 110 attrs.buildInputs ++
111 111 (with self; [
112 112 pkgs.nodePackages.grunt-cli
113 113 pkgs.subversion
114 114 pytest-catchlog
115 115 rhodecode-testdata
116 116 ]);
117 117
118 118 propagatedBuildInputs = attrs.propagatedBuildInputs ++ (with self; [
119 119 rhodecode-tools
120 120 ]);
121 121
122 122 # TODO: johbo: Make a nicer way to expose the parts. Maybe
123 123 # pkgs/default.nix?
124 124 passthru = {
125 125 inherit
126 126 linkNodeModules
127 127 myPythonPackagesUnfix
128 128 pythonLocalOverrides;
129 129 pythonPackages = self;
130 130 };
131 131
132 132 LC_ALL = "en_US.UTF-8";
133 133 LOCALE_ARCHIVE =
134 134 if pkgs.stdenv ? glibc
135 135 then "${pkgs.glibcLocales}/lib/locale/locale-archive"
136 136 else "";
137 137
138 138 # Somewhat snappier setup of the development environment
139 139 # TODO: move into shell.nix
140 140 # TODO: think of supporting a stable path again, so that multiple shells
141 141 # can share it.
142 142 shellHook = ''
143 143 tmp_path=$(mktemp -d)
144 144 export PATH="$tmp_path/bin:$PATH"
145 145 export PYTHONPATH="$tmp_path/${self.python.sitePackages}:$PYTHONPATH"
146 146 mkdir -p $tmp_path/${self.python.sitePackages}
147 147 python setup.py develop --prefix $tmp_path --allow-hosts ""
148 148 '' + linkNodeModules;
149 149
150 150 preCheck = ''
151 151 export PATH="$out/bin:$PATH"
152 152 '';
153 153
154 154 postCheck = ''
155 155 rm -rf $out/lib/${self.python.libPrefix}/site-packages/pytest_pylons
156 156 rm -rf $out/lib/${self.python.libPrefix}/site-packages/rhodecode/tests
157 157 '';
158 158
159 159 preBuild = linkNodeModules + ''
160 160 grunt
161 161 rm -fr node_modules
162 162 '';
163 163
164 164 postInstall = ''
165 165 # python based programs need to be wrapped
166 166 ln -s ${self.supervisor}/bin/supervisor* $out/bin/
167 167 ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
168 168 ln -s ${self.PasteScript}/bin/paster $out/bin/
169 ln -s ${self.channelstream}/bin/channelstream $out/bin/
169 170 ln -s ${self.pyramid}/bin/* $out/bin/ #*/
170 171
171 172 # rhodecode-tools
172 173 # TODO: johbo: re-think this. Do the tools import anything from enterprise?
173 174 ln -s ${self.rhodecode-tools}/bin/rhodecode-* $out/bin/
174 175
175 176 # note that condition should be restricted when adding further tools
176 177 for file in $out/bin/*; do #*/
177 178 wrapProgram $file \
178 179 --prefix PYTHONPATH : $PYTHONPATH \
179 180 --prefix PATH : $PATH \
180 181 --set PYTHONHASHSEED random
181 182 done
182 183
183 184 mkdir $out/etc
184 185 cp configs/production.ini $out/etc
185 186
186 187 echo "Writing meta information for rccontrol to nix-support/rccontrol"
187 188 mkdir -p $out/nix-support/rccontrol
188 189 cp -v rhodecode/VERSION $out/nix-support/rccontrol/version
189 190 echo "DONE: Meta information for rccontrol written"
190 191
191 192 # TODO: johbo: Make part of ac-tests
192 193 if [ ! -f rhodecode/public/js/scripts.js ]; then
193 194 echo "Missing scripts.js"
194 195 exit 1
195 196 fi
196 197 if [ ! -f rhodecode/public/css/style.css ]; then
197 198 echo "Missing style.css"
198 199 exit 1
199 200 fi
200 201 '';
201 202
202 203 });
203 204
204 205 rhodecode-testdata = import "${rhodecode-testdata-src}/default.nix" {
205 206 inherit
206 207 doCheck
207 208 pkgs
208 209 pythonPackages;
209 210 };
210 211
211 212 };
212 213
213 214 rhodecode-testdata-src = sources.rhodecode-testdata or (
214 215 pkgs.fetchhg {
215 216 url = "https://code.rhodecode.com/upstream/rc_testdata";
216 217 rev = "v0.8.0";
217 218 sha256 = "0hy1ba134rq2f9si85yx7j4qhc9ky0hjzdk553s3q026i7km809m";
218 219 });
219 220
220 221 # Apply all overrides and fix the final package set
221 222 myPythonPackagesUnfix =
222 223 (extends pythonExternalOverrides
223 224 (extends pythonLocalOverrides
224 225 (extends pythonOverrides
225 226 pythonGeneratedPackages)));
226 227 myPythonPackages = (fix myPythonPackagesUnfix);
227 228
228 229 in myPythonPackages.rhodecode-enterprise-ce
@@ -1,192 +1,199 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 environment configuration
23 23 """
24 24
25 25 import os
26 26 import logging
27 27 import rhodecode
28 28 import platform
29 29 import re
30 30 import io
31 31
32 32 from mako.lookup import TemplateLookup
33 33 from pylons.configuration import PylonsConfig
34 34 from pylons.error import handle_mako_error
35 35 from pyramid.settings import asbool
36 36
37 37 # don't remove this import it does magic for celery
38 38 from rhodecode.lib import celerypylons # noqa
39 39
40 40 import rhodecode.lib.app_globals as app_globals
41 41
42 42 from rhodecode.config import utils
43 43 from rhodecode.config.routing import make_map
44 44 from rhodecode.config.jsroutes import generate_jsroutes_content
45 45
46 46 from rhodecode.lib import helpers
47 47 from rhodecode.lib.auth import set_available_permissions
48 48 from rhodecode.lib.utils import (
49 49 repo2db_mapper, make_db_config, set_rhodecode_config,
50 50 load_rcextensions)
51 51 from rhodecode.lib.utils2 import str2bool, aslist
52 52 from rhodecode.lib.vcs import connect_vcs, start_vcs_server
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57 def load_environment(global_conf, app_conf, initial=False,
58 58 test_env=None, test_index=None):
59 59 """
60 60 Configure the Pylons environment via the ``pylons.config``
61 61 object
62 62 """
63 63 config = PylonsConfig()
64 64
65 65
66 66 # Pylons paths
67 67 root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
68 68 paths = {
69 69 'root': root,
70 70 'controllers': os.path.join(root, 'controllers'),
71 71 'static_files': os.path.join(root, 'public'),
72 72 'templates': [os.path.join(root, 'templates')],
73 73 }
74 74
75 75 # Initialize config with the basic options
76 76 config.init_app(global_conf, app_conf, package='rhodecode', paths=paths)
77 77
78 78 # store some globals into rhodecode
79 79 rhodecode.CELERY_ENABLED = str2bool(config['app_conf'].get('use_celery'))
80 80 rhodecode.CELERY_EAGER = str2bool(
81 81 config['app_conf'].get('celery.always.eager'))
82 82
83 83 config['routes.map'] = make_map(config)
84 84
85 85 if asbool(config.get('generate_js_files', 'false')):
86 86 jsroutes = config['routes.map'].jsroutes()
87 87 jsroutes_file_content = generate_jsroutes_content(jsroutes)
88 88 jsroutes_file_path = os.path.join(
89 89 paths['static_files'], 'js', 'rhodecode', 'routes.js')
90 90
91 91 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
92 92 f.write(jsroutes_file_content)
93 93
94 94 config['pylons.app_globals'] = app_globals.Globals(config)
95 95 config['pylons.h'] = helpers
96 96 rhodecode.CONFIG = config
97 97
98 98 load_rcextensions(root_path=config['here'])
99 99
100 100 # Setup cache object as early as possible
101 101 import pylons
102 102 pylons.cache._push_object(config['pylons.app_globals'].cache)
103 103
104 104 # Create the Mako TemplateLookup, with the default auto-escaping
105 105 config['pylons.app_globals'].mako_lookup = TemplateLookup(
106 106 directories=paths['templates'],
107 107 error_handler=handle_mako_error,
108 108 module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
109 109 input_encoding='utf-8', default_filters=['escape'],
110 110 imports=['from webhelpers.html import escape'])
111 111
112 112 # sets the c attribute access when don't existing attribute are accessed
113 113 config['pylons.strict_tmpl_context'] = True
114 114
115 # configure channelstream
116 config['channelstream_config'] = {
117 'enabled': asbool(config.get('channelstream.enabled', False)),
118 'server': config.get('channelstream.server'),
119 'secret': config.get('channelstream.secret')
120 }
121
115 122 # Limit backends to "vcs.backends" from configuration
116 123 backends = config['vcs.backends'] = aslist(
117 124 config.get('vcs.backends', 'hg,git'), sep=',')
118 125 for alias in rhodecode.BACKENDS.keys():
119 126 if alias not in backends:
120 127 del rhodecode.BACKENDS[alias]
121 128 log.info("Enabled backends: %s", backends)
122 129
123 130 # initialize vcs client and optionally run the server if enabled
124 131 vcs_server_uri = config.get('vcs.server', '')
125 132 vcs_server_enabled = str2bool(config.get('vcs.server.enable', 'true'))
126 133 start_server = (
127 134 str2bool(config.get('vcs.start_server', 'false')) and
128 135 not int(os.environ.get('RC_VCSSERVER_TEST_DISABLE', '0')))
129 136 if vcs_server_enabled and start_server:
130 137 log.info("Starting vcsserver")
131 138 start_vcs_server(server_and_port=vcs_server_uri,
132 139 protocol=utils.get_vcs_server_protocol(config),
133 140 log_level=config['vcs.server.log_level'])
134 141
135 142 set_available_permissions(config)
136 143 db_cfg = make_db_config(clear_session=True)
137 144
138 145 repos_path = list(db_cfg.items('paths'))[0][1]
139 146 config['base_path'] = repos_path
140 147
141 148 config['vcs.hooks.direct_calls'] = _use_direct_hook_calls(config)
142 149 config['vcs.hooks.protocol'] = _get_vcs_hooks_protocol(config)
143 150
144 151 # store db config also in main global CONFIG
145 152 set_rhodecode_config(config)
146 153
147 154 # configure instance id
148 155 utils.set_instance_id(config)
149 156
150 157 # CONFIGURATION OPTIONS HERE (note: all config options will override
151 158 # any Pylons config options)
152 159
153 160 # store config reference into our module to skip import magic of pylons
154 161 rhodecode.CONFIG.update(config)
155 162
156 163 utils.configure_pyro4(config)
157 164 utils.configure_vcs(config)
158 165 if vcs_server_enabled:
159 166 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(config))
160 167
161 168 import_on_startup = str2bool(config.get('startup.import_repos', False))
162 169 if vcs_server_enabled and import_on_startup:
163 170 repo2db_mapper(ScmModel().repo_scan(repos_path), remove_obsolete=False)
164 171 return config
165 172
166 173
167 174 def _use_direct_hook_calls(config):
168 175 default_direct_hook_calls = 'false'
169 176 direct_hook_calls = str2bool(
170 177 config.get('vcs.hooks.direct_calls', default_direct_hook_calls))
171 178 return direct_hook_calls
172 179
173 180
174 181 def _get_vcs_hooks_protocol(config):
175 182 protocol = config.get('vcs.hooks.protocol', 'pyro4').lower()
176 183 return protocol
177 184
178 185
179 186 def load_pyramid_environment(global_config, settings):
180 187 # Some parts of the code expect a merge of global and app settings.
181 188 settings_merged = global_config.copy()
182 189 settings_merged.update(settings)
183 190
184 191 # If this is a test run we prepare the test environment like
185 192 # creating a test database, test search index and test repositories.
186 193 # This has to be done before the database connection is initialized.
187 194 if settings['is_test']:
188 195 rhodecode.is_test = True
189 196 utils.initialize_test_environment(settings_merged)
190 197
191 198 # Initialize the database connection.
192 199 utils.initialize_database(settings_merged)
@@ -1,395 +1,403 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 from collections import OrderedDict
25 26
26 27 from paste.registry import RegistryManager
27 28 from paste.gzipper import make_gzip_middleware
28 29 from pylons.wsgiapp import PylonsApp
29 30 from pyramid.authorization import ACLAuthorizationPolicy
30 31 from pyramid.config import Configurator
31 32 from pyramid.static import static_view
32 33 from pyramid.settings import asbool, aslist
33 34 from pyramid.wsgi import wsgiapp
34 35 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError
35 36 from pylons.controllers.util import abort, redirect
36 37 import pyramid.httpexceptions as httpexceptions
37 38 from pyramid.renderers import render_to_response, render
38 39 from routes.middleware import RoutesMiddleware
39 40 import routes.util
40 41
41 42 import rhodecode
42 43 import rhodecode.integrations # do not remove this as it registers celery tasks
43 44 from rhodecode.config import patches
44 45 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 46 from rhodecode.config.environment import (
46 47 load_environment, load_pyramid_environment)
47 48 from rhodecode.lib.middleware import csrf
48 49 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 50 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
50 51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
51 52 from rhodecode.lib.middleware.vcs import VCSMiddleware
52 53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
53 54
54 55
55 56 log = logging.getLogger(__name__)
56 57
57 58
58 59 # this is used to avoid avoid the route lookup overhead in routesmiddleware
59 60 # for certain routes which won't go to pylons to - eg. static files, debugger
60 61 # it is only needed for the pylons migration and can be removed once complete
61 62 class SkippableRoutesMiddleware(RoutesMiddleware):
62 63 """ Routes middleware that allows you to skip prefixes """
63 64
64 65 def __init__(self, *args, **kw):
65 66 self.skip_prefixes = kw.pop('skip_prefixes', [])
66 67 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
67 68
68 69 def __call__(self, environ, start_response):
69 70 for prefix in self.skip_prefixes:
70 71 if environ['PATH_INFO'].startswith(prefix):
72 # added to avoid the case when a missing /_static route falls
73 # through to pylons and causes an exception as pylons is
74 # expecting wsgiorg.routingargs to be set in the environ
75 # by RoutesMiddleware.
76 if 'wsgiorg.routing_args' not in environ:
77 environ['wsgiorg.routing_args'] = (None, {})
71 78 return self.app(environ, start_response)
72 79
73 80 return super(SkippableRoutesMiddleware, self).__call__(
74 81 environ, start_response)
75 82
76 83
77 84 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
78 85 """Create a Pylons WSGI application and return it
79 86
80 87 ``global_conf``
81 88 The inherited configuration for this application. Normally from
82 89 the [DEFAULT] section of the Paste ini file.
83 90
84 91 ``full_stack``
85 92 Whether or not this application provides a full WSGI stack (by
86 93 default, meaning it handles its own exceptions and errors).
87 94 Disable full_stack when this application is "managed" by
88 95 another WSGI middleware.
89 96
90 97 ``app_conf``
91 98 The application's local configuration. Normally specified in
92 99 the [app:<name>] section of the Paste ini file (where <name>
93 100 defaults to main).
94 101
95 102 """
96 103 # Apply compatibility patches
97 104 patches.kombu_1_5_1_python_2_7_11()
98 105 patches.inspect_getargspec()
99 106
100 107 # Configure the Pylons environment
101 108 config = load_environment(global_conf, app_conf)
102 109
103 110 # The Pylons WSGI app
104 111 app = PylonsApp(config=config)
105 112 if rhodecode.is_test:
106 113 app = csrf.CSRFDetector(app)
107 114
108 115 expected_origin = config.get('expected_origin')
109 116 if expected_origin:
110 117 # The API can be accessed from other Origins.
111 118 app = csrf.OriginChecker(app, expected_origin,
112 119 skip_urls=[routes.util.url_for('api')])
113 120
114 121
115 122 if asbool(full_stack):
116 123
117 124 # Appenlight monitoring and error handler
118 125 app, appenlight_client = wrap_in_appenlight_if_enabled(app, config)
119 126
120 127 # we want our low level middleware to get to the request ASAP. We don't
121 128 # need any pylons stack middleware in them
122 129 app = VCSMiddleware(app, config, appenlight_client)
123 130
124 131 # Establish the Registry for this application
125 132 app = RegistryManager(app)
126 133
127 134 app.config = config
128 135
129 136 return app
130 137
131 138
132 139 def make_pyramid_app(global_config, **settings):
133 140 """
134 141 Constructs the WSGI application based on Pyramid and wraps the Pylons based
135 142 application.
136 143
137 144 Specials:
138 145
139 146 * We migrate from Pylons to Pyramid. While doing this, we keep both
140 147 frameworks functional. This involves moving some WSGI middlewares around
141 148 and providing access to some data internals, so that the old code is
142 149 still functional.
143 150
144 151 * The application can also be integrated like a plugin via the call to
145 152 `includeme`. This is accompanied with the other utility functions which
146 153 are called. Changing this should be done with great care to not break
147 154 cases when these fragments are assembled from another place.
148 155
149 156 """
150 157 # The edition string should be available in pylons too, so we add it here
151 158 # before copying the settings.
152 159 settings.setdefault('rhodecode.edition', 'Community Edition')
153 160
154 161 # As long as our Pylons application does expect "unprepared" settings, make
155 162 # sure that we keep an unmodified copy. This avoids unintentional change of
156 163 # behavior in the old application.
157 164 settings_pylons = settings.copy()
158 165
159 166 sanitize_settings_and_apply_defaults(settings)
160 167 config = Configurator(settings=settings)
161 168 add_pylons_compat_data(config.registry, global_config, settings_pylons)
162 169
163 170 load_pyramid_environment(global_config, settings)
164 171
165 172 includeme_first(config)
166 173 includeme(config)
167 174 pyramid_app = config.make_wsgi_app()
168 175 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
169 176 return pyramid_app
170 177
171 178
172 179 def add_pylons_compat_data(registry, global_config, settings):
173 180 """
174 181 Attach data to the registry to support the Pylons integration.
175 182 """
176 183 registry._pylons_compat_global_config = global_config
177 184 registry._pylons_compat_settings = settings
178 185
179 186
180 187 def webob_to_pyramid_http_response(webob_response):
181 188 ResponseClass = httpexceptions.status_map[webob_response.status_int]
182 189 pyramid_response = ResponseClass(webob_response.status)
183 190 pyramid_response.status = webob_response.status
184 191 pyramid_response.headers.update(webob_response.headers)
185 192 if pyramid_response.headers['content-type'] == 'text/html':
186 193 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
187 194 return pyramid_response
188 195
189 196
190 197 def error_handler(exception, request):
191 198 # TODO: dan: replace the old pylons error controller with this
192 199 from rhodecode.model.settings import SettingsModel
193 200 from rhodecode.lib.utils2 import AttributeDict
194 201
195 202 try:
196 203 rc_config = SettingsModel().get_all_settings()
197 204 except Exception:
198 205 log.exception('failed to fetch settings')
199 206 rc_config = {}
200 207
201 208 base_response = HTTPInternalServerError()
202 209 # prefer original exception for the response since it may have headers set
203 210 if isinstance(exception, HTTPError):
204 211 base_response = exception
205 212
206 213 c = AttributeDict()
207 214 c.error_message = base_response.status
208 215 c.error_explanation = base_response.explanation or str(base_response)
209 216 c.visual = AttributeDict()
210 217
211 218 c.visual.rhodecode_support_url = (
212 219 request.registry.settings.get('rhodecode_support_url') or
213 220 request.route_url('rhodecode_support')
214 221 )
215 222 c.redirect_time = 0
216 223 c.rhodecode_name = rc_config.get('rhodecode_title', '')
217 224 if not c.rhodecode_name:
218 225 c.rhodecode_name = 'Rhodecode'
219 226
220 227 response = render_to_response(
221 228 '/errors/error_document.html', {'c': c}, request=request,
222 229 response=base_response)
223 230
224 231 return response
225 232
226 233
227 234 def includeme(config):
228 235 settings = config.registry.settings
229 236
230 237 # plugin information
231 config.registry.rhodecode_plugins = {}
238 config.registry.rhodecode_plugins = OrderedDict()
232 239
233 240 config.add_directive(
234 241 'register_rhodecode_plugin', register_rhodecode_plugin)
235 242
236 243 if asbool(settings.get('appenlight', 'false')):
237 244 config.include('appenlight_client.ext.pyramid_tween')
238 245
239 246 # Includes which are required. The application would fail without them.
240 247 config.include('pyramid_mako')
241 248 config.include('pyramid_beaker')
249 config.include('rhodecode.channelstream')
242 250 config.include('rhodecode.admin')
243 251 config.include('rhodecode.authentication')
244 252 config.include('rhodecode.integrations')
245 253 config.include('rhodecode.login')
246 254 config.include('rhodecode.tweens')
247 255 config.include('rhodecode.api')
248 256 config.add_route(
249 257 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
250 258
251 259 # Set the authorization policy.
252 260 authz_policy = ACLAuthorizationPolicy()
253 261 config.set_authorization_policy(authz_policy)
254 262
255 263 # Set the default renderer for HTML templates to mako.
256 264 config.add_mako_renderer('.html')
257 265
258 266 # include RhodeCode plugins
259 267 includes = aslist(settings.get('rhodecode.includes', []))
260 268 for inc in includes:
261 269 config.include(inc)
262 270
263 271 pylons_app = make_app(
264 272 config.registry._pylons_compat_global_config,
265 273 **config.registry._pylons_compat_settings)
266 274 config.registry._pylons_compat_config = pylons_app.config
267 275
268 276 pylons_app_as_view = wsgiapp(pylons_app)
269 277
270 278 # Protect from VCS Server error related pages when server is not available
271 279 vcs_server_enabled = asbool(settings.get('vcs.server.enable', 'true'))
272 280 if not vcs_server_enabled:
273 281 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
274 282
275 283
276 284 def pylons_app_with_error_handler(context, request):
277 285 """
278 286 Handle exceptions from rc pylons app:
279 287
280 288 - old webob type exceptions get converted to pyramid exceptions
281 289 - pyramid exceptions are passed to the error handler view
282 290 """
283 291 try:
284 292 response = pylons_app_as_view(context, request)
285 293 if 400 <= response.status_int <= 599: # webob type error responses
286 294 return error_handler(
287 295 webob_to_pyramid_http_response(response), request)
288 296 except HTTPError as e: # pyramid type exceptions
289 297 return error_handler(e, request)
290 298 except Exception:
291 299 if settings.get('debugtoolbar.enabled', False):
292 300 raise
293 301 return error_handler(HTTPInternalServerError(), request)
294 302 return response
295 303
296 304 # This is the glue which allows us to migrate in chunks. By registering the
297 305 # pylons based application as the "Not Found" view in Pyramid, we will
298 306 # fallback to the old application each time the new one does not yet know
299 307 # how to handle a request.
300 308 config.add_notfound_view(pylons_app_with_error_handler)
301 309
302 310 if not settings.get('debugtoolbar.enabled', False):
303 311 # if no toolbar, then any exception gets caught and rendered
304 312 config.add_view(error_handler, context=Exception)
305 313
306 314 config.add_view(error_handler, context=HTTPError)
307 315
308 316
309 317 def includeme_first(config):
310 318 # redirect automatic browser favicon.ico requests to correct place
311 319 def favicon_redirect(context, request):
312 320 return redirect(
313 321 request.static_url('rhodecode:public/images/favicon.ico'))
314 322
315 323 config.add_view(favicon_redirect, route_name='favicon')
316 324 config.add_route('favicon', '/favicon.ico')
317 325
318 326 config.add_static_view(
319 327 '_static/deform', 'deform:static')
320 328 config.add_static_view(
321 329 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
322 330
323 331 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
324 332 """
325 333 Apply outer WSGI middlewares around the application.
326 334
327 335 Part of this has been moved up from the Pylons layer, so that the
328 336 data is also available if old Pylons code is hit through an already ported
329 337 view.
330 338 """
331 339 settings = config.registry.settings
332 340
333 341 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
334 342 pyramid_app = HttpsFixup(pyramid_app, settings)
335 343
336 344 # Add RoutesMiddleware to support the pylons compatibility tween during
337 345 # migration to pyramid.
338 346 pyramid_app = SkippableRoutesMiddleware(
339 347 pyramid_app, config.registry._pylons_compat_config['routes.map'],
340 348 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
341 349
342 350 if asbool(settings.get('appenlight', 'false')):
343 351 pyramid_app, _ = wrap_in_appenlight_if_enabled(
344 352 pyramid_app, config.registry._pylons_compat_config)
345 353
346 354 if asbool(settings.get('gzip_responses', 'true')):
347 355 pyramid_app = make_gzip_middleware(
348 356 pyramid_app, settings, compress_level=1)
349 357
350 358 return pyramid_app
351 359
352 360
353 361 def sanitize_settings_and_apply_defaults(settings):
354 362 """
355 363 Applies settings defaults and does all type conversion.
356 364
357 365 We would move all settings parsing and preparation into this place, so that
358 366 we have only one place left which deals with this part. The remaining parts
359 367 of the application would start to rely fully on well prepared settings.
360 368
361 369 This piece would later be split up per topic to avoid a big fat monster
362 370 function.
363 371 """
364 372
365 373 # Pyramid's mako renderer has to search in the templates folder so that the
366 374 # old templates still work. Ported and new templates are expected to use
367 375 # real asset specifications for the includes.
368 376 mako_directories = settings.setdefault('mako.directories', [
369 377 # Base templates of the original Pylons application
370 378 'rhodecode:templates',
371 379 ])
372 380 log.debug(
373 381 "Using the following Mako template directories: %s",
374 382 mako_directories)
375 383
376 384 # Default includes, possible to change as a user
377 385 pyramid_includes = settings.setdefault('pyramid.includes', [
378 386 'rhodecode.lib.middleware.request_wrapper',
379 387 ])
380 388 log.debug(
381 389 "Using the following pyramid.includes: %s",
382 390 pyramid_includes)
383 391
384 392 # TODO: johbo: Re-think this, usually the call to config.include
385 393 # should allow to pass in a prefix.
386 394 settings.setdefault('rhodecode.api.url', '/_admin/api')
387 395
388 396 _bool_setting(settings, 'vcs.server.enable', 'true')
389 397 _bool_setting(settings, 'is_test', 'false')
390 398
391 399 return settings
392 400
393 401
394 402 def _bool_setting(settings, name, default):
395 403 settings[name] = asbool(settings.get(name, default))
@@ -1,1155 +1,1161 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 from rhodecode.config import routing_links
36 36
37 37 # prefix for non repository related links needs to be prefixed with `/`
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41 # Default requirements for URL parts
42 42 URL_NAME_REQUIREMENTS = {
43 43 # group name can have a slash in them, but they must not end with a slash
44 44 'group_name': r'.*?[^/]',
45 45 # repo names can have a slash in them, but they must not end with a slash
46 46 'repo_name': r'.*?[^/]',
47 47 # file path eats up everything at the end
48 48 'f_path': r'.*',
49 49 # reference types
50 50 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
51 51 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
52 52 }
53 53
54 54
55 55 def add_route_requirements(route_path, requirements):
56 56 """
57 57 Adds regex requirements to pyramid routes using a mapping dict
58 58
59 59 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
60 60 '/{action}/{id:\d+}'
61 61
62 62 """
63 63 for key, regex in requirements.items():
64 64 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
65 65 return route_path
66 66
67 67
68 68 class JSRoutesMapper(Mapper):
69 69 """
70 70 Wrapper for routes.Mapper to make pyroutes compatible url definitions
71 71 """
72 72 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
73 73 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
74 74 def __init__(self, *args, **kw):
75 75 super(JSRoutesMapper, self).__init__(*args, **kw)
76 76 self._jsroutes = []
77 77
78 78 def connect(self, *args, **kw):
79 79 """
80 80 Wrapper for connect to take an extra argument jsroute=True
81 81
82 82 :param jsroute: boolean, if True will add the route to the pyroutes list
83 83 """
84 84 if kw.pop('jsroute', False):
85 85 if not self._named_route_regex.match(args[0]):
86 86 raise Exception('only named routes can be added to pyroutes')
87 87 self._jsroutes.append(args[0])
88 88
89 89 super(JSRoutesMapper, self).connect(*args, **kw)
90 90
91 91 def _extract_route_information(self, route):
92 92 """
93 93 Convert a route into tuple(name, path, args), eg:
94 94 ('user_profile', '/profile/%(username)s', ['username'])
95 95 """
96 96 routepath = route.routepath
97 97 def replace(matchobj):
98 98 if matchobj.group(1):
99 99 return "%%(%s)s" % matchobj.group(1).split(':')[0]
100 100 else:
101 101 return "%%(%s)s" % matchobj.group(2)
102 102
103 103 routepath = self._argument_prog.sub(replace, routepath)
104 104 return (
105 105 route.name,
106 106 routepath,
107 107 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
108 108 for arg in self._argument_prog.findall(route.routepath)]
109 109 )
110 110
111 111 def jsroutes(self):
112 112 """
113 113 Return a list of pyroutes.js compatible routes
114 114 """
115 115 for route_name in self._jsroutes:
116 116 yield self._extract_route_information(self._routenames[route_name])
117 117
118 118
119 119 def make_map(config):
120 120 """Create, configure and return the routes Mapper"""
121 121 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
122 122 always_scan=config['debug'])
123 123 rmap.minimization = False
124 124 rmap.explicit = False
125 125
126 126 from rhodecode.lib.utils2 import str2bool
127 127 from rhodecode.model import repo, repo_group
128 128
129 129 def check_repo(environ, match_dict):
130 130 """
131 131 check for valid repository for proper 404 handling
132 132
133 133 :param environ:
134 134 :param match_dict:
135 135 """
136 136 repo_name = match_dict.get('repo_name')
137 137
138 138 if match_dict.get('f_path'):
139 139 # fix for multiple initial slashes that causes errors
140 140 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
141 141 repo_model = repo.RepoModel()
142 142 by_name_match = repo_model.get_by_repo_name(repo_name)
143 143 # if we match quickly from database, short circuit the operation,
144 144 # and validate repo based on the type.
145 145 if by_name_match:
146 146 return True
147 147
148 148 by_id_match = repo_model.get_repo_by_id(repo_name)
149 149 if by_id_match:
150 150 repo_name = by_id_match.repo_name
151 151 match_dict['repo_name'] = repo_name
152 152 return True
153 153
154 154 return False
155 155
156 156 def check_group(environ, match_dict):
157 157 """
158 158 check for valid repository group path for proper 404 handling
159 159
160 160 :param environ:
161 161 :param match_dict:
162 162 """
163 163 repo_group_name = match_dict.get('group_name')
164 164 repo_group_model = repo_group.RepoGroupModel()
165 165 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
166 166 if by_name_match:
167 167 return True
168 168
169 169 return False
170 170
171 171 def check_user_group(environ, match_dict):
172 172 """
173 173 check for valid user group for proper 404 handling
174 174
175 175 :param environ:
176 176 :param match_dict:
177 177 """
178 178 return True
179 179
180 180 def check_int(environ, match_dict):
181 181 return match_dict.get('id').isdigit()
182 182
183 183
184 184 #==========================================================================
185 185 # CUSTOM ROUTES HERE
186 186 #==========================================================================
187 187
188 188 # MAIN PAGE
189 189 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
190 190 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
191 191 action='goto_switcher_data')
192 192 rmap.connect('repo_list_data', '/_repos', controller='home',
193 193 action='repo_list_data')
194 194
195 195 rmap.connect('user_autocomplete_data', '/_users', controller='home',
196 196 action='user_autocomplete_data', jsroute=True)
197 197 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
198 198 action='user_group_autocomplete_data')
199 199
200 200 rmap.connect(
201 201 'user_profile', '/_profiles/{username}', controller='users',
202 202 action='user_profile')
203 203
204 204 # TODO: johbo: Static links, to be replaced by our redirection mechanism
205 205 rmap.connect('rst_help',
206 206 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
207 207 _static=True)
208 208 rmap.connect('markdown_help',
209 209 'http://daringfireball.net/projects/markdown/syntax',
210 210 _static=True)
211 211 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
212 212 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
213 213 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
214 214 # TODO: anderson - making this a static link since redirect won't play
215 215 # nice with POST requests
216 216 rmap.connect('enterprise_license_convert_from_old',
217 217 'https://rhodecode.com/u/license-upgrade',
218 218 _static=True)
219 219
220 220 routing_links.connect_redirection_links(rmap)
221 221
222 222 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
223 223 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
224 224
225 225 # ADMIN REPOSITORY ROUTES
226 226 with rmap.submapper(path_prefix=ADMIN_PREFIX,
227 227 controller='admin/repos') as m:
228 228 m.connect('repos', '/repos',
229 229 action='create', conditions={'method': ['POST']})
230 230 m.connect('repos', '/repos',
231 231 action='index', conditions={'method': ['GET']})
232 232 m.connect('new_repo', '/create_repository', jsroute=True,
233 233 action='create_repository', conditions={'method': ['GET']})
234 234 m.connect('/repos/{repo_name}',
235 235 action='update', conditions={'method': ['PUT'],
236 236 'function': check_repo},
237 237 requirements=URL_NAME_REQUIREMENTS)
238 238 m.connect('delete_repo', '/repos/{repo_name}',
239 239 action='delete', conditions={'method': ['DELETE']},
240 240 requirements=URL_NAME_REQUIREMENTS)
241 241 m.connect('repo', '/repos/{repo_name}',
242 242 action='show', conditions={'method': ['GET'],
243 243 'function': check_repo},
244 244 requirements=URL_NAME_REQUIREMENTS)
245 245
246 246 # ADMIN REPOSITORY GROUPS ROUTES
247 247 with rmap.submapper(path_prefix=ADMIN_PREFIX,
248 248 controller='admin/repo_groups') as m:
249 249 m.connect('repo_groups', '/repo_groups',
250 250 action='create', conditions={'method': ['POST']})
251 251 m.connect('repo_groups', '/repo_groups',
252 252 action='index', conditions={'method': ['GET']})
253 253 m.connect('new_repo_group', '/repo_groups/new',
254 254 action='new', conditions={'method': ['GET']})
255 255 m.connect('update_repo_group', '/repo_groups/{group_name}',
256 256 action='update', conditions={'method': ['PUT'],
257 257 'function': check_group},
258 258 requirements=URL_NAME_REQUIREMENTS)
259 259
260 260 # EXTRAS REPO GROUP ROUTES
261 261 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
262 262 action='edit',
263 263 conditions={'method': ['GET'], 'function': check_group},
264 264 requirements=URL_NAME_REQUIREMENTS)
265 265 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
266 266 action='edit',
267 267 conditions={'method': ['PUT'], 'function': check_group},
268 268 requirements=URL_NAME_REQUIREMENTS)
269 269
270 270 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
271 271 action='edit_repo_group_advanced',
272 272 conditions={'method': ['GET'], 'function': check_group},
273 273 requirements=URL_NAME_REQUIREMENTS)
274 274 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
275 275 action='edit_repo_group_advanced',
276 276 conditions={'method': ['PUT'], 'function': check_group},
277 277 requirements=URL_NAME_REQUIREMENTS)
278 278
279 279 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
280 280 action='edit_repo_group_perms',
281 281 conditions={'method': ['GET'], 'function': check_group},
282 282 requirements=URL_NAME_REQUIREMENTS)
283 283 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
284 284 action='update_perms',
285 285 conditions={'method': ['PUT'], 'function': check_group},
286 286 requirements=URL_NAME_REQUIREMENTS)
287 287
288 288 m.connect('delete_repo_group', '/repo_groups/{group_name}',
289 289 action='delete', conditions={'method': ['DELETE'],
290 290 'function': check_group},
291 291 requirements=URL_NAME_REQUIREMENTS)
292 292
293 293 # ADMIN USER ROUTES
294 294 with rmap.submapper(path_prefix=ADMIN_PREFIX,
295 295 controller='admin/users') as m:
296 296 m.connect('users', '/users',
297 297 action='create', conditions={'method': ['POST']})
298 298 m.connect('users', '/users',
299 299 action='index', conditions={'method': ['GET']})
300 300 m.connect('new_user', '/users/new',
301 301 action='new', conditions={'method': ['GET']})
302 302 m.connect('update_user', '/users/{user_id}',
303 303 action='update', conditions={'method': ['PUT']})
304 304 m.connect('delete_user', '/users/{user_id}',
305 305 action='delete', conditions={'method': ['DELETE']})
306 306 m.connect('edit_user', '/users/{user_id}/edit',
307 307 action='edit', conditions={'method': ['GET']})
308 308 m.connect('user', '/users/{user_id}',
309 309 action='show', conditions={'method': ['GET']})
310 310 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
311 311 action='reset_password', conditions={'method': ['POST']})
312 312 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
313 313 action='create_personal_repo_group', conditions={'method': ['POST']})
314 314
315 315 # EXTRAS USER ROUTES
316 316 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
317 317 action='edit_advanced', conditions={'method': ['GET']})
318 318 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
319 319 action='update_advanced', conditions={'method': ['PUT']})
320 320
321 321 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
322 322 action='edit_auth_tokens', conditions={'method': ['GET']})
323 323 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
324 324 action='add_auth_token', conditions={'method': ['PUT']})
325 325 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
326 326 action='delete_auth_token', conditions={'method': ['DELETE']})
327 327
328 328 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
329 329 action='edit_global_perms', conditions={'method': ['GET']})
330 330 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
331 331 action='update_global_perms', conditions={'method': ['PUT']})
332 332
333 333 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
334 334 action='edit_perms_summary', conditions={'method': ['GET']})
335 335
336 336 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
337 337 action='edit_emails', conditions={'method': ['GET']})
338 338 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
339 339 action='add_email', conditions={'method': ['PUT']})
340 340 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
341 341 action='delete_email', conditions={'method': ['DELETE']})
342 342
343 343 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
344 344 action='edit_ips', conditions={'method': ['GET']})
345 345 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
346 346 action='add_ip', conditions={'method': ['PUT']})
347 347 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
348 348 action='delete_ip', conditions={'method': ['DELETE']})
349 349
350 350 # ADMIN USER GROUPS REST ROUTES
351 351 with rmap.submapper(path_prefix=ADMIN_PREFIX,
352 352 controller='admin/user_groups') as m:
353 353 m.connect('users_groups', '/user_groups',
354 354 action='create', conditions={'method': ['POST']})
355 355 m.connect('users_groups', '/user_groups',
356 356 action='index', conditions={'method': ['GET']})
357 357 m.connect('new_users_group', '/user_groups/new',
358 358 action='new', conditions={'method': ['GET']})
359 359 m.connect('update_users_group', '/user_groups/{user_group_id}',
360 360 action='update', conditions={'method': ['PUT']})
361 361 m.connect('delete_users_group', '/user_groups/{user_group_id}',
362 362 action='delete', conditions={'method': ['DELETE']})
363 363 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
364 364 action='edit', conditions={'method': ['GET']},
365 365 function=check_user_group)
366 366
367 367 # EXTRAS USER GROUP ROUTES
368 368 m.connect('edit_user_group_global_perms',
369 369 '/user_groups/{user_group_id}/edit/global_permissions',
370 370 action='edit_global_perms', conditions={'method': ['GET']})
371 371 m.connect('edit_user_group_global_perms',
372 372 '/user_groups/{user_group_id}/edit/global_permissions',
373 373 action='update_global_perms', conditions={'method': ['PUT']})
374 374 m.connect('edit_user_group_perms_summary',
375 375 '/user_groups/{user_group_id}/edit/permissions_summary',
376 376 action='edit_perms_summary', conditions={'method': ['GET']})
377 377
378 378 m.connect('edit_user_group_perms',
379 379 '/user_groups/{user_group_id}/edit/permissions',
380 380 action='edit_perms', conditions={'method': ['GET']})
381 381 m.connect('edit_user_group_perms',
382 382 '/user_groups/{user_group_id}/edit/permissions',
383 383 action='update_perms', conditions={'method': ['PUT']})
384 384
385 385 m.connect('edit_user_group_advanced',
386 386 '/user_groups/{user_group_id}/edit/advanced',
387 387 action='edit_advanced', conditions={'method': ['GET']})
388 388
389 389 m.connect('edit_user_group_members',
390 390 '/user_groups/{user_group_id}/edit/members', jsroute=True,
391 391 action='edit_members', conditions={'method': ['GET']})
392 392
393 393 # ADMIN PERMISSIONS ROUTES
394 394 with rmap.submapper(path_prefix=ADMIN_PREFIX,
395 395 controller='admin/permissions') as m:
396 396 m.connect('admin_permissions_application', '/permissions/application',
397 397 action='permission_application_update', conditions={'method': ['POST']})
398 398 m.connect('admin_permissions_application', '/permissions/application',
399 399 action='permission_application', conditions={'method': ['GET']})
400 400
401 401 m.connect('admin_permissions_global', '/permissions/global',
402 402 action='permission_global_update', conditions={'method': ['POST']})
403 403 m.connect('admin_permissions_global', '/permissions/global',
404 404 action='permission_global', conditions={'method': ['GET']})
405 405
406 406 m.connect('admin_permissions_object', '/permissions/object',
407 407 action='permission_objects_update', conditions={'method': ['POST']})
408 408 m.connect('admin_permissions_object', '/permissions/object',
409 409 action='permission_objects', conditions={'method': ['GET']})
410 410
411 411 m.connect('admin_permissions_ips', '/permissions/ips',
412 412 action='permission_ips', conditions={'method': ['POST']})
413 413 m.connect('admin_permissions_ips', '/permissions/ips',
414 414 action='permission_ips', conditions={'method': ['GET']})
415 415
416 416 m.connect('admin_permissions_overview', '/permissions/overview',
417 417 action='permission_perms', conditions={'method': ['GET']})
418 418
419 419 # ADMIN DEFAULTS REST ROUTES
420 420 with rmap.submapper(path_prefix=ADMIN_PREFIX,
421 421 controller='admin/defaults') as m:
422 422 m.connect('admin_defaults_repositories', '/defaults/repositories',
423 423 action='update_repository_defaults', conditions={'method': ['POST']})
424 424 m.connect('admin_defaults_repositories', '/defaults/repositories',
425 425 action='index', conditions={'method': ['GET']})
426 426
427 427 # ADMIN DEBUG STYLE ROUTES
428 428 if str2bool(config.get('debug_style')):
429 429 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
430 430 controller='debug_style') as m:
431 431 m.connect('debug_style_home', '',
432 432 action='index', conditions={'method': ['GET']})
433 433 m.connect('debug_style_template', '/t/{t_path}',
434 434 action='template', conditions={'method': ['GET']})
435 435
436 436 # ADMIN SETTINGS ROUTES
437 437 with rmap.submapper(path_prefix=ADMIN_PREFIX,
438 438 controller='admin/settings') as m:
439 439
440 440 # default
441 441 m.connect('admin_settings', '/settings',
442 442 action='settings_global_update',
443 443 conditions={'method': ['POST']})
444 444 m.connect('admin_settings', '/settings',
445 445 action='settings_global', conditions={'method': ['GET']})
446 446
447 447 m.connect('admin_settings_vcs', '/settings/vcs',
448 448 action='settings_vcs_update',
449 449 conditions={'method': ['POST']})
450 450 m.connect('admin_settings_vcs', '/settings/vcs',
451 451 action='settings_vcs',
452 452 conditions={'method': ['GET']})
453 453 m.connect('admin_settings_vcs', '/settings/vcs',
454 454 action='delete_svn_pattern',
455 455 conditions={'method': ['DELETE']})
456 456
457 457 m.connect('admin_settings_mapping', '/settings/mapping',
458 458 action='settings_mapping_update',
459 459 conditions={'method': ['POST']})
460 460 m.connect('admin_settings_mapping', '/settings/mapping',
461 461 action='settings_mapping', conditions={'method': ['GET']})
462 462
463 463 m.connect('admin_settings_global', '/settings/global',
464 464 action='settings_global_update',
465 465 conditions={'method': ['POST']})
466 466 m.connect('admin_settings_global', '/settings/global',
467 467 action='settings_global', conditions={'method': ['GET']})
468 468
469 469 m.connect('admin_settings_visual', '/settings/visual',
470 470 action='settings_visual_update',
471 471 conditions={'method': ['POST']})
472 472 m.connect('admin_settings_visual', '/settings/visual',
473 473 action='settings_visual', conditions={'method': ['GET']})
474 474
475 475 m.connect('admin_settings_issuetracker',
476 476 '/settings/issue-tracker', action='settings_issuetracker',
477 477 conditions={'method': ['GET']})
478 478 m.connect('admin_settings_issuetracker_save',
479 479 '/settings/issue-tracker/save',
480 480 action='settings_issuetracker_save',
481 481 conditions={'method': ['POST']})
482 482 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
483 483 action='settings_issuetracker_test',
484 484 conditions={'method': ['POST']})
485 485 m.connect('admin_issuetracker_delete',
486 486 '/settings/issue-tracker/delete',
487 487 action='settings_issuetracker_delete',
488 488 conditions={'method': ['DELETE']})
489 489
490 490 m.connect('admin_settings_email', '/settings/email',
491 491 action='settings_email_update',
492 492 conditions={'method': ['POST']})
493 493 m.connect('admin_settings_email', '/settings/email',
494 494 action='settings_email', conditions={'method': ['GET']})
495 495
496 496 m.connect('admin_settings_hooks', '/settings/hooks',
497 497 action='settings_hooks_update',
498 498 conditions={'method': ['POST', 'DELETE']})
499 499 m.connect('admin_settings_hooks', '/settings/hooks',
500 500 action='settings_hooks', conditions={'method': ['GET']})
501 501
502 502 m.connect('admin_settings_search', '/settings/search',
503 503 action='settings_search', conditions={'method': ['GET']})
504 504
505 505 m.connect('admin_settings_system', '/settings/system',
506 506 action='settings_system', conditions={'method': ['GET']})
507 507
508 508 m.connect('admin_settings_system_update', '/settings/system/updates',
509 509 action='settings_system_update', conditions={'method': ['GET']})
510 510
511 511 m.connect('admin_settings_supervisor', '/settings/supervisor',
512 512 action='settings_supervisor', conditions={'method': ['GET']})
513 513 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
514 514 action='settings_supervisor_log', conditions={'method': ['GET']})
515 515
516 516 m.connect('admin_settings_labs', '/settings/labs',
517 517 action='settings_labs_update',
518 518 conditions={'method': ['POST']})
519 519 m.connect('admin_settings_labs', '/settings/labs',
520 520 action='settings_labs', conditions={'method': ['GET']})
521 521
522 522 # ADMIN MY ACCOUNT
523 523 with rmap.submapper(path_prefix=ADMIN_PREFIX,
524 524 controller='admin/my_account') as m:
525 525
526 526 m.connect('my_account', '/my_account',
527 527 action='my_account', conditions={'method': ['GET']})
528 528 m.connect('my_account_edit', '/my_account/edit',
529 529 action='my_account_edit', conditions={'method': ['GET']})
530 530 m.connect('my_account', '/my_account',
531 531 action='my_account_update', conditions={'method': ['POST']})
532 532
533 533 m.connect('my_account_password', '/my_account/password',
534 534 action='my_account_password', conditions={'method': ['GET']})
535 535 m.connect('my_account_password', '/my_account/password',
536 536 action='my_account_password_update', conditions={'method': ['POST']})
537 537
538 538 m.connect('my_account_repos', '/my_account/repos',
539 539 action='my_account_repos', conditions={'method': ['GET']})
540 540
541 541 m.connect('my_account_watched', '/my_account/watched',
542 542 action='my_account_watched', conditions={'method': ['GET']})
543 543
544 544 m.connect('my_account_pullrequests', '/my_account/pull_requests',
545 545 action='my_account_pullrequests', conditions={'method': ['GET']})
546 546
547 547 m.connect('my_account_perms', '/my_account/perms',
548 548 action='my_account_perms', conditions={'method': ['GET']})
549 549
550 550 m.connect('my_account_emails', '/my_account/emails',
551 551 action='my_account_emails', conditions={'method': ['GET']})
552 552 m.connect('my_account_emails', '/my_account/emails',
553 553 action='my_account_emails_add', conditions={'method': ['POST']})
554 554 m.connect('my_account_emails', '/my_account/emails',
555 555 action='my_account_emails_delete', conditions={'method': ['DELETE']})
556 556
557 557 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
558 558 action='my_account_auth_tokens', conditions={'method': ['GET']})
559 559 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
560 560 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
561 561 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
562 562 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
563 m.connect('my_account_notifications', '/my_account/notifications',
564 action='my_notifications',
565 conditions={'method': ['GET']})
566 m.connect('my_account_notifications_toggle_visibility',
567 '/my_account/toggle_visibility',
568 action='my_notifications_toggle_visibility',
569 conditions={'method': ['POST']})
563 570
564 571 # NOTIFICATION REST ROUTES
565 572 with rmap.submapper(path_prefix=ADMIN_PREFIX,
566 573 controller='admin/notifications') as m:
567 574 m.connect('notifications', '/notifications',
568 575 action='index', conditions={'method': ['GET']})
569 576 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
570 577 action='mark_all_read', conditions={'method': ['POST']})
571
572 578 m.connect('/notifications/{notification_id}',
573 579 action='update', conditions={'method': ['PUT']})
574 580 m.connect('/notifications/{notification_id}',
575 581 action='delete', conditions={'method': ['DELETE']})
576 582 m.connect('notification', '/notifications/{notification_id}',
577 583 action='show', conditions={'method': ['GET']})
578 584
579 585 # ADMIN GIST
580 586 with rmap.submapper(path_prefix=ADMIN_PREFIX,
581 587 controller='admin/gists') as m:
582 588 m.connect('gists', '/gists',
583 589 action='create', conditions={'method': ['POST']})
584 590 m.connect('gists', '/gists', jsroute=True,
585 591 action='index', conditions={'method': ['GET']})
586 592 m.connect('new_gist', '/gists/new', jsroute=True,
587 593 action='new', conditions={'method': ['GET']})
588 594
589 595 m.connect('/gists/{gist_id}',
590 596 action='delete', conditions={'method': ['DELETE']})
591 597 m.connect('edit_gist', '/gists/{gist_id}/edit',
592 598 action='edit_form', conditions={'method': ['GET']})
593 599 m.connect('edit_gist', '/gists/{gist_id}/edit',
594 600 action='edit', conditions={'method': ['POST']})
595 601 m.connect(
596 602 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
597 603 action='check_revision', conditions={'method': ['GET']})
598 604
599 605 m.connect('gist', '/gists/{gist_id}',
600 606 action='show', conditions={'method': ['GET']})
601 607 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
602 608 revision='tip',
603 609 action='show', conditions={'method': ['GET']})
604 610 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
605 611 revision='tip',
606 612 action='show', conditions={'method': ['GET']})
607 613 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
608 614 revision='tip',
609 615 action='show', conditions={'method': ['GET']},
610 616 requirements=URL_NAME_REQUIREMENTS)
611 617
612 618 # ADMIN MAIN PAGES
613 619 with rmap.submapper(path_prefix=ADMIN_PREFIX,
614 620 controller='admin/admin') as m:
615 621 m.connect('admin_home', '', action='index')
616 622 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
617 623 action='add_repo')
618 624 m.connect(
619 625 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
620 626 action='pull_requests')
621 627 m.connect(
622 628 'pull_requests_global', '/pull-requests/{pull_request_id:[0-9]+}',
623 629 action='pull_requests')
624 630
625 631
626 632 # USER JOURNAL
627 633 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
628 634 controller='journal', action='index')
629 635 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
630 636 controller='journal', action='journal_rss')
631 637 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
632 638 controller='journal', action='journal_atom')
633 639
634 640 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
635 641 controller='journal', action='public_journal')
636 642
637 643 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
638 644 controller='journal', action='public_journal_rss')
639 645
640 646 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
641 647 controller='journal', action='public_journal_rss')
642 648
643 649 rmap.connect('public_journal_atom',
644 650 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
645 651 action='public_journal_atom')
646 652
647 653 rmap.connect('public_journal_atom_old',
648 654 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
649 655 action='public_journal_atom')
650 656
651 657 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
652 658 controller='journal', action='toggle_following', jsroute=True,
653 659 conditions={'method': ['POST']})
654 660
655 661 # FULL TEXT SEARCH
656 662 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
657 663 controller='search')
658 664 rmap.connect('search_repo_home', '/{repo_name}/search',
659 665 controller='search',
660 666 action='index',
661 667 conditions={'function': check_repo},
662 668 requirements=URL_NAME_REQUIREMENTS)
663 669
664 670 # FEEDS
665 671 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
666 672 controller='feed', action='rss',
667 673 conditions={'function': check_repo},
668 674 requirements=URL_NAME_REQUIREMENTS)
669 675
670 676 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
671 677 controller='feed', action='atom',
672 678 conditions={'function': check_repo},
673 679 requirements=URL_NAME_REQUIREMENTS)
674 680
675 681 #==========================================================================
676 682 # REPOSITORY ROUTES
677 683 #==========================================================================
678 684
679 685 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
680 686 controller='admin/repos', action='repo_creating',
681 687 requirements=URL_NAME_REQUIREMENTS)
682 688 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
683 689 controller='admin/repos', action='repo_check',
684 690 requirements=URL_NAME_REQUIREMENTS)
685 691
686 692 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
687 693 controller='summary', action='repo_stats',
688 694 conditions={'function': check_repo},
689 695 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
690 696
691 697 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
692 698 controller='summary', action='repo_refs_data', jsroute=True,
693 699 requirements=URL_NAME_REQUIREMENTS)
694 700 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
695 701 controller='summary', action='repo_refs_changelog_data',
696 702 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
697 703
698 704 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
699 705 controller='changeset', revision='tip', jsroute=True,
700 706 conditions={'function': check_repo},
701 707 requirements=URL_NAME_REQUIREMENTS)
702 708 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
703 709 controller='changeset', revision='tip', action='changeset_children',
704 710 conditions={'function': check_repo},
705 711 requirements=URL_NAME_REQUIREMENTS)
706 712 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
707 713 controller='changeset', revision='tip', action='changeset_parents',
708 714 conditions={'function': check_repo},
709 715 requirements=URL_NAME_REQUIREMENTS)
710 716
711 717 # repo edit options
712 718 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
713 719 controller='admin/repos', action='edit',
714 720 conditions={'method': ['GET'], 'function': check_repo},
715 721 requirements=URL_NAME_REQUIREMENTS)
716 722
717 723 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
718 724 jsroute=True,
719 725 controller='admin/repos', action='edit_permissions',
720 726 conditions={'method': ['GET'], 'function': check_repo},
721 727 requirements=URL_NAME_REQUIREMENTS)
722 728 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
723 729 controller='admin/repos', action='edit_permissions_update',
724 730 conditions={'method': ['PUT'], 'function': check_repo},
725 731 requirements=URL_NAME_REQUIREMENTS)
726 732
727 733 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
728 734 controller='admin/repos', action='edit_fields',
729 735 conditions={'method': ['GET'], 'function': check_repo},
730 736 requirements=URL_NAME_REQUIREMENTS)
731 737 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
732 738 controller='admin/repos', action='create_repo_field',
733 739 conditions={'method': ['PUT'], 'function': check_repo},
734 740 requirements=URL_NAME_REQUIREMENTS)
735 741 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
736 742 controller='admin/repos', action='delete_repo_field',
737 743 conditions={'method': ['DELETE'], 'function': check_repo},
738 744 requirements=URL_NAME_REQUIREMENTS)
739 745
740 746 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
741 747 controller='admin/repos', action='edit_advanced',
742 748 conditions={'method': ['GET'], 'function': check_repo},
743 749 requirements=URL_NAME_REQUIREMENTS)
744 750
745 751 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
746 752 controller='admin/repos', action='edit_advanced_locking',
747 753 conditions={'method': ['PUT'], 'function': check_repo},
748 754 requirements=URL_NAME_REQUIREMENTS)
749 755 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
750 756 controller='admin/repos', action='toggle_locking',
751 757 conditions={'method': ['GET'], 'function': check_repo},
752 758 requirements=URL_NAME_REQUIREMENTS)
753 759
754 760 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
755 761 controller='admin/repos', action='edit_advanced_journal',
756 762 conditions={'method': ['PUT'], 'function': check_repo},
757 763 requirements=URL_NAME_REQUIREMENTS)
758 764
759 765 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
760 766 controller='admin/repos', action='edit_advanced_fork',
761 767 conditions={'method': ['PUT'], 'function': check_repo},
762 768 requirements=URL_NAME_REQUIREMENTS)
763 769
764 770 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
765 771 controller='admin/repos', action='edit_caches_form',
766 772 conditions={'method': ['GET'], 'function': check_repo},
767 773 requirements=URL_NAME_REQUIREMENTS)
768 774 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
769 775 controller='admin/repos', action='edit_caches',
770 776 conditions={'method': ['PUT'], 'function': check_repo},
771 777 requirements=URL_NAME_REQUIREMENTS)
772 778
773 779 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
774 780 controller='admin/repos', action='edit_remote_form',
775 781 conditions={'method': ['GET'], 'function': check_repo},
776 782 requirements=URL_NAME_REQUIREMENTS)
777 783 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
778 784 controller='admin/repos', action='edit_remote',
779 785 conditions={'method': ['PUT'], 'function': check_repo},
780 786 requirements=URL_NAME_REQUIREMENTS)
781 787
782 788 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
783 789 controller='admin/repos', action='edit_statistics_form',
784 790 conditions={'method': ['GET'], 'function': check_repo},
785 791 requirements=URL_NAME_REQUIREMENTS)
786 792 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
787 793 controller='admin/repos', action='edit_statistics',
788 794 conditions={'method': ['PUT'], 'function': check_repo},
789 795 requirements=URL_NAME_REQUIREMENTS)
790 796 rmap.connect('repo_settings_issuetracker',
791 797 '/{repo_name}/settings/issue-tracker',
792 798 controller='admin/repos', action='repo_issuetracker',
793 799 conditions={'method': ['GET'], 'function': check_repo},
794 800 requirements=URL_NAME_REQUIREMENTS)
795 801 rmap.connect('repo_issuetracker_test',
796 802 '/{repo_name}/settings/issue-tracker/test',
797 803 controller='admin/repos', action='repo_issuetracker_test',
798 804 conditions={'method': ['POST'], 'function': check_repo},
799 805 requirements=URL_NAME_REQUIREMENTS)
800 806 rmap.connect('repo_issuetracker_delete',
801 807 '/{repo_name}/settings/issue-tracker/delete',
802 808 controller='admin/repos', action='repo_issuetracker_delete',
803 809 conditions={'method': ['DELETE'], 'function': check_repo},
804 810 requirements=URL_NAME_REQUIREMENTS)
805 811 rmap.connect('repo_issuetracker_save',
806 812 '/{repo_name}/settings/issue-tracker/save',
807 813 controller='admin/repos', action='repo_issuetracker_save',
808 814 conditions={'method': ['POST'], 'function': check_repo},
809 815 requirements=URL_NAME_REQUIREMENTS)
810 816 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
811 817 controller='admin/repos', action='repo_settings_vcs_update',
812 818 conditions={'method': ['POST'], 'function': check_repo},
813 819 requirements=URL_NAME_REQUIREMENTS)
814 820 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
815 821 controller='admin/repos', action='repo_settings_vcs',
816 822 conditions={'method': ['GET'], 'function': check_repo},
817 823 requirements=URL_NAME_REQUIREMENTS)
818 824 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
819 825 controller='admin/repos', action='repo_delete_svn_pattern',
820 826 conditions={'method': ['DELETE'], 'function': check_repo},
821 827 requirements=URL_NAME_REQUIREMENTS)
822 828
823 829 # still working url for backward compat.
824 830 rmap.connect('raw_changeset_home_depraced',
825 831 '/{repo_name}/raw-changeset/{revision}',
826 832 controller='changeset', action='changeset_raw',
827 833 revision='tip', conditions={'function': check_repo},
828 834 requirements=URL_NAME_REQUIREMENTS)
829 835
830 836 # new URLs
831 837 rmap.connect('changeset_raw_home',
832 838 '/{repo_name}/changeset-diff/{revision}',
833 839 controller='changeset', action='changeset_raw',
834 840 revision='tip', conditions={'function': check_repo},
835 841 requirements=URL_NAME_REQUIREMENTS)
836 842
837 843 rmap.connect('changeset_patch_home',
838 844 '/{repo_name}/changeset-patch/{revision}',
839 845 controller='changeset', action='changeset_patch',
840 846 revision='tip', conditions={'function': check_repo},
841 847 requirements=URL_NAME_REQUIREMENTS)
842 848
843 849 rmap.connect('changeset_download_home',
844 850 '/{repo_name}/changeset-download/{revision}',
845 851 controller='changeset', action='changeset_download',
846 852 revision='tip', conditions={'function': check_repo},
847 853 requirements=URL_NAME_REQUIREMENTS)
848 854
849 855 rmap.connect('changeset_comment',
850 856 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
851 857 controller='changeset', revision='tip', action='comment',
852 858 conditions={'function': check_repo},
853 859 requirements=URL_NAME_REQUIREMENTS)
854 860
855 861 rmap.connect('changeset_comment_preview',
856 862 '/{repo_name}/changeset/comment/preview', jsroute=True,
857 863 controller='changeset', action='preview_comment',
858 864 conditions={'function': check_repo, 'method': ['POST']},
859 865 requirements=URL_NAME_REQUIREMENTS)
860 866
861 867 rmap.connect('changeset_comment_delete',
862 868 '/{repo_name}/changeset/comment/{comment_id}/delete',
863 869 controller='changeset', action='delete_comment',
864 870 conditions={'function': check_repo, 'method': ['DELETE']},
865 871 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
866 872
867 873 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
868 874 controller='changeset', action='changeset_info',
869 875 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
870 876
871 877 rmap.connect('compare_home',
872 878 '/{repo_name}/compare',
873 879 controller='compare', action='index',
874 880 conditions={'function': check_repo},
875 881 requirements=URL_NAME_REQUIREMENTS)
876 882
877 883 rmap.connect('compare_url',
878 884 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
879 885 controller='compare', action='compare',
880 886 conditions={'function': check_repo},
881 887 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
882 888
883 889 rmap.connect('pullrequest_home',
884 890 '/{repo_name}/pull-request/new', controller='pullrequests',
885 891 action='index', conditions={'function': check_repo,
886 892 'method': ['GET']},
887 893 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
888 894
889 895 rmap.connect('pullrequest',
890 896 '/{repo_name}/pull-request/new', controller='pullrequests',
891 897 action='create', conditions={'function': check_repo,
892 898 'method': ['POST']},
893 899 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
894 900
895 901 rmap.connect('pullrequest_repo_refs',
896 902 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
897 903 controller='pullrequests',
898 904 action='get_repo_refs',
899 905 conditions={'function': check_repo, 'method': ['GET']},
900 906 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
901 907
902 908 rmap.connect('pullrequest_repo_destinations',
903 909 '/{repo_name}/pull-request/repo-destinations',
904 910 controller='pullrequests',
905 911 action='get_repo_destinations',
906 912 conditions={'function': check_repo, 'method': ['GET']},
907 913 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
908 914
909 915 rmap.connect('pullrequest_show',
910 916 '/{repo_name}/pull-request/{pull_request_id}',
911 917 controller='pullrequests',
912 918 action='show', conditions={'function': check_repo,
913 919 'method': ['GET']},
914 920 requirements=URL_NAME_REQUIREMENTS)
915 921
916 922 rmap.connect('pullrequest_update',
917 923 '/{repo_name}/pull-request/{pull_request_id}',
918 924 controller='pullrequests',
919 925 action='update', conditions={'function': check_repo,
920 926 'method': ['PUT']},
921 927 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
922 928
923 929 rmap.connect('pullrequest_merge',
924 930 '/{repo_name}/pull-request/{pull_request_id}',
925 931 controller='pullrequests',
926 932 action='merge', conditions={'function': check_repo,
927 933 'method': ['POST']},
928 934 requirements=URL_NAME_REQUIREMENTS)
929 935
930 936 rmap.connect('pullrequest_delete',
931 937 '/{repo_name}/pull-request/{pull_request_id}',
932 938 controller='pullrequests',
933 939 action='delete', conditions={'function': check_repo,
934 940 'method': ['DELETE']},
935 941 requirements=URL_NAME_REQUIREMENTS)
936 942
937 943 rmap.connect('pullrequest_show_all',
938 944 '/{repo_name}/pull-request',
939 945 controller='pullrequests',
940 946 action='show_all', conditions={'function': check_repo,
941 947 'method': ['GET']},
942 948 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
943 949
944 950 rmap.connect('pullrequest_comment',
945 951 '/{repo_name}/pull-request-comment/{pull_request_id}',
946 952 controller='pullrequests',
947 953 action='comment', conditions={'function': check_repo,
948 954 'method': ['POST']},
949 955 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
950 956
951 957 rmap.connect('pullrequest_comment_delete',
952 958 '/{repo_name}/pull-request-comment/{comment_id}/delete',
953 959 controller='pullrequests', action='delete_comment',
954 960 conditions={'function': check_repo, 'method': ['DELETE']},
955 961 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
956 962
957 963 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
958 964 controller='summary', conditions={'function': check_repo},
959 965 requirements=URL_NAME_REQUIREMENTS)
960 966
961 967 rmap.connect('branches_home', '/{repo_name}/branches',
962 968 controller='branches', conditions={'function': check_repo},
963 969 requirements=URL_NAME_REQUIREMENTS)
964 970
965 971 rmap.connect('tags_home', '/{repo_name}/tags',
966 972 controller='tags', conditions={'function': check_repo},
967 973 requirements=URL_NAME_REQUIREMENTS)
968 974
969 975 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
970 976 controller='bookmarks', conditions={'function': check_repo},
971 977 requirements=URL_NAME_REQUIREMENTS)
972 978
973 979 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
974 980 controller='changelog', conditions={'function': check_repo},
975 981 requirements=URL_NAME_REQUIREMENTS)
976 982
977 983 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
978 984 controller='changelog', action='changelog_summary',
979 985 conditions={'function': check_repo},
980 986 requirements=URL_NAME_REQUIREMENTS)
981 987
982 988 rmap.connect('changelog_file_home',
983 989 '/{repo_name}/changelog/{revision}/{f_path}',
984 990 controller='changelog', f_path=None,
985 991 conditions={'function': check_repo},
986 992 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
987 993
988 994 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
989 995 controller='changelog', action='changelog_details',
990 996 conditions={'function': check_repo},
991 997 requirements=URL_NAME_REQUIREMENTS)
992 998
993 999 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
994 1000 controller='files', revision='tip', f_path='',
995 1001 conditions={'function': check_repo},
996 1002 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
997 1003
998 1004 rmap.connect('files_home_simple_catchrev',
999 1005 '/{repo_name}/files/{revision}',
1000 1006 controller='files', revision='tip', f_path='',
1001 1007 conditions={'function': check_repo},
1002 1008 requirements=URL_NAME_REQUIREMENTS)
1003 1009
1004 1010 rmap.connect('files_home_simple_catchall',
1005 1011 '/{repo_name}/files',
1006 1012 controller='files', revision='tip', f_path='',
1007 1013 conditions={'function': check_repo},
1008 1014 requirements=URL_NAME_REQUIREMENTS)
1009 1015
1010 1016 rmap.connect('files_history_home',
1011 1017 '/{repo_name}/history/{revision}/{f_path}',
1012 1018 controller='files', action='history', revision='tip', f_path='',
1013 1019 conditions={'function': check_repo},
1014 1020 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1015 1021
1016 1022 rmap.connect('files_authors_home',
1017 1023 '/{repo_name}/authors/{revision}/{f_path}',
1018 1024 controller='files', action='authors', revision='tip', f_path='',
1019 1025 conditions={'function': check_repo},
1020 1026 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1021 1027
1022 1028 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1023 1029 controller='files', action='diff', f_path='',
1024 1030 conditions={'function': check_repo},
1025 1031 requirements=URL_NAME_REQUIREMENTS)
1026 1032
1027 1033 rmap.connect('files_diff_2way_home',
1028 1034 '/{repo_name}/diff-2way/{f_path}',
1029 1035 controller='files', action='diff_2way', f_path='',
1030 1036 conditions={'function': check_repo},
1031 1037 requirements=URL_NAME_REQUIREMENTS)
1032 1038
1033 1039 rmap.connect('files_rawfile_home',
1034 1040 '/{repo_name}/rawfile/{revision}/{f_path}',
1035 1041 controller='files', action='rawfile', revision='tip',
1036 1042 f_path='', conditions={'function': check_repo},
1037 1043 requirements=URL_NAME_REQUIREMENTS)
1038 1044
1039 1045 rmap.connect('files_raw_home',
1040 1046 '/{repo_name}/raw/{revision}/{f_path}',
1041 1047 controller='files', action='raw', revision='tip', f_path='',
1042 1048 conditions={'function': check_repo},
1043 1049 requirements=URL_NAME_REQUIREMENTS)
1044 1050
1045 1051 rmap.connect('files_render_home',
1046 1052 '/{repo_name}/render/{revision}/{f_path}',
1047 1053 controller='files', action='index', revision='tip', f_path='',
1048 1054 rendered=True, conditions={'function': check_repo},
1049 1055 requirements=URL_NAME_REQUIREMENTS)
1050 1056
1051 1057 rmap.connect('files_annotate_home',
1052 1058 '/{repo_name}/annotate/{revision}/{f_path}',
1053 1059 controller='files', action='index', revision='tip',
1054 1060 f_path='', annotate=True, conditions={'function': check_repo},
1055 1061 requirements=URL_NAME_REQUIREMENTS)
1056 1062
1057 1063 rmap.connect('files_edit',
1058 1064 '/{repo_name}/edit/{revision}/{f_path}',
1059 1065 controller='files', action='edit', revision='tip',
1060 1066 f_path='',
1061 1067 conditions={'function': check_repo, 'method': ['POST']},
1062 1068 requirements=URL_NAME_REQUIREMENTS)
1063 1069
1064 1070 rmap.connect('files_edit_home',
1065 1071 '/{repo_name}/edit/{revision}/{f_path}',
1066 1072 controller='files', action='edit_home', revision='tip',
1067 1073 f_path='', conditions={'function': check_repo},
1068 1074 requirements=URL_NAME_REQUIREMENTS)
1069 1075
1070 1076 rmap.connect('files_add',
1071 1077 '/{repo_name}/add/{revision}/{f_path}',
1072 1078 controller='files', action='add', revision='tip',
1073 1079 f_path='',
1074 1080 conditions={'function': check_repo, 'method': ['POST']},
1075 1081 requirements=URL_NAME_REQUIREMENTS)
1076 1082
1077 1083 rmap.connect('files_add_home',
1078 1084 '/{repo_name}/add/{revision}/{f_path}',
1079 1085 controller='files', action='add_home', revision='tip',
1080 1086 f_path='', conditions={'function': check_repo},
1081 1087 requirements=URL_NAME_REQUIREMENTS)
1082 1088
1083 1089 rmap.connect('files_delete',
1084 1090 '/{repo_name}/delete/{revision}/{f_path}',
1085 1091 controller='files', action='delete', revision='tip',
1086 1092 f_path='',
1087 1093 conditions={'function': check_repo, 'method': ['POST']},
1088 1094 requirements=URL_NAME_REQUIREMENTS)
1089 1095
1090 1096 rmap.connect('files_delete_home',
1091 1097 '/{repo_name}/delete/{revision}/{f_path}',
1092 1098 controller='files', action='delete_home', revision='tip',
1093 1099 f_path='', conditions={'function': check_repo},
1094 1100 requirements=URL_NAME_REQUIREMENTS)
1095 1101
1096 1102 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1097 1103 controller='files', action='archivefile',
1098 1104 conditions={'function': check_repo},
1099 1105 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1100 1106
1101 1107 rmap.connect('files_nodelist_home',
1102 1108 '/{repo_name}/nodelist/{revision}/{f_path}',
1103 1109 controller='files', action='nodelist',
1104 1110 conditions={'function': check_repo},
1105 1111 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1106 1112
1107 1113 rmap.connect('files_nodetree_full',
1108 1114 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1109 1115 controller='files', action='nodetree_full',
1110 1116 conditions={'function': check_repo},
1111 1117 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1112 1118
1113 1119 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1114 1120 controller='forks', action='fork_create',
1115 1121 conditions={'function': check_repo, 'method': ['POST']},
1116 1122 requirements=URL_NAME_REQUIREMENTS)
1117 1123
1118 1124 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1119 1125 controller='forks', action='fork',
1120 1126 conditions={'function': check_repo},
1121 1127 requirements=URL_NAME_REQUIREMENTS)
1122 1128
1123 1129 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1124 1130 controller='forks', action='forks',
1125 1131 conditions={'function': check_repo},
1126 1132 requirements=URL_NAME_REQUIREMENTS)
1127 1133
1128 1134 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1129 1135 controller='followers', action='followers',
1130 1136 conditions={'function': check_repo},
1131 1137 requirements=URL_NAME_REQUIREMENTS)
1132 1138
1133 1139 # must be here for proper group/repo catching pattern
1134 1140 _connect_with_slash(
1135 1141 rmap, 'repo_group_home', '/{group_name}',
1136 1142 controller='home', action='index_repo_group',
1137 1143 conditions={'function': check_group},
1138 1144 requirements=URL_NAME_REQUIREMENTS)
1139 1145
1140 1146 # catch all, at the end
1141 1147 _connect_with_slash(
1142 1148 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1143 1149 controller='summary', action='index',
1144 1150 conditions={'function': check_repo},
1145 1151 requirements=URL_NAME_REQUIREMENTS)
1146 1152
1147 1153 return rmap
1148 1154
1149 1155
1150 1156 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1151 1157 """
1152 1158 Connect a route with an optional trailing slash in `path`.
1153 1159 """
1154 1160 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1155 1161 mapper.connect(name, path, *args, **kwargs)
@@ -1,348 +1,362 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2016 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 my account controller for RhodeCode admin
24 24 """
25 25
26 26 import logging
27 27
28 28 import formencode
29 29 from formencode import htmlfill
30 30 from pylons import request, tmpl_context as c, url, session
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from sqlalchemy.orm import joinedload
34 34
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib import auth
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
39 39 from rhodecode.lib.base import BaseController, render
40 40 from rhodecode.lib.utils2 import safe_int, md5
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.model.db import (
43 43 Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
44 44 UserFollowing)
45 45 from rhodecode.model.forms import UserForm, PasswordChangeForm
46 46 from rhodecode.model.scm import RepoList
47 47 from rhodecode.model.user import UserModel
48 48 from rhodecode.model.repo import RepoModel
49 49 from rhodecode.model.auth_token import AuthTokenModel
50 50 from rhodecode.model.meta import Session
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MyAccountController(BaseController):
56 56 """REST Controller styled on the Atom Publishing Protocol"""
57 57 # To properly map this controller, ensure your config/routing.py
58 58 # file has a resource setup:
59 59 # map.resource('setting', 'settings', controller='admin/settings',
60 60 # path_prefix='/admin', name_prefix='admin_')
61 61
62 62 @LoginRequired()
63 63 @NotAnonymous()
64 64 def __before__(self):
65 65 super(MyAccountController, self).__before__()
66 66
67 67 def __load_data(self):
68 68 c.user = User.get(c.rhodecode_user.user_id)
69 69 if c.user.username == User.DEFAULT_USER:
70 70 h.flash(_("You can't edit this user since it's"
71 71 " crucial for entire application"), category='warning')
72 72 return redirect(url('users'))
73 73
74 74 def _load_my_repos_data(self, watched=False):
75 75 if watched:
76 76 admin = False
77 77 follows_repos = Session().query(UserFollowing)\
78 78 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
79 79 .options(joinedload(UserFollowing.follows_repository))\
80 80 .all()
81 81 repo_list = [x.follows_repository for x in follows_repos]
82 82 else:
83 83 admin = True
84 84 repo_list = Repository.get_all_repos(
85 85 user_id=c.rhodecode_user.user_id)
86 86 repo_list = RepoList(repo_list, perm_set=[
87 87 'repository.read', 'repository.write', 'repository.admin'])
88 88
89 89 repos_data = RepoModel().get_repos_as_dict(
90 90 repo_list=repo_list, admin=admin)
91 91 # json used to render the grid
92 92 return json.dumps(repos_data)
93 93
94 94 @auth.CSRFRequired()
95 95 def my_account_update(self):
96 96 """
97 97 POST /_admin/my_account Updates info of my account
98 98 """
99 99 # url('my_account')
100 100 c.active = 'profile_edit'
101 101 self.__load_data()
102 102 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
103 103 ip_addr=self.ip_addr)
104 104 c.extern_type = c.user.extern_type
105 105 c.extern_name = c.user.extern_name
106 106
107 107 defaults = c.user.get_dict()
108 108 update = False
109 109 _form = UserForm(edit=True,
110 110 old_data={'user_id': c.rhodecode_user.user_id,
111 111 'email': c.rhodecode_user.email})()
112 112 form_result = {}
113 113 try:
114 114 post_data = dict(request.POST)
115 115 post_data['new_password'] = ''
116 116 post_data['password_confirmation'] = ''
117 117 form_result = _form.to_python(post_data)
118 118 # skip updating those attrs for my account
119 119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 120 'new_password', 'password_confirmation']
121 121 # TODO: plugin should define if username can be updated
122 122 if c.extern_type != "rhodecode":
123 123 # forbid updating username for external accounts
124 124 skip_attrs.append('username')
125 125
126 126 UserModel().update_user(
127 127 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
128 128 h.flash(_('Your account was updated successfully'),
129 129 category='success')
130 130 Session().commit()
131 131 update = True
132 132
133 133 except formencode.Invalid as errors:
134 134 return htmlfill.render(
135 135 render('admin/my_account/my_account.html'),
136 136 defaults=errors.value,
137 137 errors=errors.error_dict or {},
138 138 prefix_error=False,
139 139 encoding="UTF-8",
140 140 force_defaults=False)
141 141 except Exception:
142 142 log.exception("Exception updating user")
143 143 h.flash(_('Error occurred during update of user %s')
144 144 % form_result.get('username'), category='error')
145 145
146 146 if update:
147 147 return redirect('my_account')
148 148
149 149 return htmlfill.render(
150 150 render('admin/my_account/my_account.html'),
151 151 defaults=defaults,
152 152 encoding="UTF-8",
153 153 force_defaults=False
154 154 )
155 155
156 156 def my_account(self):
157 157 """
158 158 GET /_admin/my_account Displays info about my account
159 159 """
160 160 # url('my_account')
161 161 c.active = 'profile'
162 162 self.__load_data()
163 163
164 164 defaults = c.user.get_dict()
165 165 return htmlfill.render(
166 166 render('admin/my_account/my_account.html'),
167 167 defaults=defaults, encoding="UTF-8", force_defaults=False)
168 168
169 169 def my_account_edit(self):
170 170 """
171 171 GET /_admin/my_account/edit Displays edit form of my account
172 172 """
173 173 c.active = 'profile_edit'
174 174 self.__load_data()
175 175 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
176 176 ip_addr=self.ip_addr)
177 177 c.extern_type = c.user.extern_type
178 178 c.extern_name = c.user.extern_name
179 179
180 180 defaults = c.user.get_dict()
181 181 return htmlfill.render(
182 182 render('admin/my_account/my_account.html'),
183 183 defaults=defaults,
184 184 encoding="UTF-8",
185 185 force_defaults=False
186 186 )
187 187
188 188 @auth.CSRFRequired()
189 189 def my_account_password_update(self):
190 190 c.active = 'password'
191 191 self.__load_data()
192 192 _form = PasswordChangeForm(c.rhodecode_user.username)()
193 193 try:
194 194 form_result = _form.to_python(request.POST)
195 195 UserModel().update_user(c.rhodecode_user.user_id, **form_result)
196 196 instance = c.rhodecode_user.get_instance()
197 197 instance.update_userdata(force_password_change=False)
198 198 Session().commit()
199 199 session.setdefault('rhodecode_user', {}).update(
200 200 {'password': md5(instance.password)})
201 201 session.save()
202 202 h.flash(_("Successfully updated password"), category='success')
203 203 except formencode.Invalid as errors:
204 204 return htmlfill.render(
205 205 render('admin/my_account/my_account.html'),
206 206 defaults=errors.value,
207 207 errors=errors.error_dict or {},
208 208 prefix_error=False,
209 209 encoding="UTF-8",
210 210 force_defaults=False)
211 211 except Exception:
212 212 log.exception("Exception updating password")
213 213 h.flash(_('Error occurred during update of user password'),
214 214 category='error')
215 215 return render('admin/my_account/my_account.html')
216 216
217 217 def my_account_password(self):
218 218 c.active = 'password'
219 219 self.__load_data()
220 220 return render('admin/my_account/my_account.html')
221 221
222 222 def my_account_repos(self):
223 223 c.active = 'repos'
224 224 self.__load_data()
225 225
226 226 # json used to render the grid
227 227 c.data = self._load_my_repos_data()
228 228 return render('admin/my_account/my_account.html')
229 229
230 230 def my_account_watched(self):
231 231 c.active = 'watched'
232 232 self.__load_data()
233 233
234 234 # json used to render the grid
235 235 c.data = self._load_my_repos_data(watched=True)
236 236 return render('admin/my_account/my_account.html')
237 237
238 238 def my_account_perms(self):
239 239 c.active = 'perms'
240 240 self.__load_data()
241 241 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
242 242 ip_addr=self.ip_addr)
243 243
244 244 return render('admin/my_account/my_account.html')
245 245
246 246 def my_account_emails(self):
247 247 c.active = 'emails'
248 248 self.__load_data()
249 249
250 250 c.user_email_map = UserEmailMap.query()\
251 251 .filter(UserEmailMap.user == c.user).all()
252 252 return render('admin/my_account/my_account.html')
253 253
254 254 @auth.CSRFRequired()
255 255 def my_account_emails_add(self):
256 256 email = request.POST.get('new_email')
257 257
258 258 try:
259 259 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
260 260 Session().commit()
261 261 h.flash(_("Added new email address `%s` for user account") % email,
262 262 category='success')
263 263 except formencode.Invalid as error:
264 264 msg = error.error_dict['email']
265 265 h.flash(msg, category='error')
266 266 except Exception:
267 267 log.exception("Exception in my_account_emails")
268 268 h.flash(_('An error occurred during email saving'),
269 269 category='error')
270 270 return redirect(url('my_account_emails'))
271 271
272 272 @auth.CSRFRequired()
273 273 def my_account_emails_delete(self):
274 274 email_id = request.POST.get('del_email_id')
275 275 user_model = UserModel()
276 276 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
277 277 Session().commit()
278 278 h.flash(_("Removed email address from user account"),
279 279 category='success')
280 280 return redirect(url('my_account_emails'))
281 281
282 282 def my_account_pullrequests(self):
283 283 c.active = 'pullrequests'
284 284 self.__load_data()
285 285 c.show_closed = request.GET.get('pr_show_closed')
286 286
287 287 def _filter(pr):
288 288 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
289 289 if not c.show_closed:
290 290 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
291 291 return s
292 292
293 293 c.my_pull_requests = _filter(
294 294 PullRequest.query().filter(
295 295 PullRequest.user_id == c.rhodecode_user.user_id).all())
296 296 my_prs = [
297 297 x.pull_request for x in PullRequestReviewers.query().filter(
298 298 PullRequestReviewers.user_id == c.rhodecode_user.user_id).all()]
299 299 c.participate_in_pull_requests = _filter(my_prs)
300 300 return render('admin/my_account/my_account.html')
301 301
302 302 def my_account_auth_tokens(self):
303 303 c.active = 'auth_tokens'
304 304 self.__load_data()
305 305 show_expired = True
306 306 c.lifetime_values = [
307 307 (str(-1), _('forever')),
308 308 (str(5), _('5 minutes')),
309 309 (str(60), _('1 hour')),
310 310 (str(60 * 24), _('1 day')),
311 311 (str(60 * 24 * 30), _('1 month')),
312 312 ]
313 313 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
314 314 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
315 315 for x in AuthTokenModel.cls.ROLES]
316 316 c.role_options = [(c.role_values, _("Role"))]
317 317 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
318 318 c.rhodecode_user.user_id, show_expired=show_expired)
319 319 return render('admin/my_account/my_account.html')
320 320
321 321 @auth.CSRFRequired()
322 322 def my_account_auth_tokens_add(self):
323 323 lifetime = safe_int(request.POST.get('lifetime'), -1)
324 324 description = request.POST.get('description')
325 325 role = request.POST.get('role')
326 326 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
327 327 role)
328 328 Session().commit()
329 329 h.flash(_("Auth token successfully created"), category='success')
330 330 return redirect(url('my_account_auth_tokens'))
331 331
332 332 @auth.CSRFRequired()
333 333 def my_account_auth_tokens_delete(self):
334 334 auth_token = request.POST.get('del_auth_token')
335 335 user_id = c.rhodecode_user.user_id
336 336 if request.POST.get('del_auth_token_builtin'):
337 337 user = User.get(user_id)
338 338 if user:
339 339 user.api_key = generate_auth_token(user.username)
340 340 Session().add(user)
341 341 Session().commit()
342 342 h.flash(_("Auth token successfully reset"), category='success')
343 343 elif auth_token:
344 344 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
345 345 Session().commit()
346 346 h.flash(_("Auth token successfully deleted"), category='success')
347 347
348 348 return redirect(url('my_account_auth_tokens'))
349
350 def my_notifications(self):
351 c.active = 'notifications'
352 return render('admin/my_account/my_account.html')
353
354 @auth.CSRFRequired()
355 def my_notifications_toggle_visibility(self):
356 user = c.rhodecode_user.get_instance()
357 user_data = user.user_data
358 status = user_data.get('notification_status', False)
359 user_data['notification_status'] = not status
360 user.user_data = user_data
361 Session().commit()
362 return redirect(url('my_account_notifications'))
@@ -1,177 +1,178 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 notifications controller for RhodeCode
24 24 """
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons import request
30 30 from pylons import tmpl_context as c, url
31 31 from pylons.controllers.util import redirect, abort
32 32 import webhelpers.paginate
33 33 from webob.exc import HTTPBadRequest
34 34
35 35 from rhodecode.lib import auth
36 36 from rhodecode.lib.auth import LoginRequired, NotAnonymous
37 37 from rhodecode.lib.base import BaseController, render
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.helpers import Page
40 40 from rhodecode.lib.utils2 import safe_int
41 41 from rhodecode.model.db import Notification
42 42 from rhodecode.model.notification import NotificationModel
43 43 from rhodecode.model.meta import Session
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class NotificationsController(BaseController):
50 50 """REST Controller styled on the Atom Publishing Protocol"""
51 51 # To properly map this controller, ensure your config/routing.py
52 52 # file has a resource setup:
53 53 # map.resource('notification', 'notifications', controller='_admin/notifications',
54 54 # path_prefix='/_admin', name_prefix='_admin_')
55 55
56 56 @LoginRequired()
57 57 @NotAnonymous()
58 58 def __before__(self):
59 59 super(NotificationsController, self).__before__()
60 60
61 61 def index(self):
62 62 """GET /_admin/notifications: All items in the collection"""
63 63 # url('notifications')
64 64 c.user = c.rhodecode_user
65 65 notif = NotificationModel().get_for_user(c.rhodecode_user.user_id,
66 66 filter_=request.GET.getall('type'))
67 67
68 68 p = safe_int(request.GET.get('page', 1), 1)
69 69 notifications_url = webhelpers.paginate.PageURL(
70 70 url('notifications'), request.GET)
71 71 c.notifications = Page(notif, page=p, items_per_page=10,
72 72 url=notifications_url)
73 73 c.pull_request_type = Notification.TYPE_PULL_REQUEST
74 74 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
75 75 Notification.TYPE_PULL_REQUEST_COMMENT]
76 76
77 77 _current_filter = request.GET.getall('type')
78 78 c.current_filter = 'all'
79 79 if _current_filter == [c.pull_request_type]:
80 80 c.current_filter = 'pull_request'
81 81 elif _current_filter == c.comment_type:
82 82 c.current_filter = 'comment'
83 83
84 84 if request.is_xhr:
85 85 return render('admin/notifications/notifications_data.html')
86 86
87 87 return render('admin/notifications/notifications.html')
88 88
89
89 90 @auth.CSRFRequired()
90 91 def mark_all_read(self):
91 92 if request.is_xhr:
92 93 nm = NotificationModel()
93 94 # mark all read
94 95 nm.mark_all_read_for_user(c.rhodecode_user.user_id,
95 96 filter_=request.GET.getall('type'))
96 97 Session().commit()
97 98 c.user = c.rhodecode_user
98 99 notif = nm.get_for_user(c.rhodecode_user.user_id,
99 100 filter_=request.GET.getall('type'))
100 101 notifications_url = webhelpers.paginate.PageURL(
101 102 url('notifications'), request.GET)
102 103 c.notifications = Page(notif, page=1, items_per_page=10,
103 104 url=notifications_url)
104 105 return render('admin/notifications/notifications_data.html')
105 106
106 107 def _has_permissions(self, notification):
107 108 def is_owner():
108 109 user_id = c.rhodecode_user.user_id
109 110 for user_notification in notification.notifications_to_users:
110 111 if user_notification.user.user_id == user_id:
111 112 return True
112 113 return False
113 114 return h.HasPermissionAny('hg.admin')() or is_owner()
114 115
115 116 @auth.CSRFRequired()
116 117 def update(self, notification_id):
117 118 """PUT /_admin/notifications/id: Update an existing item"""
118 119 # Forms posted to this method should contain a hidden field:
119 120 # <input type="hidden" name="_method" value="PUT" />
120 121 # Or using helpers:
121 122 # h.form(url('notification', notification_id=ID),
122 123 # method='put')
123 124 # url('notification', notification_id=ID)
124 125 try:
125 126 no = Notification.get(notification_id)
126 127 if self._has_permissions(no):
127 128 # deletes only notification2user
128 129 NotificationModel().mark_read(c.rhodecode_user.user_id, no)
129 130 Session().commit()
130 131 return 'ok'
131 132 except Exception:
132 133 Session().rollback()
133 134 log.exception("Exception updating a notification item")
134 135 raise HTTPBadRequest()
135 136
136 137 @auth.CSRFRequired()
137 138 def delete(self, notification_id):
138 139 """DELETE /_admin/notifications/id: Delete an existing item"""
139 140 # Forms posted to this method should contain a hidden field:
140 141 # <input type="hidden" name="_method" value="DELETE" />
141 142 # Or using helpers:
142 143 # h.form(url('notification', notification_id=ID),
143 144 # method='delete')
144 145 # url('notification', notification_id=ID)
145 146 try:
146 147 no = Notification.get(notification_id)
147 148 if self._has_permissions(no):
148 149 # deletes only notification2user
149 150 NotificationModel().delete(c.rhodecode_user.user_id, no)
150 151 Session().commit()
151 152 return 'ok'
152 153 except Exception:
153 154 Session().rollback()
154 155 log.exception("Exception deleting a notification item")
155 156 raise HTTPBadRequest()
156 157
157 158 def show(self, notification_id):
158 159 """GET /_admin/notifications/id: Show a specific item"""
159 160 # url('notification', notification_id=ID)
160 161 c.user = c.rhodecode_user
161 162 no = Notification.get(notification_id)
162 163
163 164 if no and self._has_permissions(no):
164 165 unotification = NotificationModel()\
165 166 .get_user_notification(c.user.user_id, no)
166 167
167 168 # if this association to user is not valid, we don't want to show
168 169 # this message
169 170 if unotification:
170 171 if not unotification.read:
171 172 unotification.mark_as_read()
172 173 Session().commit()
173 174 c.notification = no
174 175
175 176 return render('admin/notifications/show_notification.html')
176 177
177 178 return abort(403)
@@ -1,584 +1,585 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import ipaddress
31 31
32 32 from paste.auth.basic import AuthBasicAuthenticator
33 33 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 34 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 35 from pylons import config, tmpl_context as c, request, session, url
36 36 from pylons.controllers import WSGIController
37 37 from pylons.controllers.util import redirect
38 38 from pylons.i18n import translation
39 39 # marcink: don't remove this import
40 40 from pylons.templating import render_mako as render # noqa
41 41 from pylons.i18n.translation import _
42 42 from webob.exc import HTTPFound
43 43
44 44
45 45 import rhodecode
46 46 from rhodecode.authentication.base import VCS_TYPE
47 47 from rhodecode.lib import auth, utils2
48 48 from rhodecode.lib import helpers as h
49 49 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 50 from rhodecode.lib.exceptions import UserCreationError
51 51 from rhodecode.lib.utils import (
52 52 get_repo_slug, set_rhodecode_config, password_changed,
53 53 get_enabled_hook_classes)
54 54 from rhodecode.lib.utils2 import (
55 55 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 56 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 57 from rhodecode.model import meta
58 58 from rhodecode.model.db import Repository, User
59 59 from rhodecode.model.notification import NotificationModel
60 60 from rhodecode.model.scm import ScmModel
61 61 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 62
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 def _filter_proxy(ip):
68 68 """
69 69 Passed in IP addresses in HEADERS can be in a special format of multiple
70 70 ips. Those comma separated IPs are passed from various proxies in the
71 71 chain of request processing. The left-most being the original client.
72 72 We only care about the first IP which came from the org. client.
73 73
74 74 :param ip: ip string from headers
75 75 """
76 76 if ',' in ip:
77 77 _ips = ip.split(',')
78 78 _first_ip = _ips[0].strip()
79 79 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 80 return _first_ip
81 81 return ip
82 82
83 83
84 84 def _filter_port(ip):
85 85 """
86 86 Removes a port from ip, there are 4 main cases to handle here.
87 87 - ipv4 eg. 127.0.0.1
88 88 - ipv6 eg. ::1
89 89 - ipv4+port eg. 127.0.0.1:8080
90 90 - ipv6+port eg. [::1]:8080
91 91
92 92 :param ip:
93 93 """
94 94 def is_ipv6(ip_addr):
95 95 if hasattr(socket, 'inet_pton'):
96 96 try:
97 97 socket.inet_pton(socket.AF_INET6, ip_addr)
98 98 except socket.error:
99 99 return False
100 100 else:
101 101 # fallback to ipaddress
102 102 try:
103 103 ipaddress.IPv6Address(ip_addr)
104 104 except Exception:
105 105 return False
106 106 return True
107 107
108 108 if ':' not in ip: # must be ipv4 pure ip
109 109 return ip
110 110
111 111 if '[' in ip and ']' in ip: # ipv6 with port
112 112 return ip.split(']')[0][1:].lower()
113 113
114 114 # must be ipv6 or ipv4 with port
115 115 if is_ipv6(ip):
116 116 return ip
117 117 else:
118 118 ip, _port = ip.split(':')[:2] # means ipv4+port
119 119 return ip
120 120
121 121
122 122 def get_ip_addr(environ):
123 123 proxy_key = 'HTTP_X_REAL_IP'
124 124 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 125 def_key = 'REMOTE_ADDR'
126 126 _filters = lambda x: _filter_port(_filter_proxy(x))
127 127
128 128 ip = environ.get(proxy_key)
129 129 if ip:
130 130 return _filters(ip)
131 131
132 132 ip = environ.get(proxy_key2)
133 133 if ip:
134 134 return _filters(ip)
135 135
136 136 ip = environ.get(def_key, '0.0.0.0')
137 137 return _filters(ip)
138 138
139 139
140 140 def get_server_ip_addr(environ, log_errors=True):
141 141 hostname = environ.get('SERVER_NAME')
142 142 try:
143 143 return socket.gethostbyname(hostname)
144 144 except Exception as e:
145 145 if log_errors:
146 146 # in some cases this lookup is not possible, and we don't want to
147 147 # make it an exception in logs
148 148 log.exception('Could not retrieve server ip address: %s', e)
149 149 return hostname
150 150
151 151
152 152 def get_server_port(environ):
153 153 return environ.get('SERVER_PORT')
154 154
155 155
156 156 def get_access_path(environ):
157 157 path = environ.get('PATH_INFO')
158 158 org_req = environ.get('pylons.original_request')
159 159 if org_req:
160 160 path = org_req.environ.get('PATH_INFO')
161 161 return path
162 162
163 163
164 164 def vcs_operation_context(
165 165 environ, repo_name, username, action, scm, check_locking=True):
166 166 """
167 167 Generate the context for a vcs operation, e.g. push or pull.
168 168
169 169 This context is passed over the layers so that hooks triggered by the
170 170 vcs operation know details like the user, the user's IP address etc.
171 171
172 172 :param check_locking: Allows to switch of the computation of the locking
173 173 data. This serves mainly the need of the simplevcs middleware to be
174 174 able to disable this for certain operations.
175 175
176 176 """
177 177 # Tri-state value: False: unlock, None: nothing, True: lock
178 178 make_lock = None
179 179 locked_by = [None, None, None]
180 180 is_anonymous = username == User.DEFAULT_USER
181 181 if not is_anonymous and check_locking:
182 182 log.debug('Checking locking on repository "%s"', repo_name)
183 183 user = User.get_by_username(username)
184 184 repo = Repository.get_by_repo_name(repo_name)
185 185 make_lock, __, locked_by = repo.get_locking_state(
186 186 action, user.user_id)
187 187
188 188 settings_model = VcsSettingsModel(repo=repo_name)
189 189 ui_settings = settings_model.get_ui_settings()
190 190
191 191 extras = {
192 192 'ip': get_ip_addr(environ),
193 193 'username': username,
194 194 'action': action,
195 195 'repository': repo_name,
196 196 'scm': scm,
197 197 'config': rhodecode.CONFIG['__file__'],
198 198 'make_lock': make_lock,
199 199 'locked_by': locked_by,
200 200 'server_url': utils2.get_server_url(environ),
201 201 'hooks': get_enabled_hook_classes(ui_settings),
202 202 }
203 203 return extras
204 204
205 205
206 206 class BasicAuth(AuthBasicAuthenticator):
207 207
208 208 def __init__(self, realm, authfunc, auth_http_code=None,
209 209 initial_call_detection=False):
210 210 self.realm = realm
211 211 self.initial_call = initial_call_detection
212 212 self.authfunc = authfunc
213 213 self._rc_auth_http_code = auth_http_code
214 214
215 215 def _get_response_from_code(self, http_code):
216 216 try:
217 217 return get_exception(safe_int(http_code))
218 218 except Exception:
219 219 log.exception('Failed to fetch response for code %s' % http_code)
220 220 return HTTPForbidden
221 221
222 222 def build_authentication(self):
223 223 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
224 224 if self._rc_auth_http_code and not self.initial_call:
225 225 # return alternative HTTP code if alternative http return code
226 226 # is specified in RhodeCode config, but ONLY if it's not the
227 227 # FIRST call
228 228 custom_response_klass = self._get_response_from_code(
229 229 self._rc_auth_http_code)
230 230 return custom_response_klass(headers=head)
231 231 return HTTPUnauthorized(headers=head)
232 232
233 233 def authenticate(self, environ):
234 234 authorization = AUTHORIZATION(environ)
235 235 if not authorization:
236 236 return self.build_authentication()
237 237 (authmeth, auth) = authorization.split(' ', 1)
238 238 if 'basic' != authmeth.lower():
239 239 return self.build_authentication()
240 240 auth = auth.strip().decode('base64')
241 241 _parts = auth.split(':', 1)
242 242 if len(_parts) == 2:
243 243 username, password = _parts
244 244 if self.authfunc(
245 245 username, password, environ, VCS_TYPE):
246 246 return username
247 247 if username and password:
248 248 # we mark that we actually executed authentication once, at
249 249 # that point we can use the alternative auth code
250 250 self.initial_call = False
251 251
252 252 return self.build_authentication()
253 253
254 254 __call__ = authenticate
255 255
256 256
257 257 def attach_context_attributes(context, request):
258 258 """
259 259 Attach variables into template context called `c`, please note that
260 260 request could be pylons or pyramid request in here.
261 261 """
262 262 rc_config = SettingsModel().get_all_settings(cache=True)
263 263
264 264 context.rhodecode_version = rhodecode.__version__
265 265 context.rhodecode_edition = config.get('rhodecode.edition')
266 266 # unique secret + version does not leak the version but keep consistency
267 267 context.rhodecode_version_hash = md5(
268 268 config.get('beaker.session.secret', '') +
269 269 rhodecode.__version__)[:8]
270 270
271 271 # Default language set for the incoming request
272 272 context.language = translation.get_lang()[0]
273 273
274 274 # Visual options
275 275 context.visual = AttributeDict({})
276 276
277 277 # DB store
278 278 context.visual.show_public_icon = str2bool(
279 279 rc_config.get('rhodecode_show_public_icon'))
280 280 context.visual.show_private_icon = str2bool(
281 281 rc_config.get('rhodecode_show_private_icon'))
282 282 context.visual.stylify_metatags = str2bool(
283 283 rc_config.get('rhodecode_stylify_metatags'))
284 284 context.visual.dashboard_items = safe_int(
285 285 rc_config.get('rhodecode_dashboard_items', 100))
286 286 context.visual.admin_grid_items = safe_int(
287 287 rc_config.get('rhodecode_admin_grid_items', 100))
288 288 context.visual.repository_fields = str2bool(
289 289 rc_config.get('rhodecode_repository_fields'))
290 290 context.visual.show_version = str2bool(
291 291 rc_config.get('rhodecode_show_version'))
292 292 context.visual.use_gravatar = str2bool(
293 293 rc_config.get('rhodecode_use_gravatar'))
294 294 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
295 295 context.visual.default_renderer = rc_config.get(
296 296 'rhodecode_markup_renderer', 'rst')
297 297 context.visual.rhodecode_support_url = \
298 298 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
299 299
300 300 context.pre_code = rc_config.get('rhodecode_pre_code')
301 301 context.post_code = rc_config.get('rhodecode_post_code')
302 302 context.rhodecode_name = rc_config.get('rhodecode_title')
303 303 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
304 304 # if we have specified default_encoding in the request, it has more
305 305 # priority
306 306 if request.GET.get('default_encoding'):
307 307 context.default_encodings.insert(0, request.GET.get('default_encoding'))
308 308 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
309 309
310 310 # INI stored
311 311 context.labs_active = str2bool(
312 312 config.get('labs_settings_active', 'false'))
313 313 context.visual.allow_repo_location_change = str2bool(
314 314 config.get('allow_repo_location_change', True))
315 315 context.visual.allow_custom_hooks_settings = str2bool(
316 316 config.get('allow_custom_hooks_settings', True))
317 317 context.debug_style = str2bool(config.get('debug_style', False))
318 318
319 319 context.rhodecode_instanceid = config.get('instance_id')
320 320
321 321 # AppEnlight
322 322 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
323 323 context.appenlight_api_public_key = config.get(
324 324 'appenlight.api_public_key', '')
325 325 context.appenlight_server_url = config.get('appenlight.server_url', '')
326 326
327 327 # JS template context
328 328 context.template_context = {
329 329 'repo_name': None,
330 330 'repo_type': None,
331 331 'repo_landing_commit': None,
332 332 'rhodecode_user': {
333 333 'username': None,
334 334 'email': None,
335 'notification_status': False
335 336 },
336 337 'visual': {
337 338 'default_renderer': None
338 339 },
339 340 'commit_data': {
340 341 'commit_id': None
341 342 },
342 343 'pull_request_data': {'pull_request_id': None},
343 344 'timeago': {
344 345 'refresh_time': 120 * 1000,
345 346 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
346 347 },
347 348 'pylons_dispatch': {
348 349 # 'controller': request.environ['pylons.routes_dict']['controller'],
349 350 # 'action': request.environ['pylons.routes_dict']['action'],
350 351 },
351 352 'pyramid_dispatch': {
352 353
353 354 },
354 355 'extra': {'plugins': {}}
355 356 }
356 357 # END CONFIG VARS
357 358
358 359 # TODO: This dosn't work when called from pylons compatibility tween.
359 360 # Fix this and remove it from base controller.
360 361 # context.repo_name = get_repo_slug(request) # can be empty
361 362
362 363 context.csrf_token = auth.get_csrf_token()
363 364 context.backends = rhodecode.BACKENDS.keys()
364 365 context.backends.sort()
365 366 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
366 367 context.rhodecode_user.user_id)
367 368
368 369
369 370 def get_auth_user(environ):
370 371 ip_addr = get_ip_addr(environ)
371 372 # make sure that we update permissions each time we call controller
372 373 _auth_token = (request.GET.get('auth_token', '') or
373 374 request.GET.get('api_key', ''))
374 375
375 376 if _auth_token:
376 377 # when using API_KEY we are sure user exists.
377 378 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
378 379 authenticated = False
379 380 else:
380 381 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
381 382 try:
382 383 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
383 384 ip_addr=ip_addr)
384 385 except UserCreationError as e:
385 386 h.flash(e, 'error')
386 387 # container auth or other auth functions that create users
387 388 # on the fly can throw this exception signaling that there's
388 389 # issue with user creation, explanation should be provided
389 390 # in Exception itself. We then create a simple blank
390 391 # AuthUser
391 392 auth_user = AuthUser(ip_addr=ip_addr)
392 393
393 394 if password_changed(auth_user, session):
394 395 session.invalidate()
395 396 cookie_store = CookieStoreWrapper(
396 397 session.get('rhodecode_user'))
397 398 auth_user = AuthUser(ip_addr=ip_addr)
398 399
399 400 authenticated = cookie_store.get('is_authenticated')
400 401
401 402 if not auth_user.is_authenticated and auth_user.is_user_object:
402 403 # user is not authenticated and not empty
403 404 auth_user.set_authenticated(authenticated)
404 405
405 406 return auth_user
406 407
407 408
408 409 class BaseController(WSGIController):
409 410
410 411 def __before__(self):
411 412 """
412 413 __before__ is called before controller methods and after __call__
413 414 """
414 415 # on each call propagate settings calls into global settings.
415 416 set_rhodecode_config(config)
416 417 attach_context_attributes(c, request)
417 418
418 419 # TODO: Remove this when fixed in attach_context_attributes()
419 420 c.repo_name = get_repo_slug(request) # can be empty
420 421
421 422 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
422 423 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
423 424 self.sa = meta.Session
424 425 self.scm_model = ScmModel(self.sa)
425 426
426 427 default_lang = c.language
427 428 user_lang = c.language
428 429 try:
429 430 user_obj = self._rhodecode_user.get_instance()
430 431 if user_obj:
431 432 user_lang = user_obj.user_data.get('language')
432 433 except Exception:
433 434 log.exception('Failed to fetch user language for user %s',
434 435 self._rhodecode_user)
435 436
436 437 if user_lang and user_lang != default_lang:
437 438 log.debug('set language to %s for user %s', user_lang,
438 439 self._rhodecode_user)
439 440 translation.set_lang(user_lang)
440 441
441 442 def _dispatch_redirect(self, with_url, environ, start_response):
442 443 resp = HTTPFound(with_url)
443 444 environ['SCRIPT_NAME'] = '' # handle prefix middleware
444 445 environ['PATH_INFO'] = with_url
445 446 return resp(environ, start_response)
446 447
447 448 def __call__(self, environ, start_response):
448 449 """Invoke the Controller"""
449 450 # WSGIController.__call__ dispatches to the Controller method
450 451 # the request is routed to. This routing information is
451 452 # available in environ['pylons.routes_dict']
452 453 from rhodecode.lib import helpers as h
453 454
454 455 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
455 456 if environ.get('debugtoolbar.wants_pylons_context', False):
456 457 environ['debugtoolbar.pylons_context'] = c._current_obj()
457 458
458 459 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
459 460 environ['pylons.routes_dict']['action']])
460 461
461 462 self.rc_config = SettingsModel().get_all_settings(cache=True)
462 463 self.ip_addr = get_ip_addr(environ)
463 464
464 465 # The rhodecode auth user is looked up and passed through the
465 466 # environ by the pylons compatibility tween in pyramid.
466 467 # So we can just grab it from there.
467 468 auth_user = environ['rc_auth_user']
468 469
469 470 # set globals for auth user
470 471 request.user = auth_user
471 472 c.rhodecode_user = self._rhodecode_user = auth_user
472 473
473 474 log.info('IP: %s User: %s accessed %s [%s]' % (
474 475 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
475 476 _route_name)
476 477 )
477 478
478 479 # TODO: Maybe this should be move to pyramid to cover all views.
479 480 # check user attributes for password change flag
480 481 user_obj = auth_user.get_instance()
481 482 if user_obj and user_obj.user_data.get('force_password_change'):
482 483 h.flash('You are required to change your password', 'warning',
483 484 ignore_duplicate=True)
484 485
485 486 skip_user_check_urls = [
486 487 'error.document', 'login.logout', 'login.index',
487 488 'admin/my_account.my_account_password',
488 489 'admin/my_account.my_account_password_update'
489 490 ]
490 491 if _route_name not in skip_user_check_urls:
491 492 return self._dispatch_redirect(
492 493 url('my_account_password'), environ, start_response)
493 494
494 495 return WSGIController.__call__(self, environ, start_response)
495 496
496 497
497 498 class BaseRepoController(BaseController):
498 499 """
499 500 Base class for controllers responsible for loading all needed data for
500 501 repository loaded items are
501 502
502 503 c.rhodecode_repo: instance of scm repository
503 504 c.rhodecode_db_repo: instance of db
504 505 c.repository_requirements_missing: shows that repository specific data
505 506 could not be displayed due to the missing requirements
506 507 c.repository_pull_requests: show number of open pull requests
507 508 """
508 509
509 510 def __before__(self):
510 511 super(BaseRepoController, self).__before__()
511 512 if c.repo_name: # extracted from routes
512 513 db_repo = Repository.get_by_repo_name(c.repo_name)
513 514 if not db_repo:
514 515 return
515 516
516 517 log.debug(
517 518 'Found repository in database %s with state `%s`',
518 519 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
519 520 route = getattr(request.environ.get('routes.route'), 'name', '')
520 521
521 522 # allow to delete repos that are somehow damages in filesystem
522 523 if route in ['delete_repo']:
523 524 return
524 525
525 526 if db_repo.repo_state in [Repository.STATE_PENDING]:
526 527 if route in ['repo_creating_home']:
527 528 return
528 529 check_url = url('repo_creating_home', repo_name=c.repo_name)
529 530 return redirect(check_url)
530 531
531 532 self.rhodecode_db_repo = db_repo
532 533
533 534 missing_requirements = False
534 535 try:
535 536 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
536 537 except RepositoryRequirementError as e:
537 538 missing_requirements = True
538 539 self._handle_missing_requirements(e)
539 540
540 541 if self.rhodecode_repo is None and not missing_requirements:
541 542 log.error('%s this repository is present in database but it '
542 543 'cannot be created as an scm instance', c.repo_name)
543 544
544 545 h.flash(_(
545 546 "The repository at %(repo_name)s cannot be located.") %
546 547 {'repo_name': c.repo_name},
547 548 category='error', ignore_duplicate=True)
548 549 redirect(url('home'))
549 550
550 551 # update last change according to VCS data
551 552 if not missing_requirements:
552 553 commit = db_repo.get_commit(
553 554 pre_load=["author", "date", "message", "parents"])
554 555 db_repo.update_commit_cache(commit)
555 556
556 557 # Prepare context
557 558 c.rhodecode_db_repo = db_repo
558 559 c.rhodecode_repo = self.rhodecode_repo
559 560 c.repository_requirements_missing = missing_requirements
560 561
561 562 self._update_global_counters(self.scm_model, db_repo)
562 563
563 564 def _update_global_counters(self, scm_model, db_repo):
564 565 """
565 566 Base variables that are exposed to every page of repository
566 567 """
567 568 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
568 569
569 570 def _handle_missing_requirements(self, error):
570 571 self.rhodecode_repo = None
571 572 log.error(
572 573 'Requirements are missing for repository %s: %s',
573 574 c.repo_name, error.message)
574 575
575 576 summary_url = url('summary_home', repo_name=c.repo_name)
576 577 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
577 578 settings_update_url = url('repo', repo_name=c.repo_name)
578 579 path = request.path
579 580 should_redirect = (
580 581 path not in (summary_url, settings_update_url)
581 582 and '/settings' not in path or path == statistics_url
582 583 )
583 584 if should_redirect:
584 585 redirect(summary_url)
@@ -1,471 +1,512 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 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 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 from datetime import datetime
30
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
29 33 from sqlalchemy.sql.expression import null
30 34 from sqlalchemy.sql.functions import coalesce
31 35
32 36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib.channelstream import channelstream_request
33 38 from rhodecode.lib.utils import action_logger
34 39 from rhodecode.lib.utils2 import extract_mentioned_users
35 40 from rhodecode.model import BaseModel
36 41 from rhodecode.model.db import (
37 42 ChangesetComment, User, Notification, PullRequest)
38 43 from rhodecode.model.notification import NotificationModel
39 44 from rhodecode.model.meta import Session
40 45 from rhodecode.model.settings import VcsSettingsModel
41 46 from rhodecode.model.notification import EmailNotificationModel
42 47
43 48 log = logging.getLogger(__name__)
44 49
45 50
46 51 class ChangesetCommentsModel(BaseModel):
47 52
48 53 cls = ChangesetComment
49 54
50 55 DIFF_CONTEXT_BEFORE = 3
51 56 DIFF_CONTEXT_AFTER = 3
52 57
53 58 def __get_commit_comment(self, changeset_comment):
54 59 return self._get_instance(ChangesetComment, changeset_comment)
55 60
56 61 def __get_pull_request(self, pull_request):
57 62 return self._get_instance(PullRequest, pull_request)
58 63
59 64 def _extract_mentions(self, s):
60 65 user_objects = []
61 66 for username in extract_mentioned_users(s):
62 67 user_obj = User.get_by_username(username, case_insensitive=True)
63 68 if user_obj:
64 69 user_objects.append(user_obj)
65 70 return user_objects
66 71
67 72 def _get_renderer(self, global_renderer='rst'):
68 73 try:
69 74 # try reading from visual context
70 75 from pylons import tmpl_context
71 76 global_renderer = tmpl_context.visual.default_renderer
72 77 except AttributeError:
73 78 log.debug("Renderer not set, falling back "
74 79 "to default renderer '%s'", global_renderer)
75 80 except Exception:
76 81 log.error(traceback.format_exc())
77 82 return global_renderer
78 83
79 84 def create(self, text, repo, user, revision=None, pull_request=None,
80 85 f_path=None, line_no=None, status_change=None, closing_pr=False,
81 86 send_email=True, renderer=None):
82 87 """
83 88 Creates new comment for commit or pull request.
84 89 IF status_change is not none this comment is associated with a
85 90 status change of commit or commit associated with pull request
86 91
87 92 :param text:
88 93 :param repo:
89 94 :param user:
90 95 :param revision:
91 96 :param pull_request:
92 97 :param f_path:
93 98 :param line_no:
94 99 :param status_change:
95 100 :param closing_pr:
96 101 :param send_email:
97 102 """
98 103 if not text:
99 104 log.warning('Missing text for comment, skipping...')
100 105 return
101 106
102 107 if not renderer:
103 108 renderer = self._get_renderer()
104 109
105 110 repo = self._get_repo(repo)
106 111 user = self._get_user(user)
107 112 comment = ChangesetComment()
108 113 comment.renderer = renderer
109 114 comment.repo = repo
110 115 comment.author = user
111 116 comment.text = text
112 117 comment.f_path = f_path
113 118 comment.line_no = line_no
114 119
115 120 #TODO (marcink): fix this and remove revision as param
116 121 commit_id = revision
117 122 pull_request_id = pull_request
118 123
119 124 commit_obj = None
120 125 pull_request_obj = None
121 126
122 127 if commit_id:
123 128 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
124 129 # do a lookup, so we don't pass something bad here
125 130 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
126 131 comment.revision = commit_obj.raw_id
127 132
128 133 elif pull_request_id:
129 134 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
130 135 pull_request_obj = self.__get_pull_request(pull_request_id)
131 136 comment.pull_request = pull_request_obj
132 137 else:
133 138 raise Exception('Please specify commit or pull_request_id')
134 139
135 140 Session().add(comment)
136 141 Session().flush()
137
138 if send_email:
139 kwargs = {
140 'user': user,
141 'renderer_type': renderer,
142 'repo_name': repo.repo_name,
143 'status_change': status_change,
144 'comment_body': text,
145 'comment_file': f_path,
146 'comment_line': line_no,
147 }
142 kwargs = {
143 'user': user,
144 'renderer_type': renderer,
145 'repo_name': repo.repo_name,
146 'status_change': status_change,
147 'comment_body': text,
148 'comment_file': f_path,
149 'comment_line': line_no,
150 }
148 151
149 if commit_obj:
150 recipients = ChangesetComment.get_users(
151 revision=commit_obj.raw_id)
152 # add commit author if it's in RhodeCode system
153 cs_author = User.get_from_cs_author(commit_obj.author)
154 if not cs_author:
155 # use repo owner if we cannot extract the author correctly
156 cs_author = repo.user
157 recipients += [cs_author]
152 if commit_obj:
153 recipients = ChangesetComment.get_users(
154 revision=commit_obj.raw_id)
155 # add commit author if it's in RhodeCode system
156 cs_author = User.get_from_cs_author(commit_obj.author)
157 if not cs_author:
158 # use repo owner if we cannot extract the author correctly
159 cs_author = repo.user
160 recipients += [cs_author]
158 161
159 commit_comment_url = self.get_url(comment)
162 commit_comment_url = self.get_url(comment)
160 163
161 target_repo_url = h.link_to(
162 repo.repo_name,
163 h.url('summary_home',
164 repo_name=repo.repo_name, qualified=True))
164 target_repo_url = h.link_to(
165 repo.repo_name,
166 h.url('summary_home',
167 repo_name=repo.repo_name, qualified=True))
165 168
166 # commit specifics
167 kwargs.update({
168 'commit': commit_obj,
169 'commit_message': commit_obj.message,
170 'commit_target_repo': target_repo_url,
171 'commit_comment_url': commit_comment_url,
172 })
169 # commit specifics
170 kwargs.update({
171 'commit': commit_obj,
172 'commit_message': commit_obj.message,
173 'commit_target_repo': target_repo_url,
174 'commit_comment_url': commit_comment_url,
175 })
173 176
174 elif pull_request_obj:
175 # get the current participants of this pull request
176 recipients = ChangesetComment.get_users(
177 pull_request_id=pull_request_obj.pull_request_id)
178 # add pull request author
179 recipients += [pull_request_obj.author]
177 elif pull_request_obj:
178 # get the current participants of this pull request
179 recipients = ChangesetComment.get_users(
180 pull_request_id=pull_request_obj.pull_request_id)
181 # add pull request author
182 recipients += [pull_request_obj.author]
180 183
181 # add the reviewers to notification
182 recipients += [x.user for x in pull_request_obj.reviewers]
184 # add the reviewers to notification
185 recipients += [x.user for x in pull_request_obj.reviewers]
183 186
184 pr_target_repo = pull_request_obj.target_repo
185 pr_source_repo = pull_request_obj.source_repo
187 pr_target_repo = pull_request_obj.target_repo
188 pr_source_repo = pull_request_obj.source_repo
186 189
187 pr_comment_url = h.url(
188 'pullrequest_show',
189 repo_name=pr_target_repo.repo_name,
190 pull_request_id=pull_request_obj.pull_request_id,
191 anchor='comment-%s' % comment.comment_id,
192 qualified=True,)
190 pr_comment_url = h.url(
191 'pullrequest_show',
192 repo_name=pr_target_repo.repo_name,
193 pull_request_id=pull_request_obj.pull_request_id,
194 anchor='comment-%s' % comment.comment_id,
195 qualified=True,)
193 196
194 # set some variables for email notification
195 pr_target_repo_url = h.url(
196 'summary_home', repo_name=pr_target_repo.repo_name,
197 qualified=True)
197 # set some variables for email notification
198 pr_target_repo_url = h.url(
199 'summary_home', repo_name=pr_target_repo.repo_name,
200 qualified=True)
198 201
199 pr_source_repo_url = h.url(
200 'summary_home', repo_name=pr_source_repo.repo_name,
201 qualified=True)
202 pr_source_repo_url = h.url(
203 'summary_home', repo_name=pr_source_repo.repo_name,
204 qualified=True)
202 205
203 # pull request specifics
204 kwargs.update({
205 'pull_request': pull_request_obj,
206 'pr_id': pull_request_obj.pull_request_id,
207 'pr_target_repo': pr_target_repo,
208 'pr_target_repo_url': pr_target_repo_url,
209 'pr_source_repo': pr_source_repo,
210 'pr_source_repo_url': pr_source_repo_url,
211 'pr_comment_url': pr_comment_url,
212 'pr_closing': closing_pr,
213 })
214
206 # pull request specifics
207 kwargs.update({
208 'pull_request': pull_request_obj,
209 'pr_id': pull_request_obj.pull_request_id,
210 'pr_target_repo': pr_target_repo,
211 'pr_target_repo_url': pr_target_repo_url,
212 'pr_source_repo': pr_source_repo,
213 'pr_source_repo_url': pr_source_repo_url,
214 'pr_comment_url': pr_comment_url,
215 'pr_closing': closing_pr,
216 })
217 if send_email:
215 218 # pre-generate the subject for notification itself
216 219 (subject,
217 220 _h, _e, # we don't care about those
218 221 body_plaintext) = EmailNotificationModel().render_email(
219 222 notification_type, **kwargs)
220 223
221 224 mention_recipients = set(
222 225 self._extract_mentions(text)).difference(recipients)
223 226
224 227 # create notification objects, and emails
225 228 NotificationModel().create(
226 229 created_by=user,
227 230 notification_subject=subject,
228 231 notification_body=body_plaintext,
229 232 notification_type=notification_type,
230 233 recipients=recipients,
231 234 mention_recipients=mention_recipients,
232 235 email_kwargs=kwargs,
233 236 )
234 237
235 238 action = (
236 239 'user_commented_pull_request:{}'.format(
237 240 comment.pull_request.pull_request_id)
238 241 if comment.pull_request
239 242 else 'user_commented_revision:{}'.format(comment.revision)
240 243 )
241 244 action_logger(user, action, comment.repo)
242 245
246 registry = get_current_registry()
247 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
248 channelstream_config = rhodecode_plugins.get('channelstream')
249 msg_url = ''
250 if commit_obj:
251 msg_url = commit_comment_url
252 repo_name = repo.repo_name
253 elif pull_request_obj:
254 msg_url = pr_comment_url
255 repo_name = pr_target_repo.repo_name
256
257 if channelstream_config:
258 message = '<strong>{}</strong> {} - ' \
259 '<a onclick="window.location=\'{}\';' \
260 'window.location.reload()">' \
261 '<strong>{}</strong></a>'
262 message = message.format(
263 user.username, _('made a comment'), msg_url,
264 _('Refresh page'))
265 if channelstream_config:
266 channel = '/repo${}$/pr/{}'.format(
267 repo_name,
268 pull_request_id
269 )
270 payload = {
271 'type': 'message',
272 'timestamp': datetime.utcnow(),
273 'user': 'system',
274 'channel': channel,
275 'message': {
276 'message': message,
277 'level': 'info',
278 'topic': '/notifications'
279 }
280 }
281 channelstream_request(channelstream_config, [payload],
282 '/message', raise_exc=False)
283
243 284 return comment
244 285
245 286 def delete(self, comment):
246 287 """
247 288 Deletes given comment
248 289
249 290 :param comment_id:
250 291 """
251 292 comment = self.__get_commit_comment(comment)
252 293 Session().delete(comment)
253 294
254 295 return comment
255 296
256 297 def get_all_comments(self, repo_id, revision=None, pull_request=None):
257 298 q = ChangesetComment.query()\
258 299 .filter(ChangesetComment.repo_id == repo_id)
259 300 if revision:
260 301 q = q.filter(ChangesetComment.revision == revision)
261 302 elif pull_request:
262 303 pull_request = self.__get_pull_request(pull_request)
263 304 q = q.filter(ChangesetComment.pull_request == pull_request)
264 305 else:
265 306 raise Exception('Please specify commit or pull_request')
266 307 q = q.order_by(ChangesetComment.created_on)
267 308 return q.all()
268 309
269 310 def get_url(self, comment):
270 311 comment = self.__get_commit_comment(comment)
271 312 if comment.pull_request:
272 313 return h.url(
273 314 'pullrequest_show',
274 315 repo_name=comment.pull_request.target_repo.repo_name,
275 316 pull_request_id=comment.pull_request.pull_request_id,
276 317 anchor='comment-%s' % comment.comment_id,
277 318 qualified=True,)
278 319 else:
279 320 return h.url(
280 321 'changeset_home',
281 322 repo_name=comment.repo.repo_name,
282 323 revision=comment.revision,
283 324 anchor='comment-%s' % comment.comment_id,
284 325 qualified=True,)
285 326
286 327 def get_comments(self, repo_id, revision=None, pull_request=None):
287 328 """
288 329 Gets main comments based on revision or pull_request_id
289 330
290 331 :param repo_id:
291 332 :param revision:
292 333 :param pull_request:
293 334 """
294 335
295 336 q = ChangesetComment.query()\
296 337 .filter(ChangesetComment.repo_id == repo_id)\
297 338 .filter(ChangesetComment.line_no == None)\
298 339 .filter(ChangesetComment.f_path == None)
299 340 if revision:
300 341 q = q.filter(ChangesetComment.revision == revision)
301 342 elif pull_request:
302 343 pull_request = self.__get_pull_request(pull_request)
303 344 q = q.filter(ChangesetComment.pull_request == pull_request)
304 345 else:
305 346 raise Exception('Please specify commit or pull_request')
306 347 q = q.order_by(ChangesetComment.created_on)
307 348 return q.all()
308 349
309 350 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
310 351 q = self._get_inline_comments_query(repo_id, revision, pull_request)
311 352 return self._group_comments_by_path_and_line_number(q)
312 353
313 354 def get_outdated_comments(self, repo_id, pull_request):
314 355 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
315 356 # of a pull request.
316 357 q = self._all_inline_comments_of_pull_request(pull_request)
317 358 q = q.filter(
318 359 ChangesetComment.display_state ==
319 360 ChangesetComment.COMMENT_OUTDATED
320 361 ).order_by(ChangesetComment.comment_id.asc())
321 362
322 363 return self._group_comments_by_path_and_line_number(q)
323 364
324 365 def _get_inline_comments_query(self, repo_id, revision, pull_request):
325 366 # TODO: johbo: Split this into two methods: One for PR and one for
326 367 # commit.
327 368 if revision:
328 369 q = Session().query(ChangesetComment).filter(
329 370 ChangesetComment.repo_id == repo_id,
330 371 ChangesetComment.line_no != null(),
331 372 ChangesetComment.f_path != null(),
332 373 ChangesetComment.revision == revision)
333 374
334 375 elif pull_request:
335 376 pull_request = self.__get_pull_request(pull_request)
336 377 if ChangesetCommentsModel.use_outdated_comments(pull_request):
337 378 q = self._visible_inline_comments_of_pull_request(pull_request)
338 379 else:
339 380 q = self._all_inline_comments_of_pull_request(pull_request)
340 381
341 382 else:
342 383 raise Exception('Please specify commit or pull_request_id')
343 384 q = q.order_by(ChangesetComment.comment_id.asc())
344 385 return q
345 386
346 387 def _group_comments_by_path_and_line_number(self, q):
347 388 comments = q.all()
348 389 paths = collections.defaultdict(lambda: collections.defaultdict(list))
349 390 for co in comments:
350 391 paths[co.f_path][co.line_no].append(co)
351 392 return paths
352 393
353 394 @classmethod
354 395 def needed_extra_diff_context(cls):
355 396 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
356 397
357 398 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
358 399 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
359 400 return
360 401
361 402 comments = self._visible_inline_comments_of_pull_request(pull_request)
362 403 comments_to_outdate = comments.all()
363 404
364 405 for comment in comments_to_outdate:
365 406 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
366 407
367 408 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
368 409 diff_line = _parse_comment_line_number(comment.line_no)
369 410
370 411 try:
371 412 old_context = old_diff_proc.get_context_of_line(
372 413 path=comment.f_path, diff_line=diff_line)
373 414 new_context = new_diff_proc.get_context_of_line(
374 415 path=comment.f_path, diff_line=diff_line)
375 416 except (diffs.LineNotInDiffException,
376 417 diffs.FileNotInDiffException):
377 418 comment.display_state = ChangesetComment.COMMENT_OUTDATED
378 419 return
379 420
380 421 if old_context == new_context:
381 422 return
382 423
383 424 if self._should_relocate_diff_line(diff_line):
384 425 new_diff_lines = new_diff_proc.find_context(
385 426 path=comment.f_path, context=old_context,
386 427 offset=self.DIFF_CONTEXT_BEFORE)
387 428 if not new_diff_lines:
388 429 comment.display_state = ChangesetComment.COMMENT_OUTDATED
389 430 else:
390 431 new_diff_line = self._choose_closest_diff_line(
391 432 diff_line, new_diff_lines)
392 433 comment.line_no = _diff_to_comment_line_number(new_diff_line)
393 434 else:
394 435 comment.display_state = ChangesetComment.COMMENT_OUTDATED
395 436
396 437 def _should_relocate_diff_line(self, diff_line):
397 438 """
398 439 Checks if relocation shall be tried for the given `diff_line`.
399 440
400 441 If a comment points into the first lines, then we can have a situation
401 442 that after an update another line has been added on top. In this case
402 443 we would find the context still and move the comment around. This
403 444 would be wrong.
404 445 """
405 446 should_relocate = (
406 447 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
407 448 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
408 449 return should_relocate
409 450
410 451 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
411 452 candidate = new_diff_lines[0]
412 453 best_delta = _diff_line_delta(diff_line, candidate)
413 454 for new_diff_line in new_diff_lines[1:]:
414 455 delta = _diff_line_delta(diff_line, new_diff_line)
415 456 if delta < best_delta:
416 457 candidate = new_diff_line
417 458 best_delta = delta
418 459 return candidate
419 460
420 461 def _visible_inline_comments_of_pull_request(self, pull_request):
421 462 comments = self._all_inline_comments_of_pull_request(pull_request)
422 463 comments = comments.filter(
423 464 coalesce(ChangesetComment.display_state, '') !=
424 465 ChangesetComment.COMMENT_OUTDATED)
425 466 return comments
426 467
427 468 def _all_inline_comments_of_pull_request(self, pull_request):
428 469 comments = Session().query(ChangesetComment)\
429 470 .filter(ChangesetComment.line_no != None)\
430 471 .filter(ChangesetComment.f_path != None)\
431 472 .filter(ChangesetComment.pull_request == pull_request)
432 473 return comments
433 474
434 475 @staticmethod
435 476 def use_outdated_comments(pull_request):
436 477 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
437 478 settings = settings_model.get_general_settings()
438 479 return settings.get('rhodecode_use_outdated_comments', False)
439 480
440 481
441 482 def _parse_comment_line_number(line_no):
442 483 """
443 484 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
444 485 """
445 486 old_line = None
446 487 new_line = None
447 488 if line_no.startswith('o'):
448 489 old_line = int(line_no[1:])
449 490 elif line_no.startswith('n'):
450 491 new_line = int(line_no[1:])
451 492 else:
452 493 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
453 494 return diffs.DiffLineNumber(old_line, new_line)
454 495
455 496
456 497 def _diff_to_comment_line_number(diff_line):
457 498 if diff_line.new is not None:
458 499 return u'n{}'.format(diff_line.new)
459 500 elif diff_line.old is not None:
460 501 return u'o{}'.format(diff_line.old)
461 502 return u''
462 503
463 504
464 505 def _diff_line_delta(a, b):
465 506 if None not in (a.new, b.new):
466 507 return abs(a.new - b.new)
467 508 elif None not in (a.old, b.old):
468 509 return abs(a.old - b.old)
469 510 else:
470 511 raise ValueError(
471 512 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,2100 +1,2105 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 @import 'toastr';
28 29 @import 'deform';
29 30
30 31
31 32 //--- BASE ------------------//
32 33 .noscript-error {
33 34 top: 0;
34 35 left: 0;
35 36 width: 100%;
36 37 z-index: 101;
37 38 text-align: center;
38 39 font-family: @text-semibold;
39 40 font-size: 120%;
40 41 color: white;
41 42 background-color: @alert2;
42 43 padding: 5px 0 5px 0;
43 44 }
44 45
45 46 html {
46 47 display: table;
47 48 height: 100%;
48 49 width: 100%;
49 50 }
50 51
51 52 body {
52 53 display: table-cell;
53 54 width: 100%;
54 55 }
55 56
56 57 //--- LAYOUT ------------------//
57 58
58 59 .hidden{
59 60 display: none !important;
60 61 }
61 62
62 63 .box{
63 64 float: left;
64 65 width: 100%;
65 66 }
66 67
67 68 .browser-header {
68 69 clear: both;
69 70 }
70 71 .main {
71 72 clear: both;
72 73 padding:0 0 @pagepadding;
73 74 height: auto;
74 75
75 76 &:after { //clearfix
76 77 content:"";
77 78 clear:both;
78 79 width:100%;
79 80 display:block;
80 81 }
81 82 }
82 83
83 84 .action-link{
84 85 margin-left: @padding;
85 86 padding-left: @padding;
86 87 border-left: @border-thickness solid @border-default-color;
87 88 }
88 89
89 90 input + .action-link, .action-link.first{
90 91 border-left: none;
91 92 }
92 93
93 94 .action-link.last{
94 95 margin-right: @padding;
95 96 padding-right: @padding;
96 97 }
97 98
98 99 .action-link.active,
99 100 .action-link.active a{
100 101 color: @grey4;
101 102 }
102 103
103 104 ul.simple-list{
104 105 list-style: none;
105 106 margin: 0;
106 107 padding: 0;
107 108 }
108 109
109 110 .main-content {
110 111 padding-bottom: @pagepadding;
111 112 }
112 113
113 114 .wrapper {
114 115 position: relative;
115 116 max-width: @wrapper-maxwidth;
116 117 margin: 0 auto;
117 118 }
118 119
119 120 #content {
120 121 clear: both;
121 122 padding: 0 @contentpadding;
122 123 }
123 124
124 125 .advanced-settings-fields{
125 126 input{
126 127 margin-left: @textmargin;
127 128 margin-right: @padding/2;
128 129 }
129 130 }
130 131
131 132 .cs_files_title {
132 133 margin: @pagepadding 0 0;
133 134 }
134 135
135 136 input.inline[type="file"] {
136 137 display: inline;
137 138 }
138 139
139 140 .error_page {
140 141 margin: 10% auto;
141 142
142 143 h1 {
143 144 color: @grey2;
144 145 }
145 146
146 147 .error-branding {
147 148 font-family: @text-semibold;
148 149 color: @grey4;
149 150 }
150 151
151 152 .error_message {
152 153 font-family: @text-regular;
153 154 }
154 155
155 156 .sidebar {
156 157 min-height: 275px;
157 158 margin: 0;
158 159 padding: 0 0 @sidebarpadding @sidebarpadding;
159 160 border: none;
160 161 }
161 162
162 163 .main-content {
163 164 position: relative;
164 165 margin: 0 @sidebarpadding @sidebarpadding;
165 166 padding: 0 0 0 @sidebarpadding;
166 167 border-left: @border-thickness solid @grey5;
167 168
168 169 @media (max-width:767px) {
169 170 clear: both;
170 171 width: 100%;
171 172 margin: 0;
172 173 border: none;
173 174 }
174 175 }
175 176
176 177 .inner-column {
177 178 float: left;
178 179 width: 29.75%;
179 180 min-height: 150px;
180 181 margin: @sidebarpadding 2% 0 0;
181 182 padding: 0 2% 0 0;
182 183 border-right: @border-thickness solid @grey5;
183 184
184 185 @media (max-width:767px) {
185 186 clear: both;
186 187 width: 100%;
187 188 border: none;
188 189 }
189 190
190 191 ul {
191 192 padding-left: 1.25em;
192 193 }
193 194
194 195 &:last-child {
195 196 margin: @sidebarpadding 0 0;
196 197 border: none;
197 198 }
198 199
199 200 h4 {
200 201 margin: 0 0 @padding;
201 202 font-family: @text-semibold;
202 203 }
203 204 }
204 205 }
205 206 .error-page-logo {
206 207 width: 130px;
207 208 height: 160px;
208 209 }
209 210
210 211 // HEADER
211 212 .header {
212 213
213 214 // TODO: johbo: Fix login pages, so that they work without a min-height
214 215 // for the header and then remove the min-height. I chose a smaller value
215 216 // intentionally here to avoid rendering issues in the main navigation.
216 217 min-height: 49px;
217 218
218 219 position: relative;
219 220 vertical-align: bottom;
220 221 padding: 0 @header-padding;
221 222 background-color: @grey2;
222 223 color: @grey5;
223 224
224 225 .title {
225 226 overflow: visible;
226 227 }
227 228
228 229 &:before,
229 230 &:after {
230 231 content: "";
231 232 clear: both;
232 233 width: 100%;
233 234 }
234 235
235 236 // TODO: johbo: Avoids breaking "Repositories" chooser
236 237 .select2-container .select2-choice .select2-arrow {
237 238 display: none;
238 239 }
239 240 }
240 241
241 242 #header-inner {
242 243 &.title {
243 244 margin: 0;
244 245 }
245 246 &:before,
246 247 &:after {
247 248 content: "";
248 249 clear: both;
249 250 }
250 251 }
251 252
252 253 // Gists
253 254 #files_data {
254 255 clear: both; //for firefox
255 256 }
256 257 #gistid {
257 258 margin-right: @padding;
258 259 }
259 260
260 261 // Global Settings Editor
261 262 .textarea.editor {
262 263 float: left;
263 264 position: relative;
264 265 max-width: @texteditor-width;
265 266
266 267 select {
267 268 position: absolute;
268 269 top:10px;
269 270 right:0;
270 271 }
271 272
272 273 .CodeMirror {
273 274 margin: 0;
274 275 }
275 276
276 277 .help-block {
277 278 margin: 0 0 @padding;
278 279 padding:.5em;
279 280 background-color: @grey6;
280 281 }
281 282 }
282 283
283 284 ul.auth_plugins {
284 285 margin: @padding 0 @padding @legend-width;
285 286 padding: 0;
286 287
287 288 li {
288 289 margin-bottom: @padding;
289 290 line-height: 1em;
290 291 list-style-type: none;
291 292
292 293 .auth_buttons .btn {
293 294 margin-right: @padding;
294 295 }
295 296
296 297 &:before { content: none; }
297 298 }
298 299 }
299 300
300 301
301 302 // My Account PR list
302 303
303 304 #show_closed {
304 305 margin: 0 1em 0 0;
305 306 }
306 307
307 308 .pullrequestlist {
308 309 .closed {
309 310 background-color: @grey6;
310 311 }
311 312 .td-status {
312 313 padding-left: .5em;
313 314 }
314 315 .log-container .truncate {
315 316 height: 2.75em;
316 317 white-space: pre-line;
317 318 }
318 319 table.rctable .user {
319 320 padding-left: 0;
320 321 }
321 322 table.rctable {
322 323 td.td-description,
323 324 .rc-user {
324 325 min-width: auto;
325 326 }
326 327 }
327 328 }
328 329
329 330 // Pull Requests
330 331
331 332 .pullrequests_section_head {
332 333 display: block;
333 334 clear: both;
334 335 margin: @padding 0;
335 336 font-family: @text-bold;
336 337 }
337 338
338 339 .pr-origininfo, .pr-targetinfo {
339 340 position: relative;
340 341
341 342 .tag {
342 343 display: inline-block;
343 344 margin: 0 1em .5em 0;
344 345 }
345 346
346 347 .clone-url {
347 348 display: inline-block;
348 349 margin: 0 0 .5em 0;
349 350 padding: 0;
350 351 line-height: 1.2em;
351 352 }
352 353 }
353 354
354 355 .pr-pullinfo {
355 356 clear: both;
356 357 margin: .5em 0;
357 358 }
358 359
359 360 #pr-title-input {
360 361 width: 72%;
361 362 font-size: 1em;
362 363 font-family: @text-bold;
363 364 margin: 0;
364 365 padding: 0 0 0 @padding/4;
365 366 line-height: 1.7em;
366 367 color: @text-color;
367 368 letter-spacing: .02em;
368 369 }
369 370
370 371 #pullrequest_title {
371 372 width: 100%;
372 373 box-sizing: border-box;
373 374 }
374 375
375 376 #pr_open_message {
376 377 border: @border-thickness solid #fff;
377 378 border-radius: @border-radius;
378 379 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
379 380 text-align: right;
380 381 overflow: hidden;
381 382 }
382 383
383 384 .pr-submit-button {
384 385 float: right;
385 386 margin: 0 0 0 5px;
386 387 }
387 388
388 389 .pr-spacing-container {
389 390 padding: 20px;
390 391 clear: both
391 392 }
392 393
393 394 #pr-description-input {
394 395 margin-bottom: 0;
395 396 }
396 397
397 398 .pr-description-label {
398 399 vertical-align: top;
399 400 }
400 401
401 402 .perms_section_head {
402 403 min-width: 625px;
403 404
404 405 h2 {
405 406 margin-bottom: 0;
406 407 }
407 408
408 409 .label-checkbox {
409 410 float: left;
410 411 }
411 412
412 413 &.field {
413 414 margin: @space 0 @padding;
414 415 }
415 416
416 417 &:first-child.field {
417 418 margin-top: 0;
418 419
419 420 .label {
420 421 margin-top: 0;
421 422 padding-top: 0;
422 423 }
423 424
424 425 .radios {
425 426 padding-top: 0;
426 427 }
427 428 }
428 429
429 430 .radios {
430 431 float: right;
431 432 position: relative;
432 433 width: 405px;
433 434 }
434 435 }
435 436
436 437 //--- MODULES ------------------//
437 438
438 439
439 440 // Fixed Sidebar Column
440 441 .sidebar-col-wrapper {
441 442 padding-left: @sidebar-all-width;
442 443
443 444 .sidebar {
444 445 width: @sidebar-width;
445 446 margin-left: -@sidebar-all-width;
446 447 }
447 448 }
448 449
449 450 .sidebar-col-wrapper.scw-small {
450 451 padding-left: @sidebar-small-all-width;
451 452
452 453 .sidebar {
453 454 width: @sidebar-small-width;
454 455 margin-left: -@sidebar-small-all-width;
455 456 }
456 457 }
457 458
458 459
459 460 // FOOTER
460 461 #footer {
461 462 padding: 0;
462 463 text-align: center;
463 464 vertical-align: middle;
464 465 color: @grey2;
465 466 background-color: @grey6;
466 467
467 468 p {
468 469 margin: 0;
469 470 padding: 1em;
470 471 line-height: 1em;
471 472 }
472 473
473 474 .server-instance { //server instance
474 475 display: none;
475 476 }
476 477
477 478 .title {
478 479 float: none;
479 480 margin: 0 auto;
480 481 }
481 482 }
482 483
483 484 button.close {
484 485 padding: 0;
485 486 cursor: pointer;
486 487 background: transparent;
487 488 border: 0;
488 489 .box-shadow(none);
489 490 -webkit-appearance: none;
490 491 }
491 492
492 493 .close {
493 494 float: right;
494 495 font-size: 21px;
495 496 font-family: @text-bootstrap;
496 497 line-height: 1em;
497 498 font-weight: bold;
498 499 color: @grey2;
499 500
500 501 &:hover,
501 502 &:focus {
502 503 color: @grey1;
503 504 text-decoration: none;
504 505 cursor: pointer;
505 506 }
506 507 }
507 508
508 509 // GRID
509 510 .sorting,
510 511 .sorting_desc,
511 512 .sorting_asc {
512 513 cursor: pointer;
513 514 }
514 515 .sorting_desc:after {
515 516 content: "\00A0\25B2";
516 517 font-size: .75em;
517 518 }
518 519 .sorting_asc:after {
519 520 content: "\00A0\25BC";
520 521 font-size: .68em;
521 522 }
522 523
523 524
524 525 .user_auth_tokens {
525 526
526 527 &.truncate {
527 528 white-space: nowrap;
528 529 overflow: hidden;
529 530 text-overflow: ellipsis;
530 531 }
531 532
532 533 .fields .field .input {
533 534 margin: 0;
534 535 }
535 536
536 537 input#description {
537 538 width: 100px;
538 539 margin: 0;
539 540 }
540 541
541 542 .drop-menu {
542 543 // TODO: johbo: Remove this, should work out of the box when
543 544 // having multiple inputs inline
544 545 margin: 0 0 0 5px;
545 546 }
546 547 }
547 548 #user_list_table {
548 549 .closed {
549 550 background-color: @grey6;
550 551 }
551 552 }
552 553
553 554
554 555 input {
555 556 &.disabled {
556 557 opacity: .5;
557 558 }
558 559 }
559 560
560 561 // remove extra padding in firefox
561 562 input::-moz-focus-inner { border:0; padding:0 }
562 563
563 564 .adjacent input {
564 565 margin-bottom: @padding;
565 566 }
566 567
567 568 .permissions_boxes {
568 569 display: block;
569 570 }
570 571
571 572 //TODO: lisa: this should be in tables
572 573 .show_more_col {
573 574 width: 20px;
574 575 }
575 576
576 577 //FORMS
577 578
578 579 .medium-inline,
579 580 input#description.medium-inline {
580 581 display: inline;
581 582 width: @medium-inline-input-width;
582 583 min-width: 100px;
583 584 }
584 585
585 586 select {
586 587 //reset
587 588 -webkit-appearance: none;
588 589 -moz-appearance: none;
589 590
590 591 display: inline-block;
591 592 height: 28px;
592 593 width: auto;
593 594 margin: 0 @padding @padding 0;
594 595 padding: 0 18px 0 8px;
595 596 line-height:1em;
596 597 font-size: @basefontsize;
597 598 border: @border-thickness solid @rcblue;
598 599 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
599 600 color: @rcblue;
600 601
601 602 &:after {
602 603 content: "\00A0\25BE";
603 604 }
604 605
605 606 &:focus {
606 607 outline: none;
607 608 }
608 609 }
609 610
610 611 option {
611 612 &:focus {
612 613 outline: none;
613 614 }
614 615 }
615 616
616 617 input,
617 618 textarea {
618 619 padding: @input-padding;
619 620 border: @input-border-thickness solid @border-highlight-color;
620 621 .border-radius (@border-radius);
621 622 font-family: @text-light;
622 623 font-size: @basefontsize;
623 624
624 625 &.input-sm {
625 626 padding: 5px;
626 627 }
627 628
628 629 &#description {
629 630 min-width: @input-description-minwidth;
630 631 min-height: 1em;
631 632 padding: 10px;
632 633 }
633 634 }
634 635
635 636 .field-sm {
636 637 input,
637 638 textarea {
638 639 padding: 5px;
639 640 }
640 641 }
641 642
642 643 textarea {
643 644 display: block;
644 645 clear: both;
645 646 width: 100%;
646 647 min-height: 100px;
647 648 margin-bottom: @padding;
648 649 .box-sizing(border-box);
649 650 overflow: auto;
650 651 }
651 652
652 653 label {
653 654 font-family: @text-light;
654 655 }
655 656
656 657 // GRAVATARS
657 658 // centers gravatar on username to the right
658 659
659 660 .gravatar {
660 661 display: inline;
661 662 min-width: 16px;
662 663 min-height: 16px;
663 664 margin: -5px 0;
664 665 padding: 0;
665 666 line-height: 1em;
666 667 border: 1px solid @grey4;
667 668
668 669 &.gravatar-large {
669 670 margin: -0.5em .25em -0.5em 0;
670 671 }
671 672
672 673 & + .user {
673 674 display: inline;
674 675 margin: 0;
675 676 padding: 0 0 0 .17em;
676 677 line-height: 1em;
677 678 }
678 679 }
679 680
680 681 .user-inline-data {
681 682 display: inline-block;
682 683 float: left;
683 684 padding-left: .5em;
684 685 line-height: 1.3em;
685 686 }
686 687
687 688 .rc-user { // gravatar + user wrapper
688 689 float: left;
689 690 position: relative;
690 691 min-width: 100px;
691 692 max-width: 200px;
692 693 min-height: (@gravatar-size + @border-thickness * 2); // account for border
693 694 display: block;
694 695 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
695 696
696 697
697 698 .gravatar {
698 699 display: block;
699 700 position: absolute;
700 701 top: 0;
701 702 left: 0;
702 703 min-width: @gravatar-size;
703 704 min-height: @gravatar-size;
704 705 margin: 0;
705 706 }
706 707
707 708 .user {
708 709 display: block;
709 710 max-width: 175px;
710 711 padding-top: 2px;
711 712 overflow: hidden;
712 713 text-overflow: ellipsis;
713 714 }
714 715 }
715 716
716 717 .gist-gravatar,
717 718 .journal_container {
718 719 .gravatar-large {
719 720 margin: 0 .5em -10px 0;
720 721 }
721 722 }
722 723
723 724
724 725 // ADMIN SETTINGS
725 726
726 727 // Tag Patterns
727 728 .tag_patterns {
728 729 .tag_input {
729 730 margin-bottom: @padding;
730 731 }
731 732 }
732 733
733 734 .locked_input {
734 735 position: relative;
735 736
736 737 input {
737 738 display: inline;
738 739 margin-top: 3px;
739 740 }
740 741
741 742 br {
742 743 display: none;
743 744 }
744 745
745 746 .error-message {
746 747 float: left;
747 748 width: 100%;
748 749 }
749 750
750 751 .lock_input_button {
751 752 display: inline;
752 753 }
753 754
754 755 .help-block {
755 756 clear: both;
756 757 }
757 758 }
758 759
759 760 // Notifications
760 761
761 762 .notifications_buttons {
762 763 margin: 0 0 @space 0;
763 764 padding: 0;
764 765
765 766 .btn {
766 767 display: inline-block;
767 768 }
768 769 }
769 770
770 771 .notification-list {
771 772
772 773 div {
773 774 display: inline-block;
774 775 vertical-align: middle;
775 776 }
776 777
777 778 .container {
778 779 display: block;
779 780 margin: 0 0 @padding 0;
780 781 }
781 782
782 783 .delete-notifications {
783 784 margin-left: @padding;
784 785 text-align: right;
785 786 cursor: pointer;
786 787 }
787 788
788 789 .read-notifications {
789 790 margin-left: @padding/2;
790 791 text-align: right;
791 792 width: 35px;
792 793 cursor: pointer;
793 794 }
794 795
795 796 .icon-minus-sign {
796 797 color: @alert2;
797 798 }
798 799
799 800 .icon-ok-sign {
800 801 color: @alert1;
801 802 }
802 803 }
803 804
804 805 .user_settings {
805 806 float: left;
806 807 clear: both;
807 808 display: block;
808 809 width: 100%;
809 810
810 811 .gravatar_box {
811 812 margin-bottom: @padding;
812 813
813 814 &:after {
814 815 content: " ";
815 816 clear: both;
816 817 width: 100%;
817 818 }
818 819 }
819 820
820 821 .fields .field {
821 822 clear: both;
822 823 }
823 824 }
824 825
825 826 .advanced_settings {
826 827 margin-bottom: @space;
827 828
828 829 .help-block {
829 830 margin-left: 0;
830 831 }
831 832
832 833 button + .help-block {
833 834 margin-top: @padding;
834 835 }
835 836 }
836 837
837 838 // admin settings radio buttons and labels
838 839 .label-2 {
839 840 float: left;
840 841 width: @label2-width;
841 842
842 843 label {
843 844 color: @grey1;
844 845 }
845 846 }
846 847 .checkboxes {
847 848 float: left;
848 849 width: @checkboxes-width;
849 850 margin-bottom: @padding;
850 851
851 852 .checkbox {
852 853 width: 100%;
853 854
854 855 label {
855 856 margin: 0;
856 857 padding: 0;
857 858 }
858 859 }
859 860
860 861 .checkbox + .checkbox {
861 862 display: inline-block;
862 863 }
863 864
864 865 label {
865 866 margin-right: 1em;
866 867 }
867 868 }
868 869
869 870 // CHANGELOG
870 871 .container_header {
871 872 float: left;
872 873 display: block;
873 874 width: 100%;
874 875 margin: @padding 0 @padding;
875 876
876 877 #filter_changelog {
877 878 float: left;
878 879 margin-right: @padding;
879 880 }
880 881
881 882 .breadcrumbs_light {
882 883 display: inline-block;
883 884 }
884 885 }
885 886
886 887 .info_box {
887 888 float: right;
888 889 }
889 890
890 891
891 892 #graph_nodes {
892 893 padding-top: 43px;
893 894 }
894 895
895 896 #graph_content{
896 897
897 898 // adjust for table headers so that graph renders properly
898 899 // #graph_nodes padding - table cell padding
899 900 padding-top: (@space - (@basefontsize * 2.4));
900 901
901 902 &.graph_full_width {
902 903 width: 100%;
903 904 max-width: 100%;
904 905 }
905 906 }
906 907
907 908 #graph {
908 909 .flag_status {
909 910 margin: 0;
910 911 }
911 912
912 913 .pagination-left {
913 914 float: left;
914 915 clear: both;
915 916 }
916 917
917 918 .log-container {
918 919 max-width: 345px;
919 920
920 921 .message{
921 922 max-width: 340px;
922 923 }
923 924 }
924 925
925 926 .graph-col-wrapper {
926 927 padding-left: 110px;
927 928
928 929 #graph_nodes {
929 930 width: 100px;
930 931 margin-left: -110px;
931 932 float: left;
932 933 clear: left;
933 934 }
934 935 }
935 936 }
936 937
937 938 #filter_changelog {
938 939 float: left;
939 940 }
940 941
941 942
942 943 //--- THEME ------------------//
943 944
944 945 #logo {
945 946 float: left;
946 947 margin: 9px 0 0 0;
947 948
948 949 .header {
949 950 background-color: transparent;
950 951 }
951 952
952 953 a {
953 954 display: inline-block;
954 955 }
955 956
956 957 img {
957 958 height:30px;
958 959 }
959 960 }
960 961
961 962 .logo-wrapper {
962 963 float:left;
963 964 }
964 965
965 966 .branding{
966 967 float: left;
967 968 padding: 9px 2px;
968 969 line-height: 1em;
969 970 font-size: @navigation-fontsize;
970 971 }
971 972
972 973 img {
973 974 border: none;
974 975 outline: none;
975 976 }
976 977 user-profile-header
977 978 label {
978 979
979 980 input[type="checkbox"] {
980 981 margin-right: 1em;
981 982 }
982 983 input[type="radio"] {
983 984 margin-right: 1em;
984 985 }
985 986 }
986 987
987 988 .flag_status {
988 989 margin: 2px 8px 6px 2px;
989 990 &.under_review {
990 991 .circle(5px, @alert3);
991 992 }
992 993 &.approved {
993 994 .circle(5px, @alert1);
994 995 }
995 996 &.rejected,
996 997 &.forced_closed{
997 998 .circle(5px, @alert2);
998 999 }
999 1000 &.not_reviewed {
1000 1001 .circle(5px, @grey5);
1001 1002 }
1002 1003 }
1003 1004
1004 1005 .flag_status_comment_box {
1005 1006 margin: 5px 6px 0px 2px;
1006 1007 }
1007 1008 .test_pattern_preview {
1008 1009 margin: @space 0;
1009 1010
1010 1011 p {
1011 1012 margin-bottom: 0;
1012 1013 border-bottom: @border-thickness solid @border-default-color;
1013 1014 color: @grey3;
1014 1015 }
1015 1016
1016 1017 .btn {
1017 1018 margin-bottom: @padding;
1018 1019 }
1019 1020 }
1020 1021 #test_pattern_result {
1021 1022 display: none;
1022 1023 &:extend(pre);
1023 1024 padding: .9em;
1024 1025 color: @grey3;
1025 1026 background-color: @grey7;
1026 1027 border-right: @border-thickness solid @border-default-color;
1027 1028 border-bottom: @border-thickness solid @border-default-color;
1028 1029 border-left: @border-thickness solid @border-default-color;
1029 1030 }
1030 1031
1031 1032 #repo_vcs_settings {
1032 1033 #inherit_overlay_vcs_default {
1033 1034 display: none;
1034 1035 }
1035 1036 #inherit_overlay_vcs_custom {
1036 1037 display: custom;
1037 1038 }
1038 1039 &.inherited {
1039 1040 #inherit_overlay_vcs_default {
1040 1041 display: block;
1041 1042 }
1042 1043 #inherit_overlay_vcs_custom {
1043 1044 display: none;
1044 1045 }
1045 1046 }
1046 1047 }
1047 1048
1048 1049 .issue-tracker-link {
1049 1050 color: @rcblue;
1050 1051 }
1051 1052
1052 1053 // Issue Tracker Table Show/Hide
1053 1054 #repo_issue_tracker {
1054 1055 #inherit_overlay {
1055 1056 display: none;
1056 1057 }
1057 1058 #custom_overlay {
1058 1059 display: custom;
1059 1060 }
1060 1061 &.inherited {
1061 1062 #inherit_overlay {
1062 1063 display: block;
1063 1064 }
1064 1065 #custom_overlay {
1065 1066 display: none;
1066 1067 }
1067 1068 }
1068 1069 }
1069 1070 table.issuetracker {
1070 1071 &.readonly {
1071 1072 tr, td {
1072 1073 color: @grey3;
1073 1074 }
1074 1075 }
1075 1076 .edit {
1076 1077 display: none;
1077 1078 }
1078 1079 .editopen {
1079 1080 .edit {
1080 1081 display: inline;
1081 1082 }
1082 1083 .entry {
1083 1084 display: none;
1084 1085 }
1085 1086 }
1086 1087 tr td.td-action {
1087 1088 min-width: 117px;
1088 1089 }
1089 1090 td input {
1090 1091 max-width: none;
1091 1092 min-width: 30px;
1092 1093 width: 80%;
1093 1094 }
1094 1095 .issuetracker_pref input {
1095 1096 width: 40%;
1096 1097 }
1097 1098 input.edit_issuetracker_update {
1098 1099 margin-right: 0;
1099 1100 width: auto;
1100 1101 }
1101 1102 }
1102 1103
1103 1104
1104 1105 //Permissions Settings
1105 1106 #add_perm {
1106 1107 margin: 0 0 @padding;
1107 1108 cursor: pointer;
1108 1109 }
1109 1110
1110 1111 .perm_ac {
1111 1112 input {
1112 1113 width: 95%;
1113 1114 }
1114 1115 }
1115 1116
1116 1117 .autocomplete-suggestions {
1117 1118 width: auto !important; // overrides autocomplete.js
1118 1119 margin: 0;
1119 1120 border: @border-thickness solid @rcblue;
1120 1121 border-radius: @border-radius;
1121 1122 color: @rcblue;
1122 1123 background-color: white;
1123 1124 }
1124 1125 .autocomplete-selected {
1125 1126 background: #F0F0F0;
1126 1127 }
1127 1128 .ac-container-wrap {
1128 1129 margin: 0;
1129 1130 padding: 8px;
1130 1131 border-bottom: @border-thickness solid @rclightblue;
1131 1132 list-style-type: none;
1132 1133 cursor: pointer;
1133 1134
1134 1135 &:hover {
1135 1136 background-color: @rclightblue;
1136 1137 }
1137 1138
1138 1139 img {
1139 1140 margin-right: 1em;
1140 1141 }
1141 1142
1142 1143 strong {
1143 1144 font-weight: normal;
1144 1145 }
1145 1146 }
1146 1147
1147 1148 // Settings Dropdown
1148 1149 .user-menu .container {
1149 1150 padding: 0 4px;
1150 1151 margin: 0;
1151 1152 }
1152 1153
1153 1154 .user-menu .gravatar {
1154 1155 cursor: pointer;
1155 1156 }
1156 1157
1157 1158 .codeblock {
1158 1159 margin-bottom: @padding;
1159 1160 clear: both;
1160 1161
1161 1162 .stats{
1162 1163 overflow: hidden;
1163 1164 }
1164 1165
1165 1166 .message{
1166 1167 textarea{
1167 1168 margin: 0;
1168 1169 }
1169 1170 }
1170 1171
1171 1172 .code-header {
1172 1173 .stats {
1173 1174 line-height: 2em;
1174 1175
1175 1176 .revision_id {
1176 1177 margin-left: 0;
1177 1178 }
1178 1179 .buttons {
1179 1180 padding-right: 0;
1180 1181 }
1181 1182 }
1182 1183
1183 1184 .item{
1184 1185 margin-right: 0.5em;
1185 1186 }
1186 1187 }
1187 1188
1188 1189 #editor_container{
1189 1190 position: relative;
1190 1191 margin: @padding;
1191 1192 }
1192 1193 }
1193 1194
1194 1195 #file_history_container {
1195 1196 display: none;
1196 1197 }
1197 1198
1198 1199 .file-history-inner {
1199 1200 margin-bottom: 10px;
1200 1201 }
1201 1202
1202 1203 // Pull Requests
1203 1204 .summary-details {
1204 1205 width: 72%;
1205 1206 }
1206 1207 .pr-summary {
1207 1208 border-bottom: @border-thickness solid @grey5;
1208 1209 margin-bottom: @space;
1209 1210 }
1210 1211 .reviewers-title {
1211 1212 width: 25%;
1212 1213 min-width: 200px;
1213 1214 }
1214 1215 .reviewers {
1215 1216 width: 25%;
1216 1217 min-width: 200px;
1217 1218 }
1218 1219 .reviewers ul li {
1219 1220 position: relative;
1220 1221 width: 100%;
1221 1222 margin-bottom: 8px;
1222 1223 }
1223 1224 .reviewers_member {
1224 1225 width: 100%;
1225 1226 overflow: auto;
1226 1227 }
1227 1228 .reviewer_status {
1228 1229 display: inline-block;
1229 1230 vertical-align: top;
1230 1231 width: 7%;
1231 1232 min-width: 20px;
1232 1233 height: 1.2em;
1233 1234 margin-top: 3px;
1234 1235 line-height: 1em;
1235 1236 }
1236 1237
1237 1238 .reviewer_name {
1238 1239 display: inline-block;
1239 1240 max-width: 83%;
1240 1241 padding-right: 20px;
1241 1242 vertical-align: middle;
1242 1243 line-height: 1;
1243 1244
1244 1245 .rc-user {
1245 1246 min-width: 0;
1246 1247 margin: -2px 1em 0 0;
1247 1248 }
1248 1249
1249 1250 .reviewer {
1250 1251 float: left;
1251 1252 }
1252 1253
1253 1254 &.to-delete {
1254 1255 .user,
1255 1256 .reviewer {
1256 1257 text-decoration: line-through;
1257 1258 }
1258 1259 }
1259 1260 }
1260 1261
1261 1262 .reviewer_member_remove {
1262 1263 position: absolute;
1263 1264 right: 0;
1264 1265 top: 0;
1265 1266 width: 16px;
1266 1267 margin-bottom: 10px;
1267 1268 padding: 0;
1268 1269 color: black;
1269 1270 }
1270 1271 .reviewer_member_status {
1271 1272 margin-top: 5px;
1272 1273 }
1273 1274 .pr-summary #summary{
1274 1275 width: 100%;
1275 1276 }
1276 1277 .pr-summary .action_button:hover {
1277 1278 border: 0;
1278 1279 cursor: pointer;
1279 1280 }
1280 1281 .pr-details-title {
1281 1282 padding-bottom: 8px;
1282 1283 border-bottom: @border-thickness solid @grey5;
1283 1284 .action_button {
1284 1285 color: @rcblue;
1285 1286 }
1286 1287 }
1287 1288 .pr-details-content {
1288 1289 margin-top: @textmargin;
1289 1290 margin-bottom: @textmargin;
1290 1291 }
1291 1292 .pr-description {
1292 1293 white-space:pre-wrap;
1293 1294 }
1294 1295 .group_members {
1295 1296 margin-top: 0;
1296 1297 padding: 0;
1297 1298 list-style: outside none none;
1298 1299 }
1299 1300 .reviewer_ac .ac-input {
1300 1301 width: 92%;
1301 1302 margin-bottom: 1em;
1302 1303 }
1303 1304 #update_commits {
1304 1305 float: right;
1305 1306 }
1306 1307 .compare_view_commits tr{
1307 1308 height: 20px;
1308 1309 }
1309 1310 .compare_view_commits td {
1310 1311 vertical-align: top;
1311 1312 padding-top: 10px;
1312 1313 }
1313 1314 .compare_view_commits .author {
1314 1315 margin-left: 5px;
1315 1316 }
1316 1317
1317 1318 .compare_view_files {
1318 1319 width: 100%;
1319 1320
1320 1321 td {
1321 1322 vertical-align: middle;
1322 1323 }
1323 1324 }
1324 1325
1325 1326 .compare_view_filepath {
1326 1327 color: @grey1;
1327 1328 }
1328 1329
1329 1330 .show_more {
1330 1331 display: inline-block;
1331 1332 position: relative;
1332 1333 vertical-align: middle;
1333 1334 width: 4px;
1334 1335 height: @basefontsize;
1335 1336
1336 1337 &:after {
1337 1338 content: "\00A0\25BE";
1338 1339 display: inline-block;
1339 1340 width:10px;
1340 1341 line-height: 5px;
1341 1342 font-size: 12px;
1342 1343 cursor: pointer;
1343 1344 }
1344 1345 }
1345 1346
1346 1347 .journal_more .show_more {
1347 1348 display: inline;
1348 1349
1349 1350 &:after {
1350 1351 content: none;
1351 1352 }
1352 1353 }
1353 1354
1354 1355 .open .show_more:after,
1355 1356 .select2-dropdown-open .show_more:after {
1356 1357 .rotate(180deg);
1357 1358 margin-left: 4px;
1358 1359 }
1359 1360
1360 1361
1361 1362 .compare_view_commits .collapse_commit:after {
1362 1363 cursor: pointer;
1363 1364 content: "\00A0\25B4";
1364 1365 margin-left: -3px;
1365 1366 font-size: 17px;
1366 1367 color: @grey4;
1367 1368 }
1368 1369
1369 1370 .diff_links {
1370 1371 margin-left: 8px;
1371 1372 }
1372 1373
1373 1374 p.ancestor {
1374 1375 margin: @padding 0;
1375 1376 }
1376 1377
1377 1378 .cs_icon_td input[type="checkbox"] {
1378 1379 display: none;
1379 1380 }
1380 1381
1381 1382 .cs_icon_td .expand_file_icon:after {
1382 1383 cursor: pointer;
1383 1384 content: "\00A0\25B6";
1384 1385 font-size: 12px;
1385 1386 color: @grey4;
1386 1387 }
1387 1388
1388 1389 .cs_icon_td .collapse_file_icon:after {
1389 1390 cursor: pointer;
1390 1391 content: "\00A0\25BC";
1391 1392 font-size: 12px;
1392 1393 color: @grey4;
1393 1394 }
1394 1395
1395 1396 /*new binary
1396 1397 NEW_FILENODE = 1
1397 1398 DEL_FILENODE = 2
1398 1399 MOD_FILENODE = 3
1399 1400 RENAMED_FILENODE = 4
1400 1401 COPIED_FILENODE = 5
1401 1402 CHMOD_FILENODE = 6
1402 1403 BIN_FILENODE = 7
1403 1404 */
1404 1405 .cs_files_expand {
1405 1406 font-size: @basefontsize + 5px;
1406 1407 line-height: 1.8em;
1407 1408 float: right;
1408 1409 }
1409 1410
1410 1411 .cs_files_expand span{
1411 1412 color: @rcblue;
1412 1413 cursor: pointer;
1413 1414 }
1414 1415 .cs_files {
1415 1416 clear: both;
1416 1417 padding-bottom: @padding;
1417 1418
1418 1419 .cur_cs {
1419 1420 margin: 10px 2px;
1420 1421 font-weight: bold;
1421 1422 }
1422 1423
1423 1424 .node {
1424 1425 float: left;
1425 1426 }
1426 1427
1427 1428 .changes {
1428 1429 float: right;
1429 1430 color: white;
1430 1431 font-size: @basefontsize - 4px;
1431 1432 margin-top: 4px;
1432 1433 opacity: 0.6;
1433 1434 filter: Alpha(opacity=60); /* IE8 and earlier */
1434 1435
1435 1436 .added {
1436 1437 background-color: @alert1;
1437 1438 float: left;
1438 1439 text-align: center;
1439 1440 }
1440 1441
1441 1442 .deleted {
1442 1443 background-color: @alert2;
1443 1444 float: left;
1444 1445 text-align: center;
1445 1446 }
1446 1447
1447 1448 .bin {
1448 1449 background-color: @alert1;
1449 1450 text-align: center;
1450 1451 }
1451 1452
1452 1453 /*new binary*/
1453 1454 .bin.bin1 {
1454 1455 background-color: @alert1;
1455 1456 text-align: center;
1456 1457 }
1457 1458
1458 1459 /*deleted binary*/
1459 1460 .bin.bin2 {
1460 1461 background-color: @alert2;
1461 1462 text-align: center;
1462 1463 }
1463 1464
1464 1465 /*mod binary*/
1465 1466 .bin.bin3 {
1466 1467 background-color: @grey2;
1467 1468 text-align: center;
1468 1469 }
1469 1470
1470 1471 /*rename file*/
1471 1472 .bin.bin4 {
1472 1473 background-color: @alert4;
1473 1474 text-align: center;
1474 1475 }
1475 1476
1476 1477 /*copied file*/
1477 1478 .bin.bin5 {
1478 1479 background-color: @alert4;
1479 1480 text-align: center;
1480 1481 }
1481 1482
1482 1483 /*chmod file*/
1483 1484 .bin.bin6 {
1484 1485 background-color: @grey2;
1485 1486 text-align: center;
1486 1487 }
1487 1488 }
1488 1489 }
1489 1490
1490 1491 .cs_files .cs_added, .cs_files .cs_A,
1491 1492 .cs_files .cs_added, .cs_files .cs_M,
1492 1493 .cs_files .cs_added, .cs_files .cs_D {
1493 1494 height: 16px;
1494 1495 padding-right: 10px;
1495 1496 margin-top: 7px;
1496 1497 text-align: left;
1497 1498 }
1498 1499
1499 1500 .cs_icon_td {
1500 1501 min-width: 16px;
1501 1502 width: 16px;
1502 1503 }
1503 1504
1504 1505 .pull-request-merge {
1505 1506 padding: 10px 0;
1506 1507 margin-top: 10px;
1507 1508 margin-bottom: 20px;
1508 1509 }
1509 1510
1510 1511 .pull-request-merge .pull-request-wrap {
1511 1512 height: 25px;
1512 1513 padding: 5px 0;
1513 1514 }
1514 1515
1515 1516 .pull-request-merge span {
1516 1517 margin-right: 10px;
1517 1518 }
1518 1519 #close_pull_request {
1519 1520 margin-right: 0px;
1520 1521 }
1521 1522
1522 1523 .empty_data {
1523 1524 color: @grey4;
1524 1525 }
1525 1526
1526 1527 #changeset_compare_view_content {
1527 1528 margin-bottom: @space;
1528 1529 clear: both;
1529 1530 width: 100%;
1530 1531 box-sizing: border-box;
1531 1532 .border-radius(@border-radius);
1532 1533
1533 1534 .help-block {
1534 1535 margin: @padding 0;
1535 1536 color: @text-color;
1536 1537 }
1537 1538
1538 1539 .empty_data {
1539 1540 margin: @padding 0;
1540 1541 }
1541 1542
1542 1543 .alert {
1543 1544 margin-bottom: @space;
1544 1545 }
1545 1546 }
1546 1547
1547 1548 .table_disp {
1548 1549 .status {
1549 1550 width: auto;
1550 1551
1551 1552 .flag_status {
1552 1553 float: left;
1553 1554 }
1554 1555 }
1555 1556 }
1556 1557
1557 1558 .status_box_menu {
1558 1559 margin: 0;
1559 1560 }
1560 1561
1561 1562 .notification-table{
1562 1563 margin-bottom: @space;
1563 1564 display: table;
1564 1565 width: 100%;
1565 1566
1566 1567 .container{
1567 1568 display: table-row;
1568 1569
1569 1570 .notification-header{
1570 1571 border-bottom: @border-thickness solid @border-default-color;
1571 1572 }
1572 1573
1573 1574 .notification-subject{
1574 1575 display: table-cell;
1575 1576 }
1576 1577 }
1577 1578 }
1578 1579
1579 1580 // Notifications
1580 1581 .notification-header{
1581 1582 display: table;
1582 1583 width: 100%;
1583 1584 padding: floor(@basefontsize/2) 0;
1584 1585 line-height: 1em;
1585 1586
1586 1587 .desc, .delete-notifications, .read-notifications{
1587 1588 display: table-cell;
1588 1589 text-align: left;
1589 1590 }
1590 1591
1591 1592 .desc{
1592 1593 width: 1163px;
1593 1594 }
1594 1595
1595 1596 .delete-notifications, .read-notifications{
1596 1597 width: 35px;
1597 1598 min-width: 35px; //fixes when only one button is displayed
1598 1599 }
1599 1600 }
1600 1601
1601 1602 .notification-body {
1602 1603 .markdown-block,
1603 1604 .rst-block {
1604 1605 padding: @padding 0;
1605 1606 }
1606 1607
1607 1608 .notification-subject {
1608 1609 padding: @textmargin 0;
1609 1610 border-bottom: @border-thickness solid @border-default-color;
1610 1611 }
1611 1612 }
1612 1613
1613 1614
1614 1615 .notifications_buttons{
1615 1616 float: right;
1616 1617 }
1617 1618
1619 #notification-status{
1620 display: inline;
1621 }
1622
1618 1623 // Repositories
1619 1624
1620 1625 #summary.fields{
1621 1626 display: table;
1622 1627
1623 1628 .field{
1624 1629 display: table-row;
1625 1630
1626 1631 .label-summary{
1627 1632 display: table-cell;
1628 1633 min-width: @label-summary-minwidth;
1629 1634 padding-top: @padding/2;
1630 1635 padding-bottom: @padding/2;
1631 1636 padding-right: @padding/2;
1632 1637 }
1633 1638
1634 1639 .input{
1635 1640 display: table-cell;
1636 1641 padding: @padding/2;
1637 1642
1638 1643 input{
1639 1644 min-width: 29em;
1640 1645 padding: @padding/4;
1641 1646 }
1642 1647 }
1643 1648 .statistics, .downloads{
1644 1649 .disabled{
1645 1650 color: @grey4;
1646 1651 }
1647 1652 }
1648 1653 }
1649 1654 }
1650 1655
1651 1656 #summary{
1652 1657 width: 70%;
1653 1658 }
1654 1659
1655 1660
1656 1661 // Journal
1657 1662 .journal.title {
1658 1663 h5 {
1659 1664 float: left;
1660 1665 margin: 0;
1661 1666 width: 70%;
1662 1667 }
1663 1668
1664 1669 ul {
1665 1670 float: right;
1666 1671 display: inline-block;
1667 1672 margin: 0;
1668 1673 width: 30%;
1669 1674 text-align: right;
1670 1675
1671 1676 li {
1672 1677 display: inline;
1673 1678 font-size: @journal-fontsize;
1674 1679 line-height: 1em;
1675 1680
1676 1681 &:before { content: none; }
1677 1682 }
1678 1683 }
1679 1684 }
1680 1685
1681 1686 .filterexample {
1682 1687 position: absolute;
1683 1688 top: 95px;
1684 1689 left: @contentpadding;
1685 1690 color: @rcblue;
1686 1691 font-size: 11px;
1687 1692 font-family: @text-regular;
1688 1693 cursor: help;
1689 1694
1690 1695 &:hover {
1691 1696 color: @rcdarkblue;
1692 1697 }
1693 1698
1694 1699 @media (max-width:768px) {
1695 1700 position: relative;
1696 1701 top: auto;
1697 1702 left: auto;
1698 1703 display: block;
1699 1704 }
1700 1705 }
1701 1706
1702 1707
1703 1708 #journal{
1704 1709 margin-bottom: @space;
1705 1710
1706 1711 .journal_day{
1707 1712 margin-bottom: @textmargin/2;
1708 1713 padding-bottom: @textmargin/2;
1709 1714 font-size: @journal-fontsize;
1710 1715 border-bottom: @border-thickness solid @border-default-color;
1711 1716 }
1712 1717
1713 1718 .journal_container{
1714 1719 margin-bottom: @space;
1715 1720
1716 1721 .journal_user{
1717 1722 display: inline-block;
1718 1723 }
1719 1724 .journal_action_container{
1720 1725 display: block;
1721 1726 margin-top: @textmargin;
1722 1727
1723 1728 div{
1724 1729 display: inline;
1725 1730 }
1726 1731
1727 1732 div.journal_action_params{
1728 1733 display: block;
1729 1734 }
1730 1735
1731 1736 div.journal_repo:after{
1732 1737 content: "\A";
1733 1738 white-space: pre;
1734 1739 }
1735 1740
1736 1741 div.date{
1737 1742 display: block;
1738 1743 margin-bottom: @textmargin;
1739 1744 }
1740 1745 }
1741 1746 }
1742 1747 }
1743 1748
1744 1749 // Files
1745 1750 .edit-file-title {
1746 1751 border-bottom: @border-thickness solid @border-default-color;
1747 1752
1748 1753 .breadcrumbs {
1749 1754 margin-bottom: 0;
1750 1755 }
1751 1756 }
1752 1757
1753 1758 .edit-file-fieldset {
1754 1759 margin-top: @sidebarpadding;
1755 1760
1756 1761 .fieldset {
1757 1762 .left-label {
1758 1763 width: 13%;
1759 1764 }
1760 1765 .right-content {
1761 1766 width: 87%;
1762 1767 max-width: 100%;
1763 1768 }
1764 1769 .filename-label {
1765 1770 margin-top: 13px;
1766 1771 }
1767 1772 .commit-message-label {
1768 1773 margin-top: 4px;
1769 1774 }
1770 1775 .file-upload-input {
1771 1776 input {
1772 1777 display: none;
1773 1778 }
1774 1779 }
1775 1780 p {
1776 1781 margin-top: 5px;
1777 1782 }
1778 1783
1779 1784 }
1780 1785 .custom-path-link {
1781 1786 margin-left: 5px;
1782 1787 }
1783 1788 #commit {
1784 1789 resize: vertical;
1785 1790 }
1786 1791 }
1787 1792
1788 1793 .delete-file-preview {
1789 1794 max-height: 250px;
1790 1795 }
1791 1796
1792 1797 .new-file,
1793 1798 #filter_activate,
1794 1799 #filter_deactivate {
1795 1800 float: left;
1796 1801 margin: 0 0 0 15px;
1797 1802 }
1798 1803
1799 1804 h3.files_location{
1800 1805 line-height: 2.4em;
1801 1806 }
1802 1807
1803 1808 .browser-nav {
1804 1809 display: table;
1805 1810 margin-bottom: @space;
1806 1811
1807 1812
1808 1813 .info_box {
1809 1814 display: inline-table;
1810 1815 height: 2.5em;
1811 1816
1812 1817 .browser-cur-rev, .info_box_elem {
1813 1818 display: table-cell;
1814 1819 vertical-align: middle;
1815 1820 }
1816 1821
1817 1822 .info_box_elem {
1818 1823 border-top: @border-thickness solid @rcblue;
1819 1824 border-bottom: @border-thickness solid @rcblue;
1820 1825
1821 1826 #at_rev, a {
1822 1827 padding: 0.6em 0.9em;
1823 1828 margin: 0;
1824 1829 .box-shadow(none);
1825 1830 border: 0;
1826 1831 height: 12px;
1827 1832 }
1828 1833
1829 1834 input#at_rev {
1830 1835 max-width: 50px;
1831 1836 text-align: right;
1832 1837 }
1833 1838
1834 1839 &.previous {
1835 1840 border: @border-thickness solid @rcblue;
1836 1841 .disabled {
1837 1842 color: @grey4;
1838 1843 cursor: not-allowed;
1839 1844 }
1840 1845 }
1841 1846
1842 1847 &.next {
1843 1848 border: @border-thickness solid @rcblue;
1844 1849 .disabled {
1845 1850 color: @grey4;
1846 1851 cursor: not-allowed;
1847 1852 }
1848 1853 }
1849 1854 }
1850 1855
1851 1856 .browser-cur-rev {
1852 1857
1853 1858 span{
1854 1859 margin: 0;
1855 1860 color: @rcblue;
1856 1861 height: 12px;
1857 1862 display: inline-block;
1858 1863 padding: 0.7em 1em ;
1859 1864 border: @border-thickness solid @rcblue;
1860 1865 margin-right: @padding;
1861 1866 }
1862 1867 }
1863 1868 }
1864 1869
1865 1870 .search_activate {
1866 1871 display: table-cell;
1867 1872 vertical-align: middle;
1868 1873
1869 1874 input, label{
1870 1875 margin: 0;
1871 1876 padding: 0;
1872 1877 }
1873 1878
1874 1879 input{
1875 1880 margin-left: @textmargin;
1876 1881 }
1877 1882
1878 1883 }
1879 1884 }
1880 1885
1881 1886 .browser-cur-rev{
1882 1887 margin-bottom: @textmargin;
1883 1888 }
1884 1889
1885 1890 #node_filter_box_loading{
1886 1891 .info_text;
1887 1892 }
1888 1893
1889 1894 .browser-search {
1890 1895 margin: -25px 0px 5px 0px;
1891 1896 }
1892 1897
1893 1898 .node-filter {
1894 1899 font-size: @repo-title-fontsize;
1895 1900 padding: 4px 0px 0px 0px;
1896 1901
1897 1902 .node-filter-path {
1898 1903 float: left;
1899 1904 color: @grey4;
1900 1905 }
1901 1906 .node-filter-input {
1902 1907 float: left;
1903 1908 margin: -2px 0px 0px 2px;
1904 1909 input {
1905 1910 padding: 2px;
1906 1911 border: none;
1907 1912 font-size: @repo-title-fontsize;
1908 1913 }
1909 1914 }
1910 1915 }
1911 1916
1912 1917
1913 1918 .browser-result{
1914 1919 td a{
1915 1920 margin-left: 0.5em;
1916 1921 display: inline-block;
1917 1922
1918 1923 em{
1919 1924 font-family: @text-bold;
1920 1925 }
1921 1926 }
1922 1927 }
1923 1928
1924 1929 .browser-highlight{
1925 1930 background-color: @grey5-alpha;
1926 1931 }
1927 1932
1928 1933
1929 1934 // Search
1930 1935
1931 1936 .search-form{
1932 1937 #q {
1933 1938 width: @search-form-width;
1934 1939 }
1935 1940 .fields{
1936 1941 margin: 0 0 @space;
1937 1942 }
1938 1943
1939 1944 label{
1940 1945 display: inline-block;
1941 1946 margin-right: @textmargin;
1942 1947 padding-top: 0.25em;
1943 1948 }
1944 1949
1945 1950
1946 1951 .results{
1947 1952 clear: both;
1948 1953 margin: 0 0 @padding;
1949 1954 }
1950 1955 }
1951 1956
1952 1957 div.search-feedback-items {
1953 1958 display: inline-block;
1954 1959 padding:0px 0px 0px 96px;
1955 1960 }
1956 1961
1957 1962 div.search-code-body {
1958 1963 background-color: #ffffff; padding: 5px 0 5px 10px;
1959 1964 pre {
1960 1965 .match { background-color: #faffa6;}
1961 1966 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
1962 1967 }
1963 1968 }
1964 1969
1965 1970 .expand_commit.search {
1966 1971 .show_more.open {
1967 1972 height: auto;
1968 1973 max-height: none;
1969 1974 }
1970 1975 }
1971 1976
1972 1977 .search-results {
1973 1978
1974 1979 h2 {
1975 1980 margin-bottom: 0;
1976 1981 }
1977 1982 .codeblock {
1978 1983 border: none;
1979 1984 background: transparent;
1980 1985 }
1981 1986
1982 1987 .codeblock-header {
1983 1988 border: none;
1984 1989 background: transparent;
1985 1990 }
1986 1991
1987 1992 .code-body {
1988 1993 border: @border-thickness solid @border-default-color;
1989 1994 .border-radius(@border-radius);
1990 1995 }
1991 1996
1992 1997 .td-commit {
1993 1998 &:extend(pre);
1994 1999 border-bottom: @border-thickness solid @border-default-color;
1995 2000 }
1996 2001
1997 2002 .message {
1998 2003 height: auto;
1999 2004 max-width: 350px;
2000 2005 white-space: normal;
2001 2006 text-overflow: initial;
2002 2007 overflow: visible;
2003 2008
2004 2009 .match { background-color: #faffa6;}
2005 2010 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2006 2011 }
2007 2012
2008 2013 }
2009 2014
2010 2015 table.rctable td.td-search-results div {
2011 2016 max-width: 100%;
2012 2017 }
2013 2018
2014 2019 #tip-box, .tip-box{
2015 2020 padding: @menupadding/2;
2016 2021 display: block;
2017 2022 border: @border-thickness solid @border-highlight-color;
2018 2023 .border-radius(@border-radius);
2019 2024 background-color: white;
2020 2025 z-index: 99;
2021 2026 white-space: pre-wrap;
2022 2027 }
2023 2028
2024 2029 #linktt {
2025 2030 width: 79px;
2026 2031 }
2027 2032
2028 2033 #help_kb .modal-content{
2029 2034 max-width: 750px;
2030 2035 margin: 10% auto;
2031 2036
2032 2037 table{
2033 2038 td,th{
2034 2039 border-bottom: none;
2035 2040 line-height: 2.5em;
2036 2041 }
2037 2042 th{
2038 2043 padding-bottom: @textmargin/2;
2039 2044 }
2040 2045 td.keys{
2041 2046 text-align: center;
2042 2047 }
2043 2048 }
2044 2049
2045 2050 .block-left{
2046 2051 width: 45%;
2047 2052 margin-right: 5%;
2048 2053 }
2049 2054 .modal-footer{
2050 2055 clear: both;
2051 2056 }
2052 2057 .key.tag{
2053 2058 padding: 0.5em;
2054 2059 background-color: @rcblue;
2055 2060 color: white;
2056 2061 border-color: @rcblue;
2057 2062 .box-shadow(none);
2058 2063 }
2059 2064 }
2060 2065
2061 2066
2062 2067
2063 2068 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2064 2069
2065 2070 @import 'statistics-graph';
2066 2071 @import 'tables';
2067 2072 @import 'forms';
2068 2073 @import 'diff';
2069 2074 @import 'summary';
2070 2075 @import 'navigation';
2071 2076
2072 2077 //--- SHOW/HIDE SECTIONS --//
2073 2078
2074 2079 .btn-collapse {
2075 2080 float: right;
2076 2081 text-align: right;
2077 2082 font-family: @text-light;
2078 2083 font-size: @basefontsize;
2079 2084 cursor: pointer;
2080 2085 border: none;
2081 2086 color: @rcblue;
2082 2087 }
2083 2088
2084 2089 table.rctable,
2085 2090 table.dataTable {
2086 2091 .btn-collapse {
2087 2092 float: right;
2088 2093 text-align: right;
2089 2094 }
2090 2095 }
2091 2096
2092 2097
2093 2098 // TODO: johbo: Fix for IE10, this avoids that we see a border
2094 2099 // and padding around checkboxes and radio boxes. Move to the right place,
2095 2100 // or better: Remove this once we did the form refactoring.
2096 2101 input[type=checkbox],
2097 2102 input[type=radio] {
2098 2103 padding: 0;
2099 2104 border: none;
2100 2105 }
@@ -1,3 +1,4 b''
1 1 /plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed
2 2 /ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview
3 3 /ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created
4 /notifications - shows new event notifications No newline at end of file
@@ -1,51 +1,52 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('My account')} ${c.rhodecode_user.username}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${_('My Account')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_nav()">
16 16 ${self.menu_items(active='admin')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.breadcrumbs()}
23 23 </div>
24 24
25 25 <div class="sidebar-col-wrapper scw-small">
26 26 ##main
27 27 <div class="sidebar">
28 28 <ul class="nav nav-pills nav-stacked">
29 29 <li class="${'active' if c.active=='profile' or c.active=='profile_edit' else ''}"><a href="${h.url('my_account')}">${_('My Profile')}</a></li>
30 30 <li class="${'active' if c.active=='password' else ''}"><a href="${h.url('my_account_password')}">${_('Password')}</a></li>
31 31 <li class="${'active' if c.active=='auth_tokens' else ''}"><a href="${h.url('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
32 32 ## TODO: Find a better integration of oauth views into navigation.
33 33 %try:
34 34 <li class="${'active' if c.active=='oauth' else ''}"><a href="${h.route_path('my_account_oauth')}">${_('OAuth Identities')}</a></li>
35 35 %except KeyError:
36 36 %endtry
37 37 <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('my_account_emails')}">${_('My Emails')}</a></li>
38 38 <li class="${'active' if c.active=='repos' else ''}"><a href="${h.url('my_account_repos')}">${_('My Repositories')}</a></li>
39 39 <li class="${'active' if c.active=='watched' else ''}"><a href="${h.url('my_account_watched')}">${_('Watched')}</a></li>
40 40 <li class="${'active' if c.active=='pullrequests' else ''}"><a href="${h.url('my_account_pullrequests')}">${_('Pull Requests')}</a></li>
41 41 <li class="${'active' if c.active=='perms' else ''}"><a href="${h.url('my_account_perms')}">${_('My Permissions')}</a></li>
42 <li class="${'active' if c.active=='my_notifications' else ''}"><a href="${h.url('my_account_notifications')}">${_('My Live Notifications')}</a></li>
42 43 </ul>
43 44 </div>
44 45
45 46 <div class="main-content-full-width">
46 47 <%include file="/admin/my_account/my_account_${c.active}.html"/>
47 48 </div>
48 49 </div>
49 50 </div>
50 51
51 52 </%def>
@@ -1,72 +1,56 b''
1 <%namespace name="base" file="/base/base.html"/>
2
3 1 <div class="panel panel-default">
4 2 <div class="panel-heading">
5 <h3 class="panel-title">${_('Account Emails')}</h3>
3 <h3 class="panel-title">${_('Your live notification settings')}</h3>
6 4 </div>
7 5
8 6 <div class="panel-body">
9 <div class="emails_wrap">
10 <table class="rctable account_emails">
11 <tr>
12 <td class="td-user">
13 ${base.gravatar(c.user.email, 16)}
14 <span class="user email">${c.user.email}</span>
15 </td>
16 <td class="td-tags">
17 <span class="tag tag1">${_('Primary')}</span>
18 </td>
19 </tr>
20 %if c.user_email_map:
21 %for em in c.user_email_map:
22 <tr>
23 <td class="td-user">
24 ${base.gravatar(em.email, 16)}
25 <span class="user email">${em.email}</span>
26 </td>
27 <td class="td-action">
28 ${h.secure_form(url('my_account_emails'),method='delete')}
29 ${h.hidden('del_email_id',em.email_id)}
30 <button class="btn btn-link btn-danger" type="submit" id="remove_email_%s" % em.email_id
31 onclick="return confirm('${_('Confirm to delete this email: %s') % em.email}');">
32 ${_('Delete')}
33 </button>
34 ${h.end_form()}
35 </td>
36 </tr>
37 %endfor
38 %else:
39 <tr class="noborder">
40 <td colspan="3">
41 <div class="td-email">
42 ${_('No additional emails specified')}
43 </div>
44 </td>
45 </tr>
46 %endif
47 </table>
48 </div>
7
8 <p><strong>IMPORTANT:</strong> This feature requires enabled channelstream websocket server to function correctly.</p>
9
10 <p class="hidden">Status of browser notifications permission: <strong id="browser-notification-status"></strong></p>
49 11
50 <div>
51 ${h.secure_form(url('my_account_emails'), method='post')}
52 <div class="form">
53 <!-- fields -->
54 <div class="fields">
55 <div class="field">
56 <div class="label">
57 <label for="new_email">${_('New email address')}:</label>
58 </div>
59 <div class="input">
60 ${h.text('new_email', class_='medium')}
61 </div>
62 </div>
63 <div class="buttons">
64 ${h.submit('save',_('Add'),class_="btn")}
65 ${h.reset('reset',_('Reset'),class_="btn")}
66 </div>
67 </div>
68 </div>
12 ${h.secure_form(url('my_account_notifications_toggle_visibility'), method='post', id='notification-status')}
13 <button class="btn btn-default" type="submit">
14 ${_('Notifications')} <strong>${_('Enabled') if c.rhodecode_user.get_instance().user_data.get('notification_status') else _('Disabled')}</strong>
15 </button>
69 16 ${h.end_form()}
70 </div>
17
18 <a class="btn btn-info" id="test-notification">Test notification</a>
19
71 20 </div>
72 21 </div>
22
23 <script type="application/javascript">
24
25 function checkBrowserStatus(){
26 var browserStatus = 'Unknown';
27
28 if (!("Notification" in window)) {
29 browserStatus = 'Not supported'
30 }
31 else if(Notification.permission === 'denied'){
32 browserStatus = 'Denied';
33 $('.flash_msg').append('<div class="alert alert-error">Notifications are blocked on browser level - you need to enable them in your browser settings.</div>')
34 }
35 else if(Notification.permission === 'granted'){
36 browserStatus = 'Allowed';
37 }
38
39 $('#browser-notification-status').text(browserStatus);
40 };
41
42 checkBrowserStatus();
43
44 $('#test-notification').on('click', function(e){
45 var levels = ['info', 'error', 'warning', 'success'];
46 var level = levels[Math.floor(Math.random()*levels.length)];
47 var payload = {
48 message: {
49 message: 'This is a test notification.',
50 level: level,
51 testMessage: true
52 }
53 };
54 $.Topic('/notifications').publish(payload);
55 })
56 </script>
@@ -1,14 +1,14 b''
1 1 <%
2 2 from pyramid.renderers import render as pyramid_render
3 3 from pyramid.threadlocal import get_current_registry, get_current_request
4 4 pyramid_registry = get_current_registry()
5 5 %>
6 % for plugin, config in pyramid_registry.rhodecode_plugins.items():
6 % for plugin, config in getattr(pyramid_registry, 'rhodecode_plugins', {}).items():
7 7 % if config['template_hooks'].get('plugin_init_template'):
8 8 ${pyramid_render(config['template_hooks'].get('plugin_init_template'),
9 9 {'config':config}, request=get_current_request(), package='rc_ae')|n}
10 10 % endif
11 11 % endfor
12 12 <script>
13 13 $.Topic('/plugins/__REGISTER__').prepareOrPublish({});
14 14 </script>
@@ -1,138 +1,137 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3
4 4 <%
5 5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6 6
7 7 if hasattr(c, 'rhodecode_db_repo'):
8 8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10 10
11 11 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
12 12 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
13 13 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
14 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
14 15
15 16 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
16 17 %>
17 18
18 19 <html xmlns="http://www.w3.org/1999/xhtml">
19 20 <head>
20 21 <title>${self.title()}</title>
21 22 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
22 23 <%def name="robots()">
23 24 <meta name="robots" content="index, nofollow"/>
24 25 </%def>
25 26 ${self.robots()}
26 27 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
27 28
28 29 ## CSS definitions
29 30 <%def name="css()">
30 31 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
31 32 <!--[if lt IE 9]>
32 33 <link rel="stylesheet" type="text/css" href="${h.asset('css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
33 34 <![endif]-->
34 35 ## EXTRA FOR CSS
35 36 ${self.css_extra()}
36 37 </%def>
37 38 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
38 39 <%def name="css_extra()">
39 40 </%def>
40 41
41 42 ${self.css()}
42 43
43 44 ## JAVASCRIPT
44 45 <%def name="js()">
45 46 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
46 47 <script type="text/javascript">
47 48 // register templateContext to pass template variables to JS
48 49 var templateContext = ${h.json.dumps(c.template_context)|n};
49 50
50 51 var REPO_NAME = "${getattr(c, 'repo_name', '')}";
51 52 %if hasattr(c, 'rhodecode_db_repo'):
52 53 var REPO_LANDING_REV = '${c.rhodecode_db_repo.landing_rev[1]}';
53 54 var REPO_TYPE = '${c.rhodecode_db_repo.repo_type}';
54 55 %else:
55 56 var REPO_LANDING_REV = '';
56 57 var REPO_TYPE = '';
57 58 %endif
58 59 var APPLICATION_URL = "${h.url('home').rstrip('/')}";
59 60 var ASSET_URL = "${h.asset('')}";
60 61 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
61 62 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
62 63 % if getattr(c, 'rhodecode_user', None):
63 64 var USER = {name:'${c.rhodecode_user.username}'};
64 65 % else:
65 66 var USER = {name:null};
66 67 % endif
67 68
68 69 var APPENLIGHT = {
69 70 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
70 71 key: '${getattr(c, "appenlight_api_public_key", "")}',
71 72 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
72 73 requestInfo: {
73 74 % if getattr(c, 'rhodecode_user', None):
74 75 ip: '${c.rhodecode_user.ip_addr}',
75 76 username: '${c.rhodecode_user.username}'
76 77 % endif
77 78 }
78 79 };
79 80 </script>
80
81 81 <!--[if lt IE 9]>
82 82 <script language="javascript" type="text/javascript" src="${h.asset('js/excanvas.min.js')}"></script>
83 83 <![endif]-->
84 84 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
85 85 <script language="javascript" type="text/javascript" src="${h.asset('js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
86 86 ## avoide esaping the %N
87 87 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js'}";</script>
88 88
89 89
90 90 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
91 91 ${self.js_extra()}
92 92
93 93 <script type="text/javascript">
94 94 $(document).ready(function(){
95 95 show_more_event();
96 96 timeagoActivate();
97 97 })
98 98 </script>
99 99
100 100 </%def>
101 101
102 102 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
103 103 <%def name="js_extra()"></%def>
104 104 ${self.js()}
105 105
106 106 <%def name="head_extra()"></%def>
107 107 ${self.head_extra()}
108
109 108 <%include file="/base/plugins_base.html"/>
110 109
111 110 ## extra stuff
112 111 %if c.pre_code:
113 112 ${c.pre_code|n}
114 113 %endif
115 114 </head>
116 115 <body id="body">
117 116 <noscript>
118 117 <div class="noscript-error">
119 118 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
120 119 </div>
121 120 </noscript>
122 121 ## IE hacks
123 122 <!--[if IE 7]>
124 123 <script>$(document.body).addClass('ie7')</script>
125 124 <![endif]-->
126 125 <!--[if IE 8]>
127 126 <script>$(document.body).addClass('ie8')</script>
128 127 <![endif]-->
129 128 <!--[if IE 9]>
130 129 <script>$(document.body).addClass('ie9')</script>
131 130 <![endif]-->
132 131
133 132 ${next.body()}
134 133 %if c.post_code:
135 134 ${c.post_code|n}
136 135 %endif
137 136 </body>
138 137 </html>
General Comments 0
You need to be logged in to leave comments. Login now