##// END OF EJS Templates
fixed issue with empty APIKEYS on registration #438
marcink -
r2248:72542dc5 beta
parent child Browse files
Show More
@@ -1,634 +1,635 b''
1 1 .. _changelog:
2 2
3 3 =========
4 4 Changelog
5 5 =========
6 6
7 7 1.3.5 (**2012-XX-XX**)
8 8 ----------------------
9 9
10 10 :status: in-progress
11 11 :branch: beta
12 12
13 13 news
14 14 ++++
15 15
16 16 - use ext_json for json module
17 17 - unified annotation view with file source view
18 18 - notification improvements, better inbox + css
19 19 - #419 don't strip passwords for login forms, make rhodecode
20 20 more compatible with LDAP servers
21 21 - Added HTTP_X_FORWARDED_FOR as another method of extracting
22 22 IP for pull/push logs. - moved all to base controller
23 23 - #415: Adding comment to changeset causes reload.
24 24 Comments are now added via ajax and doesn't reload the page
25 25 - #374 LDAP config is discarded when LDAP can't be activated
26 26 - limited push/pull operations are now logged for git in the journal
27 27 - bumped mercurial to 2.2.X series
28 28 - added support for displaying submodules in file-browser
29 29 - #421 added bookmarks in changlog view
30 30
31 31 fixes
32 32 +++++
33 33
34 34 - fixed dev-version marker for stable when served from source codes
35 35 - fixed missing permission checks on show forks page
36 36 - #418 cast to unicode fixes in notification objects
37 37 - #426 fixed mention extracting regex
38 38 - fixed remote-pulling for git remotes remopositories
39 39 - fixed #434: Error when accessing files or changesets of a git repository
40 40 with submodules
41 - fixed issue with empty APIKEYS for users after registration ref. #438
41 42
42 43 1.3.4 (**2012-03-28**)
43 44 ----------------------
44 45
45 46 news
46 47 ++++
47 48
48 49 - Whoosh logging is now controlled by the .ini files logging setup
49 50 - added clone-url into edit form on /settings page
50 51 - added help text into repo add/edit forms
51 52 - created rcextensions module with additional mappings (ref #322) and
52 53 post push/pull/create repo hooks callbacks
53 54 - implemented #377 Users view for his own permissions on account page
54 55 - #399 added inheritance of permissions for users group on repos groups
55 56 - #401 repository group is automatically pre-selected when adding repos
56 57 inside a repository group
57 58 - added alternative HTTP 403 response when client failed to authenticate. Helps
58 59 solving issues with Mercurial and LDAP
59 60 - #402 removed group prefix from repository name when listing repositories
60 61 inside a group
61 62 - added gravatars into permission view and permissions autocomplete
62 63 - #347 when running multiple RhodeCode instances, properly invalidates cache
63 64 for all registered servers
64 65
65 66 fixes
66 67 +++++
67 68
68 69 - fixed #390 cache invalidation problems on repos inside group
69 70 - fixed #385 clone by ID url was loosing proxy prefix in URL
70 71 - fixed some unicode problems with waitress
71 72 - fixed issue with escaping < and > in changeset commits
72 73 - fixed error occurring during recursive group creation in API
73 74 create_repo function
74 75 - fixed #393 py2.5 fixes for routes url generator
75 76 - fixed #397 Private repository groups shows up before login
76 77 - fixed #396 fixed problems with revoking users in nested groups
77 78 - fixed mysql unicode issues + specified InnoDB as default engine with
78 79 utf8 charset
79 80 - #406 trim long branch/tag names in changelog to not break UI
80 81
81 82 1.3.3 (**2012-03-02**)
82 83 ----------------------
83 84
84 85 news
85 86 ++++
86 87
87 88
88 89 fixes
89 90 +++++
90 91
91 92 - fixed some python2.5 compatibility issues
92 93 - fixed issues with removed repos was accidentally added as groups, after
93 94 full rescan of paths
94 95 - fixes #376 Cannot edit user (using container auth)
95 96 - fixes #378 Invalid image urls on changeset screen with proxy-prefix
96 97 configuration
97 98 - fixed initial sorting of repos inside repo group
98 99 - fixes issue when user tried to resubmit same permission into user/user_groups
99 100 - bumped beaker version that fixes #375 leap error bug
100 101 - fixed raw_changeset for git. It was generated with hg patch headers
101 102 - fixed vcs issue with last_changeset for filenodes
102 103 - fixed missing commit after hook delete
103 104 - fixed #372 issues with git operation detection that caused a security issue
104 105 for git repos
105 106
106 107 1.3.2 (**2012-02-28**)
107 108 ----------------------
108 109
109 110 news
110 111 ++++
111 112
112 113
113 114 fixes
114 115 +++++
115 116
116 117 - fixed git protocol issues with repos-groups
117 118 - fixed git remote repos validator that prevented from cloning remote git repos
118 119 - fixes #370 ending slashes fixes for repo and groups
119 120 - fixes #368 improved git-protocol detection to handle other clients
120 121 - fixes #366 When Setting Repository Group To Blank Repo Group Wont Be
121 122 Moved To Root
122 123 - fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys
123 124 - fixed #373 missing cascade drop on user_group_to_perm table
124 125
125 126 1.3.1 (**2012-02-27**)
126 127 ----------------------
127 128
128 129 news
129 130 ++++
130 131
131 132
132 133 fixes
133 134 +++++
134 135
135 136 - redirection loop occurs when remember-me wasn't checked during login
136 137 - fixes issues with git blob history generation
137 138 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
138 139
139 140 1.3.0 (**2012-02-26**)
140 141 ----------------------
141 142
142 143 news
143 144 ++++
144 145
145 146 - code review, inspired by github code-comments
146 147 - #215 rst and markdown README files support
147 148 - #252 Container-based and proxy pass-through authentication support
148 149 - #44 branch browser. Filtering of changelog by branches
149 150 - mercurial bookmarks support
150 151 - new hover top menu, optimized to add maximum size for important views
151 152 - configurable clone url template with possibility to specify protocol like
152 153 ssh:// or http:// and also manually alter other parts of clone_url.
153 154 - enabled largefiles extension by default
154 155 - optimized summary file pages and saved a lot of unused space in them
155 156 - #239 option to manually mark repository as fork
156 157 - #320 mapping of commit authors to RhodeCode users
157 158 - #304 hashes are displayed using monospace font
158 159 - diff configuration, toggle white lines and context lines
159 160 - #307 configurable diffs, whitespace toggle, increasing context lines
160 161 - sorting on branches, tags and bookmarks using YUI datatable
161 162 - improved file filter on files page
162 163 - implements #330 api method for listing nodes ar particular revision
163 164 - #73 added linking issues in commit messages to chosen issue tracker url
164 165 based on user defined regular expression
165 166 - added linking of changesets in commit messages
166 167 - new compact changelog with expandable commit messages
167 168 - firstname and lastname are optional in user creation
168 169 - #348 added post-create repository hook
169 170 - #212 global encoding settings is now configurable from .ini files
170 171 - #227 added repository groups permissions
171 172 - markdown gets codehilite extensions
172 173 - new API methods, delete_repositories, grante/revoke permissions for groups
173 174 and repos
174 175
175 176
176 177 fixes
177 178 +++++
178 179
179 180 - rewrote dbsession management for atomic operations, and better error handling
180 181 - fixed sorting of repo tables
181 182 - #326 escape of special html entities in diffs
182 183 - normalized user_name => username in api attributes
183 184 - fixes #298 ldap created users with mixed case emails created conflicts
184 185 on saving a form
185 186 - fixes issue when owner of a repo couldn't revoke permissions for users
186 187 and groups
187 188 - fixes #271 rare JSON serialization problem with statistics
188 189 - fixes #337 missing validation check for conflicting names of a group with a
189 190 repositories group
190 191 - #340 fixed session problem for mysql and celery tasks
191 192 - fixed #331 RhodeCode mangles repository names if the a repository group
192 193 contains the "full path" to the repositories
193 194 - #355 RhodeCode doesn't store encrypted LDAP passwords
194 195
195 196 1.2.5 (**2012-01-28**)
196 197 ----------------------
197 198
198 199 news
199 200 ++++
200 201
201 202 fixes
202 203 +++++
203 204
204 205 - #340 Celery complains about MySQL server gone away, added session cleanup
205 206 for celery tasks
206 207 - #341 "scanning for repositories in None" log message during Rescan was missing
207 208 a parameter
208 209 - fixed creating archives with subrepos. Some hooks were triggered during that
209 210 operation leading to crash.
210 211 - fixed missing email in account page.
211 212 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
212 213 forking on windows impossible
213 214
214 215 1.2.4 (**2012-01-19**)
215 216 ----------------------
216 217
217 218 news
218 219 ++++
219 220
220 221 - RhodeCode is bundled with mercurial series 2.0.X by default, with
221 222 full support to largefiles extension. Enabled by default in new installations
222 223 - #329 Ability to Add/Remove Groups to/from a Repository via AP
223 224 - added requires.txt file with requirements
224 225
225 226 fixes
226 227 +++++
227 228
228 229 - fixes db session issues with celery when emailing admins
229 230 - #331 RhodeCode mangles repository names if the a repository group
230 231 contains the "full path" to the repositories
231 232 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
232 233 - DB session cleanup after hg protocol operations, fixes issues with
233 234 `mysql has gone away` errors
234 235 - #333 doc fixes for get_repo api function
235 236 - #271 rare JSON serialization problem with statistics enabled
236 237 - #337 Fixes issues with validation of repository name conflicting with
237 238 a group name. A proper message is now displayed.
238 239 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
239 240 doesn't work
240 241 - #316 fixes issues with web description in hgrc files
241 242
242 243 1.2.3 (**2011-11-02**)
243 244 ----------------------
244 245
245 246 news
246 247 ++++
247 248
248 249 - added option to manage repos group for non admin users
249 250 - added following API methods for get_users, create_user, get_users_groups,
250 251 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
251 252 get_repo, create_repo, add_user_to_repo
252 253 - implements #237 added password confirmation for my account
253 254 and admin edit user.
254 255 - implements #291 email notification for global events are now sent to all
255 256 administrator users, and global config email.
256 257
257 258 fixes
258 259 +++++
259 260
260 261 - added option for passing auth method for smtp mailer
261 262 - #276 issue with adding a single user with id>10 to usergroups
262 263 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
263 264 - #288 fixes managing of repos in a group for non admin user
264 265
265 266 1.2.2 (**2011-10-17**)
266 267 ----------------------
267 268
268 269 news
269 270 ++++
270 271
271 272 - #226 repo groups are available by path instead of numerical id
272 273
273 274 fixes
274 275 +++++
275 276
276 277 - #259 Groups with the same name but with different parent group
277 278 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
278 279 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
279 280 - #265 ldap save fails sometimes on converting attributes to booleans,
280 281 added getter and setter into model that will prevent from this on db model level
281 282 - fixed problems with timestamps issues #251 and #213
282 283 - fixes #266 RhodeCode allows to create repo with the same name and in
283 284 the same parent as group
284 285 - fixes #245 Rescan of the repositories on Windows
285 286 - fixes #248 cannot edit repos inside a group on windows
286 287 - fixes #219 forking problems on windows
287 288
288 289 1.2.1 (**2011-10-08**)
289 290 ----------------------
290 291
291 292 news
292 293 ++++
293 294
294 295
295 296 fixes
296 297 +++++
297 298
298 299 - fixed problems with basic auth and push problems
299 300 - gui fixes
300 301 - fixed logger
301 302
302 303 1.2.0 (**2011-10-07**)
303 304 ----------------------
304 305
305 306 news
306 307 ++++
307 308
308 309 - implemented #47 repository groups
309 310 - implemented #89 Can setup google analytics code from settings menu
310 311 - implemented #91 added nicer looking archive urls with more download options
311 312 like tags, branches
312 313 - implemented #44 into file browsing, and added follow branch option
313 314 - implemented #84 downloads can be enabled/disabled for each repository
314 315 - anonymous repository can be cloned without having to pass default:default
315 316 into clone url
316 317 - fixed #90 whoosh indexer can index chooses repositories passed in command
317 318 line
318 319 - extended journal with day aggregates and paging
319 320 - implemented #107 source code lines highlight ranges
320 321 - implemented #93 customizable changelog on combined revision ranges -
321 322 equivalent of githubs compare view
322 323 - implemented #108 extended and more powerful LDAP configuration
323 324 - implemented #56 users groups
324 325 - major code rewrites optimized codes for speed and memory usage
325 326 - raw and diff downloads are now in git format
326 327 - setup command checks for write access to given path
327 328 - fixed many issues with international characters and unicode. It uses utf8
328 329 decode with replace to provide less errors even with non utf8 encoded strings
329 330 - #125 added API KEY access to feeds
330 331 - #109 Repository can be created from external Mercurial link (aka. remote
331 332 repository, and manually updated (via pull) from admin panel
332 333 - beta git support - push/pull server + basic view for git repos
333 334 - added followers page and forks page
334 335 - server side file creation (with binary file upload interface)
335 336 and edition with commits powered by codemirror
336 337 - #111 file browser file finder, quick lookup files on whole file tree
337 338 - added quick login sliding menu into main page
338 339 - changelog uses lazy loading of affected files details, in some scenarios
339 340 this can improve speed of changelog page dramatically especially for
340 341 larger repositories.
341 342 - implements #214 added support for downloading subrepos in download menu.
342 343 - Added basic API for direct operations on rhodecode via JSON
343 344 - Implemented advanced hook management
344 345
345 346 fixes
346 347 +++++
347 348
348 349 - fixed file browser bug, when switching into given form revision the url was
349 350 not changing
350 351 - fixed propagation to error controller on simplehg and simplegit middlewares
351 352 - fixed error when trying to make a download on empty repository
352 353 - fixed problem with '[' chars in commit messages in journal
353 354 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
354 355 - journal fork fixes
355 356 - removed issue with space inside renamed repository after deletion
356 357 - fixed strange issue on formencode imports
357 358 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
358 359 - #150 fixes for errors on repositories mapped in db but corrupted in
359 360 filesystem
360 361 - fixed problem with ascendant characters in realm #181
361 362 - fixed problem with sqlite file based database connection pool
362 363 - whoosh indexer and code stats share the same dynamic extensions map
363 364 - fixes #188 - relationship delete of repo_to_perm entry on user removal
364 365 - fixes issue #189 Trending source files shows "show more" when no more exist
365 366 - fixes issue #197 Relative paths for pidlocks
366 367 - fixes issue #198 password will require only 3 chars now for login form
367 368 - fixes issue #199 wrong redirection for non admin users after creating a repository
368 369 - fixes issues #202, bad db constraint made impossible to attach same group
369 370 more than one time. Affects only mysql/postgres
370 371 - fixes #218 os.kill patch for windows was missing sig param
371 372 - improved rendering of dag (they are not trimmed anymore when number of
372 373 heads exceeds 5)
373 374
374 375 1.1.8 (**2011-04-12**)
375 376 ----------------------
376 377
377 378 news
378 379 ++++
379 380
380 381 - improved windows support
381 382
382 383 fixes
383 384 +++++
384 385
385 386 - fixed #140 freeze of python dateutil library, since new version is python2.x
386 387 incompatible
387 388 - setup-app will check for write permission in given path
388 389 - cleaned up license info issue #149
389 390 - fixes for issues #137,#116 and problems with unicode and accented characters.
390 391 - fixes crashes on gravatar, when passed in email as unicode
391 392 - fixed tooltip flickering problems
392 393 - fixed came_from redirection on windows
393 394 - fixed logging modules, and sql formatters
394 395 - windows fixes for os.kill issue #133
395 396 - fixes path splitting for windows issues #148
396 397 - fixed issue #143 wrong import on migration to 1.1.X
397 398 - fixed problems with displaying binary files, thanks to Thomas Waldmann
398 399 - removed name from archive files since it's breaking ui for long repo names
399 400 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
400 401 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
401 402 Thomas Waldmann
402 403 - fixed issue #166 summary pager was skipping 10 revisions on second page
403 404
404 405
405 406 1.1.7 (**2011-03-23**)
406 407 ----------------------
407 408
408 409 news
409 410 ++++
410 411
411 412 fixes
412 413 +++++
413 414
414 415 - fixed (again) #136 installation support for FreeBSD
415 416
416 417
417 418 1.1.6 (**2011-03-21**)
418 419 ----------------------
419 420
420 421 news
421 422 ++++
422 423
423 424 fixes
424 425 +++++
425 426
426 427 - fixed #136 installation support for FreeBSD
427 428 - RhodeCode will check for python version during installation
428 429
429 430 1.1.5 (**2011-03-17**)
430 431 ----------------------
431 432
432 433 news
433 434 ++++
434 435
435 436 - basic windows support, by exchanging pybcrypt into sha256 for windows only
436 437 highly inspired by idea of mantis406
437 438
438 439 fixes
439 440 +++++
440 441
441 442 - fixed sorting by author in main page
442 443 - fixed crashes with diffs on binary files
443 444 - fixed #131 problem with boolean values for LDAP
444 445 - fixed #122 mysql problems thanks to striker69
445 446 - fixed problem with errors on calling raw/raw_files/annotate functions
446 447 with unknown revisions
447 448 - fixed returned rawfiles attachment names with international character
448 449 - cleaned out docs, big thanks to Jason Harris
449 450
450 451 1.1.4 (**2011-02-19**)
451 452 ----------------------
452 453
453 454 news
454 455 ++++
455 456
456 457 fixes
457 458 +++++
458 459
459 460 - fixed formencode import problem on settings page, that caused server crash
460 461 when that page was accessed as first after server start
461 462 - journal fixes
462 463 - fixed option to access repository just by entering http://server/<repo_name>
463 464
464 465 1.1.3 (**2011-02-16**)
465 466 ----------------------
466 467
467 468 news
468 469 ++++
469 470
470 471 - implemented #102 allowing the '.' character in username
471 472 - added option to access repository just by entering http://server/<repo_name>
472 473 - celery task ignores result for better performance
473 474
474 475 fixes
475 476 +++++
476 477
477 478 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
478 479 apollo13 and Johan Walles
479 480 - small fixes in journal
480 481 - fixed problems with getting setting for celery from .ini files
481 482 - registration, password reset and login boxes share the same title as main
482 483 application now
483 484 - fixed #113: to high permissions to fork repository
484 485 - fixed problem with '[' chars in commit messages in journal
485 486 - removed issue with space inside renamed repository after deletion
486 487 - db transaction fixes when filesystem repository creation failed
487 488 - fixed #106 relation issues on databases different than sqlite
488 489 - fixed static files paths links to use of url() method
489 490
490 491 1.1.2 (**2011-01-12**)
491 492 ----------------------
492 493
493 494 news
494 495 ++++
495 496
496 497
497 498 fixes
498 499 +++++
499 500
500 501 - fixes #98 protection against float division of percentage stats
501 502 - fixed graph bug
502 503 - forced webhelpers version since it was making troubles during installation
503 504
504 505 1.1.1 (**2011-01-06**)
505 506 ----------------------
506 507
507 508 news
508 509 ++++
509 510
510 511 - added force https option into ini files for easier https usage (no need to
511 512 set server headers with this options)
512 513 - small css updates
513 514
514 515 fixes
515 516 +++++
516 517
517 518 - fixed #96 redirect loop on files view on repositories without changesets
518 519 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
519 520 and server crashed with errors
520 521 - fixed large tooltips problems on main page
521 522 - fixed #92 whoosh indexer is more error proof
522 523
523 524 1.1.0 (**2010-12-18**)
524 525 ----------------------
525 526
526 527 news
527 528 ++++
528 529
529 530 - rewrite of internals for vcs >=0.1.10
530 531 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
531 532 with older clients
532 533 - anonymous access, authentication via ldap
533 534 - performance upgrade for cached repos list - each repository has its own
534 535 cache that's invalidated when needed.
535 536 - performance upgrades on repositories with large amount of commits (20K+)
536 537 - main page quick filter for filtering repositories
537 538 - user dashboards with ability to follow chosen repositories actions
538 539 - sends email to admin on new user registration
539 540 - added cache/statistics reset options into repository settings
540 541 - more detailed action logger (based on hooks) with pushed changesets lists
541 542 and options to disable those hooks from admin panel
542 543 - introduced new enhanced changelog for merges that shows more accurate results
543 544 - new improved and faster code stats (based on pygments lexers mapping tables,
544 545 showing up to 10 trending sources for each repository. Additionally stats
545 546 can be disabled in repository settings.
546 547 - gui optimizations, fixed application width to 1024px
547 548 - added cut off (for large files/changesets) limit into config files
548 549 - whoosh, celeryd, upgrade moved to paster command
549 550 - other than sqlite database backends can be used
550 551
551 552 fixes
552 553 +++++
553 554
554 555 - fixes #61 forked repo was showing only after cache expired
555 556 - fixes #76 no confirmation on user deletes
556 557 - fixes #66 Name field misspelled
557 558 - fixes #72 block user removal when he owns repositories
558 559 - fixes #69 added password confirmation fields
559 560 - fixes #87 RhodeCode crashes occasionally on updating repository owner
560 561 - fixes #82 broken annotations on files with more than 1 blank line at the end
561 562 - a lot of fixes and tweaks for file browser
562 563 - fixed detached session issues
563 564 - fixed when user had no repos he would see all repos listed in my account
564 565 - fixed ui() instance bug when global hgrc settings was loaded for server
565 566 instance and all hgrc options were merged with our db ui() object
566 567 - numerous small bugfixes
567 568
568 569 (special thanks for TkSoh for detailed feedback)
569 570
570 571
571 572 1.0.2 (**2010-11-12**)
572 573 ----------------------
573 574
574 575 news
575 576 ++++
576 577
577 578 - tested under python2.7
578 579 - bumped sqlalchemy and celery versions
579 580
580 581 fixes
581 582 +++++
582 583
583 584 - fixed #59 missing graph.js
584 585 - fixed repo_size crash when repository had broken symlinks
585 586 - fixed python2.5 crashes.
586 587
587 588
588 589 1.0.1 (**2010-11-10**)
589 590 ----------------------
590 591
591 592 news
592 593 ++++
593 594
594 595 - small css updated
595 596
596 597 fixes
597 598 +++++
598 599
599 600 - fixed #53 python2.5 incompatible enumerate calls
600 601 - fixed #52 disable mercurial extension for web
601 602 - fixed #51 deleting repositories don't delete it's dependent objects
602 603
603 604
604 605 1.0.0 (**2010-11-02**)
605 606 ----------------------
606 607
607 608 - security bugfix simplehg wasn't checking for permissions on commands
608 609 other than pull or push.
609 610 - fixed doubled messages after push or pull in admin journal
610 611 - templating and css corrections, fixed repo switcher on chrome, updated titles
611 612 - admin menu accessible from options menu on repository view
612 613 - permissions cached queries
613 614
614 615 1.0.0rc4 (**2010-10-12**)
615 616 --------------------------
616 617
617 618 - fixed python2.5 missing simplejson imports (thanks to Jens BΓ€ckman)
618 619 - removed cache_manager settings from sqlalchemy meta
619 620 - added sqlalchemy cache settings to ini files
620 621 - validated password length and added second try of failure on paster setup-app
621 622 - fixed setup database destroy prompt even when there was no db
622 623
623 624
624 625 1.0.0rc3 (**2010-10-11**)
625 626 -------------------------
626 627
627 628 - fixed i18n during installation.
628 629
629 630 1.0.0rc2 (**2010-10-11**)
630 631 -------------------------
631 632
632 633 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
633 634 occure. After vcs is fixed it'll be put back again.
634 635 - templating/css rewrites, optimized css. No newline at end of file
@@ -1,1292 +1,1293 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 from collections import defaultdict
31 31
32 32 from sqlalchemy import *
33 33 from sqlalchemy.ext.hybrid import hybrid_property
34 34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 35 from beaker.cache import cache_region, region_invalidate
36 36
37 37 from rhodecode.lib.vcs import get_backend
38 38 from rhodecode.lib.vcs.utils.helpers import get_scm
39 39 from rhodecode.lib.vcs.exceptions import VCSError
40 40 from rhodecode.lib.vcs.utils.lazy import LazyProperty
41 41
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
43 43 safe_unicode
44 44 from rhodecode.lib.compat import json
45 45 from rhodecode.lib.caching_query import FromCache
46 46
47 47 from rhodecode.model.meta import Base, Session
48 48 import hashlib
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53 #==============================================================================
54 54 # BASE CLASSES
55 55 #==============================================================================
56 56
57 57 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
58 58
59 59
60 60 class ModelSerializer(json.JSONEncoder):
61 61 """
62 62 Simple Serializer for JSON,
63 63
64 64 usage::
65 65
66 66 to make object customized for serialization implement a __json__
67 67 method that will return a dict for serialization into json
68 68
69 69 example::
70 70
71 71 class Task(object):
72 72
73 73 def __init__(self, name, value):
74 74 self.name = name
75 75 self.value = value
76 76
77 77 def __json__(self):
78 78 return dict(name=self.name,
79 79 value=self.value)
80 80
81 81 """
82 82
83 83 def default(self, obj):
84 84
85 85 if hasattr(obj, '__json__'):
86 86 return obj.__json__()
87 87 else:
88 88 return json.JSONEncoder.default(self, obj)
89 89
90 90
91 91 class BaseModel(object):
92 92 """
93 93 Base Model for all classess
94 94 """
95 95
96 96 @classmethod
97 97 def _get_keys(cls):
98 98 """return column names for this model """
99 99 return class_mapper(cls).c.keys()
100 100
101 101 def get_dict(self):
102 102 """
103 103 return dict with keys and values corresponding
104 104 to this model data """
105 105
106 106 d = {}
107 107 for k in self._get_keys():
108 108 d[k] = getattr(self, k)
109 109
110 110 # also use __json__() if present to get additional fields
111 111 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
112 112 d[k] = val
113 113 return d
114 114
115 115 def get_appstruct(self):
116 116 """return list with keys and values tupples corresponding
117 117 to this model data """
118 118
119 119 l = []
120 120 for k in self._get_keys():
121 121 l.append((k, getattr(self, k),))
122 122 return l
123 123
124 124 def populate_obj(self, populate_dict):
125 125 """populate model with data from given populate_dict"""
126 126
127 127 for k in self._get_keys():
128 128 if k in populate_dict:
129 129 setattr(self, k, populate_dict[k])
130 130
131 131 @classmethod
132 132 def query(cls):
133 133 return Session.query(cls)
134 134
135 135 @classmethod
136 136 def get(cls, id_):
137 137 if id_:
138 138 return cls.query().get(id_)
139 139
140 140 @classmethod
141 141 def getAll(cls):
142 142 return cls.query().all()
143 143
144 144 @classmethod
145 145 def delete(cls, id_):
146 146 obj = cls.query().get(id_)
147 147 Session.delete(obj)
148 148
149 149 def __repr__(self):
150 150 if hasattr(self, '__unicode__'):
151 151 # python repr needs to return str
152 152 return safe_str(self.__unicode__())
153 153 return '<DB:%s>' % (self.__class__.__name__)
154 154
155 155 class RhodeCodeSetting(Base, BaseModel):
156 156 __tablename__ = 'rhodecode_settings'
157 157 __table_args__ = (
158 158 UniqueConstraint('app_settings_name'),
159 159 {'extend_existing': True, 'mysql_engine':'InnoDB',
160 160 'mysql_charset': 'utf8'}
161 161 )
162 162 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
163 163 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
164 164 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
165 165
166 166 def __init__(self, k='', v=''):
167 167 self.app_settings_name = k
168 168 self.app_settings_value = v
169 169
170 170 @validates('_app_settings_value')
171 171 def validate_settings_value(self, key, val):
172 172 assert type(val) == unicode
173 173 return val
174 174
175 175 @hybrid_property
176 176 def app_settings_value(self):
177 177 v = self._app_settings_value
178 178 if self.app_settings_name == 'ldap_active':
179 179 v = str2bool(v)
180 180 return v
181 181
182 182 @app_settings_value.setter
183 183 def app_settings_value(self, val):
184 184 """
185 185 Setter that will always make sure we use unicode in app_settings_value
186 186
187 187 :param val:
188 188 """
189 189 self._app_settings_value = safe_unicode(val)
190 190
191 191 def __unicode__(self):
192 192 return u"<%s('%s:%s')>" % (
193 193 self.__class__.__name__,
194 194 self.app_settings_name, self.app_settings_value
195 195 )
196 196
197 197 @classmethod
198 198 def get_by_name(cls, ldap_key):
199 199 return cls.query()\
200 200 .filter(cls.app_settings_name == ldap_key).scalar()
201 201
202 202 @classmethod
203 203 def get_app_settings(cls, cache=False):
204 204
205 205 ret = cls.query()
206 206
207 207 if cache:
208 208 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
209 209
210 210 if not ret:
211 211 raise Exception('Could not get application settings !')
212 212 settings = {}
213 213 for each in ret:
214 214 settings['rhodecode_' + each.app_settings_name] = \
215 215 each.app_settings_value
216 216
217 217 return settings
218 218
219 219 @classmethod
220 220 def get_ldap_settings(cls, cache=False):
221 221 ret = cls.query()\
222 222 .filter(cls.app_settings_name.startswith('ldap_')).all()
223 223 fd = {}
224 224 for row in ret:
225 225 fd.update({row.app_settings_name:row.app_settings_value})
226 226
227 227 return fd
228 228
229 229
230 230 class RhodeCodeUi(Base, BaseModel):
231 231 __tablename__ = 'rhodecode_ui'
232 232 __table_args__ = (
233 233 UniqueConstraint('ui_key'),
234 234 {'extend_existing': True, 'mysql_engine':'InnoDB',
235 235 'mysql_charset': 'utf8'}
236 236 )
237 237
238 238 HOOK_UPDATE = 'changegroup.update'
239 239 HOOK_REPO_SIZE = 'changegroup.repo_size'
240 240 HOOK_PUSH = 'pretxnchangegroup.push_logger'
241 241 HOOK_PULL = 'preoutgoing.pull_logger'
242 242
243 243 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
244 244 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
245 245 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
246 246 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
247 247 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
248 248
249 249 @classmethod
250 250 def get_by_key(cls, key):
251 251 return cls.query().filter(cls.ui_key == key)
252 252
253 253 @classmethod
254 254 def get_builtin_hooks(cls):
255 255 q = cls.query()
256 256 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
257 257 cls.HOOK_REPO_SIZE,
258 258 cls.HOOK_PUSH, cls.HOOK_PULL]))
259 259 return q.all()
260 260
261 261 @classmethod
262 262 def get_custom_hooks(cls):
263 263 q = cls.query()
264 264 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
265 265 cls.HOOK_REPO_SIZE,
266 266 cls.HOOK_PUSH, cls.HOOK_PULL]))
267 267 q = q.filter(cls.ui_section == 'hooks')
268 268 return q.all()
269 269
270 270 @classmethod
271 271 def create_or_update_hook(cls, key, val):
272 272 new_ui = cls.get_by_key(key).scalar() or cls()
273 273 new_ui.ui_section = 'hooks'
274 274 new_ui.ui_active = True
275 275 new_ui.ui_key = key
276 276 new_ui.ui_value = val
277 277
278 278 Session.add(new_ui)
279 279
280 280
281 281 class User(Base, BaseModel):
282 282 __tablename__ = 'users'
283 283 __table_args__ = (
284 284 UniqueConstraint('username'), UniqueConstraint('email'),
285 285 {'extend_existing': True, 'mysql_engine':'InnoDB',
286 286 'mysql_charset': 'utf8'}
287 287 )
288 288 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
289 289 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
290 290 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
291 291 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
292 292 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
293 293 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
294 294 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
295 295 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
296 296 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
297 297 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
298 298 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
299 299
300 300 user_log = relationship('UserLog', cascade='all')
301 301 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
302 302
303 303 repositories = relationship('Repository')
304 304 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
305 305 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
306 306 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
307 307
308 308 group_member = relationship('UsersGroupMember', cascade='all')
309 309
310 310 notifications = relationship('UserNotification', cascade='all')
311 311 # notifications assigned to this user
312 312 user_created_notifications = relationship('Notification', cascade='all')
313 313 # comments created by this user
314 314 user_comments = relationship('ChangesetComment', cascade='all')
315 315
316 316 @hybrid_property
317 317 def email(self):
318 318 return self._email
319 319
320 320 @email.setter
321 321 def email(self, val):
322 322 self._email = val.lower() if val else None
323 323
324 324 @property
325 325 def full_name(self):
326 326 return '%s %s' % (self.name, self.lastname)
327 327
328 328 @property
329 329 def full_name_or_username(self):
330 330 return ('%s %s' % (self.name, self.lastname)
331 331 if (self.name and self.lastname) else self.username)
332 332
333 333 @property
334 334 def full_contact(self):
335 335 return '%s %s <%s>' % (self.name, self.lastname, self.email)
336 336
337 337 @property
338 338 def short_contact(self):
339 339 return '%s %s' % (self.name, self.lastname)
340 340
341 341 @property
342 342 def is_admin(self):
343 343 return self.admin
344 344
345 345 def __unicode__(self):
346 346 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
347 347 self.user_id, self.username)
348 348
349 349 @classmethod
350 350 def get_by_username(cls, username, case_insensitive=False, cache=False):
351 351 if case_insensitive:
352 352 q = cls.query().filter(cls.username.ilike(username))
353 353 else:
354 354 q = cls.query().filter(cls.username == username)
355 355
356 356 if cache:
357 357 q = q.options(FromCache(
358 358 "sql_cache_short",
359 359 "get_user_%s" % _hash_key(username)
360 360 )
361 361 )
362 362 return q.scalar()
363 363
364 364 @classmethod
365 365 def get_by_api_key(cls, api_key, cache=False):
366 366 q = cls.query().filter(cls.api_key == api_key)
367 367
368 368 if cache:
369 369 q = q.options(FromCache("sql_cache_short",
370 370 "get_api_key_%s" % api_key))
371 371 return q.scalar()
372 372
373 373 @classmethod
374 374 def get_by_email(cls, email, case_insensitive=False, cache=False):
375 375 if case_insensitive:
376 376 q = cls.query().filter(cls.email.ilike(email))
377 377 else:
378 378 q = cls.query().filter(cls.email == email)
379 379
380 380 if cache:
381 381 q = q.options(FromCache("sql_cache_short",
382 382 "get_api_key_%s" % email))
383 383 return q.scalar()
384 384
385 385 def update_lastlogin(self):
386 386 """Update user lastlogin"""
387 387 self.last_login = datetime.datetime.now()
388 388 Session.add(self)
389 389 log.debug('updated user %s lastlogin' % self.username)
390 390
391 391 def __json__(self):
392 392 return dict(
393 393 user_id=self.user_id,
394 394 first_name=self.name,
395 395 last_name=self.lastname,
396 396 email=self.email,
397 397 full_name=self.full_name,
398 398 full_name_or_username=self.full_name_or_username,
399 399 short_contact=self.short_contact,
400 400 full_contact=self.full_contact
401 401 )
402 402
403 403
404 404 class UserLog(Base, BaseModel):
405 405 __tablename__ = 'user_logs'
406 406 __table_args__ = (
407 407 {'extend_existing': True, 'mysql_engine':'InnoDB',
408 408 'mysql_charset': 'utf8'},
409 409 )
410 410 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
411 411 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
412 412 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
413 413 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
414 414 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
415 415 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
416 416 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
417 417
418 418 @property
419 419 def action_as_day(self):
420 420 return datetime.date(*self.action_date.timetuple()[:3])
421 421
422 422 user = relationship('User')
423 423 repository = relationship('Repository', cascade='')
424 424
425 425
426 426 class UsersGroup(Base, BaseModel):
427 427 __tablename__ = 'users_groups'
428 428 __table_args__ = (
429 429 {'extend_existing': True, 'mysql_engine':'InnoDB',
430 430 'mysql_charset': 'utf8'},
431 431 )
432 432
433 433 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
434 434 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
435 435 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
436 436
437 437 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
438 438 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
439 439 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
440 440
441 441 def __unicode__(self):
442 442 return u'<userGroup(%s)>' % (self.users_group_name)
443 443
444 444 @classmethod
445 445 def get_by_group_name(cls, group_name, cache=False,
446 446 case_insensitive=False):
447 447 if case_insensitive:
448 448 q = cls.query().filter(cls.users_group_name.ilike(group_name))
449 449 else:
450 450 q = cls.query().filter(cls.users_group_name == group_name)
451 451 if cache:
452 452 q = q.options(FromCache(
453 453 "sql_cache_short",
454 454 "get_user_%s" % _hash_key(group_name)
455 455 )
456 456 )
457 457 return q.scalar()
458 458
459 459 @classmethod
460 460 def get(cls, users_group_id, cache=False):
461 461 users_group = cls.query()
462 462 if cache:
463 463 users_group = users_group.options(FromCache("sql_cache_short",
464 464 "get_users_group_%s" % users_group_id))
465 465 return users_group.get(users_group_id)
466 466
467 467
468 468 class UsersGroupMember(Base, BaseModel):
469 469 __tablename__ = 'users_groups_members'
470 470 __table_args__ = (
471 471 {'extend_existing': True, 'mysql_engine':'InnoDB',
472 472 'mysql_charset': 'utf8'},
473 473 )
474 474
475 475 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
476 476 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
477 477 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
478 478
479 479 user = relationship('User', lazy='joined')
480 480 users_group = relationship('UsersGroup')
481 481
482 482 def __init__(self, gr_id='', u_id=''):
483 483 self.users_group_id = gr_id
484 484 self.user_id = u_id
485 485
486 486
487 487 class Repository(Base, BaseModel):
488 488 __tablename__ = 'repositories'
489 489 __table_args__ = (
490 490 UniqueConstraint('repo_name'),
491 491 {'extend_existing': True, 'mysql_engine':'InnoDB',
492 492 'mysql_charset': 'utf8'},
493 493 )
494 494
495 495 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
496 496 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
497 497 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
498 498 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
499 499 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
500 500 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
501 501 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
502 502 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
503 503 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
504 504 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
505 505
506 506 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
507 507 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
508 508
509 509 user = relationship('User')
510 510 fork = relationship('Repository', remote_side=repo_id)
511 511 group = relationship('RepoGroup')
512 512 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
513 513 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
514 514 stats = relationship('Statistics', cascade='all', uselist=False)
515 515
516 516 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
517 517
518 518 logs = relationship('UserLog')
519 519
520 520 def __unicode__(self):
521 521 return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id,
522 522 self.repo_name)
523 523
524 524 @classmethod
525 525 def url_sep(cls):
526 526 return '/'
527 527
528 528 @classmethod
529 529 def get_by_repo_name(cls, repo_name):
530 530 q = Session.query(cls).filter(cls.repo_name == repo_name)
531 531 q = q.options(joinedload(Repository.fork))\
532 532 .options(joinedload(Repository.user))\
533 533 .options(joinedload(Repository.group))
534 534 return q.scalar()
535 535
536 536 @classmethod
537 537 def get_repo_forks(cls, repo_id):
538 538 return cls.query().filter(Repository.fork_id == repo_id)
539 539
540 540 @classmethod
541 541 def base_path(cls):
542 542 """
543 543 Returns base path when all repos are stored
544 544
545 545 :param cls:
546 546 """
547 547 q = Session.query(RhodeCodeUi)\
548 548 .filter(RhodeCodeUi.ui_key == cls.url_sep())
549 549 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
550 550 return q.one().ui_value
551 551
552 552 @property
553 553 def just_name(self):
554 554 return self.repo_name.split(Repository.url_sep())[-1]
555 555
556 556 @property
557 557 def groups_with_parents(self):
558 558 groups = []
559 559 if self.group is None:
560 560 return groups
561 561
562 562 cur_gr = self.group
563 563 groups.insert(0, cur_gr)
564 564 while 1:
565 565 gr = getattr(cur_gr, 'parent_group', None)
566 566 cur_gr = cur_gr.parent_group
567 567 if gr is None:
568 568 break
569 569 groups.insert(0, gr)
570 570
571 571 return groups
572 572
573 573 @property
574 574 def groups_and_repo(self):
575 575 return self.groups_with_parents, self.just_name
576 576
577 577 @LazyProperty
578 578 def repo_path(self):
579 579 """
580 580 Returns base full path for that repository means where it actually
581 581 exists on a filesystem
582 582 """
583 583 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
584 584 Repository.url_sep())
585 585 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
586 586 return q.one().ui_value
587 587
588 588 @property
589 589 def repo_full_path(self):
590 590 p = [self.repo_path]
591 591 # we need to split the name by / since this is how we store the
592 592 # names in the database, but that eventually needs to be converted
593 593 # into a valid system path
594 594 p += self.repo_name.split(Repository.url_sep())
595 595 return os.path.join(*p)
596 596
597 597 def get_new_name(self, repo_name):
598 598 """
599 599 returns new full repository name based on assigned group and new new
600 600
601 601 :param group_name:
602 602 """
603 603 path_prefix = self.group.full_path_splitted if self.group else []
604 604 return Repository.url_sep().join(path_prefix + [repo_name])
605 605
606 606 @property
607 607 def _ui(self):
608 608 """
609 609 Creates an db based ui object for this repository
610 610 """
611 611 from mercurial import ui
612 612 from mercurial import config
613 613 baseui = ui.ui()
614 614
615 615 #clean the baseui object
616 616 baseui._ocfg = config.config()
617 617 baseui._ucfg = config.config()
618 618 baseui._tcfg = config.config()
619 619
620 620 ret = RhodeCodeUi.query()\
621 621 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
622 622
623 623 hg_ui = ret
624 624 for ui_ in hg_ui:
625 625 if ui_.ui_active:
626 626 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
627 627 ui_.ui_key, ui_.ui_value)
628 628 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
629 629
630 630 return baseui
631 631
632 632 @classmethod
633 633 def is_valid(cls, repo_name):
634 634 """
635 635 returns True if given repo name is a valid filesystem repository
636 636
637 637 :param cls:
638 638 :param repo_name:
639 639 """
640 640 from rhodecode.lib.utils import is_valid_repo
641 641
642 642 return is_valid_repo(repo_name, cls.base_path())
643 643
644 644 #==========================================================================
645 645 # SCM PROPERTIES
646 646 #==========================================================================
647 647
648 648 def get_changeset(self, rev):
649 649 return get_changeset_safe(self.scm_instance, rev)
650 650
651 651 @property
652 652 def tip(self):
653 653 return self.get_changeset('tip')
654 654
655 655 @property
656 656 def author(self):
657 657 return self.tip.author
658 658
659 659 @property
660 660 def last_change(self):
661 661 return self.scm_instance.last_change
662 662
663 663 def comments(self, revisions=None):
664 664 """
665 665 Returns comments for this repository grouped by revisions
666 666
667 667 :param revisions: filter query by revisions only
668 668 """
669 669 cmts = ChangesetComment.query()\
670 670 .filter(ChangesetComment.repo == self)
671 671 if revisions:
672 672 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
673 673 grouped = defaultdict(list)
674 674 for cmt in cmts.all():
675 675 grouped[cmt.revision].append(cmt)
676 676 return grouped
677 677
678 678 #==========================================================================
679 679 # SCM CACHE INSTANCE
680 680 #==========================================================================
681 681
682 682 @property
683 683 def invalidate(self):
684 684 return CacheInvalidation.invalidate(self.repo_name)
685 685
686 686 def set_invalidate(self):
687 687 """
688 688 set a cache for invalidation for this instance
689 689 """
690 690 CacheInvalidation.set_invalidate(self.repo_name)
691 691
692 692 @LazyProperty
693 693 def scm_instance(self):
694 694 return self.__get_instance()
695 695
696 696 @property
697 697 def scm_instance_cached(self):
698 698 @cache_region('long_term')
699 699 def _c(repo_name):
700 700 return self.__get_instance()
701 701 rn = self.repo_name
702 702 log.debug('Getting cached instance of repo')
703 703 inv = self.invalidate
704 704 if inv is not None:
705 705 region_invalidate(_c, None, rn)
706 706 # update our cache
707 707 CacheInvalidation.set_valid(inv.cache_key)
708 708 return _c(rn)
709 709
710 710 def __get_instance(self):
711 711 repo_full_path = self.repo_full_path
712 712 try:
713 713 alias = get_scm(repo_full_path)[0]
714 714 log.debug('Creating instance of %s repository' % alias)
715 715 backend = get_backend(alias)
716 716 except VCSError:
717 717 log.error(traceback.format_exc())
718 718 log.error('Perhaps this repository is in db and not in '
719 719 'filesystem run rescan repositories with '
720 720 '"destroy old data " option from admin panel')
721 721 return
722 722
723 723 if alias == 'hg':
724 724
725 725 repo = backend(safe_str(repo_full_path), create=False,
726 726 baseui=self._ui)
727 727 # skip hidden web repository
728 728 if repo._get_hidden():
729 729 return
730 730 else:
731 731 repo = backend(repo_full_path, create=False)
732 732
733 733 return repo
734 734
735 735
736 736 class RepoGroup(Base, BaseModel):
737 737 __tablename__ = 'groups'
738 738 __table_args__ = (
739 739 UniqueConstraint('group_name', 'group_parent_id'),
740 740 CheckConstraint('group_id != group_parent_id'),
741 741 {'extend_existing': True, 'mysql_engine':'InnoDB',
742 742 'mysql_charset': 'utf8'},
743 743 )
744 744 __mapper_args__ = {'order_by': 'group_name'}
745 745
746 746 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
747 747 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
748 748 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
749 749 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
750 750
751 751 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
752 752 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
753 753
754 754 parent_group = relationship('RepoGroup', remote_side=group_id)
755 755
756 756 def __init__(self, group_name='', parent_group=None):
757 757 self.group_name = group_name
758 758 self.parent_group = parent_group
759 759
760 760 def __unicode__(self):
761 761 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
762 762 self.group_name)
763 763
764 764 @classmethod
765 765 def groups_choices(cls):
766 766 from webhelpers.html import literal as _literal
767 767 repo_groups = [('', '')]
768 768 sep = ' &raquo; '
769 769 _name = lambda k: _literal(sep.join(k))
770 770
771 771 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
772 772 for x in cls.query().all()])
773 773
774 774 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
775 775 return repo_groups
776 776
777 777 @classmethod
778 778 def url_sep(cls):
779 779 return '/'
780 780
781 781 @classmethod
782 782 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
783 783 if case_insensitive:
784 784 gr = cls.query()\
785 785 .filter(cls.group_name.ilike(group_name))
786 786 else:
787 787 gr = cls.query()\
788 788 .filter(cls.group_name == group_name)
789 789 if cache:
790 790 gr = gr.options(FromCache(
791 791 "sql_cache_short",
792 792 "get_group_%s" % _hash_key(group_name)
793 793 )
794 794 )
795 795 return gr.scalar()
796 796
797 797 @property
798 798 def parents(self):
799 799 parents_recursion_limit = 5
800 800 groups = []
801 801 if self.parent_group is None:
802 802 return groups
803 803 cur_gr = self.parent_group
804 804 groups.insert(0, cur_gr)
805 805 cnt = 0
806 806 while 1:
807 807 cnt += 1
808 808 gr = getattr(cur_gr, 'parent_group', None)
809 809 cur_gr = cur_gr.parent_group
810 810 if gr is None:
811 811 break
812 812 if cnt == parents_recursion_limit:
813 813 # this will prevent accidental infinit loops
814 814 log.error('group nested more than %s' %
815 815 parents_recursion_limit)
816 816 break
817 817
818 818 groups.insert(0, gr)
819 819 return groups
820 820
821 821 @property
822 822 def children(self):
823 823 return RepoGroup.query().filter(RepoGroup.parent_group == self)
824 824
825 825 @property
826 826 def name(self):
827 827 return self.group_name.split(RepoGroup.url_sep())[-1]
828 828
829 829 @property
830 830 def full_path(self):
831 831 return self.group_name
832 832
833 833 @property
834 834 def full_path_splitted(self):
835 835 return self.group_name.split(RepoGroup.url_sep())
836 836
837 837 @property
838 838 def repositories(self):
839 839 return Repository.query()\
840 840 .filter(Repository.group == self)\
841 841 .order_by(Repository.repo_name)
842 842
843 843 @property
844 844 def repositories_recursive_count(self):
845 845 cnt = self.repositories.count()
846 846
847 847 def children_count(group):
848 848 cnt = 0
849 849 for child in group.children:
850 850 cnt += child.repositories.count()
851 851 cnt += children_count(child)
852 852 return cnt
853 853
854 854 return cnt + children_count(self)
855 855
856 856 def get_new_name(self, group_name):
857 857 """
858 858 returns new full group name based on parent and new name
859 859
860 860 :param group_name:
861 861 """
862 862 path_prefix = (self.parent_group.full_path_splitted if
863 863 self.parent_group else [])
864 864 return RepoGroup.url_sep().join(path_prefix + [group_name])
865 865
866 866
867 867 class Permission(Base, BaseModel):
868 868 __tablename__ = 'permissions'
869 869 __table_args__ = (
870 870 {'extend_existing': True, 'mysql_engine':'InnoDB',
871 871 'mysql_charset': 'utf8'},
872 872 )
873 873 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
874 874 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
875 875 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
876 876
877 877 def __unicode__(self):
878 878 return u"<%s('%s:%s')>" % (
879 879 self.__class__.__name__, self.permission_id, self.permission_name
880 880 )
881 881
882 882 @classmethod
883 883 def get_by_key(cls, key):
884 884 return cls.query().filter(cls.permission_name == key).scalar()
885 885
886 886 @classmethod
887 887 def get_default_perms(cls, default_user_id):
888 888 q = Session.query(UserRepoToPerm, Repository, cls)\
889 889 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
890 890 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
891 891 .filter(UserRepoToPerm.user_id == default_user_id)
892 892
893 893 return q.all()
894 894
895 895 @classmethod
896 896 def get_default_group_perms(cls, default_user_id):
897 897 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
898 898 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
899 899 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
900 900 .filter(UserRepoGroupToPerm.user_id == default_user_id)
901 901
902 902 return q.all()
903 903
904 904
905 905 class UserRepoToPerm(Base, BaseModel):
906 906 __tablename__ = 'repo_to_perm'
907 907 __table_args__ = (
908 908 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
909 909 {'extend_existing': True, 'mysql_engine':'InnoDB',
910 910 'mysql_charset': 'utf8'}
911 911 )
912 912 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
913 913 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
914 914 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
915 915 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
916 916
917 917 user = relationship('User')
918 918 repository = relationship('Repository')
919 919 permission = relationship('Permission')
920 920
921 921 @classmethod
922 922 def create(cls, user, repository, permission):
923 923 n = cls()
924 924 n.user = user
925 925 n.repository = repository
926 926 n.permission = permission
927 927 Session.add(n)
928 928 return n
929 929
930 930 def __unicode__(self):
931 931 return u'<user:%s => %s >' % (self.user, self.repository)
932 932
933 933
934 934 class UserToPerm(Base, BaseModel):
935 935 __tablename__ = 'user_to_perm'
936 936 __table_args__ = (
937 937 UniqueConstraint('user_id', 'permission_id'),
938 938 {'extend_existing': True, 'mysql_engine':'InnoDB',
939 939 'mysql_charset': 'utf8'}
940 940 )
941 941 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
942 942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
943 943 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
944 944
945 945 user = relationship('User')
946 946 permission = relationship('Permission', lazy='joined')
947 947
948 948
949 949 class UsersGroupRepoToPerm(Base, BaseModel):
950 950 __tablename__ = 'users_group_repo_to_perm'
951 951 __table_args__ = (
952 952 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
953 953 {'extend_existing': True, 'mysql_engine':'InnoDB',
954 954 'mysql_charset': 'utf8'}
955 955 )
956 956 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
957 957 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
958 958 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
959 959 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
960 960
961 961 users_group = relationship('UsersGroup')
962 962 permission = relationship('Permission')
963 963 repository = relationship('Repository')
964 964
965 965 @classmethod
966 966 def create(cls, users_group, repository, permission):
967 967 n = cls()
968 968 n.users_group = users_group
969 969 n.repository = repository
970 970 n.permission = permission
971 971 Session.add(n)
972 972 return n
973 973
974 974 def __unicode__(self):
975 975 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
976 976
977 977
978 978 class UsersGroupToPerm(Base, BaseModel):
979 979 __tablename__ = 'users_group_to_perm'
980 980 __table_args__ = (
981 981 UniqueConstraint('users_group_id', 'permission_id',),
982 982 {'extend_existing': True, 'mysql_engine':'InnoDB',
983 983 'mysql_charset': 'utf8'}
984 984 )
985 985 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
986 986 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
987 987 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
988 988
989 989 users_group = relationship('UsersGroup')
990 990 permission = relationship('Permission')
991 991
992 992
993 993 class UserRepoGroupToPerm(Base, BaseModel):
994 994 __tablename__ = 'user_repo_group_to_perm'
995 995 __table_args__ = (
996 996 UniqueConstraint('user_id', 'group_id', 'permission_id'),
997 997 {'extend_existing': True, 'mysql_engine':'InnoDB',
998 998 'mysql_charset': 'utf8'}
999 999 )
1000 1000
1001 1001 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1002 1002 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1003 1003 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1004 1004 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1005 1005
1006 1006 user = relationship('User')
1007 1007 group = relationship('RepoGroup')
1008 1008 permission = relationship('Permission')
1009 1009
1010 1010
1011 1011 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1012 1012 __tablename__ = 'users_group_repo_group_to_perm'
1013 1013 __table_args__ = (
1014 1014 UniqueConstraint('users_group_id', 'group_id'),
1015 1015 {'extend_existing': True, 'mysql_engine':'InnoDB',
1016 1016 'mysql_charset': 'utf8'}
1017 1017 )
1018 1018
1019 1019 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)
1020 1020 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1021 1021 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1022 1022 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1023 1023
1024 1024 users_group = relationship('UsersGroup')
1025 1025 permission = relationship('Permission')
1026 1026 group = relationship('RepoGroup')
1027 1027
1028 1028
1029 1029 class Statistics(Base, BaseModel):
1030 1030 __tablename__ = 'statistics'
1031 1031 __table_args__ = (
1032 1032 UniqueConstraint('repository_id'),
1033 1033 {'extend_existing': True, 'mysql_engine':'InnoDB',
1034 1034 'mysql_charset': 'utf8'}
1035 1035 )
1036 1036 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1037 1037 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1038 1038 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1039 1039 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1040 1040 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1041 1041 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1042 1042
1043 1043 repository = relationship('Repository', single_parent=True)
1044 1044
1045 1045
1046 1046 class UserFollowing(Base, BaseModel):
1047 1047 __tablename__ = 'user_followings'
1048 1048 __table_args__ = (
1049 1049 UniqueConstraint('user_id', 'follows_repository_id'),
1050 1050 UniqueConstraint('user_id', 'follows_user_id'),
1051 1051 {'extend_existing': True, 'mysql_engine':'InnoDB',
1052 1052 'mysql_charset': 'utf8'}
1053 1053 )
1054 1054
1055 1055 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1056 1056 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1057 1057 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1058 1058 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1059 1059 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1060 1060
1061 1061 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1062 1062
1063 1063 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1064 1064 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1065 1065
1066 1066 @classmethod
1067 1067 def get_repo_followers(cls, repo_id):
1068 1068 return cls.query().filter(cls.follows_repo_id == repo_id)
1069 1069
1070 1070
1071 1071 class CacheInvalidation(Base, BaseModel):
1072 1072 __tablename__ = 'cache_invalidation'
1073 1073 __table_args__ = (
1074 1074 UniqueConstraint('cache_key'),
1075 1075 {'extend_existing': True, 'mysql_engine':'InnoDB',
1076 1076 'mysql_charset': 'utf8'},
1077 1077 )
1078 1078 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1079 1079 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1080 1080 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1081 1081 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1082 1082
1083 1083 def __init__(self, cache_key, cache_args=''):
1084 1084 self.cache_key = cache_key
1085 1085 self.cache_args = cache_args
1086 1086 self.cache_active = False
1087 1087
1088 1088 def __unicode__(self):
1089 1089 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1090 1090 self.cache_id, self.cache_key)
1091 1091 @classmethod
1092 1092 def clear_cache(cls):
1093 1093 cls.query().delete()
1094 1094
1095 1095 @classmethod
1096 1096 def _get_key(cls, key):
1097 1097 """
1098 1098 Wrapper for generating a key, together with a prefix
1099 1099
1100 1100 :param key:
1101 1101 """
1102 1102 import rhodecode
1103 1103 prefix = ''
1104 1104 iid = rhodecode.CONFIG.get('instance_id')
1105 1105 if iid:
1106 1106 prefix = iid
1107 1107 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1108 1108
1109 1109 @classmethod
1110 1110 def get_by_key(cls, key):
1111 1111 return cls.query().filter(cls.cache_key == key).scalar()
1112 1112
1113 1113 @classmethod
1114 1114 def _get_or_create_key(cls, key, prefix, org_key):
1115 1115 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1116 1116 if not inv_obj:
1117 1117 try:
1118 1118 inv_obj = CacheInvalidation(key, org_key)
1119 1119 Session.add(inv_obj)
1120 1120 Session.commit()
1121 1121 except Exception:
1122 1122 log.error(traceback.format_exc())
1123 1123 Session.rollback()
1124 1124 return inv_obj
1125 1125
1126 1126 @classmethod
1127 1127 def invalidate(cls, key):
1128 1128 """
1129 1129 Returns Invalidation object if this given key should be invalidated
1130 1130 None otherwise. `cache_active = False` means that this cache
1131 1131 state is not valid and needs to be invalidated
1132 1132
1133 1133 :param key:
1134 1134 """
1135 1135
1136 1136 key, _prefix, _org_key = cls._get_key(key)
1137 1137 inv = cls._get_or_create_key(key, _prefix, _org_key)
1138 1138
1139 1139 if inv and inv.cache_active is False:
1140 1140 return inv
1141 1141
1142 1142 @classmethod
1143 1143 def set_invalidate(cls, key):
1144 1144 """
1145 1145 Mark this Cache key for invalidation
1146 1146
1147 1147 :param key:
1148 1148 """
1149 1149
1150 1150 key, _prefix, _org_key = cls._get_key(key)
1151 1151 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1152 1152 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1153 1153 _org_key))
1154 1154 try:
1155 1155 for inv_obj in inv_objs:
1156 1156 if inv_obj:
1157 1157 inv_obj.cache_active = False
1158 1158
1159 1159 Session.add(inv_obj)
1160 1160 Session.commit()
1161 1161 except Exception:
1162 1162 log.error(traceback.format_exc())
1163 1163 Session.rollback()
1164 1164
1165 1165 @classmethod
1166 1166 def set_valid(cls, key):
1167 1167 """
1168 1168 Mark this cache key as active and currently cached
1169 1169
1170 1170 :param key:
1171 1171 """
1172 1172 inv_obj = cls.get_by_key(key)
1173 1173 inv_obj.cache_active = True
1174 1174 Session.add(inv_obj)
1175 1175 Session.commit()
1176 1176
1177 1177
1178 1178 class ChangesetComment(Base, BaseModel):
1179 1179 __tablename__ = 'changeset_comments'
1180 1180 __table_args__ = (
1181 1181 {'extend_existing': True, 'mysql_engine':'InnoDB',
1182 1182 'mysql_charset': 'utf8'},
1183 1183 )
1184 1184 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1185 1185 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1186 1186 revision = Column('revision', String(40), nullable=False)
1187 1187 line_no = Column('line_no', Unicode(10), nullable=True)
1188 1188 f_path = Column('f_path', Unicode(1000), nullable=True)
1189 1189 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1190 1190 text = Column('text', Unicode(25000), nullable=False)
1191 1191 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1192 1192
1193 1193 author = relationship('User', lazy='joined')
1194 1194 repo = relationship('Repository')
1195 1195
1196 1196 @classmethod
1197 1197 def get_users(cls, revision):
1198 1198 """
1199 1199 Returns user associated with this changesetComment. ie those
1200 1200 who actually commented
1201 1201
1202 1202 :param cls:
1203 1203 :param revision:
1204 1204 """
1205 1205 return Session.query(User)\
1206 1206 .filter(cls.revision == revision)\
1207 1207 .join(ChangesetComment.author).all()
1208 1208
1209 1209
1210 1210 class Notification(Base, BaseModel):
1211 1211 __tablename__ = 'notifications'
1212 1212 __table_args__ = (
1213 1213 {'extend_existing': True, 'mysql_engine':'InnoDB',
1214 1214 'mysql_charset': 'utf8'},
1215 1215 )
1216 1216
1217 1217 TYPE_CHANGESET_COMMENT = u'cs_comment'
1218 1218 TYPE_MESSAGE = u'message'
1219 1219 TYPE_MENTION = u'mention'
1220 1220 TYPE_REGISTRATION = u'registration'
1221 1221
1222 1222 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1223 1223 subject = Column('subject', Unicode(512), nullable=True)
1224 1224 body = Column('body', Unicode(50000), nullable=True)
1225 1225 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1226 1226 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1227 1227 type_ = Column('type', Unicode(256))
1228 1228
1229 1229 created_by_user = relationship('User')
1230 1230 notifications_to_users = relationship('UserNotification', lazy='joined',
1231 1231 cascade="all, delete, delete-orphan")
1232 1232
1233 1233 @property
1234 1234 def recipients(self):
1235 1235 return [x.user for x in UserNotification.query()\
1236 .filter(UserNotification.notification == self).all()]
1236 .filter(UserNotification.notification == self)\
1237 .order_by(UserNotification.user).all()]
1237 1238
1238 1239 @classmethod
1239 1240 def create(cls, created_by, subject, body, recipients, type_=None):
1240 1241 if type_ is None:
1241 1242 type_ = Notification.TYPE_MESSAGE
1242 1243
1243 1244 notification = cls()
1244 1245 notification.created_by_user = created_by
1245 1246 notification.subject = subject
1246 1247 notification.body = body
1247 1248 notification.type_ = type_
1248 1249 notification.created_on = datetime.datetime.now()
1249 1250
1250 1251 for u in recipients:
1251 1252 assoc = UserNotification()
1252 1253 assoc.notification = notification
1253 1254 u.notifications.append(assoc)
1254 1255 Session.add(notification)
1255 1256 return notification
1256 1257
1257 1258 @property
1258 1259 def description(self):
1259 1260 from rhodecode.model.notification import NotificationModel
1260 1261 return NotificationModel().make_description(self)
1261 1262
1262 1263
1263 1264 class UserNotification(Base, BaseModel):
1264 1265 __tablename__ = 'user_to_notification'
1265 1266 __table_args__ = (
1266 1267 UniqueConstraint('user_id', 'notification_id'),
1267 1268 {'extend_existing': True, 'mysql_engine':'InnoDB',
1268 1269 'mysql_charset': 'utf8'}
1269 1270 )
1270 1271 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1271 1272 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1272 1273 read = Column('read', Boolean, default=False)
1273 1274 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1274 1275
1275 1276 user = relationship('User', lazy="joined")
1276 1277 notification = relationship('Notification', lazy="joined",
1277 1278 order_by=lambda: Notification.created_on.desc(),)
1278 1279
1279 1280 def mark_as_read(self):
1280 1281 self.read = True
1281 1282 Session.add(self)
1282 1283
1283 1284
1284 1285 class DbMigrateVersion(Base, BaseModel):
1285 1286 __tablename__ = 'db_migrate_version'
1286 1287 __table_args__ = (
1287 1288 {'extend_existing': True, 'mysql_engine':'InnoDB',
1288 1289 'mysql_charset': 'utf8'},
1289 1290 )
1290 1291 repository_id = Column('repository_id', String(250), primary_key=True)
1291 1292 repository_path = Column('repository_path', Text)
1292 1293 version = Column('version', Integer)
@@ -1,592 +1,590 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.user
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 users model for RhodeCode
7 7
8 8 :created_on: Apr 9, 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 logging
27 27 import traceback
28 28
29 29 from pylons import url
30 30 from pylons.i18n.translation import _
31 31
32 32 from rhodecode.lib.utils2 import safe_unicode, generate_api_key
33 33 from rhodecode.lib.caching_query import FromCache
34 34
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
37 37 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
38 38 Notification, RepoGroup, UserRepoGroupToPerm, UsersGroup,\
39 39 UsersGroupRepoGroupToPerm
40 40 from rhodecode.lib.exceptions import DefaultUserException, \
41 41 UserOwnsReposException
42 42
43 43 from sqlalchemy.exc import DatabaseError
44 44
45 45 from sqlalchemy.orm import joinedload
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 PERM_WEIGHTS = {
51 51 'repository.none': 0,
52 52 'repository.read': 1,
53 53 'repository.write': 3,
54 54 'repository.admin': 4,
55 55 'group.none': 0,
56 56 'group.read': 1,
57 57 'group.write': 3,
58 58 'group.admin': 4,
59 59 }
60 60
61 61
62 62 class UserModel(BaseModel):
63 63
64 64 def __get_user(self, user):
65 65 return self._get_instance(User, user, callback=User.get_by_username)
66 66
67 67 def __get_perm(self, permission):
68 68 return self._get_instance(Permission, permission,
69 69 callback=Permission.get_by_key)
70 70
71 71 def get(self, user_id, cache=False):
72 72 user = self.sa.query(User)
73 73 if cache:
74 74 user = user.options(FromCache("sql_cache_short",
75 75 "get_user_%s" % user_id))
76 76 return user.get(user_id)
77 77
78 78 def get_user(self, user):
79 79 return self.__get_user(user)
80 80
81 81 def get_by_username(self, username, cache=False, case_insensitive=False):
82 82
83 83 if case_insensitive:
84 84 user = self.sa.query(User).filter(User.username.ilike(username))
85 85 else:
86 86 user = self.sa.query(User)\
87 87 .filter(User.username == username)
88 88 if cache:
89 89 user = user.options(FromCache("sql_cache_short",
90 90 "get_user_%s" % username))
91 91 return user.scalar()
92 92
93 93 def get_by_api_key(self, api_key, cache=False):
94 94 return User.get_by_api_key(api_key, cache)
95 95
96 96 def create(self, form_data):
97 97 try:
98 98 new_user = User()
99 99 for k, v in form_data.items():
100 100 setattr(new_user, k, v)
101 101
102 102 new_user.api_key = generate_api_key(form_data['username'])
103 103 self.sa.add(new_user)
104 104 return new_user
105 105 except:
106 106 log.error(traceback.format_exc())
107 107 raise
108 108
109 109 def create_or_update(self, username, password, email, name, lastname,
110 110 active=True, admin=False, ldap_dn=None):
111 111 """
112 112 Creates a new instance if not found, or updates current one
113 113
114 114 :param username:
115 115 :param password:
116 116 :param email:
117 117 :param active:
118 118 :param name:
119 119 :param lastname:
120 120 :param active:
121 121 :param admin:
122 122 :param ldap_dn:
123 123 """
124 124
125 125 from rhodecode.lib.auth import get_crypt_password
126 126
127 127 log.debug('Checking for %s account in RhodeCode database' % username)
128 128 user = User.get_by_username(username, case_insensitive=True)
129 129 if user is None:
130 130 log.debug('creating new user %s' % username)
131 131 new_user = User()
132 132 else:
133 133 log.debug('updating user %s' % username)
134 134 new_user = user
135 135
136 136 try:
137 137 new_user.username = username
138 138 new_user.admin = admin
139 139 new_user.password = get_crypt_password(password)
140 140 new_user.api_key = generate_api_key(username)
141 141 new_user.email = email
142 142 new_user.active = active
143 143 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
144 144 new_user.name = name
145 145 new_user.lastname = lastname
146 146 self.sa.add(new_user)
147 147 return new_user
148 148 except (DatabaseError,):
149 149 log.error(traceback.format_exc())
150 150 raise
151 151
152 152 def create_for_container_auth(self, username, attrs):
153 153 """
154 154 Creates the given user if it's not already in the database
155 155
156 156 :param username:
157 157 :param attrs:
158 158 """
159 159 if self.get_by_username(username, case_insensitive=True) is None:
160 160
161 161 # autogenerate email for container account without one
162 162 generate_email = lambda usr: '%s@container_auth.account' % usr
163 163
164 164 try:
165 165 new_user = User()
166 166 new_user.username = username
167 167 new_user.password = None
168 168 new_user.api_key = generate_api_key(username)
169 169 new_user.email = attrs['email']
170 170 new_user.active = attrs.get('active', True)
171 171 new_user.name = attrs['name'] or generate_email(username)
172 172 new_user.lastname = attrs['lastname']
173 173
174 174 self.sa.add(new_user)
175 175 return new_user
176 176 except (DatabaseError,):
177 177 log.error(traceback.format_exc())
178 178 self.sa.rollback()
179 179 raise
180 180 log.debug('User %s already exists. Skipping creation of account'
181 181 ' for container auth.', username)
182 182 return None
183 183
184 184 def create_ldap(self, username, password, user_dn, attrs):
185 185 """
186 186 Checks if user is in database, if not creates this user marked
187 187 as ldap user
188 188
189 189 :param username:
190 190 :param password:
191 191 :param user_dn:
192 192 :param attrs:
193 193 """
194 194 from rhodecode.lib.auth import get_crypt_password
195 195 log.debug('Checking for such ldap account in RhodeCode database')
196 196 if self.get_by_username(username, case_insensitive=True) is None:
197 197
198 198 # autogenerate email for ldap account without one
199 199 generate_email = lambda usr: '%s@ldap.account' % usr
200 200
201 201 try:
202 202 new_user = User()
203 203 username = username.lower()
204 204 # add ldap account always lowercase
205 205 new_user.username = username
206 206 new_user.password = get_crypt_password(password)
207 207 new_user.api_key = generate_api_key(username)
208 208 new_user.email = attrs['email'] or generate_email(username)
209 209 new_user.active = attrs.get('active', True)
210 210 new_user.ldap_dn = safe_unicode(user_dn)
211 211 new_user.name = attrs['name']
212 212 new_user.lastname = attrs['lastname']
213 213
214 214 self.sa.add(new_user)
215 215 return new_user
216 216 except (DatabaseError,):
217 217 log.error(traceback.format_exc())
218 218 self.sa.rollback()
219 219 raise
220 220 log.debug('this %s user exists skipping creation of ldap account',
221 221 username)
222 222 return None
223 223
224 224 def create_registration(self, form_data):
225 225 from rhodecode.model.notification import NotificationModel
226 226
227 227 try:
228 new_user = User()
229 for k, v in form_data.items():
230 if k != 'admin':
231 setattr(new_user, k, v)
228 form_data['admin'] = False
229 new_user = self.create(form_data)
232 230
233 231 self.sa.add(new_user)
234 232 self.sa.flush()
235 233
236 234 # notification to admins
237 235 subject = _('new user registration')
238 236 body = ('New user registration\n'
239 237 '---------------------\n'
240 238 '- Username: %s\n'
241 239 '- Full Name: %s\n'
242 240 '- Email: %s\n')
243 241 body = body % (new_user.username, new_user.full_name,
244 242 new_user.email)
245 243 edit_url = url('edit_user', id=new_user.user_id, qualified=True)
246 244 kw = {'registered_user_url': edit_url}
247 245 NotificationModel().create(created_by=new_user, subject=subject,
248 246 body=body, recipients=None,
249 247 type_=Notification.TYPE_REGISTRATION,
250 248 email_kwargs=kw)
251 249
252 250 except:
253 251 log.error(traceback.format_exc())
254 252 raise
255 253
256 254 def update(self, user_id, form_data):
257 255 try:
258 256 user = self.get(user_id, cache=False)
259 257 if user.username == 'default':
260 258 raise DefaultUserException(
261 259 _("You can't Edit this user since it's"
262 260 " crucial for entire application"))
263 261
264 262 for k, v in form_data.items():
265 263 if k == 'new_password' and v != '':
266 264 user.password = v
267 265 user.api_key = generate_api_key(user.username)
268 266 else:
269 267 setattr(user, k, v)
270 268
271 269 self.sa.add(user)
272 270 except:
273 271 log.error(traceback.format_exc())
274 272 raise
275 273
276 274 def update_my_account(self, user_id, form_data):
277 275 try:
278 276 user = self.get(user_id, cache=False)
279 277 if user.username == 'default':
280 278 raise DefaultUserException(
281 279 _("You can't Edit this user since it's"
282 280 " crucial for entire application"))
283 281 for k, v in form_data.items():
284 282 if k == 'new_password' and v != '':
285 283 user.password = v
286 284 user.api_key = generate_api_key(user.username)
287 285 else:
288 286 if k not in ['admin', 'active']:
289 287 setattr(user, k, v)
290 288
291 289 self.sa.add(user)
292 290 except:
293 291 log.error(traceback.format_exc())
294 292 raise
295 293
296 294 def delete(self, user):
297 295 user = self.__get_user(user)
298 296
299 297 try:
300 298 if user.username == 'default':
301 299 raise DefaultUserException(
302 300 _(u"You can't remove this user since it's"
303 301 " crucial for entire application")
304 302 )
305 303 if user.repositories:
306 304 repos = [x.repo_name for x in user.repositories]
307 305 raise UserOwnsReposException(
308 306 _(u'user "%s" still owns %s repositories and cannot be '
309 307 'removed. Switch owners or remove those repositories. %s')
310 308 % (user.username, len(repos), ', '.join(repos))
311 309 )
312 310 self.sa.delete(user)
313 311 except:
314 312 log.error(traceback.format_exc())
315 313 raise
316 314
317 315 def reset_password_link(self, data):
318 316 from rhodecode.lib.celerylib import tasks, run_task
319 317 run_task(tasks.send_password_link, data['email'])
320 318
321 319 def reset_password(self, data):
322 320 from rhodecode.lib.celerylib import tasks, run_task
323 321 run_task(tasks.reset_user_password, data['email'])
324 322
325 323 def fill_data(self, auth_user, user_id=None, api_key=None):
326 324 """
327 325 Fetches auth_user by user_id,or api_key if present.
328 326 Fills auth_user attributes with those taken from database.
329 327 Additionally set's is_authenitated if lookup fails
330 328 present in database
331 329
332 330 :param auth_user: instance of user to set attributes
333 331 :param user_id: user id to fetch by
334 332 :param api_key: api key to fetch by
335 333 """
336 334 if user_id is None and api_key is None:
337 335 raise Exception('You need to pass user_id or api_key')
338 336
339 337 try:
340 338 if api_key:
341 339 dbuser = self.get_by_api_key(api_key)
342 340 else:
343 341 dbuser = self.get(user_id)
344 342
345 343 if dbuser is not None and dbuser.active:
346 344 log.debug('filling %s data' % dbuser)
347 345 for k, v in dbuser.get_dict().items():
348 346 setattr(auth_user, k, v)
349 347 else:
350 348 return False
351 349
352 350 except:
353 351 log.error(traceback.format_exc())
354 352 auth_user.is_authenticated = False
355 353 return False
356 354
357 355 return True
358 356
359 357 def fill_perms(self, user):
360 358 """
361 359 Fills user permission attribute with permissions taken from database
362 360 works for permissions given for repositories, and for permissions that
363 361 are granted to groups
364 362
365 363 :param user: user instance to fill his perms
366 364 """
367 365 RK = 'repositories'
368 366 GK = 'repositories_groups'
369 367 GLOBAL = 'global'
370 368 user.permissions[RK] = {}
371 369 user.permissions[GK] = {}
372 370 user.permissions[GLOBAL] = set()
373 371
374 372 #======================================================================
375 373 # fetch default permissions
376 374 #======================================================================
377 375 default_user = User.get_by_username('default', cache=True)
378 376 default_user_id = default_user.user_id
379 377
380 378 default_repo_perms = Permission.get_default_perms(default_user_id)
381 379 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
382 380
383 381 if user.is_admin:
384 382 #==================================================================
385 383 # admin user have all default rights for repositories
386 384 # and groups set to admin
387 385 #==================================================================
388 386 user.permissions[GLOBAL].add('hg.admin')
389 387
390 388 # repositories
391 389 for perm in default_repo_perms:
392 390 r_k = perm.UserRepoToPerm.repository.repo_name
393 391 p = 'repository.admin'
394 392 user.permissions[RK][r_k] = p
395 393
396 394 # repositories groups
397 395 for perm in default_repo_groups_perms:
398 396 rg_k = perm.UserRepoGroupToPerm.group.group_name
399 397 p = 'group.admin'
400 398 user.permissions[GK][rg_k] = p
401 399 return user
402 400
403 401 #==================================================================
404 402 # set default permissions first for repositories and groups
405 403 #==================================================================
406 404 uid = user.user_id
407 405
408 406 # default global permissions
409 407 default_global_perms = self.sa.query(UserToPerm)\
410 408 .filter(UserToPerm.user_id == default_user_id)
411 409
412 410 for perm in default_global_perms:
413 411 user.permissions[GLOBAL].add(perm.permission.permission_name)
414 412
415 413 # defaults for repositories, taken from default user
416 414 for perm in default_repo_perms:
417 415 r_k = perm.UserRepoToPerm.repository.repo_name
418 416 if perm.Repository.private and not (perm.Repository.user_id == uid):
419 417 # disable defaults for private repos,
420 418 p = 'repository.none'
421 419 elif perm.Repository.user_id == uid:
422 420 # set admin if owner
423 421 p = 'repository.admin'
424 422 else:
425 423 p = perm.Permission.permission_name
426 424
427 425 user.permissions[RK][r_k] = p
428 426
429 427 # defaults for repositories groups taken from default user permission
430 428 # on given group
431 429 for perm in default_repo_groups_perms:
432 430 rg_k = perm.UserRepoGroupToPerm.group.group_name
433 431 p = perm.Permission.permission_name
434 432 user.permissions[GK][rg_k] = p
435 433
436 434 #==================================================================
437 435 # overwrite defaults with user permissions if any found
438 436 #==================================================================
439 437
440 438 # user global permissions
441 439 user_perms = self.sa.query(UserToPerm)\
442 440 .options(joinedload(UserToPerm.permission))\
443 441 .filter(UserToPerm.user_id == uid).all()
444 442
445 443 for perm in user_perms:
446 444 user.permissions[GLOBAL].add(perm.permission.permission_name)
447 445
448 446 # user explicit permissions for repositories
449 447 user_repo_perms = \
450 448 self.sa.query(UserRepoToPerm, Permission, Repository)\
451 449 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
452 450 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
453 451 .filter(UserRepoToPerm.user_id == uid)\
454 452 .all()
455 453
456 454 for perm in user_repo_perms:
457 455 # set admin if owner
458 456 r_k = perm.UserRepoToPerm.repository.repo_name
459 457 if perm.Repository.user_id == uid:
460 458 p = 'repository.admin'
461 459 else:
462 460 p = perm.Permission.permission_name
463 461 user.permissions[RK][r_k] = p
464 462
465 463 # USER GROUP
466 464 #==================================================================
467 465 # check if user is part of user groups for this repository and
468 466 # fill in (or replace with higher) permissions
469 467 #==================================================================
470 468
471 469 # users group global
472 470 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
473 471 .options(joinedload(UsersGroupToPerm.permission))\
474 472 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
475 473 UsersGroupMember.users_group_id))\
476 474 .filter(UsersGroupMember.user_id == uid).all()
477 475
478 476 for perm in user_perms_from_users_groups:
479 477 user.permissions[GLOBAL].add(perm.permission.permission_name)
480 478
481 479 # users group for repositories permissions
482 480 user_repo_perms_from_users_groups = \
483 481 self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
484 482 .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\
485 483 .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\
486 484 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\
487 485 .filter(UsersGroupMember.user_id == uid)\
488 486 .all()
489 487
490 488 for perm in user_repo_perms_from_users_groups:
491 489 r_k = perm.UsersGroupRepoToPerm.repository.repo_name
492 490 p = perm.Permission.permission_name
493 491 cur_perm = user.permissions[RK][r_k]
494 492 # overwrite permission only if it's greater than permission
495 493 # given from other sources
496 494 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
497 495 user.permissions[RK][r_k] = p
498 496
499 497 # REPO GROUP
500 498 #==================================================================
501 499 # get access for this user for repos group and override defaults
502 500 #==================================================================
503 501
504 502 # user explicit permissions for repository
505 503 user_repo_groups_perms = \
506 504 self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
507 505 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
508 506 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
509 507 .filter(UserRepoGroupToPerm.user_id == uid)\
510 508 .all()
511 509
512 510 for perm in user_repo_groups_perms:
513 511 rg_k = perm.UserRepoGroupToPerm.group.group_name
514 512 p = perm.Permission.permission_name
515 513 cur_perm = user.permissions[GK][rg_k]
516 514 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
517 515 user.permissions[GK][rg_k] = p
518 516
519 517 # REPO GROUP + USER GROUP
520 518 #==================================================================
521 519 # check if user is part of user groups for this repo group and
522 520 # fill in (or replace with higher) permissions
523 521 #==================================================================
524 522
525 523 # users group for repositories permissions
526 524 user_repo_group_perms_from_users_groups = \
527 525 self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\
528 526 .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
529 527 .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\
530 528 .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\
531 529 .filter(UsersGroupMember.user_id == uid)\
532 530 .all()
533 531
534 532 for perm in user_repo_group_perms_from_users_groups:
535 533 g_k = perm.UsersGroupRepoGroupToPerm.group.group_name
536 534 print perm, g_k
537 535 p = perm.Permission.permission_name
538 536 cur_perm = user.permissions[GK][g_k]
539 537 # overwrite permission only if it's greater than permission
540 538 # given from other sources
541 539 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
542 540 user.permissions[GK][g_k] = p
543 541
544 542 return user
545 543
546 544 def has_perm(self, user, perm):
547 545 if not isinstance(perm, Permission):
548 546 raise Exception('perm needs to be an instance of Permission class '
549 547 'got %s instead' % type(perm))
550 548
551 549 user = self.__get_user(user)
552 550
553 551 return UserToPerm.query().filter(UserToPerm.user == user)\
554 552 .filter(UserToPerm.permission == perm).scalar() is not None
555 553
556 554 def grant_perm(self, user, perm):
557 555 """
558 556 Grant user global permissions
559 557
560 558 :param user:
561 559 :param perm:
562 560 """
563 561 user = self.__get_user(user)
564 562 perm = self.__get_perm(perm)
565 563 # if this permission is already granted skip it
566 564 _perm = UserToPerm.query()\
567 565 .filter(UserToPerm.user == user)\
568 566 .filter(UserToPerm.permission == perm)\
569 567 .scalar()
570 568 if _perm:
571 569 return
572 570 new = UserToPerm()
573 571 new.user = user
574 572 new.permission = perm
575 573 self.sa.add(new)
576 574
577 575 def revoke_perm(self, user, perm):
578 576 """
579 577 Revoke users global permissions
580 578
581 579 :param user:
582 580 :param perm:
583 581 """
584 582 user = self.__get_user(user)
585 583 perm = self.__get_perm(perm)
586 584
587 585 obj = UserToPerm.query()\
588 586 .filter(UserToPerm.user == user)\
589 587 .filter(UserToPerm.permission == perm)\
590 588 .scalar()
591 589 if obj:
592 590 self.sa.delete(obj)
@@ -1,268 +1,267 b''
1 1 # -*- coding: utf-8 -*-
2 2 from rhodecode.tests import *
3 3 from rhodecode.model.db import User, Notification
4 4 from rhodecode.lib.utils2 import generate_api_key
5 5 from rhodecode.lib.auth import check_password
6 6 from rhodecode.model.meta import Session
7 7
8 8
9 9 class TestLoginController(TestController):
10 10
11 11 def tearDown(self):
12 12 for n in Notification.query().all():
13 13 Session.delete(n)
14 14
15 15 Session.commit()
16 16 self.assertEqual(Notification.query().all(), [])
17 17
18 18 def test_index(self):
19 19 response = self.app.get(url(controller='login', action='index'))
20 20 self.assertEqual(response.status, '200 OK')
21 21 # Test response...
22 22
23 23 def test_login_admin_ok(self):
24 24 response = self.app.post(url(controller='login', action='index'),
25 25 {'username':'test_admin',
26 26 'password':'test12'})
27 27 self.assertEqual(response.status, '302 Found')
28 28 self.assertEqual(response.session['rhodecode_user'].get('username') ,
29 29 'test_admin')
30 30 response = response.follow()
31 31 self.assertTrue('%s repository' % HG_REPO in response.body)
32 32
33 33 def test_login_regular_ok(self):
34 34 response = self.app.post(url(controller='login', action='index'),
35 35 {'username':'test_regular',
36 36 'password':'test12'})
37 37
38 38 self.assertEqual(response.status, '302 Found')
39 39 self.assertEqual(response.session['rhodecode_user'].get('username') ,
40 40 'test_regular')
41 41 response = response.follow()
42 42 self.assertTrue('%s repository' % HG_REPO in response.body)
43 43 self.assertTrue('<a title="Admin" href="/_admin">' not in response.body)
44 44
45 45 def test_login_ok_came_from(self):
46 46 test_came_from = '/_admin/users'
47 47 response = self.app.post(url(controller='login', action='index',
48 48 came_from=test_came_from),
49 49 {'username':'test_admin',
50 50 'password':'test12'})
51 51 self.assertEqual(response.status, '302 Found')
52 52 response = response.follow()
53 53
54 54 self.assertEqual(response.status, '200 OK')
55 55 self.assertTrue('Users administration' in response.body)
56 56
57
58 57 def test_login_short_password(self):
59 58 response = self.app.post(url(controller='login', action='index'),
60 59 {'username':'test_admin',
61 60 'password':'as'})
62 61 self.assertEqual(response.status, '200 OK')
63 62
64 63 self.assertTrue('Enter 3 characters or more' in response.body)
65 64
66 65 def test_login_wrong_username_password(self):
67 66 response = self.app.post(url(controller='login', action='index'),
68 67 {'username':'error',
69 68 'password':'test12'})
70 69 self.assertEqual(response.status , '200 OK')
71 70
72 71 self.assertTrue('invalid user name' in response.body)
73 72 self.assertTrue('invalid password' in response.body)
74 73
75 74 #==========================================================================
76 75 # REGISTRATIONS
77 76 #==========================================================================
78 77 def test_register(self):
79 78 response = self.app.get(url(controller='login', action='register'))
80 79 self.assertTrue('Sign Up to RhodeCode' in response.body)
81 80
82 81 def test_register_err_same_username(self):
83 82 response = self.app.post(url(controller='login', action='register'),
84 83 {'username':'test_admin',
85 84 'password':'test12',
86 85 'password_confirmation':'test12',
87 86 'email':'goodmail@domain.com',
88 87 'name':'test',
89 88 'lastname':'test'})
90 89
91 90 self.assertEqual(response.status , '200 OK')
92 91 self.assertTrue('This username already exists' in response.body)
93 92
94 93 def test_register_err_same_email(self):
95 94 response = self.app.post(url(controller='login', action='register'),
96 95 {'username':'test_admin_0',
97 96 'password':'test12',
98 97 'password_confirmation':'test12',
99 98 'email':'test_admin@mail.com',
100 99 'name':'test',
101 100 'lastname':'test'})
102 101
103 102 self.assertEqual(response.status , '200 OK')
104 assert 'This e-mail address is already taken' in response.body
103 response.mustcontain('This e-mail address is already taken')
105 104
106 105 def test_register_err_same_email_case_sensitive(self):
107 106 response = self.app.post(url(controller='login', action='register'),
108 107 {'username':'test_admin_1',
109 108 'password':'test12',
110 109 'password_confirmation':'test12',
111 110 'email':'TesT_Admin@mail.COM',
112 111 'name':'test',
113 112 'lastname':'test'})
114 113 self.assertEqual(response.status , '200 OK')
115 assert 'This e-mail address is already taken' in response.body
114 response.mustcontain('This e-mail address is already taken')
116 115
117 116 def test_register_err_wrong_data(self):
118 117 response = self.app.post(url(controller='login', action='register'),
119 118 {'username':'xs',
120 119 'password':'test',
121 120 'password_confirmation':'test',
122 121 'email':'goodmailm',
123 122 'name':'test',
124 123 'lastname':'test'})
125 124 self.assertEqual(response.status , '200 OK')
126 assert 'An email address must contain a single @' in response.body
127 assert 'Enter a value 6 characters long or more' in response.body
128
125 response.mustcontain('An email address must contain a single @')
126 response.mustcontain('Enter a value 6 characters long or more')
129 127
130 128 def test_register_err_username(self):
131 129 response = self.app.post(url(controller='login', action='register'),
132 130 {'username':'error user',
133 131 'password':'test12',
134 132 'password_confirmation':'test12',
135 133 'email':'goodmailm',
136 134 'name':'test',
137 135 'lastname':'test'})
138 136
139 137 self.assertEqual(response.status , '200 OK')
140 assert 'An email address must contain a single @' in response.body
141 assert ('Username may only contain '
138 response.mustcontain('An email address must contain a single @')
139 response.mustcontain('Username may only contain '
142 140 'alphanumeric characters underscores, '
143 141 'periods or dashes and must begin with '
144 'alphanumeric character') in response.body
142 'alphanumeric character')
145 143
146 144 def test_register_err_case_sensitive(self):
147 145 response = self.app.post(url(controller='login', action='register'),
148 146 {'username':'Test_Admin',
149 147 'password':'test12',
150 148 'password_confirmation':'test12',
151 149 'email':'goodmailm',
152 150 'name':'test',
153 151 'lastname':'test'})
154 152
155 153 self.assertEqual(response.status , '200 OK')
156 154 self.assertTrue('An email address must contain a single @' in response.body)
157 155 self.assertTrue('This username already exists' in response.body)
158 156
159
160
161 157 def test_register_special_chars(self):
162 158 response = self.app.post(url(controller='login', action='register'),
163 159 {'username':'xxxaxn',
164 160 'password':'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
165 161 'password_confirmation':'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
166 162 'email':'goodmailm@test.plx',
167 163 'name':'test',
168 164 'lastname':'test'})
169 165
170 166 self.assertEqual(response.status , '200 OK')
171 167 self.assertTrue('Invalid characters in password' in response.body)
172 168
173
174 169 def test_register_password_mismatch(self):
175 170 response = self.app.post(url(controller='login', action='register'),
176 171 {'username':'xs',
177 172 'password':'123qwe',
178 173 'password_confirmation':'qwe123',
179 174 'email':'goodmailm@test.plxa',
180 175 'name':'test',
181 176 'lastname':'test'})
182 177
183 178 self.assertEqual(response.status , '200 OK')
184 assert 'Passwords do not match' in response.body
179 response.mustcontain('Passwords do not match')
185 180
186 181 def test_register_ok(self):
187 182 username = 'test_regular4'
188 183 password = 'qweqwe'
189 184 email = 'marcin@test.com'
190 185 name = 'testname'
191 186 lastname = 'testlastname'
192 187
193 188 response = self.app.post(url(controller='login', action='register'),
194 189 {'username':username,
195 190 'password':password,
196 191 'password_confirmation':password,
197 192 'email':email,
198 193 'name':name,
199 'lastname':lastname})
194 'lastname':lastname,
195 'admin':True}) # This should be overriden
200 196 self.assertEqual(response.status , '302 Found')
201 assert 'You have successfully registered into rhodecode' in response.session['flash'][0], 'No flash message about user registration'
197 self.checkSessionFlash(response, 'You have successfully registered into rhodecode')
202 198
203 199 ret = self.Session.query(User).filter(User.username == 'test_regular4').one()
204 assert ret.username == username , 'field mismatch %s %s' % (ret.username, username)
205 assert check_password(password, ret.password) == True , 'password mismatch'
206 assert ret.email == email , 'field mismatch %s %s' % (ret.email, email)
207 assert ret.name == name , 'field mismatch %s %s' % (ret.name, name)
208 assert ret.lastname == lastname , 'field mismatch %s %s' % (ret.lastname, lastname)
209
200 self.assertEqual(ret.username, username)
201 self.assertEqual(check_password(password, ret.password), True)
202 self.assertEqual(ret.email, email)
203 self.assertEqual(ret.name, name)
204 self.assertEqual(ret.lastname, lastname)
205 self.assertNotEqual(ret.api_key, None)
206 self.assertEqual(ret.admin, False)
210 207
211 208 def test_forgot_password_wrong_mail(self):
212 response = self.app.post(url(controller='login', action='password_reset'),
213 {'email':'marcin@wrongmail.org', })
209 response = self.app.post(
210 url(controller='login', action='password_reset'),
211 {'email': 'marcin@wrongmail.org',}
212 )
214 213
215 assert "This e-mail address doesn't exist" in response.body, 'Missing error message about wrong email'
214 response.mustcontain("This e-mail address doesn't exist")
216 215
217 216 def test_forgot_password(self):
218 217 response = self.app.get(url(controller='login',
219 218 action='password_reset'))
220 219 self.assertEqual(response.status , '200 OK')
221 220
222 221 username = 'test_password_reset_1'
223 222 password = 'qweqwe'
224 223 email = 'marcin@python-works.com'
225 224 name = 'passwd'
226 225 lastname = 'reset'
227 226
228 227 new = User()
229 228 new.username = username
230 229 new.password = password
231 230 new.email = email
232 231 new.name = name
233 232 new.lastname = lastname
234 233 new.api_key = generate_api_key(username)
235 234 self.Session.add(new)
236 235 self.Session.commit()
237 236
238 237 response = self.app.post(url(controller='login',
239 238 action='password_reset'),
240 239 {'email':email, })
241 240
242 241 self.checkSessionFlash(response, 'Your password reset link was sent')
243 242
244 243 response = response.follow()
245 244
246 245 # BAD KEY
247 246
248 247 key = "bad"
249 248 response = self.app.get(url(controller='login',
250 249 action='password_reset_confirmation',
251 250 key=key))
252 251 self.assertEqual(response.status, '302 Found')
253 252 self.assertTrue(response.location.endswith(url('reset_password')))
254 253
255 254 # GOOD KEY
256 255
257 256 key = User.get_by_username(username).api_key
258 257 response = self.app.get(url(controller='login',
259 258 action='password_reset_confirmation',
260 259 key=key))
261 260 self.assertEqual(response.status, '302 Found')
262 261 self.assertTrue(response.location.endswith(url('login_home')))
263 262
264 263 self.checkSessionFlash(response,
265 264 ('Your password reset was successful, '
266 265 'new password has been sent to your email'))
267 266
268 267 response = response.follow()
General Comments 0
You need to be logged in to leave comments. Login now