##// END OF EJS Templates
python3: fixed raw-input
super-admin -
r4962:562a7580 default
parent child Browse files
Show More
@@ -1,680 +1,680 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 Database creation, and setup module for RhodeCode Enterprise. Used for creation
23 23 of database as well as for migration operations
24 24 """
25 25
26 26 import os
27 27 import sys
28 28 import time
29 29 import uuid
30 30 import logging
31 31 import getpass
32 32 from os.path import dirname as dn, join as jn
33 33
34 34 from sqlalchemy.engine import create_engine
35 35
36 36 from rhodecode import __dbversion__
37 37 from rhodecode.model import init_model
38 38 from rhodecode.model.user import UserModel
39 39 from rhodecode.model.db import (
40 40 User, Permission, RhodeCodeUi, RhodeCodeSetting, UserToPerm,
41 41 DbMigrateVersion, RepoGroup, UserRepoGroupToPerm, CacheKey, Repository)
42 42 from rhodecode.model.meta import Session, Base
43 43 from rhodecode.model.permission import PermissionModel
44 44 from rhodecode.model.repo import RepoModel
45 45 from rhodecode.model.repo_group import RepoGroupModel
46 46 from rhodecode.model.settings import SettingsModel
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 def notify(msg):
53 53 """
54 54 Notification for migrations messages
55 55 """
56 56 ml = len(msg) + (4 * 2)
57 57 print(('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper())
58 58
59 59
60 60 class DbManage(object):
61 61
62 62 def __init__(self, log_sql, dbconf, root, tests=False,
63 63 SESSION=None, cli_args=None):
64 64 self.dbname = dbconf.split('/')[-1]
65 65 self.tests = tests
66 66 self.root = root
67 67 self.dburi = dbconf
68 68 self.log_sql = log_sql
69 69 self.cli_args = cli_args or {}
70 70 self.init_db(SESSION=SESSION)
71 71 self.ask_ok = self.get_ask_ok_func(self.cli_args.get('force_ask'))
72 72
73 73 def db_exists(self):
74 74 if not self.sa:
75 75 self.init_db()
76 76 try:
77 77 self.sa.query(RhodeCodeUi)\
78 78 .filter(RhodeCodeUi.ui_key == '/')\
79 79 .scalar()
80 80 return True
81 81 except Exception:
82 82 return False
83 83 finally:
84 84 self.sa.rollback()
85 85
86 86 def get_ask_ok_func(self, param):
87 87 if param not in [None]:
88 88 # return a function lambda that has a default set to param
89 89 return lambda *args, **kwargs: param
90 90 else:
91 91 from rhodecode.lib.utils import ask_ok
92 92 return ask_ok
93 93
94 94 def init_db(self, SESSION=None):
95 95 if SESSION:
96 96 self.sa = SESSION
97 97 else:
98 98 # init new sessions
99 99 engine = create_engine(self.dburi, echo=self.log_sql)
100 100 init_model(engine)
101 101 self.sa = Session()
102 102
103 103 def create_tables(self, override=False):
104 104 """
105 105 Create a auth database
106 106 """
107 107
108 108 log.info("Existing database with the same name is going to be destroyed.")
109 109 log.info("Setup command will run DROP ALL command on that database.")
110 110 if self.tests:
111 111 destroy = True
112 112 else:
113 113 destroy = self.ask_ok('Are you sure that you want to destroy the old database? [y/n]')
114 114 if not destroy:
115 115 log.info('db tables bootstrap: Nothing done.')
116 116 sys.exit(0)
117 117 if destroy:
118 118 Base.metadata.drop_all()
119 119
120 120 checkfirst = not override
121 121 Base.metadata.create_all(checkfirst=checkfirst)
122 122 log.info('Created tables for %s', self.dbname)
123 123
124 124 def set_db_version(self):
125 125 ver = DbMigrateVersion()
126 126 ver.version = __dbversion__
127 127 ver.repository_id = 'rhodecode_db_migrations'
128 128 ver.repository_path = 'versions'
129 129 self.sa.add(ver)
130 130 log.info('db version set to: %s', __dbversion__)
131 131
132 132 def run_post_migration_tasks(self):
133 133 """
134 134 Run various tasks before actually doing migrations
135 135 """
136 136 # delete cache keys on each upgrade
137 137 total = CacheKey.query().count()
138 138 log.info("Deleting (%s) cache keys now...", total)
139 139 CacheKey.delete_all_cache()
140 140
141 141 def upgrade(self, version=None):
142 142 """
143 143 Upgrades given database schema to given revision following
144 144 all needed steps, to perform the upgrade
145 145
146 146 """
147 147
148 148 from rhodecode.lib.dbmigrate.migrate.versioning import api
149 149 from rhodecode.lib.dbmigrate.migrate.exceptions import \
150 150 DatabaseNotControlledError
151 151
152 152 if 'sqlite' in self.dburi:
153 153 print(
154 154 '********************** WARNING **********************\n'
155 155 'Make sure your version of sqlite is at least 3.7.X. \n'
156 156 'Earlier versions are known to fail on some migrations\n'
157 157 '*****************************************************\n')
158 158
159 159 upgrade = self.ask_ok(
160 160 'You are about to perform a database upgrade. Make '
161 161 'sure you have backed up your database. '
162 162 'Continue ? [y/n]')
163 163 if not upgrade:
164 164 log.info('No upgrade performed')
165 165 sys.exit(0)
166 166
167 167 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
168 168 'rhodecode/lib/dbmigrate')
169 169 db_uri = self.dburi
170 170
171 171 if version:
172 172 DbMigrateVersion.set_version(version)
173 173
174 174 try:
175 175 curr_version = api.db_version(db_uri, repository_path)
176 176 msg = ('Found current database db_uri under version '
177 177 'control with version {}'.format(curr_version))
178 178
179 179 except (RuntimeError, DatabaseNotControlledError):
180 180 curr_version = 1
181 181 msg = ('Current database is not under version control. Setting '
182 182 'as version %s' % curr_version)
183 183 api.version_control(db_uri, repository_path, curr_version)
184 184
185 185 notify(msg)
186 186
187 187
188 188 if curr_version == __dbversion__:
189 189 log.info('This database is already at the newest version')
190 190 sys.exit(0)
191 191
192 192 upgrade_steps = range(curr_version + 1, __dbversion__ + 1)
193 193 notify('attempting to upgrade database from '
194 194 'version %s to version %s' % (curr_version, __dbversion__))
195 195
196 196 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
197 197 _step = None
198 198 for step in upgrade_steps:
199 199 notify('performing upgrade step %s' % step)
200 200 time.sleep(0.5)
201 201
202 202 api.upgrade(db_uri, repository_path, step)
203 203 self.sa.rollback()
204 204 notify('schema upgrade for step %s completed' % (step,))
205 205
206 206 _step = step
207 207
208 208 self.run_post_migration_tasks()
209 209 notify('upgrade to version %s successful' % _step)
210 210
211 211 def fix_repo_paths(self):
212 212 """
213 213 Fixes an old RhodeCode version path into new one without a '*'
214 214 """
215 215
216 216 paths = self.sa.query(RhodeCodeUi)\
217 217 .filter(RhodeCodeUi.ui_key == '/')\
218 218 .scalar()
219 219
220 220 paths.ui_value = paths.ui_value.replace('*', '')
221 221
222 222 try:
223 223 self.sa.add(paths)
224 224 self.sa.commit()
225 225 except Exception:
226 226 self.sa.rollback()
227 227 raise
228 228
229 229 def fix_default_user(self):
230 230 """
231 231 Fixes an old default user with some 'nicer' default values,
232 232 used mostly for anonymous access
233 233 """
234 234 def_user = self.sa.query(User)\
235 235 .filter(User.username == User.DEFAULT_USER)\
236 236 .one()
237 237
238 238 def_user.name = 'Anonymous'
239 239 def_user.lastname = 'User'
240 240 def_user.email = User.DEFAULT_USER_EMAIL
241 241
242 242 try:
243 243 self.sa.add(def_user)
244 244 self.sa.commit()
245 245 except Exception:
246 246 self.sa.rollback()
247 247 raise
248 248
249 249 def fix_settings(self):
250 250 """
251 251 Fixes rhodecode settings and adds ga_code key for google analytics
252 252 """
253 253
254 254 hgsettings3 = RhodeCodeSetting('ga_code', '')
255 255
256 256 try:
257 257 self.sa.add(hgsettings3)
258 258 self.sa.commit()
259 259 except Exception:
260 260 self.sa.rollback()
261 261 raise
262 262
263 263 def create_admin_and_prompt(self):
264 264
265 265 # defaults
266 266 defaults = self.cli_args
267 267 username = defaults.get('username')
268 268 password = defaults.get('password')
269 269 email = defaults.get('email')
270 270
271 271 if username is None:
272 username = raw_input('Specify admin username:')
272 username = input('Specify admin username:')
273 273 if password is None:
274 274 password = self._get_admin_password()
275 275 if not password:
276 276 # second try
277 277 password = self._get_admin_password()
278 278 if not password:
279 279 sys.exit()
280 280 if email is None:
281 email = raw_input('Specify admin email:')
281 email = input('Specify admin email:')
282 282 api_key = self.cli_args.get('api_key')
283 283 self.create_user(username, password, email, True,
284 284 strict_creation_check=False,
285 285 api_key=api_key)
286 286
287 287 def _get_admin_password(self):
288 288 password = getpass.getpass('Specify admin password '
289 289 '(min 6 chars):')
290 290 confirm = getpass.getpass('Confirm password:')
291 291
292 292 if password != confirm:
293 293 log.error('passwords mismatch')
294 294 return False
295 295 if len(password) < 6:
296 296 log.error('password is too short - use at least 6 characters')
297 297 return False
298 298
299 299 return password
300 300
301 301 def create_test_admin_and_users(self):
302 302 log.info('creating admin and regular test users')
303 303 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
304 304 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
305 305 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
306 306 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
307 307 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
308 308
309 309 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
310 310 TEST_USER_ADMIN_EMAIL, True, api_key=True)
311 311
312 312 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
313 313 TEST_USER_REGULAR_EMAIL, False, api_key=True)
314 314
315 315 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
316 316 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
317 317
318 318 def create_ui_settings(self, repo_store_path):
319 319 """
320 320 Creates ui settings, fills out hooks
321 321 and disables dotencode
322 322 """
323 323 settings_model = SettingsModel(sa=self.sa)
324 324 from rhodecode.lib.vcs.backends.hg import largefiles_store
325 325 from rhodecode.lib.vcs.backends.git import lfs_store
326 326
327 327 # Build HOOKS
328 328 hooks = [
329 329 (RhodeCodeUi.HOOK_REPO_SIZE, 'python:vcsserver.hooks.repo_size'),
330 330
331 331 # HG
332 332 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
333 333 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
334 334 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
335 335 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
336 336 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
337 337 (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'),
338 338
339 339 ]
340 340
341 341 for key, value in hooks:
342 342 hook_obj = settings_model.get_ui_by_key(key)
343 343 hooks2 = hook_obj if hook_obj else RhodeCodeUi()
344 344 hooks2.ui_section = 'hooks'
345 345 hooks2.ui_key = key
346 346 hooks2.ui_value = value
347 347 self.sa.add(hooks2)
348 348
349 349 # enable largefiles
350 350 largefiles = RhodeCodeUi()
351 351 largefiles.ui_section = 'extensions'
352 352 largefiles.ui_key = 'largefiles'
353 353 largefiles.ui_value = ''
354 354 self.sa.add(largefiles)
355 355
356 356 # set default largefiles cache dir, defaults to
357 357 # /repo_store_location/.cache/largefiles
358 358 largefiles = RhodeCodeUi()
359 359 largefiles.ui_section = 'largefiles'
360 360 largefiles.ui_key = 'usercache'
361 361 largefiles.ui_value = largefiles_store(repo_store_path)
362 362
363 363 self.sa.add(largefiles)
364 364
365 365 # set default lfs cache dir, defaults to
366 366 # /repo_store_location/.cache/lfs_store
367 367 lfsstore = RhodeCodeUi()
368 368 lfsstore.ui_section = 'vcs_git_lfs'
369 369 lfsstore.ui_key = 'store_location'
370 370 lfsstore.ui_value = lfs_store(repo_store_path)
371 371
372 372 self.sa.add(lfsstore)
373 373
374 374 # enable hgsubversion disabled by default
375 375 hgsubversion = RhodeCodeUi()
376 376 hgsubversion.ui_section = 'extensions'
377 377 hgsubversion.ui_key = 'hgsubversion'
378 378 hgsubversion.ui_value = ''
379 379 hgsubversion.ui_active = False
380 380 self.sa.add(hgsubversion)
381 381
382 382 # enable hgevolve disabled by default
383 383 hgevolve = RhodeCodeUi()
384 384 hgevolve.ui_section = 'extensions'
385 385 hgevolve.ui_key = 'evolve'
386 386 hgevolve.ui_value = ''
387 387 hgevolve.ui_active = False
388 388 self.sa.add(hgevolve)
389 389
390 390 hgevolve = RhodeCodeUi()
391 391 hgevolve.ui_section = 'experimental'
392 392 hgevolve.ui_key = 'evolution'
393 393 hgevolve.ui_value = ''
394 394 hgevolve.ui_active = False
395 395 self.sa.add(hgevolve)
396 396
397 397 hgevolve = RhodeCodeUi()
398 398 hgevolve.ui_section = 'experimental'
399 399 hgevolve.ui_key = 'evolution.exchange'
400 400 hgevolve.ui_value = ''
401 401 hgevolve.ui_active = False
402 402 self.sa.add(hgevolve)
403 403
404 404 hgevolve = RhodeCodeUi()
405 405 hgevolve.ui_section = 'extensions'
406 406 hgevolve.ui_key = 'topic'
407 407 hgevolve.ui_value = ''
408 408 hgevolve.ui_active = False
409 409 self.sa.add(hgevolve)
410 410
411 411 # enable hggit disabled by default
412 412 hggit = RhodeCodeUi()
413 413 hggit.ui_section = 'extensions'
414 414 hggit.ui_key = 'hggit'
415 415 hggit.ui_value = ''
416 416 hggit.ui_active = False
417 417 self.sa.add(hggit)
418 418
419 419 # set svn branch defaults
420 420 branches = ["/branches/*", "/trunk"]
421 421 tags = ["/tags/*"]
422 422
423 423 for branch in branches:
424 424 settings_model.create_ui_section_value(
425 425 RhodeCodeUi.SVN_BRANCH_ID, branch)
426 426
427 427 for tag in tags:
428 428 settings_model.create_ui_section_value(RhodeCodeUi.SVN_TAG_ID, tag)
429 429
430 430 def create_auth_plugin_options(self, skip_existing=False):
431 431 """
432 432 Create default auth plugin settings, and make it active
433 433
434 434 :param skip_existing:
435 435 """
436 436 defaults = [
437 437 ('auth_plugins',
438 438 'egg:rhodecode-enterprise-ce#token,egg:rhodecode-enterprise-ce#rhodecode',
439 439 'list'),
440 440
441 441 ('auth_authtoken_enabled',
442 442 'True',
443 443 'bool'),
444 444
445 445 ('auth_rhodecode_enabled',
446 446 'True',
447 447 'bool'),
448 448 ]
449 449 for k, v, t in defaults:
450 450 if (skip_existing and
451 451 SettingsModel().get_setting_by_name(k) is not None):
452 452 log.debug('Skipping option %s', k)
453 453 continue
454 454 setting = RhodeCodeSetting(k, v, t)
455 455 self.sa.add(setting)
456 456
457 457 def create_default_options(self, skip_existing=False):
458 458 """Creates default settings"""
459 459
460 460 for k, v, t in [
461 461 ('default_repo_enable_locking', False, 'bool'),
462 462 ('default_repo_enable_downloads', False, 'bool'),
463 463 ('default_repo_enable_statistics', False, 'bool'),
464 464 ('default_repo_private', False, 'bool'),
465 465 ('default_repo_type', 'hg', 'unicode')]:
466 466
467 467 if (skip_existing and
468 468 SettingsModel().get_setting_by_name(k) is not None):
469 469 log.debug('Skipping option %s', k)
470 470 continue
471 471 setting = RhodeCodeSetting(k, v, t)
472 472 self.sa.add(setting)
473 473
474 474 def fixup_groups(self):
475 475 def_usr = User.get_default_user()
476 476 for g in RepoGroup.query().all():
477 477 g.group_name = g.get_new_name(g.name)
478 478 self.sa.add(g)
479 479 # get default perm
480 480 default = UserRepoGroupToPerm.query()\
481 481 .filter(UserRepoGroupToPerm.group == g)\
482 482 .filter(UserRepoGroupToPerm.user == def_usr)\
483 483 .scalar()
484 484
485 485 if default is None:
486 486 log.debug('missing default permission for group %s adding', g)
487 487 perm_obj = RepoGroupModel()._create_default_perms(g)
488 488 self.sa.add(perm_obj)
489 489
490 490 def reset_permissions(self, username):
491 491 """
492 492 Resets permissions to default state, useful when old systems had
493 493 bad permissions, we must clean them up
494 494
495 495 :param username:
496 496 """
497 497 default_user = User.get_by_username(username)
498 498 if not default_user:
499 499 return
500 500
501 501 u2p = UserToPerm.query()\
502 502 .filter(UserToPerm.user == default_user).all()
503 503 fixed = False
504 504 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
505 505 for p in u2p:
506 506 Session().delete(p)
507 507 fixed = True
508 508 self.populate_default_permissions()
509 509 return fixed
510 510
511 511 def config_prompt(self, test_repo_path='', retries=3):
512 512 defaults = self.cli_args
513 513 _path = defaults.get('repos_location')
514 514 if retries == 3:
515 515 log.info('Setting up repositories config')
516 516
517 517 if _path is not None:
518 518 path = _path
519 519 elif not self.tests and not test_repo_path:
520 path = raw_input(
520 path = input(
521 521 'Enter a valid absolute path to store repositories. '
522 522 'All repositories in that path will be added automatically:'
523 523 )
524 524 else:
525 525 path = test_repo_path
526 526 path_ok = True
527 527
528 528 # check proper dir
529 529 if not os.path.isdir(path):
530 530 path_ok = False
531 531 log.error('Given path %s is not a valid directory', path)
532 532
533 533 elif not os.path.isabs(path):
534 534 path_ok = False
535 535 log.error('Given path %s is not an absolute path', path)
536 536
537 537 # check if path is at least readable.
538 538 if not os.access(path, os.R_OK):
539 539 path_ok = False
540 540 log.error('Given path %s is not readable', path)
541 541
542 542 # check write access, warn user about non writeable paths
543 543 elif not os.access(path, os.W_OK) and path_ok:
544 544 log.warning('No write permission to given path %s', path)
545 545
546 546 q = ('Given path %s is not writeable, do you want to '
547 547 'continue with read only mode ? [y/n]' % (path,))
548 548 if not self.ask_ok(q):
549 549 log.error('Canceled by user')
550 550 sys.exit(-1)
551 551
552 552 if retries == 0:
553 553 sys.exit('max retries reached')
554 554 if not path_ok:
555 555 retries -= 1
556 556 return self.config_prompt(test_repo_path, retries)
557 557
558 558 real_path = os.path.normpath(os.path.realpath(path))
559 559
560 560 if real_path != os.path.normpath(path):
561 561 q = ('Path looks like a symlink, RhodeCode Enterprise will store '
562 562 'given path as %s ? [y/n]') % (real_path,)
563 563 if not self.ask_ok(q):
564 564 log.error('Canceled by user')
565 565 sys.exit(-1)
566 566
567 567 return real_path
568 568
569 569 def create_settings(self, path):
570 570
571 571 self.create_ui_settings(path)
572 572
573 573 ui_config = [
574 574 ('web', 'push_ssl', 'False'),
575 575 ('web', 'allow_archive', 'gz zip bz2'),
576 576 ('web', 'allow_push', '*'),
577 577 ('web', 'baseurl', '/'),
578 578 ('paths', '/', path),
579 579 ('phases', 'publish', 'True')
580 580 ]
581 581 for section, key, value in ui_config:
582 582 ui_conf = RhodeCodeUi()
583 583 setattr(ui_conf, 'ui_section', section)
584 584 setattr(ui_conf, 'ui_key', key)
585 585 setattr(ui_conf, 'ui_value', value)
586 586 self.sa.add(ui_conf)
587 587
588 588 # rhodecode app settings
589 589 settings = [
590 590 ('realm', 'RhodeCode', 'unicode'),
591 591 ('title', '', 'unicode'),
592 592 ('pre_code', '', 'unicode'),
593 593 ('post_code', '', 'unicode'),
594 594
595 595 # Visual
596 596 ('show_public_icon', True, 'bool'),
597 597 ('show_private_icon', True, 'bool'),
598 598 ('stylify_metatags', True, 'bool'),
599 599 ('dashboard_items', 100, 'int'),
600 600 ('admin_grid_items', 25, 'int'),
601 601
602 602 ('markup_renderer', 'markdown', 'unicode'),
603 603
604 604 ('repository_fields', True, 'bool'),
605 605 ('show_version', True, 'bool'),
606 606 ('show_revision_number', True, 'bool'),
607 607 ('show_sha_length', 12, 'int'),
608 608
609 609 ('use_gravatar', False, 'bool'),
610 610 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
611 611
612 612 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
613 613 ('clone_uri_id_tmpl', Repository.DEFAULT_CLONE_URI_ID, 'unicode'),
614 614 ('clone_uri_ssh_tmpl', Repository.DEFAULT_CLONE_URI_SSH, 'unicode'),
615 615 ('support_url', '', 'unicode'),
616 616 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
617 617
618 618 # VCS Settings
619 619 ('pr_merge_enabled', True, 'bool'),
620 620 ('use_outdated_comments', True, 'bool'),
621 621 ('diff_cache', True, 'bool'),
622 622 ]
623 623
624 624 for key, val, type_ in settings:
625 625 sett = RhodeCodeSetting(key, val, type_)
626 626 self.sa.add(sett)
627 627
628 628 self.create_auth_plugin_options()
629 629 self.create_default_options()
630 630
631 631 log.info('created ui config')
632 632
633 633 def create_user(self, username, password, email='', admin=False,
634 634 strict_creation_check=True, api_key=None):
635 635 log.info('creating user `%s`', username)
636 636 user = UserModel().create_or_update(
637 637 username, password, email, firstname=u'RhodeCode', lastname=u'Admin',
638 638 active=True, admin=admin, extern_type="rhodecode",
639 639 strict_creation_check=strict_creation_check)
640 640
641 641 if api_key:
642 642 log.info('setting a new default auth token for user `%s`', username)
643 643 UserModel().add_auth_token(
644 644 user=user, lifetime_minutes=-1,
645 645 role=UserModel.auth_token_role.ROLE_ALL,
646 646 description=u'BUILTIN TOKEN')
647 647
648 648 def create_default_user(self):
649 649 log.info('creating default user')
650 650 # create default user for handling default permissions.
651 651 user = UserModel().create_or_update(username=User.DEFAULT_USER,
652 652 password=str(uuid.uuid1())[:20],
653 653 email=User.DEFAULT_USER_EMAIL,
654 654 firstname=u'Anonymous',
655 655 lastname=u'User',
656 656 strict_creation_check=False)
657 657 # based on configuration options activate/de-activate this user which
658 658 # controlls anonymous access
659 659 if self.cli_args.get('public_access') is False:
660 660 log.info('Public access disabled')
661 661 user.active = False
662 662 Session().add(user)
663 663 Session().commit()
664 664
665 665 def create_permissions(self):
666 666 """
667 667 Creates all permissions defined in the system
668 668 """
669 669 # module.(access|create|change|delete)_[name]
670 670 # module.(none|read|write|admin)
671 671 log.info('creating permissions')
672 672 PermissionModel(self.sa).create_permissions()
673 673
674 674 def populate_default_permissions(self):
675 675 """
676 676 Populate default permissions. It will create only the default
677 677 permissions that are missing, and not alter already defined ones
678 678 """
679 679 log.info('creating default user permissions')
680 680 PermissionModel(self.sa).create_default_user_permissions(user=User.DEFAULT_USER)
@@ -1,1060 +1,1061 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (c) 2007-2012 Christoph Haas <email@christoph-haas.de>
4 4 # NOTE: MIT license based code, backported and edited by RhodeCode GmbH
5 5
6 6 """
7 7 paginate: helps split up large collections into individual pages
8 8 ================================================================
9 9
10 10 What is pagination?
11 11 ---------------------
12 12
13 13 This module helps split large lists of items into pages. The user is shown one page at a time and
14 14 can navigate to other pages. Imagine you are offering a company phonebook and let the user search
15 15 the entries. The entire search result may contains 23 entries but you want to display no more than
16 16 10 entries at once. The first page contains entries 1-10, the second 11-20 and the third 21-23.
17 17 Each "Page" instance represents the items of one of these three pages.
18 18
19 19 See the documentation of the "Page" class for more information.
20 20
21 21 How do I use it?
22 22 ------------------
23 23
24 24 A page of items is represented by the *Page* object. A *Page* gets initialized with these arguments:
25 25
26 26 - The collection of items to pick a range from. Usually just a list.
27 27 - The page number you want to display. Default is 1: the first page.
28 28
29 29 Now we can make up a collection and create a Page instance of it::
30 30
31 31 # Create a sample collection of 1000 items
32 32 >> my_collection = range(1000)
33 33
34 34 # Create a Page object for the 3rd page (20 items per page is the default)
35 35 >> my_page = Page(my_collection, page=3)
36 36
37 37 # The page object can be printed as a string to get its details
38 38 >> str(my_page)
39 39 Page:
40 40 Collection type: <type 'range'>
41 41 Current page: 3
42 42 First item: 41
43 43 Last item: 60
44 44 First page: 1
45 45 Last page: 50
46 46 Previous page: 2
47 47 Next page: 4
48 48 Items per page: 20
49 49 Number of items: 1000
50 50 Number of pages: 50
51 51
52 52 # Print a list of items on the current page
53 53 >> my_page.items
54 54 [40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59]
55 55
56 56 # The *Page* object can be used as an iterator:
57 57 >> for my_item in my_page: print(my_item)
58 58 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
59 59
60 60 # The .pager() method returns an HTML fragment with links to surrounding pages.
61 61 >> my_page.pager(url="http://example.org/foo/page=$page")
62 62
63 63 <a href="http://example.org/foo/page=1">1</a>
64 64 <a href="http://example.org/foo/page=2">2</a>
65 65 3
66 66 <a href="http://example.org/foo/page=4">4</a>
67 67 <a href="http://example.org/foo/page=5">5</a>
68 68 ..
69 69 <a href="http://example.org/foo/page=50">50</a>'
70 70
71 71 # Without the HTML it would just look like:
72 72 # 1 2 [3] 4 5 .. 50
73 73
74 74 # The pager can be customized:
75 75 >> my_page.pager('$link_previous ~3~ $link_next (Page $page of $page_count)',
76 76 url="http://example.org/foo/page=$page")
77 77
78 78 <a href="http://example.org/foo/page=2">&lt;</a>
79 79 <a href="http://example.org/foo/page=1">1</a>
80 80 <a href="http://example.org/foo/page=2">2</a>
81 81 3
82 82 <a href="http://example.org/foo/page=4">4</a>
83 83 <a href="http://example.org/foo/page=5">5</a>
84 84 <a href="http://example.org/foo/page=6">6</a>
85 85 ..
86 86 <a href="http://example.org/foo/page=50">50</a>
87 87 <a href="http://example.org/foo/page=4">&gt;</a>
88 88 (Page 3 of 50)
89 89
90 90 # Without the HTML it would just look like:
91 91 # 1 2 [3] 4 5 6 .. 50 > (Page 3 of 50)
92 92
93 93 # The url argument to the pager method can be omitted when an url_maker is
94 94 # given during instantiation:
95 95 >> my_page = Page(my_collection, page=3,
96 96 url_maker=lambda p: "http://example.org/%s" % p)
97 97 >> page.pager()
98 98
99 99 There are some interesting parameters that customize the Page's behavior. See the documentation on
100 100 ``Page`` and ``Page.pager()``.
101 101
102 102
103 103 Notes
104 104 -------
105 105
106 106 Page numbers and item numbers start at 1. This concept has been used because users expect that the
107 107 first page has number 1 and the first item on a page also has number 1. So if you want to use the
108 108 page's items by their index number please note that you have to subtract 1.
109 109 """
110 110
111 111 import re
112 112 import sys
113 113 from string import Template
114 114 from webhelpers2.html import literal
115 115
116 116 # are we running at least python 3.x ?
117 117 PY3 = sys.version_info[0] >= 3
118 118
119 119 if PY3:
120 120 unicode = str
121 121
122 122
123 123 def make_html_tag(tag, text=None, **params):
124 124 """Create an HTML tag string.
125 125
126 126 tag
127 127 The HTML tag to use (e.g. 'a', 'span' or 'div')
128 128
129 129 text
130 130 The text to enclose between opening and closing tag. If no text is specified then only
131 131 the opening tag is returned.
132 132
133 133 Example::
134 134 make_html_tag('a', text="Hello", href="/another/page")
135 135 -> <a href="/another/page">Hello</a>
136 136
137 137 To use reserved Python keywords like "class" as a parameter prepend it with
138 138 an underscore. Instead of "class='green'" use "_class='green'".
139 139
140 140 Warning: Quotes and apostrophes are not escaped."""
141 141 params_string = ""
142 142
143 143 # Parameters are passed. Turn the dict into a string like "a=1 b=2 c=3" string.
144 144 for key, value in sorted(params.items()):
145 145 # Strip off a leading underscore from the attribute's key to allow attributes like '_class'
146 146 # to be used as a CSS class specification instead of the reserved Python keyword 'class'.
147 147 key = key.lstrip("_")
148 148
149 149 params_string += u' {0}="{1}"'.format(key, value)
150 150
151 151 # Create the tag string
152 152 tag_string = u"<{0}{1}>".format(tag, params_string)
153 153
154 154 # Add text and closing tag if required.
155 155 if text:
156 156 tag_string += u"{0}</{1}>".format(text, tag)
157 157
158 158 return tag_string
159 159
160 160
161 161 # Since the items on a page are mainly a list we subclass the "list" type
162 162 class _Page(list):
163 163 """A list/iterator representing the items on one page of a larger collection.
164 164
165 165 An instance of the "Page" class is created from a _collection_ which is any
166 166 list-like object that allows random access to its elements.
167 167
168 168 The instance works as an iterator running from the first item to the last item on the given
169 169 page. The Page.pager() method creates a link list allowing the user to go to other pages.
170 170
171 171 A "Page" does not only carry the items on a certain page. It gives you additional information
172 172 about the page in these "Page" object attributes:
173 173
174 174 item_count
175 175 Number of items in the collection
176 176
177 177 **WARNING:** Unless you pass in an item_count, a count will be
178 178 performed on the collection every time a Page instance is created.
179 179
180 180 page
181 181 Number of the current page
182 182
183 183 items_per_page
184 184 Maximal number of items displayed on a page
185 185
186 186 first_page
187 187 Number of the first page - usually 1 :)
188 188
189 189 last_page
190 190 Number of the last page
191 191
192 192 previous_page
193 193 Number of the previous page. If this is the first page it returns None.
194 194
195 195 next_page
196 196 Number of the next page. If this is the last page it returns None.
197 197
198 198 page_count
199 199 Number of pages
200 200
201 201 items
202 202 Sequence/iterator of items on the current page
203 203
204 204 first_item
205 205 Index of first item on the current page - starts with 1
206 206
207 207 last_item
208 208 Index of last item on the current page
209 209 """
210 210
211 211 def __init__(
212 212 self,
213 213 collection,
214 214 page=1,
215 215 items_per_page=20,
216 216 item_count=None,
217 217 wrapper_class=None,
218 218 url_maker=None,
219 219 bar_size=10,
220 220 **kwargs
221 221 ):
222 222 """Create a "Page" instance.
223 223
224 224 Parameters:
225 225
226 226 collection
227 227 Sequence representing the collection of items to page through.
228 228
229 229 page
230 230 The requested page number - starts with 1. Default: 1.
231 231
232 232 items_per_page
233 233 The maximal number of items to be displayed per page.
234 234 Default: 20.
235 235
236 236 item_count (optional)
237 237 The total number of items in the collection - if known.
238 238 If this parameter is not given then the paginator will count
239 239 the number of elements in the collection every time a "Page"
240 240 is created. Giving this parameter will speed up things. In a busy
241 241 real-life application you may want to cache the number of items.
242 242
243 243 url_maker (optional)
244 244 Callback to generate the URL of other pages, given its numbers.
245 245 Must accept one int parameter and return a URI string.
246 246
247 247 bar_size
248 248 maximum size of rendered pages numbers within radius
249 249
250 250 """
251 251 if collection is not None:
252 252 if wrapper_class is None:
253 253 # Default case. The collection is already a list-type object.
254 254 self.collection = collection
255 255 else:
256 256 # Special case. A custom wrapper class is used to access elements of the collection.
257 257 self.collection = wrapper_class(collection)
258 258 else:
259 259 self.collection = []
260 260
261 261 self.collection_type = type(collection)
262 262
263 263 if url_maker is not None:
264 264 self.url_maker = url_maker
265 265 else:
266 266 self.url_maker = self._default_url_maker
267 267 self.bar_size = bar_size
268 268 # Assign kwargs to self
269 269 self.kwargs = kwargs
270 270
271 271 # The self.page is the number of the current page.
272 272 # The first page has the number 1!
273 273 try:
274 274 self.page = int(page) # make it int() if we get it as a string
275 275 except (ValueError, TypeError):
276 276 self.page = 1
277 277 # normally page should be always at least 1 but the original maintainer
278 278 # decided that for empty collection and empty page it can be...0? (based on tests)
279 279 # preserving behavior for BW compat
280 280 if self.page < 1:
281 281 self.page = 1
282 282
283 283 self.items_per_page = items_per_page
284 284
285 285 # We subclassed "list" so we need to call its init() method
286 286 # and fill the new list with the items to be displayed on the page.
287 287 # We use list() so that the items on the current page are retrieved
288 288 # only once. In an SQL context that could otherwise lead to running the
289 289 # same SQL query every time items would be accessed.
290 290 # We do this here, prior to calling len() on the collection so that a
291 291 # wrapper class can execute a query with the knowledge of what the
292 292 # slice will be (for efficiency) and, in the same query, ask for the
293 293 # total number of items and only execute one query.
294
294 295 try:
295 296 first = (self.page - 1) * items_per_page
296 297 last = first + items_per_page
297 298 self.items = list(self.collection[first:last])
298 299 except TypeError as err:
299 300 raise TypeError(
300 "Your collection of type {} cannot be handled "
301 "by paginate. ERROR:{}".format(type(self.collection), err)
301 f"Your collection of type {type(self.collection)} cannot be handled "
302 f"by paginate. ERROR:{err}"
302 303 )
303 304
304 305 # Unless the user tells us how many items the collections has
305 306 # we calculate that ourselves.
306 307 if item_count is not None:
307 308 self.item_count = item_count
308 309 else:
309 310 self.item_count = len(self.collection)
310 311
311 312 # Compute the number of the first and last available page
312 313 if self.item_count > 0:
313 314 self.first_page = 1
314 315 self.page_count = ((self.item_count - 1) // self.items_per_page) + 1
315 316 self.last_page = self.first_page + self.page_count - 1
316 317
317 318 # Make sure that the requested page number is the range of valid pages
318 319 if self.page > self.last_page:
319 320 self.page = self.last_page
320 321 elif self.page < self.first_page:
321 322 self.page = self.first_page
322 323
323 324 # Note: the number of items on this page can be less than
324 325 # items_per_page if the last page is not full
325 326 self.first_item = (self.page - 1) * items_per_page + 1
326 327 self.last_item = min(self.first_item + items_per_page - 1, self.item_count)
327 328
328 329 # Links to previous and next page
329 330 if self.page > self.first_page:
330 331 self.previous_page = self.page - 1
331 332 else:
332 333 self.previous_page = None
333 334
334 335 if self.page < self.last_page:
335 336 self.next_page = self.page + 1
336 337 else:
337 338 self.next_page = None
338 339
339 340 # No items available
340 341 else:
341 342 self.first_page = None
342 343 self.page_count = 0
343 344 self.last_page = None
344 345 self.first_item = None
345 346 self.last_item = None
346 347 self.previous_page = None
347 348 self.next_page = None
348 349 self.items = []
349 350
350 351 # This is a subclass of the 'list' type. Initialise the list now.
351 352 list.__init__(self, self.items)
352 353
353 354 def __str__(self):
354 355 return (
355 356 "Page:\n"
356 357 "Collection type: {0.collection_type}\n"
357 358 "Current page: {0.page}\n"
358 359 "First item: {0.first_item}\n"
359 360 "Last item: {0.last_item}\n"
360 361 "First page: {0.first_page}\n"
361 362 "Last page: {0.last_page}\n"
362 363 "Previous page: {0.previous_page}\n"
363 364 "Next page: {0.next_page}\n"
364 365 "Items per page: {0.items_per_page}\n"
365 366 "Total number of items: {0.item_count}\n"
366 367 "Number of pages: {0.page_count}\n"
367 368 ).format(self)
368 369
369 370 def __repr__(self):
370 371 return "<paginate.Page: Page {0}/{1}>".format(self.page, self.page_count)
371 372
372 373 def pager(
373 374 self,
374 375 tmpl_format="~2~",
375 376 url=None,
376 377 show_if_single_page=False,
377 378 separator=" ",
378 379 symbol_first="&lt;&lt;",
379 380 symbol_last="&gt;&gt;",
380 381 symbol_previous="&lt;",
381 382 symbol_next="&gt;",
382 383 link_attr=None,
383 384 curpage_attr=None,
384 385 dotdot_attr=None,
385 386 link_tag=None,
386 387 ):
387 388 """
388 389 Return string with links to other pages (e.g. '1 .. 5 6 7 [8] 9 10 11 .. 50').
389 390
390 391 tmpl_format:
391 392 Format string that defines how the pager is rendered. The string
392 393 can contain the following $-tokens that are substituted by the
393 394 string.Template module:
394 395
395 396 - $first_page: number of first reachable page
396 397 - $last_page: number of last reachable page
397 398 - $page: number of currently selected page
398 399 - $page_count: number of reachable pages
399 400 - $items_per_page: maximal number of items per page
400 401 - $first_item: index of first item on the current page
401 402 - $last_item: index of last item on the current page
402 403 - $item_count: total number of items
403 404 - $link_first: link to first page (unless this is first page)
404 405 - $link_last: link to last page (unless this is last page)
405 406 - $link_previous: link to previous page (unless this is first page)
406 407 - $link_next: link to next page (unless this is last page)
407 408
408 409 To render a range of pages the token '~3~' can be used. The
409 410 number sets the radius of pages around the current page.
410 411 Example for a range with radius 3:
411 412
412 413 '1 .. 5 6 7 [8] 9 10 11 .. 50'
413 414
414 415 Default: '~2~'
415 416
416 417 url
417 418 The URL that page links will point to. Make sure it contains the string
418 419 $page which will be replaced by the actual page number.
419 420 Must be given unless a url_maker is specified to __init__, in which
420 421 case this parameter is ignored.
421 422
422 423 symbol_first
423 424 String to be displayed as the text for the $link_first link above.
424 425
425 426 Default: '&lt;&lt;' (<<)
426 427
427 428 symbol_last
428 429 String to be displayed as the text for the $link_last link above.
429 430
430 431 Default: '&gt;&gt;' (>>)
431 432
432 433 symbol_previous
433 434 String to be displayed as the text for the $link_previous link above.
434 435
435 436 Default: '&lt;' (<)
436 437
437 438 symbol_next
438 439 String to be displayed as the text for the $link_next link above.
439 440
440 441 Default: '&gt;' (>)
441 442
442 443 separator:
443 444 String that is used to separate page links/numbers in the above range of pages.
444 445
445 446 Default: ' '
446 447
447 448 show_if_single_page:
448 449 if True the navigator will be shown even if there is only one page.
449 450
450 451 Default: False
451 452
452 453 link_attr (optional)
453 454 A dictionary of attributes that get added to A-HREF links pointing to other pages. Can
454 455 be used to define a CSS style or class to customize the look of links.
455 456
456 457 Example: { 'style':'border: 1px solid green' }
457 458 Example: { 'class':'pager_link' }
458 459
459 460 curpage_attr (optional)
460 461 A dictionary of attributes that get added to the current page number in the pager (which
461 462 is obviously not a link). If this dictionary is not empty then the elements will be
462 463 wrapped in a SPAN tag with the given attributes.
463 464
464 465 Example: { 'style':'border: 3px solid blue' }
465 466 Example: { 'class':'pager_curpage' }
466 467
467 468 dotdot_attr (optional)
468 469 A dictionary of attributes that get added to the '..' string in the pager (which is
469 470 obviously not a link). If this dictionary is not empty then the elements will be wrapped
470 471 in a SPAN tag with the given attributes.
471 472
472 473 Example: { 'style':'color: #808080' }
473 474 Example: { 'class':'pager_dotdot' }
474 475
475 476 link_tag (optional)
476 477 A callable that accepts single argument `page` (page link information)
477 478 and generates string with html that represents the link for specific page.
478 479 Page objects are supplied from `link_map()` so the keys are the same.
479 480
480 481
481 482 """
482 483 link_attr = link_attr or {}
483 484 curpage_attr = curpage_attr or {}
484 485 dotdot_attr = dotdot_attr or {}
485 486 self.curpage_attr = curpage_attr
486 487 self.separator = separator
487 488 self.link_attr = link_attr
488 489 self.dotdot_attr = dotdot_attr
489 490 self.url = url
490 491 self.link_tag = link_tag or self.default_link_tag
491 492
492 493 # Don't show navigator if there is no more than one page
493 494 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
494 495 return ""
495 496
496 497 regex_res = re.search(r"~(\d+)~", tmpl_format)
497 498 if regex_res:
498 499 radius = regex_res.group(1)
499 500 else:
500 501 radius = 2
501 502
502 503 self.radius = int(radius)
503 504 link_map = self.link_map(
504 505 tmpl_format=tmpl_format,
505 506 url=url,
506 507 show_if_single_page=show_if_single_page,
507 508 separator=separator,
508 509 symbol_first=symbol_first,
509 510 symbol_last=symbol_last,
510 511 symbol_previous=symbol_previous,
511 512 symbol_next=symbol_next,
512 513 link_attr=link_attr,
513 514 curpage_attr=curpage_attr,
514 515 dotdot_attr=dotdot_attr,
515 516 link_tag=link_tag,
516 517 )
517 518 links_markup = self._range(link_map, self.radius)
518 519
519 520 # Replace ~...~ in token tmpl_format by range of pages
520 521 result = re.sub(r"~(\d+)~", links_markup, tmpl_format)
521 522
522 523 link_first = (
523 524 self.page > self.first_page and self.link_tag(link_map["first_page"]) or ""
524 525 )
525 526 link_last = (
526 527 self.page < self.last_page and self.link_tag(link_map["last_page"]) or ""
527 528 )
528 529 link_previous = (
529 530 self.previous_page and self.link_tag(link_map["previous_page"]) or ""
530 531 )
531 532 link_next = self.next_page and self.link_tag(link_map["next_page"]) or ""
532 533 # Interpolate '$' variables
533 534 result = Template(result).safe_substitute(
534 535 {
535 536 "first_page": self.first_page,
536 537 "last_page": self.last_page,
537 538 "page": self.page,
538 539 "page_count": self.page_count,
539 540 "items_per_page": self.items_per_page,
540 541 "first_item": self.first_item,
541 542 "last_item": self.last_item,
542 543 "item_count": self.item_count,
543 544 "link_first": link_first,
544 545 "link_last": link_last,
545 546 "link_previous": link_previous,
546 547 "link_next": link_next,
547 548 }
548 549 )
549 550
550 551 return result
551 552
552 553 def _get_edges(self, cur_page, max_page, items):
553 554 cur_page = int(cur_page)
554 555 edge = (items / 2) + 1
555 556 if cur_page <= edge:
556 557 radius = max(items / 2, items - cur_page)
557 558 elif (max_page - cur_page) < edge:
558 559 radius = (items - 1) - (max_page - cur_page)
559 560 else:
560 561 radius = (items / 2) - 1
561 562
562 563 left = max(1, (cur_page - radius))
563 564 right = min(max_page, cur_page + radius)
564 565 return left, right
565 566
566 567 def link_map(
567 568 self,
568 569 tmpl_format="~2~",
569 570 url=None,
570 571 show_if_single_page=False,
571 572 separator=" ",
572 573 symbol_first="&lt;&lt;",
573 574 symbol_last="&gt;&gt;",
574 575 symbol_previous="&lt;",
575 576 symbol_next="&gt;",
576 577 link_attr=None,
577 578 curpage_attr=None,
578 579 dotdot_attr=None,
579 580 link_tag=None
580 581 ):
581 582 """ Return map with links to other pages if default pager() function is not suitable solution.
582 583 tmpl_format:
583 584 Format string that defines how the pager would be normally rendered rendered. Uses same arguments as pager()
584 585 method, but returns a simple dictionary in form of:
585 586 {'current_page': {'attrs': {},
586 587 'href': 'http://example.org/foo/page=1',
587 588 'value': 1},
588 589 'first_page': {'attrs': {},
589 590 'href': 'http://example.org/foo/page=1',
590 591 'type': 'first_page',
591 592 'value': 1},
592 593 'last_page': {'attrs': {},
593 594 'href': 'http://example.org/foo/page=8',
594 595 'type': 'last_page',
595 596 'value': 8},
596 597 'next_page': {'attrs': {}, 'href': 'HREF', 'type': 'next_page', 'value': 2},
597 598 'previous_page': None,
598 599 'range_pages': [{'attrs': {},
599 600 'href': 'http://example.org/foo/page=1',
600 601 'type': 'current_page',
601 602 'value': 1},
602 603 ....
603 604 {'attrs': {}, 'href': '', 'type': 'span', 'value': '..'}]}
604 605
605 606
606 607 The string can contain the following $-tokens that are substituted by the
607 608 string.Template module:
608 609
609 610 - $first_page: number of first reachable page
610 611 - $last_page: number of last reachable page
611 612 - $page: number of currently selected page
612 613 - $page_count: number of reachable pages
613 614 - $items_per_page: maximal number of items per page
614 615 - $first_item: index of first item on the current page
615 616 - $last_item: index of last item on the current page
616 617 - $item_count: total number of items
617 618 - $link_first: link to first page (unless this is first page)
618 619 - $link_last: link to last page (unless this is last page)
619 620 - $link_previous: link to previous page (unless this is first page)
620 621 - $link_next: link to next page (unless this is last page)
621 622
622 623 To render a range of pages the token '~3~' can be used. The
623 624 number sets the radius of pages around the current page.
624 625 Example for a range with radius 3:
625 626
626 627 '1 .. 5 6 7 [8] 9 10 11 .. 50'
627 628
628 629 Default: '~2~'
629 630
630 631 url
631 632 The URL that page links will point to. Make sure it contains the string
632 633 $page which will be replaced by the actual page number.
633 634 Must be given unless a url_maker is specified to __init__, in which
634 635 case this parameter is ignored.
635 636
636 637 symbol_first
637 638 String to be displayed as the text for the $link_first link above.
638 639
639 640 Default: '&lt;&lt;' (<<)
640 641
641 642 symbol_last
642 643 String to be displayed as the text for the $link_last link above.
643 644
644 645 Default: '&gt;&gt;' (>>)
645 646
646 647 symbol_previous
647 648 String to be displayed as the text for the $link_previous link above.
648 649
649 650 Default: '&lt;' (<)
650 651
651 652 symbol_next
652 653 String to be displayed as the text for the $link_next link above.
653 654
654 655 Default: '&gt;' (>)
655 656
656 657 separator:
657 658 String that is used to separate page links/numbers in the above range of pages.
658 659
659 660 Default: ' '
660 661
661 662 show_if_single_page:
662 663 if True the navigator will be shown even if there is only one page.
663 664
664 665 Default: False
665 666
666 667 link_attr (optional)
667 668 A dictionary of attributes that get added to A-HREF links pointing to other pages. Can
668 669 be used to define a CSS style or class to customize the look of links.
669 670
670 671 Example: { 'style':'border: 1px solid green' }
671 672 Example: { 'class':'pager_link' }
672 673
673 674 curpage_attr (optional)
674 675 A dictionary of attributes that get added to the current page number in the pager (which
675 676 is obviously not a link). If this dictionary is not empty then the elements will be
676 677 wrapped in a SPAN tag with the given attributes.
677 678
678 679 Example: { 'style':'border: 3px solid blue' }
679 680 Example: { 'class':'pager_curpage' }
680 681
681 682 dotdot_attr (optional)
682 683 A dictionary of attributes that get added to the '..' string in the pager (which is
683 684 obviously not a link). If this dictionary is not empty then the elements will be wrapped
684 685 in a SPAN tag with the given attributes.
685 686
686 687 Example: { 'style':'color: #808080' }
687 688 Example: { 'class':'pager_dotdot' }
688 689 """
689 690 link_attr = link_attr or {}
690 691 curpage_attr = curpage_attr or {}
691 692 dotdot_attr = dotdot_attr or {}
692 693 self.curpage_attr = curpage_attr
693 694 self.separator = separator
694 695 self.link_attr = link_attr
695 696 self.dotdot_attr = dotdot_attr
696 697 self.url = url
697 698
698 699 regex_res = re.search(r"~(\d+)~", tmpl_format)
699 700 if regex_res:
700 701 radius = regex_res.group(1)
701 702 else:
702 703 radius = 2
703 704
704 705 self.radius = int(radius)
705 706
706 707 # Compute the first and last page number within the radius
707 708 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
708 709 # -> leftmost_page = 5
709 710 # -> rightmost_page = 9
710 711 leftmost_page, rightmost_page = self._get_edges(
711 712 self.page, self.last_page, (self.radius * 2) + 1)
712 713
713 714 nav_items = {
714 715 "first_page": None,
715 716 "last_page": None,
716 717 "previous_page": None,
717 718 "next_page": None,
718 719 "current_page": None,
719 720 "radius": self.radius,
720 721 "range_pages": [],
721 722 }
722 723
723 724 if leftmost_page is None or rightmost_page is None:
724 725 return nav_items
725 726
726 727 nav_items["first_page"] = {
727 728 "type": "first_page",
728 729 "value": unicode(symbol_first),
729 730 "attrs": self.link_attr,
730 731 "number": self.first_page,
731 732 "href": self.url_maker(self.first_page),
732 733 }
733 734
734 735 # Insert dots if there are pages between the first page
735 736 # and the currently displayed page range
736 737 if leftmost_page - self.first_page > 1:
737 738 # Wrap in a SPAN tag if dotdot_attr is set
738 739 nav_items["range_pages"].append(
739 740 {
740 741 "type": "span",
741 742 "value": "..",
742 743 "attrs": self.dotdot_attr,
743 744 "href": "",
744 745 "number": None,
745 746 }
746 747 )
747 748
748 749 for this_page in range(leftmost_page, rightmost_page + 1):
749 750 # Highlight the current page number and do not use a link
750 751 if this_page == self.page:
751 752 # Wrap in a SPAN tag if curpage_attr is set
752 753 nav_items["range_pages"].append(
753 754 {
754 755 "type": "current_page",
755 756 "value": unicode(this_page),
756 757 "number": this_page,
757 758 "attrs": self.curpage_attr,
758 759 "href": self.url_maker(this_page),
759 760 }
760 761 )
761 762 nav_items["current_page"] = {
762 763 "value": this_page,
763 764 "attrs": self.curpage_attr,
764 765 "type": "current_page",
765 766 "href": self.url_maker(this_page),
766 767 }
767 768 # Otherwise create just a link to that page
768 769 else:
769 770 nav_items["range_pages"].append(
770 771 {
771 772 "type": "page",
772 773 "value": unicode(this_page),
773 774 "number": this_page,
774 775 "attrs": self.link_attr,
775 776 "href": self.url_maker(this_page),
776 777 }
777 778 )
778 779
779 780 # Insert dots if there are pages between the displayed
780 781 # page numbers and the end of the page range
781 782 if self.last_page - rightmost_page > 1:
782 783 # Wrap in a SPAN tag if dotdot_attr is set
783 784 nav_items["range_pages"].append(
784 785 {
785 786 "type": "span",
786 787 "value": "..",
787 788 "attrs": self.dotdot_attr,
788 789 "href": "",
789 790 "number": None,
790 791 }
791 792 )
792 793
793 794 # Create a link to the very last page (unless we are on the last
794 795 # page or there would be no need to insert '..' spacers)
795 796 nav_items["last_page"] = {
796 797 "type": "last_page",
797 798 "value": unicode(symbol_last),
798 799 "attrs": self.link_attr,
799 800 "href": self.url_maker(self.last_page),
800 801 "number": self.last_page,
801 802 }
802 803
803 804 nav_items["previous_page"] = {
804 805 "type": "previous_page",
805 806 "value": unicode(symbol_previous),
806 807 "attrs": self.link_attr,
807 808 "number": self.previous_page or self.first_page,
808 809 "href": self.url_maker(self.previous_page or self.first_page),
809 810 }
810 811
811 812 nav_items["next_page"] = {
812 813 "type": "next_page",
813 814 "value": unicode(symbol_next),
814 815 "attrs": self.link_attr,
815 816 "number": self.next_page or self.last_page,
816 817 "href": self.url_maker(self.next_page or self.last_page),
817 818 }
818 819
819 820 return nav_items
820 821
821 822 def _range(self, link_map, radius):
822 823 """
823 824 Return range of linked pages to substitute placeholder in pattern
824 825 """
825 826 # Compute the first and last page number within the radius
826 827 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
827 828 # -> leftmost_page = 5
828 829 # -> rightmost_page = 9
829 830 leftmost_page, rightmost_page = self._get_edges(
830 831 self.page, self.last_page, (radius * 2) + 1)
831 832
832 833 nav_items = []
833 834 # Create a link to the first page (unless we are on the first page
834 835 # or there would be no need to insert '..' spacers)
835 836 if self.first_page and self.page != self.first_page and self.first_page < leftmost_page:
836 837 page = link_map["first_page"].copy()
837 838 page["value"] = unicode(page["number"])
838 839 nav_items.append(self.link_tag(page))
839 840
840 841 for item in link_map["range_pages"]:
841 842 nav_items.append(self.link_tag(item))
842 843
843 844 # Create a link to the very last page (unless we are on the last
844 845 # page or there would be no need to insert '..' spacers)
845 846 if self.last_page and self.page != self.last_page and rightmost_page < self.last_page:
846 847 page = link_map["last_page"].copy()
847 848 page["value"] = unicode(page["number"])
848 849 nav_items.append(self.link_tag(page))
849 850
850 851 return self.separator.join(nav_items)
851 852
852 853 def _default_url_maker(self, page_number):
853 854 if self.url is None:
854 855 raise Exception(
855 856 "You need to specify a 'url' parameter containing a '$page' placeholder."
856 857 )
857 858
858 859 if "$page" not in self.url:
859 860 raise Exception("The 'url' parameter must contain a '$page' placeholder.")
860 861
861 862 return self.url.replace("$page", unicode(page_number))
862 863
863 864 @staticmethod
864 865 def default_link_tag(item):
865 866 """
866 867 Create an A-HREF tag that points to another page.
867 868 """
868 869 text = item["value"]
869 870 target_url = item["href"]
870 871
871 872 if not item["href"] or item["type"] in ("span", "current_page"):
872 873 if item["attrs"]:
873 874 text = make_html_tag("span", **item["attrs"]) + text + "</span>"
874 875 return text
875 876
876 877 return make_html_tag("a", text=text, href=target_url, **item["attrs"])
877 878
878 879 # Below is RhodeCode custom code
879 880
880 881 # Copyright (C) 2010-2020 RhodeCode GmbH
881 882 #
882 883 # This program is free software: you can redistribute it and/or modify
883 884 # it under the terms of the GNU Affero General Public License, version 3
884 885 # (only), as published by the Free Software Foundation.
885 886 #
886 887 # This program is distributed in the hope that it will be useful,
887 888 # but WITHOUT ANY WARRANTY; without even the implied warranty of
888 889 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
889 890 # GNU General Public License for more details.
890 891 #
891 892 # You should have received a copy of the GNU Affero General Public License
892 893 # along with this program. If not, see <http://www.gnu.org/licenses/>.
893 894 #
894 895 # This program is dual-licensed. If you wish to learn more about the
895 896 # RhodeCode Enterprise Edition, including its added features, Support services,
896 897 # and proprietary license terms, please see https://rhodecode.com/licenses/
897 898
898 899
899 900 PAGE_FORMAT = '$link_previous ~3~ $link_next'
900 901
901 902
902 903 class SqlalchemyOrmWrapper(object):
903 904 """Wrapper class to access elements of a collection."""
904 905
905 906 def __init__(self, pager, collection):
906 907 self.pager = pager
907 908 self.collection = collection
908 909
909 910 def __getitem__(self, range):
910 911 # Return a range of objects of an sqlalchemy.orm.query.Query object
911 912 return self.collection[range]
912 913
913 914 def __len__(self):
914 915 # support empty types, without actually making a query.
915 916 if self.collection is None or self.collection == []:
916 917 return 0
917 918
918 919 # Count the number of objects in an sqlalchemy.orm.query.Query object
919 920 return self.collection.count()
920 921
921 922
922 923 class CustomPager(_Page):
923 924
924 925 @staticmethod
925 926 def disabled_link_tag(item):
926 927 """
927 928 Create an A-HREF tag that is disabled
928 929 """
929 930 text = item['value']
930 931 attrs = item['attrs'].copy()
931 932 attrs['class'] = 'disabled ' + attrs['class']
932 933
933 934 return make_html_tag('a', text=text, **attrs)
934 935
935 936 def render(self):
936 937 # Don't show navigator if there is no more than one page
937 938 if self.page_count == 0:
938 939 return ""
939 940
940 941 self.link_tag = self.default_link_tag
941 942
942 943 link_map = self.link_map(
943 944 tmpl_format=PAGE_FORMAT, url=None,
944 945 show_if_single_page=False, separator=' ',
945 946 symbol_first='<<', symbol_last='>>',
946 947 symbol_previous='<', symbol_next='>',
947 948 link_attr={'class': 'pager_link'},
948 949 curpage_attr={'class': 'pager_curpage'},
949 950 dotdot_attr={'class': 'pager_dotdot'})
950 951
951 952 links_markup = self._range(link_map, self.radius)
952 953
953 954 link_first = (
954 955 self.page > self.first_page and self.link_tag(link_map['first_page']) or ''
955 956 )
956 957 link_last = (
957 958 self.page < self.last_page and self.link_tag(link_map['last_page']) or ''
958 959 )
959 960
960 961 link_previous = (
961 962 self.previous_page and self.link_tag(link_map['previous_page'])
962 963 or self.disabled_link_tag(link_map['previous_page'])
963 964 )
964 965 link_next = (
965 966 self.next_page and self.link_tag(link_map['next_page'])
966 967 or self.disabled_link_tag(link_map['next_page'])
967 968 )
968 969
969 970 # Interpolate '$' variables
970 971 # Replace ~...~ in token tmpl_format by range of pages
971 972 result = re.sub(r"~(\d+)~", links_markup, PAGE_FORMAT)
972 973 result = Template(result).safe_substitute(
973 974 {
974 975 "links": links_markup,
975 976 "first_page": self.first_page,
976 977 "last_page": self.last_page,
977 978 "page": self.page,
978 979 "page_count": self.page_count,
979 980 "items_per_page": self.items_per_page,
980 981 "first_item": self.first_item,
981 982 "last_item": self.last_item,
982 983 "item_count": self.item_count,
983 984 "link_first": link_first,
984 985 "link_last": link_last,
985 986 "link_previous": link_previous,
986 987 "link_next": link_next,
987 988 }
988 989 )
989 990
990 991 return literal(result)
991 992
992 993
993 994 class Page(CustomPager):
994 995 """
995 996 Custom pager to match rendering style with paginator
996 997 """
997 998
998 999 def __init__(self, collection, page=1, items_per_page=20, item_count=None,
999 1000 url_maker=None, **kwargs):
1000 1001 """
1001 1002 Special type of pager. We intercept collection to wrap it in our custom
1002 1003 logic instead of using wrapper_class
1003 1004 """
1004 1005
1005 1006 super(Page, self).__init__(collection=collection, page=page,
1006 1007 items_per_page=items_per_page, item_count=item_count,
1007 1008 wrapper_class=None, url_maker=url_maker, **kwargs)
1008 1009
1009 1010
1010 1011 class SqlPage(CustomPager):
1011 1012 """
1012 1013 Custom pager to match rendering style with paginator
1013 1014 """
1014 1015
1015 1016 def __init__(self, collection, page=1, items_per_page=20, item_count=None,
1016 1017 url_maker=None, **kwargs):
1017 1018 """
1018 1019 Special type of pager. We intercept collection to wrap it in our custom
1019 1020 logic instead of using wrapper_class
1020 1021 """
1021 1022 collection = SqlalchemyOrmWrapper(self, collection)
1022 1023
1023 1024 super(SqlPage, self).__init__(collection=collection, page=page,
1024 1025 items_per_page=items_per_page, item_count=item_count,
1025 1026 wrapper_class=None, url_maker=url_maker, **kwargs)
1026 1027
1027 1028
1028 1029 class RepoCommitsWrapper(object):
1029 1030 """Wrapper class to access elements of a collection."""
1030 1031
1031 1032 def __init__(self, pager, collection):
1032 1033 self.pager = pager
1033 1034 self.collection = collection
1034 1035
1035 1036 def __getitem__(self, range):
1036 1037 cur_page = self.pager.page
1037 1038 items_per_page = self.pager.items_per_page
1038 1039 first_item = max(0, (len(self.collection) - (cur_page * items_per_page)))
1039 1040 last_item = ((len(self.collection) - 1) - items_per_page * (cur_page - 1))
1040 1041 return reversed(list(self.collection[first_item:last_item + 1]))
1041 1042
1042 1043 def __len__(self):
1043 1044 return len(self.collection)
1044 1045
1045 1046
1046 1047 class RepoPage(CustomPager):
1047 1048 """
1048 1049 Create a "RepoPage" instance. special pager for paging repository
1049 1050 """
1050 1051
1051 1052 def __init__(self, collection, page=1, items_per_page=20, item_count=None,
1052 1053 url_maker=None, **kwargs):
1053 1054 """
1054 1055 Special type of pager. We intercept collection to wrap it in our custom
1055 1056 logic instead of using wrapper_class
1056 1057 """
1057 1058 collection = RepoCommitsWrapper(self, collection)
1058 1059 super(RepoPage, self).__init__(collection=collection, page=page,
1059 1060 items_per_page=items_per_page, item_count=item_count,
1060 1061 wrapper_class=None, url_maker=url_maker, **kwargs)
General Comments 0
You need to be logged in to leave comments. Login now