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