##// END OF EJS Templates
fixed bug when new repo had no last commiter,...
marcink -
r489:460ad816 celery
parent child Browse files
Show More
@@ -1,293 +1,298
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 # settings controller for pylons
4 4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; version 2
9 9 # of the License or (at your opinion) any later version of the license.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 19 # MA 02110-1301, USA.
20 20 """
21 21 Created on July 14, 2010
22 22 settings controller for pylons
23 23 @author: marcink
24 24 """
25 25 from formencode import htmlfill
26 26 from pylons import request, session, tmpl_context as c, url, app_globals as g, \
27 27 config
28 28 from pylons.controllers.util import abort, redirect
29 29 from pylons.i18n.translation import _
30 30 from pylons_app.lib import helpers as h
31 31 from pylons_app.lib.auth import LoginRequired, HasPermissionAllDecorator, \
32 32 HasPermissionAnyDecorator
33 33 from pylons_app.lib.base import BaseController, render
34 34 from pylons_app.lib.utils import repo2db_mapper, invalidate_cache, \
35 35 set_hg_app_config, get_hg_settings, get_hg_ui_settings, make_ui
36 36 from pylons_app.model.db import User, UserLog, HgAppSettings, HgAppUi
37 37 from pylons_app.model.forms import UserForm, ApplicationSettingsForm, \
38 38 ApplicationUiSettingsForm
39 39 from pylons_app.model.hg_model import HgModel
40 40 from pylons_app.model.user_model import UserModel
41 41 from pylons_app.lib.celerylib import tasks,run_task
42 42 import formencode
43 43 import logging
44 44 import traceback
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class SettingsController(BaseController):
50 50 """REST Controller styled on the Atom Publishing Protocol"""
51 51 # To properly map this controller, ensure your config/routing.py
52 52 # file has a resource setup:
53 53 # map.resource('setting', 'settings', controller='admin/settings',
54 54 # path_prefix='/admin', name_prefix='admin_')
55 55
56 56
57 57 @LoginRequired()
58 58 def __before__(self):
59 59 c.admin_user = session.get('admin_user')
60 60 c.admin_username = session.get('admin_username')
61 61 super(SettingsController, self).__before__()
62 62
63 63
64 64 @HasPermissionAllDecorator('hg.admin')
65 65 def index(self, format='html'):
66 66 """GET /admin/settings: All items in the collection"""
67 67 # url('admin_settings')
68 68
69 69 defaults = get_hg_settings()
70 70 defaults.update(get_hg_ui_settings())
71 71 return htmlfill.render(
72 72 render('admin/settings/settings.html'),
73 73 defaults=defaults,
74 74 encoding="UTF-8",
75 75 force_defaults=False
76 76 )
77 77
78 78 @HasPermissionAllDecorator('hg.admin')
79 79 def create(self):
80 80 """POST /admin/settings: Create a new item"""
81 81 # url('admin_settings')
82 82
83 83 @HasPermissionAllDecorator('hg.admin')
84 84 def new(self, format='html'):
85 85 """GET /admin/settings/new: Form to create a new item"""
86 86 # url('admin_new_setting')
87 87
88 88 @HasPermissionAllDecorator('hg.admin')
89 89 def update(self, setting_id):
90 90 """PUT /admin/settings/setting_id: Update an existing item"""
91 91 # Forms posted to this method should contain a hidden field:
92 92 # <input type="hidden" name="_method" value="PUT" />
93 93 # Or using helpers:
94 94 # h.form(url('admin_setting', setting_id=ID),
95 95 # method='put')
96 96 # url('admin_setting', setting_id=ID)
97 97 if setting_id == 'mapping':
98 98 rm_obsolete = request.POST.get('destroy', False)
99 99 log.debug('Rescanning directories with destroy=%s', rm_obsolete)
100 100
101 101 initial = HgModel.repo_scan(g.paths[0][0], g.paths[0][1], g.baseui)
102 102 repo2db_mapper(initial, rm_obsolete)
103 103 invalidate_cache('cached_repo_list')
104 104 h.flash(_('Repositories sucessfully rescanned'), category='success')
105 105
106 106 if setting_id == 'whoosh':
107 107 repo_location = get_hg_ui_settings()['paths_root_path']
108 108 full_index = request.POST.get('full_index',False)
109 109 task = run_task(tasks.whoosh_index,repo_location,full_index)
110 110
111 111 h.flash(_('Whoosh reindex task scheduled'), category='success')
112 112 if setting_id == 'global':
113 113
114 114 application_form = ApplicationSettingsForm()()
115 115 try:
116 116 form_result = application_form.to_python(dict(request.POST))
117 117
118 118 try:
119 119 hgsettings1 = self.sa.query(HgAppSettings)\
120 120 .filter(HgAppSettings.app_settings_name == 'title').one()
121 121 hgsettings1.app_settings_value = form_result['hg_app_title']
122 122
123 123 hgsettings2 = self.sa.query(HgAppSettings)\
124 124 .filter(HgAppSettings.app_settings_name == 'realm').one()
125 125 hgsettings2.app_settings_value = form_result['hg_app_realm']
126 126
127 127
128 128 self.sa.add(hgsettings1)
129 129 self.sa.add(hgsettings2)
130 130 self.sa.commit()
131 131 set_hg_app_config(config)
132 132 h.flash(_('Updated application settings'),
133 133 category='success')
134 134
135 135 except:
136 136 log.error(traceback.format_exc())
137 137 h.flash(_('error occured during updating application settings'),
138 138 category='error')
139 139
140 140 self.sa.rollback()
141 141
142 142
143 143 except formencode.Invalid as errors:
144 144 return htmlfill.render(
145 145 render('admin/settings/settings.html'),
146 146 defaults=errors.value,
147 147 errors=errors.error_dict or {},
148 148 prefix_error=False,
149 149 encoding="UTF-8")
150 150
151 151 if setting_id == 'mercurial':
152 152 application_form = ApplicationUiSettingsForm()()
153 153 try:
154 154 form_result = application_form.to_python(dict(request.POST))
155 155
156 156 try:
157 157
158 158 hgsettings1 = self.sa.query(HgAppUi)\
159 159 .filter(HgAppUi.ui_key == 'push_ssl').one()
160 160 hgsettings1.ui_value = form_result['web_push_ssl']
161 161
162 162 hgsettings2 = self.sa.query(HgAppUi)\
163 163 .filter(HgAppUi.ui_key == '/').one()
164 164 hgsettings2.ui_value = form_result['paths_root_path']
165 165
166 166
167 167 #HOOKS
168 168 hgsettings3 = self.sa.query(HgAppUi)\
169 169 .filter(HgAppUi.ui_key == 'changegroup.update').one()
170 170 hgsettings3.ui_active = bool(form_result['hooks_changegroup_update'])
171 171
172 172 hgsettings4 = self.sa.query(HgAppUi)\
173 173 .filter(HgAppUi.ui_key == 'changegroup.repo_size').one()
174 174 hgsettings4.ui_active = bool(form_result['hooks_changegroup_repo_size'])
175 175
176 176
177 177
178 178
179 179 self.sa.add(hgsettings1)
180 180 self.sa.add(hgsettings2)
181 181 self.sa.add(hgsettings3)
182 182 self.sa.add(hgsettings4)
183 183 self.sa.commit()
184 184
185 185 h.flash(_('Updated mercurial settings'),
186 186 category='success')
187 187
188 188 except:
189 189 log.error(traceback.format_exc())
190 190 h.flash(_('error occured during updating application settings'),
191 191 category='error')
192 192
193 193 self.sa.rollback()
194 194
195 195
196 196 except formencode.Invalid as errors:
197 197 return htmlfill.render(
198 198 render('admin/settings/settings.html'),
199 199 defaults=errors.value,
200 200 errors=errors.error_dict or {},
201 201 prefix_error=False,
202 202 encoding="UTF-8")
203 203
204 204
205 205
206 206 return redirect(url('admin_settings'))
207 207
208 208 @HasPermissionAllDecorator('hg.admin')
209 209 def delete(self, setting_id):
210 210 """DELETE /admin/settings/setting_id: Delete an existing item"""
211 211 # Forms posted to this method should contain a hidden field:
212 212 # <input type="hidden" name="_method" value="DELETE" />
213 213 # Or using helpers:
214 214 # h.form(url('admin_setting', setting_id=ID),
215 215 # method='delete')
216 216 # url('admin_setting', setting_id=ID)
217 217
218 218 @HasPermissionAllDecorator('hg.admin')
219 219 def show(self, setting_id, format='html'):
220 220 """GET /admin/settings/setting_id: Show a specific item"""
221 221 # url('admin_setting', setting_id=ID)
222 222
223 223 @HasPermissionAllDecorator('hg.admin')
224 224 def edit(self, setting_id, format='html'):
225 225 """GET /admin/settings/setting_id/edit: Form to edit an existing item"""
226 226 # url('admin_edit_setting', setting_id=ID)
227 227
228 228
229 229 def my_account(self):
230 230 """
231 231 GET /_admin/my_account Displays info about my account
232 232 """
233 233 # url('admin_settings_my_account')
234 234 c.user = self.sa.query(User).get(c.hg_app_user.user_id)
235 235 c.user_repos = []
236 236 for repo in c.cached_repo_list.values():
237 237 if repo.dbrepo.user.username == c.user.username:
238 238 c.user_repos.append(repo)
239 239
240 240 if c.user.username == 'default':
241 241 h.flash(_("You can't edit this user since it's"
242 242 " crucial for entire application"), category='warning')
243 243 return redirect(url('users'))
244 244
245 245 defaults = c.user.__dict__
246 246 return htmlfill.render(
247 247 render('admin/users/user_edit_my_account.html'),
248 248 defaults=defaults,
249 249 encoding="UTF-8",
250 250 force_defaults=False
251 251 )
252 252
253 253 def my_account_update(self):
254 254 """PUT /_admin/my_account_update: Update an existing item"""
255 255 # Forms posted to this method should contain a hidden field:
256 256 # <input type="hidden" name="_method" value="PUT" />
257 257 # Or using helpers:
258 258 # h.form(url('admin_settings_my_account_update'),
259 259 # method='put')
260 260 # url('admin_settings_my_account_update', id=ID)
261 261 user_model = UserModel()
262 262 uid = c.hg_app_user.user_id
263 _form = UserForm(edit=True, old_data={'user_id':uid})()
263 _form = UserForm(edit=True, old_data={'user_id':uid,
264 'email':c.hg_app_user.email})()
264 265 form_result = {}
265 266 try:
266 267 form_result = _form.to_python(dict(request.POST))
267 268 user_model.update_my_account(uid, form_result)
268 269 h.flash(_('Your account was updated succesfully'),
269 270 category='success')
270 271
271 272 except formencode.Invalid as errors:
272 #c.user = self.sa.query(User).get(c.hg_app_user.user_id)
273 c.user = self.sa.query(User).get(c.hg_app_user.user_id)
274 c.user_repos = []
275 for repo in c.cached_repo_list.values():
276 if repo.dbrepo.user.username == c.user.username:
277 c.user_repos.append(repo)
273 278 return htmlfill.render(
274 279 render('admin/users/user_edit_my_account.html'),
275 280 defaults=errors.value,
276 281 errors=errors.error_dict or {},
277 282 prefix_error=False,
278 283 encoding="UTF-8")
279 284 except Exception:
280 285 log.error(traceback.format_exc())
281 286 h.flash(_('error occured during update of user %s') \
282 287 % form_result.get('username'), category='error')
283 288
284 289 return redirect(url('my_account'))
285 290
286 291 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
287 292 def create_repository(self):
288 293 """GET /_admin/create_repository: Form to create a new item"""
289 294 new_repo = request.GET.get('repo', '')
290 295 c.new_repo = h.repo_name_slug(new_repo)
291 296
292 297 return render('admin/repos/repo_add_create_repository.html')
293 298
@@ -1,432 +1,434
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 # Utilities for hg app
4 4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 5 # This program is free software; you can redistribute it and/or
6 6 # modify it under the terms of the GNU General Public License
7 7 # as published by the Free Software Foundation; version 2
8 8 # of the License or (at your opinion) any later version of the license.
9 9 #
10 10 # This program is distributed in the hope that it will be useful,
11 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 13 # GNU General Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License
16 16 # along with this program; if not, write to the Free Software
17 17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 18 # MA 02110-1301, USA.
19 19 import shutil
20 20
21 21 """
22 22 Created on April 18, 2010
23 23 Utilities for hg app
24 24 @author: marcink
25 25 """
26 26 from beaker.cache import cache_region
27 27 from mercurial import ui, config, hg
28 28 from mercurial.error import RepoError
29 29 from pylons_app.model import meta
30 30 from pylons_app.model.db import Repository, User, HgAppUi, HgAppSettings
31 31 from vcs.backends.base import BaseChangeset
32 32 from vcs.utils.lazy import LazyProperty
33 33 import logging
34 34 import os
35 35 from os.path import dirname as dn, join as jn
36 36 import tarfile
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 def get_repo_slug(request):
41 41 return request.environ['pylons.routes_dict'].get('repo_name')
42 42
43 43 def is_mercurial(environ):
44 44 """
45 45 Returns True if request's target is mercurial server - header
46 46 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
47 47 """
48 48 http_accept = environ.get('HTTP_ACCEPT')
49 49 if http_accept and http_accept.startswith('application/mercurial'):
50 50 return True
51 51 return False
52 52
53 53 def check_repo_dir(paths):
54 54 repos_path = paths[0][1].split('/')
55 55 if repos_path[-1] in ['*', '**']:
56 56 repos_path = repos_path[:-1]
57 57 if repos_path[0] != '/':
58 58 repos_path[0] = '/'
59 59 if not os.path.isdir(os.path.join(*repos_path)):
60 60 raise Exception('Not a valid repository in %s' % paths[0][1])
61 61
62 62 def check_repo_fast(repo_name, base_path):
63 63 if os.path.isdir(os.path.join(base_path, repo_name)):return False
64 64 return True
65 65
66 66 def check_repo(repo_name, base_path, verify=True):
67 67
68 68 repo_path = os.path.join(base_path, repo_name)
69 69
70 70 try:
71 71 if not check_repo_fast(repo_name, base_path):
72 72 return False
73 73 r = hg.repository(ui.ui(), repo_path)
74 74 if verify:
75 75 hg.verify(r)
76 76 #here we hnow that repo exists it was verified
77 77 log.info('%s repo is already created', repo_name)
78 78 return False
79 79 except RepoError:
80 80 #it means that there is no valid repo there...
81 81 log.info('%s repo is free for creation', repo_name)
82 82 return True
83 83
84 84 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
85 85 while True:
86 86 ok = raw_input(prompt)
87 87 if ok in ('y', 'ye', 'yes'): return True
88 88 if ok in ('n', 'no', 'nop', 'nope'): return False
89 89 retries = retries - 1
90 90 if retries < 0: raise IOError
91 91 print complaint
92 92
93 93 @cache_region('super_short_term', 'cached_hg_ui')
94 94 def get_hg_ui_cached():
95 95 try:
96 96 sa = meta.Session
97 97 ret = sa.query(HgAppUi).all()
98 98 finally:
99 99 meta.Session.remove()
100 100 return ret
101 101
102 102
103 103 def get_hg_settings():
104 104 try:
105 105 sa = meta.Session
106 106 ret = sa.query(HgAppSettings).all()
107 107 finally:
108 108 meta.Session.remove()
109 109
110 110 if not ret:
111 111 raise Exception('Could not get application settings !')
112 112 settings = {}
113 113 for each in ret:
114 114 settings['hg_app_' + each.app_settings_name] = each.app_settings_value
115 115
116 116 return settings
117 117
118 118 def get_hg_ui_settings():
119 119 try:
120 120 sa = meta.Session
121 121 ret = sa.query(HgAppUi).all()
122 122 finally:
123 123 meta.Session.remove()
124 124
125 125 if not ret:
126 126 raise Exception('Could not get application ui settings !')
127 127 settings = {}
128 128 for each in ret:
129 129 k = each.ui_key
130 130 v = each.ui_value
131 131 if k == '/':
132 132 k = 'root_path'
133 133
134 134 if k.find('.') != -1:
135 135 k = k.replace('.', '_')
136 136
137 137 if each.ui_section == 'hooks':
138 138 v = each.ui_active
139 139
140 140 settings[each.ui_section + '_' + k] = v
141 141
142 142 return settings
143 143
144 144 #propagated from mercurial documentation
145 145 ui_sections = ['alias', 'auth',
146 146 'decode/encode', 'defaults',
147 147 'diff', 'email',
148 148 'extensions', 'format',
149 149 'merge-patterns', 'merge-tools',
150 150 'hooks', 'http_proxy',
151 151 'smtp', 'patch',
152 152 'paths', 'profiling',
153 153 'server', 'trusted',
154 154 'ui', 'web', ]
155 155
156 156 def make_ui(read_from='file', path=None, checkpaths=True):
157 157 """
158 158 A function that will read python rc files or database
159 159 and make an mercurial ui object from read options
160 160
161 161 @param path: path to mercurial config file
162 162 @param checkpaths: check the path
163 163 @param read_from: read from 'file' or 'db'
164 164 """
165 165
166 166 baseui = ui.ui()
167 167
168 168 if read_from == 'file':
169 169 if not os.path.isfile(path):
170 170 log.warning('Unable to read config file %s' % path)
171 171 return False
172 172 log.debug('reading hgrc from %s', path)
173 173 cfg = config.config()
174 174 cfg.read(path)
175 175 for section in ui_sections:
176 176 for k, v in cfg.items(section):
177 177 baseui.setconfig(section, k, v)
178 178 log.debug('settings ui from file[%s]%s:%s', section, k, v)
179 179 if checkpaths:check_repo_dir(cfg.items('paths'))
180 180
181 181
182 182 elif read_from == 'db':
183 183 hg_ui = get_hg_ui_cached()
184 184 for ui_ in hg_ui:
185 185 if ui_.ui_active:
186 186 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section, ui_.ui_key, ui_.ui_value)
187 187 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
188 188
189 189
190 190 return baseui
191 191
192 192
193 193 def set_hg_app_config(config):
194 194 hgsettings = get_hg_settings()
195 195
196 196 for k, v in hgsettings.items():
197 197 config[k] = v
198 198
199 199 def invalidate_cache(name, *args):
200 200 """Invalidates given name cache"""
201 201
202 202 from beaker.cache import region_invalidate
203 203 log.info('INVALIDATING CACHE FOR %s', name)
204 204
205 205 """propagate our arguments to make sure invalidation works. First
206 206 argument has to be the name of cached func name give to cache decorator
207 207 without that the invalidation would not work"""
208 208 tmp = [name]
209 209 tmp.extend(args)
210 210 args = tuple(tmp)
211 211
212 212 if name == 'cached_repo_list':
213 213 from pylons_app.model.hg_model import _get_repos_cached
214 214 region_invalidate(_get_repos_cached, None, *args)
215 215
216 216 if name == 'full_changelog':
217 217 from pylons_app.model.hg_model import _full_changelog_cached
218 218 region_invalidate(_full_changelog_cached, None, *args)
219 219
220 220 class EmptyChangeset(BaseChangeset):
221 221
222 222 revision = -1
223 223 message = ''
224 author = ''
224 225
225 226 @LazyProperty
226 227 def raw_id(self):
227 228 """
228 229 Returns raw string identifing this changeset, useful for web
229 230 representation.
230 231 """
231 232 return '0' * 12
232 233
233 234
234 235 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
235 236 """
236 237 maps all found repositories into db
237 238 """
238 239 from pylons_app.model.repo_model import RepoModel
239 240
240 241 sa = meta.Session
241 242 user = sa.query(User).filter(User.admin == True).first()
242 243
243 244 rm = RepoModel()
244 245
245 246 for name, repo in initial_repo_list.items():
246 247 if not sa.query(Repository).filter(Repository.repo_name == name).scalar():
247 248 log.info('repository %s not found creating default', name)
248 249
249 250 form_data = {
250 251 'repo_name':name,
251 252 'description':repo.description if repo.description != 'unknown' else \
252 253 'auto description for %s' % name,
253 254 'private':False
254 255 }
255 256 rm.create(form_data, user, just_db=True)
256 257
257 258
258 259 if remove_obsolete:
259 260 #remove from database those repositories that are not in the filesystem
260 261 for repo in sa.query(Repository).all():
261 262 if repo.repo_name not in initial_repo_list.keys():
262 263 sa.delete(repo)
263 264 sa.commit()
264 265
265 266
266 267 meta.Session.remove()
267 268
268 269 from UserDict import DictMixin
269 270
270 271 class OrderedDict(dict, DictMixin):
271 272
272 273 def __init__(self, *args, **kwds):
273 274 if len(args) > 1:
274 275 raise TypeError('expected at most 1 arguments, got %d' % len(args))
275 276 try:
276 277 self.__end
277 278 except AttributeError:
278 279 self.clear()
279 280 self.update(*args, **kwds)
280 281
281 282 def clear(self):
282 283 self.__end = end = []
283 284 end += [None, end, end] # sentinel node for doubly linked list
284 285 self.__map = {} # key --> [key, prev, next]
285 286 dict.clear(self)
286 287
287 288 def __setitem__(self, key, value):
288 289 if key not in self:
289 290 end = self.__end
290 291 curr = end[1]
291 292 curr[2] = end[1] = self.__map[key] = [key, curr, end]
292 293 dict.__setitem__(self, key, value)
293 294
294 295 def __delitem__(self, key):
295 296 dict.__delitem__(self, key)
296 297 key, prev, next = self.__map.pop(key)
297 298 prev[2] = next
298 299 next[1] = prev
299 300
300 301 def __iter__(self):
301 302 end = self.__end
302 303 curr = end[2]
303 304 while curr is not end:
304 305 yield curr[0]
305 306 curr = curr[2]
306 307
307 308 def __reversed__(self):
308 309 end = self.__end
309 310 curr = end[1]
310 311 while curr is not end:
311 312 yield curr[0]
312 313 curr = curr[1]
313 314
314 315 def popitem(self, last=True):
315 316 if not self:
316 317 raise KeyError('dictionary is empty')
317 318 if last:
318 319 key = reversed(self).next()
319 320 else:
320 321 key = iter(self).next()
321 322 value = self.pop(key)
322 323 return key, value
323 324
324 325 def __reduce__(self):
325 326 items = [[k, self[k]] for k in self]
326 327 tmp = self.__map, self.__end
327 328 del self.__map, self.__end
328 329 inst_dict = vars(self).copy()
329 330 self.__map, self.__end = tmp
330 331 if inst_dict:
331 332 return (self.__class__, (items,), inst_dict)
332 333 return self.__class__, (items,)
333 334
334 335 def keys(self):
335 336 return list(self)
336 337
337 338 setdefault = DictMixin.setdefault
338 339 update = DictMixin.update
339 340 pop = DictMixin.pop
340 341 values = DictMixin.values
341 342 items = DictMixin.items
342 343 iterkeys = DictMixin.iterkeys
343 344 itervalues = DictMixin.itervalues
344 345 iteritems = DictMixin.iteritems
345 346
346 347 def __repr__(self):
347 348 if not self:
348 349 return '%s()' % (self.__class__.__name__,)
349 350 return '%s(%r)' % (self.__class__.__name__, self.items())
350 351
351 352 def copy(self):
352 353 return self.__class__(self)
353 354
354 355 @classmethod
355 356 def fromkeys(cls, iterable, value=None):
356 357 d = cls()
357 358 for key in iterable:
358 359 d[key] = value
359 360 return d
360 361
361 362 def __eq__(self, other):
362 363 if isinstance(other, OrderedDict):
363 364 return len(self) == len(other) and self.items() == other.items()
364 365 return dict.__eq__(self, other)
365 366
366 367 def __ne__(self, other):
367 368 return not self == other
368 369
369 370 def make_test_env():
370 371 """Makes a fresh database from base64+zlib dump and
371 372 install test repository into tmp dir
372 373 """
373 374 new_db_dump = """
374 375 eJztXN1vE8sVn9nxR+wAIXDpFiiXjSAXfEOc2ElwQkVLPjYf5NNOAklUydrYG3tv1t5ldx0nuUJV\noL
375 376 cPrVr1X7jSfUJ96nMfK1Xty23VqlWlPlRIlahUXbXqFUL0pTNjx5614xAoKEDmJ3t2zpkzM2fO\neHe+
376 377 zno+PqU5qrRmWDnFkXqAB0AIbkkSAKANf8+BKprwFzI0G28ECXQ+PufFEYT+Tehz6L/oaSnK\nwcFxGP
377 378 igFQfHjuMg4CehH7UA9Af0Y2ShWdSPLmOSg+N9x7U9eKf9PiC2nIWm4mTtri4nZ3Z9DE/5\nfOD0+RZY
378 379 VFdXFVstWHoXPOPFvDbKU3TdKCbNgp39GLZ5MPtKW5WtWKmstqFmtqVtzZRWt6NQRFjk\ngkhESJ6kbe
379 380 trim6rcFTAdcfuwqxhrNuprJLPqBnLKJhhSzWNpK1tq+aWkzXyN8wt3cjbScU0w7q2\nGqbyVSHYAXE5
380 381 kSv15RTMtOKo2YxUikjf+SgKg4Dc/38C6Dn6Gn2FnqDH6K+Y5ODgeGfhRRD6/ST0\n+Ujo9ZLQ4yEhQi
381 382 QUBBJCeFy4BLywHaCfCEXM+AJHOWpx39sMrux4IbzQ3gMc1XaSlpop6IoVvRxV\nLke6L4/cmx7vjedG
382 383 4qmVmXvTW5nl7PDaSmFEXR6ejC+YVrpnsNi1fn17fHldj06p6YH84tzaGKBF\n5ZWcSq66Uorn8Iih5W
383 384 /ZBolqejhl5O57mkEPqf6sOFCq3lRsu2hYaayHrTplJeJD/Uu3p7u3Er19\nS4sb26PmemQiE54vLKfn
384 385 I8Wx2/Nd+XurmbH4TOpupHdk25I/sYbmCgDQstK0oHLdpWGmc1U3MqR6\nbICF123RHb/QDNpIm1rFnk
385 386 HaJiWd0/Llpgzq41lzIJMrjMXi2/JmdyGxMDKnjs1FR9WMcduMb3TZ\nfZuZTXVs1uiS53NxY9yan4Vw
386 387 PDNICqEl3dKNlKJnDdshbYh2R7o7uwc6I1EpGr3RHbvREwn3D/T3\nd/fuBFAzaHdpUu7csi6Tw4ou94
387 388 zOLt3JxTNZo7g8muvV1Lg6sNj/SX4dD7srqenpfCJ6d3g5vKRM\njq/Ob3VHIXgJXaKx8PWBvoHrvfdg
388 389 MzhPVDl/vgek1TWloO927tbUdsqeNzfurK5Frq+v5NbHZ1bG\nCnZxdnxxbGStmOudnwub6+rQYNxZku
389 390 Wh28Ph9Nos2C3EfblVvhJlyPjvRY+Z8f91dzUHB8fhYf/x\nv3T/PwL47v87+iX6I45ycHC8dWhFV6Br
390 391 7ukVUQ/cYzroOYnaXZLoBGqD1TmW0IzOw/IUAJL9v6Dg\nA+jP6Ofo+yiBelFA+IvwC2EFMzmOCBJBD/
391 392 huMZsJ41+MZjuqFVYKjpFUUo62oThqosyV8mpRKtg4\nUtScrJTNdCqmSeNGwZFIFqmcRTPydwIeMPwp
392 393 W2ZOyRcU/SVLLWViym1v8oDOLrbcvJGvFpbWbGVV\nV9NhvweEZCyWslRcWVnINGzNMawtiXJxaRX5kM
393 394 8D+rqq8lZFtjaX+i2vB1zoxKL0dhrPSHSmj6u3\nFCzV4cH6fbuavSTFFEJp3KCUatsdqEa4aGkOqyel
394 395 y8IhwQM6BhhhrE2akSVkWfQKxKJ9jGhN8/NG\nWZCM/0H0q5r9P/Q79FvM5ODgeOtBZvLBIAkDARI2Nb
395 396 3E/h/O7wdDAAzBj+Cy8IXwpfAc/eZlat9R\noF+8eBE+bHXIgzSbIQcTyYJWiQjDCXlwQZYWBoemZKnC
396 397 lq4GAwUtqaWliZkFeUxOSDOzC9LM4tTU\nNYmm2GqKPqEX5KWFMmtd3WLJDUUvqCyDjhKqNDQ7OyUPzh
397 398 DmXGJiejCxLE3Ky9JVWl2IsBdnJuKL\nMssZHpeHJymjXMjEjHS1+5oUCYWCoYjgE+WLEGj5tLpp39Px
398 399 MzlJhjtKJytNSkYqUfRgHPlFUYQ/\nMKhZyPhm08DjMgdlUVPgSENj4DSyN1hp6u6Er8Kob3hplGEYrg
399 400 J2dxsrDLrZ6EpO6kYGlzCCdV2Y\nmJbrjVlS2G1Ohlc2aJ012TSqozuJLYpoiK0f8vjEm2Ij61MLJiP0
400 401 4g15XywapRffzpTPL166BB8k\naQeZqpXTbBv/4Gwm6nd1FpNAuqxKNuo4RsLdf1W+buQzrjSXkV1VuO
401 402 zjTgmG+vw+ceJSo5Yzmicj\nDNFE7n8BfQnQ33DAwcHxLqMFLxHEs47mkIGYrKM+xAsBMYZXBnquvLDC
402 403 D4Wsmne0FF3/kPm/gL6m\n8//DVp6Dg+PNo3b+7wOPAHgEH8F/CFfRT9GvD1u/vbFzv8kvdnTAhxF2nW
403 404 GrjqPlM3YNGdxrzbGb\nSOZuLN1o9uaScc3RXCnuVYhr+lZTi2sCd+C08iz4ZsAnxjtesAapZIrUMJpv
404 405 Bl8me7SGcfxBqtkv\ntrfDzwLU+pWdJU212fgJl93ZFGJ06qPWwNg0rWLkuuVPwxm2RfcS2YVOWrVTlm
405 406 a61o6uXimr4bJ4\npfp67r6So7MJeWJshhRcWf1ICXlUTsgzw/L87vpuj4XRrubsOjN2zCdOtjfqJNac
406 407 yQhLtcSOHzhj\nlKVOlsb/fwL0FAccHBzvLQJIhHRpIJAYXRPQ8R+i3wP84eDgeNfRCX3gAoRjGyk7Sc
407 408 78BUDPZdlJ\n0ZphSbvJZPyH6D8Afzg4ON5/HEMX4O7tD0v3/3OAPxwcHEcG1f0/hJ4A9Az9C184ODje
408 409 Q/gQ+WcP\nKPgEevX5IL0GyPiP0Fdl/7/D1pKDg+PNYe/3f+j4/wSP/88OWz8ODo43Ab+H3O0CKl19Qu
409 410 kaoPN/\nD/gcgM+FD4W7ws8OW886PNg+UTp4jlX8aJOOQR0a2XhrnVftbkrFubZM7+dkewA/zgYS9a6x
410 411 1erq\nXWRr0thDZLdfJ3uU7PI+rXcMfYWT6Bq33WtSrVNprGW/Y2VXUyIsdSp28sAZoyx1+kGulXqTfx
411 412 aq\ndrduZOxK5Ex9RxN2pZcx8So9XEozKw4D1Vdn6v0RFLdfeolM0r/U2d9buqRbvekZ/iv0IpulqrYr
412 413 \nl9sRo+rBEAyR+x8/ADg4OI4gyPyf3/8cHEcTJf+fpwB/ODg4jgSaoBfQ/QB+/s/BcSRR3f+H6Bng\n
413 414 e/8cHEcHpf1/CI+jHwEP3AToLtx8e9/9e//w8Hun6bHGDz+tvE+3uwfOxsW69+nYYw2WfjPHGtX9\n5A
414 415 MdfNQo9P+eS7youNdyVuJq4ot2zRsdnLgLCYYip/b7w5jKqUX51IREv4F/FJ7YBy96ja963sJS\n34yd
415 416 OXDGKEud/R8efZUt\n
416 417 """
417 418 newdb = open('test.db','wb')
418 419 newdb.write(new_db_dump.decode('base64').decode('zlib'))
419 420 newdb.close()
420 421
421 422
422 423 #PART TWO make test repo
423 424 if os.path.isdir('/tmp/vcs_test'):
424 425 shutil.rmtree('/tmp/vcs_test')
425 426
426 427 cur_dir = dn(dn(os.path.abspath(__file__)))
427 428 tar = tarfile.open(jn(cur_dir,'tests',"vcs_test.tar.gz"))
428 429 tar.extractall('/tmp')
429 430 tar.close()
430 431
431 432
433
432 434 No newline at end of file
@@ -1,347 +1,351
1 1 """ this is forms validation classes
2 2 http://formencode.org/module-formencode.validators.html
3 3 for list off all availible validators
4 4
5 5 we can create our own validators
6 6
7 7 The table below outlines the options which can be used in a schema in addition to the validators themselves
8 8 pre_validators [] These validators will be applied before the schema
9 9 chained_validators [] These validators will be applied after the schema
10 10 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
11 11 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
12 12 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
13 13 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
14 14
15 15
16 16 <name> = formencode.validators.<name of validator>
17 17 <name> must equal form name
18 18 list=[1,2,3,4,5]
19 19 for SELECT use formencode.All(OneOf(list), Int())
20 20
21 21 """
22 22 from formencode import All
23 23 from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \
24 24 Email, Bool, StringBoolean
25 25 from pylons import session
26 26 from pylons.i18n.translation import _
27 27 from pylons_app.lib.auth import check_password, get_crypt_password
28 28 from pylons_app.model import meta
29 29 from pylons_app.model.user_model import UserModel
30 30 from pylons_app.model.db import User, Repository
31 31 from sqlalchemy.exc import OperationalError
32 32 from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
33 33 from webhelpers.pylonslib.secure_form import authentication_token
34 34 import formencode
35 35 import logging
36 36 import os
37 37 import pylons_app.lib.helpers as h
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 #this is needed to translate the messages using _() in validators
42 42 class State_obj(object):
43 43 _ = staticmethod(_)
44 44
45 45 #===============================================================================
46 46 # VALIDATORS
47 47 #===============================================================================
48 48 class ValidAuthToken(formencode.validators.FancyValidator):
49 49 messages = {'invalid_token':_('Token mismatch')}
50 50
51 51 def validate_python(self, value, state):
52 52
53 53 if value != authentication_token():
54 54 raise formencode.Invalid(self.message('invalid_token', state,
55 55 search_number=value), value, state)
56 56
57 57 def ValidUsername(edit, old_data):
58 58 class _ValidUsername(formencode.validators.FancyValidator):
59 59
60 60 def validate_python(self, value, state):
61 61 if value in ['default', 'new_user']:
62 62 raise formencode.Invalid(_('Invalid username'), value, state)
63 63 #check if user is uniq
64 64 sa = meta.Session
65 65 old_un = None
66 66 if edit:
67 67 old_un = sa.query(User).get(old_data.get('user_id')).username
68 68
69 69 if old_un != value or not edit:
70 70 if sa.query(User).filter(User.username == value).scalar():
71 71 raise formencode.Invalid(_('This username already exists') ,
72 72 value, state)
73 73 meta.Session.remove()
74 74
75 75 return _ValidUsername
76 76
77 77 class ValidPassword(formencode.validators.FancyValidator):
78 78
79 79 def to_python(self, value, state):
80 80 if value:
81 81 return get_crypt_password(value)
82 82
83 83 class ValidAuth(formencode.validators.FancyValidator):
84 84 messages = {
85 85 'invalid_password':_('invalid password'),
86 86 'invalid_login':_('invalid user name'),
87 87 'disabled_account':_('Your acccount is disabled')
88 88
89 89 }
90 90 #error mapping
91 91 e_dict = {'username':messages['invalid_login'],
92 92 'password':messages['invalid_password']}
93 93 e_dict_disable = {'username':messages['disabled_account']}
94 94
95 95 def validate_python(self, value, state):
96 96 password = value['password']
97 97 username = value['username']
98 98 user = UserModel().get_user_by_name(username)
99 99 if user is None:
100 100 raise formencode.Invalid(self.message('invalid_password',
101 101 state=State_obj), value, state,
102 102 error_dict=self.e_dict)
103 103 if user:
104 104 if user.active:
105 105 if user.username == username and check_password(password,
106 106 user.password):
107 107 return value
108 108 else:
109 109 log.warning('user %s not authenticated', username)
110 110 raise formencode.Invalid(self.message('invalid_password',
111 111 state=State_obj), value, state,
112 112 error_dict=self.e_dict)
113 113 else:
114 114 log.warning('user %s is disabled', username)
115 115 raise formencode.Invalid(self.message('disabled_account',
116 116 state=State_obj),
117 117 value, state,
118 118 error_dict=self.e_dict_disable)
119 119
120 120 class ValidRepoUser(formencode.validators.FancyValidator):
121 121
122 122 def to_python(self, value, state):
123 123 try:
124 124 self.user_db = meta.Session.query(User)\
125 125 .filter(User.active == True)\
126 126 .filter(User.username == value).one()
127 127 except Exception:
128 128 raise formencode.Invalid(_('This username is not valid'),
129 129 value, state)
130 130 finally:
131 131 meta.Session.remove()
132 132
133 133 return self.user_db.user_id
134 134
135 135 def ValidRepoName(edit, old_data):
136 136 class _ValidRepoName(formencode.validators.FancyValidator):
137 137
138 138 def to_python(self, value, state):
139 139 slug = h.repo_name_slug(value)
140 140 if slug in ['_admin']:
141 141 raise formencode.Invalid(_('This repository name is disallowed'),
142 142 value, state)
143 143 if old_data.get('repo_name') != value or not edit:
144 144 sa = meta.Session
145 145 if sa.query(Repository).filter(Repository.repo_name == slug).scalar():
146 146 raise formencode.Invalid(_('This repository already exists') ,
147 147 value, state)
148 148 meta.Session.remove()
149 149 return slug
150 150
151 151
152 152 return _ValidRepoName
153 153
154 154 class ValidPerms(formencode.validators.FancyValidator):
155 155 messages = {'perm_new_user_name':_('This username is not valid')}
156 156
157 157 def to_python(self, value, state):
158 158 perms_update = []
159 159 perms_new = []
160 160 #build a list of permission to update and new permission to create
161 161 for k, v in value.items():
162 162 if k.startswith('perm_'):
163 163 if k.startswith('perm_new_user'):
164 164 new_perm = value.get('perm_new_user', False)
165 165 new_user = value.get('perm_new_user_name', False)
166 166 if new_user and new_perm:
167 167 if (new_user, new_perm) not in perms_new:
168 168 perms_new.append((new_user, new_perm))
169 169 else:
170 170 usr = k[5:]
171 171 if usr == 'default':
172 172 if value['private']:
173 173 #set none for default when updating to private repo
174 174 v = 'repository.none'
175 175 perms_update.append((usr, v))
176 176 value['perms_updates'] = perms_update
177 177 value['perms_new'] = perms_new
178 178 sa = meta.Session
179 179 for k, v in perms_new:
180 180 try:
181 181 self.user_db = sa.query(User)\
182 182 .filter(User.active == True)\
183 183 .filter(User.username == k).one()
184 184 except Exception:
185 185 msg = self.message('perm_new_user_name',
186 186 state=State_obj)
187 187 raise formencode.Invalid(msg, value, state, error_dict={'perm_new_user_name':msg})
188 188 return value
189 189
190 190 class ValidSettings(formencode.validators.FancyValidator):
191 191
192 192 def to_python(self, value, state):
193 193 #settings form can't edit user
194 194 if value.has_key('user'):
195 195 del['value']['user']
196 196
197 197 return value
198 198
199 199 class ValidPath(formencode.validators.FancyValidator):
200 200 def to_python(self, value, state):
201 201 isdir = os.path.isdir(value.replace('*', ''))
202 202 if (value.endswith('/*') or value.endswith('/**')) and isdir:
203 203 return value
204 204 elif not isdir:
205 205 msg = _('This is not a valid path')
206 206 else:
207 207 msg = _('You need to specify * or ** at the end of path (ie. /tmp/*)')
208 208
209 209 raise formencode.Invalid(msg, value, state,
210 210 error_dict={'paths_root_path':msg})
211 211
212 class UniqSystemEmail(formencode.validators.FancyValidator):
212 def UniqSystemEmail(old_data):
213 class _UniqSystemEmail(formencode.validators.FancyValidator):
213 214 def to_python(self, value, state):
215 if old_data['email'] != value:
214 216 sa = meta.Session
215 217 try:
216 218 user = sa.query(User).filter(User.email == value).scalar()
217 219 if user:
218 220 raise formencode.Invalid(_("That e-mail address is already taken") ,
219 221 value, state)
220 222 finally:
221 223 meta.Session.remove()
222 224
223 225 return value
224 226
227 return _UniqSystemEmail
228
225 229 class ValidSystemEmail(formencode.validators.FancyValidator):
226 230 def to_python(self, value, state):
227 231 sa = meta.Session
228 232 try:
229 233 user = sa.query(User).filter(User.email == value).scalar()
230 234 if user is None:
231 235 raise formencode.Invalid(_("That e-mail address doesn't exist.") ,
232 236 value, state)
233 237 finally:
234 238 meta.Session.remove()
235 239
236 240 return value
237 241
238 242 #===============================================================================
239 243 # FORMS
240 244 #===============================================================================
241 245 class LoginForm(formencode.Schema):
242 246 allow_extra_fields = True
243 247 filter_extra_fields = True
244 248 username = UnicodeString(
245 249 strip=True,
246 250 min=3,
247 251 not_empty=True,
248 252 messages={
249 253 'empty':_('Please enter a login'),
250 254 'tooShort':_('Enter a value %(min)i characters long or more')}
251 255 )
252 256
253 257 password = UnicodeString(
254 258 strip=True,
255 259 min=3,
256 260 not_empty=True,
257 261 messages={
258 262 'empty':_('Please enter a password'),
259 263 'tooShort':_('Enter a value %(min)i characters long or more')}
260 264 )
261 265
262 266
263 267 #chained validators have access to all data
264 268 chained_validators = [ValidAuth]
265 269
266 270 def UserForm(edit=False, old_data={}):
267 271 class _UserForm(formencode.Schema):
268 272 allow_extra_fields = True
269 273 filter_extra_fields = True
270 274 username = All(UnicodeString(strip=True, min=3, not_empty=True), ValidUsername(edit, old_data))
271 275 if edit:
272 276 new_password = All(UnicodeString(strip=True, min=3, not_empty=False), ValidPassword)
273 277 admin = StringBoolean(if_missing=False)
274 278 else:
275 279 password = All(UnicodeString(strip=True, min=8, not_empty=True), ValidPassword)
276 280 active = StringBoolean(if_missing=False)
277 281 name = UnicodeString(strip=True, min=3, not_empty=True)
278 282 lastname = UnicodeString(strip=True, min=3, not_empty=True)
279 email = All(Email(not_empty=True), UniqSystemEmail())
283 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
280 284
281 285 return _UserForm
282 286
283 287 RegisterForm = UserForm
284 288
285 289 def PasswordResetForm():
286 290 class _PasswordResetForm(formencode.Schema):
287 291 allow_extra_fields = True
288 292 filter_extra_fields = True
289 293 email = All(ValidSystemEmail(), Email(not_empty=True))
290 294 return _PasswordResetForm
291 295
292 296 def RepoForm(edit=False, old_data={}):
293 297 class _RepoForm(formencode.Schema):
294 298 allow_extra_fields = True
295 299 filter_extra_fields = False
296 300 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data))
297 301 description = UnicodeString(strip=True, min=3, not_empty=True)
298 302 private = StringBoolean(if_missing=False)
299 303
300 304 if edit:
301 305 user = All(Int(not_empty=True), ValidRepoUser)
302 306
303 307 chained_validators = [ValidPerms]
304 308 return _RepoForm
305 309
306 310 def RepoSettingsForm(edit=False, old_data={}):
307 311 class _RepoForm(formencode.Schema):
308 312 allow_extra_fields = True
309 313 filter_extra_fields = False
310 314 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data))
311 315 description = UnicodeString(strip=True, min=3, not_empty=True)
312 316 private = StringBoolean(if_missing=False)
313 317
314 318 chained_validators = [ValidPerms, ValidSettings]
315 319 return _RepoForm
316 320
317 321
318 322 def ApplicationSettingsForm():
319 323 class _ApplicationSettingsForm(formencode.Schema):
320 324 allow_extra_fields = True
321 325 filter_extra_fields = False
322 326 hg_app_title = UnicodeString(strip=True, min=3, not_empty=True)
323 327 hg_app_realm = UnicodeString(strip=True, min=3, not_empty=True)
324 328
325 329 return _ApplicationSettingsForm
326 330
327 331 def ApplicationUiSettingsForm():
328 332 class _ApplicationUiSettingsForm(formencode.Schema):
329 333 allow_extra_fields = True
330 334 filter_extra_fields = False
331 335 web_push_ssl = OneOf(['true', 'false'], if_missing='false')
332 336 paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=3, not_empty=True))
333 337 hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
334 338 hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False)
335 339
336 340 return _ApplicationUiSettingsForm
337 341
338 342 def DefaultPermissionsForm(perms_choices, register_choices, create_choices):
339 343 class _DefaultPermissionsForm(formencode.Schema):
340 344 allow_extra_fields = True
341 345 filter_extra_fields = True
342 346 overwrite_default = OneOf(['true', 'false'], if_missing='false')
343 347 default_perm = OneOf(perms_choices)
344 348 default_register = OneOf(register_choices)
345 349 default_create = OneOf(create_choices)
346 350
347 351 return _DefaultPermissionsForm
@@ -1,508 +1,508
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${_('Mercurial Repository Overview')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${h.link_to(u'Home',h.url('/'))}
9 9 &raquo;
10 10 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
11 11 &raquo;
12 12 ${_('summary')}
13 13 </%def>
14 14
15 15 <%def name="page_nav()">
16 16 ${self.menu('summary')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <script type="text/javascript">
21 21 var E = YAHOO.util.Event;
22 22 var D = YAHOO.util.Dom;
23 23
24 24 E.onDOMReady(function(e){
25 25 id = 'clone_url';
26 26 E.addListener(id,'click',function(e){
27 27 D.get('clone_url').select();
28 28 })
29 29 })
30 30 </script>
31 31 <div class="box box-left">
32 32 <!-- box / title -->
33 33 <div class="title">
34 34 ${self.breadcrumbs()}
35 35 </div>
36 36 <!-- end box / title -->
37 37 <div class="form">
38 38 <div class="fields">
39 39
40 40 <div class="field">
41 41 <div class="label">
42 42 <label>${_('Name')}:</label>
43 43 </div>
44 44 <div class="input-short">
45 45 <span style="font-size: 1.6em;font-weight: bold">${c.repo_info.name}</span>
46 46 </div>
47 47 </div>
48 48
49 49
50 50 <div class="field">
51 51 <div class="label">
52 52 <label>${_('Description')}:</label>
53 53 </div>
54 54 <div class="input-short">
55 55 ${c.repo_info.description}
56 56 </div>
57 57 </div>
58 58
59 59
60 60 <div class="field">
61 61 <div class="label">
62 62 <label>${_('Contact')}:</label>
63 63 </div>
64 64 <div class="input-short">
65 65 <div class="gravatar">
66 66 <img alt="gravatar" src="${h.gravatar_url(c.repo_info.dbrepo.user.email)}"/>
67 67 </div>
68 68 ${_('Username')}: ${c.repo_info.dbrepo.user.username}<br/>
69 69 ${_('Name')}: ${c.repo_info.dbrepo.user.name} ${c.repo_info.dbrepo.user.lastname}<br/>
70 70 ${_('Email')}: <a href="mailto:${c.repo_info.dbrepo.user.email}">${c.repo_info.dbrepo.user.email}</a>
71 71 </div>
72 72 </div>
73 73
74 74 <div class="field">
75 75 <div class="label">
76 76 <label>${_('Last change')}:</label>
77 77 </div>
78 78 <div class="input-short">
79 79 ${h.age(c.repo_info.last_change)} - ${h.rfc822date(c.repo_info.last_change)}
80 ${_('by')} ${c.repo_info.get_changeset('tip').author}
80 ${_('by')} ${h.get_changeset_safe(c.repo_info,'tip').author}
81 81
82 82 </div>
83 83 </div>
84 84
85 85 <div class="field">
86 86 <div class="label">
87 87 <label>${_('Clone url')}:</label>
88 88 </div>
89 89 <div class="input-short">
90 90 <input type="text" id="clone_url" readonly="readonly" value="hg clone ${c.clone_repo_url}" size="70"/>
91 91 </div>
92 92 </div>
93 93
94 94 <div class="field">
95 95 <div class="label">
96 96 <label>${_('Download')}:</label>
97 97 </div>
98 98 <div class="input-short">
99 99 %for cnt,archive in enumerate(c.repo_info._get_archives()):
100 100 %if cnt >=1:
101 101 |
102 102 %endif
103 103 ${h.link_to(c.repo_info.name+'.'+archive['type'],
104 104 h.url('files_archive_home',repo_name=c.repo_info.name,
105 105 revision='tip',fileformat=archive['extension']),class_="archive_icon")}
106 106 %endfor
107 107 </div>
108 108 </div>
109 109
110 110 <div class="field">
111 111 <div class="label">
112 112 <label>${_('Feeds')}:</label>
113 113 </div>
114 114 <div class="input-short">
115 115 ${h.link_to(_('RSS'),h.url('rss_feed_home',repo_name=c.repo_info.name),class_='rss_icon')}
116 116 ${h.link_to(_('Atom'),h.url('atom_feed_home',repo_name=c.repo_info.name),class_='atom_icon')}
117 117 </div>
118 118 </div>
119 119 </div>
120 120 </div>
121 121 </div>
122 122
123 123 <div class="box box-right" style="min-height:455px">
124 124 <!-- box / title -->
125 125 <div class="title">
126 126 <h5>${_('Commit activity')}</h5>
127 127 </div>
128 128
129 129 <div class="table">
130 130 <div id="commit_history" style="width:560px;height:300px;float:left"></div>
131 131 <div style="clear: both;height: 10px"></div>
132 132 <div id="overview" style="width:560px;height:100px;float:left"></div>
133 133
134 134 <div id="legend_data" style="clear:both;margin-top:10px;">
135 135 <div id="legend_container"></div>
136 136 <div id="legend_choices">
137 137 <table id="legend_choices_tables" style="font-size:smaller;color:#545454"></table>
138 138 </div>
139 139 </div>
140 140 <script type="text/javascript">
141 141 /**
142 142 * Plots summary graph
143 143 *
144 144 * @class SummaryPlot
145 145 * @param {from} initial from for detailed graph
146 146 * @param {to} initial to for detailed graph
147 147 * @param {dataset}
148 148 * @param {overview_dataset}
149 149 */
150 150 function SummaryPlot(from,to,dataset,overview_dataset) {
151 151 var initial_ranges = {
152 152 "xaxis":{
153 153 "from":from,
154 154 "to":to,
155 155 },
156 156 };
157 157 var dataset = dataset;
158 158 var overview_dataset = [overview_dataset];
159 159 var choiceContainer = YAHOO.util.Dom.get("legend_choices");
160 160 var choiceContainerTable = YAHOO.util.Dom.get("legend_choices_tables");
161 161 var plotContainer = YAHOO.util.Dom.get('commit_history');
162 162 var overviewContainer = YAHOO.util.Dom.get('overview');
163 163
164 164 var plot_options = {
165 165 bars: {show:true,align:'center',lineWidth:4},
166 166 legend: {show:true, container:"legend_container"},
167 167 points: {show:true,radius:0,fill:false},
168 168 yaxis: {tickDecimals:0,},
169 169 xaxis: {
170 170 mode: "time",
171 171 timeformat: "%d/%m",
172 172 min:from,
173 173 max:to,
174 174 },
175 175 grid: {
176 176 hoverable: true,
177 177 clickable: true,
178 178 autoHighlight:true,
179 179 color: "#999"
180 180 },
181 181 //selection: {mode: "x"}
182 182 };
183 183 var overview_options = {
184 184 legend:{show:false},
185 185 bars: {show:true,barWidth: 2,},
186 186 shadowSize: 0,
187 187 xaxis: {mode: "time", timeformat: "%d/%m/%y",},
188 188 yaxis: {ticks: 3, min: 0,},
189 189 grid: {color: "#999",},
190 190 selection: {mode: "x"}
191 191 };
192 192
193 193 /**
194 194 *get dummy data needed in few places
195 195 */
196 196 function getDummyData(label){
197 197 return {"label":label,
198 198 "data":[{"time":0,
199 199 "commits":0,
200 200 "added":0,
201 201 "changed":0,
202 202 "removed":0,
203 203 }],
204 204 "schema":["commits"],
205 205 "color":'#ffffff',
206 206 }
207 207 }
208 208
209 209 /**
210 210 * generate checkboxes accordindly to data
211 211 * @param keys
212 212 * @returns
213 213 */
214 214 function generateCheckboxes(data) {
215 215 //append checkboxes
216 216 var i = 0;
217 217 choiceContainerTable.innerHTML = '';
218 218 for(var pos in data) {
219 219
220 220 data[pos].color = i;
221 221 i++;
222 222 if(data[pos].label != ''){
223 223 choiceContainerTable.innerHTML += '<tr><td>'+
224 224 '<input type="checkbox" name="' + data[pos].label +'" checked="checked" />'
225 225 +data[pos].label+
226 226 '</td></tr>';
227 227 }
228 228 }
229 229 }
230 230
231 231 /**
232 232 * ToolTip show
233 233 */
234 234 function showTooltip(x, y, contents) {
235 235 var div=document.getElementById('tooltip');
236 236 if(!div) {
237 237 div = document.createElement('div');
238 238 div.id="tooltip";
239 239 div.style.position="absolute";
240 240 div.style.border='1px solid #fdd';
241 241 div.style.padding='2px';
242 242 div.style.backgroundColor='#fee';
243 243 document.body.appendChild(div);
244 244 }
245 245 YAHOO.util.Dom.setStyle(div, 'opacity', 0);
246 246 div.innerHTML = contents;
247 247 div.style.top=(y + 5) + "px";
248 248 div.style.left=(x + 5) + "px";
249 249
250 250 var anim = new YAHOO.util.Anim(div, {opacity: {to: 0.8}}, 0.2);
251 251 anim.animate();
252 252 }
253 253
254 254 /**
255 255 * This function will detect if selected period has some changesets for this user
256 256 if it does this data is then pushed for displaying
257 257 Additionally it will only display users that are selected by the checkbox
258 258 */
259 259 function getDataAccordingToRanges(ranges) {
260 260
261 261 var data = [];
262 262 var keys = [];
263 263 for(var key in dataset){
264 264 var push = false;
265 265 //method1 slow !!
266 266 ///*
267 267 for(var ds in dataset[key].data){
268 268 commit_data = dataset[key].data[ds];
269 269 //console.log(key);
270 270 //console.log(new Date(commit_data.time*1000));
271 271 //console.log(new Date(ranges.xaxis.from*1000));
272 272 //console.log(new Date(ranges.xaxis.to*1000));
273 273 if (commit_data.time >= ranges.xaxis.from && commit_data.time <= ranges.xaxis.to){
274 274 push = true;
275 275 break;
276 276 }
277 277 }
278 278 //*/
279 279 /*//method2 sorted commit data !!!
280 280 var first_commit = dataset[key].data[0].time;
281 281 var last_commit = dataset[key].data[dataset[key].data.length-1].time;
282 282
283 283 console.log(first_commit);
284 284 console.log(last_commit);
285 285
286 286 if (first_commit >= ranges.xaxis.from && last_commit <= ranges.xaxis.to){
287 287 push = true;
288 288 }
289 289 */
290 290 if(push){
291 291 data.push(dataset[key]);
292 292 }
293 293 }
294 294 if(data.length >= 1){
295 295 return data;
296 296 }
297 297 else{
298 298 //just return dummy data for graph to plot itself
299 299 return [getDummyData('')];
300 300 }
301 301
302 302 }
303 303
304 304 /**
305 305 * redraw using new checkbox data
306 306 */
307 307 function plotchoiced(e,args){
308 308 var cur_data = args[0];
309 309 var cur_ranges = args[1];
310 310
311 311 var new_data = [];
312 312 var inputs = choiceContainer.getElementsByTagName("input");
313 313
314 314 //show only checked labels
315 315 for(var i=0; i<inputs.length; i++) {
316 316 var checkbox_key = inputs[i].name;
317 317
318 318 if(inputs[i].checked){
319 319 for(var d in cur_data){
320 320 if(cur_data[d].label == checkbox_key){
321 321 new_data.push(cur_data[d]);
322 322 }
323 323 }
324 324 }
325 325 else{
326 326 //push dummy data to not hide the label
327 327 new_data.push(getDummyData(checkbox_key));
328 328 }
329 329 }
330 330
331 331 var new_options = YAHOO.lang.merge(plot_options, {
332 332 xaxis: {
333 333 min: cur_ranges.xaxis.from,
334 334 max: cur_ranges.xaxis.to,
335 335 mode:"time",
336 336 timeformat: "%d/%m",
337 337 }
338 338 });
339 339 if (!new_data){
340 340 new_data = [[0,1]];
341 341 }
342 342 // do the zooming
343 343 plot = YAHOO.widget.Flot(plotContainer, new_data, new_options);
344 344
345 345 plot.subscribe("plotselected", plotselected);
346 346
347 347 //resubscribe plothover
348 348 plot.subscribe("plothover", plothover);
349 349
350 350 // don't fire event on the overview to prevent eternal loop
351 351 overview.setSelection(cur_ranges, true);
352 352
353 353 }
354 354
355 355 /**
356 356 * plot only selected items from overview
357 357 * @param ranges
358 358 * @returns
359 359 */
360 360 function plotselected(ranges,cur_data) {
361 361 //updates the data for new plot
362 362 data = getDataAccordingToRanges(ranges);
363 363 generateCheckboxes(data);
364 364
365 365 var new_options = YAHOO.lang.merge(plot_options, {
366 366 xaxis: {
367 367 min: ranges.xaxis.from,
368 368 max: ranges.xaxis.to,
369 369 mode:"time",
370 370 timeformat: "%d/%m",
371 371 }
372 372 });
373 373 // do the zooming
374 374 plot = YAHOO.widget.Flot(plotContainer, data, new_options);
375 375
376 376 plot.subscribe("plotselected", plotselected);
377 377
378 378 //resubscribe plothover
379 379 plot.subscribe("plothover", plothover);
380 380
381 381 // don't fire event on the overview to prevent eternal loop
382 382 overview.setSelection(ranges, true);
383 383
384 384 //resubscribe choiced
385 385 YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, ranges]);
386 386 }
387 387
388 388 var previousPoint = null;
389 389
390 390 function plothover(o) {
391 391 var pos = o.pos;
392 392 var item = o.item;
393 393
394 394 //YAHOO.util.Dom.get("x").innerHTML = pos.x.toFixed(2);
395 395 //YAHOO.util.Dom.get("y").innerHTML = pos.y.toFixed(2);
396 396 if (item) {
397 397 if (previousPoint != item.datapoint) {
398 398 previousPoint = item.datapoint;
399 399
400 400 var tooltip = YAHOO.util.Dom.get("tooltip");
401 401 if(tooltip) {
402 402 tooltip.parentNode.removeChild(tooltip);
403 403 }
404 404 var x = item.datapoint.x.toFixed(2);
405 405 var y = item.datapoint.y.toFixed(2);
406 406
407 407 if (!item.series.label){
408 408 item.series.label = 'commits';
409 409 }
410 410 var d = new Date(x*1000);
411 411 var fd = d.toDateString()
412 412 var nr_commits = parseInt(y);
413 413
414 414 var cur_data = dataset[item.series.label].data[item.dataIndex];
415 415 var added = cur_data.added;
416 416 var changed = cur_data.changed;
417 417 var removed = cur_data.removed;
418 418
419 419 var nr_commits_suffix = " ${_('commits')} ";
420 420 var added_suffix = " ${_('files added')} ";
421 421 var changed_suffix = " ${_('files changed')} ";
422 422 var removed_suffix = " ${_('files removed')} ";
423 423
424 424
425 425 if(nr_commits == 1){nr_commits_suffix = " ${_('commit')} ";}
426 426 if(added==1){added_suffix=" ${_('file added')} ";}
427 427 if(changed==1){changed_suffix=" ${_('file changed')} ";}
428 428 if(removed==1){removed_suffix=" ${_('file removed')} ";}
429 429
430 430 showTooltip(item.pageX, item.pageY, item.series.label + " on " + fd
431 431 +'<br/>'+
432 432 nr_commits + nr_commits_suffix+'<br/>'+
433 433 added + added_suffix +'<br/>'+
434 434 changed + changed_suffix + '<br/>'+
435 435 removed + removed_suffix + '<br/>');
436 436 }
437 437 }
438 438 else {
439 439 var tooltip = YAHOO.util.Dom.get("tooltip");
440 440
441 441 if(tooltip) {
442 442 tooltip.parentNode.removeChild(tooltip);
443 443 }
444 444 previousPoint = null;
445 445 }
446 446 }
447 447
448 448 /**
449 449 * MAIN EXECUTION
450 450 */
451 451
452 452 var data = getDataAccordingToRanges(initial_ranges);
453 453 generateCheckboxes(data);
454 454
455 455 //main plot
456 456 var plot = YAHOO.widget.Flot(plotContainer,data,plot_options);
457 457
458 458 //overview
459 459 var overview = YAHOO.widget.Flot(overviewContainer, overview_dataset, overview_options);
460 460
461 461 //show initial selection on overview
462 462 overview.setSelection(initial_ranges);
463 463
464 464 plot.subscribe("plotselected", plotselected);
465 465
466 466 overview.subscribe("plotselected", function (ranges) {
467 467 plot.setSelection(ranges);
468 468 });
469 469
470 470 plot.subscribe("plothover", plothover);
471 471
472 472 YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, initial_ranges]);
473 473 }
474 474 SummaryPlot(${c.ts_min},${c.ts_max},${c.commit_data|n},${c.overview_data|n});
475 475 </script>
476 476
477 477 </div>
478 478 </div>
479 479
480 480 <div class="box">
481 481 <div class="title">
482 482 <div class="breadcrumbs">${h.link_to(_('Last ten changes'),h.url('changelog_home',repo_name=c.repo_name))}</div>
483 483 </div>
484 484 <div class="table">
485 485 <%include file='../shortlog/shortlog_data.html'/>
486 486 ${h.link_to(_('show more'),h.url('changelog_home',repo_name=c.repo_name))}
487 487 </div>
488 488 </div>
489 489 <div class="box">
490 490 <div class="title">
491 491 <div class="breadcrumbs">${h.link_to(_('Last ten tags'),h.url('tags_home',repo_name=c.repo_name))}</div>
492 492 </div>
493 493 <div class="table">
494 494 <%include file='../tags/tags_data.html'/>
495 495 ${h.link_to(_('show more'),h.url('tags_home',repo_name=c.repo_name))}
496 496 </div>
497 497 </div>
498 498 <div class="box">
499 499 <div class="title">
500 500 <div class="breadcrumbs">${h.link_to(_('Last ten branches'),h.url('branches_home',repo_name=c.repo_name))}</div>
501 501 </div>
502 502 <div class="table">
503 503 <%include file='../branches/branches_data.html'/>
504 504 ${h.link_to(_('show more'),h.url('branches_home',repo_name=c.repo_name))}
505 505 </div>
506 506 </div>
507 507
508 508 </%def> No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now