##// END OF EJS Templates
Use space after , in lists
Mads Kiilerich -
r3987:b58ed6d6 default
parent child Browse files
Show More
@@ -1,348 +1,348 b''
1 1 """The base Controller API
2 2
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 6 import time
7 7 import traceback
8 8
9 9 from paste.auth.basic import AuthBasicAuthenticator
10 10 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
11 11 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
12 12
13 13 from pylons import config, tmpl_context as c, request, session, url
14 14 from pylons.controllers import WSGIController
15 15 from pylons.controllers.util import redirect
16 16 from pylons.templating import render_mako as render
17 17
18 18 from rhodecode import __version__, BACKENDS
19 19
20 20 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
21 21 safe_str, safe_int
22 22 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
23 23 HasPermissionAnyMiddleware, CookieStoreWrapper
24 24 from rhodecode.lib.utils import get_repo_slug
25 25 from rhodecode.model import meta
26 26
27 27 from rhodecode.model.db import Repository, RhodeCodeUi, User, RhodeCodeSetting
28 28 from rhodecode.model.notification import NotificationModel
29 29 from rhodecode.model.scm import ScmModel
30 30 from rhodecode.model.meta import Session
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 def _filter_proxy(ip):
36 36 """
37 HEADERS can have mutliple ips inside the left-most being the original
37 HEADERS can have multiple ips inside the left-most being the original
38 38 client, and each successive proxy that passed the request adding the IP
39 39 address where it received the request from.
40 40
41 41 :param ip:
42 42 """
43 43 if ',' in ip:
44 44 _ips = ip.split(',')
45 45 _first_ip = _ips[0].strip()
46 46 log.debug('Got multiple IPs %s, using %s' % (','.join(_ips), _first_ip))
47 47 return _first_ip
48 48 return ip
49 49
50 50
51 51 def _get_ip_addr(environ):
52 52 proxy_key = 'HTTP_X_REAL_IP'
53 53 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
54 54 def_key = 'REMOTE_ADDR'
55 55
56 56 ip = environ.get(proxy_key)
57 57 if ip:
58 58 return _filter_proxy(ip)
59 59
60 60 ip = environ.get(proxy_key2)
61 61 if ip:
62 62 return _filter_proxy(ip)
63 63
64 64 ip = environ.get(def_key, '0.0.0.0')
65 65 return _filter_proxy(ip)
66 66
67 67
68 68 def _get_access_path(environ):
69 69 path = environ.get('PATH_INFO')
70 70 org_req = environ.get('pylons.original_request')
71 71 if org_req:
72 72 path = org_req.environ.get('PATH_INFO')
73 73 return path
74 74
75 75
76 76 class BasicAuth(AuthBasicAuthenticator):
77 77
78 78 def __init__(self, realm, authfunc, auth_http_code=None):
79 79 self.realm = realm
80 80 self.authfunc = authfunc
81 81 self._rc_auth_http_code = auth_http_code
82 82
83 83 def build_authentication(self):
84 84 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
85 85 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
86 86 # return 403 if alternative http return code is specified in
87 87 # RhodeCode config
88 88 return HTTPForbidden(headers=head)
89 89 return HTTPUnauthorized(headers=head)
90 90
91 91 def authenticate(self, environ):
92 92 authorization = AUTHORIZATION(environ)
93 93 if not authorization:
94 94 return self.build_authentication()
95 95 (authmeth, auth) = authorization.split(' ', 1)
96 96 if 'basic' != authmeth.lower():
97 97 return self.build_authentication()
98 98 auth = auth.strip().decode('base64')
99 99 _parts = auth.split(':', 1)
100 100 if len(_parts) == 2:
101 101 username, password = _parts
102 102 if self.authfunc(environ, username, password):
103 103 return username
104 104 return self.build_authentication()
105 105
106 106 __call__ = authenticate
107 107
108 108
109 109 class BaseVCSController(object):
110 110
111 111 def __init__(self, application, config):
112 112 self.application = application
113 113 self.config = config
114 114 # base path of repo locations
115 115 self.basepath = self.config['base_path']
116 116 #authenticate this mercurial request using authfunc
117 117 self.authenticate = BasicAuth('', authfunc,
118 118 config.get('auth_ret_code'))
119 119 self.ip_addr = '0.0.0.0'
120 120
121 121 def _handle_request(self, environ, start_response):
122 122 raise NotImplementedError()
123 123
124 124 def _get_by_id(self, repo_name):
125 125 """
126 126 Get's a special pattern _<ID> from clone url and tries to replace it
127 127 with a repository_name for support of _<ID> non changable urls
128 128
129 129 :param repo_name:
130 130 """
131 131 try:
132 132 data = repo_name.split('/')
133 133 if len(data) >= 2:
134 134 by_id = data[1].split('_')
135 135 if len(by_id) == 2 and by_id[1].isdigit():
136 136 _repo_name = Repository.get(by_id[1]).repo_name
137 137 data[1] = _repo_name
138 138 except Exception:
139 139 log.debug('Failed to extract repo_name from id %s' % (
140 140 traceback.format_exc()
141 141 )
142 142 )
143 143
144 144 return '/'.join(data)
145 145
146 146 def _invalidate_cache(self, repo_name):
147 147 """
148 148 Set's cache for this repository for invalidation on next access
149 149
150 150 :param repo_name: full repo name, also a cache key
151 151 """
152 152 ScmModel().mark_for_invalidation(repo_name)
153 153
154 154 def _check_permission(self, action, user, repo_name, ip_addr=None):
155 155 """
156 156 Checks permissions using action (push/pull) user and repository
157 157 name
158 158
159 159 :param action: push or pull action
160 160 :param user: user instance
161 161 :param repo_name: repository name
162 162 """
163 163 #check IP
164 164 authuser = AuthUser(user_id=user.user_id, ip_addr=ip_addr)
165 165 if not authuser.ip_allowed:
166 166 return False
167 167 else:
168 168 log.info('Access for IP:%s allowed' % (ip_addr))
169 169 if action == 'push':
170 170 if not HasPermissionAnyMiddleware('repository.write',
171 171 'repository.admin')(user,
172 172 repo_name):
173 173 return False
174 174
175 175 else:
176 176 #any other action need at least read permission
177 177 if not HasPermissionAnyMiddleware('repository.read',
178 178 'repository.write',
179 179 'repository.admin')(user,
180 180 repo_name):
181 181 return False
182 182
183 183 return True
184 184
185 185 def _get_ip_addr(self, environ):
186 186 return _get_ip_addr(environ)
187 187
188 188 def _check_ssl(self, environ, start_response):
189 189 """
190 190 Checks the SSL check flag and returns False if SSL is not present
191 191 and required True otherwise
192 192 """
193 193 org_proto = environ['wsgi._org_proto']
194 194 #check if we have SSL required ! if not it's a bad request !
195 195 require_ssl = str2bool(RhodeCodeUi.get_by_key('push_ssl').ui_value)
196 196 if require_ssl and org_proto == 'http':
197 197 log.debug('proto is %s and SSL is required BAD REQUEST !'
198 198 % org_proto)
199 199 return False
200 200 return True
201 201
202 202 def _check_locking_state(self, environ, action, repo, user_id):
203 203 """
204 204 Checks locking on this repository, if locking is enabled and lock is
205 205 present returns a tuple of make_lock, locked, locked_by.
206 206 make_lock can have 3 states None (do nothing) True, make lock
207 207 False release lock, This value is later propagated to hooks, which
208 208 do the locking. Think about this as signals passed to hooks what to do.
209 209
210 210 """
211 211 locked = False # defines that locked error should be thrown to user
212 212 make_lock = None
213 213 repo = Repository.get_by_repo_name(repo)
214 214 user = User.get(user_id)
215 215
216 216 # this is kind of hacky, but due to how mercurial handles client-server
217 217 # server see all operation on changeset; bookmarks, phases and
218 218 # obsolescence marker in different transaction, we don't want to check
219 219 # locking on those
220 220 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
221 221 locked_by = repo.locked
222 222 if repo and repo.enable_locking and not obsolete_call:
223 223 if action == 'push':
224 224 #check if it's already locked !, if it is compare users
225 225 user_id, _date = repo.locked
226 226 if user.user_id == user_id:
227 227 log.debug('Got push from user %s, now unlocking' % (user))
228 228 # unlock if we have push from user who locked
229 229 make_lock = False
230 230 else:
231 231 # we're not the same user who locked, ban with 423 !
232 232 locked = True
233 233 if action == 'pull':
234 234 if repo.locked[0] and repo.locked[1]:
235 235 locked = True
236 236 else:
237 237 log.debug('Setting lock on repo %s by %s' % (repo, user))
238 238 make_lock = True
239 239
240 240 else:
241 241 log.debug('Repository %s do not have locking enabled' % (repo))
242 242 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
243 243 % (make_lock, locked, locked_by))
244 244 return make_lock, locked, locked_by
245 245
246 246 def __call__(self, environ, start_response):
247 247 start = time.time()
248 248 try:
249 249 return self._handle_request(environ, start_response)
250 250 finally:
251 251 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
252 252 log.debug('Request time: %.3fs' % (time.time() - start))
253 253 meta.Session.remove()
254 254
255 255
256 256 class BaseController(WSGIController):
257 257
258 258 def __before__(self):
259 259 """
260 260 __before__ is called before controller methods and after __call__
261 261 """
262 262 c.rhodecode_version = __version__
263 263 c.rhodecode_instanceid = config.get('instance_id')
264 264 c.rhodecode_name = config.get('rhodecode_title')
265 265 c.use_gravatar = str2bool(config.get('use_gravatar'))
266 266 c.ga_code = config.get('rhodecode_ga_code')
267 267 # Visual options
268 268 c.visual = AttributeDict({})
269 269 rc_config = RhodeCodeSetting.get_app_settings()
270 270 ## DB stored
271 271 c.visual.show_public_icon = str2bool(rc_config.get('rhodecode_show_public_icon'))
272 272 c.visual.show_private_icon = str2bool(rc_config.get('rhodecode_show_private_icon'))
273 273 c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags'))
274 274 c.visual.dashboard_items = safe_int(rc_config.get('rhodecode_dashboard_items', 100))
275 275 c.visual.repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
276 276 c.visual.show_version = str2bool(rc_config.get('rhodecode_show_version'))
277 277
278 278 ## INI stored
279 279 self.cut_off_limit = int(config.get('cut_off_limit'))
280 280 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
281 281
282 282 c.repo_name = get_repo_slug(request) # can be empty
283 283 c.backends = BACKENDS.keys()
284 284 c.unread_notifications = NotificationModel()\
285 285 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
286 286 self.sa = meta.Session
287 287 self.scm_model = ScmModel(self.sa)
288 288
289 289 def __call__(self, environ, start_response):
290 290 """Invoke the Controller"""
291 291 # WSGIController.__call__ dispatches to the Controller method
292 292 # the request is routed to. This routing information is
293 293 # available in environ['pylons.routes_dict']
294 294 try:
295 295 self.ip_addr = _get_ip_addr(environ)
296 296 # make sure that we update permissions each time we call controller
297 297 api_key = request.GET.get('api_key')
298 298 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
299 299 user_id = cookie_store.get('user_id', None)
300 300 username = get_container_username(environ, config)
301 301 auth_user = AuthUser(user_id, api_key, username, self.ip_addr)
302 302 request.user = auth_user
303 303 self.rhodecode_user = c.rhodecode_user = auth_user
304 304 if not self.rhodecode_user.is_authenticated and \
305 305 self.rhodecode_user.user_id is not None:
306 306 self.rhodecode_user.set_authenticated(
307 307 cookie_store.get('is_authenticated')
308 308 )
309 309 log.info('IP: %s User: %s accessed %s' % (
310 310 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
311 311 )
312 312 return WSGIController.__call__(self, environ, start_response)
313 313 finally:
314 314 meta.Session.remove()
315 315
316 316
317 317 class BaseRepoController(BaseController):
318 318 """
319 319 Base class for controllers responsible for loading all needed data for
320 320 repository loaded items are
321 321
322 322 c.rhodecode_repo: instance of scm repository
323 323 c.rhodecode_db_repo: instance of db
324 324 c.repository_followers: number of followers
325 325 c.repository_forks: number of forks
326 326 c.repository_following: weather the current user is following the current repo
327 327 """
328 328
329 329 def __before__(self):
330 330 super(BaseRepoController, self).__before__()
331 331 if c.repo_name:
332 332
333 333 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
334 334 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
335 335 # update last change according to VCS data
336 336 dbr.update_changeset_cache(dbr.get_changeset())
337 337 if c.rhodecode_repo is None:
338 338 log.error('%s this repository is present in database but it '
339 339 'cannot be created as an scm instance', c.repo_name)
340 340
341 341 redirect(url('home'))
342 342
343 343 # some globals counter for menu
344 344 c.repository_followers = self.scm_model.get_followers(dbr)
345 345 c.repository_forks = self.scm_model.get_forks(dbr)
346 346 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
347 347 c.repository_following = self.scm_model.is_following_repo(c.repo_name,
348 348 self.rhodecode_user.user_id)
@@ -1,70 +1,70 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.paster_commands.make_rcextensions
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 repo-scan paster command for RhodeCode
7 7
8 8
9 9 :created_on: Feb 9, 2013
10 10 :author: marcink
11 11 :copyright: (C) 2010-2013 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 from __future__ import with_statement
27 27
28 28 import os
29 29 import sys
30 30 import logging
31 31
32 32 from rhodecode.model.scm import ScmModel
33 33 from rhodecode.lib.utils import BasePasterCommand, repo2db_mapper
34 34
35 35 # fix rhodecode import
36 36 from os.path import dirname as dn
37 37 rc_path = dn(dn(dn(os.path.realpath(__file__))))
38 38 sys.path.append(rc_path)
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class Command(BasePasterCommand):
44 44
45 45 max_args = 1
46 46 min_args = 1
47 47
48 48 usage = "CONFIG_FILE"
49 49 group_name = "RhodeCode"
50 50 takes_config_file = -1
51 51 parser = BasePasterCommand.standard_parser(verbose=True)
52 52 summary = "Rescan default location for new repositories"
53 53
54 54 def command(self):
55 55 #get SqlAlchemy session
56 56 self._init_session()
57 57 rm_obsolete = self.options.delete_obsolete
58 58 log.info('Now scanning root location for new repos...')
59 59 added, removed = repo2db_mapper(ScmModel().repo_scan(),
60 60 remove_obsolete=rm_obsolete)
61 added = ','.join(added) or '-'
62 removed = ','.join(removed) or '-'
63 log.info('Scan completed added:%s removed:%s' % (added, removed))
61 added = ', '.join(added) or '-'
62 removed = ', '.join(removed) or '-'
63 log.info('Scan completed added: %s removed:%s' % (added, removed))
64 64
65 65 def update_parser(self):
66 66 self.parser.add_option('--delete-obsolete',
67 67 action='store_true',
68 68 help="Use this flag do delete repositories that are "
69 69 "present in RhodeCode database but not on the filesystem",
70 70 )
@@ -1,243 +1,243 b''
1 1 """
2 2 Utitlites aimed to help achieve mostly basic tasks.
3 3 """
4 4 from __future__ import division
5 5
6 6 import re
7 7 import os
8 8 import time
9 9 import datetime
10 10 from subprocess import Popen, PIPE
11 11
12 12 from rhodecode.lib.vcs.exceptions import VCSError
13 13 from rhodecode.lib.vcs.exceptions import RepositoryError
14 14 from rhodecode.lib.vcs.utils.paths import abspath
15 15
16 16 ALIASES = ['hg', 'git']
17 17
18 18
19 19 def get_scm(path, search_up=False, explicit_alias=None):
20 20 """
21 21 Returns one of alias from ``ALIASES`` (in order of precedence same as
22 22 shortcuts given in ``ALIASES``) and top working dir path for the given
23 23 argument. If no scm-specific directory is found or more than one scm is
24 24 found at that directory, ``VCSError`` is raised.
25 25
26 26 :param search_up: if set to ``True``, this function would try to
27 27 move up to parent directory every time no scm is recognized for the
28 28 currently checked path. Default: ``False``.
29 29 :param explicit_alias: can be one of available backend aliases, when given
30 30 it will return given explicit alias in repositories under more than one
31 31 version control, if explicit_alias is different than found it will raise
32 32 VCSError
33 33 """
34 34 if not os.path.isdir(path):
35 35 raise VCSError("Given path %s is not a directory" % path)
36 36
37 37 def get_scms(path):
38 38 return [(scm, path) for scm in get_scms_for_path(path)]
39 39
40 40 found_scms = get_scms(path)
41 41 while not found_scms and search_up:
42 42 newpath = abspath(path, '..')
43 43 if newpath == path:
44 44 break
45 45 path = newpath
46 46 found_scms = get_scms(path)
47 47
48 48 if len(found_scms) > 1:
49 49 for scm in found_scms:
50 50 if scm[0] == explicit_alias:
51 51 return scm
52 52 raise VCSError('More than one [%s] scm found at given path %s'
53 % (','.join((x[0] for x in found_scms)), path))
53 % (', '.join((x[0] for x in found_scms)), path))
54 54
55 55 if len(found_scms) is 0:
56 56 raise VCSError('No scm found at given path %s' % path)
57 57
58 58 return found_scms[0]
59 59
60 60
61 61 def get_scms_for_path(path):
62 62 """
63 63 Returns all scm's found at the given path. If no scm is recognized
64 64 - empty list is returned.
65 65
66 66 :param path: path to directory which should be checked. May be callable.
67 67
68 68 :raises VCSError: if given ``path`` is not a directory
69 69 """
70 70 from rhodecode.lib.vcs.backends import get_backend
71 71 if hasattr(path, '__call__'):
72 72 path = path()
73 73 if not os.path.isdir(path):
74 74 raise VCSError("Given path %r is not a directory" % path)
75 75
76 76 result = []
77 77 for key in ALIASES:
78 78 dirname = os.path.join(path, '.' + key)
79 79 if os.path.isdir(dirname):
80 80 result.append(key)
81 81 continue
82 82 dirname = os.path.join(path, 'rm__.' + key)
83 83 if os.path.isdir(dirname):
84 84 return result
85 85 # We still need to check if it's not bare repository as
86 86 # bare repos don't have working directories
87 87 try:
88 88 get_backend(key)(path)
89 89 result.append(key)
90 90 continue
91 91 except RepositoryError:
92 92 # Wrong backend
93 93 pass
94 94 except VCSError:
95 95 # No backend at all
96 96 pass
97 97 return result
98 98
99 99
100 100 def run_command(cmd, *args):
101 101 """
102 102 Runs command on the system with given ``args``.
103 103 """
104 104 command = ' '.join((cmd, args))
105 105 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
106 106 stdout, stderr = p.communicate()
107 107 return p.retcode, stdout, stderr
108 108
109 109
110 110 def get_highlighted_code(name, code, type='terminal'):
111 111 """
112 112 If pygments are available on the system
113 113 then returned output is colored. Otherwise
114 114 unchanged content is returned.
115 115 """
116 116 import logging
117 117 try:
118 118 import pygments
119 119 pygments
120 120 except ImportError:
121 121 return code
122 122 from pygments import highlight
123 123 from pygments.lexers import guess_lexer_for_filename, ClassNotFound
124 124 from pygments.formatters import TerminalFormatter
125 125
126 126 try:
127 127 lexer = guess_lexer_for_filename(name, code)
128 128 formatter = TerminalFormatter()
129 129 content = highlight(code, lexer, formatter)
130 130 except ClassNotFound:
131 131 logging.debug("Couldn't guess Lexer, will not use pygments.")
132 132 content = code
133 133 return content
134 134
135 135
136 136 def parse_changesets(text):
137 137 """
138 138 Returns dictionary with *start*, *main* and *end* ids.
139 139
140 140 Examples::
141 141
142 142 >>> parse_changesets('aaabbb')
143 143 {'start': None, 'main': 'aaabbb', 'end': None}
144 144 >>> parse_changesets('aaabbb..cccddd')
145 145 {'start': 'aaabbb', 'main': None, 'end': 'cccddd'}
146 146
147 147 """
148 148 text = text.strip()
149 149 CID_RE = r'[a-zA-Z0-9]+'
150 150 if not '..' in text:
151 151 m = re.match(r'^(?P<cid>%s)$' % CID_RE, text)
152 152 if m:
153 153 return {
154 154 'start': None,
155 155 'main': text,
156 156 'end': None,
157 157 }
158 158 else:
159 159 RE = r'^(?P<start>%s)?\.{2,3}(?P<end>%s)?$' % (CID_RE, CID_RE)
160 160 m = re.match(RE, text)
161 161 if m:
162 162 result = m.groupdict()
163 163 result['main'] = None
164 164 return result
165 165 raise ValueError("IDs not recognized")
166 166
167 167
168 168 def parse_datetime(text):
169 169 """
170 170 Parses given text and returns ``datetime.datetime`` instance or raises
171 171 ``ValueError``.
172 172
173 173 :param text: string of desired date/datetime or something more verbose,
174 174 like *yesterday*, *2weeks 3days*, etc.
175 175 """
176 176
177 177 text = text.strip().lower()
178 178
179 179 INPUT_FORMATS = (
180 180 '%Y-%m-%d %H:%M:%S',
181 181 '%Y-%m-%d %H:%M',
182 182 '%Y-%m-%d',
183 183 '%m/%d/%Y %H:%M:%S',
184 184 '%m/%d/%Y %H:%M',
185 185 '%m/%d/%Y',
186 186 '%m/%d/%y %H:%M:%S',
187 187 '%m/%d/%y %H:%M',
188 188 '%m/%d/%y',
189 189 )
190 190 for format in INPUT_FORMATS:
191 191 try:
192 192 return datetime.datetime(*time.strptime(text, format)[:6])
193 193 except ValueError:
194 194 pass
195 195
196 196 # Try descriptive texts
197 197 if text == 'tomorrow':
198 198 future = datetime.datetime.now() + datetime.timedelta(days=1)
199 199 args = future.timetuple()[:3] + (23, 59, 59)
200 200 return datetime.datetime(*args)
201 201 elif text == 'today':
202 202 return datetime.datetime(*datetime.datetime.today().timetuple()[:3])
203 203 elif text == 'now':
204 204 return datetime.datetime.now()
205 205 elif text == 'yesterday':
206 206 past = datetime.datetime.now() - datetime.timedelta(days=1)
207 207 return datetime.datetime(*past.timetuple()[:3])
208 208 else:
209 209 days = 0
210 210 matched = re.match(
211 211 r'^((?P<weeks>\d+) ?w(eeks?)?)? ?((?P<days>\d+) ?d(ays?)?)?$', text)
212 212 if matched:
213 213 groupdict = matched.groupdict()
214 214 if groupdict['days']:
215 215 days += int(matched.groupdict()['days'])
216 216 if groupdict['weeks']:
217 217 days += int(matched.groupdict()['weeks']) * 7
218 218 past = datetime.datetime.now() - datetime.timedelta(days=days)
219 219 return datetime.datetime(*past.timetuple()[:3])
220 220
221 221 raise ValueError('Wrong date: "%s"' % text)
222 222
223 223
224 224 def get_dict_for_attrs(obj, attrs):
225 225 """
226 226 Returns dictionary for each attribute from given ``obj``.
227 227 """
228 228 data = {}
229 229 for attr in attrs:
230 230 data[attr] = getattr(obj, attr)
231 231 return data
232 232
233 233
234 234 def get_total_seconds(timedelta):
235 235 """
236 236 Backported for Python 2.5.
237 237
238 238 See http://docs.python.org/library/datetime.html.
239 239 """
240 240 return ((timedelta.microseconds + (
241 241 timedelta.seconds +
242 242 timedelta.days * 24 * 60 * 60
243 243 ) * 10**6) / 10**6)
@@ -1,252 +1,252 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)} &middot; ${c.rhodecode_name}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('Pull request #%s') % c.pull_request.pull_request_id}
9 9 </%def>
10 10
11 11 <%def name="page_nav()">
12 12 ${self.menu('repositories')}
13 13 </%def>
14 14
15 15 <%def name="main()">
16 16 ${self.context_bar('showpullrequest')}
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22
23 23 <h3 class="${'closed' if c.pull_request.is_closed() else ''}">
24 24 <img src="${h.url('/images/icons/flag_status_%s.png' % str(c.pull_request.last_review_status))}" />
25 25 ${_('Title')}: ${c.pull_request.title}
26 26 %if c.pull_request.is_closed():
27 27 (${_('Closed')})
28 28 %endif
29 29 </h3>
30 30
31 31 <div class="form">
32 32 <div id="summary" class="fields">
33 33 <div class="field">
34 34 <div class="label-summary">
35 35 <label>${_('Review status')}:</label>
36 36 </div>
37 37 <div class="input">
38 38 <div class="changeset-status-container" style="float:none;clear:both">
39 39 %if c.current_changeset_status:
40 40 <div title="${_('Pull request status')}" class="changeset-status-lbl">
41 41 %if c.pull_request.is_closed():
42 42 ${_('Closed')},
43 43 %endif
44 44 ${h.changeset_status_lbl(c.current_changeset_status)}
45 45 </div>
46 46 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
47 47 %endif
48 48 </div>
49 49 </div>
50 50 </div>
51 51 <div class="field">
52 52 <div class="label-summary">
53 53 <label>${_('Still not reviewed by')}:</label>
54 54 </div>
55 55 <div class="input">
56 56 % if len(c.pull_request_pending_reviewers) > 0:
57 <div class="tooltip" title="${h.tooltip(','.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
57 <div class="tooltip" title="${h.tooltip(', '.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
58 58 %else:
59 59 <div>${_('Pull request was reviewed by all reviewers')}</div>
60 60 %endif
61 61 </div>
62 62 </div>
63 63 <div class="field">
64 64 <div class="label-summary">
65 65 <label>${_('Origin repository')}:</label>
66 66 </div>
67 67 <div class="input">
68 68 <div>
69 69 ##%if h.is_hg(c.pull_request.org_repo):
70 70 ## <img class="icon" title="${_('Mercurial repository')}" alt="${_('Mercurial repository')}" src="${h.url('/images/icons/hgicon.png')}"/>
71 71 ##%elif h.is_git(c.pull_request.org_repo):
72 72 ## <img class="icon" title="${_('Git repository')}" alt="${_('Git repository')}" src="${h.url('/images/icons/giticon.png')}"/>
73 73 ##%endif
74 74 <span class="spantag">${c.pull_request.org_ref_parts[0]}: ${c.pull_request.org_ref_parts[1]}</span>
75 75 <span><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
76 76 </div>
77 77 </div>
78 78 </div>
79 79 <div class="field">
80 80 <div class="label-summary">
81 81 <label>${_('Description')}:</label>
82 82 </div>
83 83 <div class="input">
84 84 <div style="white-space:pre-wrap">${h.literal(c.pull_request.description)}</div>
85 85 </div>
86 86 </div>
87 87 <div class="field">
88 88 <div class="label-summary">
89 89 <label>${_('Created on')}:</label>
90 90 </div>
91 91 <div class="input">
92 92 <div>${h.fmt_date(c.pull_request.created_on)}</div>
93 93 </div>
94 94 </div>
95 95 </div>
96 96 </div>
97 97
98 98 <div style="overflow: auto;">
99 99 ##DIFF
100 100 <div class="table" style="float:left;clear:none">
101 101 <div id="body" class="diffblock">
102 102 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
103 103 </div>
104 104 <div id="changeset_compare_view_content">
105 105 ##CS
106 106 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
107 107 <%include file="/compare/compare_cs.html" />
108 108
109 109 ## FILES
110 110 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
111 111
112 112 % if c.limited_diff:
113 113 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
114 114 % else:
115 115 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
116 116 %endif
117 117
118 118 </div>
119 119 <div class="cs_files">
120 120 %if not c.files:
121 121 <span class="empty_data">${_('No files')}</span>
122 122 %endif
123 123 %for fid, change, f, stat in c.files:
124 124 <div class="cs_${change}">
125 125 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
126 126 <div class="changes">${h.fancy_file_stats(stat)}</div>
127 127 </div>
128 128 %endfor
129 129 </div>
130 130 % if c.limited_diff:
131 131 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
132 132 % endif
133 133 </div>
134 134 </div>
135 135 ## REVIEWERS
136 136 <div style="float:left; border-left:1px dashed #eee">
137 137 <h4>${_('Pull request reviewers')}</h4>
138 138 <div id="reviewers" style="padding:0px 0px 5px 10px">
139 139 ## members goes here !
140 140 <div class="group_members_wrap" style="min-height:45px">
141 141 <ul id="review_members" class="group_members">
142 142 %for member,status in c.pull_request_reviewers:
143 143 <li id="reviewer_${member.user_id}">
144 144 <div class="reviewers_member">
145 145 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
146 146 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
147 147 </div>
148 148 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
149 149 <div style="float:left">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
150 150 <input type="hidden" value="${member.user_id}" name="review_members" />
151 151 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.user_id == c.rhodecode_user.user_id):
152 152 <span class="delete_icon action_button" onclick="removeReviewMember(${member.user_id})"></span>
153 153 %endif
154 154 </div>
155 155 </li>
156 156 %endfor
157 157 </ul>
158 158 </div>
159 159 %if not c.pull_request.is_closed():
160 160 <div class='ac'>
161 161 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
162 162 <div class="reviewer_ac">
163 163 ${h.text('user', class_='yui-ac-input')}
164 164 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
165 165 <div id="reviewers_container"></div>
166 166 </div>
167 167 <div style="padding:0px 10px">
168 168 <span id="update_pull_request" class="ui-btn xsmall">${_('Save changes')}</span>
169 169 </div>
170 170 %endif
171 171 </div>
172 172 %endif
173 173 </div>
174 174 </div>
175 175 </div>
176 176 <script>
177 177 var _USERS_AC_DATA = ${c.users_array|n};
178 178 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
179 179 // TODO: switch this to pyroutes
180 180 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
181 181 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
182 182
183 183 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
184 184 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
185 185 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
186 186
187 187 </script>
188 188
189 189 ## diff block
190 190 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
191 191 %for fid, change, f, stat in c.files:
192 192 ${diff_block.diff_block_simple([c.changes[fid]])}
193 193 %endfor
194 194 % if c.limited_diff:
195 195 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h4>
196 196 % endif
197 197
198 198
199 199 ## template for inline comment form
200 200 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
201 201 ${comment.comment_inline_form()}
202 202
203 203 ## render comments and inlines
204 204 ${comment.generate_comments(include_pr=True)}
205 205
206 206 % if not c.pull_request.is_closed():
207 207 ## main comment form and it status
208 208 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
209 209 pull_request_id=c.pull_request.pull_request_id),
210 210 c.current_changeset_status,
211 211 close_btn=True, change_status=c.allowed_to_change_status)}
212 212 %endif
213 213
214 214 <script type="text/javascript">
215 215 YUE.onDOMReady(function(){
216 216 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
217 217
218 218 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
219 219 var show = 'none';
220 220 var target = e.currentTarget;
221 221 if(target.checked){
222 222 var show = ''
223 223 }
224 224 var boxid = YUD.getAttribute(target,'id_for');
225 225 var comments = YUQ('#{0} .inline-comments'.format(boxid));
226 226 for(c in comments){
227 227 YUD.setStyle(comments[c],'display',show);
228 228 }
229 229 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
230 230 for(c in btns){
231 231 YUD.setStyle(btns[c],'display',show);
232 232 }
233 233 })
234 234
235 235 YUE.on(YUQ('.line'),'click',function(e){
236 236 var tr = e.currentTarget;
237 237 injectInlineForm(tr);
238 238 });
239 239
240 240 // inject comments into they proper positions
241 241 var file_comments = YUQ('.inline-comment-placeholder');
242 242 renderInlineComments(file_comments);
243 243
244 244 YUE.on(YUD.get('update_pull_request'),'click',function(e){
245 245 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
246 246 })
247 247 })
248 248 </script>
249 249
250 250 </div>
251 251
252 252 </%def>
General Comments 0
You need to be logged in to leave comments. Login now