##// END OF EJS Templates
updated CONTRIBUTORS...
marcink -
r2058:fb51a6fc beta
parent child Browse files
Show More
@@ -1,18 +1,19 b''
1 1 List of contributors to RhodeCode project:
2 2 Marcin Kuźmiński <marcin@python-works.com>
3 3 Lukasz Balcerzak <lukaszbalcerzak@gmail.com>
4 4 Jason Harris <jason@jasonfharris.com>
5 5 Thayne Harbaugh <thayne@fusionio.com>
6 6 cejones
7 7 Thomas Waldmann <tw-public@gmx.de>
8 8 Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
9 9 Dmitri Kuznetsov
10 10 Jared Bunting <jared.bunting@peachjean.com>
11 11 Steve Romanow <slestak989@gmail.com>
12 12 Augosto Hermann <augusto.herrmann@planejamento.gov.br>
13 13 Ankit Solanki <ankit.solanki@gmail.com>
14 14 Liad Shani <liadff@gmail.com>
15 15 Les Peabody <lpeabody@gmail.com>
16 16 Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
17 17 Matt Zuba <matt.zuba@goodwillaz.org>
18 Aras Pranckevicius <aras@unity3d.com> No newline at end of file
18 Aras Pranckevicius <aras@unity3d.com>
19 Tony Bussieres <t.bussieres@gmail.com>
@@ -1,533 +1,534 b''
1 1 .. _changelog:
2 2
3 3 Changelog
4 4 =========
5 5
6 6
7 7 1.3.2 (**2012-XX-XX**)
8 8 ----------------------
9 9
10 10 :status: in-progress
11 11 :branch: beta
12 12
13 13 news
14 14 ++++
15 15
16 16
17 17 fixes
18 18 +++++
19 19
20 20 - fixed git protocol issues with repos-groups
21 21 - fixed git remote repos validator that prevented from cloning remote git repos
22 22 - fixes #370 ending slashes fixes for repo and groups
23 - fixes #368 improved git-protocol detection to handle other clients
23 24
24 25 1.3.1 (**2012-02-27**)
25 26 ----------------------
26 27
27 28 news
28 29 ++++
29 30
30 31
31 32 fixes
32 33 +++++
33 34
34 35 - redirection loop occurs when remember-me wasn't checked during login
35 36 - fixes issues with git blob history generation
36 37 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
37 38
38 39 1.3.0 (**2012-02-26**)
39 40 ----------------------
40 41
41 42 news
42 43 ++++
43 44
44 45 - code review, inspired by github code-comments
45 46 - #215 rst and markdown README files support
46 47 - #252 Container-based and proxy pass-through authentication support
47 48 - #44 branch browser. Filtering of changelog by branches
48 49 - mercurial bookmarks support
49 50 - new hover top menu, optimized to add maximum size for important views
50 51 - configurable clone url template with possibility to specify protocol like
51 52 ssh:// or http:// and also manually alter other parts of clone_url.
52 53 - enabled largefiles extension by default
53 54 - optimized summary file pages and saved a lot of unused space in them
54 55 - #239 option to manually mark repository as fork
55 56 - #320 mapping of commit authors to RhodeCode users
56 57 - #304 hashes are displayed using monospace font
57 58 - diff configuration, toggle white lines and context lines
58 59 - #307 configurable diffs, whitespace toggle, increasing context lines
59 60 - sorting on branches, tags and bookmarks using YUI datatable
60 61 - improved file filter on files page
61 62 - implements #330 api method for listing nodes ar particular revision
62 63 - #73 added linking issues in commit messages to chosen issue tracker url
63 64 based on user defined regular expression
64 65 - added linking of changesets in commit messages
65 66 - new compact changelog with expandable commit messages
66 67 - firstname and lastname are optional in user creation
67 68 - #348 added post-create repository hook
68 69 - #212 global encoding settings is now configurable from .ini files
69 70 - #227 added repository groups permissions
70 71 - markdown gets codehilite extensions
71 72 - new API methods, delete_repositories, grante/revoke permissions for groups
72 73 and repos
73 74
74 75
75 76 fixes
76 77 +++++
77 78
78 79 - rewrote dbsession management for atomic operations, and better error handling
79 80 - fixed sorting of repo tables
80 81 - #326 escape of special html entities in diffs
81 82 - normalized user_name => username in api attributes
82 83 - fixes #298 ldap created users with mixed case emails created conflicts
83 84 on saving a form
84 85 - fixes issue when owner of a repo couldn't revoke permissions for users
85 86 and groups
86 87 - fixes #271 rare JSON serialization problem with statistics
87 88 - fixes #337 missing validation check for conflicting names of a group with a
88 89 repositories group
89 90 - #340 fixed session problem for mysql and celery tasks
90 91 - fixed #331 RhodeCode mangles repository names if the a repository group
91 92 contains the "full path" to the repositories
92 93 - #355 RhodeCode doesn't store encrypted LDAP passwords
93 94
94 95 1.2.5 (**2012-01-28**)
95 96 ----------------------
96 97
97 98 news
98 99 ++++
99 100
100 101 fixes
101 102 +++++
102 103
103 104 - #340 Celery complains about MySQL server gone away, added session cleanup
104 105 for celery tasks
105 106 - #341 "scanning for repositories in None" log message during Rescan was missing
106 107 a parameter
107 108 - fixed creating archives with subrepos. Some hooks were triggered during that
108 109 operation leading to crash.
109 110 - fixed missing email in account page.
110 111 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
111 112 forking on windows impossible
112 113
113 114 1.2.4 (**2012-01-19**)
114 115 ----------------------
115 116
116 117 news
117 118 ++++
118 119
119 120 - RhodeCode is bundled with mercurial series 2.0.X by default, with
120 121 full support to largefiles extension. Enabled by default in new installations
121 122 - #329 Ability to Add/Remove Groups to/from a Repository via AP
122 123 - added requires.txt file with requirements
123 124
124 125 fixes
125 126 +++++
126 127
127 128 - fixes db session issues with celery when emailing admins
128 129 - #331 RhodeCode mangles repository names if the a repository group
129 130 contains the "full path" to the repositories
130 131 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
131 132 - DB session cleanup after hg protocol operations, fixes issues with
132 133 `mysql has gone away` errors
133 134 - #333 doc fixes for get_repo api function
134 135 - #271 rare JSON serialization problem with statistics enabled
135 136 - #337 Fixes issues with validation of repository name conflicting with
136 137 a group name. A proper message is now displayed.
137 138 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
138 139 doesn't work
139 140 - #316 fixes issues with web description in hgrc files
140 141
141 142 1.2.3 (**2011-11-02**)
142 143 ----------------------
143 144
144 145 news
145 146 ++++
146 147
147 148 - added option to manage repos group for non admin users
148 149 - added following API methods for get_users, create_user, get_users_groups,
149 150 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
150 151 get_repo, create_repo, add_user_to_repo
151 152 - implements #237 added password confirmation for my account
152 153 and admin edit user.
153 154 - implements #291 email notification for global events are now sent to all
154 155 administrator users, and global config email.
155 156
156 157 fixes
157 158 +++++
158 159
159 160 - added option for passing auth method for smtp mailer
160 161 - #276 issue with adding a single user with id>10 to usergroups
161 162 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
162 163 - #288 fixes managing of repos in a group for non admin user
163 164
164 165 1.2.2 (**2011-10-17**)
165 166 ----------------------
166 167
167 168 news
168 169 ++++
169 170
170 171 - #226 repo groups are available by path instead of numerical id
171 172
172 173 fixes
173 174 +++++
174 175
175 176 - #259 Groups with the same name but with different parent group
176 177 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
177 178 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
178 179 - #265 ldap save fails sometimes on converting attributes to booleans,
179 180 added getter and setter into model that will prevent from this on db model level
180 181 - fixed problems with timestamps issues #251 and #213
181 182 - fixes #266 RhodeCode allows to create repo with the same name and in
182 183 the same parent as group
183 184 - fixes #245 Rescan of the repositories on Windows
184 185 - fixes #248 cannot edit repos inside a group on windows
185 186 - fixes #219 forking problems on windows
186 187
187 188 1.2.1 (**2011-10-08**)
188 189 ----------------------
189 190
190 191 news
191 192 ++++
192 193
193 194
194 195 fixes
195 196 +++++
196 197
197 198 - fixed problems with basic auth and push problems
198 199 - gui fixes
199 200 - fixed logger
200 201
201 202 1.2.0 (**2011-10-07**)
202 203 ----------------------
203 204
204 205 news
205 206 ++++
206 207
207 208 - implemented #47 repository groups
208 209 - implemented #89 Can setup google analytics code from settings menu
209 210 - implemented #91 added nicer looking archive urls with more download options
210 211 like tags, branches
211 212 - implemented #44 into file browsing, and added follow branch option
212 213 - implemented #84 downloads can be enabled/disabled for each repository
213 214 - anonymous repository can be cloned without having to pass default:default
214 215 into clone url
215 216 - fixed #90 whoosh indexer can index chooses repositories passed in command
216 217 line
217 218 - extended journal with day aggregates and paging
218 219 - implemented #107 source code lines highlight ranges
219 220 - implemented #93 customizable changelog on combined revision ranges -
220 221 equivalent of githubs compare view
221 222 - implemented #108 extended and more powerful LDAP configuration
222 223 - implemented #56 users groups
223 224 - major code rewrites optimized codes for speed and memory usage
224 225 - raw and diff downloads are now in git format
225 226 - setup command checks for write access to given path
226 227 - fixed many issues with international characters and unicode. It uses utf8
227 228 decode with replace to provide less errors even with non utf8 encoded strings
228 229 - #125 added API KEY access to feeds
229 230 - #109 Repository can be created from external Mercurial link (aka. remote
230 231 repository, and manually updated (via pull) from admin panel
231 232 - beta git support - push/pull server + basic view for git repos
232 233 - added followers page and forks page
233 234 - server side file creation (with binary file upload interface)
234 235 and edition with commits powered by codemirror
235 236 - #111 file browser file finder, quick lookup files on whole file tree
236 237 - added quick login sliding menu into main page
237 238 - changelog uses lazy loading of affected files details, in some scenarios
238 239 this can improve speed of changelog page dramatically especially for
239 240 larger repositories.
240 241 - implements #214 added support for downloading subrepos in download menu.
241 242 - Added basic API for direct operations on rhodecode via JSON
242 243 - Implemented advanced hook management
243 244
244 245 fixes
245 246 +++++
246 247
247 248 - fixed file browser bug, when switching into given form revision the url was
248 249 not changing
249 250 - fixed propagation to error controller on simplehg and simplegit middlewares
250 251 - fixed error when trying to make a download on empty repository
251 252 - fixed problem with '[' chars in commit messages in journal
252 253 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
253 254 - journal fork fixes
254 255 - removed issue with space inside renamed repository after deletion
255 256 - fixed strange issue on formencode imports
256 257 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
257 258 - #150 fixes for errors on repositories mapped in db but corrupted in
258 259 filesystem
259 260 - fixed problem with ascendant characters in realm #181
260 261 - fixed problem with sqlite file based database connection pool
261 262 - whoosh indexer and code stats share the same dynamic extensions map
262 263 - fixes #188 - relationship delete of repo_to_perm entry on user removal
263 264 - fixes issue #189 Trending source files shows "show more" when no more exist
264 265 - fixes issue #197 Relative paths for pidlocks
265 266 - fixes issue #198 password will require only 3 chars now for login form
266 267 - fixes issue #199 wrong redirection for non admin users after creating a repository
267 268 - fixes issues #202, bad db constraint made impossible to attach same group
268 269 more than one time. Affects only mysql/postgres
269 270 - fixes #218 os.kill patch for windows was missing sig param
270 271 - improved rendering of dag (they are not trimmed anymore when number of
271 272 heads exceeds 5)
272 273
273 274 1.1.8 (**2011-04-12**)
274 275 ----------------------
275 276
276 277 news
277 278 ++++
278 279
279 280 - improved windows support
280 281
281 282 fixes
282 283 +++++
283 284
284 285 - fixed #140 freeze of python dateutil library, since new version is python2.x
285 286 incompatible
286 287 - setup-app will check for write permission in given path
287 288 - cleaned up license info issue #149
288 289 - fixes for issues #137,#116 and problems with unicode and accented characters.
289 290 - fixes crashes on gravatar, when passed in email as unicode
290 291 - fixed tooltip flickering problems
291 292 - fixed came_from redirection on windows
292 293 - fixed logging modules, and sql formatters
293 294 - windows fixes for os.kill issue #133
294 295 - fixes path splitting for windows issues #148
295 296 - fixed issue #143 wrong import on migration to 1.1.X
296 297 - fixed problems with displaying binary files, thanks to Thomas Waldmann
297 298 - removed name from archive files since it's breaking ui for long repo names
298 299 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
299 300 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
300 301 Thomas Waldmann
301 302 - fixed issue #166 summary pager was skipping 10 revisions on second page
302 303
303 304
304 305 1.1.7 (**2011-03-23**)
305 306 ----------------------
306 307
307 308 news
308 309 ++++
309 310
310 311 fixes
311 312 +++++
312 313
313 314 - fixed (again) #136 installation support for FreeBSD
314 315
315 316
316 317 1.1.6 (**2011-03-21**)
317 318 ----------------------
318 319
319 320 news
320 321 ++++
321 322
322 323 fixes
323 324 +++++
324 325
325 326 - fixed #136 installation support for FreeBSD
326 327 - RhodeCode will check for python version during installation
327 328
328 329 1.1.5 (**2011-03-17**)
329 330 ----------------------
330 331
331 332 news
332 333 ++++
333 334
334 335 - basic windows support, by exchanging pybcrypt into sha256 for windows only
335 336 highly inspired by idea of mantis406
336 337
337 338 fixes
338 339 +++++
339 340
340 341 - fixed sorting by author in main page
341 342 - fixed crashes with diffs on binary files
342 343 - fixed #131 problem with boolean values for LDAP
343 344 - fixed #122 mysql problems thanks to striker69
344 345 - fixed problem with errors on calling raw/raw_files/annotate functions
345 346 with unknown revisions
346 347 - fixed returned rawfiles attachment names with international character
347 348 - cleaned out docs, big thanks to Jason Harris
348 349
349 350 1.1.4 (**2011-02-19**)
350 351 ----------------------
351 352
352 353 news
353 354 ++++
354 355
355 356 fixes
356 357 +++++
357 358
358 359 - fixed formencode import problem on settings page, that caused server crash
359 360 when that page was accessed as first after server start
360 361 - journal fixes
361 362 - fixed option to access repository just by entering http://server/<repo_name>
362 363
363 364 1.1.3 (**2011-02-16**)
364 365 ----------------------
365 366
366 367 news
367 368 ++++
368 369
369 370 - implemented #102 allowing the '.' character in username
370 371 - added option to access repository just by entering http://server/<repo_name>
371 372 - celery task ignores result for better performance
372 373
373 374 fixes
374 375 +++++
375 376
376 377 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
377 378 apollo13 and Johan Walles
378 379 - small fixes in journal
379 380 - fixed problems with getting setting for celery from .ini files
380 381 - registration, password reset and login boxes share the same title as main
381 382 application now
382 383 - fixed #113: to high permissions to fork repository
383 384 - fixed problem with '[' chars in commit messages in journal
384 385 - removed issue with space inside renamed repository after deletion
385 386 - db transaction fixes when filesystem repository creation failed
386 387 - fixed #106 relation issues on databases different than sqlite
387 388 - fixed static files paths links to use of url() method
388 389
389 390 1.1.2 (**2011-01-12**)
390 391 ----------------------
391 392
392 393 news
393 394 ++++
394 395
395 396
396 397 fixes
397 398 +++++
398 399
399 400 - fixes #98 protection against float division of percentage stats
400 401 - fixed graph bug
401 402 - forced webhelpers version since it was making troubles during installation
402 403
403 404 1.1.1 (**2011-01-06**)
404 405 ----------------------
405 406
406 407 news
407 408 ++++
408 409
409 410 - added force https option into ini files for easier https usage (no need to
410 411 set server headers with this options)
411 412 - small css updates
412 413
413 414 fixes
414 415 +++++
415 416
416 417 - fixed #96 redirect loop on files view on repositories without changesets
417 418 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
418 419 and server crashed with errors
419 420 - fixed large tooltips problems on main page
420 421 - fixed #92 whoosh indexer is more error proof
421 422
422 423 1.1.0 (**2010-12-18**)
423 424 ----------------------
424 425
425 426 news
426 427 ++++
427 428
428 429 - rewrite of internals for vcs >=0.1.10
429 430 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
430 431 with older clients
431 432 - anonymous access, authentication via ldap
432 433 - performance upgrade for cached repos list - each repository has its own
433 434 cache that's invalidated when needed.
434 435 - performance upgrades on repositories with large amount of commits (20K+)
435 436 - main page quick filter for filtering repositories
436 437 - user dashboards with ability to follow chosen repositories actions
437 438 - sends email to admin on new user registration
438 439 - added cache/statistics reset options into repository settings
439 440 - more detailed action logger (based on hooks) with pushed changesets lists
440 441 and options to disable those hooks from admin panel
441 442 - introduced new enhanced changelog for merges that shows more accurate results
442 443 - new improved and faster code stats (based on pygments lexers mapping tables,
443 444 showing up to 10 trending sources for each repository. Additionally stats
444 445 can be disabled in repository settings.
445 446 - gui optimizations, fixed application width to 1024px
446 447 - added cut off (for large files/changesets) limit into config files
447 448 - whoosh, celeryd, upgrade moved to paster command
448 449 - other than sqlite database backends can be used
449 450
450 451 fixes
451 452 +++++
452 453
453 454 - fixes #61 forked repo was showing only after cache expired
454 455 - fixes #76 no confirmation on user deletes
455 456 - fixes #66 Name field misspelled
456 457 - fixes #72 block user removal when he owns repositories
457 458 - fixes #69 added password confirmation fields
458 459 - fixes #87 RhodeCode crashes occasionally on updating repository owner
459 460 - fixes #82 broken annotations on files with more than 1 blank line at the end
460 461 - a lot of fixes and tweaks for file browser
461 462 - fixed detached session issues
462 463 - fixed when user had no repos he would see all repos listed in my account
463 464 - fixed ui() instance bug when global hgrc settings was loaded for server
464 465 instance and all hgrc options were merged with our db ui() object
465 466 - numerous small bugfixes
466 467
467 468 (special thanks for TkSoh for detailed feedback)
468 469
469 470
470 471 1.0.2 (**2010-11-12**)
471 472 ----------------------
472 473
473 474 news
474 475 ++++
475 476
476 477 - tested under python2.7
477 478 - bumped sqlalchemy and celery versions
478 479
479 480 fixes
480 481 +++++
481 482
482 483 - fixed #59 missing graph.js
483 484 - fixed repo_size crash when repository had broken symlinks
484 485 - fixed python2.5 crashes.
485 486
486 487
487 488 1.0.1 (**2010-11-10**)
488 489 ----------------------
489 490
490 491 news
491 492 ++++
492 493
493 494 - small css updated
494 495
495 496 fixes
496 497 +++++
497 498
498 499 - fixed #53 python2.5 incompatible enumerate calls
499 500 - fixed #52 disable mercurial extension for web
500 501 - fixed #51 deleting repositories don't delete it's dependent objects
501 502
502 503
503 504 1.0.0 (**2010-11-02**)
504 505 ----------------------
505 506
506 507 - security bugfix simplehg wasn't checking for permissions on commands
507 508 other than pull or push.
508 509 - fixed doubled messages after push or pull in admin journal
509 510 - templating and css corrections, fixed repo switcher on chrome, updated titles
510 511 - admin menu accessible from options menu on repository view
511 512 - permissions cached queries
512 513
513 514 1.0.0rc4 (**2010-10-12**)
514 515 --------------------------
515 516
516 517 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
517 518 - removed cache_manager settings from sqlalchemy meta
518 519 - added sqlalchemy cache settings to ini files
519 520 - validated password length and added second try of failure on paster setup-app
520 521 - fixed setup database destroy prompt even when there was no db
521 522
522 523
523 524 1.0.0rc3 (**2010-10-11**)
524 525 -------------------------
525 526
526 527 - fixed i18n during installation.
527 528
528 529 1.0.0rc2 (**2010-10-11**)
529 530 -------------------------
530 531
531 532 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
532 533 occure. After vcs is fixed it'll be put back again.
533 534 - templating/css rewrites, optimized css. No newline at end of file
@@ -1,18 +1,18 b''
1 1 {% extends "basic/layout.html" %}
2 2
3 3 {% block sidebarlogo %}
4 4 <h3>Support RhodeCode development.</h3>
5 5 <div style="text-align:center">
6 6 <form action="https://www.paypal.com/cgi-bin/webscr" method="post">
7 7 <input type="hidden" name="cmd" value="_s-xclick">
8 8 <input type="hidden" name="hosted_button_id" value="8U2LLRPLBKWDU">
9 9 <input style="border:0px !important" type="image" src="https://www.paypal.com/en_US/i/btn/btn_donate_SM.gif"
10 10 border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
11 11 <img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1">
12 12 </form>
13 13 <div style="padding:5px">
14 14 <a href="http://flattr.com/thing/167489/RhodeCode" target="_blank">
15 15 <img src="http://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" border="0" /></a>
16 </div>
16 </div>
17 17 </div>
18 18 {% endblock %}}
@@ -1,465 +1,465 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.__init__
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Some simple helper functions
7 7
8 8 :created_on: Jan 5, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import re
28 28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 29
30 30
31 31 def __get_lem():
32 32 from pygments import lexers
33 33 from string import lower
34 34 from collections import defaultdict
35 35
36 36 d = defaultdict(lambda: [])
37 37
38 38 def __clean(s):
39 39 s = s.lstrip('*')
40 40 s = s.lstrip('.')
41 41
42 42 if s.find('[') != -1:
43 43 exts = []
44 44 start, stop = s.find('['), s.find(']')
45 45
46 46 for suffix in s[start + 1:stop]:
47 47 exts.append(s[:s.find('[')] + suffix)
48 48 return map(lower, exts)
49 49 else:
50 50 return map(lower, [s])
51 51
52 52 for lx, t in sorted(lexers.LEXERS.items()):
53 53 m = map(__clean, t[-2])
54 54 if m:
55 55 m = reduce(lambda x, y: x + y, m)
56 56 for ext in m:
57 57 desc = lx.replace('Lexer', '')
58 58 d[ext].append(desc)
59 59
60 60 return dict(d)
61 61
62 62 # language map is also used by whoosh indexer, which for those specified
63 63 # extensions will index it's content
64 64 LANGUAGES_EXTENSIONS_MAP = __get_lem()
65 65
66 66 # Additional mappings that are not present in the pygments lexers
67 67 # NOTE: that this will overide any mappings in LANGUAGES_EXTENSIONS_MAP
68 68 ADDITIONAL_MAPPINGS = {'xaml': 'XAML'}
69 69
70 70 LANGUAGES_EXTENSIONS_MAP.update(ADDITIONAL_MAPPINGS)
71 71
72 72 # list of readme files to search in file tree and display in summary
73 73 # attached weights defines the search order lower is first
74 74 ALL_READMES = [
75 75 ('readme', 0), ('README', 0), ('Readme', 0),
76 76 ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1),
77 77 ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2),
78 78 ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2),
79 79 ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2),
80 80 ]
81 81
82 82 # extension together with weights to search lower is first
83 83 RST_EXTS = [
84 84 ('', 0), ('.rst', 1), ('.rest', 1),
85 85 ('.RST', 2), ('.REST', 2),
86 86 ('.txt', 3), ('.TXT', 3)
87 87 ]
88 88
89 89 MARKDOWN_EXTS = [
90 90 ('.md', 1), ('.MD', 1),
91 91 ('.mkdn', 2), ('.MKDN', 2),
92 92 ('.mdown', 3), ('.MDOWN', 3),
93 93 ('.markdown', 4), ('.MARKDOWN', 4)
94 94 ]
95 95
96 96 PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)]
97 97
98 98 ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS
99 99
100 100
101 101 def str2bool(_str):
102 102 """
103 103 returs True/False value from given string, it tries to translate the
104 104 string into boolean
105 105
106 106 :param _str: string value to translate into boolean
107 107 :rtype: boolean
108 108 :returns: boolean from given string
109 109 """
110 110 if _str is None:
111 111 return False
112 112 if _str in (True, False):
113 113 return _str
114 114 _str = str(_str).strip().lower()
115 115 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
116 116
117 117
118 118 def convert_line_endings(line, mode):
119 119 """
120 120 Converts a given line "line end" accordingly to given mode
121 121
122 122 Available modes are::
123 123 0 - Unix
124 124 1 - Mac
125 125 2 - DOS
126 126
127 127 :param line: given line to convert
128 128 :param mode: mode to convert to
129 129 :rtype: str
130 130 :return: converted line according to mode
131 131 """
132 132 from string import replace
133 133
134 134 if mode == 0:
135 135 line = replace(line, '\r\n', '\n')
136 136 line = replace(line, '\r', '\n')
137 137 elif mode == 1:
138 138 line = replace(line, '\r\n', '\r')
139 139 line = replace(line, '\n', '\r')
140 140 elif mode == 2:
141 141 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
142 142 return line
143 143
144 144
145 145 def detect_mode(line, default):
146 146 """
147 147 Detects line break for given line, if line break couldn't be found
148 148 given default value is returned
149 149
150 150 :param line: str line
151 151 :param default: default
152 152 :rtype: int
153 153 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
154 154 """
155 155 if line.endswith('\r\n'):
156 156 return 2
157 157 elif line.endswith('\n'):
158 158 return 0
159 159 elif line.endswith('\r'):
160 160 return 1
161 161 else:
162 162 return default
163 163
164 164
165 165 def generate_api_key(username, salt=None):
166 166 """
167 167 Generates unique API key for given username, if salt is not given
168 168 it'll be generated from some random string
169 169
170 170 :param username: username as string
171 171 :param salt: salt to hash generate KEY
172 172 :rtype: str
173 173 :returns: sha1 hash from username+salt
174 174 """
175 175 from tempfile import _RandomNameSequence
176 176 import hashlib
177 177
178 178 if salt is None:
179 179 salt = _RandomNameSequence().next()
180 180
181 181 return hashlib.sha1(username + salt).hexdigest()
182 182
183 183
184 184 def safe_unicode(str_, from_encoding=None):
185 185 """
186 186 safe unicode function. Does few trick to turn str_ into unicode
187 187
188 188 In case of UnicodeDecode error we try to return it with encoding detected
189 189 by chardet library if it fails fallback to unicode with errors replaced
190 190
191 191 :param str_: string to decode
192 192 :rtype: unicode
193 193 :returns: unicode object
194 194 """
195 195 if isinstance(str_, unicode):
196 196 return str_
197 197
198 198 if not from_encoding:
199 199 import rhodecode
200 200 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
201 201 from_encoding = DEFAULT_ENCODING
202 202
203 203 try:
204 204 return unicode(str_)
205 205 except UnicodeDecodeError:
206 206 pass
207 207
208 208 try:
209 209 return unicode(str_, from_encoding)
210 210 except UnicodeDecodeError:
211 211 pass
212 212
213 213 try:
214 214 import chardet
215 215 encoding = chardet.detect(str_)['encoding']
216 216 if encoding is None:
217 217 raise Exception()
218 218 return str_.decode(encoding)
219 219 except (ImportError, UnicodeDecodeError, Exception):
220 220 return unicode(str_, from_encoding, 'replace')
221 221
222 222
223 223 def safe_str(unicode_, to_encoding=None):
224 224 """
225 225 safe str function. Does few trick to turn unicode_ into string
226 226
227 227 In case of UnicodeEncodeError we try to return it with encoding detected
228 228 by chardet library if it fails fallback to string with errors replaced
229 229
230 230 :param unicode_: unicode to encode
231 231 :rtype: str
232 232 :returns: str object
233 233 """
234
234
235 235 # if it's not basestr cast to str
236 236 if not isinstance(unicode_, basestring):
237 237 return str(unicode_)
238 238
239 239 if isinstance(unicode_, str):
240 240 return unicode_
241 241
242 242 if not to_encoding:
243 243 import rhodecode
244 244 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
245 245 to_encoding = DEFAULT_ENCODING
246 246
247 247 try:
248 248 return unicode_.encode(to_encoding)
249 249 except UnicodeEncodeError:
250 250 pass
251 251
252 252 try:
253 253 import chardet
254 254 encoding = chardet.detect(unicode_)['encoding']
255 255 print encoding
256 256 if encoding is None:
257 257 raise UnicodeEncodeError()
258 258
259 259 return unicode_.encode(encoding)
260 260 except (ImportError, UnicodeEncodeError):
261 261 return unicode_.encode(to_encoding, 'replace')
262 262
263 263 return safe_str
264 264
265 265
266 266 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
267 267 """
268 268 Custom engine_from_config functions that makes sure we use NullPool for
269 269 file based sqlite databases. This prevents errors on sqlite. This only
270 270 applies to sqlalchemy versions < 0.7.0
271 271
272 272 """
273 273 import sqlalchemy
274 274 from sqlalchemy import engine_from_config as efc
275 275 import logging
276 276
277 277 if int(sqlalchemy.__version__.split('.')[1]) < 7:
278 278
279 279 # This solution should work for sqlalchemy < 0.7.0, and should use
280 280 # proxy=TimerProxy() for execution time profiling
281 281
282 282 from sqlalchemy.pool import NullPool
283 283 url = configuration[prefix + 'url']
284 284
285 285 if url.startswith('sqlite'):
286 286 kwargs.update({'poolclass': NullPool})
287 287 return efc(configuration, prefix, **kwargs)
288 288 else:
289 289 import time
290 290 from sqlalchemy import event
291 291 from sqlalchemy.engine import Engine
292 292
293 293 log = logging.getLogger('sqlalchemy.engine')
294 294 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
295 295 engine = efc(configuration, prefix, **kwargs)
296 296
297 297 def color_sql(sql):
298 298 COLOR_SEQ = "\033[1;%dm"
299 299 COLOR_SQL = YELLOW
300 300 normal = '\x1b[0m'
301 301 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
302 302
303 303 if configuration['debug']:
304 304 #attach events only for debug configuration
305 305
306 306 def before_cursor_execute(conn, cursor, statement,
307 307 parameters, context, executemany):
308 308 context._query_start_time = time.time()
309 309 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
310 310
311 311
312 312 def after_cursor_execute(conn, cursor, statement,
313 313 parameters, context, executemany):
314 314 total = time.time() - context._query_start_time
315 315 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
316 316
317 317 event.listen(engine, "before_cursor_execute",
318 318 before_cursor_execute)
319 319 event.listen(engine, "after_cursor_execute",
320 320 after_cursor_execute)
321 321
322 322 return engine
323 323
324 324
325 325 def age(curdate):
326 326 """
327 327 turns a datetime into an age string.
328 328
329 329 :param curdate: datetime object
330 330 :rtype: unicode
331 331 :returns: unicode words describing age
332 332 """
333 333
334 334 from datetime import datetime
335 335 from webhelpers.date import time_ago_in_words
336 336
337 337 _ = lambda s: s
338 338
339 339 if not curdate:
340 340 return ''
341 341
342 342 agescales = [(_(u"year"), 3600 * 24 * 365),
343 343 (_(u"month"), 3600 * 24 * 30),
344 344 (_(u"day"), 3600 * 24),
345 345 (_(u"hour"), 3600),
346 346 (_(u"minute"), 60),
347 347 (_(u"second"), 1), ]
348 348
349 349 age = datetime.now() - curdate
350 350 age_seconds = (age.days * agescales[2][1]) + age.seconds
351 351 pos = 1
352 352 for scale in agescales:
353 353 if scale[1] <= age_seconds:
354 354 if pos == 6:
355 355 pos = 5
356 356 return '%s %s' % (time_ago_in_words(curdate,
357 357 agescales[pos][0]), _('ago'))
358 358 pos += 1
359 359
360 360 return _(u'just now')
361 361
362 362
363 363 def uri_filter(uri):
364 364 """
365 365 Removes user:password from given url string
366 366
367 367 :param uri:
368 368 :rtype: unicode
369 369 :returns: filtered list of strings
370 370 """
371 371 if not uri:
372 372 return ''
373 373
374 374 proto = ''
375 375
376 376 for pat in ('https://', 'http://'):
377 377 if uri.startswith(pat):
378 378 uri = uri[len(pat):]
379 379 proto = pat
380 380 break
381 381
382 382 # remove passwords and username
383 383 uri = uri[uri.find('@') + 1:]
384 384
385 385 # get the port
386 386 cred_pos = uri.find(':')
387 387 if cred_pos == -1:
388 388 host, port = uri, None
389 389 else:
390 390 host, port = uri[:cred_pos], uri[cred_pos + 1:]
391 391
392 392 return filter(None, [proto, host, port])
393 393
394 394
395 395 def credentials_filter(uri):
396 396 """
397 397 Returns a url with removed credentials
398 398
399 399 :param uri:
400 400 """
401 401
402 402 uri = uri_filter(uri)
403 403 #check if we have port
404 404 if len(uri) > 2 and uri[2]:
405 405 uri[2] = ':' + uri[2]
406 406
407 407 return ''.join(uri)
408 408
409 409
410 410 def get_changeset_safe(repo, rev):
411 411 """
412 412 Safe version of get_changeset if this changeset doesn't exists for a
413 413 repo it returns a Dummy one instead
414 414
415 415 :param repo:
416 416 :param rev:
417 417 """
418 418 from rhodecode.lib.vcs.backends.base import BaseRepository
419 419 from rhodecode.lib.vcs.exceptions import RepositoryError
420 420 if not isinstance(repo, BaseRepository):
421 421 raise Exception('You must pass an Repository '
422 422 'object as first argument got %s', type(repo))
423 423
424 424 try:
425 425 cs = repo.get_changeset(rev)
426 426 except RepositoryError:
427 427 from rhodecode.lib.utils import EmptyChangeset
428 428 cs = EmptyChangeset(requested_revision=rev)
429 429 return cs
430 430
431 431
432 432 def get_current_revision(quiet=False):
433 433 """
434 434 Returns tuple of (number, id) from repository containing this package
435 435 or None if repository could not be found.
436 436
437 437 :param quiet: prints error for fetching revision if True
438 438 """
439 439
440 440 try:
441 441 from rhodecode.lib.vcs import get_repo
442 442 from rhodecode.lib.vcs.utils.helpers import get_scm
443 443 repopath = os.path.join(os.path.dirname(__file__), '..', '..')
444 444 scm = get_scm(repopath)[0]
445 445 repo = get_repo(path=repopath, alias=scm)
446 446 tip = repo.get_changeset()
447 447 return (tip.revision, tip.short_id)
448 448 except Exception, err:
449 449 if not quiet:
450 450 print ("Cannot retrieve rhodecode's revision. Original error "
451 451 "was: %s" % err)
452 452 return None
453 453
454 454
455 455 def extract_mentioned_users(s):
456 456 """
457 457 Returns unique usernames from given string s that have @mention
458 458
459 459 :param s: string to get mentions
460 460 """
461 461 usrs = {}
462 462 for username in re.findall(r'(?:^@|\s@)(\w+)', s):
463 463 usrs[username] = username
464 464
465 465 return sorted(usrs.keys())
@@ -1,247 +1,251 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import re
29 29 import logging
30 30 import traceback
31 31
32 32 from dulwich import server as dulserver
33 33
34 34
35 35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
36 36
37 37 def handle(self):
38 38 write = lambda x: self.proto.write_sideband(1, x)
39 39
40 40 graph_walker = dulserver.ProtocolGraphWalker(self,
41 41 self.repo.object_store,
42 42 self.repo.get_peeled)
43 43 objects_iter = self.repo.fetch_objects(
44 44 graph_walker.determine_wants, graph_walker, self.progress,
45 45 get_tagged=self.get_tagged)
46 46
47 47 # Do they want any objects?
48 48 if objects_iter is None or len(objects_iter) == 0:
49 49 return
50 50
51 51 self.progress("counting objects: %d, done.\n" % len(objects_iter))
52 52 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
53 53 objects_iter, len(objects_iter))
54 54 messages = []
55 55 messages.append('thank you for using rhodecode')
56 56
57 57 for msg in messages:
58 58 self.progress(msg + "\n")
59 59 # we are done
60 60 self.proto.write("0000")
61 61
62 62 dulserver.DEFAULT_HANDLERS = {
63 63 'git-upload-pack': SimpleGitUploadPackHandler,
64 64 'git-receive-pack': dulserver.ReceivePackHandler,
65 65 }
66 66
67 67 from dulwich.repo import Repo
68 68 from dulwich.web import HTTPGitApplication
69 69
70 70 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
71 71
72 72 from rhodecode.lib import safe_str
73 73 from rhodecode.lib.base import BaseVCSController
74 74 from rhodecode.lib.auth import get_container_username
75 75 from rhodecode.lib.utils import is_valid_repo
76 76 from rhodecode.model.db import User
77 77
78 78 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
79 79
80 80 log = logging.getLogger(__name__)
81 81
82 82
83 83 GIT_PROTO_PAT = re.compile(r'git-upload-pack|git-receive-pack|info\/refs')
84 84
85
85 86 def is_git(action):
86 87 return action in ['pull','push']
87 88
89
88 90 class SimpleGit(BaseVCSController):
89 91
90 92 def _handle_request(self, environ, start_response):
91 93 #======================================================================
92 94 # GET ACTION PULL or PUSH
93 95 #======================================================================
94 96 action = self.__get_action(environ)
95 97
96 98 if not is_git(action):
97 99 return self.application(environ, start_response)
98 100
99 101 proxy_key = 'HTTP_X_REAL_IP'
100 102 def_key = 'REMOTE_ADDR'
101 103 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
102 104 username = None
103 105 # skip passing error to error controller
104 106 environ['pylons.status_code_redirect'] = True
105 107
106 108 #======================================================================
107 109 # EXTRACT REPOSITORY NAME FROM ENV
108 110 #======================================================================
109 111 try:
110 112 repo_name = self.__get_repository(environ)
111 113 log.debug('Extracted repo name is %s' % repo_name)
112 114 except:
113 115 return HTTPInternalServerError()(environ, start_response)
114 116
115 117
116 118 #======================================================================
117 119 # CHECK ANONYMOUS PERMISSION
118 120 #======================================================================
119 121 if action in ['pull', 'push']:
120 122 anonymous_user = self.__get_user('default')
121 123 username = anonymous_user.username
122 124 anonymous_perm = self._check_permission(action, anonymous_user,
123 125 repo_name)
124 126
125 127 if anonymous_perm is not True or anonymous_user.active is False:
126 128 if anonymous_perm is not True:
127 129 log.debug('Not enough credentials to access this '
128 130 'repository as anonymous user')
129 131 if anonymous_user.active is False:
130 132 log.debug('Anonymous access is disabled, running '
131 133 'authentication')
132 134 #==============================================================
133 135 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
134 136 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
135 137 #==============================================================
136 138
137 139 # Attempting to retrieve username from the container
138 140 username = get_container_username(environ, self.config)
139 141
140 142 # If not authenticated by the container, running basic auth
141 143 if not username:
142 144 self.authenticate.realm = \
143 145 safe_str(self.config['rhodecode_realm'])
144 146 result = self.authenticate(environ)
145 147 if isinstance(result, str):
146 148 AUTH_TYPE.update(environ, 'basic')
147 149 REMOTE_USER.update(environ, result)
148 150 username = result
149 151 else:
150 152 return result.wsgi_application(environ, start_response)
151 153
152 154 #==============================================================
153 155 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
154 156 #==============================================================
155 157 if action in ['pull', 'push']:
156 158 try:
157 159 user = self.__get_user(username)
158 160 if user is None or not user.active:
159 161 return HTTPForbidden()(environ, start_response)
160 162 username = user.username
161 163 except:
162 164 log.error(traceback.format_exc())
163 165 return HTTPInternalServerError()(environ,
164 166 start_response)
165 167
166 168 #check permissions for this repository
167 169 perm = self._check_permission(action, user,
168 170 repo_name)
169 171 if perm is not True:
170 172 return HTTPForbidden()(environ, start_response)
171 173
172 174 #===================================================================
173 175 # GIT REQUEST HANDLING
174 176 #===================================================================
175 177
176 178 repo_path = safe_str(os.path.join(self.basepath, repo_name))
177 179 log.debug('Repository path is %s' % repo_path)
178 180
179 181 # quick check if that dir exists...
180 182 if is_valid_repo(repo_name, self.basepath) is False:
181 183 return HTTPNotFound()(environ, start_response)
182 184
183 185 try:
184 186 #invalidate cache on push
185 187 if action == 'push':
186 188 self._invalidate_cache(repo_name)
187 189 log.info('%s action on GIT repo "%s"' % (action, repo_name))
188 190 app = self.__make_app(repo_name, repo_path)
189 191 return app(environ, start_response)
190 192 except Exception:
191 193 log.error(traceback.format_exc())
192 194 return HTTPInternalServerError()(environ, start_response)
193 195
194 196 def __make_app(self, repo_name, repo_path):
195 197 """
196 198 Make an wsgi application using dulserver
197 199
198 200 :param repo_name: name of the repository
199 201 :param repo_path: full path to the repository
200 202 """
201 203
202 204 _d = {'/' + repo_name: Repo(repo_path)}
203 205 backend = dulserver.DictBackend(_d)
204 206 gitserve = HTTPGitApplication(backend)
205 207
206 208 return gitserve
207 209
208 210 def __get_repository(self, environ):
209 211 """
210 212 Get's repository name out of PATH_INFO header
211 213
212 214 :param environ: environ where PATH_INFO is stored
213 215 """
214 216 try:
215 217 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
216 218 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
217 219 repo_name = GIT_PROTO_PAT.split(repo_name)
218 220 if repo_name:
219 221 repo_name = repo_name[0]
220 222
221 223 if repo_name.endswith('/'):
222 224 repo_name = repo_name.rstrip('/')
223 225 except:
224 226 log.error(traceback.format_exc())
225 227 raise
226 228
227 229 return repo_name
228 230
229 231 def __get_user(self, username):
230 232 return User.get_by_username(username)
231 233
232 234 def __get_action(self, environ):
233 """Maps git request commands into a pull or push command.
235 """
236 Maps git request commands into a pull or push command.
234 237
235 238 :param environ:
236 239 """
237 240 service = environ['QUERY_STRING'].split('=')
238 241 if len(service) > 1:
239 242 service_cmd = service[1]
240 mapping = {'git-receive-pack': 'push',
241 'git-upload-pack': 'pull',
242 }
243 mapping = {
244 'git-receive-pack': 'push',
245 'git-upload-pack': 'pull',
246 }
243 247
244 248 return mapping.get(service_cmd,
245 249 service_cmd if service_cmd else 'other')
246 250 else:
247 251 return 'other'
@@ -1,1203 +1,1203 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 from collections import defaultdict
31 31
32 32 from sqlalchemy import *
33 33 from sqlalchemy.ext.hybrid import hybrid_property
34 34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 35 from beaker.cache import cache_region, region_invalidate
36 36
37 37 from rhodecode.lib.vcs import get_backend
38 38 from rhodecode.lib.vcs.utils.helpers import get_scm
39 39 from rhodecode.lib.vcs.exceptions import VCSError
40 40 from rhodecode.lib.vcs.utils.lazy import LazyProperty
41 41
42 42 from rhodecode.lib import str2bool, safe_str, get_changeset_safe, safe_unicode
43 43 from rhodecode.lib.compat import json
44 44 from rhodecode.lib.caching_query import FromCache
45 45
46 46 from rhodecode.model.meta import Base, Session
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51 #==============================================================================
52 52 # BASE CLASSES
53 53 #==============================================================================
54 54
55 55
56 56 class ModelSerializer(json.JSONEncoder):
57 57 """
58 58 Simple Serializer for JSON,
59 59
60 60 usage::
61 61
62 62 to make object customized for serialization implement a __json__
63 63 method that will return a dict for serialization into json
64 64
65 65 example::
66 66
67 67 class Task(object):
68 68
69 69 def __init__(self, name, value):
70 70 self.name = name
71 71 self.value = value
72 72
73 73 def __json__(self):
74 74 return dict(name=self.name,
75 75 value=self.value)
76 76
77 77 """
78 78
79 79 def default(self, obj):
80 80
81 81 if hasattr(obj, '__json__'):
82 82 return obj.__json__()
83 83 else:
84 84 return json.JSONEncoder.default(self, obj)
85 85
86 86
87 87 class BaseModel(object):
88 88 """
89 89 Base Model for all classess
90 90 """
91 91
92 92 @classmethod
93 93 def _get_keys(cls):
94 94 """return column names for this model """
95 95 return class_mapper(cls).c.keys()
96 96
97 97 def get_dict(self):
98 98 """
99 99 return dict with keys and values corresponding
100 100 to this model data """
101 101
102 102 d = {}
103 103 for k in self._get_keys():
104 104 d[k] = getattr(self, k)
105 105
106 106 # also use __json__() if present to get additional fields
107 107 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
108 108 d[k] = val
109 109 return d
110 110
111 111 def get_appstruct(self):
112 112 """return list with keys and values tupples corresponding
113 113 to this model data """
114 114
115 115 l = []
116 116 for k in self._get_keys():
117 117 l.append((k, getattr(self, k),))
118 118 return l
119 119
120 120 def populate_obj(self, populate_dict):
121 121 """populate model with data from given populate_dict"""
122 122
123 123 for k in self._get_keys():
124 124 if k in populate_dict:
125 125 setattr(self, k, populate_dict[k])
126 126
127 127 @classmethod
128 128 def query(cls):
129 129 return Session.query(cls)
130 130
131 131 @classmethod
132 132 def get(cls, id_):
133 133 if id_:
134 134 return cls.query().get(id_)
135 135
136 136 @classmethod
137 137 def getAll(cls):
138 138 return cls.query().all()
139 139
140 140 @classmethod
141 141 def delete(cls, id_):
142 142 obj = cls.query().get(id_)
143 143 Session.delete(obj)
144 144
145 145
146 146 class RhodeCodeSetting(Base, BaseModel):
147 147 __tablename__ = 'rhodecode_settings'
148 148 __table_args__ = (
149 149 UniqueConstraint('app_settings_name'),
150 150 {'extend_existing': True}
151 151 )
152 152 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
153 153 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
154 154 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
155 155
156 156 def __init__(self, k='', v=''):
157 157 self.app_settings_name = k
158 158 self.app_settings_value = v
159 159
160 160 @validates('_app_settings_value')
161 161 def validate_settings_value(self, key, val):
162 162 assert type(val) == unicode
163 163 return val
164 164
165 165 @hybrid_property
166 166 def app_settings_value(self):
167 167 v = self._app_settings_value
168 168 if self.app_settings_name == 'ldap_active':
169 169 v = str2bool(v)
170 170 return v
171 171
172 172 @app_settings_value.setter
173 173 def app_settings_value(self, val):
174 174 """
175 175 Setter that will always make sure we use unicode in app_settings_value
176 176
177 177 :param val:
178 178 """
179 179 self._app_settings_value = safe_unicode(val)
180 180
181 181 def __repr__(self):
182 182 return "<%s('%s:%s')>" % (
183 183 self.__class__.__name__,
184 184 self.app_settings_name, self.app_settings_value
185 185 )
186 186
187 187 @classmethod
188 188 def get_by_name(cls, ldap_key):
189 189 return cls.query()\
190 190 .filter(cls.app_settings_name == ldap_key).scalar()
191 191
192 192 @classmethod
193 193 def get_app_settings(cls, cache=False):
194 194
195 195 ret = cls.query()
196 196
197 197 if cache:
198 198 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
199 199
200 200 if not ret:
201 201 raise Exception('Could not get application settings !')
202 202 settings = {}
203 203 for each in ret:
204 204 settings['rhodecode_' + each.app_settings_name] = \
205 205 each.app_settings_value
206 206
207 207 return settings
208 208
209 209 @classmethod
210 210 def get_ldap_settings(cls, cache=False):
211 211 ret = cls.query()\
212 212 .filter(cls.app_settings_name.startswith('ldap_')).all()
213 213 fd = {}
214 214 for row in ret:
215 215 fd.update({row.app_settings_name:row.app_settings_value})
216 216
217 217 return fd
218 218
219 219
220 220 class RhodeCodeUi(Base, BaseModel):
221 221 __tablename__ = 'rhodecode_ui'
222 222 __table_args__ = (
223 223 UniqueConstraint('ui_key'),
224 224 {'extend_existing': True}
225 225 )
226 226
227 227 HOOK_UPDATE = 'changegroup.update'
228 228 HOOK_REPO_SIZE = 'changegroup.repo_size'
229 229 HOOK_PUSH = 'pretxnchangegroup.push_logger'
230 230 HOOK_PULL = 'preoutgoing.pull_logger'
231 231
232 232 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
233 233 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
234 234 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
235 235 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
236 236 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
237 237
238 238 @classmethod
239 239 def get_by_key(cls, key):
240 240 return cls.query().filter(cls.ui_key == key)
241 241
242 242 @classmethod
243 243 def get_builtin_hooks(cls):
244 244 q = cls.query()
245 245 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
246 246 cls.HOOK_REPO_SIZE,
247 247 cls.HOOK_PUSH, cls.HOOK_PULL]))
248 248 return q.all()
249 249
250 250 @classmethod
251 251 def get_custom_hooks(cls):
252 252 q = cls.query()
253 253 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
254 254 cls.HOOK_REPO_SIZE,
255 255 cls.HOOK_PUSH, cls.HOOK_PULL]))
256 256 q = q.filter(cls.ui_section == 'hooks')
257 257 return q.all()
258 258
259 259 @classmethod
260 260 def create_or_update_hook(cls, key, val):
261 261 new_ui = cls.get_by_key(key).scalar() or cls()
262 262 new_ui.ui_section = 'hooks'
263 263 new_ui.ui_active = True
264 264 new_ui.ui_key = key
265 265 new_ui.ui_value = val
266 266
267 267 Session.add(new_ui)
268 268
269 269
270 270 class User(Base, BaseModel):
271 271 __tablename__ = 'users'
272 272 __table_args__ = (
273 273 UniqueConstraint('username'), UniqueConstraint('email'),
274 274 {'extend_existing': True}
275 275 )
276 276 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
277 277 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
278 278 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
279 279 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
280 280 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
281 281 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
282 282 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
283 283 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
284 284 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
285 285 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
286 286 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
287 287
288 288 user_log = relationship('UserLog', cascade='all')
289 289 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
290 290
291 291 repositories = relationship('Repository')
292 292 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
293 293 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
294 294
295 295 group_member = relationship('UsersGroupMember', cascade='all')
296 296
297 297 notifications = relationship('UserNotification',)
298 298
299 299 @hybrid_property
300 300 def email(self):
301 301 return self._email
302 302
303 303 @email.setter
304 304 def email(self, val):
305 305 self._email = val.lower() if val else None
306 306
307 307 @property
308 308 def full_name(self):
309 309 return '%s %s' % (self.name, self.lastname)
310 310
311 311 @property
312 312 def full_name_or_username(self):
313 313 return ('%s %s' % (self.name, self.lastname)
314 314 if (self.name and self.lastname) else self.username)
315 315
316 316 @property
317 317 def full_contact(self):
318 318 return '%s %s <%s>' % (self.name, self.lastname, self.email)
319 319
320 320 @property
321 321 def short_contact(self):
322 322 return '%s %s' % (self.name, self.lastname)
323 323
324 324 @property
325 325 def is_admin(self):
326 326 return self.admin
327 327
328 328 def __repr__(self):
329 329 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
330 330 self.user_id, self.username)
331 331
332 332 @classmethod
333 333 def get_by_username(cls, username, case_insensitive=False, cache=False):
334 334 if case_insensitive:
335 335 q = cls.query().filter(cls.username.ilike(username))
336 336 else:
337 337 q = cls.query().filter(cls.username == username)
338 338
339 339 if cache:
340 340 q = q.options(FromCache("sql_cache_short",
341 341 "get_user_%s" % username))
342 342 return q.scalar()
343 343
344 344 @classmethod
345 345 def get_by_api_key(cls, api_key, cache=False):
346 346 q = cls.query().filter(cls.api_key == api_key)
347 347
348 348 if cache:
349 349 q = q.options(FromCache("sql_cache_short",
350 350 "get_api_key_%s" % api_key))
351 351 return q.scalar()
352 352
353 353 @classmethod
354 354 def get_by_email(cls, email, case_insensitive=False, cache=False):
355 355 if case_insensitive:
356 356 q = cls.query().filter(cls.email.ilike(email))
357 357 else:
358 358 q = cls.query().filter(cls.email == email)
359 359
360 360 if cache:
361 361 q = q.options(FromCache("sql_cache_short",
362 362 "get_api_key_%s" % email))
363 363 return q.scalar()
364 364
365 365 def update_lastlogin(self):
366 366 """Update user lastlogin"""
367 367 self.last_login = datetime.datetime.now()
368 368 Session.add(self)
369 369 log.debug('updated user %s lastlogin' % self.username)
370 370
371 371 def __json__(self):
372 372 return dict(
373 373 email=self.email,
374 374 full_name=self.full_name,
375 375 full_name_or_username=self.full_name_or_username,
376 376 short_contact=self.short_contact,
377 377 full_contact=self.full_contact
378 378 )
379 379
380 380
381 381 class UserLog(Base, BaseModel):
382 382 __tablename__ = 'user_logs'
383 383 __table_args__ = {'extend_existing': True}
384 384 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
385 385 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
386 386 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
387 387 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
388 388 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
389 389 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
390 390 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
391 391
392 392 @property
393 393 def action_as_day(self):
394 394 return datetime.date(*self.action_date.timetuple()[:3])
395 395
396 396 user = relationship('User')
397 397 repository = relationship('Repository',cascade='')
398 398
399 399
400 400 class UsersGroup(Base, BaseModel):
401 401 __tablename__ = 'users_groups'
402 402 __table_args__ = {'extend_existing': True}
403 403
404 404 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
405 405 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
406 406 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
407 407
408 408 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
409 409
410 410 def __repr__(self):
411 411 return '<userGroup(%s)>' % (self.users_group_name)
412 412
413 413 @classmethod
414 414 def get_by_group_name(cls, group_name, cache=False,
415 415 case_insensitive=False):
416 416 if case_insensitive:
417 417 q = cls.query().filter(cls.users_group_name.ilike(group_name))
418 418 else:
419 419 q = cls.query().filter(cls.users_group_name == group_name)
420 420 if cache:
421 421 q = q.options(FromCache("sql_cache_short",
422 422 "get_user_%s" % group_name))
423 423 return q.scalar()
424 424
425 425 @classmethod
426 426 def get(cls, users_group_id, cache=False):
427 427 users_group = cls.query()
428 428 if cache:
429 429 users_group = users_group.options(FromCache("sql_cache_short",
430 430 "get_users_group_%s" % users_group_id))
431 431 return users_group.get(users_group_id)
432 432
433 433
434 434 class UsersGroupMember(Base, BaseModel):
435 435 __tablename__ = 'users_groups_members'
436 436 __table_args__ = {'extend_existing': True}
437 437
438 438 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
439 439 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
440 440 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
441 441
442 442 user = relationship('User', lazy='joined')
443 443 users_group = relationship('UsersGroup')
444 444
445 445 def __init__(self, gr_id='', u_id=''):
446 446 self.users_group_id = gr_id
447 447 self.user_id = u_id
448 448
449 449
450 450 class Repository(Base, BaseModel):
451 451 __tablename__ = 'repositories'
452 452 __table_args__ = (
453 453 UniqueConstraint('repo_name'),
454 454 {'extend_existing': True},
455 455 )
456 456
457 457 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
458 458 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
459 459 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
460 460 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
461 461 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
462 462 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
463 463 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
464 464 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
465 465 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
466 466 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
467 467
468 468 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
469 469 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
470 470
471 471 user = relationship('User')
472 472 fork = relationship('Repository', remote_side=repo_id)
473 473 group = relationship('RepoGroup')
474 474 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
475 475 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
476 476 stats = relationship('Statistics', cascade='all', uselist=False)
477 477
478 478 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
479 479
480 480 logs = relationship('UserLog')
481 481
482 482 def __repr__(self):
483 483 return "<%s('%s:%s')>" % (self.__class__.__name__,
484 484 self.repo_id, self.repo_name)
485 485
486 486 @classmethod
487 487 def url_sep(cls):
488 488 return '/'
489 489
490 490 @classmethod
491 491 def get_by_repo_name(cls, repo_name):
492 492 q = Session.query(cls).filter(cls.repo_name == repo_name)
493 493 q = q.options(joinedload(Repository.fork))\
494 494 .options(joinedload(Repository.user))\
495 495 .options(joinedload(Repository.group))
496 496 return q.scalar()
497 497
498 498 @classmethod
499 499 def get_repo_forks(cls, repo_id):
500 500 return cls.query().filter(Repository.fork_id == repo_id)
501 501
502 502 @classmethod
503 503 def base_path(cls):
504 504 """
505 505 Returns base path when all repos are stored
506 506
507 507 :param cls:
508 508 """
509 509 q = Session.query(RhodeCodeUi)\
510 510 .filter(RhodeCodeUi.ui_key == cls.url_sep())
511 511 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
512 512 return q.one().ui_value
513 513
514 514 @property
515 515 def just_name(self):
516 516 return self.repo_name.split(Repository.url_sep())[-1]
517 517
518 518 @property
519 519 def groups_with_parents(self):
520 520 groups = []
521 521 if self.group is None:
522 522 return groups
523 523
524 524 cur_gr = self.group
525 525 groups.insert(0, cur_gr)
526 526 while 1:
527 527 gr = getattr(cur_gr, 'parent_group', None)
528 528 cur_gr = cur_gr.parent_group
529 529 if gr is None:
530 530 break
531 531 groups.insert(0, gr)
532 532
533 533 return groups
534 534
535 535 @property
536 536 def groups_and_repo(self):
537 537 return self.groups_with_parents, self.just_name
538 538
539 539 @LazyProperty
540 540 def repo_path(self):
541 541 """
542 542 Returns base full path for that repository means where it actually
543 543 exists on a filesystem
544 544 """
545 545 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
546 546 Repository.url_sep())
547 547 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
548 548 return q.one().ui_value
549 549
550 550 @property
551 551 def repo_full_path(self):
552 552 p = [self.repo_path]
553 553 # we need to split the name by / since this is how we store the
554 554 # names in the database, but that eventually needs to be converted
555 555 # into a valid system path
556 556 p += self.repo_name.split(Repository.url_sep())
557 557 return os.path.join(*p)
558 558
559 559 def get_new_name(self, repo_name):
560 560 """
561 561 returns new full repository name based on assigned group and new new
562 562
563 563 :param group_name:
564 564 """
565 565 path_prefix = self.group.full_path_splitted if self.group else []
566 566 return Repository.url_sep().join(path_prefix + [repo_name])
567 567
568 568 @property
569 569 def _ui(self):
570 570 """
571 571 Creates an db based ui object for this repository
572 572 """
573 573 from mercurial import ui
574 574 from mercurial import config
575 575 baseui = ui.ui()
576 576
577 577 #clean the baseui object
578 578 baseui._ocfg = config.config()
579 579 baseui._ucfg = config.config()
580 580 baseui._tcfg = config.config()
581 581
582 582 ret = RhodeCodeUi.query()\
583 583 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
584 584
585 585 hg_ui = ret
586 586 for ui_ in hg_ui:
587 587 if ui_.ui_active:
588 588 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
589 589 ui_.ui_key, ui_.ui_value)
590 590 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
591 591
592 592 return baseui
593 593
594 594 @classmethod
595 595 def is_valid(cls, repo_name):
596 596 """
597 597 returns True if given repo name is a valid filesystem repository
598 598
599 599 :param cls:
600 600 :param repo_name:
601 601 """
602 602 from rhodecode.lib.utils import is_valid_repo
603 603
604 604 return is_valid_repo(repo_name, cls.base_path())
605 605
606 606 #==========================================================================
607 607 # SCM PROPERTIES
608 608 #==========================================================================
609 609
610 610 def get_changeset(self, rev):
611 611 return get_changeset_safe(self.scm_instance, rev)
612 612
613 613 @property
614 614 def tip(self):
615 615 return self.get_changeset('tip')
616 616
617 617 @property
618 618 def author(self):
619 619 return self.tip.author
620 620
621 621 @property
622 622 def last_change(self):
623 623 return self.scm_instance.last_change
624 624
625 625 def comments(self, revisions=None):
626 626 """
627 627 Returns comments for this repository grouped by revisions
628 628
629 629 :param revisions: filter query by revisions only
630 630 """
631 631 cmts = ChangesetComment.query()\
632 632 .filter(ChangesetComment.repo == self)
633 633 if revisions:
634 634 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
635 635 grouped = defaultdict(list)
636 636 for cmt in cmts.all():
637 637 grouped[cmt.revision].append(cmt)
638 638 return grouped
639 639
640 640 #==========================================================================
641 641 # SCM CACHE INSTANCE
642 642 #==========================================================================
643 643
644 644 @property
645 645 def invalidate(self):
646 646 return CacheInvalidation.invalidate(self.repo_name)
647 647
648 648 def set_invalidate(self):
649 649 """
650 650 set a cache for invalidation for this instance
651 651 """
652 652 CacheInvalidation.set_invalidate(self.repo_name)
653 653
654 654 @LazyProperty
655 655 def scm_instance(self):
656 656 return self.__get_instance()
657 657
658 658 @property
659 659 def scm_instance_cached(self):
660 660 @cache_region('long_term')
661 661 def _c(repo_name):
662 662 return self.__get_instance()
663 663 rn = self.repo_name
664 664 log.debug('Getting cached instance of repo')
665 665 inv = self.invalidate
666 666 if inv is not None:
667 667 region_invalidate(_c, None, rn)
668 668 # update our cache
669 669 CacheInvalidation.set_valid(inv.cache_key)
670 670 return _c(rn)
671 671
672 672 def __get_instance(self):
673 673 repo_full_path = self.repo_full_path
674 674 try:
675 675 alias = get_scm(repo_full_path)[0]
676 676 log.debug('Creating instance of %s repository' % alias)
677 677 backend = get_backend(alias)
678 678 except VCSError:
679 679 log.error(traceback.format_exc())
680 680 log.error('Perhaps this repository is in db and not in '
681 681 'filesystem run rescan repositories with '
682 682 '"destroy old data " option from admin panel')
683 683 return
684 684
685 685 if alias == 'hg':
686 686
687 687 repo = backend(safe_str(repo_full_path), create=False,
688 688 baseui=self._ui)
689 689 # skip hidden web repository
690 690 if repo._get_hidden():
691 691 return
692 692 else:
693 693 repo = backend(repo_full_path, create=False)
694 694
695 695 return repo
696 696
697 697
698 698 class RepoGroup(Base, BaseModel):
699 699 __tablename__ = 'groups'
700 700 __table_args__ = (
701 701 UniqueConstraint('group_name', 'group_parent_id'),
702 702 CheckConstraint('group_id != group_parent_id'),
703 703 {'extend_existing': True},
704 704 )
705 705 __mapper_args__ = {'order_by': 'group_name'}
706 706
707 707 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
708 708 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
709 709 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
710 710 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
711 711
712 712 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
713 713 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
714 714
715 715 parent_group = relationship('RepoGroup', remote_side=group_id)
716 716
717 717 def __init__(self, group_name='', parent_group=None):
718 718 self.group_name = group_name
719 719 self.parent_group = parent_group
720 720
721 721 def __repr__(self):
722 722 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
723 723 self.group_name)
724 724
725 725 @classmethod
726 726 def groups_choices(cls):
727 727 from webhelpers.html import literal as _literal
728 728 repo_groups = [('', '')]
729 729 sep = ' &raquo; '
730 730 _name = lambda k: _literal(sep.join(k))
731 731
732 732 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
733 733 for x in cls.query().all()])
734 734
735 735 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
736 736 return repo_groups
737 737
738 738 @classmethod
739 739 def url_sep(cls):
740 740 return '/'
741 741
742 742 @classmethod
743 743 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
744 744 if case_insensitive:
745 745 gr = cls.query()\
746 746 .filter(cls.group_name.ilike(group_name))
747 747 else:
748 748 gr = cls.query()\
749 749 .filter(cls.group_name == group_name)
750 750 if cache:
751 751 gr = gr.options(FromCache("sql_cache_short",
752 752 "get_group_%s" % group_name))
753 753 return gr.scalar()
754 754
755 755 @property
756 756 def parents(self):
757 757 parents_recursion_limit = 5
758 758 groups = []
759 759 if self.parent_group is None:
760 760 return groups
761 761 cur_gr = self.parent_group
762 762 groups.insert(0, cur_gr)
763 763 cnt = 0
764 764 while 1:
765 765 cnt += 1
766 766 gr = getattr(cur_gr, 'parent_group', None)
767 767 cur_gr = cur_gr.parent_group
768 768 if gr is None:
769 769 break
770 770 if cnt == parents_recursion_limit:
771 771 # this will prevent accidental infinit loops
772 772 log.error('group nested more than %s' %
773 773 parents_recursion_limit)
774 774 break
775 775
776 776 groups.insert(0, gr)
777 777 return groups
778 778
779 779 @property
780 780 def children(self):
781 781 return RepoGroup.query().filter(RepoGroup.parent_group == self)
782 782
783 783 @property
784 784 def name(self):
785 785 return self.group_name.split(RepoGroup.url_sep())[-1]
786 786
787 787 @property
788 788 def full_path(self):
789 789 return self.group_name
790 790
791 791 @property
792 792 def full_path_splitted(self):
793 793 return self.group_name.split(RepoGroup.url_sep())
794 794
795 795 @property
796 796 def repositories(self):
797 797 return Repository.query().filter(Repository.group == self)
798 798
799 799 @property
800 800 def repositories_recursive_count(self):
801 801 cnt = self.repositories.count()
802 802
803 803 def children_count(group):
804 804 cnt = 0
805 805 for child in group.children:
806 806 cnt += child.repositories.count()
807 807 cnt += children_count(child)
808 808 return cnt
809 809
810 810 return cnt + children_count(self)
811 811
812 812 def get_new_name(self, group_name):
813 813 """
814 814 returns new full group name based on parent and new name
815 815
816 816 :param group_name:
817 817 """
818 818 path_prefix = (self.parent_group.full_path_splitted if
819 819 self.parent_group else [])
820 820 return RepoGroup.url_sep().join(path_prefix + [group_name])
821 821
822 822
823 823 class Permission(Base, BaseModel):
824 824 __tablename__ = 'permissions'
825 825 __table_args__ = {'extend_existing': True}
826 826 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
827 827 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
828 828 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
829 829
830 830 def __repr__(self):
831 831 return "<%s('%s:%s')>" % (
832 832 self.__class__.__name__, self.permission_id, self.permission_name
833 833 )
834 834
835 835 @classmethod
836 836 def get_by_key(cls, key):
837 837 return cls.query().filter(cls.permission_name == key).scalar()
838 838
839 839 @classmethod
840 840 def get_default_perms(cls, default_user_id):
841 841 q = Session.query(UserRepoToPerm, Repository, cls)\
842 842 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
843 843 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
844 844 .filter(UserRepoToPerm.user_id == default_user_id)
845 845
846 846 return q.all()
847 847
848 848 @classmethod
849 849 def get_default_group_perms(cls, default_user_id):
850 850 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
851 851 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
852 852 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
853 853 .filter(UserRepoGroupToPerm.user_id == default_user_id)
854 854
855 855 return q.all()
856 856
857 857
858 858 class UserRepoToPerm(Base, BaseModel):
859 859 __tablename__ = 'repo_to_perm'
860 860 __table_args__ = (
861 861 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
862 862 {'extend_existing': True}
863 863 )
864 864 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
865 865 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
866 866 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
867 867 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
868 868
869 869 user = relationship('User')
870 870 repository = relationship('Repository')
871 871 permission = relationship('Permission')
872 872
873 873 @classmethod
874 874 def create(cls, user, repository, permission):
875 875 n = cls()
876 876 n.user = user
877 877 n.repository = repository
878 878 n.permission = permission
879 879 Session.add(n)
880 880 return n
881 881
882 882 def __repr__(self):
883 883 return '<user:%s => %s >' % (self.user, self.repository)
884 884
885 885
886 886 class UserToPerm(Base, BaseModel):
887 887 __tablename__ = 'user_to_perm'
888 888 __table_args__ = (
889 889 UniqueConstraint('user_id', 'permission_id'),
890 890 {'extend_existing': True}
891 891 )
892 892 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
893 893 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
894 894 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
895 895
896 896 user = relationship('User')
897 897 permission = relationship('Permission', lazy='joined')
898 898
899 899
900 900 class UsersGroupRepoToPerm(Base, BaseModel):
901 901 __tablename__ = 'users_group_repo_to_perm'
902 902 __table_args__ = (
903 903 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
904 904 {'extend_existing': True}
905 905 )
906 906 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
907 907 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
908 908 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
909 909 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
910 910
911 911 users_group = relationship('UsersGroup')
912 912 permission = relationship('Permission')
913 913 repository = relationship('Repository')
914 914
915 915 @classmethod
916 916 def create(cls, users_group, repository, permission):
917 917 n = cls()
918 918 n.users_group = users_group
919 919 n.repository = repository
920 920 n.permission = permission
921 921 Session.add(n)
922 922 return n
923 923
924 924 def __repr__(self):
925 925 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
926 926
927 927
928 928 class UsersGroupToPerm(Base, BaseModel):
929 929 __tablename__ = 'users_group_to_perm'
930 930 __table_args__ = (
931 931 UniqueConstraint('users_group_id', 'permission_id',),
932 932 {'extend_existing': True}
933 933 )
934 934 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
935 935 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
936 936 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
937 937
938 938 users_group = relationship('UsersGroup')
939 939 permission = relationship('Permission')
940 940
941 941
942 942 class UserRepoGroupToPerm(Base, BaseModel):
943 943 __tablename__ = 'user_repo_group_to_perm'
944 944 __table_args__ = (
945 945 UniqueConstraint('user_id', 'group_id', 'permission_id'),
946 946 {'extend_existing': True}
947 947 )
948 948
949 949 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
950 950 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
951 951 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
952 952 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
953 953
954 954 user = relationship('User')
955 955 group = relationship('RepoGroup')
956 956 permission = relationship('Permission')
957 957
958 958
959 959 class UsersGroupRepoGroupToPerm(Base, BaseModel):
960 960 __tablename__ = 'users_group_repo_group_to_perm'
961 961 __table_args__ = (
962 962 UniqueConstraint('users_group_id', 'group_id'),
963 963 {'extend_existing': True}
964 964 )
965 965
966 966 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
967 967 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
968 968 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
969 969 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
970 970
971 971 users_group = relationship('UsersGroup')
972 972 permission = relationship('Permission')
973 973 group = relationship('RepoGroup')
974 974
975 975
976 976 class Statistics(Base, BaseModel):
977 977 __tablename__ = 'statistics'
978 978 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing': True})
979 979 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
980 980 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
981 981 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
982 982 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
983 983 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
984 984 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
985 985
986 986 repository = relationship('Repository', single_parent=True)
987 987
988 988
989 989 class UserFollowing(Base, BaseModel):
990 990 __tablename__ = 'user_followings'
991 991 __table_args__ = (
992 992 UniqueConstraint('user_id', 'follows_repository_id'),
993 993 UniqueConstraint('user_id', 'follows_user_id'),
994 994 {'extend_existing': True}
995 995 )
996 996
997 997 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
998 998 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
999 999 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1000 1000 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1001 1001 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1002 1002
1003 1003 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1004 1004
1005 1005 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1006 1006 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1007 1007
1008 1008 @classmethod
1009 1009 def get_repo_followers(cls, repo_id):
1010 1010 return cls.query().filter(cls.follows_repo_id == repo_id)
1011 1011
1012 1012
1013 1013 class CacheInvalidation(Base, BaseModel):
1014 1014 __tablename__ = 'cache_invalidation'
1015 1015 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing': True})
1016 1016 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1017 1017 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1018 1018 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1019 1019 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1020 1020
1021 1021 def __init__(self, cache_key, cache_args=''):
1022 1022 self.cache_key = cache_key
1023 1023 self.cache_args = cache_args
1024 1024 self.cache_active = False
1025 1025
1026 1026 def __repr__(self):
1027 1027 return "<%s('%s:%s')>" % (self.__class__.__name__,
1028 1028 self.cache_id, self.cache_key)
1029 1029
1030 1030 @classmethod
1031 1031 def _get_key(cls, key):
1032 1032 """
1033 1033 Wrapper for generating a key
1034 1034
1035 1035 :param key:
1036 1036 """
1037 1037 import rhodecode
1038 1038 prefix = ''
1039 1039 iid = rhodecode.CONFIG.get('instance_id')
1040 1040 if iid:
1041 prefix = iid
1041 prefix = iid
1042 1042 return "%s%s" % (prefix, key)
1043 1043
1044 1044 @classmethod
1045 1045 def get_by_key(cls, key):
1046 1046 return cls.query().filter(cls.cache_key == key).scalar()
1047 1047
1048 1048 @classmethod
1049 1049 def invalidate(cls, key):
1050 1050 """
1051 1051 Returns Invalidation object if this given key should be invalidated
1052 1052 None otherwise. `cache_active = False` means that this cache
1053 1053 state is not valid and needs to be invalidated
1054 1054
1055 1055 :param key:
1056 1056 """
1057 1057 return cls.query()\
1058 1058 .filter(CacheInvalidation.cache_key == key)\
1059 1059 .filter(CacheInvalidation.cache_active == False)\
1060 1060 .scalar()
1061 1061
1062 1062 @classmethod
1063 1063 def set_invalidate(cls, key):
1064 1064 """
1065 1065 Mark this Cache key for invalidation
1066 1066
1067 1067 :param key:
1068 1068 """
1069 1069
1070 1070 log.debug('marking %s for invalidation' % key)
1071 1071 inv_obj = Session.query(cls)\
1072 1072 .filter(cls.cache_key == key).scalar()
1073 1073 if inv_obj:
1074 1074 inv_obj.cache_active = False
1075 1075 else:
1076 1076 log.debug('cache key not found in invalidation db -> creating one')
1077 1077 inv_obj = CacheInvalidation(key)
1078 1078
1079 1079 try:
1080 1080 Session.add(inv_obj)
1081 1081 Session.commit()
1082 1082 except Exception:
1083 1083 log.error(traceback.format_exc())
1084 1084 Session.rollback()
1085 1085
1086 1086 @classmethod
1087 1087 def set_valid(cls, key):
1088 1088 """
1089 1089 Mark this cache key as active and currently cached
1090 1090
1091 1091 :param key:
1092 1092 """
1093 1093 inv_obj = cls.get_by_key(key)
1094 1094 inv_obj.cache_active = True
1095 1095 Session.add(inv_obj)
1096 1096 Session.commit()
1097 1097
1098 1098
1099 1099 class ChangesetComment(Base, BaseModel):
1100 1100 __tablename__ = 'changeset_comments'
1101 1101 __table_args__ = ({'extend_existing': True},)
1102 1102 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1103 1103 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1104 1104 revision = Column('revision', String(40), nullable=False)
1105 1105 line_no = Column('line_no', Unicode(10), nullable=True)
1106 1106 f_path = Column('f_path', Unicode(1000), nullable=True)
1107 1107 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1108 1108 text = Column('text', Unicode(25000), nullable=False)
1109 1109 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1110 1110
1111 1111 author = relationship('User', lazy='joined')
1112 1112 repo = relationship('Repository')
1113 1113
1114 1114 @classmethod
1115 1115 def get_users(cls, revision):
1116 1116 """
1117 1117 Returns user associated with this changesetComment. ie those
1118 1118 who actually commented
1119 1119
1120 1120 :param cls:
1121 1121 :param revision:
1122 1122 """
1123 1123 return Session.query(User)\
1124 1124 .filter(cls.revision == revision)\
1125 1125 .join(ChangesetComment.author).all()
1126 1126
1127 1127
1128 1128 class Notification(Base, BaseModel):
1129 1129 __tablename__ = 'notifications'
1130 1130 __table_args__ = ({'extend_existing': True},)
1131 1131
1132 1132 TYPE_CHANGESET_COMMENT = u'cs_comment'
1133 1133 TYPE_MESSAGE = u'message'
1134 1134 TYPE_MENTION = u'mention'
1135 1135 TYPE_REGISTRATION = u'registration'
1136 1136
1137 1137 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1138 1138 subject = Column('subject', Unicode(512), nullable=True)
1139 1139 body = Column('body', Unicode(50000), nullable=True)
1140 1140 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1141 1141 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1142 1142 type_ = Column('type', Unicode(256))
1143 1143
1144 1144 created_by_user = relationship('User')
1145 1145 notifications_to_users = relationship('UserNotification', lazy='joined',
1146 1146 cascade="all, delete, delete-orphan")
1147 1147
1148 1148 @property
1149 1149 def recipients(self):
1150 1150 return [x.user for x in UserNotification.query()\
1151 1151 .filter(UserNotification.notification == self).all()]
1152 1152
1153 1153 @classmethod
1154 1154 def create(cls, created_by, subject, body, recipients, type_=None):
1155 1155 if type_ is None:
1156 1156 type_ = Notification.TYPE_MESSAGE
1157 1157
1158 1158 notification = cls()
1159 1159 notification.created_by_user = created_by
1160 1160 notification.subject = subject
1161 1161 notification.body = body
1162 1162 notification.type_ = type_
1163 1163 notification.created_on = datetime.datetime.now()
1164 1164
1165 1165 for u in recipients:
1166 1166 assoc = UserNotification()
1167 1167 assoc.notification = notification
1168 1168 u.notifications.append(assoc)
1169 1169 Session.add(notification)
1170 1170 return notification
1171 1171
1172 1172 @property
1173 1173 def description(self):
1174 1174 from rhodecode.model.notification import NotificationModel
1175 1175 return NotificationModel().make_description(self)
1176 1176
1177 1177
1178 1178 class UserNotification(Base, BaseModel):
1179 1179 __tablename__ = 'user_to_notification'
1180 1180 __table_args__ = (
1181 1181 UniqueConstraint('user_id', 'notification_id'),
1182 1182 {'extend_existing': True}
1183 1183 )
1184 1184 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1185 1185 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1186 1186 read = Column('read', Boolean, default=False)
1187 1187 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1188 1188
1189 1189 user = relationship('User', lazy="joined")
1190 1190 notification = relationship('Notification', lazy="joined",
1191 1191 order_by=lambda: Notification.created_on.desc(),)
1192 1192
1193 1193 def mark_as_read(self):
1194 1194 self.read = True
1195 1195 Session.add(self)
1196 1196
1197 1197
1198 1198 class DbMigrateVersion(Base, BaseModel):
1199 1199 __tablename__ = 'db_migrate_version'
1200 1200 __table_args__ = {'extend_existing': True}
1201 1201 repository_id = Column('repository_id', String(250), primary_key=True)
1202 1202 repository_path = Column('repository_path', Text)
1203 1203 version = Column('version', Integer)
@@ -1,773 +1,773 b''
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 import os
23 23 import re
24 24 import logging
25 25 import traceback
26 26
27 27 import formencode
28 28 from formencode import All
29 29 from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \
30 30 Email, Bool, StringBoolean, Set
31 31
32 32 from pylons.i18n.translation import _
33 33 from webhelpers.pylonslib.secure_form import authentication_token
34 34
35 35 from rhodecode.config.routing import ADMIN_PREFIX
36 36 from rhodecode.lib.utils import repo_name_slug
37 37 from rhodecode.lib.auth import authenticate, get_crypt_password
38 38 from rhodecode.lib.exceptions import LdapImportError
39 39 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
40 40 from rhodecode import BACKENDS
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 #this is needed to translate the messages using _() in validators
46 46 class State_obj(object):
47 47 _ = staticmethod(_)
48 48
49 49
50 50 #==============================================================================
51 51 # VALIDATORS
52 52 #==============================================================================
53 53 class ValidAuthToken(formencode.validators.FancyValidator):
54 54 messages = {'invalid_token': _('Token mismatch')}
55 55
56 56 def validate_python(self, value, state):
57 57
58 58 if value != authentication_token():
59 59 raise formencode.Invalid(
60 60 self.message('invalid_token',
61 61 state, search_number=value),
62 62 value,
63 63 state
64 64 )
65 65
66 66
67 67 def ValidUsername(edit, old_data):
68 68 class _ValidUsername(formencode.validators.FancyValidator):
69 69
70 70 def validate_python(self, value, state):
71 71 if value in ['default', 'new_user']:
72 72 raise formencode.Invalid(_('Invalid username'), value, state)
73 73 #check if user is unique
74 74 old_un = None
75 75 if edit:
76 76 old_un = User.get(old_data.get('user_id')).username
77 77
78 78 if old_un != value or not edit:
79 79 if User.get_by_username(value, case_insensitive=True):
80 80 raise formencode.Invalid(_('This username already '
81 81 'exists') , value, state)
82 82
83 83 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
84 84 raise formencode.Invalid(
85 85 _('Username may only contain alphanumeric characters '
86 86 'underscores, periods or dashes and must begin with '
87 87 'alphanumeric character'),
88 88 value,
89 89 state
90 90 )
91 91
92 92 return _ValidUsername
93 93
94 94
95 95 def ValidUsersGroup(edit, old_data):
96 96
97 97 class _ValidUsersGroup(formencode.validators.FancyValidator):
98 98
99 99 def validate_python(self, value, state):
100 100 if value in ['default']:
101 101 raise formencode.Invalid(_('Invalid group name'), value, state)
102 102 #check if group is unique
103 103 old_ugname = None
104 104 if edit:
105 105 old_ugname = UsersGroup.get(
106 106 old_data.get('users_group_id')).users_group_name
107 107
108 108 if old_ugname != value or not edit:
109 109 if UsersGroup.get_by_group_name(value, cache=False,
110 110 case_insensitive=True):
111 111 raise formencode.Invalid(_('This users group '
112 112 'already exists'), value,
113 113 state)
114 114
115 115 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
116 116 raise formencode.Invalid(
117 117 _('RepoGroup name may only contain alphanumeric characters '
118 118 'underscores, periods or dashes and must begin with '
119 119 'alphanumeric character'),
120 120 value,
121 121 state
122 122 )
123 123
124 124 return _ValidUsersGroup
125 125
126 126
127 127 def ValidReposGroup(edit, old_data):
128 128 class _ValidReposGroup(formencode.validators.FancyValidator):
129 129
130 130 def validate_python(self, value, state):
131 131 # TODO WRITE VALIDATIONS
132 132 group_name = value.get('group_name')
133 133 group_parent_id = value.get('group_parent_id')
134 134
135 135 # slugify repo group just in case :)
136 136 slug = repo_name_slug(group_name)
137 137
138 138 # check for parent of self
139 139 parent_of_self = lambda: (
140 140 old_data['group_id'] == int(group_parent_id)
141 141 if group_parent_id else False
142 142 )
143 143 if edit and parent_of_self():
144 144 e_dict = {
145 145 'group_parent_id': _('Cannot assign this group as parent')
146 146 }
147 147 raise formencode.Invalid('', value, state,
148 148 error_dict=e_dict)
149 149
150 150 old_gname = None
151 151 if edit:
152 152 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
153 153
154 154 if old_gname != group_name or not edit:
155 155
156 156 # check group
157 157 gr = RepoGroup.query()\
158 158 .filter(RepoGroup.group_name == slug)\
159 159 .filter(RepoGroup.group_parent_id == group_parent_id)\
160 160 .scalar()
161 161
162 162 if gr:
163 163 e_dict = {
164 164 'group_name': _('This group already exists')
165 165 }
166 166 raise formencode.Invalid('', value, state,
167 167 error_dict=e_dict)
168 168
169 169 # check for same repo
170 170 repo = Repository.query()\
171 171 .filter(Repository.repo_name == slug)\
172 172 .scalar()
173 173
174 174 if repo:
175 175 e_dict = {
176 176 'group_name': _('Repository with this name already exists')
177 177 }
178 178 raise formencode.Invalid('', value, state,
179 179 error_dict=e_dict)
180 180
181 181 return _ValidReposGroup
182 182
183 183
184 184 class ValidPassword(formencode.validators.FancyValidator):
185 185
186 186 def to_python(self, value, state):
187 187
188 188 if not value:
189 189 return
190 190
191 191 if value.get('password'):
192 192 try:
193 193 value['password'] = get_crypt_password(value['password'])
194 194 except UnicodeEncodeError:
195 195 e_dict = {'password': _('Invalid characters in password')}
196 196 raise formencode.Invalid('', value, state, error_dict=e_dict)
197 197
198 198 if value.get('password_confirmation'):
199 199 try:
200 200 value['password_confirmation'] = \
201 201 get_crypt_password(value['password_confirmation'])
202 202 except UnicodeEncodeError:
203 203 e_dict = {
204 204 'password_confirmation': _('Invalid characters in password')
205 205 }
206 206 raise formencode.Invalid('', value, state, error_dict=e_dict)
207 207
208 208 if value.get('new_password'):
209 209 try:
210 210 value['new_password'] = \
211 211 get_crypt_password(value['new_password'])
212 212 except UnicodeEncodeError:
213 213 e_dict = {'new_password': _('Invalid characters in password')}
214 214 raise formencode.Invalid('', value, state, error_dict=e_dict)
215 215
216 216 return value
217 217
218 218
219 219 class ValidPasswordsMatch(formencode.validators.FancyValidator):
220 220
221 221 def validate_python(self, value, state):
222 222
223 223 pass_val = value.get('password') or value.get('new_password')
224 224 if pass_val != value['password_confirmation']:
225 225 e_dict = {'password_confirmation':
226 226 _('Passwords do not match')}
227 227 raise formencode.Invalid('', value, state, error_dict=e_dict)
228 228
229 229
230 230 class ValidAuth(formencode.validators.FancyValidator):
231 231 messages = {
232 232 'invalid_password':_('invalid password'),
233 233 'invalid_login':_('invalid user name'),
234 234 'disabled_account':_('Your account is disabled')
235 235 }
236 236
237 237 # error mapping
238 238 e_dict = {'username': messages['invalid_login'],
239 239 'password': messages['invalid_password']}
240 240 e_dict_disable = {'username': messages['disabled_account']}
241 241
242 242 def validate_python(self, value, state):
243 243 password = value['password']
244 244 username = value['username']
245 245 user = User.get_by_username(username)
246 246
247 247 if authenticate(username, password):
248 248 return value
249 249 else:
250 250 if user and user.active is False:
251 251 log.warning('user %s is disabled' % username)
252 252 raise formencode.Invalid(
253 253 self.message('disabled_account',
254 254 state=State_obj),
255 255 value, state,
256 256 error_dict=self.e_dict_disable
257 257 )
258 258 else:
259 259 log.warning('user %s failed to authenticate' % username)
260 260 raise formencode.Invalid(
261 261 self.message('invalid_password',
262 262 state=State_obj), value, state,
263 263 error_dict=self.e_dict
264 264 )
265 265
266 266
267 267 class ValidRepoUser(formencode.validators.FancyValidator):
268 268
269 269 def to_python(self, value, state):
270 270 try:
271 271 User.query().filter(User.active == True)\
272 272 .filter(User.username == value).one()
273 273 except Exception:
274 274 raise formencode.Invalid(_('This username is not valid'),
275 275 value, state)
276 276 return value
277 277
278 278
279 279 def ValidRepoName(edit, old_data):
280 280 class _ValidRepoName(formencode.validators.FancyValidator):
281 281 def to_python(self, value, state):
282 282
283 283 repo_name = value.get('repo_name')
284 284
285 285 slug = repo_name_slug(repo_name)
286 286 if slug in [ADMIN_PREFIX, '']:
287 287 e_dict = {'repo_name': _('This repository name is disallowed')}
288 288 raise formencode.Invalid('', value, state, error_dict=e_dict)
289 289
290 290 if value.get('repo_group'):
291 291 gr = RepoGroup.get(value.get('repo_group'))
292 292 group_path = gr.full_path
293 293 # value needs to be aware of group name in order to check
294 294 # db key This is an actual just the name to store in the
295 295 # database
296 296 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
297 297
298 298 else:
299 299 group_path = ''
300 300 repo_name_full = repo_name
301 301
302 302 value['repo_name_full'] = repo_name_full
303 303 rename = old_data.get('repo_name') != repo_name_full
304 304 create = not edit
305 305 if rename or create:
306 306
307 307 if group_path != '':
308 308 if Repository.get_by_repo_name(repo_name_full):
309 309 e_dict = {
310 310 'repo_name': _('This repository already exists in '
311 311 'a group "%s"') % gr.group_name
312 312 }
313 313 raise formencode.Invalid('', value, state,
314 314 error_dict=e_dict)
315 315 elif RepoGroup.get_by_group_name(repo_name_full):
316 316 e_dict = {
317 317 'repo_name': _('There is a group with this name '
318 318 'already "%s"') % repo_name_full
319 319 }
320 320 raise formencode.Invalid('', value, state,
321 321 error_dict=e_dict)
322 322
323 323 elif Repository.get_by_repo_name(repo_name_full):
324 324 e_dict = {'repo_name': _('This repository '
325 325 'already exists')}
326 326 raise formencode.Invalid('', value, state,
327 327 error_dict=e_dict)
328 328
329 329 return value
330 330
331 331 return _ValidRepoName
332 332
333 333
334 334 def ValidForkName(*args, **kwargs):
335 335 return ValidRepoName(*args, **kwargs)
336 336
337 337
338 338 def SlugifyName():
339 339 class _SlugifyName(formencode.validators.FancyValidator):
340 340
341 341 def to_python(self, value, state):
342 342 return repo_name_slug(value)
343 343
344 344 return _SlugifyName
345 345
346 346
347 347 def ValidCloneUri():
348 348 from rhodecode.lib.utils import make_ui
349 349
350 350 def url_handler(repo_type, url, proto, ui=None):
351 351 if repo_type == 'hg':
352 352 from mercurial.httprepo import httprepository, httpsrepository
353 353 if proto == 'https':
354 354 httpsrepository(make_ui('db'), url).capabilities
355 355 elif proto == 'http':
356 356 httprepository(make_ui('db'), url).capabilities
357 357 elif repo_type == 'git':
358 358 #TODO: write a git url validator
359 359 pass
360 360
361 361 class _ValidCloneUri(formencode.validators.FancyValidator):
362 362
363 363 def to_python(self, value, state):
364 364
365 365 repo_type = value.get('repo_type')
366 366 url = value.get('clone_uri')
367 367 e_dict = {'clone_uri': _('invalid clone url')}
368 368
369 369 if not url:
370 370 pass
371 371 elif url.startswith('https'):
372 372 try:
373 373 url_handler(repo_type, url, 'https', make_ui('db'))
374 374 except Exception:
375 375 log.error(traceback.format_exc())
376 376 raise formencode.Invalid('', value, state, error_dict=e_dict)
377 377 elif url.startswith('http'):
378 378 try:
379 379 url_handler(repo_type, url, 'http', make_ui('db'))
380 380 except Exception:
381 381 log.error(traceback.format_exc())
382 382 raise formencode.Invalid('', value, state, error_dict=e_dict)
383 383 else:
384 384 e_dict = {'clone_uri': _('Invalid clone url, provide a '
385 385 'valid clone http\s url')}
386 386 raise formencode.Invalid('', value, state, error_dict=e_dict)
387 387
388 388 return value
389 389
390 390 return _ValidCloneUri
391 391
392 392
393 393 def ValidForkType(old_data):
394 394 class _ValidForkType(formencode.validators.FancyValidator):
395 395
396 396 def to_python(self, value, state):
397 397 if old_data['repo_type'] != value:
398 398 raise formencode.Invalid(_('Fork have to be the same '
399 399 'type as original'), value, state)
400 400
401 401 return value
402 402 return _ValidForkType
403 403
404 404
405 405 def ValidPerms(type_='repo'):
406 406 if type_ == 'group':
407 407 EMPTY_PERM = 'group.none'
408 408 elif type_ == 'repo':
409 409 EMPTY_PERM = 'repository.none'
410 410
411 411 class _ValidPerms(formencode.validators.FancyValidator):
412 412 messages = {
413 413 'perm_new_member_name':
414 414 _('This username or users group name is not valid')
415 415 }
416 416
417 417 def to_python(self, value, state):
418 418 perms_update = []
419 419 perms_new = []
420 420 # build a list of permission to update and new permission to create
421 421 for k, v in value.items():
422 422 # means new added member to permissions
423 423 if k.startswith('perm_new_member'):
424 424 new_perm = value.get('perm_new_member', False)
425 425 new_member = value.get('perm_new_member_name', False)
426 426 new_type = value.get('perm_new_member_type')
427 427
428 428 if new_member and new_perm:
429 429 if (new_member, new_perm, new_type) not in perms_new:
430 430 perms_new.append((new_member, new_perm, new_type))
431 431 elif k.startswith('u_perm_') or k.startswith('g_perm_'):
432 432 member = k[7:]
433 433 t = {'u': 'user',
434 434 'g': 'users_group'
435 435 }[k[0]]
436 436 if member == 'default':
437 437 if value.get('private'):
438 438 # set none for default when updating to private repo
439 439 v = EMPTY_PERM
440 440 perms_update.append((member, v, t))
441 441
442 442 value['perms_updates'] = perms_update
443 443 value['perms_new'] = perms_new
444 444
445 445 # update permissions
446 446 for k, v, t in perms_new:
447 447 try:
448 448 if t is 'user':
449 449 self.user_db = User.query()\
450 450 .filter(User.active == True)\
451 451 .filter(User.username == k).one()
452 452 if t is 'users_group':
453 453 self.user_db = UsersGroup.query()\
454 454 .filter(UsersGroup.users_group_active == True)\
455 455 .filter(UsersGroup.users_group_name == k).one()
456 456
457 457 except Exception:
458 458 msg = self.message('perm_new_member_name',
459 459 state=State_obj)
460 460 raise formencode.Invalid(
461 461 msg, value, state, error_dict={'perm_new_member_name': msg}
462 462 )
463 463 return value
464 464 return _ValidPerms
465 465
466 466
467 467 class ValidSettings(formencode.validators.FancyValidator):
468 468
469 469 def to_python(self, value, state):
470 470 # settings form can't edit user
471 471 if 'user' in value:
472 472 del['value']['user']
473 473 return value
474 474
475 475
476 476 class ValidPath(formencode.validators.FancyValidator):
477 477 def to_python(self, value, state):
478 478
479 479 if not os.path.isdir(value):
480 480 msg = _('This is not a valid path')
481 481 raise formencode.Invalid(msg, value, state,
482 482 error_dict={'paths_root_path': msg})
483 483 return value
484 484
485 485
486 486 def UniqSystemEmail(old_data):
487 487 class _UniqSystemEmail(formencode.validators.FancyValidator):
488 488 def to_python(self, value, state):
489 489 value = value.lower()
490 490 if old_data.get('email', '').lower() != value:
491 491 user = User.get_by_email(value, case_insensitive=True)
492 492 if user:
493 493 raise formencode.Invalid(
494 494 _("This e-mail address is already taken"), value, state
495 495 )
496 496 return value
497 497
498 498 return _UniqSystemEmail
499 499
500 500
501 501 class ValidSystemEmail(formencode.validators.FancyValidator):
502 502 def to_python(self, value, state):
503 503 value = value.lower()
504 504 user = User.get_by_email(value, case_insensitive=True)
505 505 if user is None:
506 506 raise formencode.Invalid(
507 507 _("This e-mail address doesn't exist."), value, state
508 508 )
509 509
510 510 return value
511 511
512 512
513 513 class LdapLibValidator(formencode.validators.FancyValidator):
514 514
515 515 def to_python(self, value, state):
516 516
517 517 try:
518 518 import ldap
519 519 except ImportError:
520 520 raise LdapImportError
521 521 return value
522 522
523 523
524 524 class AttrLoginValidator(formencode.validators.FancyValidator):
525 525
526 526 def to_python(self, value, state):
527 527
528 528 if not value or not isinstance(value, (str, unicode)):
529 529 raise formencode.Invalid(
530 530 _("The LDAP Login attribute of the CN must be specified - "
531 531 "this is the name of the attribute that is equivalent "
532 532 "to 'username'"), value, state
533 533 )
534 534
535 535 return value
536 536
537 537
538 538 #==============================================================================
539 539 # FORMS
540 540 #==============================================================================
541 541 class LoginForm(formencode.Schema):
542 542 allow_extra_fields = True
543 543 filter_extra_fields = True
544 544 username = UnicodeString(
545 545 strip=True,
546 546 min=1,
547 547 not_empty=True,
548 548 messages={
549 549 'empty': _('Please enter a login'),
550 550 'tooShort': _('Enter a value %(min)i characters long or more')}
551 551 )
552 552
553 553 password = UnicodeString(
554 554 strip=True,
555 555 min=3,
556 556 not_empty=True,
557 557 messages={
558 558 'empty': _('Please enter a password'),
559 559 'tooShort': _('Enter %(min)i characters or more')}
560 560 )
561 561
562 562 remember = StringBoolean(if_missing=False)
563 563
564 564 chained_validators = [ValidAuth]
565 565
566 566
567 567 def UserForm(edit=False, old_data={}):
568 568 class _UserForm(formencode.Schema):
569 569 allow_extra_fields = True
570 570 filter_extra_fields = True
571 571 username = All(UnicodeString(strip=True, min=1, not_empty=True),
572 572 ValidUsername(edit, old_data))
573 573 if edit:
574 574 new_password = All(UnicodeString(strip=True, min=6, not_empty=False))
575 575 password_confirmation = All(UnicodeString(strip=True, min=6,
576 576 not_empty=False))
577 577 admin = StringBoolean(if_missing=False)
578 578 else:
579 579 password = All(UnicodeString(strip=True, min=6, not_empty=True))
580 580 password_confirmation = All(UnicodeString(strip=True, min=6,
581 581 not_empty=False))
582 582
583 583 active = StringBoolean(if_missing=False)
584 584 name = UnicodeString(strip=True, min=1, not_empty=False)
585 585 lastname = UnicodeString(strip=True, min=1, not_empty=False)
586 586 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
587 587
588 588 chained_validators = [ValidPasswordsMatch, ValidPassword]
589 589
590 590 return _UserForm
591 591
592 592
593 593 def UsersGroupForm(edit=False, old_data={}, available_members=[]):
594 594 class _UsersGroupForm(formencode.Schema):
595 595 allow_extra_fields = True
596 596 filter_extra_fields = True
597 597
598 598 users_group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
599 599 ValidUsersGroup(edit, old_data))
600 600
601 601 users_group_active = StringBoolean(if_missing=False)
602 602
603 603 if edit:
604 604 users_group_members = OneOf(available_members, hideList=False,
605 605 testValueList=True,
606 606 if_missing=None, not_empty=False)
607 607
608 608 return _UsersGroupForm
609 609
610 610
611 611 def ReposGroupForm(edit=False, old_data={}, available_groups=[]):
612 612 class _ReposGroupForm(formencode.Schema):
613 613 allow_extra_fields = True
614 614 filter_extra_fields = False
615 615
616 616 group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
617 617 SlugifyName())
618 618 group_description = UnicodeString(strip=True, min=1,
619 619 not_empty=True)
620 620 group_parent_id = OneOf(available_groups, hideList=False,
621 621 testValueList=True,
622 622 if_missing=None, not_empty=False)
623 623
624 624 chained_validators = [ValidReposGroup(edit, old_data), ValidPerms('group')]
625 625
626 626 return _ReposGroupForm
627 627
628 628
629 629 def RegisterForm(edit=False, old_data={}):
630 630 class _RegisterForm(formencode.Schema):
631 631 allow_extra_fields = True
632 632 filter_extra_fields = True
633 633 username = All(ValidUsername(edit, old_data),
634 634 UnicodeString(strip=True, min=1, not_empty=True))
635 635 password = All(UnicodeString(strip=True, min=6, not_empty=True))
636 636 password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=True))
637 637 active = StringBoolean(if_missing=False)
638 638 name = UnicodeString(strip=True, min=1, not_empty=False)
639 639 lastname = UnicodeString(strip=True, min=1, not_empty=False)
640 640 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
641 641
642 642 chained_validators = [ValidPasswordsMatch, ValidPassword]
643 643
644 644 return _RegisterForm
645 645
646 646
647 647 def PasswordResetForm():
648 648 class _PasswordResetForm(formencode.Schema):
649 649 allow_extra_fields = True
650 650 filter_extra_fields = True
651 651 email = All(ValidSystemEmail(), Email(not_empty=True))
652 652 return _PasswordResetForm
653 653
654 654
655 655 def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
656 656 repo_groups=[]):
657 657 class _RepoForm(formencode.Schema):
658 658 allow_extra_fields = True
659 659 filter_extra_fields = False
660 660 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
661 661 SlugifyName())
662 662 clone_uri = All(UnicodeString(strip=True, min=1, not_empty=False))
663 663 repo_group = OneOf(repo_groups, hideList=True)
664 664 repo_type = OneOf(supported_backends)
665 665 description = UnicodeString(strip=True, min=1, not_empty=True)
666 666 private = StringBoolean(if_missing=False)
667 667 enable_statistics = StringBoolean(if_missing=False)
668 668 enable_downloads = StringBoolean(if_missing=False)
669 669
670 670 if edit:
671 671 #this is repo owner
672 672 user = All(UnicodeString(not_empty=True), ValidRepoUser)
673 673
674 674 chained_validators = [ValidCloneUri()(),
675 ValidRepoName(edit, old_data),
675 ValidRepoName(edit, old_data),
676 676 ValidPerms()]
677 677 return _RepoForm
678 678
679 679
680 680 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
681 681 repo_groups=[]):
682 682 class _RepoForkForm(formencode.Schema):
683 683 allow_extra_fields = True
684 684 filter_extra_fields = False
685 685 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
686 686 SlugifyName())
687 687 repo_group = OneOf(repo_groups, hideList=True)
688 688 repo_type = All(ValidForkType(old_data), OneOf(supported_backends))
689 689 description = UnicodeString(strip=True, min=1, not_empty=True)
690 690 private = StringBoolean(if_missing=False)
691 691 copy_permissions = StringBoolean(if_missing=False)
692 692 update_after_clone = StringBoolean(if_missing=False)
693 693 fork_parent_id = UnicodeString()
694 694 chained_validators = [ValidForkName(edit, old_data)]
695 695
696 696 return _RepoForkForm
697 697
698 698
699 699 def RepoSettingsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
700 700 repo_groups=[]):
701 701 class _RepoForm(formencode.Schema):
702 702 allow_extra_fields = True
703 703 filter_extra_fields = False
704 704 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
705 705 SlugifyName())
706 706 description = UnicodeString(strip=True, min=1, not_empty=True)
707 707 repo_group = OneOf(repo_groups, hideList=True)
708 708 private = StringBoolean(if_missing=False)
709 709
710 710 chained_validators = [ValidRepoName(edit, old_data), ValidPerms(),
711 711 ValidSettings]
712 712 return _RepoForm
713 713
714 714
715 715 def ApplicationSettingsForm():
716 716 class _ApplicationSettingsForm(formencode.Schema):
717 717 allow_extra_fields = True
718 718 filter_extra_fields = False
719 719 rhodecode_title = UnicodeString(strip=True, min=1, not_empty=True)
720 720 rhodecode_realm = UnicodeString(strip=True, min=1, not_empty=True)
721 721 rhodecode_ga_code = UnicodeString(strip=True, min=1, not_empty=False)
722 722
723 723 return _ApplicationSettingsForm
724 724
725 725
726 726 def ApplicationUiSettingsForm():
727 727 class _ApplicationUiSettingsForm(formencode.Schema):
728 728 allow_extra_fields = True
729 729 filter_extra_fields = False
730 730 web_push_ssl = OneOf(['true', 'false'], if_missing='false')
731 731 paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True))
732 732 hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
733 733 hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False)
734 734 hooks_pretxnchangegroup_push_logger = OneOf(['True', 'False'], if_missing=False)
735 735 hooks_preoutgoing_pull_logger = OneOf(['True', 'False'], if_missing=False)
736 736
737 737 return _ApplicationUiSettingsForm
738 738
739 739
740 740 def DefaultPermissionsForm(perms_choices, register_choices, create_choices):
741 741 class _DefaultPermissionsForm(formencode.Schema):
742 742 allow_extra_fields = True
743 743 filter_extra_fields = True
744 744 overwrite_default = StringBoolean(if_missing=False)
745 745 anonymous = OneOf(['True', 'False'], if_missing=False)
746 746 default_perm = OneOf(perms_choices)
747 747 default_register = OneOf(register_choices)
748 748 default_create = OneOf(create_choices)
749 749
750 750 return _DefaultPermissionsForm
751 751
752 752
753 753 def LdapSettingsForm(tls_reqcert_choices, search_scope_choices, tls_kind_choices):
754 754 class _LdapSettingsForm(formencode.Schema):
755 755 allow_extra_fields = True
756 756 filter_extra_fields = True
757 757 pre_validators = [LdapLibValidator]
758 758 ldap_active = StringBoolean(if_missing=False)
759 759 ldap_host = UnicodeString(strip=True,)
760 760 ldap_port = Number(strip=True,)
761 761 ldap_tls_kind = OneOf(tls_kind_choices)
762 762 ldap_tls_reqcert = OneOf(tls_reqcert_choices)
763 763 ldap_dn_user = UnicodeString(strip=True,)
764 764 ldap_dn_pass = UnicodeString(strip=True,)
765 765 ldap_base_dn = UnicodeString(strip=True,)
766 766 ldap_filter = UnicodeString(strip=True,)
767 767 ldap_search_scope = OneOf(search_scope_choices)
768 768 ldap_attr_login = All(AttrLoginValidator, UnicodeString(strip=True,))
769 769 ldap_attr_firstname = UnicodeString(strip=True,)
770 770 ldap_attr_lastname = UnicodeString(strip=True,)
771 771 ldap_attr_email = UnicodeString(strip=True,)
772 772
773 773 return _LdapSettingsForm
General Comments 0
You need to be logged in to leave comments. Login now