##// END OF EJS Templates
routing: fix default argument mutability.
marcink -
r3091:7a298dcc default
parent child Browse files
Show More
@@ -1,677 +1,678 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23 import operator
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
26 26
27 27 from rhodecode.lib import helpers as h, diffs
28 28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 30 from rhodecode.model import repo
31 31 from rhodecode.model import repo_group
32 32 from rhodecode.model import user_group
33 33 from rhodecode.model import user
34 34 from rhodecode.model.db import User
35 35 from rhodecode.model.scm import ScmModel
36 36 from rhodecode.model.settings import VcsSettingsModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 ADMIN_PREFIX = '/_admin'
42 42 STATIC_FILE_PREFIX = '/_static'
43 43
44 44 URL_NAME_REQUIREMENTS = {
45 45 # group name can have a slash in them, but they must not end with a slash
46 46 'group_name': r'.*?[^/]',
47 47 'repo_group_name': r'.*?[^/]',
48 48 # repo names can have a slash in them, but they must not end with a slash
49 49 'repo_name': r'.*?[^/]',
50 50 # file path eats up everything at the end
51 51 'f_path': r'.*',
52 52 # reference types
53 53 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
54 54 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
55 55 }
56 56
57 57
58 58 def add_route_with_slash(config,name, pattern, **kw):
59 59 config.add_route(name, pattern, **kw)
60 60 if not pattern.endswith('/'):
61 61 config.add_route(name + '_slash', pattern + '/', **kw)
62 62
63 63
64 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
64 def add_route_requirements(route_path, requirements=None):
65 65 """
66 66 Adds regex requirements to pyramid routes using a mapping dict
67 67 e.g::
68 68 add_route_requirements('{repo_name}/settings')
69 69 """
70 requirements = requirements or URL_NAME_REQUIREMENTS
70 71 for key, regex in requirements.items():
71 72 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
72 73 return route_path
73 74
74 75
75 76 def get_format_ref_id(repo):
76 77 """Returns a `repo` specific reference formatter function"""
77 78 if h.is_svn(repo):
78 79 return _format_ref_id_svn
79 80 else:
80 81 return _format_ref_id
81 82
82 83
83 84 def _format_ref_id(name, raw_id):
84 85 """Default formatting of a given reference `name`"""
85 86 return name
86 87
87 88
88 89 def _format_ref_id_svn(name, raw_id):
89 90 """Special way of formatting a reference for Subversion including path"""
90 91 return '%s@%s' % (name, raw_id)
91 92
92 93
93 94 class TemplateArgs(StrictAttributeDict):
94 95 pass
95 96
96 97
97 98 class BaseAppView(object):
98 99
99 100 def __init__(self, context, request):
100 101 self.request = request
101 102 self.context = context
102 103 self.session = request.session
103 104 if not hasattr(request, 'user'):
104 105 # NOTE(marcink): edge case, we ended up in matched route
105 106 # but probably of web-app context, e.g API CALL/VCS CALL
106 107 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
107 108 log.warning('Unable to process request `%s` in this scope', request)
108 109 raise HTTPBadRequest()
109 110
110 111 self._rhodecode_user = request.user # auth user
111 112 self._rhodecode_db_user = self._rhodecode_user.get_instance()
112 113 self._maybe_needs_password_change(
113 114 request.matched_route.name, self._rhodecode_db_user)
114 115
115 116 def _maybe_needs_password_change(self, view_name, user_obj):
116 117 log.debug('Checking if user %s needs password change on view %s',
117 118 user_obj, view_name)
118 119 skip_user_views = [
119 120 'logout', 'login',
120 121 'my_account_password', 'my_account_password_update'
121 122 ]
122 123
123 124 if not user_obj:
124 125 return
125 126
126 127 if user_obj.username == User.DEFAULT_USER:
127 128 return
128 129
129 130 now = time.time()
130 131 should_change = user_obj.user_data.get('force_password_change')
131 132 change_after = safe_int(should_change) or 0
132 133 if should_change and now > change_after:
133 134 log.debug('User %s requires password change', user_obj)
134 135 h.flash('You are required to change your password', 'warning',
135 136 ignore_duplicate=True)
136 137
137 138 if view_name not in skip_user_views:
138 139 raise HTTPFound(
139 140 self.request.route_path('my_account_password'))
140 141
141 142 def _log_creation_exception(self, e, repo_name):
142 143 _ = self.request.translate
143 144 reason = None
144 145 if len(e.args) == 2:
145 146 reason = e.args[1]
146 147
147 148 if reason == 'INVALID_CERTIFICATE':
148 149 log.exception(
149 150 'Exception creating a repository: invalid certificate')
150 151 msg = (_('Error creating repository %s: invalid certificate')
151 152 % repo_name)
152 153 else:
153 154 log.exception("Exception creating a repository")
154 155 msg = (_('Error creating repository %s')
155 156 % repo_name)
156 157 return msg
157 158
158 159 def _get_local_tmpl_context(self, include_app_defaults=True):
159 160 c = TemplateArgs()
160 161 c.auth_user = self.request.user
161 162 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
162 163 c.rhodecode_user = self.request.user
163 164
164 165 if include_app_defaults:
165 166 from rhodecode.lib.base import attach_context_attributes
166 167 attach_context_attributes(c, self.request, self.request.user.user_id)
167 168
168 169 return c
169 170
170 171 def _get_template_context(self, tmpl_args, **kwargs):
171 172
172 173 local_tmpl_args = {
173 174 'defaults': {},
174 175 'errors': {},
175 176 'c': tmpl_args
176 177 }
177 178 local_tmpl_args.update(kwargs)
178 179 return local_tmpl_args
179 180
180 181 def load_default_context(self):
181 182 """
182 183 example:
183 184
184 185 def load_default_context(self):
185 186 c = self._get_local_tmpl_context()
186 187 c.custom_var = 'foobar'
187 188
188 189 return c
189 190 """
190 191 raise NotImplementedError('Needs implementation in view class')
191 192
192 193
193 194 class RepoAppView(BaseAppView):
194 195
195 196 def __init__(self, context, request):
196 197 super(RepoAppView, self).__init__(context, request)
197 198 self.db_repo = request.db_repo
198 199 self.db_repo_name = self.db_repo.repo_name
199 200 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
200 201
201 202 def _handle_missing_requirements(self, error):
202 203 log.error(
203 204 'Requirements are missing for repository %s: %s',
204 205 self.db_repo_name, error.message)
205 206
206 207 def _get_local_tmpl_context(self, include_app_defaults=True):
207 208 _ = self.request.translate
208 209 c = super(RepoAppView, self)._get_local_tmpl_context(
209 210 include_app_defaults=include_app_defaults)
210 211
211 212 # register common vars for this type of view
212 213 c.rhodecode_db_repo = self.db_repo
213 214 c.repo_name = self.db_repo_name
214 215 c.repository_pull_requests = self.db_repo_pull_requests
215 216 self.path_filter = PathFilter(None)
216 217
217 218 c.repository_requirements_missing = {}
218 219 try:
219 220 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
220 221 if self.rhodecode_vcs_repo:
221 222 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
222 223 c.auth_user.username)
223 224 self.path_filter = PathFilter(path_perms)
224 225 except RepositoryRequirementError as e:
225 226 c.repository_requirements_missing = {'error': str(e)}
226 227 self._handle_missing_requirements(e)
227 228 self.rhodecode_vcs_repo = None
228 229
229 230 c.path_filter = self.path_filter # used by atom_feed_entry.mako
230 231
231 232 if self.rhodecode_vcs_repo is None:
232 233 # unable to fetch this repo as vcs instance, report back to user
233 234 h.flash(_(
234 235 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
235 236 "Please check if it exist, or is not damaged.") %
236 237 {'repo_name': c.repo_name},
237 238 category='error', ignore_duplicate=True)
238 239 if c.repository_requirements_missing:
239 240 route = self.request.matched_route.name
240 241 if route.startswith(('edit_repo', 'repo_summary')):
241 242 # allow summary and edit repo on missing requirements
242 243 return c
243 244
244 245 raise HTTPFound(
245 246 h.route_path('repo_summary', repo_name=self.db_repo_name))
246 247
247 248 else: # redirect if we don't show missing requirements
248 249 raise HTTPFound(h.route_path('home'))
249 250
250 251 return c
251 252
252 253 def _get_f_path_unchecked(self, matchdict, default=None):
253 254 """
254 255 Should only be used by redirects, everything else should call _get_f_path
255 256 """
256 257 f_path = matchdict.get('f_path')
257 258 if f_path:
258 259 # fix for multiple initial slashes that causes errors for GIT
259 260 return f_path.lstrip('/')
260 261
261 262 return default
262 263
263 264 def _get_f_path(self, matchdict, default=None):
264 265 f_path_match = self._get_f_path_unchecked(matchdict, default)
265 266 return self.path_filter.assert_path_permissions(f_path_match)
266 267
267 268 def _get_general_setting(self, target_repo, settings_key, default=False):
268 269 settings_model = VcsSettingsModel(repo=target_repo)
269 270 settings = settings_model.get_general_settings()
270 271 return settings.get(settings_key, default)
271 272
272 273
273 274 class PathFilter(object):
274 275
275 276 # Expects and instance of BasePathPermissionChecker or None
276 277 def __init__(self, permission_checker):
277 278 self.permission_checker = permission_checker
278 279
279 280 def assert_path_permissions(self, path):
280 281 if path and self.permission_checker and not self.permission_checker.has_access(path):
281 282 raise HTTPForbidden()
282 283 return path
283 284
284 285 def filter_patchset(self, patchset):
285 286 if not self.permission_checker or not patchset:
286 287 return patchset, False
287 288 had_filtered = False
288 289 filtered_patchset = []
289 290 for patch in patchset:
290 291 filename = patch.get('filename', None)
291 292 if not filename or self.permission_checker.has_access(filename):
292 293 filtered_patchset.append(patch)
293 294 else:
294 295 had_filtered = True
295 296 if had_filtered:
296 297 if isinstance(patchset, diffs.LimitedDiffContainer):
297 298 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
298 299 return filtered_patchset, True
299 300 else:
300 301 return patchset, False
301 302
302 303 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
303 304 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
304 305 result = diffset.render_patchset(filtered_patchset, source_ref=source_ref, target_ref=target_ref)
305 306 result.has_hidden_changes = has_hidden_changes
306 307 return result
307 308
308 309 def get_raw_patch(self, diff_processor):
309 310 if self.permission_checker is None:
310 311 return diff_processor.as_raw()
311 312 elif self.permission_checker.has_full_access:
312 313 return diff_processor.as_raw()
313 314 else:
314 315 return '# Repository has user-specific filters, raw patch generation is disabled.'
315 316
316 317 @property
317 318 def is_enabled(self):
318 319 return self.permission_checker is not None
319 320
320 321
321 322 class RepoGroupAppView(BaseAppView):
322 323 def __init__(self, context, request):
323 324 super(RepoGroupAppView, self).__init__(context, request)
324 325 self.db_repo_group = request.db_repo_group
325 326 self.db_repo_group_name = self.db_repo_group.group_name
326 327
327 328 def _revoke_perms_on_yourself(self, form_result):
328 329 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
329 330 form_result['perm_updates'])
330 331 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
331 332 form_result['perm_additions'])
332 333 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
333 334 form_result['perm_deletions'])
334 335 admin_perm = 'group.admin'
335 336 if _updates and _updates[0][1] != admin_perm or \
336 337 _additions and _additions[0][1] != admin_perm or \
337 338 _deletions and _deletions[0][1] != admin_perm:
338 339 return True
339 340 return False
340 341
341 342
342 343 class UserGroupAppView(BaseAppView):
343 344 def __init__(self, context, request):
344 345 super(UserGroupAppView, self).__init__(context, request)
345 346 self.db_user_group = request.db_user_group
346 347 self.db_user_group_name = self.db_user_group.users_group_name
347 348
348 349
349 350 class UserAppView(BaseAppView):
350 351 def __init__(self, context, request):
351 352 super(UserAppView, self).__init__(context, request)
352 353 self.db_user = request.db_user
353 354 self.db_user_id = self.db_user.user_id
354 355
355 356 _ = self.request.translate
356 357 if not request.db_user_supports_default:
357 358 if self.db_user.username == User.DEFAULT_USER:
358 359 h.flash(_("Editing user `{}` is disabled.".format(
359 360 User.DEFAULT_USER)), category='warning')
360 361 raise HTTPFound(h.route_path('users'))
361 362
362 363
363 364 class DataGridAppView(object):
364 365 """
365 366 Common class to have re-usable grid rendering components
366 367 """
367 368
368 369 def _extract_ordering(self, request, column_map=None):
369 370 column_map = column_map or {}
370 371 column_index = safe_int(request.GET.get('order[0][column]'))
371 372 order_dir = request.GET.get(
372 373 'order[0][dir]', 'desc')
373 374 order_by = request.GET.get(
374 375 'columns[%s][data][sort]' % column_index, 'name_raw')
375 376
376 377 # translate datatable to DB columns
377 378 order_by = column_map.get(order_by) or order_by
378 379
379 380 search_q = request.GET.get('search[value]')
380 381 return search_q, order_by, order_dir
381 382
382 383 def _extract_chunk(self, request):
383 384 start = safe_int(request.GET.get('start'), 0)
384 385 length = safe_int(request.GET.get('length'), 25)
385 386 draw = safe_int(request.GET.get('draw'))
386 387 return draw, start, length
387 388
388 389 def _get_order_col(self, order_by, model):
389 390 if isinstance(order_by, basestring):
390 391 try:
391 392 return operator.attrgetter(order_by)(model)
392 393 except AttributeError:
393 394 return None
394 395 else:
395 396 return order_by
396 397
397 398
398 399 class BaseReferencesView(RepoAppView):
399 400 """
400 401 Base for reference view for branches, tags and bookmarks.
401 402 """
402 403 def load_default_context(self):
403 404 c = self._get_local_tmpl_context()
404 405
405 406
406 407 return c
407 408
408 409 def load_refs_context(self, ref_items, partials_template):
409 410 _render = self.request.get_partial_renderer(partials_template)
410 411 pre_load = ["author", "date", "message"]
411 412
412 413 is_svn = h.is_svn(self.rhodecode_vcs_repo)
413 414 is_hg = h.is_hg(self.rhodecode_vcs_repo)
414 415
415 416 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
416 417
417 418 closed_refs = {}
418 419 if is_hg:
419 420 closed_refs = self.rhodecode_vcs_repo.branches_closed
420 421
421 422 data = []
422 423 for ref_name, commit_id in ref_items:
423 424 commit = self.rhodecode_vcs_repo.get_commit(
424 425 commit_id=commit_id, pre_load=pre_load)
425 426 closed = ref_name in closed_refs
426 427
427 428 # TODO: johbo: Unify generation of reference links
428 429 use_commit_id = '/' in ref_name or is_svn
429 430
430 431 if use_commit_id:
431 432 files_url = h.route_path(
432 433 'repo_files',
433 434 repo_name=self.db_repo_name,
434 435 f_path=ref_name if is_svn else '',
435 436 commit_id=commit_id)
436 437
437 438 else:
438 439 files_url = h.route_path(
439 440 'repo_files',
440 441 repo_name=self.db_repo_name,
441 442 f_path=ref_name if is_svn else '',
442 443 commit_id=ref_name,
443 444 _query=dict(at=ref_name))
444 445
445 446 data.append({
446 447 "name": _render('name', ref_name, files_url, closed),
447 448 "name_raw": ref_name,
448 449 "date": _render('date', commit.date),
449 450 "date_raw": datetime_to_time(commit.date),
450 451 "author": _render('author', commit.author),
451 452 "commit": _render(
452 453 'commit', commit.message, commit.raw_id, commit.idx),
453 454 "commit_raw": commit.idx,
454 455 "compare": _render(
455 456 'compare', format_ref_id(ref_name, commit.raw_id)),
456 457 })
457 458
458 459 return data
459 460
460 461
461 462 class RepoRoutePredicate(object):
462 463 def __init__(self, val, config):
463 464 self.val = val
464 465
465 466 def text(self):
466 467 return 'repo_route = %s' % self.val
467 468
468 469 phash = text
469 470
470 471 def __call__(self, info, request):
471 472 if hasattr(request, 'vcs_call'):
472 473 # skip vcs calls
473 474 return
474 475
475 476 repo_name = info['match']['repo_name']
476 477 repo_model = repo.RepoModel()
477 478
478 479 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
479 480
480 481 def redirect_if_creating(route_info, db_repo):
481 482 skip_views = ['edit_repo_advanced_delete']
482 483 route = route_info['route']
483 484 # we should skip delete view so we can actually "remove" repositories
484 485 # if they get stuck in creating state.
485 486 if route.name in skip_views:
486 487 return
487 488
488 489 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
489 490 repo_creating_url = request.route_path(
490 491 'repo_creating', repo_name=db_repo.repo_name)
491 492 raise HTTPFound(repo_creating_url)
492 493
493 494 if by_name_match:
494 495 # register this as request object we can re-use later
495 496 request.db_repo = by_name_match
496 497 redirect_if_creating(info, by_name_match)
497 498 return True
498 499
499 500 by_id_match = repo_model.get_repo_by_id(repo_name)
500 501 if by_id_match:
501 502 request.db_repo = by_id_match
502 503 redirect_if_creating(info, by_id_match)
503 504 return True
504 505
505 506 return False
506 507
507 508
508 509 class RepoForbidArchivedRoutePredicate(object):
509 510 def __init__(self, val, config):
510 511 self.val = val
511 512
512 513 def text(self):
513 514 return 'repo_forbid_archived = %s' % self.val
514 515
515 516 phash = text
516 517
517 518 def __call__(self, info, request):
518 519 _ = request.translate
519 520 rhodecode_db_repo = request.db_repo
520 521
521 522 log.debug(
522 523 '%s checking if archived flag for repo for %s',
523 524 self.__class__.__name__, rhodecode_db_repo.repo_name)
524 525
525 526 if rhodecode_db_repo.archived:
526 527 log.warning('Current view is not supported for archived repo:%s',
527 528 rhodecode_db_repo.repo_name)
528 529
529 530 h.flash(
530 531 h.literal(_('Action not supported for archived repository.')),
531 532 category='warning')
532 533 summary_url = request.route_path(
533 534 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
534 535 raise HTTPFound(summary_url)
535 536 return True
536 537
537 538
538 539 class RepoTypeRoutePredicate(object):
539 540 def __init__(self, val, config):
540 541 self.val = val or ['hg', 'git', 'svn']
541 542
542 543 def text(self):
543 544 return 'repo_accepted_type = %s' % self.val
544 545
545 546 phash = text
546 547
547 548 def __call__(self, info, request):
548 549 if hasattr(request, 'vcs_call'):
549 550 # skip vcs calls
550 551 return
551 552
552 553 rhodecode_db_repo = request.db_repo
553 554
554 555 log.debug(
555 556 '%s checking repo type for %s in %s',
556 557 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
557 558
558 559 if rhodecode_db_repo.repo_type in self.val:
559 560 return True
560 561 else:
561 562 log.warning('Current view is not supported for repo type:%s',
562 563 rhodecode_db_repo.repo_type)
563 564 return False
564 565
565 566
566 567 class RepoGroupRoutePredicate(object):
567 568 def __init__(self, val, config):
568 569 self.val = val
569 570
570 571 def text(self):
571 572 return 'repo_group_route = %s' % self.val
572 573
573 574 phash = text
574 575
575 576 def __call__(self, info, request):
576 577 if hasattr(request, 'vcs_call'):
577 578 # skip vcs calls
578 579 return
579 580
580 581 repo_group_name = info['match']['repo_group_name']
581 582 repo_group_model = repo_group.RepoGroupModel()
582 583 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
583 584
584 585 if by_name_match:
585 586 # register this as request object we can re-use later
586 587 request.db_repo_group = by_name_match
587 588 return True
588 589
589 590 return False
590 591
591 592
592 593 class UserGroupRoutePredicate(object):
593 594 def __init__(self, val, config):
594 595 self.val = val
595 596
596 597 def text(self):
597 598 return 'user_group_route = %s' % self.val
598 599
599 600 phash = text
600 601
601 602 def __call__(self, info, request):
602 603 if hasattr(request, 'vcs_call'):
603 604 # skip vcs calls
604 605 return
605 606
606 607 user_group_id = info['match']['user_group_id']
607 608 user_group_model = user_group.UserGroup()
608 609 by_id_match = user_group_model.get(user_group_id, cache=False)
609 610
610 611 if by_id_match:
611 612 # register this as request object we can re-use later
612 613 request.db_user_group = by_id_match
613 614 return True
614 615
615 616 return False
616 617
617 618
618 619 class UserRoutePredicateBase(object):
619 620 supports_default = None
620 621
621 622 def __init__(self, val, config):
622 623 self.val = val
623 624
624 625 def text(self):
625 626 raise NotImplementedError()
626 627
627 628 def __call__(self, info, request):
628 629 if hasattr(request, 'vcs_call'):
629 630 # skip vcs calls
630 631 return
631 632
632 633 user_id = info['match']['user_id']
633 634 user_model = user.User()
634 635 by_id_match = user_model.get(user_id, cache=False)
635 636
636 637 if by_id_match:
637 638 # register this as request object we can re-use later
638 639 request.db_user = by_id_match
639 640 request.db_user_supports_default = self.supports_default
640 641 return True
641 642
642 643 return False
643 644
644 645
645 646 class UserRoutePredicate(UserRoutePredicateBase):
646 647 supports_default = False
647 648
648 649 def text(self):
649 650 return 'user_route = %s' % self.val
650 651
651 652 phash = text
652 653
653 654
654 655 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
655 656 supports_default = True
656 657
657 658 def text(self):
658 659 return 'user_with_default_route = %s' % self.val
659 660
660 661 phash = text
661 662
662 663
663 664 def includeme(config):
664 665 config.add_route_predicate(
665 666 'repo_route', RepoRoutePredicate)
666 667 config.add_route_predicate(
667 668 'repo_accepted_types', RepoTypeRoutePredicate)
668 669 config.add_route_predicate(
669 670 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
670 671 config.add_route_predicate(
671 672 'repo_group_route', RepoGroupRoutePredicate)
672 673 config.add_route_predicate(
673 674 'user_group_route', UserGroupRoutePredicate)
674 675 config.add_route_predicate(
675 676 'user_route_with_default', UserRouteWithDefaultPredicate)
676 677 config.add_route_predicate(
677 678 'user_route', UserRoutePredicate)
General Comments 0
You need to be logged in to leave comments. Login now