##// END OF EJS Templates
follow-up on texts missing from 'users groups'/'repositories group' cleanup
Mads Kiilerich -
r3416:5706f6ab beta
parent child Browse files
Show More
@@ -1,968 +1,968 b''
1 1 .. _changelog:
2 2
3 3 =========
4 4 Changelog
5 5 =========
6 6
7 7 1.6.0 (**2013-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 fixes
17 17 +++++
18 18
19 19 1.5.3 (**2013-02-12**)
20 20 ----------------------
21 21
22 22 news
23 23 ++++
24 24
25 25 - IP restrictions now also enabled for IPv6
26 26
27 27 fixes
28 28 +++++
29 29
30 30 - fixed issues with private checkbox not always working
31 31 - fixed #746 unicodeDedode errors on feed controllers
32 32 - fixes issue #756 cleanup repos didn't properly compose paths of repos to be cleaned up.
33 33 - fixed cache invalidation issues together with vcs_full_cache option
34 34 - repo scan should skip directories with starting with '.'
35 35 - fixes for issue #731, update-repoinfo sometimes failed to update data when changesets
36 36 were initial commits
37 37 - recursive mode of setting permission skips private repositories
38 38
39 39 1.5.2 (**2013-01-14**)
40 40 ----------------------
41 41
42 42 news
43 43 ++++
44 44
45 45 - IP restrictions for users. Each user can get a set of whitelist IP+mask for
46 46 extra protection. Useful for buildbots etc.
47 47 - added full last changeset info to lightweight dashboard. lightweight dashboard
48 48 is now fully functional replacement of original dashboard.
49 49 - implemented certain API calls for non-admin users.
50 50 - enabled all Markdown Extra plugins
51 51 - implemented #725 Pull Request View - Show origin repo URL
52 52 - show comments from pull requests into associated changesets
53 53
54 54 fixes
55 55 +++++
56 56
57 57 - update repoinfo script is more failsafe
58 58 - fixed #687 Lazy loaded tooltip bug with simultaneous ajax requests
59 59 - fixed #691: Notifications for pull requests: move link to top for better
60 60 readability
61 61 - fixed #699: fix missing fork docs for API
62 62 - fixed #693 Opening changeset from pull request fails
63 63 - fixed #710 File view stripping empty lines from beginning and end of file
64 64 - fixed issues with getting repos by path on windows, caused GIT hooks to fail
65 65 - fixed issues with groups paginator on main dashboard
66 66 - improved fetch/pull command for git repos, now pulling all refs
67 67 - fixed issue #719 Journal revision ID tooltip AJAX query path is incorrect
68 68 when running in a subdir
69 69 - fixed issue #702 API methods without arguments fail when "args":null
70 70 - set the status of changesets initially on pull request. Fixes issues #690 and #587
71 71
72 72 1.5.1 (**2012-12-13**)
73 73 ----------------------
74 74
75 75 news
76 76 ++++
77 77
78 78 - implements #677: Don't allow to close pull requests when they are
79 79 under-review status
80 80 - implemented #670 Implementation of Roles in Pull Request
81 81
82 82 fixes
83 83 +++++
84 84
85 85 - default permissions can get duplicated after migration
86 86 - fixed changeset status labels, they now select radio buttons
87 87 - #682 translation difficult for multi-line text
88 88 - #683 fixed difference between messages about not mapped repositories
89 89 - email: fail nicely when no SMTP server has been configured
90 90
91 91 1.5.0 (**2012-12-12**)
92 92 ----------------------
93 93
94 94 news
95 95 ++++
96 96
97 97 - new rewritten from scratch diff engine. 10x faster in edge cases. Handling
98 98 of file renames, copies, change flags and binary files
99 99 - added lightweight dashboard option. ref #500. New version of dashboard
100 100 page that doesn't use any VCS data and is super fast to render. Recommended
101 101 for large amount of repositories.
102 102 - implements #648 write Script for updating last modification time for
103 103 lightweight dashboard
104 104 - implemented compare engine for git repositories.
105 105 - LDAP failover, option to specify multiple servers
106 106 - added Errormator and Sentry support for monitoring RhodeCode
107 107 - implemented #628: Pass server URL to rc-extensions hooks
108 108 - new tooltip implementation - added lazy loading of changesets from journal
109 109 pages. This can significantly improve speed of rendering the page
110 110 - implements #632,added branch/tag/bookmarks info into feeds
111 111 added changeset link to body of message
112 112 - implemented #638 permissions overview to groups
113 113 - implements #636, lazy loading of history and authors to speed up source
114 114 pages rendering
115 115 - implemented #647, option to pass list of default encoding used to
116 116 encode to/decode from unicode
117 117 - added caching layer into RSS/ATOM feeds.
118 118 - basic implementation of cherry picking changesets for pull request, ref #575
119 119 - implemented #661 Add option to include diff in RSS feed
120 120 - implemented file history page for showing detailed changelog for a given file
121 121 - implemented #663 Admin/permission: specify default repogroup perms
122 122 - implemented #379 defaults settings page for creation of repositories, locking
123 123 statistics, downloads, repository type
124 124 - implemented #210 filtering of admin journal based on Whoosh Query language
125 125 - added parents/children links in changeset viewref #650
126 126
127 127 fixes
128 128 +++++
129 129
130 130 - fixed git version checker
131 131 - #586 patched basic auth handler to fix issues with git behind proxy
132 132 - #589 search urlgenerator didn't properly escape special characters
133 133 - fixed issue #614 Include repo name in delete confirmation dialog
134 134 - fixed #623: Lang meta-tag doesn't work with C#/C++
135 135 - fixes #612 Double quotes to Single quotes result in bad html in diff
136 136 - fixes #630 git statistics do too much work making them slow.
137 137 - fixes #625 Git-Tags are not displayed in Shortlog
138 138 - fix for issue #602, enforce str when setting mercurial UI object.
139 139 When this is used together with mercurial internal translation system
140 140 it can lead to UnicodeDecodeErrors
141 141 - fixes #645 Fix git handler when doing delete remote branch
142 142 - implements #649 added two seperate method for author and commiter to VCS
143 143 changeset class switch author for git backed to be the real author not commiter
144 144 - fix issue #504 RhodeCode is showing different versions of README on
145 145 different summary page loads
146 146 - implemented #658 Changing username in LDAP-Mode should not be allowed.
147 147 - fixes #652 switch to generator approach when doing file annotation to prevent
148 148 huge memory consumption
149 149 - fixes #666 move lockkey path location to cache_dir to ensure this path is
150 150 always writable for rhodecode server
151 151 - many more small fixes and improvements
152 152 - fixed issues with recursive scans on removed repositories that could take
153 153 long time on instance start
154 154
155 155 1.4.4 (**2012-10-08**)
156 156 ----------------------
157 157
158 158 news
159 159 ++++
160 160
161 161 - obfuscate db password in logs for engine connection string
162 162 - #574 Show pull request status also in shortlog (if any)
163 163 - remember selected tab in my account page
164 164 - Bumped mercurial version to 2.3.2
165 165 - #595 rcextension hook for repository delete
166 166
167 167 fixes
168 168 +++++
169 169
170 170 - Add git version detection to warn users that Git used in system is to
171 171 old. Ref #588 - also show git version in system details in settings page
172 172 - fixed files quick filter links
173 173 - #590 Add GET flag that controls the way the diff are generated, for pull
174 174 requests we want to use non-bundle based diffs, That are far better for
175 175 doing code reviews. The /compare url still uses bundle compare for full
176 176 comparison including the incoming changesets
177 177 - Fixed #585, checks for status of revision where to strict, and made
178 178 opening pull request with those revision impossible due to previously set
179 179 status. Checks now are made also for the repository.
180 180 - fixes #591 git backend was causing encoding errors when handling binary
181 181 files - added a test case for VCS lib tests
182 182 - fixed #597 commits in future get negative age.
183 183 - fixed #598 API docs methods had wrong members parameter as returned data
184 184
185 185 1.4.3 (**2012-09-28**)
186 186 ----------------------
187 187
188 188 news
189 189 ++++
190 190
191 191 - #558 Added config file to hooks extra data
192 192 - bumped mercurial version to 2.3.1
193 193 - #518 added possibility of specifying multiple patterns for issues
194 194 - update codemirror to latest version
195 195
196 196 fixes
197 197 +++++
198 198
199 199 - fixed #570 explicit user group permissions can overwrite owner permissions
200 200 - fixed #578 set proper PATH with current Python for Git
201 201 hooks to execute within same Python as RhodeCode
202 202 - fixed issue with Git bare repos that ends with .git in name
203 203
204 204 1.4.2 (**2012-09-12**)
205 205 ----------------------
206 206
207 207 news
208 208 ++++
209 209
210 210 - added option to menu to quick lock/unlock repository for users that have
211 211 write access to
212 212 - Implemented permissions for writing to repo
213 213 groups. Now only write access to group allows to create a repostiory
214 214 within that group
215 215 - #565 Add support for {netloc} and {scheme} to alternative_gravatar_url
216 216 - updated translation for zh_CN
217 217
218 218 fixes
219 219 +++++
220 220
221 221 - fixed visual permissions check on repos groups inside groups
222 222 - fixed issues with non-ascii search terms in search, and indexers
223 223 - fixed parsing of page number in GET parameters
224 224 - fixed issues with generating pull-request overview for repos with
225 225 bookmarks and tags, also preview doesn't loose chosen revision from
226 226 select dropdown
227 227
228 228 1.4.1 (**2012-09-07**)
229 229 ----------------------
230 230
231 231 news
232 232 ++++
233 233
234 234 - always put a comment about code-review status change even if user send
235 235 empty data
236 236 - modified_on column saves repository update and it's going to be used
237 237 later for light version of main page ref #500
238 238 - pull request notifications send much nicer emails with details about pull
239 239 request
240 240 - #551 show breadcrumbs in summary view for repositories inside a group
241 241
242 242 fixes
243 243 +++++
244 244
245 245 - fixed migrations of permissions that can lead to inconsistency.
246 246 Some users sent feedback that after upgrading from older versions issues
247 247 with updating default permissions occurred. RhodeCode detects that now and
248 248 resets default user permission to initial state if there is a need for that.
249 249 Also forces users to set the default value for new forking permission.
250 250 - #535 improved apache wsgi example configuration in docs
251 251 - fixes #550 mercurial repositories comparision failed when origin repo had
252 252 additional not-common changesets
253 253 - fixed status of code-review in preview windows of pull request
254 254 - git forks were not initialized at bare repos
255 255 - fixes #555 fixes issues with comparing non-related repositories
256 256 - fixes #557 follower counter always counts up
257 257 - fixed issue #560 require push ssl checkbox wasn't shown when option was
258 258 enabled
259 259 - fixed #559
260 260 - fixed issue #559 fixed bug in routing that mapped repo names with <name>_<num> in name as
261 261 if it was a request to url by repository ID
262 262
263 263 1.4.0 (**2012-09-03**)
264 264 ----------------------
265 265
266 266 news
267 267 ++++
268 268
269 269 - new codereview system
270 270 - email map, allowing users to have multiple email addresses mapped into
271 271 their accounts
272 272 - improved git-hook system. Now all actions for git are logged into journal
273 273 including pushed revisions, user and IP address
274 274 - changed setup-app into setup-rhodecode and added default options to it.
275 275 - new git repos are created as bare now by default
276 276 - #464 added links to groups in permission box
277 277 - #465 mentions autocomplete inside comments boxes
278 278 - #469 added --update-only option to whoosh to re-index only given list
279 279 of repos in index
280 280 - rhodecode-api CLI client
281 281 - new git http protocol replaced buggy dulwich implementation.
282 282 Now based on pygrack & gitweb
283 283 - Improved RSS/ATOM feeds. Discoverable by browsers using proper headers, and
284 284 reformated based on user suggestions. Additional rss/atom feeds for user
285 285 journal
286 286 - various i18n improvements
287 287 - #478 permissions overview for admin in user edit view
288 288 - File view now displays small gravatars off all authors of given file
289 289 - Implemented landing revisions. Each repository will get landing_rev attribute
290 290 that defines 'default' revision/branch for generating readme files
291 291 - Implemented #509, RhodeCode enforces SSL for push/pulling if requested at
292 292 earliest possible call.
293 293 - Import remote svn repositories to mercurial using hgsubversion.
294 294 - Fixed #508 RhodeCode now has a option to explicitly set forking permissions
295 295 - RhodeCode can use alternative server for generating avatar icons
296 296 - implemented repositories locking. Pull locks, push unlocks. Also can be done
297 297 via API calls
298 298 - #538 form for permissions can handle multiple users at once
299 299
300 300 fixes
301 301 +++++
302 302
303 303 - improved translations
304 304 - fixes issue #455 Creating an archive generates an exception on Windows
305 305 - fixes #448 Download ZIP archive keeps file in /tmp open and results
306 306 in out of disk space
307 307 - fixes issue #454 Search results under Windows include proceeding
308 308 backslash
309 309 - fixed issue #450. Rhodecode no longer will crash when bad revision is
310 310 present in journal data.
311 311 - fix for issue #417, git execution was broken on windows for certain
312 312 commands.
313 313 - fixed #413. Don't disable .git directory for bare repos on deleting
314 314 - fixed issue #459. Changed the way of obtaining logger in reindex task.
315 315 - fixed #453 added ID field in whoosh SCHEMA that solves the issue of
316 316 reindexing modified files
317 317 - fixed #481 rhodecode emails are sent without Date header
318 318 - fixed #458 wrong count when no repos are present
319 319 - fixed issue #492 missing `\ No newline at end of file` test at the end of
320 320 new chunk in html diff
321 321 - full text search now works also for commit messages
322 322
323 323 1.3.6 (**2012-05-17**)
324 324 ----------------------
325 325
326 326 news
327 327 ++++
328 328
329 329 - chinese traditional translation
330 330 - changed setup-app into setup-rhodecode and added arguments for auto-setup
331 331 mode that doesn't need user interaction
332 332
333 333 fixes
334 334 +++++
335 335
336 336 - fixed no scm found warning
337 337 - fixed __future__ import error on rcextensions
338 338 - made simplejson required lib for speedup on JSON encoding
339 339 - fixes #449 bad regex could get more than revisions from parsing history
340 340 - don't clear DB session when CELERY_EAGER is turned ON
341 341
342 342 1.3.5 (**2012-05-10**)
343 343 ----------------------
344 344
345 345 news
346 346 ++++
347 347
348 348 - use ext_json for json module
349 349 - unified annotation view with file source view
350 350 - notification improvements, better inbox + css
351 351 - #419 don't strip passwords for login forms, make rhodecode
352 352 more compatible with LDAP servers
353 353 - Added HTTP_X_FORWARDED_FOR as another method of extracting
354 354 IP for pull/push logs. - moved all to base controller
355 355 - #415: Adding comment to changeset causes reload.
356 356 Comments are now added via ajax and doesn't reload the page
357 357 - #374 LDAP config is discarded when LDAP can't be activated
358 358 - limited push/pull operations are now logged for git in the journal
359 359 - bumped mercurial to 2.2.X series
360 360 - added support for displaying submodules in file-browser
361 361 - #421 added bookmarks in changelog view
362 362
363 363 fixes
364 364 +++++
365 365
366 366 - fixed dev-version marker for stable when served from source codes
367 367 - fixed missing permission checks on show forks page
368 368 - #418 cast to unicode fixes in notification objects
369 369 - #426 fixed mention extracting regex
370 370 - fixed remote-pulling for git remotes remopositories
371 371 - fixed #434: Error when accessing files or changesets of a git repository
372 372 with submodules
373 373 - fixed issue with empty APIKEYS for users after registration ref. #438
374 374 - fixed issue with getting README files from git repositories
375 375
376 376 1.3.4 (**2012-03-28**)
377 377 ----------------------
378 378
379 379 news
380 380 ++++
381 381
382 382 - Whoosh logging is now controlled by the .ini files logging setup
383 383 - added clone-url into edit form on /settings page
384 384 - added help text into repo add/edit forms
385 385 - created rcextensions module with additional mappings (ref #322) and
386 386 post push/pull/create repo hooks callbacks
387 387 - implemented #377 Users view for his own permissions on account page
388 388 - #399 added inheritance of permissions for user group on repos groups
389 389 - #401 repository group is automatically pre-selected when adding repos
390 390 inside a repository group
391 391 - added alternative HTTP 403 response when client failed to authenticate. Helps
392 392 solving issues with Mercurial and LDAP
393 393 - #402 removed group prefix from repository name when listing repositories
394 394 inside a group
395 395 - added gravatars into permission view and permissions autocomplete
396 396 - #347 when running multiple RhodeCode instances, properly invalidates cache
397 397 for all registered servers
398 398
399 399 fixes
400 400 +++++
401 401
402 402 - fixed #390 cache invalidation problems on repos inside group
403 403 - fixed #385 clone by ID url was loosing proxy prefix in URL
404 404 - fixed some unicode problems with waitress
405 405 - fixed issue with escaping < and > in changeset commits
406 406 - fixed error occurring during recursive group creation in API
407 407 create_repo function
408 408 - fixed #393 py2.5 fixes for routes url generator
409 409 - fixed #397 Private repository groups shows up before login
410 410 - fixed #396 fixed problems with revoking users in nested groups
411 411 - fixed mysql unicode issues + specified InnoDB as default engine with
412 412 utf8 charset
413 413 - #406 trim long branch/tag names in changelog to not break UI
414 414
415 415 1.3.3 (**2012-03-02**)
416 416 ----------------------
417 417
418 418 news
419 419 ++++
420 420
421 421
422 422 fixes
423 423 +++++
424 424
425 425 - fixed some python2.5 compatibility issues
426 426 - fixed issues with removed repos was accidentally added as groups, after
427 427 full rescan of paths
428 428 - fixes #376 Cannot edit user (using container auth)
429 429 - fixes #378 Invalid image urls on changeset screen with proxy-prefix
430 430 configuration
431 431 - fixed initial sorting of repos inside repo group
432 432 - fixes issue when user tried to resubmit same permission into user/user_groups
433 433 - bumped beaker version that fixes #375 leap error bug
434 434 - fixed raw_changeset for git. It was generated with hg patch headers
435 435 - fixed vcs issue with last_changeset for filenodes
436 436 - fixed missing commit after hook delete
437 437 - fixed #372 issues with git operation detection that caused a security issue
438 438 for git repos
439 439
440 440 1.3.2 (**2012-02-28**)
441 441 ----------------------
442 442
443 443 news
444 444 ++++
445 445
446 446
447 447 fixes
448 448 +++++
449 449
450 450 - fixed git protocol issues with repos-groups
451 451 - fixed git remote repos validator that prevented from cloning remote git repos
452 452 - fixes #370 ending slashes fixes for repo and groups
453 453 - fixes #368 improved git-protocol detection to handle other clients
454 454 - fixes #366 When Setting Repository Group To Blank Repo Group Wont Be
455 455 Moved To Root
456 456 - fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys
457 457 - fixed #373 missing cascade drop on user_group_to_perm table
458 458
459 459 1.3.1 (**2012-02-27**)
460 460 ----------------------
461 461
462 462 news
463 463 ++++
464 464
465 465
466 466 fixes
467 467 +++++
468 468
469 469 - redirection loop occurs when remember-me wasn't checked during login
470 470 - fixes issues with git blob history generation
471 471 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
472 472
473 473 1.3.0 (**2012-02-26**)
474 474 ----------------------
475 475
476 476 news
477 477 ++++
478 478
479 479 - code review, inspired by github code-comments
480 480 - #215 rst and markdown README files support
481 481 - #252 Container-based and proxy pass-through authentication support
482 482 - #44 branch browser. Filtering of changelog by branches
483 483 - mercurial bookmarks support
484 484 - new hover top menu, optimized to add maximum size for important views
485 485 - configurable clone url template with possibility to specify protocol like
486 486 ssh:// or http:// and also manually alter other parts of clone_url.
487 487 - enabled largefiles extension by default
488 488 - optimized summary file pages and saved a lot of unused space in them
489 489 - #239 option to manually mark repository as fork
490 490 - #320 mapping of commit authors to RhodeCode users
491 491 - #304 hashes are displayed using monospace font
492 492 - diff configuration, toggle white lines and context lines
493 493 - #307 configurable diffs, whitespace toggle, increasing context lines
494 494 - sorting on branches, tags and bookmarks using YUI datatable
495 495 - improved file filter on files page
496 496 - implements #330 api method for listing nodes ar particular revision
497 497 - #73 added linking issues in commit messages to chosen issue tracker url
498 498 based on user defined regular expression
499 499 - added linking of changesets in commit messages
500 500 - new compact changelog with expandable commit messages
501 501 - firstname and lastname are optional in user creation
502 502 - #348 added post-create repository hook
503 503 - #212 global encoding settings is now configurable from .ini files
504 504 - #227 added repository groups permissions
505 505 - markdown gets codehilite extensions
506 506 - new API methods, delete_repositories, grante/revoke permissions for groups
507 507 and repos
508 508
509 509
510 510 fixes
511 511 +++++
512 512
513 513 - rewrote dbsession management for atomic operations, and better error handling
514 514 - fixed sorting of repo tables
515 515 - #326 escape of special html entities in diffs
516 516 - normalized user_name => username in api attributes
517 517 - fixes #298 ldap created users with mixed case emails created conflicts
518 518 on saving a form
519 519 - fixes issue when owner of a repo couldn't revoke permissions for users
520 520 and groups
521 521 - fixes #271 rare JSON serialization problem with statistics
522 522 - fixes #337 missing validation check for conflicting names of a group with a
523 repositories group
523 repository group
524 524 - #340 fixed session problem for mysql and celery tasks
525 525 - fixed #331 RhodeCode mangles repository names if the a repository group
526 526 contains the "full path" to the repositories
527 527 - #355 RhodeCode doesn't store encrypted LDAP passwords
528 528
529 529 1.2.5 (**2012-01-28**)
530 530 ----------------------
531 531
532 532 news
533 533 ++++
534 534
535 535 fixes
536 536 +++++
537 537
538 538 - #340 Celery complains about MySQL server gone away, added session cleanup
539 539 for celery tasks
540 540 - #341 "scanning for repositories in None" log message during Rescan was missing
541 541 a parameter
542 542 - fixed creating archives with subrepos. Some hooks were triggered during that
543 543 operation leading to crash.
544 544 - fixed missing email in account page.
545 545 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
546 546 forking on windows impossible
547 547
548 548 1.2.4 (**2012-01-19**)
549 549 ----------------------
550 550
551 551 news
552 552 ++++
553 553
554 554 - RhodeCode is bundled with mercurial series 2.0.X by default, with
555 555 full support to largefiles extension. Enabled by default in new installations
556 556 - #329 Ability to Add/Remove Groups to/from a Repository via AP
557 557 - added requires.txt file with requirements
558 558
559 559 fixes
560 560 +++++
561 561
562 562 - fixes db session issues with celery when emailing admins
563 563 - #331 RhodeCode mangles repository names if the a repository group
564 564 contains the "full path" to the repositories
565 565 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
566 566 - DB session cleanup after hg protocol operations, fixes issues with
567 567 `mysql has gone away` errors
568 568 - #333 doc fixes for get_repo api function
569 569 - #271 rare JSON serialization problem with statistics enabled
570 570 - #337 Fixes issues with validation of repository name conflicting with
571 571 a group name. A proper message is now displayed.
572 572 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
573 573 doesn't work
574 574 - #316 fixes issues with web description in hgrc files
575 575
576 576 1.2.3 (**2011-11-02**)
577 577 ----------------------
578 578
579 579 news
580 580 ++++
581 581
582 582 - added option to manage repos group for non admin users
583 583 - added following API methods for get_users, create_user, get_users_groups,
584 584 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
585 585 get_repo, create_repo, add_user_to_repo
586 586 - implements #237 added password confirmation for my account
587 587 and admin edit user.
588 588 - implements #291 email notification for global events are now sent to all
589 589 administrator users, and global config email.
590 590
591 591 fixes
592 592 +++++
593 593
594 594 - added option for passing auth method for smtp mailer
595 595 - #276 issue with adding a single user with id>10 to usergroups
596 596 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
597 597 - #288 fixes managing of repos in a group for non admin user
598 598
599 599 1.2.2 (**2011-10-17**)
600 600 ----------------------
601 601
602 602 news
603 603 ++++
604 604
605 605 - #226 repo groups are available by path instead of numerical id
606 606
607 607 fixes
608 608 +++++
609 609
610 610 - #259 Groups with the same name but with different parent group
611 611 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
612 612 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
613 613 - #265 ldap save fails sometimes on converting attributes to booleans,
614 614 added getter and setter into model that will prevent from this on db model level
615 615 - fixed problems with timestamps issues #251 and #213
616 616 - fixes #266 RhodeCode allows to create repo with the same name and in
617 617 the same parent as group
618 618 - fixes #245 Rescan of the repositories on Windows
619 619 - fixes #248 cannot edit repos inside a group on windows
620 620 - fixes #219 forking problems on windows
621 621
622 622 1.2.1 (**2011-10-08**)
623 623 ----------------------
624 624
625 625 news
626 626 ++++
627 627
628 628
629 629 fixes
630 630 +++++
631 631
632 632 - fixed problems with basic auth and push problems
633 633 - gui fixes
634 634 - fixed logger
635 635
636 636 1.2.0 (**2011-10-07**)
637 637 ----------------------
638 638
639 639 news
640 640 ++++
641 641
642 642 - implemented #47 repository groups
643 643 - implemented #89 Can setup google analytics code from settings menu
644 644 - implemented #91 added nicer looking archive urls with more download options
645 645 like tags, branches
646 646 - implemented #44 into file browsing, and added follow branch option
647 647 - implemented #84 downloads can be enabled/disabled for each repository
648 648 - anonymous repository can be cloned without having to pass default:default
649 649 into clone url
650 650 - fixed #90 whoosh indexer can index chooses repositories passed in command
651 651 line
652 652 - extended journal with day aggregates and paging
653 653 - implemented #107 source code lines highlight ranges
654 654 - implemented #93 customizable changelog on combined revision ranges -
655 655 equivalent of githubs compare view
656 656 - implemented #108 extended and more powerful LDAP configuration
657 657 - implemented #56 user groups
658 658 - major code rewrites optimized codes for speed and memory usage
659 659 - raw and diff downloads are now in git format
660 660 - setup command checks for write access to given path
661 661 - fixed many issues with international characters and unicode. It uses utf8
662 662 decode with replace to provide less errors even with non utf8 encoded strings
663 663 - #125 added API KEY access to feeds
664 664 - #109 Repository can be created from external Mercurial link (aka. remote
665 665 repository, and manually updated (via pull) from admin panel
666 666 - beta git support - push/pull server + basic view for git repos
667 667 - added followers page and forks page
668 668 - server side file creation (with binary file upload interface)
669 669 and edition with commits powered by codemirror
670 670 - #111 file browser file finder, quick lookup files on whole file tree
671 671 - added quick login sliding menu into main page
672 672 - changelog uses lazy loading of affected files details, in some scenarios
673 673 this can improve speed of changelog page dramatically especially for
674 674 larger repositories.
675 675 - implements #214 added support for downloading subrepos in download menu.
676 676 - Added basic API for direct operations on rhodecode via JSON
677 677 - Implemented advanced hook management
678 678
679 679 fixes
680 680 +++++
681 681
682 682 - fixed file browser bug, when switching into given form revision the url was
683 683 not changing
684 684 - fixed propagation to error controller on simplehg and simplegit middlewares
685 685 - fixed error when trying to make a download on empty repository
686 686 - fixed problem with '[' chars in commit messages in journal
687 687 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
688 688 - journal fork fixes
689 689 - removed issue with space inside renamed repository after deletion
690 690 - fixed strange issue on formencode imports
691 691 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
692 692 - #150 fixes for errors on repositories mapped in db but corrupted in
693 693 filesystem
694 694 - fixed problem with ascendant characters in realm #181
695 695 - fixed problem with sqlite file based database connection pool
696 696 - whoosh indexer and code stats share the same dynamic extensions map
697 697 - fixes #188 - relationship delete of repo_to_perm entry on user removal
698 698 - fixes issue #189 Trending source files shows "show more" when no more exist
699 699 - fixes issue #197 Relative paths for pidlocks
700 700 - fixes issue #198 password will require only 3 chars now for login form
701 701 - fixes issue #199 wrong redirection for non admin users after creating a repository
702 702 - fixes issues #202, bad db constraint made impossible to attach same group
703 703 more than one time. Affects only mysql/postgres
704 704 - fixes #218 os.kill patch for windows was missing sig param
705 705 - improved rendering of dag (they are not trimmed anymore when number of
706 706 heads exceeds 5)
707 707
708 708 1.1.8 (**2011-04-12**)
709 709 ----------------------
710 710
711 711 news
712 712 ++++
713 713
714 714 - improved windows support
715 715
716 716 fixes
717 717 +++++
718 718
719 719 - fixed #140 freeze of python dateutil library, since new version is python2.x
720 720 incompatible
721 721 - setup-app will check for write permission in given path
722 722 - cleaned up license info issue #149
723 723 - fixes for issues #137,#116 and problems with unicode and accented characters.
724 724 - fixes crashes on gravatar, when passed in email as unicode
725 725 - fixed tooltip flickering problems
726 726 - fixed came_from redirection on windows
727 727 - fixed logging modules, and sql formatters
728 728 - windows fixes for os.kill issue #133
729 729 - fixes path splitting for windows issues #148
730 730 - fixed issue #143 wrong import on migration to 1.1.X
731 731 - fixed problems with displaying binary files, thanks to Thomas Waldmann
732 732 - removed name from archive files since it's breaking ui for long repo names
733 733 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
734 734 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
735 735 Thomas Waldmann
736 736 - fixed issue #166 summary pager was skipping 10 revisions on second page
737 737
738 738
739 739 1.1.7 (**2011-03-23**)
740 740 ----------------------
741 741
742 742 news
743 743 ++++
744 744
745 745 fixes
746 746 +++++
747 747
748 748 - fixed (again) #136 installation support for FreeBSD
749 749
750 750
751 751 1.1.6 (**2011-03-21**)
752 752 ----------------------
753 753
754 754 news
755 755 ++++
756 756
757 757 fixes
758 758 +++++
759 759
760 760 - fixed #136 installation support for FreeBSD
761 761 - RhodeCode will check for python version during installation
762 762
763 763 1.1.5 (**2011-03-17**)
764 764 ----------------------
765 765
766 766 news
767 767 ++++
768 768
769 769 - basic windows support, by exchanging pybcrypt into sha256 for windows only
770 770 highly inspired by idea of mantis406
771 771
772 772 fixes
773 773 +++++
774 774
775 775 - fixed sorting by author in main page
776 776 - fixed crashes with diffs on binary files
777 777 - fixed #131 problem with boolean values for LDAP
778 778 - fixed #122 mysql problems thanks to striker69
779 779 - fixed problem with errors on calling raw/raw_files/annotate functions
780 780 with unknown revisions
781 781 - fixed returned rawfiles attachment names with international character
782 782 - cleaned out docs, big thanks to Jason Harris
783 783
784 784 1.1.4 (**2011-02-19**)
785 785 ----------------------
786 786
787 787 news
788 788 ++++
789 789
790 790 fixes
791 791 +++++
792 792
793 793 - fixed formencode import problem on settings page, that caused server crash
794 794 when that page was accessed as first after server start
795 795 - journal fixes
796 796 - fixed option to access repository just by entering http://server/<repo_name>
797 797
798 798 1.1.3 (**2011-02-16**)
799 799 ----------------------
800 800
801 801 news
802 802 ++++
803 803
804 804 - implemented #102 allowing the '.' character in username
805 805 - added option to access repository just by entering http://server/<repo_name>
806 806 - celery task ignores result for better performance
807 807
808 808 fixes
809 809 +++++
810 810
811 811 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
812 812 apollo13 and Johan Walles
813 813 - small fixes in journal
814 814 - fixed problems with getting setting for celery from .ini files
815 815 - registration, password reset and login boxes share the same title as main
816 816 application now
817 817 - fixed #113: to high permissions to fork repository
818 818 - fixed problem with '[' chars in commit messages in journal
819 819 - removed issue with space inside renamed repository after deletion
820 820 - db transaction fixes when filesystem repository creation failed
821 821 - fixed #106 relation issues on databases different than sqlite
822 822 - fixed static files paths links to use of url() method
823 823
824 824 1.1.2 (**2011-01-12**)
825 825 ----------------------
826 826
827 827 news
828 828 ++++
829 829
830 830
831 831 fixes
832 832 +++++
833 833
834 834 - fixes #98 protection against float division of percentage stats
835 835 - fixed graph bug
836 836 - forced webhelpers version since it was making troubles during installation
837 837
838 838 1.1.1 (**2011-01-06**)
839 839 ----------------------
840 840
841 841 news
842 842 ++++
843 843
844 844 - added force https option into ini files for easier https usage (no need to
845 845 set server headers with this options)
846 846 - small css updates
847 847
848 848 fixes
849 849 +++++
850 850
851 851 - fixed #96 redirect loop on files view on repositories without changesets
852 852 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
853 853 and server crashed with errors
854 854 - fixed large tooltips problems on main page
855 855 - fixed #92 whoosh indexer is more error proof
856 856
857 857 1.1.0 (**2010-12-18**)
858 858 ----------------------
859 859
860 860 news
861 861 ++++
862 862
863 863 - rewrite of internals for vcs >=0.1.10
864 864 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
865 865 with older clients
866 866 - anonymous access, authentication via ldap
867 867 - performance upgrade for cached repos list - each repository has its own
868 868 cache that's invalidated when needed.
869 869 - performance upgrades on repositories with large amount of commits (20K+)
870 870 - main page quick filter for filtering repositories
871 871 - user dashboards with ability to follow chosen repositories actions
872 872 - sends email to admin on new user registration
873 873 - added cache/statistics reset options into repository settings
874 874 - more detailed action logger (based on hooks) with pushed changesets lists
875 875 and options to disable those hooks from admin panel
876 876 - introduced new enhanced changelog for merges that shows more accurate results
877 877 - new improved and faster code stats (based on pygments lexers mapping tables,
878 878 showing up to 10 trending sources for each repository. Additionally stats
879 879 can be disabled in repository settings.
880 880 - gui optimizations, fixed application width to 1024px
881 881 - added cut off (for large files/changesets) limit into config files
882 882 - whoosh, celeryd, upgrade moved to paster command
883 883 - other than sqlite database backends can be used
884 884
885 885 fixes
886 886 +++++
887 887
888 888 - fixes #61 forked repo was showing only after cache expired
889 889 - fixes #76 no confirmation on user deletes
890 890 - fixes #66 Name field misspelled
891 891 - fixes #72 block user removal when he owns repositories
892 892 - fixes #69 added password confirmation fields
893 893 - fixes #87 RhodeCode crashes occasionally on updating repository owner
894 894 - fixes #82 broken annotations on files with more than 1 blank line at the end
895 895 - a lot of fixes and tweaks for file browser
896 896 - fixed detached session issues
897 897 - fixed when user had no repos he would see all repos listed in my account
898 898 - fixed ui() instance bug when global hgrc settings was loaded for server
899 899 instance and all hgrc options were merged with our db ui() object
900 900 - numerous small bugfixes
901 901
902 902 (special thanks for TkSoh for detailed feedback)
903 903
904 904
905 905 1.0.2 (**2010-11-12**)
906 906 ----------------------
907 907
908 908 news
909 909 ++++
910 910
911 911 - tested under python2.7
912 912 - bumped sqlalchemy and celery versions
913 913
914 914 fixes
915 915 +++++
916 916
917 917 - fixed #59 missing graph.js
918 918 - fixed repo_size crash when repository had broken symlinks
919 919 - fixed python2.5 crashes.
920 920
921 921
922 922 1.0.1 (**2010-11-10**)
923 923 ----------------------
924 924
925 925 news
926 926 ++++
927 927
928 928 - small css updated
929 929
930 930 fixes
931 931 +++++
932 932
933 933 - fixed #53 python2.5 incompatible enumerate calls
934 934 - fixed #52 disable mercurial extension for web
935 935 - fixed #51 deleting repositories don't delete it's dependent objects
936 936
937 937
938 938 1.0.0 (**2010-11-02**)
939 939 ----------------------
940 940
941 941 - security bugfix simplehg wasn't checking for permissions on commands
942 942 other than pull or push.
943 943 - fixed doubled messages after push or pull in admin journal
944 944 - templating and css corrections, fixed repo switcher on chrome, updated titles
945 945 - admin menu accessible from options menu on repository view
946 946 - permissions cached queries
947 947
948 948 1.0.0rc4 (**2010-10-12**)
949 949 --------------------------
950 950
951 951 - fixed python2.5 missing simplejson imports (thanks to Jens BΓ€ckman)
952 952 - removed cache_manager settings from sqlalchemy meta
953 953 - added sqlalchemy cache settings to ini files
954 954 - validated password length and added second try of failure on paster setup-app
955 955 - fixed setup database destroy prompt even when there was no db
956 956
957 957
958 958 1.0.0rc3 (**2010-10-11**)
959 959 -------------------------
960 960
961 961 - fixed i18n during installation.
962 962
963 963 1.0.0rc2 (**2010-10-11**)
964 964 -------------------------
965 965
966 966 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
967 967 occure. After vcs is fixed it'll be put back again.
968 968 - templating/css rewrites, optimized css.
@@ -1,648 +1,648 b''
1 1 """
2 2 Routes configuration
3 3
4 4 The more specific and detailed routes should be defined first so they
5 5 may take precedent over the more generic routes. For more information
6 6 refer to the routes manual at http://routes.groovie.org/docs/
7 7 """
8 8 from __future__ import with_statement
9 9 from routes import Mapper
10 10
11 11 # prefix for non repository related links needs to be prefixed with `/`
12 12 ADMIN_PREFIX = '/_admin'
13 13
14 14
15 15 def make_map(config):
16 16 """Create, configure and return the routes Mapper"""
17 17 rmap = Mapper(directory=config['pylons.paths']['controllers'],
18 18 always_scan=config['debug'])
19 19 rmap.minimization = False
20 20 rmap.explicit = False
21 21
22 22 from rhodecode.lib.utils import is_valid_repo
23 23 from rhodecode.lib.utils import is_valid_repos_group
24 24
25 25 def check_repo(environ, match_dict):
26 26 """
27 27 check for valid repository for proper 404 handling
28 28
29 29 :param environ:
30 30 :param match_dict:
31 31 """
32 32 from rhodecode.model.db import Repository
33 33 repo_name = match_dict.get('repo_name')
34 34
35 35 if match_dict.get('f_path'):
36 36 #fix for multiple initial slashes that causes errors
37 37 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
38 38
39 39 try:
40 40 by_id = repo_name.split('_')
41 41 if len(by_id) == 2 and by_id[1].isdigit() and by_id[0] == '':
42 42 repo_name = Repository.get(by_id[1]).repo_name
43 43 match_dict['repo_name'] = repo_name
44 44 except:
45 45 pass
46 46
47 47 return is_valid_repo(repo_name, config['base_path'])
48 48
49 49 def check_group(environ, match_dict):
50 50 """
51 check for valid repositories group for proper 404 handling
51 check for valid repository group for proper 404 handling
52 52
53 53 :param environ:
54 54 :param match_dict:
55 55 """
56 56 repos_group_name = match_dict.get('group_name')
57 57 return is_valid_repos_group(repos_group_name, config['base_path'])
58 58
59 59 def check_int(environ, match_dict):
60 60 return match_dict.get('id').isdigit()
61 61
62 62 # The ErrorController route (handles 404/500 error pages); it should
63 63 # likely stay at the top, ensuring it can always be resolved
64 64 rmap.connect('/error/{action}', controller='error')
65 65 rmap.connect('/error/{action}/{id}', controller='error')
66 66
67 67 #==========================================================================
68 68 # CUSTOM ROUTES HERE
69 69 #==========================================================================
70 70
71 71 #MAIN PAGE
72 72 rmap.connect('home', '/', controller='home', action='index')
73 73 rmap.connect('repo_switcher', '/repos', controller='home',
74 74 action='repo_switcher')
75 75 rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*?}',
76 76 controller='home', action='branch_tag_switcher')
77 77 rmap.connect('bugtracker',
78 78 "http://bitbucket.org/marcinkuzminski/rhodecode/issues",
79 79 _static=True)
80 80 rmap.connect('rst_help',
81 81 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
82 82 _static=True)
83 83 rmap.connect('rhodecode_official', "http://rhodecode.org", _static=True)
84 84
85 85 #ADMIN REPOSITORY REST ROUTES
86 86 with rmap.submapper(path_prefix=ADMIN_PREFIX,
87 87 controller='admin/repos') as m:
88 88 m.connect("repos", "/repos",
89 89 action="create", conditions=dict(method=["POST"]))
90 90 m.connect("repos", "/repos",
91 91 action="index", conditions=dict(method=["GET"]))
92 92 m.connect("formatted_repos", "/repos.{format}",
93 93 action="index",
94 94 conditions=dict(method=["GET"]))
95 95 m.connect("new_repo", "/repos/new",
96 96 action="new", conditions=dict(method=["GET"]))
97 97 m.connect("formatted_new_repo", "/repos/new.{format}",
98 98 action="new", conditions=dict(method=["GET"]))
99 99 m.connect("/repos/{repo_name:.*?}",
100 100 action="update", conditions=dict(method=["PUT"],
101 101 function=check_repo))
102 102 m.connect("/repos/{repo_name:.*?}",
103 103 action="delete", conditions=dict(method=["DELETE"],
104 104 function=check_repo))
105 105 # no longer used:
106 106 m.connect("edit_repo_admin", "/repos/{repo_name:.*?}/edit",
107 107 action="edit", conditions=dict(method=["GET"],
108 108 function=check_repo))
109 109 m.connect("formatted_edit_repo", "/repos/{repo_name:.*?}.{format}/edit",
110 110 action="edit", conditions=dict(method=["GET"],
111 111 function=check_repo))
112 112 m.connect("repo", "/repos/{repo_name:.*?}",
113 113 action="show", conditions=dict(method=["GET"],
114 114 function=check_repo))
115 115 m.connect("formatted_repo", "/repos/{repo_name:.*?}.{format}",
116 116 action="show", conditions=dict(method=["GET"],
117 117 function=check_repo))
118 118 #ajax delete repo perm user
119 119 m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*?}",
120 120 action="delete_perm_user",
121 121 conditions=dict(method=["DELETE"], function=check_repo))
122 122
123 123 #ajax delete repo perm users_group
124 124 m.connect('delete_repo_users_group',
125 125 "/repos_delete_users_group/{repo_name:.*?}",
126 126 action="delete_perm_users_group",
127 127 conditions=dict(method=["DELETE"], function=check_repo))
128 128
129 129 #settings actions
130 130 m.connect('repo_stats', "/repos_stats/{repo_name:.*?}",
131 131 action="repo_stats", conditions=dict(method=["DELETE"],
132 132 function=check_repo))
133 133 m.connect('repo_cache', "/repos_cache/{repo_name:.*?}",
134 134 action="repo_cache", conditions=dict(method=["DELETE"],
135 135 function=check_repo))
136 136 m.connect('repo_public_journal', "/repos_public_journal/{repo_name:.*?}",
137 137 action="repo_public_journal", conditions=dict(method=["PUT"],
138 138 function=check_repo))
139 139 m.connect('repo_pull', "/repo_pull/{repo_name:.*?}",
140 140 action="repo_pull", conditions=dict(method=["PUT"],
141 141 function=check_repo))
142 142 m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*?}",
143 143 action="repo_as_fork", conditions=dict(method=["PUT"],
144 144 function=check_repo))
145 145 m.connect('repo_locking', "/repo_locking/{repo_name:.*?}",
146 146 action="repo_locking", conditions=dict(method=["PUT"],
147 147 function=check_repo))
148 148 #repo fields
149 149 m.connect('create_repo_fields', "/repo_fields/{repo_name:.*?}/new",
150 150 action="create_repo_field", conditions=dict(method=["PUT"],
151 151 function=check_repo))
152 152
153 153 m.connect('delete_repo_fields', "/repo_fields/{repo_name:.*?}/{field_id}",
154 154 action="delete_repo_field", conditions=dict(method=["DELETE"],
155 155 function=check_repo))
156 156
157 157 with rmap.submapper(path_prefix=ADMIN_PREFIX,
158 158 controller='admin/repos_groups') as m:
159 159 m.connect("repos_groups", "/repos_groups",
160 160 action="create", conditions=dict(method=["POST"]))
161 161 m.connect("repos_groups", "/repos_groups",
162 162 action="index", conditions=dict(method=["GET"]))
163 163 m.connect("formatted_repos_groups", "/repos_groups.{format}",
164 164 action="index", conditions=dict(method=["GET"]))
165 165 m.connect("new_repos_group", "/repos_groups/new",
166 166 action="new", conditions=dict(method=["GET"]))
167 167 m.connect("formatted_new_repos_group", "/repos_groups/new.{format}",
168 168 action="new", conditions=dict(method=["GET"]))
169 169 m.connect("update_repos_group", "/repos_groups/{group_name:.*?}",
170 170 action="update", conditions=dict(method=["PUT"],
171 171 function=check_group))
172 172 m.connect("delete_repos_group", "/repos_groups/{group_name:.*?}",
173 173 action="delete", conditions=dict(method=["DELETE"],
174 174 function=check_group))
175 175 m.connect("edit_repos_group", "/repos_groups/{group_name:.*?}/edit",
176 176 action="edit", conditions=dict(method=["GET"],))
177 177 m.connect("formatted_edit_repos_group",
178 178 "/repos_groups/{group_name:.*?}.{format}/edit",
179 179 action="edit", conditions=dict(method=["GET"],
180 180 function=check_group))
181 181 m.connect("repos_group", "/repos_groups/{group_name:.*?}",
182 182 action="show", conditions=dict(method=["GET"],
183 183 function=check_group))
184 184 m.connect("formatted_repos_group", "/repos_groups/{group_name:.*?}.{format}",
185 185 action="show", conditions=dict(method=["GET"],
186 186 function=check_group))
187 187 # ajax delete repos group perm user
188 188 m.connect('delete_repos_group_user_perm',
189 189 "/delete_repos_group_user_perm/{group_name:.*?}",
190 190 action="delete_repos_group_user_perm",
191 191 conditions=dict(method=["DELETE"], function=check_group))
192 192
193 193 # ajax delete repos group perm users_group
194 194 m.connect('delete_repos_group_users_group_perm',
195 195 "/delete_repos_group_users_group_perm/{group_name:.*?}",
196 196 action="delete_repos_group_users_group_perm",
197 197 conditions=dict(method=["DELETE"], function=check_group))
198 198
199 199 #ADMIN USER REST ROUTES
200 200 with rmap.submapper(path_prefix=ADMIN_PREFIX,
201 201 controller='admin/users') as m:
202 202 m.connect("users", "/users",
203 203 action="create", conditions=dict(method=["POST"]))
204 204 m.connect("users", "/users",
205 205 action="index", conditions=dict(method=["GET"]))
206 206 m.connect("formatted_users", "/users.{format}",
207 207 action="index", conditions=dict(method=["GET"]))
208 208 m.connect("new_user", "/users/new",
209 209 action="new", conditions=dict(method=["GET"]))
210 210 m.connect("formatted_new_user", "/users/new.{format}",
211 211 action="new", conditions=dict(method=["GET"]))
212 212 m.connect("update_user", "/users/{id}",
213 213 action="update", conditions=dict(method=["PUT"]))
214 214 m.connect("delete_user", "/users/{id}",
215 215 action="delete", conditions=dict(method=["DELETE"]))
216 216 m.connect("edit_user", "/users/{id}/edit",
217 217 action="edit", conditions=dict(method=["GET"]))
218 218 m.connect("formatted_edit_user",
219 219 "/users/{id}.{format}/edit",
220 220 action="edit", conditions=dict(method=["GET"]))
221 221 m.connect("user", "/users/{id}",
222 222 action="show", conditions=dict(method=["GET"]))
223 223 m.connect("formatted_user", "/users/{id}.{format}",
224 224 action="show", conditions=dict(method=["GET"]))
225 225
226 226 #EXTRAS USER ROUTES
227 227 m.connect("user_perm", "/users_perm/{id}",
228 228 action="update_perm", conditions=dict(method=["PUT"]))
229 229 m.connect("user_emails", "/users_emails/{id}",
230 230 action="add_email", conditions=dict(method=["PUT"]))
231 231 m.connect("user_emails_delete", "/users_emails/{id}",
232 232 action="delete_email", conditions=dict(method=["DELETE"]))
233 233 m.connect("user_ips", "/users_ips/{id}",
234 234 action="add_ip", conditions=dict(method=["PUT"]))
235 235 m.connect("user_ips_delete", "/users_ips/{id}",
236 236 action="delete_ip", conditions=dict(method=["DELETE"]))
237 237
238 238 #ADMIN USER GROUPS REST ROUTES
239 239 with rmap.submapper(path_prefix=ADMIN_PREFIX,
240 240 controller='admin/users_groups') as m:
241 241 m.connect("users_groups", "/users_groups",
242 242 action="create", conditions=dict(method=["POST"]))
243 243 m.connect("users_groups", "/users_groups",
244 244 action="index", conditions=dict(method=["GET"]))
245 245 m.connect("formatted_users_groups", "/users_groups.{format}",
246 246 action="index", conditions=dict(method=["GET"]))
247 247 m.connect("new_users_group", "/users_groups/new",
248 248 action="new", conditions=dict(method=["GET"]))
249 249 m.connect("formatted_new_users_group", "/users_groups/new.{format}",
250 250 action="new", conditions=dict(method=["GET"]))
251 251 m.connect("update_users_group", "/users_groups/{id}",
252 252 action="update", conditions=dict(method=["PUT"]))
253 253 m.connect("delete_users_group", "/users_groups/{id}",
254 254 action="delete", conditions=dict(method=["DELETE"]))
255 255 m.connect("edit_users_group", "/users_groups/{id}/edit",
256 256 action="edit", conditions=dict(method=["GET"]))
257 257 m.connect("formatted_edit_users_group",
258 258 "/users_groups/{id}.{format}/edit",
259 259 action="edit", conditions=dict(method=["GET"]))
260 260 m.connect("users_group", "/users_groups/{id}",
261 261 action="show", conditions=dict(method=["GET"]))
262 262 m.connect("formatted_users_group", "/users_groups/{id}.{format}",
263 263 action="show", conditions=dict(method=["GET"]))
264 264
265 265 #EXTRAS USER ROUTES
266 266 m.connect("users_group_perm", "/users_groups_perm/{id}",
267 267 action="update_perm", conditions=dict(method=["PUT"]))
268 268
269 269 #ADMIN GROUP REST ROUTES
270 270 rmap.resource('group', 'groups',
271 271 controller='admin/groups', path_prefix=ADMIN_PREFIX)
272 272
273 273 #ADMIN PERMISSIONS REST ROUTES
274 274 rmap.resource('permission', 'permissions',
275 275 controller='admin/permissions', path_prefix=ADMIN_PREFIX)
276 276
277 277 #ADMIN DEFAULTS REST ROUTES
278 278 rmap.resource('default', 'defaults',
279 279 controller='admin/defaults', path_prefix=ADMIN_PREFIX)
280 280
281 281 ##ADMIN LDAP SETTINGS
282 282 rmap.connect('ldap_settings', '%s/ldap' % ADMIN_PREFIX,
283 283 controller='admin/ldap_settings', action='ldap_settings',
284 284 conditions=dict(method=["POST"]))
285 285
286 286 rmap.connect('ldap_home', '%s/ldap' % ADMIN_PREFIX,
287 287 controller='admin/ldap_settings')
288 288
289 289 #ADMIN SETTINGS REST ROUTES
290 290 with rmap.submapper(path_prefix=ADMIN_PREFIX,
291 291 controller='admin/settings') as m:
292 292 m.connect("admin_settings", "/settings",
293 293 action="create", conditions=dict(method=["POST"]))
294 294 m.connect("admin_settings", "/settings",
295 295 action="index", conditions=dict(method=["GET"]))
296 296 m.connect("formatted_admin_settings", "/settings.{format}",
297 297 action="index", conditions=dict(method=["GET"]))
298 298 m.connect("admin_new_setting", "/settings/new",
299 299 action="new", conditions=dict(method=["GET"]))
300 300 m.connect("formatted_admin_new_setting", "/settings/new.{format}",
301 301 action="new", conditions=dict(method=["GET"]))
302 302 m.connect("/settings/{setting_id}",
303 303 action="update", conditions=dict(method=["PUT"]))
304 304 m.connect("/settings/{setting_id}",
305 305 action="delete", conditions=dict(method=["DELETE"]))
306 306 m.connect("admin_edit_setting", "/settings/{setting_id}/edit",
307 307 action="edit", conditions=dict(method=["GET"]))
308 308 m.connect("formatted_admin_edit_setting",
309 309 "/settings/{setting_id}.{format}/edit",
310 310 action="edit", conditions=dict(method=["GET"]))
311 311 m.connect("admin_setting", "/settings/{setting_id}",
312 312 action="show", conditions=dict(method=["GET"]))
313 313 m.connect("formatted_admin_setting", "/settings/{setting_id}.{format}",
314 314 action="show", conditions=dict(method=["GET"]))
315 315 m.connect("admin_settings_my_account", "/my_account",
316 316 action="my_account", conditions=dict(method=["GET"]))
317 317 m.connect("admin_settings_my_account_update", "/my_account_update",
318 318 action="my_account_update", conditions=dict(method=["PUT"]))
319 319 m.connect("admin_settings_create_repository", "/create_repository",
320 320 action="create_repository", conditions=dict(method=["GET"]))
321 321 m.connect("admin_settings_my_repos", "/my_account/repos",
322 322 action="my_account_my_repos", conditions=dict(method=["GET"]))
323 323 m.connect("admin_settings_my_pullrequests", "/my_account/pull_requests",
324 324 action="my_account_my_pullrequests", conditions=dict(method=["GET"]))
325 325
326 326 #NOTIFICATION REST ROUTES
327 327 with rmap.submapper(path_prefix=ADMIN_PREFIX,
328 328 controller='admin/notifications') as m:
329 329 m.connect("notifications", "/notifications",
330 330 action="create", conditions=dict(method=["POST"]))
331 331 m.connect("notifications", "/notifications",
332 332 action="index", conditions=dict(method=["GET"]))
333 333 m.connect("notifications_mark_all_read", "/notifications/mark_all_read",
334 334 action="mark_all_read", conditions=dict(method=["GET"]))
335 335 m.connect("formatted_notifications", "/notifications.{format}",
336 336 action="index", conditions=dict(method=["GET"]))
337 337 m.connect("new_notification", "/notifications/new",
338 338 action="new", conditions=dict(method=["GET"]))
339 339 m.connect("formatted_new_notification", "/notifications/new.{format}",
340 340 action="new", conditions=dict(method=["GET"]))
341 341 m.connect("/notification/{notification_id}",
342 342 action="update", conditions=dict(method=["PUT"]))
343 343 m.connect("/notification/{notification_id}",
344 344 action="delete", conditions=dict(method=["DELETE"]))
345 345 m.connect("edit_notification", "/notification/{notification_id}/edit",
346 346 action="edit", conditions=dict(method=["GET"]))
347 347 m.connect("formatted_edit_notification",
348 348 "/notification/{notification_id}.{format}/edit",
349 349 action="edit", conditions=dict(method=["GET"]))
350 350 m.connect("notification", "/notification/{notification_id}",
351 351 action="show", conditions=dict(method=["GET"]))
352 352 m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
353 353 action="show", conditions=dict(method=["GET"]))
354 354
355 355 #ADMIN MAIN PAGES
356 356 with rmap.submapper(path_prefix=ADMIN_PREFIX,
357 357 controller='admin/admin') as m:
358 358 m.connect('admin_home', '', action='index')
359 359 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
360 360 action='add_repo')
361 361
362 362 #==========================================================================
363 363 # API V2
364 364 #==========================================================================
365 365 with rmap.submapper(path_prefix=ADMIN_PREFIX,
366 366 controller='api/api') as m:
367 367 m.connect('api', '/api')
368 368
369 369 #USER JOURNAL
370 370 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
371 371 controller='journal', action='index')
372 372 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
373 373 controller='journal', action='journal_rss')
374 374 rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
375 375 controller='journal', action='journal_atom')
376 376
377 377 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
378 378 controller='journal', action="public_journal")
379 379
380 380 rmap.connect('public_journal_rss', '%s/public_journal/rss' % ADMIN_PREFIX,
381 381 controller='journal', action="public_journal_rss")
382 382
383 383 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
384 384 controller='journal', action="public_journal_rss")
385 385
386 386 rmap.connect('public_journal_atom',
387 387 '%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
388 388 action="public_journal_atom")
389 389
390 390 rmap.connect('public_journal_atom_old',
391 391 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
392 392 action="public_journal_atom")
393 393
394 394 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
395 395 controller='journal', action='toggle_following',
396 396 conditions=dict(method=["POST"]))
397 397
398 398 #SEARCH
399 399 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
400 400 rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
401 401 controller='search',
402 402 conditions=dict(function=check_repo))
403 403 rmap.connect('search_repo', '/{repo_name:.*?}/search',
404 404 controller='search',
405 405 conditions=dict(function=check_repo),
406 406 )
407 407
408 408 #LOGIN/LOGOUT/REGISTER/SIGN IN
409 409 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
410 410 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
411 411 action='logout')
412 412
413 413 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
414 414 action='register')
415 415
416 416 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
417 417 controller='login', action='password_reset')
418 418
419 419 rmap.connect('reset_password_confirmation',
420 420 '%s/password_reset_confirmation' % ADMIN_PREFIX,
421 421 controller='login', action='password_reset_confirmation')
422 422
423 423 #FEEDS
424 424 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
425 425 controller='feed', action='rss',
426 426 conditions=dict(function=check_repo))
427 427
428 428 rmap.connect('atom_feed_home', '/{repo_name:.*?}/feed/atom',
429 429 controller='feed', action='atom',
430 430 conditions=dict(function=check_repo))
431 431
432 432 #==========================================================================
433 433 # REPOSITORY ROUTES
434 434 #==========================================================================
435 435 rmap.connect('summary_home', '/{repo_name:.*?}',
436 436 controller='summary',
437 437 conditions=dict(function=check_repo))
438 438
439 439 rmap.connect('repo_size', '/{repo_name:.*?}/repo_size',
440 440 controller='summary', action='repo_size',
441 441 conditions=dict(function=check_repo))
442 442
443 443 rmap.connect('repos_group_home', '/{group_name:.*}',
444 444 controller='admin/repos_groups', action="show_by_name",
445 445 conditions=dict(function=check_group))
446 446
447 447 rmap.connect('changeset_home', '/{repo_name:.*?}/changeset/{revision}',
448 448 controller='changeset', revision='tip',
449 449 conditions=dict(function=check_repo))
450 450
451 451 rmap.connect("edit_repo", "/{repo_name:.*?}/edit",
452 452 controller='admin/repos', action="edit",
453 453 conditions=dict(method=["GET"], function=check_repo)
454 454 )
455 455
456 456 #still working url for backward compat.
457 457 rmap.connect('raw_changeset_home_depraced',
458 458 '/{repo_name:.*?}/raw-changeset/{revision}',
459 459 controller='changeset', action='changeset_raw',
460 460 revision='tip', conditions=dict(function=check_repo))
461 461
462 462 ## new URLs
463 463 rmap.connect('changeset_raw_home',
464 464 '/{repo_name:.*?}/changeset-diff/{revision}',
465 465 controller='changeset', action='changeset_raw',
466 466 revision='tip', conditions=dict(function=check_repo))
467 467
468 468 rmap.connect('changeset_patch_home',
469 469 '/{repo_name:.*?}/changeset-patch/{revision}',
470 470 controller='changeset', action='changeset_patch',
471 471 revision='tip', conditions=dict(function=check_repo))
472 472
473 473 rmap.connect('changeset_download_home',
474 474 '/{repo_name:.*?}/changeset-download/{revision}',
475 475 controller='changeset', action='changeset_download',
476 476 revision='tip', conditions=dict(function=check_repo))
477 477
478 478 rmap.connect('changeset_comment',
479 479 '/{repo_name:.*?}/changeset/{revision}/comment',
480 480 controller='changeset', revision='tip', action='comment',
481 481 conditions=dict(function=check_repo))
482 482
483 483 rmap.connect('changeset_comment_delete',
484 484 '/{repo_name:.*?}/changeset/comment/{comment_id}/delete',
485 485 controller='changeset', action='delete_comment',
486 486 conditions=dict(function=check_repo, method=["DELETE"]))
487 487
488 488 rmap.connect('changeset_info', '/changeset_info/{repo_name:.*?}/{revision}',
489 489 controller='changeset', action='changeset_info')
490 490
491 491 rmap.connect('compare_url',
492 492 '/{repo_name:.*?}/compare/{org_ref_type}@{org_ref:.*?}...{other_ref_type}@{other_ref:.*?}',
493 493 controller='compare', action='index',
494 494 conditions=dict(function=check_repo),
495 495 requirements=dict(
496 496 org_ref_type='(branch|book|tag|rev|__other_ref_type__)',
497 497 other_ref_type='(branch|book|tag|rev|__org_ref_type__)')
498 498 )
499 499
500 500 rmap.connect('pullrequest_home',
501 501 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
502 502 action='index', conditions=dict(function=check_repo,
503 503 method=["GET"]))
504 504
505 505 rmap.connect('pullrequest',
506 506 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
507 507 action='create', conditions=dict(function=check_repo,
508 508 method=["POST"]))
509 509
510 510 rmap.connect('pullrequest_show',
511 511 '/{repo_name:.*?}/pull-request/{pull_request_id}',
512 512 controller='pullrequests',
513 513 action='show', conditions=dict(function=check_repo,
514 514 method=["GET"]))
515 515 rmap.connect('pullrequest_update',
516 516 '/{repo_name:.*?}/pull-request/{pull_request_id}',
517 517 controller='pullrequests',
518 518 action='update', conditions=dict(function=check_repo,
519 519 method=["PUT"]))
520 520 rmap.connect('pullrequest_delete',
521 521 '/{repo_name:.*?}/pull-request/{pull_request_id}',
522 522 controller='pullrequests',
523 523 action='delete', conditions=dict(function=check_repo,
524 524 method=["DELETE"]))
525 525
526 526 rmap.connect('pullrequest_show_all',
527 527 '/{repo_name:.*?}/pull-request',
528 528 controller='pullrequests',
529 529 action='show_all', conditions=dict(function=check_repo,
530 530 method=["GET"]))
531 531
532 532 rmap.connect('pullrequest_comment',
533 533 '/{repo_name:.*?}/pull-request-comment/{pull_request_id}',
534 534 controller='pullrequests',
535 535 action='comment', conditions=dict(function=check_repo,
536 536 method=["POST"]))
537 537
538 538 rmap.connect('pullrequest_comment_delete',
539 539 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
540 540 controller='pullrequests', action='delete_comment',
541 541 conditions=dict(function=check_repo, method=["DELETE"]))
542 542
543 543 rmap.connect('summary_home_summary', '/{repo_name:.*?}/summary',
544 544 controller='summary', conditions=dict(function=check_repo))
545 545
546 546 rmap.connect('shortlog_home', '/{repo_name:.*?}/shortlog',
547 547 controller='shortlog', conditions=dict(function=check_repo))
548 548
549 549 rmap.connect('shortlog_file_home', '/{repo_name:.*?}/shortlog/{revision}/{f_path:.*}',
550 550 controller='shortlog', f_path=None,
551 551 conditions=dict(function=check_repo))
552 552
553 553 rmap.connect('branches_home', '/{repo_name:.*?}/branches',
554 554 controller='branches', conditions=dict(function=check_repo))
555 555
556 556 rmap.connect('tags_home', '/{repo_name:.*?}/tags',
557 557 controller='tags', conditions=dict(function=check_repo))
558 558
559 559 rmap.connect('bookmarks_home', '/{repo_name:.*?}/bookmarks',
560 560 controller='bookmarks', conditions=dict(function=check_repo))
561 561
562 562 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
563 563 controller='changelog', conditions=dict(function=check_repo))
564 564
565 565 rmap.connect('changelog_details', '/{repo_name:.*?}/changelog_details/{cs}',
566 566 controller='changelog', action='changelog_details',
567 567 conditions=dict(function=check_repo))
568 568
569 569 rmap.connect('files_home', '/{repo_name:.*?}/files/{revision}/{f_path:.*}',
570 570 controller='files', revision='tip', f_path='',
571 571 conditions=dict(function=check_repo))
572 572
573 573 rmap.connect('files_history_home',
574 574 '/{repo_name:.*?}/history/{revision}/{f_path:.*}',
575 575 controller='files', action='history', revision='tip', f_path='',
576 576 conditions=dict(function=check_repo))
577 577
578 578 rmap.connect('files_diff_home', '/{repo_name:.*?}/diff/{f_path:.*}',
579 579 controller='files', action='diff', revision='tip', f_path='',
580 580 conditions=dict(function=check_repo))
581 581
582 582 rmap.connect('files_rawfile_home',
583 583 '/{repo_name:.*?}/rawfile/{revision}/{f_path:.*}',
584 584 controller='files', action='rawfile', revision='tip',
585 585 f_path='', conditions=dict(function=check_repo))
586 586
587 587 rmap.connect('files_raw_home',
588 588 '/{repo_name:.*?}/raw/{revision}/{f_path:.*}',
589 589 controller='files', action='raw', revision='tip', f_path='',
590 590 conditions=dict(function=check_repo))
591 591
592 592 rmap.connect('files_annotate_home',
593 593 '/{repo_name:.*?}/annotate/{revision}/{f_path:.*}',
594 594 controller='files', action='index', revision='tip',
595 595 f_path='', annotate=True, conditions=dict(function=check_repo))
596 596
597 597 rmap.connect('files_edit_home',
598 598 '/{repo_name:.*?}/edit/{revision}/{f_path:.*}',
599 599 controller='files', action='edit', revision='tip',
600 600 f_path='', conditions=dict(function=check_repo))
601 601
602 602 rmap.connect('files_add_home',
603 603 '/{repo_name:.*?}/add/{revision}/{f_path:.*}',
604 604 controller='files', action='add', revision='tip',
605 605 f_path='', conditions=dict(function=check_repo))
606 606
607 607 rmap.connect('files_archive_home', '/{repo_name:.*?}/archive/{fname}',
608 608 controller='files', action='archivefile',
609 609 conditions=dict(function=check_repo))
610 610
611 611 rmap.connect('files_nodelist_home',
612 612 '/{repo_name:.*?}/nodelist/{revision}/{f_path:.*}',
613 613 controller='files', action='nodelist',
614 614 conditions=dict(function=check_repo))
615 615
616 616 rmap.connect('repo_settings_delete', '/{repo_name:.*?}/settings',
617 617 controller='settings', action="delete",
618 618 conditions=dict(method=["DELETE"], function=check_repo))
619 619
620 620 rmap.connect('repo_settings_update', '/{repo_name:.*?}/settings',
621 621 controller='settings', action="update",
622 622 conditions=dict(method=["PUT"], function=check_repo))
623 623
624 624 rmap.connect('repo_settings_home', '/{repo_name:.*?}/settings',
625 625 controller='settings', action='index',
626 626 conditions=dict(function=check_repo))
627 627
628 628 rmap.connect('toggle_locking', "/{repo_name:.*?}/locking_toggle",
629 629 controller='settings', action="toggle_locking",
630 630 conditions=dict(method=["GET"], function=check_repo))
631 631
632 632 rmap.connect('repo_fork_create_home', '/{repo_name:.*?}/fork',
633 633 controller='forks', action='fork_create',
634 634 conditions=dict(function=check_repo, method=["POST"]))
635 635
636 636 rmap.connect('repo_fork_home', '/{repo_name:.*?}/fork',
637 637 controller='forks', action='fork',
638 638 conditions=dict(function=check_repo))
639 639
640 640 rmap.connect('repo_forks_home', '/{repo_name:.*?}/forks',
641 641 controller='forks', action='forks',
642 642 conditions=dict(function=check_repo))
643 643
644 644 rmap.connect('repo_followers_home', '/{repo_name:.*?}/followers',
645 645 controller='followers', action='followers',
646 646 conditions=dict(function=check_repo))
647 647
648 648 return rmap
@@ -1,2042 +1,2042 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 import hashlib
31 31 import time
32 32 from collections import defaultdict
33 33
34 34 from sqlalchemy import *
35 35 from sqlalchemy.ext.hybrid import hybrid_property
36 36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 37 from sqlalchemy.exc import DatabaseError
38 38 from beaker.cache import cache_region, region_invalidate
39 39 from webob.exc import HTTPNotFound
40 40
41 41 from pylons.i18n.translation import lazy_ugettext as _
42 42
43 43 from rhodecode.lib.vcs import get_backend
44 44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 45 from rhodecode.lib.vcs.exceptions import VCSError
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47
48 48 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
49 49 safe_unicode, remove_suffix, remove_prefix
50 50 from rhodecode.lib.compat import json
51 51 from rhodecode.lib.caching_query import FromCache
52 52
53 53 from rhodecode.model.meta import Base, Session
54 54
55 55 URL_SEP = '/'
56 56 log = logging.getLogger(__name__)
57 57
58 58 #==============================================================================
59 59 # BASE CLASSES
60 60 #==============================================================================
61 61
62 62 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
63 63
64 64
65 65 class BaseModel(object):
66 66 """
67 67 Base Model for all classess
68 68 """
69 69
70 70 @classmethod
71 71 def _get_keys(cls):
72 72 """return column names for this model """
73 73 return class_mapper(cls).c.keys()
74 74
75 75 def get_dict(self):
76 76 """
77 77 return dict with keys and values corresponding
78 78 to this model data """
79 79
80 80 d = {}
81 81 for k in self._get_keys():
82 82 d[k] = getattr(self, k)
83 83
84 84 # also use __json__() if present to get additional fields
85 85 _json_attr = getattr(self, '__json__', None)
86 86 if _json_attr:
87 87 # update with attributes from __json__
88 88 if callable(_json_attr):
89 89 _json_attr = _json_attr()
90 90 for k, val in _json_attr.iteritems():
91 91 d[k] = val
92 92 return d
93 93
94 94 def get_appstruct(self):
95 95 """return list with keys and values tupples corresponding
96 96 to this model data """
97 97
98 98 l = []
99 99 for k in self._get_keys():
100 100 l.append((k, getattr(self, k),))
101 101 return l
102 102
103 103 def populate_obj(self, populate_dict):
104 104 """populate model with data from given populate_dict"""
105 105
106 106 for k in self._get_keys():
107 107 if k in populate_dict:
108 108 setattr(self, k, populate_dict[k])
109 109
110 110 @classmethod
111 111 def query(cls):
112 112 return Session().query(cls)
113 113
114 114 @classmethod
115 115 def get(cls, id_):
116 116 if id_:
117 117 return cls.query().get(id_)
118 118
119 119 @classmethod
120 120 def get_or_404(cls, id_):
121 121 try:
122 122 id_ = int(id_)
123 123 except (TypeError, ValueError):
124 124 raise HTTPNotFound
125 125
126 126 res = cls.query().get(id_)
127 127 if not res:
128 128 raise HTTPNotFound
129 129 return res
130 130
131 131 @classmethod
132 132 def getAll(cls):
133 133 return cls.query().all()
134 134
135 135 @classmethod
136 136 def delete(cls, id_):
137 137 obj = cls.query().get(id_)
138 138 Session().delete(obj)
139 139
140 140 def __repr__(self):
141 141 if hasattr(self, '__unicode__'):
142 142 # python repr needs to return str
143 143 return safe_str(self.__unicode__())
144 144 return '<DB:%s>' % (self.__class__.__name__)
145 145
146 146
147 147 class RhodeCodeSetting(Base, BaseModel):
148 148 __tablename__ = 'rhodecode_settings'
149 149 __table_args__ = (
150 150 UniqueConstraint('app_settings_name'),
151 151 {'extend_existing': True, 'mysql_engine': 'InnoDB',
152 152 'mysql_charset': 'utf8'}
153 153 )
154 154 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
155 155 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
156 156 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 157
158 158 def __init__(self, k='', v=''):
159 159 self.app_settings_name = k
160 160 self.app_settings_value = v
161 161
162 162 @validates('_app_settings_value')
163 163 def validate_settings_value(self, key, val):
164 164 assert type(val) == unicode
165 165 return val
166 166
167 167 @hybrid_property
168 168 def app_settings_value(self):
169 169 v = self._app_settings_value
170 170 if self.app_settings_name in ["ldap_active",
171 171 "default_repo_enable_statistics",
172 172 "default_repo_enable_locking",
173 173 "default_repo_private",
174 174 "default_repo_enable_downloads"]:
175 175 v = str2bool(v)
176 176 return v
177 177
178 178 @app_settings_value.setter
179 179 def app_settings_value(self, val):
180 180 """
181 181 Setter that will always make sure we use unicode in app_settings_value
182 182
183 183 :param val:
184 184 """
185 185 self._app_settings_value = safe_unicode(val)
186 186
187 187 def __unicode__(self):
188 188 return u"<%s('%s:%s')>" % (
189 189 self.__class__.__name__,
190 190 self.app_settings_name, self.app_settings_value
191 191 )
192 192
193 193 @classmethod
194 194 def get_by_name(cls, key):
195 195 return cls.query()\
196 196 .filter(cls.app_settings_name == key).scalar()
197 197
198 198 @classmethod
199 199 def get_by_name_or_create(cls, key):
200 200 res = cls.get_by_name(key)
201 201 if not res:
202 202 res = cls(key)
203 203 return res
204 204
205 205 @classmethod
206 206 def get_app_settings(cls, cache=False):
207 207
208 208 ret = cls.query()
209 209
210 210 if cache:
211 211 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
212 212
213 213 if not ret:
214 214 raise Exception('Could not get application settings !')
215 215 settings = {}
216 216 for each in ret:
217 217 settings['rhodecode_' + each.app_settings_name] = \
218 218 each.app_settings_value
219 219
220 220 return settings
221 221
222 222 @classmethod
223 223 def get_ldap_settings(cls, cache=False):
224 224 ret = cls.query()\
225 225 .filter(cls.app_settings_name.startswith('ldap_')).all()
226 226 fd = {}
227 227 for row in ret:
228 228 fd.update({row.app_settings_name: row.app_settings_value})
229 229
230 230 return fd
231 231
232 232 @classmethod
233 233 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
234 234 ret = cls.query()\
235 235 .filter(cls.app_settings_name.startswith('default_')).all()
236 236 fd = {}
237 237 for row in ret:
238 238 key = row.app_settings_name
239 239 if strip_prefix:
240 240 key = remove_prefix(key, prefix='default_')
241 241 fd.update({key: row.app_settings_value})
242 242
243 243 return fd
244 244
245 245
246 246 class RhodeCodeUi(Base, BaseModel):
247 247 __tablename__ = 'rhodecode_ui'
248 248 __table_args__ = (
249 249 UniqueConstraint('ui_key'),
250 250 {'extend_existing': True, 'mysql_engine': 'InnoDB',
251 251 'mysql_charset': 'utf8'}
252 252 )
253 253
254 254 HOOK_UPDATE = 'changegroup.update'
255 255 HOOK_REPO_SIZE = 'changegroup.repo_size'
256 256 HOOK_PUSH = 'changegroup.push_logger'
257 257 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
258 258 HOOK_PULL = 'outgoing.pull_logger'
259 259 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
260 260
261 261 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
262 262 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
263 263 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 264 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
265 265 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
266 266
267 267 @classmethod
268 268 def get_by_key(cls, key):
269 269 return cls.query().filter(cls.ui_key == key).scalar()
270 270
271 271 @classmethod
272 272 def get_builtin_hooks(cls):
273 273 q = cls.query()
274 274 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
275 275 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
276 276 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
277 277 return q.all()
278 278
279 279 @classmethod
280 280 def get_custom_hooks(cls):
281 281 q = cls.query()
282 282 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
283 283 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
284 284 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
285 285 q = q.filter(cls.ui_section == 'hooks')
286 286 return q.all()
287 287
288 288 @classmethod
289 289 def get_repos_location(cls):
290 290 return cls.get_by_key('/').ui_value
291 291
292 292 @classmethod
293 293 def create_or_update_hook(cls, key, val):
294 294 new_ui = cls.get_by_key(key) or cls()
295 295 new_ui.ui_section = 'hooks'
296 296 new_ui.ui_active = True
297 297 new_ui.ui_key = key
298 298 new_ui.ui_value = val
299 299
300 300 Session().add(new_ui)
301 301
302 302 def __repr__(self):
303 303 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
304 304 self.ui_value)
305 305
306 306
307 307 class User(Base, BaseModel):
308 308 __tablename__ = 'users'
309 309 __table_args__ = (
310 310 UniqueConstraint('username'), UniqueConstraint('email'),
311 311 Index('u_username_idx', 'username'),
312 312 Index('u_email_idx', 'email'),
313 313 {'extend_existing': True, 'mysql_engine': 'InnoDB',
314 314 'mysql_charset': 'utf8'}
315 315 )
316 316 DEFAULT_USER = 'default'
317 317 DEFAULT_PERMISSIONS = [
318 318 'hg.register.manual_activate', 'hg.create.repository',
319 319 'hg.fork.repository', 'repository.read', 'group.read'
320 320 ]
321 321 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
322 322 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
323 323 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
324 324 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
325 325 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
326 326 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
327 327 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
328 328 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
329 329 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
330 330 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
331 331 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 332 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
333 333
334 334 user_log = relationship('UserLog')
335 335 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
336 336
337 337 repositories = relationship('Repository')
338 338 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
339 339 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
340 340
341 341 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
342 342 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
343 343
344 344 group_member = relationship('UsersGroupMember', cascade='all')
345 345
346 346 notifications = relationship('UserNotification', cascade='all')
347 347 # notifications assigned to this user
348 348 user_created_notifications = relationship('Notification', cascade='all')
349 349 # comments created by this user
350 350 user_comments = relationship('ChangesetComment', cascade='all')
351 351 #extra emails for this user
352 352 user_emails = relationship('UserEmailMap', cascade='all')
353 353
354 354 @hybrid_property
355 355 def email(self):
356 356 return self._email
357 357
358 358 @email.setter
359 359 def email(self, val):
360 360 self._email = val.lower() if val else None
361 361
362 362 @property
363 363 def firstname(self):
364 364 # alias for future
365 365 return self.name
366 366
367 367 @property
368 368 def emails(self):
369 369 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
370 370 return [self.email] + [x.email for x in other]
371 371
372 372 @property
373 373 def ip_addresses(self):
374 374 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
375 375 return [x.ip_addr for x in ret]
376 376
377 377 @property
378 378 def username_and_name(self):
379 379 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
380 380
381 381 @property
382 382 def full_name(self):
383 383 return '%s %s' % (self.firstname, self.lastname)
384 384
385 385 @property
386 386 def full_name_or_username(self):
387 387 return ('%s %s' % (self.firstname, self.lastname)
388 388 if (self.firstname and self.lastname) else self.username)
389 389
390 390 @property
391 391 def full_contact(self):
392 392 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
393 393
394 394 @property
395 395 def short_contact(self):
396 396 return '%s %s' % (self.firstname, self.lastname)
397 397
398 398 @property
399 399 def is_admin(self):
400 400 return self.admin
401 401
402 402 @property
403 403 def AuthUser(self):
404 404 """
405 405 Returns instance of AuthUser for this user
406 406 """
407 407 from rhodecode.lib.auth import AuthUser
408 408 return AuthUser(user_id=self.user_id, api_key=self.api_key,
409 409 username=self.username)
410 410
411 411 def __unicode__(self):
412 412 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
413 413 self.user_id, self.username)
414 414
415 415 @classmethod
416 416 def get_by_username(cls, username, case_insensitive=False, cache=False):
417 417 if case_insensitive:
418 418 q = cls.query().filter(cls.username.ilike(username))
419 419 else:
420 420 q = cls.query().filter(cls.username == username)
421 421
422 422 if cache:
423 423 q = q.options(FromCache(
424 424 "sql_cache_short",
425 425 "get_user_%s" % _hash_key(username)
426 426 )
427 427 )
428 428 return q.scalar()
429 429
430 430 @classmethod
431 431 def get_by_api_key(cls, api_key, cache=False):
432 432 q = cls.query().filter(cls.api_key == api_key)
433 433
434 434 if cache:
435 435 q = q.options(FromCache("sql_cache_short",
436 436 "get_api_key_%s" % api_key))
437 437 return q.scalar()
438 438
439 439 @classmethod
440 440 def get_by_email(cls, email, case_insensitive=False, cache=False):
441 441 if case_insensitive:
442 442 q = cls.query().filter(cls.email.ilike(email))
443 443 else:
444 444 q = cls.query().filter(cls.email == email)
445 445
446 446 if cache:
447 447 q = q.options(FromCache("sql_cache_short",
448 448 "get_email_key_%s" % email))
449 449
450 450 ret = q.scalar()
451 451 if ret is None:
452 452 q = UserEmailMap.query()
453 453 # try fetching in alternate email map
454 454 if case_insensitive:
455 455 q = q.filter(UserEmailMap.email.ilike(email))
456 456 else:
457 457 q = q.filter(UserEmailMap.email == email)
458 458 q = q.options(joinedload(UserEmailMap.user))
459 459 if cache:
460 460 q = q.options(FromCache("sql_cache_short",
461 461 "get_email_map_key_%s" % email))
462 462 ret = getattr(q.scalar(), 'user', None)
463 463
464 464 return ret
465 465
466 466 @classmethod
467 467 def get_from_cs_author(cls, author):
468 468 """
469 469 Tries to get User objects out of commit author string
470 470
471 471 :param author:
472 472 """
473 473 from rhodecode.lib.helpers import email, author_name
474 474 # Valid email in the attribute passed, see if they're in the system
475 475 _email = email(author)
476 476 if _email:
477 477 user = cls.get_by_email(_email, case_insensitive=True)
478 478 if user:
479 479 return user
480 480 # Maybe we can match by username?
481 481 _author = author_name(author)
482 482 user = cls.get_by_username(_author, case_insensitive=True)
483 483 if user:
484 484 return user
485 485
486 486 def update_lastlogin(self):
487 487 """Update user lastlogin"""
488 488 self.last_login = datetime.datetime.now()
489 489 Session().add(self)
490 490 log.debug('updated user %s lastlogin' % self.username)
491 491
492 492 def get_api_data(self):
493 493 """
494 494 Common function for generating user related data for API
495 495 """
496 496 user = self
497 497 data = dict(
498 498 user_id=user.user_id,
499 499 username=user.username,
500 500 firstname=user.name,
501 501 lastname=user.lastname,
502 502 email=user.email,
503 503 emails=user.emails,
504 504 api_key=user.api_key,
505 505 active=user.active,
506 506 admin=user.admin,
507 507 ldap_dn=user.ldap_dn,
508 508 last_login=user.last_login,
509 509 ip_addresses=user.ip_addresses
510 510 )
511 511 return data
512 512
513 513 def __json__(self):
514 514 data = dict(
515 515 full_name=self.full_name,
516 516 full_name_or_username=self.full_name_or_username,
517 517 short_contact=self.short_contact,
518 518 full_contact=self.full_contact
519 519 )
520 520 data.update(self.get_api_data())
521 521 return data
522 522
523 523
524 524 class UserEmailMap(Base, BaseModel):
525 525 __tablename__ = 'user_email_map'
526 526 __table_args__ = (
527 527 Index('uem_email_idx', 'email'),
528 528 UniqueConstraint('email'),
529 529 {'extend_existing': True, 'mysql_engine': 'InnoDB',
530 530 'mysql_charset': 'utf8'}
531 531 )
532 532 __mapper_args__ = {}
533 533
534 534 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
535 535 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
536 536 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
537 537 user = relationship('User', lazy='joined')
538 538
539 539 @validates('_email')
540 540 def validate_email(self, key, email):
541 541 # check if this email is not main one
542 542 main_email = Session().query(User).filter(User.email == email).scalar()
543 543 if main_email is not None:
544 544 raise AttributeError('email %s is present is user table' % email)
545 545 return email
546 546
547 547 @hybrid_property
548 548 def email(self):
549 549 return self._email
550 550
551 551 @email.setter
552 552 def email(self, val):
553 553 self._email = val.lower() if val else None
554 554
555 555
556 556 class UserIpMap(Base, BaseModel):
557 557 __tablename__ = 'user_ip_map'
558 558 __table_args__ = (
559 559 UniqueConstraint('user_id', 'ip_addr'),
560 560 {'extend_existing': True, 'mysql_engine': 'InnoDB',
561 561 'mysql_charset': 'utf8'}
562 562 )
563 563 __mapper_args__ = {}
564 564
565 565 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
566 566 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
567 567 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
568 568 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
569 569 user = relationship('User', lazy='joined')
570 570
571 571 @classmethod
572 572 def _get_ip_range(cls, ip_addr):
573 573 from rhodecode.lib import ipaddr
574 574 net = ipaddr.IPNetwork(address=ip_addr)
575 575 return [str(net.network), str(net.broadcast)]
576 576
577 577 def __json__(self):
578 578 return dict(
579 579 ip_addr=self.ip_addr,
580 580 ip_range=self._get_ip_range(self.ip_addr)
581 581 )
582 582
583 583
584 584 class UserLog(Base, BaseModel):
585 585 __tablename__ = 'user_logs'
586 586 __table_args__ = (
587 587 {'extend_existing': True, 'mysql_engine': 'InnoDB',
588 588 'mysql_charset': 'utf8'},
589 589 )
590 590 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 591 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
592 592 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
593 593 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
594 594 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
595 595 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
596 596 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
597 597 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
598 598
599 599 @property
600 600 def action_as_day(self):
601 601 return datetime.date(*self.action_date.timetuple()[:3])
602 602
603 603 user = relationship('User')
604 604 repository = relationship('Repository', cascade='')
605 605
606 606
607 607 class UsersGroup(Base, BaseModel):
608 608 __tablename__ = 'users_groups'
609 609 __table_args__ = (
610 610 {'extend_existing': True, 'mysql_engine': 'InnoDB',
611 611 'mysql_charset': 'utf8'},
612 612 )
613 613
614 614 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
615 615 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
616 616 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
617 617 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
618 618
619 619 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
620 620 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
621 621 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
622 622
623 623 def __unicode__(self):
624 624 return u'<userGroup(%s)>' % (self.users_group_name)
625 625
626 626 @classmethod
627 627 def get_by_group_name(cls, group_name, cache=False,
628 628 case_insensitive=False):
629 629 if case_insensitive:
630 630 q = cls.query().filter(cls.users_group_name.ilike(group_name))
631 631 else:
632 632 q = cls.query().filter(cls.users_group_name == group_name)
633 633 if cache:
634 634 q = q.options(FromCache(
635 635 "sql_cache_short",
636 636 "get_user_%s" % _hash_key(group_name)
637 637 )
638 638 )
639 639 return q.scalar()
640 640
641 641 @classmethod
642 642 def get(cls, users_group_id, cache=False):
643 643 users_group = cls.query()
644 644 if cache:
645 645 users_group = users_group.options(FromCache("sql_cache_short",
646 646 "get_users_group_%s" % users_group_id))
647 647 return users_group.get(users_group_id)
648 648
649 649 def get_api_data(self):
650 650 users_group = self
651 651
652 652 data = dict(
653 653 users_group_id=users_group.users_group_id,
654 654 group_name=users_group.users_group_name,
655 655 active=users_group.users_group_active,
656 656 )
657 657
658 658 return data
659 659
660 660
661 661 class UsersGroupMember(Base, BaseModel):
662 662 __tablename__ = 'users_groups_members'
663 663 __table_args__ = (
664 664 {'extend_existing': True, 'mysql_engine': 'InnoDB',
665 665 'mysql_charset': 'utf8'},
666 666 )
667 667
668 668 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
669 669 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
670 670 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
671 671
672 672 user = relationship('User', lazy='joined')
673 673 users_group = relationship('UsersGroup')
674 674
675 675 def __init__(self, gr_id='', u_id=''):
676 676 self.users_group_id = gr_id
677 677 self.user_id = u_id
678 678
679 679
680 680 class RepositoryField(Base, BaseModel):
681 681 __tablename__ = 'repositories_fields'
682 682 __table_args__ = (
683 683 UniqueConstraint('repository_id', 'field_key'), # no-multi field
684 684 {'extend_existing': True, 'mysql_engine': 'InnoDB',
685 685 'mysql_charset': 'utf8'},
686 686 )
687 687 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
688 688
689 689 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
690 690 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
691 691 field_key = Column("field_key", String(250, convert_unicode=False, assert_unicode=None))
692 692 field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
693 693 field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
694 694 field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
695 695 field_type = Column("field_type", String(256), nullable=False, unique=None)
696 696 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
697 697
698 698 repository = relationship('Repository')
699 699
700 700 @property
701 701 def field_key_prefixed(self):
702 702 return 'ex_%s' % self.field_key
703 703
704 704 @classmethod
705 705 def un_prefix_key(cls, key):
706 706 if key.startswith(cls.PREFIX):
707 707 return key[len(cls.PREFIX):]
708 708 return key
709 709
710 710 @classmethod
711 711 def get_by_key_name(cls, key, repo):
712 712 row = cls.query()\
713 713 .filter(cls.repository == repo)\
714 714 .filter(cls.field_key == key).scalar()
715 715 return row
716 716
717 717
718 718 class Repository(Base, BaseModel):
719 719 __tablename__ = 'repositories'
720 720 __table_args__ = (
721 721 UniqueConstraint('repo_name'),
722 722 Index('r_repo_name_idx', 'repo_name'),
723 723 {'extend_existing': True, 'mysql_engine': 'InnoDB',
724 724 'mysql_charset': 'utf8'},
725 725 )
726 726
727 727 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
728 728 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
729 729 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
730 730 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
731 731 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
732 732 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
733 733 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
734 734 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
735 735 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
736 736 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
737 737 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
738 738 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
739 739 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
740 740 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
741 741 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
742 742
743 743 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
744 744 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
745 745
746 746 user = relationship('User')
747 747 fork = relationship('Repository', remote_side=repo_id)
748 748 group = relationship('RepoGroup')
749 749 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
750 750 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
751 751 stats = relationship('Statistics', cascade='all', uselist=False)
752 752
753 753 followers = relationship('UserFollowing',
754 754 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
755 755 cascade='all')
756 756 extra_fields = relationship('RepositoryField',
757 757 cascade="all, delete, delete-orphan")
758 758
759 759 logs = relationship('UserLog')
760 760 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
761 761
762 762 pull_requests_org = relationship('PullRequest',
763 763 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
764 764 cascade="all, delete, delete-orphan")
765 765
766 766 pull_requests_other = relationship('PullRequest',
767 767 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
768 768 cascade="all, delete, delete-orphan")
769 769
770 770 def __unicode__(self):
771 771 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
772 772 self.repo_name)
773 773
774 774 @hybrid_property
775 775 def locked(self):
776 776 # always should return [user_id, timelocked]
777 777 if self._locked:
778 778 _lock_info = self._locked.split(':')
779 779 return int(_lock_info[0]), _lock_info[1]
780 780 return [None, None]
781 781
782 782 @locked.setter
783 783 def locked(self, val):
784 784 if val and isinstance(val, (list, tuple)):
785 785 self._locked = ':'.join(map(str, val))
786 786 else:
787 787 self._locked = None
788 788
789 789 @hybrid_property
790 790 def changeset_cache(self):
791 791 from rhodecode.lib.vcs.backends.base import EmptyChangeset
792 792 dummy = EmptyChangeset().__json__()
793 793 if not self._changeset_cache:
794 794 return dummy
795 795 try:
796 796 return json.loads(self._changeset_cache)
797 797 except TypeError:
798 798 return dummy
799 799
800 800 @changeset_cache.setter
801 801 def changeset_cache(self, val):
802 802 try:
803 803 self._changeset_cache = json.dumps(val)
804 804 except:
805 805 log.error(traceback.format_exc())
806 806
807 807 @classmethod
808 808 def url_sep(cls):
809 809 return URL_SEP
810 810
811 811 @classmethod
812 812 def normalize_repo_name(cls, repo_name):
813 813 """
814 814 Normalizes os specific repo_name to the format internally stored inside
815 815 dabatabase using URL_SEP
816 816
817 817 :param cls:
818 818 :param repo_name:
819 819 """
820 820 return cls.url_sep().join(repo_name.split(os.sep))
821 821
822 822 @classmethod
823 823 def get_by_repo_name(cls, repo_name):
824 824 q = Session().query(cls).filter(cls.repo_name == repo_name)
825 825 q = q.options(joinedload(Repository.fork))\
826 826 .options(joinedload(Repository.user))\
827 827 .options(joinedload(Repository.group))
828 828 return q.scalar()
829 829
830 830 @classmethod
831 831 def get_by_full_path(cls, repo_full_path):
832 832 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
833 833 repo_name = cls.normalize_repo_name(repo_name)
834 834 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
835 835
836 836 @classmethod
837 837 def get_repo_forks(cls, repo_id):
838 838 return cls.query().filter(Repository.fork_id == repo_id)
839 839
840 840 @classmethod
841 841 def base_path(cls):
842 842 """
843 843 Returns base path when all repos are stored
844 844
845 845 :param cls:
846 846 """
847 847 q = Session().query(RhodeCodeUi)\
848 848 .filter(RhodeCodeUi.ui_key == cls.url_sep())
849 849 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
850 850 return q.one().ui_value
851 851
852 852 @property
853 853 def forks(self):
854 854 """
855 855 Return forks of this repo
856 856 """
857 857 return Repository.get_repo_forks(self.repo_id)
858 858
859 859 @property
860 860 def parent(self):
861 861 """
862 862 Returns fork parent
863 863 """
864 864 return self.fork
865 865
866 866 @property
867 867 def just_name(self):
868 868 return self.repo_name.split(Repository.url_sep())[-1]
869 869
870 870 @property
871 871 def groups_with_parents(self):
872 872 groups = []
873 873 if self.group is None:
874 874 return groups
875 875
876 876 cur_gr = self.group
877 877 groups.insert(0, cur_gr)
878 878 while 1:
879 879 gr = getattr(cur_gr, 'parent_group', None)
880 880 cur_gr = cur_gr.parent_group
881 881 if gr is None:
882 882 break
883 883 groups.insert(0, gr)
884 884
885 885 return groups
886 886
887 887 @property
888 888 def groups_and_repo(self):
889 889 return self.groups_with_parents, self.just_name
890 890
891 891 @LazyProperty
892 892 def repo_path(self):
893 893 """
894 894 Returns base full path for that repository means where it actually
895 895 exists on a filesystem
896 896 """
897 897 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
898 898 Repository.url_sep())
899 899 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
900 900 return q.one().ui_value
901 901
902 902 @property
903 903 def repo_full_path(self):
904 904 p = [self.repo_path]
905 905 # we need to split the name by / since this is how we store the
906 906 # names in the database, but that eventually needs to be converted
907 907 # into a valid system path
908 908 p += self.repo_name.split(Repository.url_sep())
909 909 return os.path.join(*p)
910 910
911 911 @property
912 912 def cache_keys(self):
913 913 """
914 914 Returns associated cache keys for that repo
915 915 """
916 916 return CacheInvalidation.query()\
917 917 .filter(CacheInvalidation.cache_args == self.repo_name)\
918 918 .order_by(CacheInvalidation.cache_key)\
919 919 .all()
920 920
921 921 def get_new_name(self, repo_name):
922 922 """
923 923 returns new full repository name based on assigned group and new new
924 924
925 925 :param group_name:
926 926 """
927 927 path_prefix = self.group.full_path_splitted if self.group else []
928 928 return Repository.url_sep().join(path_prefix + [repo_name])
929 929
930 930 @property
931 931 def _ui(self):
932 932 """
933 933 Creates an db based ui object for this repository
934 934 """
935 935 from rhodecode.lib.utils import make_ui
936 936 return make_ui('db', clear_session=False)
937 937
938 938 @classmethod
939 939 def inject_ui(cls, repo, extras={}):
940 940 from rhodecode.lib.vcs.backends.hg import MercurialRepository
941 941 from rhodecode.lib.vcs.backends.git import GitRepository
942 942 required = (MercurialRepository, GitRepository)
943 943 if not isinstance(repo, required):
944 944 raise Exception('repo must be instance of %s' % required)
945 945
946 946 # inject ui extra param to log this action via push logger
947 947 for k, v in extras.items():
948 948 repo._repo.ui.setconfig('rhodecode_extras', k, v)
949 949
950 950 @classmethod
951 951 def is_valid(cls, repo_name):
952 952 """
953 953 returns True if given repo name is a valid filesystem repository
954 954
955 955 :param cls:
956 956 :param repo_name:
957 957 """
958 958 from rhodecode.lib.utils import is_valid_repo
959 959
960 960 return is_valid_repo(repo_name, cls.base_path())
961 961
962 962 def get_api_data(self):
963 963 """
964 964 Common function for generating repo api data
965 965
966 966 """
967 967 repo = self
968 968 data = dict(
969 969 repo_id=repo.repo_id,
970 970 repo_name=repo.repo_name,
971 971 repo_type=repo.repo_type,
972 972 clone_uri=repo.clone_uri,
973 973 private=repo.private,
974 974 created_on=repo.created_on,
975 975 description=repo.description,
976 976 landing_rev=repo.landing_rev,
977 977 owner=repo.user.username,
978 978 fork_of=repo.fork.repo_name if repo.fork else None,
979 979 enable_statistics=repo.enable_statistics,
980 980 enable_locking=repo.enable_locking,
981 981 enable_downloads=repo.enable_downloads,
982 982 last_changeset=repo.changeset_cache
983 983 )
984 984 rc_config = RhodeCodeSetting.get_app_settings()
985 985 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
986 986 if repository_fields:
987 987 for f in self.extra_fields:
988 988 data[f.field_key_prefixed] = f.field_value
989 989
990 990 return data
991 991
992 992 @classmethod
993 993 def lock(cls, repo, user_id):
994 994 repo.locked = [user_id, time.time()]
995 995 Session().add(repo)
996 996 Session().commit()
997 997
998 998 @classmethod
999 999 def unlock(cls, repo):
1000 1000 repo.locked = None
1001 1001 Session().add(repo)
1002 1002 Session().commit()
1003 1003
1004 1004 @property
1005 1005 def last_db_change(self):
1006 1006 return self.updated_on
1007 1007
1008 1008 def clone_url(self, **override):
1009 1009 from pylons import url
1010 1010 from urlparse import urlparse
1011 1011 import urllib
1012 1012 parsed_url = urlparse(url('home', qualified=True))
1013 1013 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
1014 1014 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
1015 1015 args = {
1016 1016 'user': '',
1017 1017 'pass': '',
1018 1018 'scheme': parsed_url.scheme,
1019 1019 'netloc': parsed_url.netloc,
1020 1020 'prefix': decoded_path,
1021 1021 'path': self.repo_name
1022 1022 }
1023 1023
1024 1024 args.update(override)
1025 1025 return default_clone_uri % args
1026 1026
1027 1027 #==========================================================================
1028 1028 # SCM PROPERTIES
1029 1029 #==========================================================================
1030 1030
1031 1031 def get_changeset(self, rev=None):
1032 1032 return get_changeset_safe(self.scm_instance, rev)
1033 1033
1034 1034 def get_landing_changeset(self):
1035 1035 """
1036 1036 Returns landing changeset, or if that doesn't exist returns the tip
1037 1037 """
1038 1038 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
1039 1039 return cs
1040 1040
1041 1041 def update_changeset_cache(self, cs_cache=None):
1042 1042 """
1043 1043 Update cache of last changeset for repository, keys should be::
1044 1044
1045 1045 short_id
1046 1046 raw_id
1047 1047 revision
1048 1048 message
1049 1049 date
1050 1050 author
1051 1051
1052 1052 :param cs_cache:
1053 1053 """
1054 1054 from rhodecode.lib.vcs.backends.base import BaseChangeset
1055 1055 if cs_cache is None:
1056 1056 cs_cache = self.get_changeset()
1057 1057 if isinstance(cs_cache, BaseChangeset):
1058 1058 cs_cache = cs_cache.__json__()
1059 1059
1060 1060 if (cs_cache != self.changeset_cache
1061 1061 or not self.last_change
1062 1062 or not self.changeset_cache):
1063 1063 _default = datetime.datetime.fromtimestamp(0)
1064 1064 last_change = cs_cache.get('date') or self.last_change or _default
1065 1065 log.debug('updated repo %s with new cs cache %s' % (self, cs_cache))
1066 1066 self.updated_on = last_change
1067 1067 self.changeset_cache = cs_cache
1068 1068 Session().add(self)
1069 1069 Session().commit()
1070 1070 else:
1071 1071 log.debug('Skipping repo:%s already with latest changes' % self)
1072 1072
1073 1073 @property
1074 1074 def tip(self):
1075 1075 return self.get_changeset('tip')
1076 1076
1077 1077 @property
1078 1078 def author(self):
1079 1079 return self.tip.author
1080 1080
1081 1081 @property
1082 1082 def last_change(self):
1083 1083 return self.scm_instance.last_change
1084 1084
1085 1085 def get_comments(self, revisions=None):
1086 1086 """
1087 1087 Returns comments for this repository grouped by revisions
1088 1088
1089 1089 :param revisions: filter query by revisions only
1090 1090 """
1091 1091 cmts = ChangesetComment.query()\
1092 1092 .filter(ChangesetComment.repo == self)
1093 1093 if revisions:
1094 1094 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1095 1095 grouped = defaultdict(list)
1096 1096 for cmt in cmts.all():
1097 1097 grouped[cmt.revision].append(cmt)
1098 1098 return grouped
1099 1099
1100 1100 def statuses(self, revisions=None):
1101 1101 """
1102 1102 Returns statuses for this repository
1103 1103
1104 1104 :param revisions: list of revisions to get statuses for
1105 1105 :type revisions: list
1106 1106 """
1107 1107
1108 1108 statuses = ChangesetStatus.query()\
1109 1109 .filter(ChangesetStatus.repo == self)\
1110 1110 .filter(ChangesetStatus.version == 0)
1111 1111 if revisions:
1112 1112 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1113 1113 grouped = {}
1114 1114
1115 1115 #maybe we have open new pullrequest without a status ?
1116 1116 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1117 1117 status_lbl = ChangesetStatus.get_status_lbl(stat)
1118 1118 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1119 1119 for rev in pr.revisions:
1120 1120 pr_id = pr.pull_request_id
1121 1121 pr_repo = pr.other_repo.repo_name
1122 1122 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1123 1123
1124 1124 for stat in statuses.all():
1125 1125 pr_id = pr_repo = None
1126 1126 if stat.pull_request:
1127 1127 pr_id = stat.pull_request.pull_request_id
1128 1128 pr_repo = stat.pull_request.other_repo.repo_name
1129 1129 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1130 1130 pr_id, pr_repo]
1131 1131 return grouped
1132 1132
1133 1133 def _repo_size(self):
1134 1134 from rhodecode.lib import helpers as h
1135 1135 log.debug('calculating repository size...')
1136 1136 return h.format_byte_size(self.scm_instance.size)
1137 1137
1138 1138 #==========================================================================
1139 1139 # SCM CACHE INSTANCE
1140 1140 #==========================================================================
1141 1141
1142 1142 @property
1143 1143 def invalidate(self):
1144 1144 return CacheInvalidation.invalidate(self.repo_name)
1145 1145
1146 1146 def set_invalidate(self):
1147 1147 """
1148 1148 set a cache for invalidation for this instance
1149 1149 """
1150 1150 CacheInvalidation.set_invalidate(repo_name=self.repo_name)
1151 1151
1152 1152 @LazyProperty
1153 1153 def scm_instance_no_cache(self):
1154 1154 return self.__get_instance()
1155 1155
1156 1156 @LazyProperty
1157 1157 def scm_instance(self):
1158 1158 import rhodecode
1159 1159 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1160 1160 if full_cache:
1161 1161 return self.scm_instance_cached()
1162 1162 return self.__get_instance()
1163 1163
1164 1164 def scm_instance_cached(self, cache_map=None):
1165 1165 @cache_region('long_term')
1166 1166 def _c(repo_name):
1167 1167 return self.__get_instance()
1168 1168 rn = self.repo_name
1169 1169 log.debug('Getting cached instance of repo')
1170 1170
1171 1171 if cache_map:
1172 1172 # get using prefilled cache_map
1173 1173 invalidate_repo = cache_map[self.repo_name]
1174 1174 if invalidate_repo:
1175 1175 invalidate_repo = (None if invalidate_repo.cache_active
1176 1176 else invalidate_repo)
1177 1177 else:
1178 1178 # get from invalidate
1179 1179 invalidate_repo = self.invalidate
1180 1180
1181 1181 if invalidate_repo is not None:
1182 1182 region_invalidate(_c, None, rn)
1183 1183 # update our cache
1184 1184 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1185 1185 return _c(rn)
1186 1186
1187 1187 def __get_instance(self):
1188 1188 repo_full_path = self.repo_full_path
1189 1189 try:
1190 1190 alias = get_scm(repo_full_path)[0]
1191 1191 log.debug('Creating instance of %s repository' % alias)
1192 1192 backend = get_backend(alias)
1193 1193 except VCSError:
1194 1194 log.error(traceback.format_exc())
1195 1195 log.error('Perhaps this repository is in db and not in '
1196 1196 'filesystem run rescan repositories with '
1197 1197 '"destroy old data " option from admin panel')
1198 1198 return
1199 1199
1200 1200 if alias == 'hg':
1201 1201
1202 1202 repo = backend(safe_str(repo_full_path), create=False,
1203 1203 baseui=self._ui)
1204 1204 # skip hidden web repository
1205 1205 if repo._get_hidden():
1206 1206 return
1207 1207 else:
1208 1208 repo = backend(repo_full_path, create=False)
1209 1209
1210 1210 return repo
1211 1211
1212 1212
1213 1213 class RepoGroup(Base, BaseModel):
1214 1214 __tablename__ = 'groups'
1215 1215 __table_args__ = (
1216 1216 UniqueConstraint('group_name', 'group_parent_id'),
1217 1217 CheckConstraint('group_id != group_parent_id'),
1218 1218 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1219 1219 'mysql_charset': 'utf8'},
1220 1220 )
1221 1221 __mapper_args__ = {'order_by': 'group_name'}
1222 1222
1223 1223 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1224 1224 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1225 1225 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1226 1226 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1227 1227 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1228 1228
1229 1229 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1230 1230 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
1231 1231
1232 1232 parent_group = relationship('RepoGroup', remote_side=group_id)
1233 1233
1234 1234 def __init__(self, group_name='', parent_group=None):
1235 1235 self.group_name = group_name
1236 1236 self.parent_group = parent_group
1237 1237
1238 1238 def __unicode__(self):
1239 1239 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1240 1240 self.group_name)
1241 1241
1242 1242 @classmethod
1243 1243 def groups_choices(cls, groups=None, show_empty_group=True):
1244 1244 from webhelpers.html import literal as _literal
1245 1245 if not groups:
1246 1246 groups = cls.query().all()
1247 1247
1248 1248 repo_groups = []
1249 1249 if show_empty_group:
1250 1250 repo_groups = [('-1', '-- no parent --')]
1251 1251 sep = ' &raquo; '
1252 1252 _name = lambda k: _literal(sep.join(k))
1253 1253
1254 1254 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1255 1255 for x in groups])
1256 1256
1257 1257 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1258 1258 return repo_groups
1259 1259
1260 1260 @classmethod
1261 1261 def url_sep(cls):
1262 1262 return URL_SEP
1263 1263
1264 1264 @classmethod
1265 1265 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1266 1266 if case_insensitive:
1267 1267 gr = cls.query()\
1268 1268 .filter(cls.group_name.ilike(group_name))
1269 1269 else:
1270 1270 gr = cls.query()\
1271 1271 .filter(cls.group_name == group_name)
1272 1272 if cache:
1273 1273 gr = gr.options(FromCache(
1274 1274 "sql_cache_short",
1275 1275 "get_group_%s" % _hash_key(group_name)
1276 1276 )
1277 1277 )
1278 1278 return gr.scalar()
1279 1279
1280 1280 @property
1281 1281 def parents(self):
1282 1282 parents_recursion_limit = 5
1283 1283 groups = []
1284 1284 if self.parent_group is None:
1285 1285 return groups
1286 1286 cur_gr = self.parent_group
1287 1287 groups.insert(0, cur_gr)
1288 1288 cnt = 0
1289 1289 while 1:
1290 1290 cnt += 1
1291 1291 gr = getattr(cur_gr, 'parent_group', None)
1292 1292 cur_gr = cur_gr.parent_group
1293 1293 if gr is None:
1294 1294 break
1295 1295 if cnt == parents_recursion_limit:
1296 1296 # this will prevent accidental infinit loops
1297 1297 log.error('group nested more than %s' %
1298 1298 parents_recursion_limit)
1299 1299 break
1300 1300
1301 1301 groups.insert(0, gr)
1302 1302 return groups
1303 1303
1304 1304 @property
1305 1305 def children(self):
1306 1306 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1307 1307
1308 1308 @property
1309 1309 def name(self):
1310 1310 return self.group_name.split(RepoGroup.url_sep())[-1]
1311 1311
1312 1312 @property
1313 1313 def full_path(self):
1314 1314 return self.group_name
1315 1315
1316 1316 @property
1317 1317 def full_path_splitted(self):
1318 1318 return self.group_name.split(RepoGroup.url_sep())
1319 1319
1320 1320 @property
1321 1321 def repositories(self):
1322 1322 return Repository.query()\
1323 1323 .filter(Repository.group == self)\
1324 1324 .order_by(Repository.repo_name)
1325 1325
1326 1326 @property
1327 1327 def repositories_recursive_count(self):
1328 1328 cnt = self.repositories.count()
1329 1329
1330 1330 def children_count(group):
1331 1331 cnt = 0
1332 1332 for child in group.children:
1333 1333 cnt += child.repositories.count()
1334 1334 cnt += children_count(child)
1335 1335 return cnt
1336 1336
1337 1337 return cnt + children_count(self)
1338 1338
1339 1339 def recursive_groups_and_repos(self):
1340 1340 """
1341 1341 Recursive return all groups, with repositories in those groups
1342 1342 """
1343 1343 all_ = []
1344 1344
1345 1345 def _get_members(root_gr):
1346 1346 for r in root_gr.repositories:
1347 1347 all_.append(r)
1348 1348 childs = root_gr.children.all()
1349 1349 if childs:
1350 1350 for gr in childs:
1351 1351 all_.append(gr)
1352 1352 _get_members(gr)
1353 1353
1354 1354 _get_members(self)
1355 1355 return [self] + all_
1356 1356
1357 1357 def get_new_name(self, group_name):
1358 1358 """
1359 1359 returns new full group name based on parent and new name
1360 1360
1361 1361 :param group_name:
1362 1362 """
1363 1363 path_prefix = (self.parent_group.full_path_splitted if
1364 1364 self.parent_group else [])
1365 1365 return RepoGroup.url_sep().join(path_prefix + [group_name])
1366 1366
1367 1367
1368 1368 class Permission(Base, BaseModel):
1369 1369 __tablename__ = 'permissions'
1370 1370 __table_args__ = (
1371 1371 Index('p_perm_name_idx', 'permission_name'),
1372 1372 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1373 1373 'mysql_charset': 'utf8'},
1374 1374 )
1375 1375 PERMS = [
1376 1376 ('repository.none', _('Repository no access')),
1377 1377 ('repository.read', _('Repository read access')),
1378 1378 ('repository.write', _('Repository write access')),
1379 1379 ('repository.admin', _('Repository admin access')),
1380 1380
1381 ('group.none', _('Repositories Group no access')),
1382 ('group.read', _('Repositories Group read access')),
1383 ('group.write', _('Repositories Group write access')),
1384 ('group.admin', _('Repositories Group admin access')),
1381 ('group.none', _('Repository group no access')),
1382 ('group.read', _('Repository group read access')),
1383 ('group.write', _('Repository group write access')),
1384 ('group.admin', _('Repository group admin access')),
1385 1385
1386 1386 ('hg.admin', _('RhodeCode Administrator')),
1387 1387 ('hg.create.none', _('Repository creation disabled')),
1388 1388 ('hg.create.repository', _('Repository creation enabled')),
1389 1389 ('hg.fork.none', _('Repository forking disabled')),
1390 1390 ('hg.fork.repository', _('Repository forking enabled')),
1391 1391 ('hg.register.none', _('Register disabled')),
1392 1392 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1393 1393 'with manual activation')),
1394 1394
1395 1395 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1396 1396 'with auto activation')),
1397 1397 ]
1398 1398
1399 1399 # defines which permissions are more important higher the more important
1400 1400 PERM_WEIGHTS = {
1401 1401 'repository.none': 0,
1402 1402 'repository.read': 1,
1403 1403 'repository.write': 3,
1404 1404 'repository.admin': 4,
1405 1405
1406 1406 'group.none': 0,
1407 1407 'group.read': 1,
1408 1408 'group.write': 3,
1409 1409 'group.admin': 4,
1410 1410
1411 1411 'hg.fork.none': 0,
1412 1412 'hg.fork.repository': 1,
1413 1413 'hg.create.none': 0,
1414 1414 'hg.create.repository':1
1415 1415 }
1416 1416
1417 1417 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1418 1418 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1419 1419 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1420 1420
1421 1421 def __unicode__(self):
1422 1422 return u"<%s('%s:%s')>" % (
1423 1423 self.__class__.__name__, self.permission_id, self.permission_name
1424 1424 )
1425 1425
1426 1426 @classmethod
1427 1427 def get_by_key(cls, key):
1428 1428 return cls.query().filter(cls.permission_name == key).scalar()
1429 1429
1430 1430 @classmethod
1431 1431 def get_default_perms(cls, default_user_id):
1432 1432 q = Session().query(UserRepoToPerm, Repository, cls)\
1433 1433 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1434 1434 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1435 1435 .filter(UserRepoToPerm.user_id == default_user_id)
1436 1436
1437 1437 return q.all()
1438 1438
1439 1439 @classmethod
1440 1440 def get_default_group_perms(cls, default_user_id):
1441 1441 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1442 1442 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1443 1443 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1444 1444 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1445 1445
1446 1446 return q.all()
1447 1447
1448 1448
1449 1449 class UserRepoToPerm(Base, BaseModel):
1450 1450 __tablename__ = 'repo_to_perm'
1451 1451 __table_args__ = (
1452 1452 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1453 1453 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1454 1454 'mysql_charset': 'utf8'}
1455 1455 )
1456 1456 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1457 1457 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1458 1458 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1459 1459 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1460 1460
1461 1461 user = relationship('User')
1462 1462 repository = relationship('Repository')
1463 1463 permission = relationship('Permission')
1464 1464
1465 1465 @classmethod
1466 1466 def create(cls, user, repository, permission):
1467 1467 n = cls()
1468 1468 n.user = user
1469 1469 n.repository = repository
1470 1470 n.permission = permission
1471 1471 Session().add(n)
1472 1472 return n
1473 1473
1474 1474 def __unicode__(self):
1475 1475 return u'<user:%s => %s >' % (self.user, self.repository)
1476 1476
1477 1477
1478 1478 class UserToPerm(Base, BaseModel):
1479 1479 __tablename__ = 'user_to_perm'
1480 1480 __table_args__ = (
1481 1481 UniqueConstraint('user_id', 'permission_id'),
1482 1482 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1483 1483 'mysql_charset': 'utf8'}
1484 1484 )
1485 1485 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1486 1486 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1487 1487 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1488 1488
1489 1489 user = relationship('User')
1490 1490 permission = relationship('Permission', lazy='joined')
1491 1491
1492 1492
1493 1493 class UsersGroupRepoToPerm(Base, BaseModel):
1494 1494 __tablename__ = 'users_group_repo_to_perm'
1495 1495 __table_args__ = (
1496 1496 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1497 1497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1498 1498 'mysql_charset': 'utf8'}
1499 1499 )
1500 1500 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1501 1501 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1502 1502 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1503 1503 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1504 1504
1505 1505 users_group = relationship('UsersGroup')
1506 1506 permission = relationship('Permission')
1507 1507 repository = relationship('Repository')
1508 1508
1509 1509 @classmethod
1510 1510 def create(cls, users_group, repository, permission):
1511 1511 n = cls()
1512 1512 n.users_group = users_group
1513 1513 n.repository = repository
1514 1514 n.permission = permission
1515 1515 Session().add(n)
1516 1516 return n
1517 1517
1518 1518 def __unicode__(self):
1519 1519 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1520 1520
1521 1521
1522 1522 class UsersGroupToPerm(Base, BaseModel):
1523 1523 __tablename__ = 'users_group_to_perm'
1524 1524 __table_args__ = (
1525 1525 UniqueConstraint('users_group_id', 'permission_id',),
1526 1526 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1527 1527 'mysql_charset': 'utf8'}
1528 1528 )
1529 1529 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1530 1530 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1531 1531 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1532 1532
1533 1533 users_group = relationship('UsersGroup')
1534 1534 permission = relationship('Permission')
1535 1535
1536 1536
1537 1537 class UserRepoGroupToPerm(Base, BaseModel):
1538 1538 __tablename__ = 'user_repo_group_to_perm'
1539 1539 __table_args__ = (
1540 1540 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1541 1541 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1542 1542 'mysql_charset': 'utf8'}
1543 1543 )
1544 1544
1545 1545 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1546 1546 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1547 1547 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1548 1548 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1549 1549
1550 1550 user = relationship('User')
1551 1551 group = relationship('RepoGroup')
1552 1552 permission = relationship('Permission')
1553 1553
1554 1554
1555 1555 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1556 1556 __tablename__ = 'users_group_repo_group_to_perm'
1557 1557 __table_args__ = (
1558 1558 UniqueConstraint('users_group_id', 'group_id'),
1559 1559 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1560 1560 'mysql_charset': 'utf8'}
1561 1561 )
1562 1562
1563 1563 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)
1564 1564 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1565 1565 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1566 1566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1567 1567
1568 1568 users_group = relationship('UsersGroup')
1569 1569 permission = relationship('Permission')
1570 1570 group = relationship('RepoGroup')
1571 1571
1572 1572
1573 1573 class Statistics(Base, BaseModel):
1574 1574 __tablename__ = 'statistics'
1575 1575 __table_args__ = (
1576 1576 UniqueConstraint('repository_id'),
1577 1577 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1578 1578 'mysql_charset': 'utf8'}
1579 1579 )
1580 1580 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1581 1581 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1582 1582 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1583 1583 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1584 1584 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1585 1585 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1586 1586
1587 1587 repository = relationship('Repository', single_parent=True)
1588 1588
1589 1589
1590 1590 class UserFollowing(Base, BaseModel):
1591 1591 __tablename__ = 'user_followings'
1592 1592 __table_args__ = (
1593 1593 UniqueConstraint('user_id', 'follows_repository_id'),
1594 1594 UniqueConstraint('user_id', 'follows_user_id'),
1595 1595 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1596 1596 'mysql_charset': 'utf8'}
1597 1597 )
1598 1598
1599 1599 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1600 1600 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1601 1601 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1602 1602 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1603 1603 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1604 1604
1605 1605 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1606 1606
1607 1607 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1608 1608 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1609 1609
1610 1610 @classmethod
1611 1611 def get_repo_followers(cls, repo_id):
1612 1612 return cls.query().filter(cls.follows_repo_id == repo_id)
1613 1613
1614 1614
1615 1615 class CacheInvalidation(Base, BaseModel):
1616 1616 __tablename__ = 'cache_invalidation'
1617 1617 __table_args__ = (
1618 1618 UniqueConstraint('cache_key'),
1619 1619 Index('key_idx', 'cache_key'),
1620 1620 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1621 1621 'mysql_charset': 'utf8'},
1622 1622 )
1623 1623 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1624 1624 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1625 1625 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1626 1626 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1627 1627
1628 1628 def __init__(self, cache_key, cache_args=''):
1629 1629 self.cache_key = cache_key
1630 1630 self.cache_args = cache_args
1631 1631 self.cache_active = False
1632 1632
1633 1633 def __unicode__(self):
1634 1634 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1635 1635 self.cache_id, self.cache_key)
1636 1636
1637 1637 @property
1638 1638 def prefix(self):
1639 1639 _split = self.cache_key.split(self.cache_args, 1)
1640 1640 if _split and len(_split) == 2:
1641 1641 return _split[0]
1642 1642 return ''
1643 1643
1644 1644 @classmethod
1645 1645 def clear_cache(cls):
1646 1646 cls.query().delete()
1647 1647
1648 1648 @classmethod
1649 1649 def _get_key(cls, key):
1650 1650 """
1651 1651 Wrapper for generating a key, together with a prefix
1652 1652
1653 1653 :param key:
1654 1654 """
1655 1655 import rhodecode
1656 1656 prefix = ''
1657 1657 org_key = key
1658 1658 iid = rhodecode.CONFIG.get('instance_id')
1659 1659 if iid:
1660 1660 prefix = iid
1661 1661
1662 1662 return "%s%s" % (prefix, key), prefix, org_key
1663 1663
1664 1664 @classmethod
1665 1665 def get_by_key(cls, key):
1666 1666 return cls.query().filter(cls.cache_key == key).scalar()
1667 1667
1668 1668 @classmethod
1669 1669 def get_by_repo_name(cls, repo_name):
1670 1670 return cls.query().filter(cls.cache_args == repo_name).all()
1671 1671
1672 1672 @classmethod
1673 1673 def _get_or_create_key(cls, key, repo_name, commit=True):
1674 1674 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1675 1675 if not inv_obj:
1676 1676 try:
1677 1677 inv_obj = CacheInvalidation(key, repo_name)
1678 1678 Session().add(inv_obj)
1679 1679 if commit:
1680 1680 Session().commit()
1681 1681 except Exception:
1682 1682 log.error(traceback.format_exc())
1683 1683 Session().rollback()
1684 1684 return inv_obj
1685 1685
1686 1686 @classmethod
1687 1687 def invalidate(cls, key):
1688 1688 """
1689 1689 Returns Invalidation object if this given key should be invalidated
1690 1690 None otherwise. `cache_active = False` means that this cache
1691 1691 state is not valid and needs to be invalidated
1692 1692
1693 1693 :param key:
1694 1694 """
1695 1695 repo_name = key
1696 1696 repo_name = remove_suffix(repo_name, '_README')
1697 1697 repo_name = remove_suffix(repo_name, '_RSS')
1698 1698 repo_name = remove_suffix(repo_name, '_ATOM')
1699 1699
1700 1700 # adds instance prefix
1701 1701 key, _prefix, _org_key = cls._get_key(key)
1702 1702 inv = cls._get_or_create_key(key, repo_name)
1703 1703
1704 1704 if inv and inv.cache_active is False:
1705 1705 return inv
1706 1706
1707 1707 @classmethod
1708 1708 def set_invalidate(cls, key=None, repo_name=None):
1709 1709 """
1710 1710 Mark this Cache key for invalidation, either by key or whole
1711 1711 cache sets based on repo_name
1712 1712
1713 1713 :param key:
1714 1714 """
1715 1715 invalidated_keys = []
1716 1716 if key:
1717 1717 key, _prefix, _org_key = cls._get_key(key)
1718 1718 inv_objs = Session().query(cls).filter(cls.cache_key == key).all()
1719 1719 elif repo_name:
1720 1720 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1721 1721
1722 1722 try:
1723 1723 for inv_obj in inv_objs:
1724 1724 inv_obj.cache_active = False
1725 1725 log.debug('marking %s key for invalidation based on key=%s,repo_name=%s'
1726 1726 % (inv_obj, key, repo_name))
1727 1727 invalidated_keys.append(inv_obj.cache_key)
1728 1728 Session().add(inv_obj)
1729 1729 Session().commit()
1730 1730 except Exception:
1731 1731 log.error(traceback.format_exc())
1732 1732 Session().rollback()
1733 1733 return invalidated_keys
1734 1734
1735 1735 @classmethod
1736 1736 def set_valid(cls, key):
1737 1737 """
1738 1738 Mark this cache key as active and currently cached
1739 1739
1740 1740 :param key:
1741 1741 """
1742 1742 inv_obj = cls.get_by_key(key)
1743 1743 inv_obj.cache_active = True
1744 1744 Session().add(inv_obj)
1745 1745 Session().commit()
1746 1746
1747 1747 @classmethod
1748 1748 def get_cache_map(cls):
1749 1749
1750 1750 class cachemapdict(dict):
1751 1751
1752 1752 def __init__(self, *args, **kwargs):
1753 1753 fixkey = kwargs.get('fixkey')
1754 1754 if fixkey:
1755 1755 del kwargs['fixkey']
1756 1756 self.fixkey = fixkey
1757 1757 super(cachemapdict, self).__init__(*args, **kwargs)
1758 1758
1759 1759 def __getattr__(self, name):
1760 1760 key = name
1761 1761 if self.fixkey:
1762 1762 key, _prefix, _org_key = cls._get_key(key)
1763 1763 if key in self.__dict__:
1764 1764 return self.__dict__[key]
1765 1765 else:
1766 1766 return self[key]
1767 1767
1768 1768 def __getitem__(self, key):
1769 1769 if self.fixkey:
1770 1770 key, _prefix, _org_key = cls._get_key(key)
1771 1771 try:
1772 1772 return super(cachemapdict, self).__getitem__(key)
1773 1773 except KeyError:
1774 1774 return
1775 1775
1776 1776 cache_map = cachemapdict(fixkey=True)
1777 1777 for obj in cls.query().all():
1778 1778 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1779 1779 return cache_map
1780 1780
1781 1781
1782 1782 class ChangesetComment(Base, BaseModel):
1783 1783 __tablename__ = 'changeset_comments'
1784 1784 __table_args__ = (
1785 1785 Index('cc_revision_idx', 'revision'),
1786 1786 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1787 1787 'mysql_charset': 'utf8'},
1788 1788 )
1789 1789 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1790 1790 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1791 1791 revision = Column('revision', String(40), nullable=True)
1792 1792 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1793 1793 line_no = Column('line_no', Unicode(10), nullable=True)
1794 1794 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1795 1795 f_path = Column('f_path', Unicode(1000), nullable=True)
1796 1796 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1797 1797 text = Column('text', UnicodeText(25000), nullable=False)
1798 1798 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1799 1799 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1800 1800
1801 1801 author = relationship('User', lazy='joined')
1802 1802 repo = relationship('Repository')
1803 1803 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1804 1804 pull_request = relationship('PullRequest', lazy='joined')
1805 1805
1806 1806 @classmethod
1807 1807 def get_users(cls, revision=None, pull_request_id=None):
1808 1808 """
1809 1809 Returns user associated with this ChangesetComment. ie those
1810 1810 who actually commented
1811 1811
1812 1812 :param cls:
1813 1813 :param revision:
1814 1814 """
1815 1815 q = Session().query(User)\
1816 1816 .join(ChangesetComment.author)
1817 1817 if revision:
1818 1818 q = q.filter(cls.revision == revision)
1819 1819 elif pull_request_id:
1820 1820 q = q.filter(cls.pull_request_id == pull_request_id)
1821 1821 return q.all()
1822 1822
1823 1823
1824 1824 class ChangesetStatus(Base, BaseModel):
1825 1825 __tablename__ = 'changeset_statuses'
1826 1826 __table_args__ = (
1827 1827 Index('cs_revision_idx', 'revision'),
1828 1828 Index('cs_version_idx', 'version'),
1829 1829 UniqueConstraint('repo_id', 'revision', 'version'),
1830 1830 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1831 1831 'mysql_charset': 'utf8'}
1832 1832 )
1833 1833 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1834 1834 STATUS_APPROVED = 'approved'
1835 1835 STATUS_REJECTED = 'rejected'
1836 1836 STATUS_UNDER_REVIEW = 'under_review'
1837 1837
1838 1838 STATUSES = [
1839 1839 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1840 1840 (STATUS_APPROVED, _("Approved")),
1841 1841 (STATUS_REJECTED, _("Rejected")),
1842 1842 (STATUS_UNDER_REVIEW, _("Under Review")),
1843 1843 ]
1844 1844
1845 1845 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1846 1846 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1847 1847 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1848 1848 revision = Column('revision', String(40), nullable=False)
1849 1849 status = Column('status', String(128), nullable=False, default=DEFAULT)
1850 1850 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1851 1851 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1852 1852 version = Column('version', Integer(), nullable=False, default=0)
1853 1853 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1854 1854
1855 1855 author = relationship('User', lazy='joined')
1856 1856 repo = relationship('Repository')
1857 1857 comment = relationship('ChangesetComment', lazy='joined')
1858 1858 pull_request = relationship('PullRequest', lazy='joined')
1859 1859
1860 1860 def __unicode__(self):
1861 1861 return u"<%s('%s:%s')>" % (
1862 1862 self.__class__.__name__,
1863 1863 self.status, self.author
1864 1864 )
1865 1865
1866 1866 @classmethod
1867 1867 def get_status_lbl(cls, value):
1868 1868 return dict(cls.STATUSES).get(value)
1869 1869
1870 1870 @property
1871 1871 def status_lbl(self):
1872 1872 return ChangesetStatus.get_status_lbl(self.status)
1873 1873
1874 1874
1875 1875 class PullRequest(Base, BaseModel):
1876 1876 __tablename__ = 'pull_requests'
1877 1877 __table_args__ = (
1878 1878 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1879 1879 'mysql_charset': 'utf8'},
1880 1880 )
1881 1881
1882 1882 STATUS_NEW = u'new'
1883 1883 STATUS_OPEN = u'open'
1884 1884 STATUS_CLOSED = u'closed'
1885 1885
1886 1886 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1887 1887 title = Column('title', Unicode(256), nullable=True)
1888 1888 description = Column('description', UnicodeText(10240), nullable=True)
1889 1889 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1890 1890 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1891 1891 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1892 1892 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1893 1893 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1894 1894 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1895 1895 org_ref = Column('org_ref', Unicode(256), nullable=False)
1896 1896 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1897 1897 other_ref = Column('other_ref', Unicode(256), nullable=False)
1898 1898
1899 1899 @hybrid_property
1900 1900 def revisions(self):
1901 1901 return self._revisions.split(':')
1902 1902
1903 1903 @revisions.setter
1904 1904 def revisions(self, val):
1905 1905 self._revisions = ':'.join(val)
1906 1906
1907 1907 @property
1908 1908 def org_ref_parts(self):
1909 1909 return self.org_ref.split(':')
1910 1910
1911 1911 @property
1912 1912 def other_ref_parts(self):
1913 1913 return self.other_ref.split(':')
1914 1914
1915 1915 author = relationship('User', lazy='joined')
1916 1916 reviewers = relationship('PullRequestReviewers',
1917 1917 cascade="all, delete, delete-orphan")
1918 1918 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1919 1919 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1920 1920 statuses = relationship('ChangesetStatus')
1921 1921 comments = relationship('ChangesetComment',
1922 1922 cascade="all, delete, delete-orphan")
1923 1923
1924 1924 def is_closed(self):
1925 1925 return self.status == self.STATUS_CLOSED
1926 1926
1927 1927 @property
1928 1928 def last_review_status(self):
1929 1929 return self.statuses[-1].status if self.statuses else ''
1930 1930
1931 1931 def __json__(self):
1932 1932 return dict(
1933 1933 revisions=self.revisions
1934 1934 )
1935 1935
1936 1936
1937 1937 class PullRequestReviewers(Base, BaseModel):
1938 1938 __tablename__ = 'pull_request_reviewers'
1939 1939 __table_args__ = (
1940 1940 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1941 1941 'mysql_charset': 'utf8'},
1942 1942 )
1943 1943
1944 1944 def __init__(self, user=None, pull_request=None):
1945 1945 self.user = user
1946 1946 self.pull_request = pull_request
1947 1947
1948 1948 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1949 1949 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1950 1950 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1951 1951
1952 1952 user = relationship('User')
1953 1953 pull_request = relationship('PullRequest')
1954 1954
1955 1955
1956 1956 class Notification(Base, BaseModel):
1957 1957 __tablename__ = 'notifications'
1958 1958 __table_args__ = (
1959 1959 Index('notification_type_idx', 'type'),
1960 1960 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1961 1961 'mysql_charset': 'utf8'},
1962 1962 )
1963 1963
1964 1964 TYPE_CHANGESET_COMMENT = u'cs_comment'
1965 1965 TYPE_MESSAGE = u'message'
1966 1966 TYPE_MENTION = u'mention'
1967 1967 TYPE_REGISTRATION = u'registration'
1968 1968 TYPE_PULL_REQUEST = u'pull_request'
1969 1969 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1970 1970
1971 1971 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1972 1972 subject = Column('subject', Unicode(512), nullable=True)
1973 1973 body = Column('body', UnicodeText(50000), nullable=True)
1974 1974 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1975 1975 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1976 1976 type_ = Column('type', Unicode(256))
1977 1977
1978 1978 created_by_user = relationship('User')
1979 1979 notifications_to_users = relationship('UserNotification', lazy='joined',
1980 1980 cascade="all, delete, delete-orphan")
1981 1981
1982 1982 @property
1983 1983 def recipients(self):
1984 1984 return [x.user for x in UserNotification.query()\
1985 1985 .filter(UserNotification.notification == self)\
1986 1986 .order_by(UserNotification.user_id.asc()).all()]
1987 1987
1988 1988 @classmethod
1989 1989 def create(cls, created_by, subject, body, recipients, type_=None):
1990 1990 if type_ is None:
1991 1991 type_ = Notification.TYPE_MESSAGE
1992 1992
1993 1993 notification = cls()
1994 1994 notification.created_by_user = created_by
1995 1995 notification.subject = subject
1996 1996 notification.body = body
1997 1997 notification.type_ = type_
1998 1998 notification.created_on = datetime.datetime.now()
1999 1999
2000 2000 for u in recipients:
2001 2001 assoc = UserNotification()
2002 2002 assoc.notification = notification
2003 2003 u.notifications.append(assoc)
2004 2004 Session().add(notification)
2005 2005 return notification
2006 2006
2007 2007 @property
2008 2008 def description(self):
2009 2009 from rhodecode.model.notification import NotificationModel
2010 2010 return NotificationModel().make_description(self)
2011 2011
2012 2012
2013 2013 class UserNotification(Base, BaseModel):
2014 2014 __tablename__ = 'user_to_notification'
2015 2015 __table_args__ = (
2016 2016 UniqueConstraint('user_id', 'notification_id'),
2017 2017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2018 2018 'mysql_charset': 'utf8'}
2019 2019 )
2020 2020 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2021 2021 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2022 2022 read = Column('read', Boolean, default=False)
2023 2023 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2024 2024
2025 2025 user = relationship('User', lazy="joined")
2026 2026 notification = relationship('Notification', lazy="joined",
2027 2027 order_by=lambda: Notification.created_on.desc(),)
2028 2028
2029 2029 def mark_as_read(self):
2030 2030 self.read = True
2031 2031 Session().add(self)
2032 2032
2033 2033
2034 2034 class DbMigrateVersion(Base, BaseModel):
2035 2035 __tablename__ = 'db_migrate_version'
2036 2036 __table_args__ = (
2037 2037 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2038 2038 'mysql_charset': 'utf8'},
2039 2039 )
2040 2040 repository_id = Column('repository_id', String(250), primary_key=True)
2041 2041 repository_path = Column('repository_path', Text)
2042 2042 version = Column('version', Integer)
@@ -1,426 +1,426 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.user_group
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 repo group model for RhodeCode
7 7
8 8 :created_on: Jan 25, 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 logging
28 28 import traceback
29 29 import shutil
30 30 import datetime
31 31
32 32 from rhodecode.lib.utils2 import LazyProperty
33 33
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import RepoGroup, RhodeCodeUi, UserRepoGroupToPerm, \
36 36 User, Permission, UsersGroupRepoGroupToPerm, UsersGroup, Repository
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class ReposGroupModel(BaseModel):
42 42
43 43 cls = RepoGroup
44 44
45 45 def __get_users_group(self, users_group):
46 46 return self._get_instance(UsersGroup, users_group,
47 47 callback=UsersGroup.get_by_group_name)
48 48
49 49 def _get_repos_group(self, repos_group):
50 50 return self._get_instance(RepoGroup, repos_group,
51 51 callback=RepoGroup.get_by_group_name)
52 52
53 53 @LazyProperty
54 54 def repos_path(self):
55 55 """
56 56 Get's the repositories root path from database
57 57 """
58 58
59 59 q = RhodeCodeUi.get_by_key('/')
60 60 return q.ui_value
61 61
62 62 def _create_default_perms(self, new_group):
63 63 # create default permission
64 64 repo_group_to_perm = UserRepoGroupToPerm()
65 65 default_perm = 'group.read'
66 66 for p in User.get_by_username('default').user_perms:
67 67 if p.permission.permission_name.startswith('group.'):
68 68 default_perm = p.permission.permission_name
69 69 break
70 70
71 71 repo_group_to_perm.permission_id = self.sa.query(Permission)\
72 72 .filter(Permission.permission_name == default_perm)\
73 73 .one().permission_id
74 74
75 75 repo_group_to_perm.group = new_group
76 76 repo_group_to_perm.user_id = User.get_by_username('default').user_id
77 77
78 78 self.sa.add(repo_group_to_perm)
79 79
80 80 def __create_group(self, group_name):
81 81 """
82 makes repositories group on filesystem
82 makes repository group on filesystem
83 83
84 84 :param repo_name:
85 85 :param parent_id:
86 86 """
87 87
88 88 create_path = os.path.join(self.repos_path, group_name)
89 89 log.debug('creating new group in %s' % create_path)
90 90
91 91 if os.path.isdir(create_path):
92 92 raise Exception('That directory already exists !')
93 93
94 94 os.makedirs(create_path)
95 95
96 96 def __rename_group(self, old, new):
97 97 """
98 98 Renames a group on filesystem
99 99
100 100 :param group_name:
101 101 """
102 102
103 103 if old == new:
104 104 log.debug('skipping group rename')
105 105 return
106 106
107 107 log.debug('renaming repos group from %s to %s' % (old, new))
108 108
109 109 old_path = os.path.join(self.repos_path, old)
110 110 new_path = os.path.join(self.repos_path, new)
111 111
112 112 log.debug('renaming repos paths from %s to %s' % (old_path, new_path))
113 113
114 114 if os.path.isdir(new_path):
115 115 raise Exception('Was trying to rename to already '
116 116 'existing dir %s' % new_path)
117 117 shutil.move(old_path, new_path)
118 118
119 119 def __delete_group(self, group, force_delete=False):
120 120 """
121 121 Deletes a group from a filesystem
122 122
123 123 :param group: instance of group from database
124 124 :param force_delete: use shutil rmtree to remove all objects
125 125 """
126 126 paths = group.full_path.split(RepoGroup.url_sep())
127 127 paths = os.sep.join(paths)
128 128
129 129 rm_path = os.path.join(self.repos_path, paths)
130 130 log.info("Removing group %s" % (rm_path))
131 131 # delete only if that path really exists
132 132 if os.path.isdir(rm_path):
133 133 if force_delete:
134 134 shutil.rmtree(rm_path)
135 135 else:
136 136 #archive that group`
137 137 _now = datetime.datetime.now()
138 138 _ms = str(_now.microsecond).rjust(6, '0')
139 139 _d = 'rm__%s_GROUP_%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
140 140 group.name)
141 141 shutil.move(rm_path, os.path.join(self.repos_path, _d))
142 142
143 143 def create(self, group_name, group_description, owner, parent=None, just_db=False):
144 144 try:
145 145 new_repos_group = RepoGroup()
146 146 new_repos_group.group_description = group_description or group_name
147 147 new_repos_group.parent_group = self._get_repos_group(parent)
148 148 new_repos_group.group_name = new_repos_group.get_new_name(group_name)
149 149
150 150 self.sa.add(new_repos_group)
151 151 self._create_default_perms(new_repos_group)
152 152
153 153 #create an ADMIN permission for owner, later owner should go into
154 154 #the owner field of groups
155 155 self.grant_user_permission(repos_group=new_repos_group,
156 156 user=owner, perm='group.admin')
157 157
158 158 if not just_db:
159 159 # we need to flush here, in order to check if database won't
160 160 # throw any exceptions, create filesystem dirs at the very end
161 161 self.sa.flush()
162 162 self.__create_group(new_repos_group.group_name)
163 163
164 164 return new_repos_group
165 165 except:
166 166 log.error(traceback.format_exc())
167 167 raise
168 168
169 169 def _update_permissions(self, repos_group, perms_new=None,
170 170 perms_updates=None, recursive=False):
171 171 from rhodecode.model.repo import RepoModel
172 172 if not perms_new:
173 173 perms_new = []
174 174 if not perms_updates:
175 175 perms_updates = []
176 176
177 177 def _set_perm_user(obj, user, perm):
178 178 if isinstance(obj, RepoGroup):
179 179 ReposGroupModel().grant_user_permission(
180 180 repos_group=obj, user=user, perm=perm
181 181 )
182 182 elif isinstance(obj, Repository):
183 183 #we do this ONLY IF repository is non-private
184 184 if obj.private:
185 185 return
186 186
187 187 # we set group permission but we have to switch to repo
188 188 # permission
189 189 perm = perm.replace('group.', 'repository.')
190 190 RepoModel().grant_user_permission(
191 191 repo=obj, user=user, perm=perm
192 192 )
193 193
194 194 def _set_perm_group(obj, users_group, perm):
195 195 if isinstance(obj, RepoGroup):
196 196 ReposGroupModel().grant_users_group_permission(
197 197 repos_group=obj, group_name=users_group, perm=perm
198 198 )
199 199 elif isinstance(obj, Repository):
200 200 # we set group permission but we have to switch to repo
201 201 # permission
202 202 perm = perm.replace('group.', 'repository.')
203 203 RepoModel().grant_users_group_permission(
204 204 repo=obj, group_name=users_group, perm=perm
205 205 )
206 206 updates = []
207 207 log.debug('Now updating permissions for %s in recursive mode:%s'
208 208 % (repos_group, recursive))
209 209
210 210 for obj in repos_group.recursive_groups_and_repos():
211 211 #obj is an instance of a group or repositories in that group
212 212 if not recursive:
213 213 obj = repos_group
214 214
215 215 # update permissions
216 216 for member, perm, member_type in perms_updates:
217 217 ## set for user
218 218 if member_type == 'user':
219 219 # this updates also current one if found
220 220 _set_perm_user(obj, user=member, perm=perm)
221 221 ## set for user group
222 222 else:
223 223 _set_perm_group(obj, users_group=member, perm=perm)
224 224 # set new permissions
225 225 for member, perm, member_type in perms_new:
226 226 if member_type == 'user':
227 227 _set_perm_user(obj, user=member, perm=perm)
228 228 else:
229 229 _set_perm_group(obj, users_group=member, perm=perm)
230 230 updates.append(obj)
231 231 #if it's not recursive call
232 232 # break the loop and don't proceed with other changes
233 233 if not recursive:
234 234 break
235 235 return updates
236 236
237 237 def update(self, repos_group, form_data):
238 238
239 239 try:
240 240 repos_group = self._get_repos_group(repos_group)
241 241 recursive = form_data['recursive']
242 242 # iterate over all members(if in recursive mode) of this groups and
243 243 # set the permissions !
244 244 # this can be potentially heavy operation
245 245 self._update_permissions(repos_group, form_data['perms_new'],
246 246 form_data['perms_updates'], recursive)
247 247
248 248 old_path = repos_group.full_path
249 249
250 250 # change properties
251 251 repos_group.group_description = form_data['group_description']
252 252 repos_group.parent_group = RepoGroup.get(form_data['group_parent_id'])
253 253 repos_group.group_parent_id = form_data['group_parent_id']
254 254 repos_group.enable_locking = form_data['enable_locking']
255 255 repos_group.group_name = repos_group.get_new_name(form_data['group_name'])
256 256 new_path = repos_group.full_path
257 257
258 258 self.sa.add(repos_group)
259 259
260 260 # iterate over all members of this groups and set the locking !
261 261 # this can be potentially heavy operation
262 262 for obj in repos_group.recursive_groups_and_repos():
263 263 #set the value from it's parent
264 264 obj.enable_locking = repos_group.enable_locking
265 265 self.sa.add(obj)
266 266
267 267 # we need to get all repositories from this new group and
268 268 # rename them accordingly to new group path
269 269 for r in repos_group.repositories:
270 270 r.repo_name = r.get_new_name(r.just_name)
271 271 self.sa.add(r)
272 272
273 273 self.__rename_group(old_path, new_path)
274 274
275 275 return repos_group
276 276 except:
277 277 log.error(traceback.format_exc())
278 278 raise
279 279
280 280 def delete(self, repos_group, force_delete=False):
281 281 repos_group = self._get_repos_group(repos_group)
282 282 try:
283 283 self.sa.delete(repos_group)
284 284 self.__delete_group(repos_group, force_delete)
285 285 except:
286 286 log.error('Error removing repos_group %s' % repos_group)
287 287 raise
288 288
289 289 def delete_permission(self, repos_group, obj, obj_type, recursive):
290 290 """
291 291 Revokes permission for repos_group for given obj(user or users_group),
292 292 obj_type can be user or user group
293 293
294 294 :param repos_group:
295 295 :param obj: user or user group id
296 296 :param obj_type: user or user group type
297 297 :param recursive: recurse to all children of group
298 298 """
299 299 from rhodecode.model.repo import RepoModel
300 300 repos_group = self._get_repos_group(repos_group)
301 301
302 302 for el in repos_group.recursive_groups_and_repos():
303 303 if not recursive:
304 304 # if we don't recurse set the permission on only the top level
305 305 # object
306 306 el = repos_group
307 307
308 308 if isinstance(el, RepoGroup):
309 309 if obj_type == 'user':
310 310 ReposGroupModel().revoke_user_permission(el, user=obj)
311 311 elif obj_type == 'users_group':
312 312 ReposGroupModel().revoke_users_group_permission(el, group_name=obj)
313 313 else:
314 314 raise Exception('undefined object type %s' % obj_type)
315 315 elif isinstance(el, Repository):
316 316 if obj_type == 'user':
317 317 RepoModel().revoke_user_permission(el, user=obj)
318 318 elif obj_type == 'users_group':
319 319 RepoModel().revoke_users_group_permission(el, group_name=obj)
320 320 else:
321 321 raise Exception('undefined object type %s' % obj_type)
322 322
323 323 #if it's not recursive call
324 324 # break the loop and don't proceed with other changes
325 325 if not recursive:
326 326 break
327 327
328 328 def grant_user_permission(self, repos_group, user, perm):
329 329 """
330 Grant permission for user on given repositories group, or update
330 Grant permission for user on given repository group, or update
331 331 existing one if found
332 332
333 333 :param repos_group: Instance of ReposGroup, repositories_group_id,
334 334 or repositories_group name
335 335 :param user: Instance of User, user_id or username
336 336 :param perm: Instance of Permission, or permission_name
337 337 """
338 338
339 339 repos_group = self._get_repos_group(repos_group)
340 340 user = self._get_user(user)
341 341 permission = self._get_perm(perm)
342 342
343 343 # check if we have that permission already
344 344 obj = self.sa.query(UserRepoGroupToPerm)\
345 345 .filter(UserRepoGroupToPerm.user == user)\
346 346 .filter(UserRepoGroupToPerm.group == repos_group)\
347 347 .scalar()
348 348 if obj is None:
349 349 # create new !
350 350 obj = UserRepoGroupToPerm()
351 351 obj.group = repos_group
352 352 obj.user = user
353 353 obj.permission = permission
354 354 self.sa.add(obj)
355 355 log.debug('Granted perm %s to %s on %s' % (perm, user, repos_group))
356 356
357 357 def revoke_user_permission(self, repos_group, user):
358 358 """
359 Revoke permission for user on given repositories group
359 Revoke permission for user on given repository group
360 360
361 361 :param repos_group: Instance of ReposGroup, repositories_group_id,
362 362 or repositories_group name
363 363 :param user: Instance of User, user_id or username
364 364 """
365 365
366 366 repos_group = self._get_repos_group(repos_group)
367 367 user = self._get_user(user)
368 368
369 369 obj = self.sa.query(UserRepoGroupToPerm)\
370 370 .filter(UserRepoGroupToPerm.user == user)\
371 371 .filter(UserRepoGroupToPerm.group == repos_group)\
372 372 .scalar()
373 373 if obj:
374 374 self.sa.delete(obj)
375 375 log.debug('Revoked perm on %s on %s' % (repos_group, user))
376 376
377 377 def grant_users_group_permission(self, repos_group, group_name, perm):
378 378 """
379 Grant permission for user group on given repositories group, or update
379 Grant permission for user group on given repository group, or update
380 380 existing one if found
381 381
382 382 :param repos_group: Instance of ReposGroup, repositories_group_id,
383 383 or repositories_group name
384 384 :param group_name: Instance of UserGroup, users_group_id,
385 385 or user group name
386 386 :param perm: Instance of Permission, or permission_name
387 387 """
388 388 repos_group = self._get_repos_group(repos_group)
389 389 group_name = self.__get_users_group(group_name)
390 390 permission = self._get_perm(perm)
391 391
392 392 # check if we have that permission already
393 393 obj = self.sa.query(UsersGroupRepoGroupToPerm)\
394 394 .filter(UsersGroupRepoGroupToPerm.group == repos_group)\
395 395 .filter(UsersGroupRepoGroupToPerm.users_group == group_name)\
396 396 .scalar()
397 397
398 398 if obj is None:
399 399 # create new
400 400 obj = UsersGroupRepoGroupToPerm()
401 401
402 402 obj.group = repos_group
403 403 obj.users_group = group_name
404 404 obj.permission = permission
405 405 self.sa.add(obj)
406 406 log.debug('Granted perm %s to %s on %s' % (perm, group_name, repos_group))
407 407
408 408 def revoke_users_group_permission(self, repos_group, group_name):
409 409 """
410 Revoke permission for user group on given repositories group
410 Revoke permission for user group on given repository group
411 411
412 412 :param repos_group: Instance of ReposGroup, repositories_group_id,
413 413 or repositories_group name
414 414 :param group_name: Instance of UserGroup, users_group_id,
415 415 or user group name
416 416 """
417 417 repos_group = self._get_repos_group(repos_group)
418 418 group_name = self.__get_users_group(group_name)
419 419
420 420 obj = self.sa.query(UsersGroupRepoGroupToPerm)\
421 421 .filter(UsersGroupRepoGroupToPerm.group == repos_group)\
422 422 .filter(UsersGroupRepoGroupToPerm.users_group == group_name)\
423 423 .scalar()
424 424 if obj:
425 425 self.sa.delete(obj)
426 426 log.debug('Revoked perm to %s on %s' % (repos_group, group_name))
@@ -1,809 +1,809 b''
1 1 """
2 2 Set of generic validators
3 3 """
4 4 import os
5 5 import re
6 6 import formencode
7 7 import logging
8 8 from collections import defaultdict
9 9 from pylons.i18n.translation import _
10 10 from webhelpers.pylonslib.secure_form import authentication_token
11 11
12 12 from formencode.validators import (
13 13 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
14 14 NotEmpty, IPAddress, CIDR
15 15 )
16 16 from rhodecode.lib.compat import OrderedSet
17 17 from rhodecode.lib import ipaddr
18 18 from rhodecode.lib.utils import repo_name_slug
19 19 from rhodecode.model.db import RepoGroup, Repository, UsersGroup, User,\
20 20 ChangesetStatus
21 21 from rhodecode.lib.exceptions import LdapImportError
22 22 from rhodecode.config.routing import ADMIN_PREFIX
23 23 from rhodecode.lib.auth import HasReposGroupPermissionAny, HasPermissionAny
24 24
25 25 # silence warnings and pylint
26 26 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
27 27 NotEmpty, IPAddress, CIDR
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 class UniqueList(formencode.FancyValidator):
33 33 """
34 34 Unique List !
35 35 """
36 36 messages = dict(
37 37 empty=_('Value cannot be an empty list'),
38 38 missing_value=_('Value cannot be an empty list'),
39 39 )
40 40
41 41 def _to_python(self, value, state):
42 42 if isinstance(value, list):
43 43 return value
44 44 elif isinstance(value, set):
45 45 return list(value)
46 46 elif isinstance(value, tuple):
47 47 return list(value)
48 48 elif value is None:
49 49 return []
50 50 else:
51 51 return [value]
52 52
53 53 def empty_value(self, value):
54 54 return []
55 55
56 56
57 57 class StateObj(object):
58 58 """
59 59 this is needed to translate the messages using _() in validators
60 60 """
61 61 _ = staticmethod(_)
62 62
63 63
64 64 def M(self, key, state=None, **kwargs):
65 65 """
66 66 returns string from self.message based on given key,
67 67 passed kw params are used to substitute %(named)s params inside
68 68 translated strings
69 69
70 70 :param msg:
71 71 :param state:
72 72 """
73 73 if state is None:
74 74 state = StateObj()
75 75 else:
76 76 state._ = staticmethod(_)
77 77 #inject validator into state object
78 78 return self.message(key, state, **kwargs)
79 79
80 80
81 81 def ValidUsername(edit=False, old_data={}):
82 82 class _validator(formencode.validators.FancyValidator):
83 83 messages = {
84 84 'username_exists': _(u'Username "%(username)s" already exists'),
85 85 'system_invalid_username':
86 86 _(u'Username "%(username)s" is forbidden'),
87 87 'invalid_username':
88 88 _(u'Username may only contain alphanumeric characters '
89 89 'underscores, periods or dashes and must begin with '
90 90 'alphanumeric character')
91 91 }
92 92
93 93 def validate_python(self, value, state):
94 94 if value in ['default', 'new_user']:
95 95 msg = M(self, 'system_invalid_username', state, username=value)
96 96 raise formencode.Invalid(msg, value, state)
97 97 #check if user is unique
98 98 old_un = None
99 99 if edit:
100 100 old_un = User.get(old_data.get('user_id')).username
101 101
102 102 if old_un != value or not edit:
103 103 if User.get_by_username(value, case_insensitive=True):
104 104 msg = M(self, 'username_exists', state, username=value)
105 105 raise formencode.Invalid(msg, value, state)
106 106
107 107 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
108 108 msg = M(self, 'invalid_username', state)
109 109 raise formencode.Invalid(msg, value, state)
110 110 return _validator
111 111
112 112
113 113 def ValidRepoUser():
114 114 class _validator(formencode.validators.FancyValidator):
115 115 messages = {
116 116 'invalid_username': _(u'Username %(username)s is not valid')
117 117 }
118 118
119 119 def validate_python(self, value, state):
120 120 try:
121 121 User.query().filter(User.active == True)\
122 122 .filter(User.username == value).one()
123 123 except Exception:
124 124 msg = M(self, 'invalid_username', state, username=value)
125 125 raise formencode.Invalid(msg, value, state,
126 126 error_dict=dict(username=msg)
127 127 )
128 128
129 129 return _validator
130 130
131 131
132 132 def ValidUsersGroup(edit=False, old_data={}):
133 133 class _validator(formencode.validators.FancyValidator):
134 134 messages = {
135 135 'invalid_group': _(u'Invalid user group name'),
136 'group_exist': _(u'Users group "%(usersgroup)s" already exists'),
136 'group_exist': _(u'User group "%(usersgroup)s" already exists'),
137 137 'invalid_usersgroup_name':
138 138 _(u'user group name may only contain alphanumeric '
139 139 'characters underscores, periods or dashes and must begin '
140 140 'with alphanumeric character')
141 141 }
142 142
143 143 def validate_python(self, value, state):
144 144 if value in ['default']:
145 145 msg = M(self, 'invalid_group', state)
146 146 raise formencode.Invalid(msg, value, state,
147 147 error_dict=dict(users_group_name=msg)
148 148 )
149 149 #check if group is unique
150 150 old_ugname = None
151 151 if edit:
152 152 old_id = old_data.get('users_group_id')
153 153 old_ugname = UsersGroup.get(old_id).users_group_name
154 154
155 155 if old_ugname != value or not edit:
156 156 is_existing_group = UsersGroup.get_by_group_name(value,
157 157 case_insensitive=True)
158 158 if is_existing_group:
159 159 msg = M(self, 'group_exist', state, usersgroup=value)
160 160 raise formencode.Invalid(msg, value, state,
161 161 error_dict=dict(users_group_name=msg)
162 162 )
163 163
164 164 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
165 165 msg = M(self, 'invalid_usersgroup_name', state)
166 166 raise formencode.Invalid(msg, value, state,
167 167 error_dict=dict(users_group_name=msg)
168 168 )
169 169
170 170 return _validator
171 171
172 172
173 173 def ValidReposGroup(edit=False, old_data={}):
174 174 class _validator(formencode.validators.FancyValidator):
175 175 messages = {
176 176 'group_parent_id': _(u'Cannot assign this group as parent'),
177 177 'group_exists': _(u'Group "%(group_name)s" already exists'),
178 178 'repo_exists':
179 179 _(u'Repository with name "%(group_name)s" already exists')
180 180 }
181 181
182 182 def validate_python(self, value, state):
183 183 # TODO WRITE VALIDATIONS
184 184 group_name = value.get('group_name')
185 185 group_parent_id = value.get('group_parent_id')
186 186
187 187 # slugify repo group just in case :)
188 188 slug = repo_name_slug(group_name)
189 189
190 190 # check for parent of self
191 191 parent_of_self = lambda: (
192 192 old_data['group_id'] == int(group_parent_id)
193 193 if group_parent_id else False
194 194 )
195 195 if edit and parent_of_self():
196 196 msg = M(self, 'group_parent_id', state)
197 197 raise formencode.Invalid(msg, value, state,
198 198 error_dict=dict(group_parent_id=msg)
199 199 )
200 200
201 201 old_gname = None
202 202 if edit:
203 203 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
204 204
205 205 if old_gname != group_name or not edit:
206 206
207 207 # check group
208 208 gr = RepoGroup.query()\
209 209 .filter(RepoGroup.group_name == slug)\
210 210 .filter(RepoGroup.group_parent_id == group_parent_id)\
211 211 .scalar()
212 212
213 213 if gr:
214 214 msg = M(self, 'group_exists', state, group_name=slug)
215 215 raise formencode.Invalid(msg, value, state,
216 216 error_dict=dict(group_name=msg)
217 217 )
218 218
219 219 # check for same repo
220 220 repo = Repository.query()\
221 221 .filter(Repository.repo_name == slug)\
222 222 .scalar()
223 223
224 224 if repo:
225 225 msg = M(self, 'repo_exists', state, group_name=slug)
226 226 raise formencode.Invalid(msg, value, state,
227 227 error_dict=dict(group_name=msg)
228 228 )
229 229
230 230 return _validator
231 231
232 232
233 233 def ValidPassword():
234 234 class _validator(formencode.validators.FancyValidator):
235 235 messages = {
236 236 'invalid_password':
237 237 _(u'Invalid characters (non-ascii) in password')
238 238 }
239 239
240 240 def validate_python(self, value, state):
241 241 try:
242 242 (value or '').decode('ascii')
243 243 except UnicodeError:
244 244 msg = M(self, 'invalid_password', state)
245 245 raise formencode.Invalid(msg, value, state,)
246 246 return _validator
247 247
248 248
249 249 def ValidPasswordsMatch():
250 250 class _validator(formencode.validators.FancyValidator):
251 251 messages = {
252 252 'password_mismatch': _(u'Passwords do not match'),
253 253 }
254 254
255 255 def validate_python(self, value, state):
256 256
257 257 pass_val = value.get('password') or value.get('new_password')
258 258 if pass_val != value['password_confirmation']:
259 259 msg = M(self, 'password_mismatch', state)
260 260 raise formencode.Invalid(msg, value, state,
261 261 error_dict=dict(password_confirmation=msg)
262 262 )
263 263 return _validator
264 264
265 265
266 266 def ValidAuth():
267 267 class _validator(formencode.validators.FancyValidator):
268 268 messages = {
269 269 'invalid_password': _(u'invalid password'),
270 270 'invalid_username': _(u'invalid user name'),
271 271 'disabled_account': _(u'Your account is disabled')
272 272 }
273 273
274 274 def validate_python(self, value, state):
275 275 from rhodecode.lib.auth import authenticate
276 276
277 277 password = value['password']
278 278 username = value['username']
279 279
280 280 if not authenticate(username, password):
281 281 user = User.get_by_username(username)
282 282 if user and user.active is False:
283 283 log.warning('user %s is disabled' % username)
284 284 msg = M(self, 'disabled_account', state)
285 285 raise formencode.Invalid(msg, value, state,
286 286 error_dict=dict(username=msg)
287 287 )
288 288 else:
289 289 log.warning('user %s failed to authenticate' % username)
290 290 msg = M(self, 'invalid_username', state)
291 291 msg2 = M(self, 'invalid_password', state)
292 292 raise formencode.Invalid(msg, value, state,
293 293 error_dict=dict(username=msg, password=msg2)
294 294 )
295 295 return _validator
296 296
297 297
298 298 def ValidAuthToken():
299 299 class _validator(formencode.validators.FancyValidator):
300 300 messages = {
301 301 'invalid_token': _(u'Token mismatch')
302 302 }
303 303
304 304 def validate_python(self, value, state):
305 305 if value != authentication_token():
306 306 msg = M(self, 'invalid_token', state)
307 307 raise formencode.Invalid(msg, value, state)
308 308 return _validator
309 309
310 310
311 311 def ValidRepoName(edit=False, old_data={}):
312 312 class _validator(formencode.validators.FancyValidator):
313 313 messages = {
314 314 'invalid_repo_name':
315 315 _(u'Repository name %(repo)s is disallowed'),
316 316 'repository_exists':
317 317 _(u'Repository named %(repo)s already exists'),
318 318 'repository_in_group_exists': _(u'Repository "%(repo)s" already '
319 319 'exists in group "%(group)s"'),
320 'same_group_exists': _(u'Repositories group with name "%(repo)s" '
320 'same_group_exists': _(u'Repository group with name "%(repo)s" '
321 321 'already exists')
322 322 }
323 323
324 324 def _to_python(self, value, state):
325 325 repo_name = repo_name_slug(value.get('repo_name', ''))
326 326 repo_group = value.get('repo_group')
327 327 if repo_group:
328 328 gr = RepoGroup.get(repo_group)
329 329 group_path = gr.full_path
330 330 group_name = gr.group_name
331 331 # value needs to be aware of group name in order to check
332 332 # db key This is an actual just the name to store in the
333 333 # database
334 334 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
335 335 else:
336 336 group_name = group_path = ''
337 337 repo_name_full = repo_name
338 338
339 339 value['repo_name'] = repo_name
340 340 value['repo_name_full'] = repo_name_full
341 341 value['group_path'] = group_path
342 342 value['group_name'] = group_name
343 343 return value
344 344
345 345 def validate_python(self, value, state):
346 346
347 347 repo_name = value.get('repo_name')
348 348 repo_name_full = value.get('repo_name_full')
349 349 group_path = value.get('group_path')
350 350 group_name = value.get('group_name')
351 351
352 352 if repo_name in [ADMIN_PREFIX, '']:
353 353 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
354 354 raise formencode.Invalid(msg, value, state,
355 355 error_dict=dict(repo_name=msg)
356 356 )
357 357
358 358 rename = old_data.get('repo_name') != repo_name_full
359 359 create = not edit
360 360 if rename or create:
361 361
362 362 if group_path != '':
363 363 if Repository.get_by_repo_name(repo_name_full):
364 364 msg = M(self, 'repository_in_group_exists', state,
365 365 repo=repo_name, group=group_name)
366 366 raise formencode.Invalid(msg, value, state,
367 367 error_dict=dict(repo_name=msg)
368 368 )
369 369 elif RepoGroup.get_by_group_name(repo_name_full):
370 370 msg = M(self, 'same_group_exists', state,
371 371 repo=repo_name)
372 372 raise formencode.Invalid(msg, value, state,
373 373 error_dict=dict(repo_name=msg)
374 374 )
375 375
376 376 elif Repository.get_by_repo_name(repo_name_full):
377 377 msg = M(self, 'repository_exists', state,
378 378 repo=repo_name)
379 379 raise formencode.Invalid(msg, value, state,
380 380 error_dict=dict(repo_name=msg)
381 381 )
382 382 return value
383 383 return _validator
384 384
385 385
386 386 def ValidForkName(*args, **kwargs):
387 387 return ValidRepoName(*args, **kwargs)
388 388
389 389
390 390 def SlugifyName():
391 391 class _validator(formencode.validators.FancyValidator):
392 392
393 393 def _to_python(self, value, state):
394 394 return repo_name_slug(value)
395 395
396 396 def validate_python(self, value, state):
397 397 pass
398 398
399 399 return _validator
400 400
401 401
402 402 def ValidCloneUri():
403 403 from rhodecode.lib.utils import make_ui
404 404
405 405 def url_handler(repo_type, url, ui=None):
406 406 if repo_type == 'hg':
407 407 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
408 408 from mercurial.httppeer import httppeer
409 409 if url.startswith('http'):
410 410 ## initially check if it's at least the proper URL
411 411 ## or does it pass basic auth
412 412 MercurialRepository._check_url(url)
413 413 httppeer(ui, url)._capabilities()
414 414 elif url.startswith('svn+http'):
415 415 from hgsubversion.svnrepo import svnremoterepo
416 416 svnremoterepo(ui, url).capabilities
417 417 elif url.startswith('git+http'):
418 418 raise NotImplementedError()
419 419
420 420 elif repo_type == 'git':
421 421 from rhodecode.lib.vcs.backends.git.repository import GitRepository
422 422 if url.startswith('http'):
423 423 ## initially check if it's at least the proper URL
424 424 ## or does it pass basic auth
425 425 GitRepository._check_url(url)
426 426 elif url.startswith('svn+http'):
427 427 raise NotImplementedError()
428 428 elif url.startswith('hg+http'):
429 429 raise NotImplementedError()
430 430
431 431 class _validator(formencode.validators.FancyValidator):
432 432 messages = {
433 433 'clone_uri': _(u'invalid clone url'),
434 434 'invalid_clone_uri': _(u'Invalid clone url, provide a '
435 435 'valid clone http(s)/svn+http(s) url')
436 436 }
437 437
438 438 def validate_python(self, value, state):
439 439 repo_type = value.get('repo_type')
440 440 url = value.get('clone_uri')
441 441
442 442 if not url:
443 443 pass
444 444 else:
445 445 try:
446 446 url_handler(repo_type, url, make_ui('db', clear_session=False))
447 447 except Exception:
448 448 log.exception('Url validation failed')
449 449 msg = M(self, 'clone_uri')
450 450 raise formencode.Invalid(msg, value, state,
451 451 error_dict=dict(clone_uri=msg)
452 452 )
453 453 return _validator
454 454
455 455
456 456 def ValidForkType(old_data={}):
457 457 class _validator(formencode.validators.FancyValidator):
458 458 messages = {
459 459 'invalid_fork_type': _(u'Fork have to be the same type as parent')
460 460 }
461 461
462 462 def validate_python(self, value, state):
463 463 if old_data['repo_type'] != value:
464 464 msg = M(self, 'invalid_fork_type', state)
465 465 raise formencode.Invalid(msg, value, state,
466 466 error_dict=dict(repo_type=msg)
467 467 )
468 468 return _validator
469 469
470 470
471 471 def CanWriteGroup():
472 472 class _validator(formencode.validators.FancyValidator):
473 473 messages = {
474 474 'permission_denied': _(u"You don't have permissions "
475 475 "to create repository in this group"),
476 476 'permission_denied_root': _(u"no permission to create repository "
477 477 "in root location")
478 478 }
479 479
480 480 def _to_python(self, value, state):
481 481 #root location
482 482 if value in [-1, "-1"]:
483 483 return None
484 484 return value
485 485
486 486 def validate_python(self, value, state):
487 487 gr = RepoGroup.get(value)
488 488 gr_name = gr.group_name if gr else None # None means ROOT location
489 489 val = HasReposGroupPermissionAny('group.write', 'group.admin')
490 490 can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
491 491 forbidden = not val(gr_name, 'can write into group validator')
492 492 #parent group need to be existing
493 493 if gr and forbidden:
494 494 msg = M(self, 'permission_denied', state)
495 495 raise formencode.Invalid(msg, value, state,
496 496 error_dict=dict(repo_type=msg)
497 497 )
498 498 ## check if we can write to root location !
499 499 elif gr is None and can_create_repos() is False:
500 500 msg = M(self, 'permission_denied_root', state)
501 501 raise formencode.Invalid(msg, value, state,
502 502 error_dict=dict(repo_type=msg)
503 503 )
504 504
505 505 return _validator
506 506
507 507
508 508 def CanCreateGroup(can_create_in_root=False):
509 509 class _validator(formencode.validators.FancyValidator):
510 510 messages = {
511 511 'permission_denied': _(u"You don't have permissions "
512 512 "to create a group in this location")
513 513 }
514 514
515 515 def to_python(self, value, state):
516 516 #root location
517 517 if value in [-1, "-1"]:
518 518 return None
519 519 return value
520 520
521 521 def validate_python(self, value, state):
522 522 gr = RepoGroup.get(value)
523 523 gr_name = gr.group_name if gr else None # None means ROOT location
524 524
525 525 if can_create_in_root and gr is None:
526 526 #we can create in root, we're fine no validations required
527 527 return
528 528
529 529 forbidden_in_root = gr is None and can_create_in_root is False
530 530 val = HasReposGroupPermissionAny('group.admin')
531 531 forbidden = not val(gr_name, 'can create group validator')
532 532 if forbidden_in_root or forbidden:
533 533 msg = M(self, 'permission_denied', state)
534 534 raise formencode.Invalid(msg, value, state,
535 535 error_dict=dict(group_parent_id=msg)
536 536 )
537 537
538 538 return _validator
539 539
540 540
541 541 def ValidPerms(type_='repo'):
542 542 if type_ == 'group':
543 543 EMPTY_PERM = 'group.none'
544 544 elif type_ == 'repo':
545 545 EMPTY_PERM = 'repository.none'
546 546
547 547 class _validator(formencode.validators.FancyValidator):
548 548 messages = {
549 549 'perm_new_member_name':
550 550 _(u'This username or user group name is not valid')
551 551 }
552 552
553 553 def to_python(self, value, state):
554 554 perms_update = OrderedSet()
555 555 perms_new = OrderedSet()
556 556 # build a list of permission to update and new permission to create
557 557
558 558 #CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
559 559 new_perms_group = defaultdict(dict)
560 560 for k, v in value.copy().iteritems():
561 561 if k.startswith('perm_new_member'):
562 562 del value[k]
563 563 _type, part = k.split('perm_new_member_')
564 564 args = part.split('_')
565 565 if len(args) == 1:
566 566 new_perms_group[args[0]]['perm'] = v
567 567 elif len(args) == 2:
568 568 _key, pos = args
569 569 new_perms_group[pos][_key] = v
570 570
571 571 # fill new permissions in order of how they were added
572 572 for k in sorted(map(int, new_perms_group.keys())):
573 573 perm_dict = new_perms_group[str(k)]
574 574 new_member = perm_dict.get('name')
575 575 new_perm = perm_dict.get('perm')
576 576 new_type = perm_dict.get('type')
577 577 if new_member and new_perm and new_type:
578 578 perms_new.add((new_member, new_perm, new_type))
579 579
580 580 for k, v in value.iteritems():
581 581 if k.startswith('u_perm_') or k.startswith('g_perm_'):
582 582 member = k[7:]
583 583 t = {'u': 'user',
584 584 'g': 'users_group'
585 585 }[k[0]]
586 586 if member == 'default':
587 587 if value.get('repo_private'):
588 588 # set none for default when updating to
589 589 # private repo
590 590 v = EMPTY_PERM
591 591 perms_update.add((member, v, t))
592 592 #always set NONE when private flag is set
593 593 if value.get('repo_private'):
594 594 perms_update.add(('default', EMPTY_PERM, 'user'))
595 595
596 596 value['perms_updates'] = list(perms_update)
597 597 value['perms_new'] = list(perms_new)
598 598
599 599 # update permissions
600 600 for k, v, t in perms_new:
601 601 try:
602 602 if t is 'user':
603 603 self.user_db = User.query()\
604 604 .filter(User.active == True)\
605 605 .filter(User.username == k).one()
606 606 if t is 'users_group':
607 607 self.user_db = UsersGroup.query()\
608 608 .filter(UsersGroup.users_group_active == True)\
609 609 .filter(UsersGroup.users_group_name == k).one()
610 610
611 611 except Exception:
612 612 log.exception('Updated permission failed')
613 613 msg = M(self, 'perm_new_member_type', state)
614 614 raise formencode.Invalid(msg, value, state,
615 615 error_dict=dict(perm_new_member_name=msg)
616 616 )
617 617 return value
618 618 return _validator
619 619
620 620
621 621 def ValidSettings():
622 622 class _validator(formencode.validators.FancyValidator):
623 623 def _to_python(self, value, state):
624 624 # settings form for users that are not admin
625 625 # can't edit certain parameters, it's extra backup if they mangle
626 626 # with forms
627 627
628 628 forbidden_params = [
629 629 'user', 'repo_type', 'repo_enable_locking',
630 630 'repo_enable_downloads', 'repo_enable_statistics'
631 631 ]
632 632
633 633 for param in forbidden_params:
634 634 if param in value:
635 635 del value[param]
636 636 return value
637 637
638 638 def validate_python(self, value, state):
639 639 pass
640 640 return _validator
641 641
642 642
643 643 def ValidPath():
644 644 class _validator(formencode.validators.FancyValidator):
645 645 messages = {
646 646 'invalid_path': _(u'This is not a valid path')
647 647 }
648 648
649 649 def validate_python(self, value, state):
650 650 if not os.path.isdir(value):
651 651 msg = M(self, 'invalid_path', state)
652 652 raise formencode.Invalid(msg, value, state,
653 653 error_dict=dict(paths_root_path=msg)
654 654 )
655 655 return _validator
656 656
657 657
658 658 def UniqSystemEmail(old_data={}):
659 659 class _validator(formencode.validators.FancyValidator):
660 660 messages = {
661 661 'email_taken': _(u'This e-mail address is already taken')
662 662 }
663 663
664 664 def _to_python(self, value, state):
665 665 return value.lower()
666 666
667 667 def validate_python(self, value, state):
668 668 if (old_data.get('email') or '').lower() != value:
669 669 user = User.get_by_email(value, case_insensitive=True)
670 670 if user:
671 671 msg = M(self, 'email_taken', state)
672 672 raise formencode.Invalid(msg, value, state,
673 673 error_dict=dict(email=msg)
674 674 )
675 675 return _validator
676 676
677 677
678 678 def ValidSystemEmail():
679 679 class _validator(formencode.validators.FancyValidator):
680 680 messages = {
681 681 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
682 682 }
683 683
684 684 def _to_python(self, value, state):
685 685 return value.lower()
686 686
687 687 def validate_python(self, value, state):
688 688 user = User.get_by_email(value, case_insensitive=True)
689 689 if user is None:
690 690 msg = M(self, 'non_existing_email', state, email=value)
691 691 raise formencode.Invalid(msg, value, state,
692 692 error_dict=dict(email=msg)
693 693 )
694 694
695 695 return _validator
696 696
697 697
698 698 def LdapLibValidator():
699 699 class _validator(formencode.validators.FancyValidator):
700 700 messages = {
701 701
702 702 }
703 703
704 704 def validate_python(self, value, state):
705 705 try:
706 706 import ldap
707 707 ldap # pyflakes silence !
708 708 except ImportError:
709 709 raise LdapImportError()
710 710
711 711 return _validator
712 712
713 713
714 714 def AttrLoginValidator():
715 715 class _validator(formencode.validators.FancyValidator):
716 716 messages = {
717 717 'invalid_cn':
718 718 _(u'The LDAP Login attribute of the CN must be specified - '
719 719 'this is the name of the attribute that is equivalent '
720 720 'to "username"')
721 721 }
722 722
723 723 def validate_python(self, value, state):
724 724 if not value or not isinstance(value, (str, unicode)):
725 725 msg = M(self, 'invalid_cn', state)
726 726 raise formencode.Invalid(msg, value, state,
727 727 error_dict=dict(ldap_attr_login=msg)
728 728 )
729 729
730 730 return _validator
731 731
732 732
733 733 def NotReviewedRevisions(repo_id):
734 734 class _validator(formencode.validators.FancyValidator):
735 735 messages = {
736 736 'rev_already_reviewed':
737 737 _(u'Revisions %(revs)s are already part of pull request '
738 738 'or have set status')
739 739 }
740 740
741 741 def validate_python(self, value, state):
742 742 # check revisions if they are not reviewed, or a part of another
743 743 # pull request
744 744 statuses = ChangesetStatus.query()\
745 745 .filter(ChangesetStatus.revision.in_(value))\
746 746 .filter(ChangesetStatus.repo_id == repo_id)\
747 747 .all()
748 748
749 749 errors = []
750 750 for cs in statuses:
751 751 if cs.pull_request_id:
752 752 errors.append(['pull_req', cs.revision[:12]])
753 753 elif cs.status:
754 754 errors.append(['status', cs.revision[:12]])
755 755
756 756 if errors:
757 757 revs = ','.join([x[1] for x in errors])
758 758 msg = M(self, 'rev_already_reviewed', state, revs=revs)
759 759 raise formencode.Invalid(msg, value, state,
760 760 error_dict=dict(revisions=revs)
761 761 )
762 762
763 763 return _validator
764 764
765 765
766 766 def ValidIp():
767 767 class _validator(CIDR):
768 768 messages = dict(
769 769 badFormat=_('Please enter a valid IPv4 or IpV6 address'),
770 770 illegalBits=_('The network size (bits) must be within the range'
771 771 ' of 0-32 (not %(bits)r)'))
772 772
773 773 def to_python(self, value, state):
774 774 v = super(_validator, self).to_python(value, state)
775 775 v = v.strip()
776 776 net = ipaddr.IPNetwork(address=v)
777 777 if isinstance(net, ipaddr.IPv4Network):
778 778 #if IPv4 doesn't end with a mask, add /32
779 779 if '/' not in value:
780 780 v += '/32'
781 781 if isinstance(net, ipaddr.IPv6Network):
782 782 #if IPv6 doesn't end with a mask, add /128
783 783 if '/' not in value:
784 784 v += '/128'
785 785 return v
786 786
787 787 def validate_python(self, value, state):
788 788 try:
789 789 addr = value.strip()
790 790 #this raises an ValueError if address is not IpV4 or IpV6
791 791 ipaddr.IPNetwork(address=addr)
792 792 except ValueError:
793 793 raise formencode.Invalid(self.message('badFormat', state),
794 794 value, state)
795 795
796 796 return _validator
797 797
798 798
799 799 def FieldKey():
800 800 class _validator(formencode.validators.FancyValidator):
801 801 messages = dict(
802 802 badFormat=_('Key name can only consist of letters, '
803 803 'underscore, dash or numbers'),)
804 804
805 805 def validate_python(self, value, state):
806 806 if not re.match('[a-zA-Z0-9_-]+$', value):
807 807 raise formencode.Invalid(self.message('badFormat', state),
808 808 value, state)
809 809 return _validator
@@ -1,215 +1,215 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Permissions administration')} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(_('Admin'),h.url('admin_home'))}
10 10 &raquo;
11 11 ${_('permissions')}
12 12 </%def>
13 13
14 14 <%def name="page_nav()">
15 15 ${self.menu('admin')}
16 16 </%def>
17 17
18 18 <%def name="main()">
19 19 <div class="box box-left">
20 20 <!-- box / title -->
21 21 <div class="title">
22 22 ${self.breadcrumbs()}
23 23 </div>
24 24 <h3>${_('Default permissions')}</h3>
25 25 ${h.form(url('permission', id='default'),method='put')}
26 26 <div class="form">
27 27 <!-- fields -->
28 28 <div class="fields">
29 29 <div class="field">
30 30 <div class="label label-checkbox">
31 31 <label for="anonymous">${_('Anonymous access')}:</label>
32 32 </div>
33 33 <div class="checkboxes">
34 34 <div class="checkbox">
35 35 ${h.checkbox('anonymous',True)}
36 36 </div>
37 37 </div>
38 38 </div>
39 39 <div class="field">
40 40 <div class="label">
41 41 <label for="default_repo_perm">${_('Repository')}:</label>
42 42 </div>
43 43 <div class="select">
44 44 ${h.select('default_repo_perm','',c.repo_perms_choices)}
45 45
46 46 ${h.checkbox('overwrite_default_repo','true')}
47 47 <label for="overwrite_default_repo">
48 48 <span class="tooltip"
49 49 title="${h.tooltip(_('All default permissions on each repository will be reset to choosen permission, note that all custom default permission on repositories will be lost'))}">
50 50 ${_('overwrite existing settings')}</span> </label>
51 51 </div>
52 52 </div>
53 53 <div class="field">
54 54 <div class="label">
55 55 <label for="default_group_perm">${_('Repository group')}:</label>
56 56 </div>
57 57 <div class="select">
58 58 ${h.select('default_group_perm','',c.group_perms_choices)}
59 59 ${h.checkbox('overwrite_default_group','true')}
60 60 <label for="overwrite_default_group">
61 61 <span class="tooltip"
62 title="${h.tooltip(_('All default permissions on each repository group will be reset to choosen permission, note that all custom default permission on repositories group will be lost'))}">
62 title="${h.tooltip(_('All default permissions on each repository group will be reset to choosen permission, note that all custom default permission on repository groups will be lost'))}">
63 63 ${_('overwrite existing settings')}</span> </label>
64 64
65 65 </div>
66 66 </div>
67 67 <div class="field">
68 68 <div class="label">
69 69 <label for="default_register">${_('Registration')}:</label>
70 70 </div>
71 71 <div class="select">
72 72 ${h.select('default_register','',c.register_choices)}
73 73 </div>
74 74 </div>
75 75 <div class="field">
76 76 <div class="label">
77 77 <label for="default_create">${_('Repository creation')}:</label>
78 78 </div>
79 79 <div class="select">
80 80 ${h.select('default_create','',c.create_choices)}
81 81 </div>
82 82 </div>
83 83 <div class="field">
84 84 <div class="label">
85 85 <label for="default_fork">${_('Repository forking')}:</label>
86 86 </div>
87 87 <div class="select">
88 88 ${h.select('default_fork','',c.fork_choices)}
89 89 </div>
90 90 </div>
91 91 <div class="buttons">
92 92 ${h.submit('save',_('Save'),class_="ui-btn large")}
93 93 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
94 94 </div>
95 95 </div>
96 96 </div>
97 97 ${h.end_form()}
98 98 </div>
99 99
100 100 <div style="min-height:780px" class="box box-right">
101 101 <!-- box / title -->
102 102 <div class="title">
103 103 <h5>${_('Default User Permissions')}</h5>
104 104 </div>
105 105
106 106 ## permissions overview
107 107 <div id="perms" class="table">
108 108 %for section in sorted(c.perm_user.permissions.keys()):
109 109 <div class="perms_section_head">${section.replace("_"," ").capitalize()}</div>
110 110 %if not c.perm_user.permissions[section]:
111 111 <span class="empty_data">${_('Nothing here yet')}</span>
112 112 %else:
113 113 <div id='tbl_list_wrap_${section}' class="yui-skin-sam">
114 114 <table id="tbl_list_${section}">
115 115 <thead>
116 116 <tr>
117 117 <th class="left">${_('Name')}</th>
118 118 <th class="left">${_('Permission')}</th>
119 119 <th class="left">${_('Edit Permission')}</th>
120 120 </thead>
121 121 <tbody>
122 122 %for k in c.perm_user.permissions[section]:
123 123 <%
124 124 if section != 'global':
125 125 section_perm = c.perm_user.permissions[section].get(k)
126 126 _perm = section_perm.split('.')[-1]
127 127 else:
128 128 _perm = section_perm = None
129 129 %>
130 130 <tr>
131 131 <td>
132 132 %if section == 'repositories':
133 133 <a href="${h.url('summary_home',repo_name=k)}">${k}</a>
134 134 %elif section == 'repositories_groups':
135 135 <a href="${h.url('repos_group_home',group_name=k)}">${k}</a>
136 136 %else:
137 137 ${h.get_permission_name(k)}
138 138 %endif
139 139 </td>
140 140 <td>
141 141 %if section == 'global':
142 142 ${h.bool2icon(k.split('.')[-1] != 'none')}
143 143 %else:
144 144 <span class="perm_tag ${_perm}">${section_perm}</span>
145 145 %endif
146 146 </td>
147 147 <td>
148 148 %if section == 'repositories':
149 149 <a href="${h.url('edit_repo',repo_name=k,anchor='permissions_manage')}">${_('edit')}</a>
150 150 %elif section == 'repositories_groups':
151 151 <a href="${h.url('edit_repos_group',group_name=k,anchor='permissions_manage')}">${_('edit')}</a>
152 152 %else:
153 153 --
154 154 %endif
155 155 </td>
156 156 </tr>
157 157 %endfor
158 158 </tbody>
159 159 </table>
160 160 </div>
161 161 %endif
162 162 %endfor
163 163 </div>
164 164 </div>
165 165 <div class="box box-left" style="clear:left">
166 166 <!-- box / title -->
167 167 <div class="title">
168 168 <h5>${_('Allowed IP addresses')}</h5>
169 169 </div>
170 170
171 171 <div class="ips_wrap">
172 172 <table class="noborder">
173 173 %if c.user_ip_map:
174 174 %for ip in c.user_ip_map:
175 175 <tr>
176 176 <td><div class="ip">${ip.ip_addr}</div></td>
177 177 <td><div class="ip">${h.ip_range(ip.ip_addr)}</div></td>
178 178 <td>
179 179 ${h.form(url('user_ips_delete', id=c.user.user_id),method='delete')}
180 180 ${h.hidden('del_ip',ip.ip_id)}
181 181 ${h.hidden('default_user', 'True')}
182 182 ${h.submit('remove_',_('delete'),id="remove_ip_%s" % ip.ip_id,
183 183 class_="delete_icon action_button", onclick="return confirm('"+_('Confirm to delete this ip: %s') % ip.ip_addr+"');")}
184 184 ${h.end_form()}
185 185 </td>
186 186 </tr>
187 187 %endfor
188 188 %else:
189 189 <tr><td><div class="ip">${_('All IP addresses are allowed')}</div></td></tr>
190 190 %endif
191 191 </table>
192 192 </div>
193 193
194 194 ${h.form(url('user_ips', id=c.user.user_id),method='put')}
195 195 <div class="form">
196 196 <!-- fields -->
197 197 <div class="fields">
198 198 <div class="field">
199 199 <div class="label">
200 200 <label for="new_ip">${_('New ip address')}:</label>
201 201 </div>
202 202 <div class="input">
203 203 ${h.hidden('default_user', 'True')}
204 204 ${h.text('new_ip', class_='medium')}
205 205 </div>
206 206 </div>
207 207 <div class="buttons">
208 208 ${h.submit('save',_('Add'),class_="ui-btn large")}
209 209 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
210 210 </div>
211 211 </div>
212 212 </div>
213 213 ${h.end_form()}
214 214 </div>
215 215 </%def>
@@ -1,340 +1,340 b''
1 1 <%page args="parent" />
2 2 <div class="box">
3 3 <!-- box / title -->
4 4 <div class="title">
5 5 <h5>
6 6 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" value="${_('quick filter...')}"/> ${parent.breadcrumbs()} <span id="repo_count">0</span> ${_('repositories')}
7 7 </h5>
8 8 %if c.rhodecode_user.username != 'default':
9 9 <ul class="links">
10 10 %if h.HasPermissionAny('hg.admin','hg.create.repository')() or h.HasReposGroupPermissionAny('group.write', 'group.admin')(c.group.group_name if c.group else None):
11 11 <li>
12 12 %if c.group:
13 13 <span>${h.link_to(_('Add repository'),h.url('admin_settings_create_repository',parent_group=c.group.group_id))}</span>
14 14 %if h.HasPermissionAny('hg.admin')() or h.HasReposGroupPermissionAny('group.admin')(c.group.group_name):
15 15 <span>${h.link_to(_(u'Add group'),h.url('new_repos_group', parent_group=c.group.group_id))}</span>
16 16 %endif
17 17 %else:
18 18 <span>${h.link_to(_('Add repository'),h.url('admin_settings_create_repository'))}</span>
19 19 %if h.HasPermissionAny('hg.admin')():
20 20 <span>${h.link_to(_(u'Add group'),h.url('new_repos_group'))}</span>
21 21 %endif
22 22 %endif
23 23 </li>
24 24 %endif
25 25 %if c.group and h.HasReposGroupPermissionAny('group.admin')(c.group.group_name):
26 26 <li>
27 27 <span>${h.link_to(_('Edit group'),h.url('edit_repos_group',group_name=c.group.group_name), title=_('You have admin right to this group, and can edit it'))}</span>
28 28 </li>
29 29 %endif
30 30 </ul>
31 31 %endif
32 32 </div>
33 33 <!-- end box / title -->
34 34 <div class="table">
35 35 % if c.groups:
36 36 <div id='groups_list_wrap' class="yui-skin-sam">
37 37 <table id="groups_list">
38 38 <thead>
39 39 <tr>
40 40 <th class="left"><a href="#">${_('Group name')}</a></th>
41 41 <th class="left"><a href="#">${_('Description')}</a></th>
42 42 ##<th class="left"><a href="#">${_('Number of repositories')}</a></th>
43 43 </tr>
44 44 </thead>
45 45
46 46 ## REPO GROUPS
47 47 % for gr in c.groups:
48 48 <tr>
49 49 <td>
50 50 <div style="white-space: nowrap">
51 <img class="icon" alt="${_('Repositories group')}" src="${h.url('/images/icons/database_link.png')}"/>
51 <img class="icon" alt="${_('Repository group')}" src="${h.url('/images/icons/database_link.png')}"/>
52 52 ${h.link_to(gr.name,url('repos_group_home',group_name=gr.group_name))}
53 53 </div>
54 54 </td>
55 55 %if c.visual.stylify_metatags:
56 56 <td>${h.urlify_text(h.desc_stylize(gr.group_description))}</td>
57 57 %else:
58 58 <td>${gr.group_description}</td>
59 59 %endif
60 60 ## this is commented out since for multi nested repos can be HEAVY!
61 61 ## in number of executed queries during traversing uncomment at will
62 62 ##<td><b>${gr.repositories_recursive_count}</b></td>
63 63 </tr>
64 64 % endfor
65 65 </table>
66 66 </div>
67 67 <div id="group-user-paginator" style="padding: 0px 0px 0px 0px"></div>
68 68 <div style="height: 20px"></div>
69 69 % endif
70 70 <div id="welcome" style="display:none;text-align:center">
71 71 <h1><a href="${h.url('home')}">${c.rhodecode_name} ${c.rhodecode_version}</a></h1>
72 72 </div>
73 73 <%cnt=0%>
74 74 <%namespace name="dt" file="/data_table/_dt_elements.html"/>
75 75 % if c.visual.lightweight_dashboard is False:
76 76 ## old full detailed version
77 77 <div id='repos_list_wrap' class="yui-skin-sam">
78 78 <table id="repos_list">
79 79 <thead>
80 80 <tr>
81 81 <th class="left"></th>
82 82 <th class="left">${_('Name')}</th>
83 83 <th class="left">${_('Description')}</th>
84 84 <th class="left">${_('Last change')}</th>
85 85 <th class="left">${_('Tip')}</th>
86 86 <th class="left">${_('Owner')}</th>
87 87 <th class="left">${_('Atom')}</th>
88 88 </tr>
89 89 </thead>
90 90 <tbody>
91 91 %for cnt,repo in enumerate(c.repos_list):
92 92 <tr class="parity${(cnt+1)%2}">
93 93 ##QUICK MENU
94 94 <td class="quick_repo_menu">
95 95 ${dt.quick_menu(repo['name'])}
96 96 </td>
97 97 ##REPO NAME AND ICONS
98 98 <td class="reponame">
99 99 ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],h.AttributeDict(repo['dbrepo_fork']),pageargs.get('short_repo_names'))}
100 100 </td>
101 101 ##DESCRIPTION
102 102 <td><span class="tooltip" title="${h.tooltip(repo['description'])}">
103 103 %if c.visual.stylify_metatags:
104 104 ${h.urlify_text(h.desc_stylize(h.truncate(repo['description'],60)))}</span>
105 105 %else:
106 106 ${h.truncate(repo['description'],60)}</span>
107 107 %endif
108 108 </td>
109 109 ##LAST CHANGE DATE
110 110 <td>
111 111 ${dt.last_change(repo['last_change'])}
112 112 </td>
113 113 ##LAST REVISION
114 114 <td>
115 115 ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])}
116 116 </td>
117 117 ##
118 118 <td title="${repo['contact']}">${h.person(repo['contact'])}</td>
119 119 <td>
120 120 ${dt.atom(repo['name'])}
121 121 </td>
122 122 </tr>
123 123 %endfor
124 124 </tbody>
125 125 </table>
126 126 </div>
127 127 % else:
128 128 ## lightweight version
129 129 <div class="yui-skin-sam" id="repos_list_wrap"></div>
130 130 <div id="user-paginator" style="padding: 0px 0px 0px 0px"></div>
131 131 % endif
132 132 </div>
133 133 </div>
134 134 % if c.visual.lightweight_dashboard is False:
135 135 <script>
136 136 YUD.get('repo_count').innerHTML = ${cnt+1 if cnt else 0};
137 137
138 138 // groups table sorting
139 139 var myColumnDefs = [
140 140 {key:"name",label:"${_('Group name')}",sortable:true,
141 141 sortOptions: { sortFunction: groupNameSort }},
142 142 {key:"desc",label:"${_('Description')}",sortable:true},
143 143 ];
144 144
145 145 var myDataSource = new YAHOO.util.DataSource(YUD.get("groups_list"));
146 146
147 147 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
148 148 myDataSource.responseSchema = {
149 149 fields: [
150 150 {key:"name"},
151 151 {key:"desc"},
152 152 ]
153 153 };
154 154
155 155 var myDataTable = new YAHOO.widget.DataTable("groups_list_wrap", myColumnDefs, myDataSource,{
156 156 sortedBy:{key:"name",dir:"asc"},
157 157 paginator: new YAHOO.widget.Paginator({
158 158 rowsPerPage: 50,
159 159 alwaysVisible: false,
160 160 template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}",
161 161 pageLinks: 5,
162 162 containerClass: 'pagination-wh',
163 163 currentPageClass: 'pager_curpage',
164 164 pageLinkClass: 'pager_link',
165 165 nextPageLinkLabel: '&gt;',
166 166 previousPageLinkLabel: '&lt;',
167 167 firstPageLinkLabel: '&lt;&lt;',
168 168 lastPageLinkLabel: '&gt;&gt;',
169 169 containers:['group-user-paginator']
170 170 }),
171 171 MSG_SORTASC:"${_('Click to sort ascending')}",
172 172 MSG_SORTDESC:"${_('Click to sort descending')}"
173 173 });
174 174
175 175 // main table sorting
176 176 var myColumnDefs = [
177 177 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
178 178 {key:"name",label:"${_('Name')}",sortable:true,
179 179 sortOptions: { sortFunction: nameSort }},
180 180 {key:"desc",label:"${_('Description')}",sortable:true},
181 181 {key:"last_change",label:"${_('Last Change')}",sortable:true,
182 182 sortOptions: { sortFunction: ageSort }},
183 183 {key:"tip",label:"${_('Tip')}",sortable:true,
184 184 sortOptions: { sortFunction: revisionSort }},
185 185 {key:"owner",label:"${_('Owner')}",sortable:true},
186 186 {key:"atom",label:"",sortable:false},
187 187 ];
188 188
189 189 var myDataSource = new YAHOO.util.DataSource(YUD.get("repos_list"));
190 190
191 191 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
192 192
193 193 myDataSource.responseSchema = {
194 194 fields: [
195 195 {key:"menu"},
196 196 //{key:"raw_name"},
197 197 {key:"name"},
198 198 {key:"desc"},
199 199 {key:"last_change"},
200 200 {key:"tip"},
201 201 {key:"owner"},
202 202 {key:"atom"},
203 203 ]
204 204 };
205 205
206 206 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,
207 207 {
208 208 sortedBy:{key:"name",dir:"asc"},
209 209 MSG_SORTASC:"${_('Click to sort ascending')}",
210 210 MSG_SORTDESC:"${_('Click to sort descending')}",
211 211 MSG_EMPTY:"${_('No records found.')}",
212 212 MSG_ERROR:"${_('Data error.')}",
213 213 MSG_LOADING:"${_('Loading...')}",
214 214 }
215 215 );
216 216 myDataTable.subscribe('postRenderEvent',function(oArgs) {
217 217 tooltip_activate();
218 218 quick_repo_menu();
219 219 var func = function(node){
220 220 return node.parentNode.parentNode.parentNode.parentNode;
221 221 }
222 222 q_filter('q_filter',YUQ('div.table tr td a.repo_name'),func);
223 223 });
224 224
225 225 </script>
226 226 % else:
227 227 <script>
228 228 var data = ${c.data|n};
229 229 var myDataSource = new YAHOO.util.DataSource(data);
230 230 myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
231 231
232 232 myDataSource.responseSchema = {
233 233 resultsList: "records",
234 234 fields: [
235 235 {key:"menu"},
236 236 {key:"raw_name"},
237 237 {key:"name"},
238 238 {key:"desc"},
239 239 {key:"last_change"},
240 240 {key:"last_changeset"},
241 241 {key:"owner"},
242 242 {key:"atom"},
243 243 ]
244 244 };
245 245 myDataSource.doBeforeCallback = function(req,raw,res,cb) {
246 246 // This is the filter function
247 247 var data = res.results || [],
248 248 filtered = [],
249 249 i,l;
250 250
251 251 if (req) {
252 252 req = req.toLowerCase();
253 253 for (i = 0; i<data.length; i++) {
254 254 var pos = data[i].raw_name.toLowerCase().indexOf(req)
255 255 if (pos != -1) {
256 256 filtered.push(data[i]);
257 257 }
258 258 }
259 259 res.results = filtered;
260 260 }
261 261 YUD.get('repo_count').innerHTML = res.results.length;
262 262 return res;
263 263 }
264 264
265 265 // main table sorting
266 266 var myColumnDefs = [
267 267 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
268 268 {key:"name",label:"${_('Name')}",sortable:true,
269 269 sortOptions: { sortFunction: nameSort }},
270 270 {key:"desc",label:"${_('Description')}",sortable:true},
271 271 {key:"last_change",label:"${_('Last Change')}",sortable:true,
272 272 sortOptions: { sortFunction: ageSort }},
273 273 {key:"last_changeset",label:"${_('Tip')}",sortable:true,
274 274 sortOptions: { sortFunction: revisionSort }},
275 275 {key:"owner",label:"${_('Owner')}",sortable:true},
276 276 {key:"atom",label:"",sortable:false},
277 277 ];
278 278
279 279 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,{
280 280 sortedBy:{key:"name",dir:"asc"},
281 281 paginator: new YAHOO.widget.Paginator({
282 282 rowsPerPage: ${c.visual.lightweight_dashboard_items},
283 283 alwaysVisible: false,
284 284 template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}",
285 285 pageLinks: 5,
286 286 containerClass: 'pagination-wh',
287 287 currentPageClass: 'pager_curpage',
288 288 pageLinkClass: 'pager_link',
289 289 nextPageLinkLabel: '&gt;',
290 290 previousPageLinkLabel: '&lt;',
291 291 firstPageLinkLabel: '&lt;&lt;',
292 292 lastPageLinkLabel: '&gt;&gt;',
293 293 containers:['user-paginator']
294 294 }),
295 295
296 296 MSG_SORTASC:"${_('Click to sort ascending')}",
297 297 MSG_SORTDESC:"${_('Click to sort descending')}",
298 298 MSG_EMPTY:"${_('No records found.')}",
299 299 MSG_ERROR:"${_('Data error.')}",
300 300 MSG_LOADING:"${_('Loading...')}",
301 301 }
302 302 );
303 303 myDataTable.subscribe('postRenderEvent',function(oArgs) {
304 304 tooltip_activate();
305 305 quick_repo_menu();
306 306 });
307 307
308 308 var filterTimeout = null;
309 309
310 310 updateFilter = function () {
311 311 // Reset timeout
312 312 filterTimeout = null;
313 313
314 314 // Reset sort
315 315 var state = myDataTable.getState();
316 316 state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC};
317 317
318 318 // Get filtered data
319 319 myDataSource.sendRequest(YUD.get('q_filter').value,{
320 320 success : myDataTable.onDataReturnInitializeTable,
321 321 failure : myDataTable.onDataReturnInitializeTable,
322 322 scope : myDataTable,
323 323 argument: state
324 324 });
325 325
326 326 };
327 327 YUE.on('q_filter','click',function(){
328 328 if(!YUD.hasClass('q_filter', 'loaded')){
329 329 YUD.get('q_filter').value = '';
330 330 //TODO: load here full list later to do search within groups
331 331 YUD.addClass('q_filter', 'loaded');
332 332 }
333 333 });
334 334
335 335 YUE.on('q_filter','keyup',function (e) {
336 336 clearTimeout(filterTimeout);
337 337 filterTimeout = setTimeout(updateFilter,600);
338 338 });
339 339 </script>
340 340 % endif
General Comments 0
You need to be logged in to leave comments. Login now