##// END OF EJS Templates
merge beta into codereview
marcink -
r2280:c9e3ea5b merge codereview
parent child Browse files
Show More
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
This diff has been collapsed as it changes many lines, (2532 lines changed) Show them Hide them
@@ -0,0 +1,2532 b''
1 # Translations template for RhodeCode.
2 # Copyright (C) 2011 ORGANIZATION
3 # This file is distributed under the same license as the RhodeCode project.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
5 #
6 msgid ""
7 msgstr ""
8 "Project-Id-Version: RhodeCode 1.2.0\n"
9 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10 "POT-Creation-Date: 2011-09-14 15:50-0300\n"
11 "PO-Revision-Date: 2012-05-09 22:23+0800\n"
12 "Last-Translator: Nansen <nansenat16@gmail.com>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "MIME-Version: 1.0\n"
15 "Content-Type: text/plain; charset=utf-8\n"
16 "Content-Transfer-Encoding: 8bit\n"
17 "Generated-By: Babel 0.9.6\n"
18 "X-Poedit-Language: Chinese\n"
19 "X-Poedit-Country: TAIWAN\n"
20 "X-Poedit-SourceCharset: utf-8\n"
21
22 #: rhodecode/controllers/changeset.py:108
23 #: rhodecode/controllers/changeset.py:149
24 #: rhodecode/controllers/changeset.py:216
25 #: rhodecode/controllers/changeset.py:229
26 msgid "binary file"
27 msgstr "二進位檔"
28
29 #: rhodecode/controllers/changeset.py:123
30 #: rhodecode/controllers/changeset.py:168
31 msgid "Changeset is to big and was cut off, see raw changeset instead"
32 msgstr ""
33
34 #: rhodecode/controllers/changeset.py:159
35 msgid "Diff is to big and was cut off, see raw diff instead"
36 msgstr ""
37
38 #: rhodecode/controllers/error.py:69
39 msgid "Home page"
40 msgstr "首頁"
41
42 #: rhodecode/controllers/error.py:98
43 msgid "The request could not be understood by the server due to malformed syntax."
44 msgstr ""
45
46 #: rhodecode/controllers/error.py:101
47 msgid "Unauthorized access to resource"
48 msgstr ""
49
50 #: rhodecode/controllers/error.py:103
51 msgid "You don't have permission to view this page"
52 msgstr "您沒有權限瀏覽這個頁面"
53
54 #: rhodecode/controllers/error.py:105
55 msgid "The resource could not be found"
56 msgstr "找不到這個資源"
57
58 #: rhodecode/controllers/error.py:107
59 msgid "The server encountered an unexpected condition which prevented it from fulfilling the request."
60 msgstr ""
61
62 #: rhodecode/controllers/feed.py:48
63 #, python-format
64 msgid "Changes on %s repository"
65 msgstr "修改於版本庫 %s"
66
67 #: rhodecode/controllers/feed.py:49
68 #, python-format
69 msgid "%s %s feed"
70 msgstr ""
71
72 #: rhodecode/controllers/files.py:72
73 msgid "There are no files yet"
74 msgstr "尚未有任何檔案"
75
76 #: rhodecode/controllers/files.py:262
77 #, python-format
78 msgid "Edited %s via RhodeCode"
79 msgstr "使用 RhodeCode 編輯 %s"
80
81 #: rhodecode/controllers/files.py:267
82 #: rhodecode/templates/files/file_diff.html:40
83 msgid "No changes"
84 msgstr "沒有修改"
85
86 #: rhodecode/controllers/files.py:278
87 #, python-format
88 msgid "Successfully committed to %s"
89 msgstr "成功遞交至 %s"
90
91 #: rhodecode/controllers/files.py:283
92 msgid "Error occurred during commit"
93 msgstr ""
94
95 #: rhodecode/controllers/files.py:308
96 msgid "downloads disabled"
97 msgstr "下載已關閉"
98
99 #: rhodecode/controllers/files.py:313
100 #, python-format
101 msgid "Unknown revision %s"
102 msgstr "未知修訂 %s"
103
104 #: rhodecode/controllers/files.py:315
105 msgid "Empty repository"
106 msgstr "空的版本庫"
107
108 #: rhodecode/controllers/files.py:317
109 msgid "Unknown archive type"
110 msgstr "未知的存檔類型"
111
112 #: rhodecode/controllers/files.py:385
113 #: rhodecode/controllers/files.py:398
114 msgid "Binary file"
115 msgstr "二進位檔"
116
117 #: rhodecode/controllers/files.py:417
118 #: rhodecode/templates/changeset/changeset_range.html:4
119 #: rhodecode/templates/changeset/changeset_range.html:12
120 #: rhodecode/templates/changeset/changeset_range.html:29
121 msgid "Changesets"
122 msgstr "變更"
123
124 #: rhodecode/controllers/files.py:418
125 #: rhodecode/controllers/summary.py:175
126 #: rhodecode/templates/branches/branches.html:5
127 #: rhodecode/templates/summary/summary.html:690
128 msgid "Branches"
129 msgstr "分支"
130
131 #: rhodecode/controllers/files.py:419
132 #: rhodecode/controllers/summary.py:176
133 #: rhodecode/templates/summary/summary.html:679
134 #: rhodecode/templates/tags/tags.html:5
135 msgid "Tags"
136 msgstr "標籤"
137
138 #: rhodecode/controllers/journal.py:50
139 #, python-format
140 msgid "%s public journal %s feed"
141 msgstr "%s 公開日誌 %s feed"
142
143 #: rhodecode/controllers/journal.py:178
144 #: rhodecode/controllers/journal.py:212
145 #: rhodecode/templates/admin/repos/repo_edit.html:171
146 #: rhodecode/templates/base/base.html:50
147 msgid "Public journal"
148 msgstr "公開日誌"
149
150 #: rhodecode/controllers/login.py:111
151 msgid "You have successfully registered into rhodecode"
152 msgstr "您已經成功註冊rhodecode"
153
154 #: rhodecode/controllers/login.py:133
155 msgid "Your password reset link was sent"
156 msgstr "您的密碼重設連結已寄出"
157
158 #: rhodecode/controllers/login.py:155
159 msgid "Your password reset was successful, new password has been sent to your email"
160 msgstr "您的密碼重設動作已完成,新的密碼已寄至您的信箱"
161
162 #: rhodecode/controllers/search.py:109
163 msgid "Invalid search query. Try quoting it."
164 msgstr "無效的查詢。請使用跳脫字元"
165
166 #: rhodecode/controllers/search.py:114
167 msgid "There is no index to search in. Please run whoosh indexer"
168 msgstr "沒有任何索引可以搜尋。請執行 whoosh 建立索引"
169
170 #: rhodecode/controllers/search.py:118
171 msgid "An error occurred during this search operation"
172 msgstr ""
173
174 #: rhodecode/controllers/settings.py:61
175 #: rhodecode/controllers/settings.py:171
176 #, python-format
177 msgid "%s repository is not mapped to db perhaps it was created or renamed from the file system please run the application again in order to rescan repositories"
178 msgstr ""
179
180 #: rhodecode/controllers/settings.py:109
181 #: rhodecode/controllers/admin/repos.py:239
182 #, python-format
183 msgid "Repository %s updated successfully"
184 msgstr "版本庫 %s 更新完成"
185
186 #: rhodecode/controllers/settings.py:126
187 #: rhodecode/controllers/admin/repos.py:257
188 #, python-format
189 msgid "error occurred during update of repository %s"
190 msgstr ""
191
192 #: rhodecode/controllers/settings.py:144
193 #: rhodecode/controllers/admin/repos.py:275
194 #, python-format
195 msgid "%s repository is not mapped to db perhaps it was moved or renamed from the filesystem please run the application again in order to rescan repositories"
196 msgstr ""
197
198 #: rhodecode/controllers/settings.py:156
199 #: rhodecode/controllers/admin/repos.py:287
200 #, python-format
201 msgid "deleted repository %s"
202 msgstr "刪除版本庫 %s"
203
204 #: rhodecode/controllers/settings.py:159
205 #: rhodecode/controllers/admin/repos.py:297
206 #: rhodecode/controllers/admin/repos.py:303
207 #, python-format
208 msgid "An error occurred during deletion of %s"
209 msgstr ""
210
211 #: rhodecode/controllers/settings.py:193
212 #, python-format
213 msgid "forked %s repository as %s"
214 msgstr "forked %s 版本庫為 %s"
215
216 #: rhodecode/controllers/settings.py:211
217 #, python-format
218 msgid "An error occurred during repository forking %s"
219 msgstr ""
220
221 #: rhodecode/controllers/summary.py:123
222 msgid "No data loaded yet"
223 msgstr ""
224
225 #: rhodecode/controllers/summary.py:126
226 msgid "Statistics are disabled for this repository"
227 msgstr "這個版本庫的統計功能已停用"
228
229 #: rhodecode/controllers/admin/ldap_settings.py:49
230 msgid "BASE"
231 msgstr ""
232
233 #: rhodecode/controllers/admin/ldap_settings.py:50
234 msgid "ONELEVEL"
235 msgstr ""
236
237 #: rhodecode/controllers/admin/ldap_settings.py:51
238 msgid "SUBTREE"
239 msgstr ""
240
241 #: rhodecode/controllers/admin/ldap_settings.py:55
242 msgid "NEVER"
243 msgstr ""
244
245 #: rhodecode/controllers/admin/ldap_settings.py:56
246 msgid "ALLOW"
247 msgstr ""
248
249 #: rhodecode/controllers/admin/ldap_settings.py:57
250 msgid "TRY"
251 msgstr ""
252
253 #: rhodecode/controllers/admin/ldap_settings.py:58
254 msgid "DEMAND"
255 msgstr ""
256
257 #: rhodecode/controllers/admin/ldap_settings.py:59
258 msgid "HARD"
259 msgstr ""
260
261 #: rhodecode/controllers/admin/ldap_settings.py:63
262 msgid "No encryption"
263 msgstr "無加密"
264
265 #: rhodecode/controllers/admin/ldap_settings.py:64
266 msgid "LDAPS connection"
267 msgstr ""
268
269 #: rhodecode/controllers/admin/ldap_settings.py:65
270 msgid "START_TLS on LDAP connection"
271 msgstr ""
272
273 #: rhodecode/controllers/admin/ldap_settings.py:115
274 msgid "Ldap settings updated successfully"
275 msgstr "LDAP設定更新完成"
276
277 #: rhodecode/controllers/admin/ldap_settings.py:120
278 msgid "Unable to activate ldap. The \"python-ldap\" library is missing."
279 msgstr "無法啟用LDAP。找不到python-ldap函式庫"
280
281 #: rhodecode/controllers/admin/ldap_settings.py:134
282 msgid "error occurred during update of ldap settings"
283 msgstr ""
284
285 #: rhodecode/controllers/admin/permissions.py:56
286 msgid "None"
287 msgstr "無"
288
289 #: rhodecode/controllers/admin/permissions.py:57
290 msgid "Read"
291 msgstr "讀"
292
293 #: rhodecode/controllers/admin/permissions.py:58
294 msgid "Write"
295 msgstr "寫"
296
297 #: rhodecode/controllers/admin/permissions.py:59
298 #: rhodecode/templates/admin/ldap/ldap.html:9
299 #: rhodecode/templates/admin/permissions/permissions.html:9
300 #: rhodecode/templates/admin/repos/repo_add.html:9
301 #: rhodecode/templates/admin/repos/repo_edit.html:9
302 #: rhodecode/templates/admin/repos/repos.html:10
303 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:8
304 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:8
305 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:10
306 #: rhodecode/templates/admin/settings/hooks.html:9
307 #: rhodecode/templates/admin/settings/settings.html:9
308 #: rhodecode/templates/admin/users/user_add.html:8
309 #: rhodecode/templates/admin/users/user_edit.html:9
310 #: rhodecode/templates/admin/users/user_edit.html:110
311 #: rhodecode/templates/admin/users/users.html:9
312 #: rhodecode/templates/admin/users_groups/users_group_add.html:8
313 #: rhodecode/templates/admin/users_groups/users_group_edit.html:9
314 #: rhodecode/templates/admin/users_groups/users_groups.html:9
315 #: rhodecode/templates/base/base.html:279
316 #: rhodecode/templates/base/base.html:366
317 #: rhodecode/templates/base/base.html:368
318 #: rhodecode/templates/base/base.html:370
319 msgid "Admin"
320 msgstr "管理"
321
322 #: rhodecode/controllers/admin/permissions.py:62
323 msgid "disabled"
324 msgstr "停用"
325
326 #: rhodecode/controllers/admin/permissions.py:64
327 msgid "allowed with manual account activation"
328 msgstr "允許手動啟用帳號"
329
330 #: rhodecode/controllers/admin/permissions.py:66
331 msgid "allowed with automatic account activation"
332 msgstr "允許自動啟用帳號"
333
334 #: rhodecode/controllers/admin/permissions.py:68
335 msgid "Disabled"
336 msgstr "停用"
337
338 #: rhodecode/controllers/admin/permissions.py:69
339 msgid "Enabled"
340 msgstr "啟用"
341
342 #: rhodecode/controllers/admin/permissions.py:102
343 msgid "Default permissions updated successfully"
344 msgstr "預設權限更新完成"
345
346 #: rhodecode/controllers/admin/permissions.py:119
347 msgid "error occurred during update of permissions"
348 msgstr ""
349
350 #: rhodecode/controllers/admin/repos.py:96
351 #, python-format
352 msgid "%s repository is not mapped to db perhaps it was created or renamed from the filesystem please run the application again in order to rescan repositories"
353 msgstr ""
354
355 #: rhodecode/controllers/admin/repos.py:172
356 #, python-format
357 msgid "created repository %s from %s"
358 msgstr "建立版本庫 %s 到 %s"
359
360 #: rhodecode/controllers/admin/repos.py:176
361 #, python-format
362 msgid "created repository %s"
363 msgstr "建立版本庫 %s"
364
365 #: rhodecode/controllers/admin/repos.py:205
366 #, python-format
367 msgid "error occurred during creation of repository %s"
368 msgstr ""
369
370 #: rhodecode/controllers/admin/repos.py:292
371 #, python-format
372 msgid "Cannot delete %s it still contains attached forks"
373 msgstr ""
374
375 #: rhodecode/controllers/admin/repos.py:320
376 msgid "An error occurred during deletion of repository user"
377 msgstr ""
378
379 #: rhodecode/controllers/admin/repos.py:335
380 msgid "An error occurred during deletion of repository users groups"
381 msgstr ""
382
383 #: rhodecode/controllers/admin/repos.py:352
384 msgid "An error occurred during deletion of repository stats"
385 msgstr ""
386
387 #: rhodecode/controllers/admin/repos.py:367
388 msgid "An error occurred during cache invalidation"
389 msgstr ""
390
391 #: rhodecode/controllers/admin/repos.py:387
392 msgid "Updated repository visibility in public journal"
393 msgstr ""
394
395 #: rhodecode/controllers/admin/repos.py:390
396 msgid "An error occurred during setting this repository in public journal"
397 msgstr ""
398
399 #: rhodecode/controllers/admin/repos.py:395
400 #: rhodecode/model/forms.py:53
401 msgid "Token mismatch"
402 msgstr ""
403
404 #: rhodecode/controllers/admin/repos.py:408
405 msgid "Pulled from remote location"
406 msgstr ""
407
408 #: rhodecode/controllers/admin/repos.py:410
409 msgid "An error occurred during pull from remote location"
410 msgstr ""
411
412 #: rhodecode/controllers/admin/repos_groups.py:83
413 #, python-format
414 msgid "created repos group %s"
415 msgstr "建立版本庫群組 %s"
416
417 #: rhodecode/controllers/admin/repos_groups.py:96
418 #, python-format
419 msgid "error occurred during creation of repos group %s"
420 msgstr ""
421
422 #: rhodecode/controllers/admin/repos_groups.py:130
423 #, python-format
424 msgid "updated repos group %s"
425 msgstr "更新版本庫群組 %s"
426
427 #: rhodecode/controllers/admin/repos_groups.py:143
428 #, python-format
429 msgid "error occurred during update of repos group %s"
430 msgstr ""
431
432 #: rhodecode/controllers/admin/repos_groups.py:164
433 #, python-format
434 msgid "This group contains %s repositores and cannot be deleted"
435 msgstr ""
436
437 #: rhodecode/controllers/admin/repos_groups.py:171
438 #, python-format
439 msgid "removed repos group %s"
440 msgstr "移除版本庫群組 %s"
441
442 #: rhodecode/controllers/admin/repos_groups.py:175
443 #, python-format
444 msgid "error occurred during deletion of repos group %s"
445 msgstr ""
446
447 #: rhodecode/controllers/admin/settings.py:109
448 #, python-format
449 msgid "Repositories successfully rescanned added: %s,removed: %s"
450 msgstr ""
451
452 #: rhodecode/controllers/admin/settings.py:118
453 msgid "Whoosh reindex task scheduled"
454 msgstr "Whoosh 重新索引工作排程"
455
456 #: rhodecode/controllers/admin/settings.py:143
457 msgid "Updated application settings"
458 msgstr "更新應用設定"
459
460 #: rhodecode/controllers/admin/settings.py:148
461 #: rhodecode/controllers/admin/settings.py:215
462 msgid "error occurred during updating application settings"
463 msgstr ""
464
465 #: rhodecode/controllers/admin/settings.py:210
466 msgid "Updated mercurial settings"
467 msgstr "更新 mercurial 設定"
468
469 #: rhodecode/controllers/admin/settings.py:236
470 msgid "Added new hook"
471 msgstr "新增hook"
472
473 #: rhodecode/controllers/admin/settings.py:247
474 msgid "Updated hooks"
475 msgstr "更新hook"
476
477 #: rhodecode/controllers/admin/settings.py:251
478 msgid "error occurred during hook creation"
479 msgstr ""
480
481 #: rhodecode/controllers/admin/settings.py:310
482 msgid "You can't edit this user since it's crucial for entire application"
483 msgstr ""
484
485 #: rhodecode/controllers/admin/settings.py:339
486 msgid "Your account was updated successfully"
487 msgstr "您的帳號已更新完成"
488
489 #: rhodecode/controllers/admin/settings.py:359
490 #: rhodecode/controllers/admin/users.py:130
491 #, python-format
492 msgid "error occurred during update of user %s"
493 msgstr ""
494
495 #: rhodecode/controllers/admin/users.py:78
496 #, python-format
497 msgid "created user %s"
498 msgstr "建立使用者 %s"
499
500 #: rhodecode/controllers/admin/users.py:90
501 #, python-format
502 msgid "error occurred during creation of user %s"
503 msgstr ""
504
505 #: rhodecode/controllers/admin/users.py:116
506 msgid "User updated successfully"
507 msgstr "使用者更新完成"
508
509 #: rhodecode/controllers/admin/users.py:146
510 msgid "successfully deleted user"
511 msgstr "成功刪除使用者"
512
513 #: rhodecode/controllers/admin/users.py:150
514 msgid "An error occurred during deletion of user"
515 msgstr ""
516
517 #: rhodecode/controllers/admin/users.py:166
518 msgid "You can't edit this user"
519 msgstr "您無法編輯這位使用者"
520
521 #: rhodecode/controllers/admin/users.py:195
522 #: rhodecode/controllers/admin/users_groups.py:202
523 msgid "Granted 'repository create' permission to user"
524 msgstr ""
525
526 #: rhodecode/controllers/admin/users.py:204
527 #: rhodecode/controllers/admin/users_groups.py:211
528 msgid "Revoked 'repository create' permission to user"
529 msgstr ""
530
531 #: rhodecode/controllers/admin/users_groups.py:74
532 #, python-format
533 msgid "created users group %s"
534 msgstr "建立使用者群組 %s"
535
536 #: rhodecode/controllers/admin/users_groups.py:86
537 #, python-format
538 msgid "error occurred during creation of users group %s"
539 msgstr ""
540
541 #: rhodecode/controllers/admin/users_groups.py:119
542 #, python-format
543 msgid "updated users group %s"
544 msgstr "更新使用者群組 %s"
545
546 #: rhodecode/controllers/admin/users_groups.py:138
547 #, python-format
548 msgid "error occurred during update of users group %s"
549 msgstr ""
550
551 #: rhodecode/controllers/admin/users_groups.py:154
552 msgid "successfully deleted users group"
553 msgstr "成功移除使用者群組"
554
555 #: rhodecode/controllers/admin/users_groups.py:158
556 msgid "An error occurred during deletion of users group"
557 msgstr ""
558
559 #: rhodecode/lib/__init__.py:279
560 msgid "year"
561 msgstr "年"
562
563 #: rhodecode/lib/__init__.py:280
564 msgid "month"
565 msgstr "月"
566
567 #: rhodecode/lib/__init__.py:281
568 msgid "day"
569 msgstr "日"
570
571 #: rhodecode/lib/__init__.py:282
572 msgid "hour"
573 msgstr "時"
574
575 #: rhodecode/lib/__init__.py:283
576 msgid "minute"
577 msgstr "分"
578
579 #: rhodecode/lib/__init__.py:284
580 msgid "second"
581 msgstr "秒"
582
583 #: rhodecode/lib/__init__.py:293
584 msgid "ago"
585 msgstr "之前"
586
587 #: rhodecode/lib/__init__.py:296
588 msgid "just now"
589 msgstr "現在"
590
591 #: rhodecode/lib/auth.py:377
592 msgid "You need to be a registered user to perform this action"
593 msgstr "您必須是註冊使用者才能執行這個動作"
594
595 #: rhodecode/lib/auth.py:421
596 msgid "You need to be a signed in to view this page"
597 msgstr "您必須登入後才能瀏覽這個頁面"
598
599 #: rhodecode/lib/helpers.py:307
600 msgid "True"
601 msgstr "真"
602
603 #: rhodecode/lib/helpers.py:311
604 msgid "False"
605 msgstr "假"
606
607 #: rhodecode/lib/helpers.py:352
608 #, python-format
609 msgid "Show all combined changesets %s->%s"
610 msgstr ""
611
612 #: rhodecode/lib/helpers.py:356
613 msgid "compare view"
614 msgstr ""
615
616 #: rhodecode/lib/helpers.py:365
617 msgid "and"
618 msgstr "和"
619
620 #: rhodecode/lib/helpers.py:365
621 #, python-format
622 msgid "%s more"
623 msgstr ""
624
625 #: rhodecode/lib/helpers.py:367
626 #: rhodecode/templates/changelog/changelog.html:14
627 #: rhodecode/templates/changelog/changelog.html:39
628 msgid "revisions"
629 msgstr "修訂"
630
631 #: rhodecode/lib/helpers.py:385
632 msgid "fork name "
633 msgstr "fork 名稱"
634
635 #: rhodecode/lib/helpers.py:388
636 msgid "[deleted] repository"
637 msgstr ""
638
639 #: rhodecode/lib/helpers.py:389
640 #: rhodecode/lib/helpers.py:393
641 msgid "[created] repository"
642 msgstr ""
643
644 #: rhodecode/lib/helpers.py:390
645 #: rhodecode/lib/helpers.py:394
646 msgid "[forked] repository"
647 msgstr ""
648
649 #: rhodecode/lib/helpers.py:391
650 #: rhodecode/lib/helpers.py:395
651 msgid "[updated] repository"
652 msgstr ""
653
654 #: rhodecode/lib/helpers.py:392
655 msgid "[delete] repository"
656 msgstr ""
657
658 #: rhodecode/lib/helpers.py:396
659 msgid "[pushed] into"
660 msgstr ""
661
662 #: rhodecode/lib/helpers.py:397
663 msgid "[committed via RhodeCode] into"
664 msgstr ""
665
666 #: rhodecode/lib/helpers.py:398
667 msgid "[pulled from remote] into"
668 msgstr ""
669
670 #: rhodecode/lib/helpers.py:399
671 msgid "[pulled] from"
672 msgstr ""
673
674 #: rhodecode/lib/helpers.py:400
675 msgid "[started following] repository"
676 msgstr ""
677
678 #: rhodecode/lib/helpers.py:401
679 msgid "[stopped following] repository"
680 msgstr ""
681
682 #: rhodecode/lib/helpers.py:577
683 #, python-format
684 msgid " and %s more"
685 msgstr ""
686
687 #: rhodecode/lib/helpers.py:581
688 msgid "No Files"
689 msgstr "沒有檔案"
690
691 #: rhodecode/model/forms.py:66
692 msgid "Invalid username"
693 msgstr "無效的使用者名稱"
694
695 #: rhodecode/model/forms.py:75
696 msgid "This username already exists"
697 msgstr "使用者名稱已存在"
698
699 #: rhodecode/model/forms.py:79
700 msgid "Username may only contain alphanumeric characters underscores, periods or dashes and must begin with alphanumeric character"
701 msgstr "使用者名稱只能使用字母數字、底線、小數點或破折號,且必須使用數字或字母開頭"
702
703 #: rhodecode/model/forms.py:94
704 msgid "Invalid group name"
705 msgstr "無效的群組名稱"
706
707 #: rhodecode/model/forms.py:104
708 msgid "This users group already exists"
709 msgstr "這個使用者群組已存在"
710
711 #: rhodecode/model/forms.py:110
712 msgid "Group name may only contain alphanumeric characters underscores, periods or dashes and must begin with alphanumeric character"
713 msgstr "群組名稱只能使用字母數字、底線、小數點或破折號,且必須使用數字或字母開頭"
714
715 #: rhodecode/model/forms.py:132
716 msgid "Cannot assign this group as parent"
717 msgstr ""
718
719 #: rhodecode/model/forms.py:148
720 msgid "This group already exists"
721 msgstr "這個群組已存在"
722
723 #: rhodecode/model/forms.py:164
724 #: rhodecode/model/forms.py:172
725 #: rhodecode/model/forms.py:180
726 msgid "Invalid characters in password"
727 msgstr "無效的字元在密碼中"
728
729 #: rhodecode/model/forms.py:191
730 msgid "Passwords do not match"
731 msgstr "密碼不相符"
732
733 #: rhodecode/model/forms.py:196
734 msgid "invalid password"
735 msgstr "無效的密碼"
736
737 #: rhodecode/model/forms.py:197
738 msgid "invalid user name"
739 msgstr "無效的使用者名稱"
740
741 #: rhodecode/model/forms.py:198
742 msgid "Your account is disabled"
743 msgstr "您的帳號已被停用"
744
745 #: rhodecode/model/forms.py:233
746 msgid "This username is not valid"
747 msgstr "無效的使用者名稱"
748
749 #: rhodecode/model/forms.py:245
750 msgid "This repository name is disallowed"
751 msgstr "不允許的版本庫名稱"
752
753 #: rhodecode/model/forms.py:266
754 #, python-format
755 msgid "This repository already exists in group \"%s\""
756 msgstr "這個版本庫已存在於群組 \"%s\""
757
758 #: rhodecode/model/forms.py:274
759 msgid "This repository already exists"
760 msgstr "這個版本庫已經存在"
761
762 #: rhodecode/model/forms.py:312
763 #: rhodecode/model/forms.py:319
764 msgid "invalid clone url"
765 msgstr "無效的複製URL"
766
767 #: rhodecode/model/forms.py:322
768 msgid "Invalid clone url, provide a valid clone http\\s url"
769 msgstr ""
770
771 #: rhodecode/model/forms.py:334
772 msgid "Fork have to be the same type as original"
773 msgstr "Fork 必須使用相同的版本庫類型"
774
775 #: rhodecode/model/forms.py:341
776 msgid "This username or users group name is not valid"
777 msgstr "使用者名稱或群組名稱無效"
778
779 #: rhodecode/model/forms.py:403
780 msgid "This is not a valid path"
781 msgstr "不是一個有效的路徑"
782
783 #: rhodecode/model/forms.py:416
784 msgid "This e-mail address is already taken"
785 msgstr "這個郵件位址已經使用了"
786
787 #: rhodecode/model/forms.py:427
788 msgid "This e-mail address doesn't exist."
789 msgstr "這個郵件位址不存在"
790
791 #: rhodecode/model/forms.py:447
792 msgid "The LDAP Login attribute of the CN must be specified - this is the name of the attribute that is equivalent to 'username'"
793 msgstr ""
794
795 #: rhodecode/model/forms.py:466
796 msgid "Please enter a login"
797 msgstr "請登入"
798
799 #: rhodecode/model/forms.py:467
800 #, python-format
801 msgid "Enter a value %(min)i characters long or more"
802 msgstr ""
803
804 #: rhodecode/model/forms.py:475
805 msgid "Please enter a password"
806 msgstr "請輸入密碼"
807
808 #: rhodecode/model/forms.py:476
809 #, python-format
810 msgid "Enter %(min)i characters or more"
811 msgstr ""
812
813 #: rhodecode/model/user.py:145
814 msgid "[RhodeCode] New User registration"
815 msgstr "[RhodeCode] 新使用者註冊"
816
817 #: rhodecode/model/user.py:157
818 #: rhodecode/model/user.py:179
819 msgid "You can't Edit this user since it's crucial for entire application"
820 msgstr "您無法編輯這個使用者,因為他是系統帳號"
821
822 #: rhodecode/model/user.py:201
823 msgid "You can't remove this user since it's crucial for entire application"
824 msgstr "您無法移除這個使用者,因為他是系統帳號"
825
826 #: rhodecode/model/user.py:204
827 #, python-format
828 msgid "This user still owns %s repositories and cannot be removed. Switch owners or remove those repositories"
829 msgstr "這個使用者擁有 %s 個版本庫所以無法移除,請先變更版本庫擁有者或者刪除版本庫"
830
831 #: rhodecode/templates/index.html:4
832 msgid "Dashboard"
833 msgstr "儀表板"
834
835 #: rhodecode/templates/index_base.html:22
836 #: rhodecode/templates/admin/users/user_edit_my_account.html:102
837 msgid "quick filter..."
838 msgstr "快速過濾..."
839
840 #: rhodecode/templates/index_base.html:23
841 #: rhodecode/templates/base/base.html:300
842 msgid "repositories"
843 msgstr "個版本庫"
844
845 #: rhodecode/templates/index_base.html:29
846 #: rhodecode/templates/admin/repos/repos.html:22
847 msgid "ADD NEW REPOSITORY"
848 msgstr "新增版本庫"
849
850 #: rhodecode/templates/index_base.html:41
851 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:32
852 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:32
853 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:33
854 #: rhodecode/templates/admin/users_groups/users_group_add.html:32
855 #: rhodecode/templates/admin/users_groups/users_group_edit.html:33
856 msgid "Group name"
857 msgstr "群組名稱"
858
859 #: rhodecode/templates/index_base.html:42
860 #: rhodecode/templates/index_base.html:73
861 #: rhodecode/templates/admin/repos/repo_add_base.html:44
862 #: rhodecode/templates/admin/repos/repo_edit.html:64
863 #: rhodecode/templates/admin/repos/repos.html:31
864 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:41
865 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:41
866 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:34
867 #: rhodecode/templates/settings/repo_fork.html:40
868 #: rhodecode/templates/settings/repo_settings.html:40
869 #: rhodecode/templates/summary/summary.html:92
870 msgid "Description"
871 msgstr "描述"
872
873 #: rhodecode/templates/index_base.html:53
874 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:46
875 msgid "Repositories group"
876 msgstr "版本庫群組"
877
878 #: rhodecode/templates/index_base.html:72
879 #: rhodecode/templates/admin/repos/repo_add_base.html:9
880 #: rhodecode/templates/admin/repos/repo_edit.html:32
881 #: rhodecode/templates/admin/repos/repos.html:30
882 #: rhodecode/templates/admin/users/user_edit_my_account.html:117
883 #: rhodecode/templates/files/files_browser.html:157
884 #: rhodecode/templates/settings/repo_settings.html:31
885 #: rhodecode/templates/summary/summary.html:31
886 #: rhodecode/templates/summary/summary.html:107
887 msgid "Name"
888 msgstr "名稱"
889
890 #: rhodecode/templates/index_base.html:74
891 #: rhodecode/templates/admin/repos/repos.html:32
892 #: rhodecode/templates/summary/summary.html:114
893 msgid "Last change"
894 msgstr "最後修改"
895
896 #: rhodecode/templates/index_base.html:75
897 #: rhodecode/templates/admin/repos/repos.html:33
898 msgid "Tip"
899 msgstr ""
900
901 #: rhodecode/templates/index_base.html:76
902 #: rhodecode/templates/admin/repos/repo_edit.html:97
903 msgid "Owner"
904 msgstr "擁有者"
905
906 #: rhodecode/templates/index_base.html:77
907 #: rhodecode/templates/journal/public_journal.html:20
908 #: rhodecode/templates/summary/summary.html:180
909 #: rhodecode/templates/summary/summary.html:183
910 msgid "RSS"
911 msgstr ""
912
913 #: rhodecode/templates/index_base.html:78
914 #: rhodecode/templates/journal/public_journal.html:23
915 #: rhodecode/templates/summary/summary.html:181
916 #: rhodecode/templates/summary/summary.html:184
917 msgid "Atom"
918 msgstr ""
919
920 #: rhodecode/templates/index_base.html:87
921 #: rhodecode/templates/index_base.html:89
922 #: rhodecode/templates/index_base.html:91
923 #: rhodecode/templates/base/base.html:209
924 #: rhodecode/templates/base/base.html:211
925 #: rhodecode/templates/base/base.html:213
926 #: rhodecode/templates/summary/summary.html:4
927 msgid "Summary"
928 msgstr "概況"
929
930 #: rhodecode/templates/index_base.html:95
931 #: rhodecode/templates/index_base.html:97
932 #: rhodecode/templates/index_base.html:99
933 #: rhodecode/templates/base/base.html:225
934 #: rhodecode/templates/base/base.html:227
935 #: rhodecode/templates/base/base.html:229
936 #: rhodecode/templates/changelog/changelog.html:6
937 #: rhodecode/templates/changelog/changelog.html:14
938 msgid "Changelog"
939 msgstr "修改紀錄"
940
941 #: rhodecode/templates/index_base.html:103
942 #: rhodecode/templates/index_base.html:105
943 #: rhodecode/templates/index_base.html:107
944 #: rhodecode/templates/base/base.html:268
945 #: rhodecode/templates/base/base.html:270
946 #: rhodecode/templates/base/base.html:272
947 #: rhodecode/templates/files/files.html:4
948 msgid "Files"
949 msgstr "檔案"
950
951 #: rhodecode/templates/index_base.html:116
952 #: rhodecode/templates/admin/repos/repos.html:42
953 #: rhodecode/templates/admin/users/user_edit_my_account.html:127
954 #: rhodecode/templates/summary/summary.html:48
955 msgid "Mercurial repository"
956 msgstr "Mercurial 版本庫"
957
958 #: rhodecode/templates/index_base.html:118
959 #: rhodecode/templates/admin/repos/repos.html:44
960 #: rhodecode/templates/admin/users/user_edit_my_account.html:129
961 #: rhodecode/templates/summary/summary.html:51
962 msgid "Git repository"
963 msgstr "Git 版本庫"
964
965 #: rhodecode/templates/index_base.html:123
966 #: rhodecode/templates/admin/repos/repo_edit_perms.html:16
967 #: rhodecode/templates/journal/journal.html:53
968 #: rhodecode/templates/summary/summary.html:56
969 msgid "private repository"
970 msgstr "私有版本庫"
971
972 #: rhodecode/templates/index_base.html:125
973 #: rhodecode/templates/journal/journal.html:55
974 #: rhodecode/templates/summary/summary.html:58
975 msgid "public repository"
976 msgstr "公開版本庫"
977
978 #: rhodecode/templates/index_base.html:133
979 #: rhodecode/templates/base/base.html:291
980 #: rhodecode/templates/settings/repo_fork.html:13
981 msgid "fork"
982 msgstr ""
983
984 #: rhodecode/templates/index_base.html:134
985 #: rhodecode/templates/admin/repos/repos.html:60
986 #: rhodecode/templates/admin/users/user_edit_my_account.html:143
987 #: rhodecode/templates/summary/summary.html:69
988 #: rhodecode/templates/summary/summary.html:71
989 msgid "Fork of"
990 msgstr ""
991
992 #: rhodecode/templates/index_base.html:155
993 #: rhodecode/templates/admin/repos/repos.html:73
994 msgid "No changesets yet"
995 msgstr "尚未有任何變更"
996
997 #: rhodecode/templates/index_base.html:161
998 #: rhodecode/templates/index_base.html:163
999 #, python-format
1000 msgid "Subscribe to %s rss feed"
1001 msgstr "訂閱 %s rss"
1002
1003 #: rhodecode/templates/index_base.html:168
1004 #: rhodecode/templates/index_base.html:170
1005 #, python-format
1006 msgid "Subscribe to %s atom feed"
1007 msgstr "訂閱 %s atom"
1008
1009 #: rhodecode/templates/login.html:5
1010 #: rhodecode/templates/login.html:54
1011 #: rhodecode/templates/base/base.html:38
1012 msgid "Sign In"
1013 msgstr "登入"
1014
1015 #: rhodecode/templates/login.html:21
1016 msgid "Sign In to"
1017 msgstr "登入"
1018
1019 #: rhodecode/templates/login.html:31
1020 #: rhodecode/templates/register.html:20
1021 #: rhodecode/templates/admin/admin_log.html:5
1022 #: rhodecode/templates/admin/users/user_add.html:32
1023 #: rhodecode/templates/admin/users/user_edit.html:47
1024 #: rhodecode/templates/admin/users/user_edit_my_account.html:45
1025 #: rhodecode/templates/base/base.html:15
1026 #: rhodecode/templates/summary/summary.html:106
1027 msgid "Username"
1028 msgstr "帳號"
1029
1030 #: rhodecode/templates/login.html:40
1031 #: rhodecode/templates/register.html:29
1032 #: rhodecode/templates/admin/ldap/ldap.html:46
1033 #: rhodecode/templates/admin/users/user_add.html:41
1034 #: rhodecode/templates/base/base.html:24
1035 msgid "Password"
1036 msgstr "密碼"
1037
1038 #: rhodecode/templates/login.html:60
1039 msgid "Forgot your password ?"
1040 msgstr "忘記您的密碼?"
1041
1042 #: rhodecode/templates/login.html:63
1043 #: rhodecode/templates/base/base.html:35
1044 msgid "Don't have an account ?"
1045 msgstr "沒有帳號?"
1046
1047 #: rhodecode/templates/password_reset.html:5
1048 msgid "Reset your password"
1049 msgstr "重設您的密碼"
1050
1051 #: rhodecode/templates/password_reset.html:11
1052 msgid "Reset your password to"
1053 msgstr "重設您的密碼"
1054
1055 #: rhodecode/templates/password_reset.html:21
1056 msgid "Email address"
1057 msgstr "郵件位址"
1058
1059 #: rhodecode/templates/password_reset.html:30
1060 msgid "Reset my password"
1061 msgstr "重設我的密碼"
1062
1063 #: rhodecode/templates/password_reset.html:31
1064 msgid "Password reset link will be send to matching email address"
1065 msgstr "密碼重設連結已郵寄至您的信箱"
1066
1067 #: rhodecode/templates/register.html:5
1068 #: rhodecode/templates/register.html:74
1069 msgid "Sign Up"
1070 msgstr "登入"
1071
1072 #: rhodecode/templates/register.html:11
1073 msgid "Sign Up to"
1074 msgstr "登入"
1075
1076 #: rhodecode/templates/register.html:38
1077 msgid "Re-enter password"
1078 msgstr "確認密碼"
1079
1080 #: rhodecode/templates/register.html:47
1081 #: rhodecode/templates/admin/users/user_add.html:50
1082 #: rhodecode/templates/admin/users/user_edit.html:74
1083 #: rhodecode/templates/admin/users/user_edit_my_account.html:63
1084 msgid "First Name"
1085 msgstr "名"
1086
1087 #: rhodecode/templates/register.html:56
1088 #: rhodecode/templates/admin/users/user_add.html:59
1089 #: rhodecode/templates/admin/users/user_edit.html:83
1090 #: rhodecode/templates/admin/users/user_edit_my_account.html:72
1091 msgid "Last Name"
1092 msgstr "姓"
1093
1094 #: rhodecode/templates/register.html:65
1095 #: rhodecode/templates/admin/users/user_add.html:68
1096 #: rhodecode/templates/admin/users/user_edit.html:92
1097 #: rhodecode/templates/admin/users/user_edit_my_account.html:81
1098 #: rhodecode/templates/summary/summary.html:108
1099 msgid "Email"
1100 msgstr "電子郵件"
1101
1102 #: rhodecode/templates/register.html:76
1103 msgid "Your account will be activated right after registration"
1104 msgstr "您的帳號註冊後將會啟用"
1105
1106 #: rhodecode/templates/register.html:78
1107 msgid "Your account must wait for activation by administrator"
1108 msgstr "您的帳號註冊後將等待管理員啟用"
1109
1110 #: rhodecode/templates/repo_switcher_list.html:14
1111 msgid "Private repository"
1112 msgstr "私有的版本庫"
1113
1114 #: rhodecode/templates/repo_switcher_list.html:19
1115 msgid "Public repository"
1116 msgstr "公開的版本庫"
1117
1118 #: rhodecode/templates/admin/admin.html:5
1119 #: rhodecode/templates/admin/admin.html:9
1120 msgid "Admin journal"
1121 msgstr "管理員日誌"
1122
1123 #: rhodecode/templates/admin/admin_log.html:6
1124 msgid "Action"
1125 msgstr "動作"
1126
1127 #: rhodecode/templates/admin/admin_log.html:7
1128 msgid "Repository"
1129 msgstr "版本庫"
1130
1131 #: rhodecode/templates/admin/admin_log.html:8
1132 msgid "Date"
1133 msgstr "時間"
1134
1135 #: rhodecode/templates/admin/admin_log.html:9
1136 msgid "From IP"
1137 msgstr "來源IP"
1138
1139 #: rhodecode/templates/admin/admin_log.html:52
1140 msgid "No actions yet"
1141 msgstr ""
1142
1143 #: rhodecode/templates/admin/ldap/ldap.html:5
1144 msgid "LDAP administration"
1145 msgstr "LDAP管理者"
1146
1147 #: rhodecode/templates/admin/ldap/ldap.html:11
1148 msgid "Ldap"
1149 msgstr ""
1150
1151 #: rhodecode/templates/admin/ldap/ldap.html:28
1152 msgid "Connection settings"
1153 msgstr "連接設定"
1154
1155 #: rhodecode/templates/admin/ldap/ldap.html:30
1156 msgid "Enable LDAP"
1157 msgstr "啟動LDAP"
1158
1159 #: rhodecode/templates/admin/ldap/ldap.html:34
1160 msgid "Host"
1161 msgstr "主機"
1162
1163 #: rhodecode/templates/admin/ldap/ldap.html:38
1164 msgid "Port"
1165 msgstr "連接埠"
1166
1167 #: rhodecode/templates/admin/ldap/ldap.html:42
1168 msgid "Account"
1169 msgstr "帳號"
1170
1171 #: rhodecode/templates/admin/ldap/ldap.html:50
1172 msgid "Connection security"
1173 msgstr "連接安全性"
1174
1175 #: rhodecode/templates/admin/ldap/ldap.html:54
1176 msgid "Certificate Checks"
1177 msgstr "憑證確認"
1178
1179 #: rhodecode/templates/admin/ldap/ldap.html:57
1180 msgid "Search settings"
1181 msgstr "搜尋選項"
1182
1183 #: rhodecode/templates/admin/ldap/ldap.html:59
1184 msgid "Base DN"
1185 msgstr ""
1186
1187 #: rhodecode/templates/admin/ldap/ldap.html:63
1188 msgid "LDAP Filter"
1189 msgstr ""
1190
1191 #: rhodecode/templates/admin/ldap/ldap.html:67
1192 msgid "LDAP Search Scope"
1193 msgstr ""
1194
1195 #: rhodecode/templates/admin/ldap/ldap.html:70
1196 msgid "Attribute mappings"
1197 msgstr "屬性對應"
1198
1199 #: rhodecode/templates/admin/ldap/ldap.html:72
1200 msgid "Login Attribute"
1201 msgstr "登入屬性"
1202
1203 #: rhodecode/templates/admin/ldap/ldap.html:76
1204 msgid "First Name Attribute"
1205 msgstr "名"
1206
1207 #: rhodecode/templates/admin/ldap/ldap.html:80
1208 msgid "Last Name Attribute"
1209 msgstr "姓"
1210
1211 #: rhodecode/templates/admin/ldap/ldap.html:84
1212 msgid "E-mail Attribute"
1213 msgstr "電子郵件屬性"
1214
1215 #: rhodecode/templates/admin/ldap/ldap.html:89
1216 #: rhodecode/templates/admin/settings/hooks.html:73
1217 #: rhodecode/templates/admin/users/user_edit.html:117
1218 #: rhodecode/templates/admin/users/user_edit.html:142
1219 #: rhodecode/templates/admin/users/user_edit_my_account.html:89
1220 #: rhodecode/templates/admin/users_groups/users_group_edit.html:263
1221 msgid "Save"
1222 msgstr "儲存"
1223
1224 #: rhodecode/templates/admin/permissions/permissions.html:5
1225 msgid "Permissions administration"
1226 msgstr "權限管理員"
1227
1228 #: rhodecode/templates/admin/permissions/permissions.html:11
1229 #: rhodecode/templates/admin/repos/repo_edit.html:109
1230 #: rhodecode/templates/admin/users/user_edit.html:127
1231 #: rhodecode/templates/admin/users_groups/users_group_edit.html:248
1232 #: rhodecode/templates/settings/repo_settings.html:58
1233 msgid "Permissions"
1234 msgstr "權限"
1235
1236 #: rhodecode/templates/admin/permissions/permissions.html:24
1237 msgid "Default permissions"
1238 msgstr "預設權限"
1239
1240 #: rhodecode/templates/admin/permissions/permissions.html:31
1241 msgid "Anonymous access"
1242 msgstr "訪客權限"
1243
1244 #: rhodecode/templates/admin/permissions/permissions.html:41
1245 msgid "Repository permission"
1246 msgstr "版本庫權限"
1247
1248 #: rhodecode/templates/admin/permissions/permissions.html:49
1249 msgid "All default permissions on each repository will be reset to choosen permission, note that all custom default permission on repositories will be lost"
1250 msgstr ""
1251
1252 #: rhodecode/templates/admin/permissions/permissions.html:50
1253 msgid "overwrite existing settings"
1254 msgstr "複寫已存在設定"
1255
1256 #: rhodecode/templates/admin/permissions/permissions.html:55
1257 msgid "Registration"
1258 msgstr "註冊"
1259
1260 #: rhodecode/templates/admin/permissions/permissions.html:63
1261 msgid "Repository creation"
1262 msgstr "版本庫建立"
1263
1264 #: rhodecode/templates/admin/permissions/permissions.html:71
1265 msgid "set"
1266 msgstr "設定"
1267
1268 #: rhodecode/templates/admin/repos/repo_add.html:5
1269 #: rhodecode/templates/admin/repos/repo_add_create_repository.html:5
1270 msgid "Add repository"
1271 msgstr "新增版本庫"
1272
1273 #: rhodecode/templates/admin/repos/repo_add.html:11
1274 #: rhodecode/templates/admin/repos/repo_edit.html:11
1275 #: rhodecode/templates/admin/repos/repos.html:10
1276 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:10
1277 msgid "Repositories"
1278 msgstr "版本庫"
1279
1280 #: rhodecode/templates/admin/repos/repo_add.html:13
1281 msgid "add new"
1282 msgstr "新增"
1283
1284 #: rhodecode/templates/admin/repos/repo_add_base.html:20
1285 #: rhodecode/templates/summary/summary.html:80
1286 #: rhodecode/templates/summary/summary.html:82
1287 msgid "Clone from"
1288 msgstr "複製由"
1289
1290 #: rhodecode/templates/admin/repos/repo_add_base.html:28
1291 #: rhodecode/templates/admin/repos/repo_edit.html:48
1292 #: rhodecode/templates/admin/repos_groups/repos_groups.html:4
1293 msgid "Repository group"
1294 msgstr "版本庫群組"
1295
1296 #: rhodecode/templates/admin/repos/repo_add_base.html:36
1297 #: rhodecode/templates/admin/repos/repo_edit.html:56
1298 msgid "Type"
1299 msgstr "類型"
1300
1301 #: rhodecode/templates/admin/repos/repo_add_base.html:52
1302 #: rhodecode/templates/admin/repos/repo_edit.html:73
1303 #: rhodecode/templates/settings/repo_fork.html:48
1304 #: rhodecode/templates/settings/repo_settings.html:49
1305 msgid "Private"
1306 msgstr "私有"
1307
1308 #: rhodecode/templates/admin/repos/repo_add_base.html:59
1309 msgid "add"
1310 msgstr "新增"
1311
1312 #: rhodecode/templates/admin/repos/repo_add_create_repository.html:9
1313 msgid "add new repository"
1314 msgstr "新增版本庫"
1315
1316 #: rhodecode/templates/admin/repos/repo_edit.html:5
1317 msgid "Edit repository"
1318 msgstr "編輯版本庫"
1319
1320 #: rhodecode/templates/admin/repos/repo_edit.html:13
1321 #: rhodecode/templates/admin/users/user_edit.html:13
1322 #: rhodecode/templates/admin/users/user_edit_my_account.html:148
1323 #: rhodecode/templates/admin/users_groups/users_group_edit.html:13
1324 #: rhodecode/templates/files/files_annotate.html:49
1325 #: rhodecode/templates/files/files_source.html:20
1326 msgid "edit"
1327 msgstr "編輯"
1328
1329 #: rhodecode/templates/admin/repos/repo_edit.html:40
1330 msgid "Clone uri"
1331 msgstr "複製URL"
1332
1333 #: rhodecode/templates/admin/repos/repo_edit.html:81
1334 msgid "Enable statistics"
1335 msgstr "啟用統計"
1336
1337 #: rhodecode/templates/admin/repos/repo_edit.html:89
1338 msgid "Enable downloads"
1339 msgstr "啟用下載"
1340
1341 #: rhodecode/templates/admin/repos/repo_edit.html:127
1342 msgid "Administration"
1343 msgstr "管理者"
1344
1345 #: rhodecode/templates/admin/repos/repo_edit.html:130
1346 msgid "Statistics"
1347 msgstr "統計"
1348
1349 #: rhodecode/templates/admin/repos/repo_edit.html:134
1350 msgid "Reset current statistics"
1351 msgstr "重設目前的統計"
1352
1353 #: rhodecode/templates/admin/repos/repo_edit.html:134
1354 msgid "Confirm to remove current statistics"
1355 msgstr "確認移除目前的統計"
1356
1357 #: rhodecode/templates/admin/repos/repo_edit.html:137
1358 msgid "Fetched to rev"
1359 msgstr ""
1360
1361 #: rhodecode/templates/admin/repos/repo_edit.html:138
1362 msgid "Percentage of stats gathered"
1363 msgstr ""
1364
1365 #: rhodecode/templates/admin/repos/repo_edit.html:147
1366 msgid "Remote"
1367 msgstr "遠端"
1368
1369 #: rhodecode/templates/admin/repos/repo_edit.html:151
1370 msgid "Pull changes from remote location"
1371 msgstr ""
1372
1373 #: rhodecode/templates/admin/repos/repo_edit.html:151
1374 msgid "Confirm to pull changes from remote side"
1375 msgstr ""
1376
1377 #: rhodecode/templates/admin/repos/repo_edit.html:162
1378 msgid "Cache"
1379 msgstr "快取"
1380
1381 #: rhodecode/templates/admin/repos/repo_edit.html:166
1382 msgid "Invalidate repository cache"
1383 msgstr ""
1384
1385 #: rhodecode/templates/admin/repos/repo_edit.html:166
1386 msgid "Confirm to invalidate repository cache"
1387 msgstr "確認廢止版本庫快取"
1388
1389 #: rhodecode/templates/admin/repos/repo_edit.html:177
1390 msgid "Remove from public journal"
1391 msgstr "從公開日誌移除"
1392
1393 #: rhodecode/templates/admin/repos/repo_edit.html:179
1394 msgid "Add to public journal"
1395 msgstr "新增至公開日誌"
1396
1397 #: rhodecode/templates/admin/repos/repo_edit.html:185
1398 msgid "Delete"
1399 msgstr "移除"
1400
1401 #: rhodecode/templates/admin/repos/repo_edit.html:189
1402 msgid "Remove this repository"
1403 msgstr "移除版本庫"
1404
1405 #: rhodecode/templates/admin/repos/repo_edit.html:189
1406 #: rhodecode/templates/admin/repos/repos.html:79
1407 msgid "Confirm to delete this repository"
1408 msgstr "確認移除這個版本庫"
1409
1410 #: rhodecode/templates/admin/repos/repo_edit_perms.html:3
1411 msgid "none"
1412 msgstr "無"
1413
1414 #: rhodecode/templates/admin/repos/repo_edit_perms.html:4
1415 msgid "read"
1416 msgstr "讀"
1417
1418 #: rhodecode/templates/admin/repos/repo_edit_perms.html:5
1419 msgid "write"
1420 msgstr "寫"
1421
1422 #: rhodecode/templates/admin/repos/repo_edit_perms.html:6
1423 #: rhodecode/templates/admin/users/users.html:38
1424 #: rhodecode/templates/base/base.html:296
1425 msgid "admin"
1426 msgstr "管理員"
1427
1428 #: rhodecode/templates/admin/repos/repo_edit_perms.html:7
1429 msgid "member"
1430 msgstr "成員"
1431
1432 #: rhodecode/templates/admin/repos/repo_edit_perms.html:33
1433 #: rhodecode/templates/admin/repos/repo_edit_perms.html:53
1434 msgid "revoke"
1435 msgstr ""
1436
1437 #: rhodecode/templates/admin/repos/repo_edit_perms.html:75
1438 msgid "Add another member"
1439 msgstr "新增另ㄧ位成員"
1440
1441 #: rhodecode/templates/admin/repos/repo_edit_perms.html:89
1442 msgid "Failed to remove user"
1443 msgstr "移除使用者失敗"
1444
1445 #: rhodecode/templates/admin/repos/repo_edit_perms.html:104
1446 msgid "Failed to remove users group"
1447 msgstr "移除使用者群組失敗"
1448
1449 #: rhodecode/templates/admin/repos/repo_edit_perms.html:205
1450 msgid "Group"
1451 msgstr "群組"
1452
1453 #: rhodecode/templates/admin/repos/repo_edit_perms.html:206
1454 #: rhodecode/templates/admin/users_groups/users_groups.html:33
1455 msgid "members"
1456 msgstr "成員"
1457
1458 #: rhodecode/templates/admin/repos/repos.html:5
1459 msgid "Repositories administration"
1460 msgstr "版本庫管理員"
1461
1462 #: rhodecode/templates/admin/repos/repos.html:34
1463 #: rhodecode/templates/summary/summary.html:100
1464 msgid "Contact"
1465 msgstr "聯絡方式"
1466
1467 #: rhodecode/templates/admin/repos/repos.html:35
1468 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:36
1469 #: rhodecode/templates/admin/users/user_edit_my_account.html:119
1470 #: rhodecode/templates/admin/users/users.html:40
1471 #: rhodecode/templates/admin/users_groups/users_groups.html:35
1472 msgid "action"
1473 msgstr "動作"
1474
1475 #: rhodecode/templates/admin/repos/repos.html:51
1476 #: rhodecode/templates/admin/users/user_edit_my_account.html:134
1477 #: rhodecode/templates/admin/users/user_edit_my_account.html:148
1478 msgid "private"
1479 msgstr "私有"
1480
1481 #: rhodecode/templates/admin/repos/repos.html:53
1482 #: rhodecode/templates/admin/repos/repos.html:59
1483 #: rhodecode/templates/admin/users/user_edit_my_account.html:136
1484 #: rhodecode/templates/admin/users/user_edit_my_account.html:142
1485 #: rhodecode/templates/summary/summary.html:68
1486 msgid "public"
1487 msgstr "公開"
1488
1489 #: rhodecode/templates/admin/repos/repos.html:79
1490 #: rhodecode/templates/admin/users/users.html:55
1491 msgid "delete"
1492 msgstr "刪除"
1493
1494 #: rhodecode/templates/admin/repos_groups/repos_groups.html:8
1495 msgid "Groups"
1496 msgstr "群組"
1497
1498 #: rhodecode/templates/admin/repos_groups/repos_groups.html:13
1499 msgid "with"
1500 msgstr ""
1501
1502 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:5
1503 msgid "Add repos group"
1504 msgstr "新增版本庫群組"
1505
1506 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:10
1507 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:10
1508 msgid "Repos groups"
1509 msgstr "版本庫群組"
1510
1511 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:12
1512 msgid "add new repos group"
1513 msgstr "新增版本庫群組"
1514
1515 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:50
1516 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:50
1517 msgid "Group parent"
1518 msgstr "父群組"
1519
1520 #: rhodecode/templates/admin/repos_groups/repos_groups_add.html:58
1521 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:58
1522 #: rhodecode/templates/admin/users/user_add.html:85
1523 #: rhodecode/templates/admin/users_groups/users_group_add.html:49
1524 #: rhodecode/templates/admin/users_groups/users_group_edit.html:90
1525 msgid "save"
1526 msgstr "儲存"
1527
1528 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:5
1529 msgid "Edit repos group"
1530 msgstr "編輯版本庫群組"
1531
1532 #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:12
1533 msgid "edit repos group"
1534 msgstr "編輯版本庫群組"
1535
1536 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:5
1537 msgid "Repositories groups administration"
1538 msgstr "版本庫群組管理員"
1539
1540 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:22
1541 msgid "ADD NEW GROUP"
1542 msgstr "新增群組"
1543
1544 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:35
1545 msgid "Number of repositories"
1546 msgstr "版本庫數量"
1547
1548 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:54
1549 msgid "Confirm to delete this group"
1550 msgstr "確認刪除這個群組"
1551
1552 #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:62
1553 msgid "There are no repositories groups yet"
1554 msgstr "沒有任何版本庫群組"
1555
1556 #: rhodecode/templates/admin/settings/hooks.html:5
1557 #: rhodecode/templates/admin/settings/settings.html:5
1558 msgid "Settings administration"
1559 msgstr "設定管理員"
1560
1561 #: rhodecode/templates/admin/settings/hooks.html:9
1562 #: rhodecode/templates/admin/settings/settings.html:9
1563 #: rhodecode/templates/settings/repo_settings.html:5
1564 #: rhodecode/templates/settings/repo_settings.html:13
1565 msgid "Settings"
1566 msgstr "設定"
1567
1568 #: rhodecode/templates/admin/settings/hooks.html:24
1569 msgid "Built in hooks - read only"
1570 msgstr "內建hook - 唯讀"
1571
1572 #: rhodecode/templates/admin/settings/hooks.html:40
1573 msgid "Custom hooks"
1574 msgstr "自訂hook"
1575
1576 #: rhodecode/templates/admin/settings/hooks.html:56
1577 msgid "remove"
1578 msgstr "移除"
1579
1580 #: rhodecode/templates/admin/settings/hooks.html:88
1581 msgid "Failed to remove hook"
1582 msgstr "移除hook失敗"
1583
1584 #: rhodecode/templates/admin/settings/settings.html:24
1585 msgid "Remap and rescan repositories"
1586 msgstr "重新對映與掃描版本庫"
1587
1588 #: rhodecode/templates/admin/settings/settings.html:32
1589 msgid "rescan option"
1590 msgstr "重新掃描選項"
1591
1592 #: rhodecode/templates/admin/settings/settings.html:38
1593 msgid "In case a repository was deleted from filesystem and there are leftovers in the database check this option to scan obsolete data in database and remove it."
1594 msgstr "如果版本庫已從檔案系統中刪除,但是資料還留在資料庫,請勾選這個項目清理資料庫中舊的資料"
1595
1596 #: rhodecode/templates/admin/settings/settings.html:39
1597 msgid "destroy old data"
1598 msgstr "移除舊資料"
1599
1600 #: rhodecode/templates/admin/settings/settings.html:45
1601 msgid "Rescan repositories"
1602 msgstr "重新掃描版本庫"
1603
1604 #: rhodecode/templates/admin/settings/settings.html:51
1605 msgid "Whoosh indexing"
1606 msgstr "Whoosh 索引"
1607
1608 #: rhodecode/templates/admin/settings/settings.html:59
1609 msgid "index build option"
1610 msgstr "索引選項"
1611
1612 #: rhodecode/templates/admin/settings/settings.html:64
1613 msgid "build from scratch"
1614 msgstr "重頭建立索引"
1615
1616 #: rhodecode/templates/admin/settings/settings.html:70
1617 msgid "Reindex"
1618 msgstr "重新索引"
1619
1620 #: rhodecode/templates/admin/settings/settings.html:76
1621 msgid "Global application settings"
1622 msgstr "全域設定"
1623
1624 #: rhodecode/templates/admin/settings/settings.html:85
1625 msgid "Application name"
1626 msgstr "應用名稱"
1627
1628 #: rhodecode/templates/admin/settings/settings.html:94
1629 msgid "Realm text"
1630 msgstr ""
1631
1632 #: rhodecode/templates/admin/settings/settings.html:103
1633 msgid "GA code"
1634 msgstr ""
1635
1636 #: rhodecode/templates/admin/settings/settings.html:111
1637 #: rhodecode/templates/admin/settings/settings.html:177
1638 msgid "Save settings"
1639 msgstr "儲存設定"
1640
1641 #: rhodecode/templates/admin/settings/settings.html:112
1642 #: rhodecode/templates/admin/settings/settings.html:178
1643 #: rhodecode/templates/admin/users/user_edit.html:118
1644 #: rhodecode/templates/admin/users/user_edit.html:143
1645 #: rhodecode/templates/admin/users/user_edit_my_account.html:90
1646 #: rhodecode/templates/admin/users_groups/users_group_edit.html:264
1647 #: rhodecode/templates/files/files_edit.html:50
1648 msgid "Reset"
1649 msgstr "重設"
1650
1651 #: rhodecode/templates/admin/settings/settings.html:118
1652 msgid "Mercurial settings"
1653 msgstr "Mercurial 設定"
1654
1655 #: rhodecode/templates/admin/settings/settings.html:127
1656 msgid "Web"
1657 msgstr ""
1658
1659 #: rhodecode/templates/admin/settings/settings.html:132
1660 msgid "require ssl for pushing"
1661 msgstr "推送時要求使用SSL"
1662
1663 #: rhodecode/templates/admin/settings/settings.html:139
1664 msgid "Hooks"
1665 msgstr ""
1666
1667 #: rhodecode/templates/admin/settings/settings.html:142
1668 msgid "advanced setup"
1669 msgstr "進階設定"
1670
1671 #: rhodecode/templates/admin/settings/settings.html:147
1672 msgid "Update repository after push (hg update)"
1673 msgstr "push後更新版本庫 (hg update)"
1674
1675 #: rhodecode/templates/admin/settings/settings.html:151
1676 msgid "Show repository size after push"
1677 msgstr "push 後顯示版本庫大小"
1678
1679 #: rhodecode/templates/admin/settings/settings.html:155
1680 msgid "Log user push commands"
1681 msgstr "紀錄使用者推送命令"
1682
1683 #: rhodecode/templates/admin/settings/settings.html:159
1684 msgid "Log user pull commands"
1685 msgstr "紀錄使用者抓取命令"
1686
1687 #: rhodecode/templates/admin/settings/settings.html:166
1688 msgid "Repositories location"
1689 msgstr "版本庫路徑"
1690
1691 #: rhodecode/templates/admin/settings/settings.html:171
1692 msgid "This a crucial application setting. If you are really sure you need to change this, you must restart application in order to make this setting take effect. Click this label to unlock."
1693 msgstr "這是一個關鍵的設定,如果您確定要修改這個設定,請重新啟動應用程式以套用設定"
1694
1695 #: rhodecode/templates/admin/settings/settings.html:172
1696 msgid "unlock"
1697 msgstr "解鎖"
1698
1699 #: rhodecode/templates/admin/users/user_add.html:5
1700 msgid "Add user"
1701 msgstr "新增使用者"
1702
1703 #: rhodecode/templates/admin/users/user_add.html:10
1704 #: rhodecode/templates/admin/users/user_edit.html:11
1705 #: rhodecode/templates/admin/users/users.html:9
1706 msgid "Users"
1707 msgstr "使用者"
1708
1709 #: rhodecode/templates/admin/users/user_add.html:12
1710 msgid "add new user"
1711 msgstr "新增使用者"
1712
1713 #: rhodecode/templates/admin/users/user_add.html:77
1714 #: rhodecode/templates/admin/users/user_edit.html:101
1715 #: rhodecode/templates/admin/users_groups/users_group_add.html:41
1716 #: rhodecode/templates/admin/users_groups/users_group_edit.html:42
1717 msgid "Active"
1718 msgstr "啟用"
1719
1720 #: rhodecode/templates/admin/users/user_edit.html:5
1721 msgid "Edit user"
1722 msgstr "編輯使用者"
1723
1724 #: rhodecode/templates/admin/users/user_edit.html:33
1725 #: rhodecode/templates/admin/users/user_edit_my_account.html:32
1726 msgid "Change your avatar at"
1727 msgstr "修改您的頭像於"
1728
1729 #: rhodecode/templates/admin/users/user_edit.html:34
1730 #: rhodecode/templates/admin/users/user_edit_my_account.html:33
1731 msgid "Using"
1732 msgstr "使用中"
1733
1734 #: rhodecode/templates/admin/users/user_edit.html:40
1735 #: rhodecode/templates/admin/users/user_edit_my_account.html:39
1736 msgid "API key"
1737 msgstr ""
1738
1739 #: rhodecode/templates/admin/users/user_edit.html:56
1740 msgid "LDAP DN"
1741 msgstr ""
1742
1743 #: rhodecode/templates/admin/users/user_edit.html:65
1744 #: rhodecode/templates/admin/users/user_edit_my_account.html:54
1745 msgid "New password"
1746 msgstr "新密碼"
1747
1748 #: rhodecode/templates/admin/users/user_edit.html:135
1749 #: rhodecode/templates/admin/users_groups/users_group_edit.html:256
1750 msgid "Create repositories"
1751 msgstr "建立版本庫"
1752
1753 #: rhodecode/templates/admin/users/user_edit_my_account.html:5
1754 msgid "My account"
1755 msgstr "我的帳號"
1756
1757 #: rhodecode/templates/admin/users/user_edit_my_account.html:9
1758 msgid "My Account"
1759 msgstr "我的帳號"
1760
1761 #: rhodecode/templates/admin/users/user_edit_my_account.html:101
1762 msgid "My repositories"
1763 msgstr "我的版本庫"
1764
1765 #: rhodecode/templates/admin/users/user_edit_my_account.html:107
1766 msgid "ADD REPOSITORY"
1767 msgstr "新增版本庫"
1768
1769 #: rhodecode/templates/admin/users/user_edit_my_account.html:118
1770 #: rhodecode/templates/branches/branches_data.html:7
1771 #: rhodecode/templates/shortlog/shortlog_data.html:8
1772 #: rhodecode/templates/tags/tags_data.html:7
1773 msgid "revision"
1774 msgstr "修訂"
1775
1776 #: rhodecode/templates/admin/users/user_edit_my_account.html:157
1777 msgid "No repositories yet"
1778 msgstr "沒有任何版本庫"
1779
1780 #: rhodecode/templates/admin/users/user_edit_my_account.html:159
1781 msgid "create one now"
1782 msgstr ""
1783
1784 #: rhodecode/templates/admin/users/users.html:5
1785 msgid "Users administration"
1786 msgstr "使用者管理員"
1787
1788 #: rhodecode/templates/admin/users/users.html:23
1789 msgid "ADD NEW USER"
1790 msgstr "新增使用者"
1791
1792 #: rhodecode/templates/admin/users/users.html:33
1793 msgid "username"
1794 msgstr "使用者名稱"
1795
1796 #: rhodecode/templates/admin/users/users.html:34
1797 #: rhodecode/templates/branches/branches_data.html:5
1798 #: rhodecode/templates/tags/tags_data.html:5
1799 msgid "name"
1800 msgstr "名字"
1801
1802 #: rhodecode/templates/admin/users/users.html:35
1803 msgid "lastname"
1804 msgstr "姓"
1805
1806 #: rhodecode/templates/admin/users/users.html:36
1807 msgid "last login"
1808 msgstr "最後登入"
1809
1810 #: rhodecode/templates/admin/users/users.html:37
1811 #: rhodecode/templates/admin/users_groups/users_groups.html:34
1812 msgid "active"
1813 msgstr "啟用"
1814
1815 #: rhodecode/templates/admin/users/users.html:39
1816 #: rhodecode/templates/base/base.html:305
1817 msgid "ldap"
1818 msgstr ""
1819
1820 #: rhodecode/templates/admin/users/users.html:56
1821 msgid "Confirm to delete this user"
1822 msgstr "確認刪除這個使用者"
1823
1824 #: rhodecode/templates/admin/users_groups/users_group_add.html:5
1825 msgid "Add users group"
1826 msgstr "新增使用者群組"
1827
1828 #: rhodecode/templates/admin/users_groups/users_group_add.html:10
1829 #: rhodecode/templates/admin/users_groups/users_groups.html:9
1830 msgid "Users groups"
1831 msgstr "使用者群組"
1832
1833 #: rhodecode/templates/admin/users_groups/users_group_add.html:12
1834 msgid "add new users group"
1835 msgstr "新增使用者群組"
1836
1837 #: rhodecode/templates/admin/users_groups/users_group_edit.html:5
1838 msgid "Edit users group"
1839 msgstr "編輯使用者群組"
1840
1841 #: rhodecode/templates/admin/users_groups/users_group_edit.html:11
1842 msgid "UsersGroups"
1843 msgstr "使用者群組"
1844
1845 #: rhodecode/templates/admin/users_groups/users_group_edit.html:50
1846 msgid "Members"
1847 msgstr "成員"
1848
1849 #: rhodecode/templates/admin/users_groups/users_group_edit.html:58
1850 msgid "Choosen group members"
1851 msgstr "選擇群組成員"
1852
1853 #: rhodecode/templates/admin/users_groups/users_group_edit.html:61
1854 msgid "Remove all elements"
1855 msgstr "移除所有元素"
1856
1857 #: rhodecode/templates/admin/users_groups/users_group_edit.html:75
1858 msgid "Available members"
1859 msgstr "啟用的成員"
1860
1861 #: rhodecode/templates/admin/users_groups/users_group_edit.html:79
1862 msgid "Add all elements"
1863 msgstr "新增索有元素"
1864
1865 #: rhodecode/templates/admin/users_groups/users_groups.html:5
1866 msgid "Users groups administration"
1867 msgstr "使用者群組管理員"
1868
1869 #: rhodecode/templates/admin/users_groups/users_groups.html:23
1870 msgid "ADD NEW USER GROUP"
1871 msgstr "建立新的使用者群組"
1872
1873 #: rhodecode/templates/admin/users_groups/users_groups.html:32
1874 msgid "group name"
1875 msgstr "群組名稱"
1876
1877 #: rhodecode/templates/base/base.html:32
1878 msgid "Forgot password ?"
1879 msgstr "忘記密碼?"
1880
1881 #: rhodecode/templates/base/base.html:57
1882 #: rhodecode/templates/base/base.html:338
1883 #: rhodecode/templates/base/base.html:340
1884 #: rhodecode/templates/base/base.html:342
1885 msgid "Home"
1886 msgstr "首頁"
1887
1888 #: rhodecode/templates/base/base.html:61
1889 #: rhodecode/templates/base/base.html:347
1890 #: rhodecode/templates/base/base.html:349
1891 #: rhodecode/templates/base/base.html:351
1892 #: rhodecode/templates/journal/journal.html:4
1893 #: rhodecode/templates/journal/journal.html:17
1894 #: rhodecode/templates/journal/public_journal.html:4
1895 msgid "Journal"
1896 msgstr "日誌"
1897
1898 #: rhodecode/templates/base/base.html:66
1899 msgid "Login"
1900 msgstr "登入"
1901
1902 #: rhodecode/templates/base/base.html:68
1903 msgid "Log Out"
1904 msgstr "登出"
1905
1906 #: rhodecode/templates/base/base.html:107
1907 msgid "Submit a bug"
1908 msgstr "回報錯誤"
1909
1910 #: rhodecode/templates/base/base.html:141
1911 msgid "Switch repository"
1912 msgstr "切換版本庫"
1913
1914 #: rhodecode/templates/base/base.html:143
1915 msgid "Products"
1916 msgstr ""
1917
1918 #: rhodecode/templates/base/base.html:149
1919 msgid "loading..."
1920 msgstr "載入中..."
1921
1922 #: rhodecode/templates/base/base.html:234
1923 #: rhodecode/templates/base/base.html:236
1924 #: rhodecode/templates/base/base.html:238
1925 msgid "Switch to"
1926 msgstr "切換至"
1927
1928 #: rhodecode/templates/base/base.html:242
1929 #: rhodecode/templates/branches/branches.html:13
1930 msgid "branches"
1931 msgstr "分支"
1932
1933 #: rhodecode/templates/base/base.html:249
1934 #: rhodecode/templates/branches/branches_data.html:52
1935 msgid "There are no branches yet"
1936 msgstr "沒有任何分支"
1937
1938 #: rhodecode/templates/base/base.html:254
1939 #: rhodecode/templates/shortlog/shortlog_data.html:10
1940 #: rhodecode/templates/tags/tags.html:14
1941 msgid "tags"
1942 msgstr "標籤"
1943
1944 #: rhodecode/templates/base/base.html:261
1945 #: rhodecode/templates/tags/tags_data.html:32
1946 msgid "There are no tags yet"
1947 msgstr "沒有任何標籤"
1948
1949 #: rhodecode/templates/base/base.html:277
1950 #: rhodecode/templates/base/base.html:281
1951 #: rhodecode/templates/files/files_annotate.html:40
1952 #: rhodecode/templates/files/files_source.html:11
1953 msgid "Options"
1954 msgstr "選項"
1955
1956 #: rhodecode/templates/base/base.html:286
1957 #: rhodecode/templates/base/base.html:288
1958 #: rhodecode/templates/base/base.html:306
1959 msgid "settings"
1960 msgstr "設定"
1961
1962 #: rhodecode/templates/base/base.html:292
1963 msgid "search"
1964 msgstr "搜尋"
1965
1966 #: rhodecode/templates/base/base.html:299
1967 msgid "journal"
1968 msgstr "日誌"
1969
1970 #: rhodecode/templates/base/base.html:301
1971 msgid "repositories groups"
1972 msgstr "版本庫群組"
1973
1974 #: rhodecode/templates/base/base.html:302
1975 msgid "users"
1976 msgstr "使用者"
1977
1978 #: rhodecode/templates/base/base.html:303
1979 msgid "users groups"
1980 msgstr "使用者群組"
1981
1982 #: rhodecode/templates/base/base.html:304
1983 msgid "permissions"
1984 msgstr "權限"
1985
1986 #: rhodecode/templates/base/base.html:317
1987 #: rhodecode/templates/base/base.html:319
1988 #: rhodecode/templates/followers/followers.html:5
1989 msgid "Followers"
1990 msgstr "追蹤者"
1991
1992 #: rhodecode/templates/base/base.html:325
1993 #: rhodecode/templates/base/base.html:327
1994 #: rhodecode/templates/forks/forks.html:5
1995 msgid "Forks"
1996 msgstr ""
1997
1998 #: rhodecode/templates/base/base.html:356
1999 #: rhodecode/templates/base/base.html:358
2000 #: rhodecode/templates/base/base.html:360
2001 #: rhodecode/templates/search/search.html:4
2002 #: rhodecode/templates/search/search.html:24
2003 #: rhodecode/templates/search/search.html:46
2004 msgid "Search"
2005 msgstr "搜尋"
2006
2007 #: rhodecode/templates/base/root.html:57
2008 #: rhodecode/templates/journal/journal.html:48
2009 #: rhodecode/templates/summary/summary.html:36
2010 msgid "Stop following this repository"
2011 msgstr "停止追蹤這個版本庫"
2012
2013 #: rhodecode/templates/base/root.html:66
2014 #: rhodecode/templates/summary/summary.html:40
2015 msgid "Start following this repository"
2016 msgstr "開始追蹤這個版本庫"
2017
2018 #: rhodecode/templates/branches/branches_data.html:4
2019 #: rhodecode/templates/tags/tags_data.html:4
2020 msgid "date"
2021 msgstr "日期"
2022
2023 #: rhodecode/templates/branches/branches_data.html:6
2024 #: rhodecode/templates/shortlog/shortlog_data.html:7
2025 #: rhodecode/templates/tags/tags_data.html:6
2026 msgid "author"
2027 msgstr "作者"
2028
2029 #: rhodecode/templates/branches/branches_data.html:8
2030 #: rhodecode/templates/shortlog/shortlog_data.html:11
2031 #: rhodecode/templates/tags/tags_data.html:8
2032 msgid "links"
2033 msgstr "連結"
2034
2035 #: rhodecode/templates/branches/branches_data.html:23
2036 #: rhodecode/templates/branches/branches_data.html:43
2037 #: rhodecode/templates/shortlog/shortlog_data.html:39
2038 #: rhodecode/templates/tags/tags_data.html:24
2039 msgid "changeset"
2040 msgstr "修改"
2041
2042 #: rhodecode/templates/branches/branches_data.html:25
2043 #: rhodecode/templates/branches/branches_data.html:45
2044 #: rhodecode/templates/files/files.html:12
2045 #: rhodecode/templates/shortlog/shortlog_data.html:41
2046 #: rhodecode/templates/summary/summary.html:233
2047 #: rhodecode/templates/tags/tags_data.html:26
2048 msgid "files"
2049 msgstr "檔案"
2050
2051 #: rhodecode/templates/changelog/changelog.html:14
2052 msgid "showing "
2053 msgstr ""
2054
2055 #: rhodecode/templates/changelog/changelog.html:14
2056 msgid "out of"
2057 msgstr ""
2058
2059 #: rhodecode/templates/changelog/changelog.html:37
2060 msgid "Show"
2061 msgstr "顯示"
2062
2063 #: rhodecode/templates/changelog/changelog.html:50
2064 #: rhodecode/templates/changeset/changeset.html:42
2065 #: rhodecode/templates/summary/summary.html:609
2066 msgid "commit"
2067 msgstr "遞交"
2068
2069 #: rhodecode/templates/changelog/changelog.html:63
2070 msgid "Affected number of files, click to show more details"
2071 msgstr ""
2072
2073 #: rhodecode/templates/changelog/changelog.html:67
2074 #: rhodecode/templates/changeset/changeset.html:66
2075 msgid "merge"
2076 msgstr "合併"
2077
2078 #: rhodecode/templates/changelog/changelog.html:72
2079 #: rhodecode/templates/changeset/changeset.html:72
2080 msgid "Parent"
2081 msgstr ""
2082
2083 #: rhodecode/templates/changelog/changelog.html:77
2084 #: rhodecode/templates/changeset/changeset.html:77
2085 msgid "No parents"
2086 msgstr ""
2087
2088 #: rhodecode/templates/changelog/changelog.html:82
2089 #: rhodecode/templates/changeset/changeset.html:80
2090 #: rhodecode/templates/files/files.html:29
2091 #: rhodecode/templates/files/files_annotate.html:25
2092 #: rhodecode/templates/files/files_edit.html:33
2093 #: rhodecode/templates/shortlog/shortlog_data.html:9
2094 msgid "branch"
2095 msgstr "分支"
2096
2097 #: rhodecode/templates/changelog/changelog.html:86
2098 #: rhodecode/templates/changeset/changeset.html:83
2099 msgid "tag"
2100 msgstr "標籤"
2101
2102 #: rhodecode/templates/changelog/changelog.html:122
2103 msgid "Show selected changes __S -> __E"
2104 msgstr ""
2105
2106 #: rhodecode/templates/changelog/changelog.html:172
2107 #: rhodecode/templates/shortlog/shortlog_data.html:61
2108 msgid "There are no changes yet"
2109 msgstr "尚未有任何變更"
2110
2111 #: rhodecode/templates/changelog/changelog_details.html:2
2112 #: rhodecode/templates/changeset/changeset.html:55
2113 msgid "removed"
2114 msgstr "移除"
2115
2116 #: rhodecode/templates/changelog/changelog_details.html:3
2117 #: rhodecode/templates/changeset/changeset.html:56
2118 msgid "changed"
2119 msgstr "修改"
2120
2121 #: rhodecode/templates/changelog/changelog_details.html:4
2122 #: rhodecode/templates/changeset/changeset.html:57
2123 msgid "added"
2124 msgstr "新增"
2125
2126 #: rhodecode/templates/changelog/changelog_details.html:6
2127 #: rhodecode/templates/changelog/changelog_details.html:7
2128 #: rhodecode/templates/changelog/changelog_details.html:8
2129 #: rhodecode/templates/changeset/changeset.html:59
2130 #: rhodecode/templates/changeset/changeset.html:60
2131 #: rhodecode/templates/changeset/changeset.html:61
2132 #, python-format
2133 msgid "affected %s files"
2134 msgstr ""
2135
2136 #: rhodecode/templates/changeset/changeset.html:6
2137 #: rhodecode/templates/changeset/changeset.html:14
2138 #: rhodecode/templates/changeset/changeset.html:31
2139 msgid "Changeset"
2140 msgstr ""
2141
2142 #: rhodecode/templates/changeset/changeset.html:32
2143 #: rhodecode/templates/changeset/changeset.html:121
2144 #: rhodecode/templates/changeset/changeset_range.html:78
2145 #: rhodecode/templates/files/file_diff.html:32
2146 #: rhodecode/templates/files/file_diff.html:42
2147 msgid "raw diff"
2148 msgstr "原始差異"
2149
2150 #: rhodecode/templates/changeset/changeset.html:34
2151 #: rhodecode/templates/changeset/changeset.html:123
2152 #: rhodecode/templates/changeset/changeset_range.html:80
2153 #: rhodecode/templates/files/file_diff.html:34
2154 msgid "download diff"
2155 msgstr "下載差異"
2156
2157 #: rhodecode/templates/changeset/changeset.html:90
2158 #, python-format
2159 msgid "%s files affected with %s additions and %s deletions."
2160 msgstr ""
2161
2162 #: rhodecode/templates/changeset/changeset.html:101
2163 msgid "Changeset was too big and was cut off..."
2164 msgstr ""
2165
2166 #: rhodecode/templates/changeset/changeset.html:119
2167 #: rhodecode/templates/changeset/changeset_range.html:76
2168 #: rhodecode/templates/files/file_diff.html:30
2169 msgid "diff"
2170 msgstr "差異"
2171
2172 #: rhodecode/templates/changeset/changeset.html:132
2173 #: rhodecode/templates/changeset/changeset_range.html:89
2174 msgid "No changes in this file"
2175 msgstr "這個檔案沒有任何變更"
2176
2177 #: rhodecode/templates/changeset/changeset_range.html:30
2178 msgid "Compare View"
2179 msgstr "比較顯示"
2180
2181 #: rhodecode/templates/changeset/changeset_range.html:52
2182 msgid "Files affected"
2183 msgstr ""
2184
2185 #: rhodecode/templates/errors/error_document.html:44
2186 #, python-format
2187 msgid "You will be redirected to %s in %s seconds"
2188 msgstr ""
2189
2190 #: rhodecode/templates/files/file_diff.html:4
2191 #: rhodecode/templates/files/file_diff.html:12
2192 msgid "File diff"
2193 msgstr "檔案差異"
2194
2195 #: rhodecode/templates/files/file_diff.html:42
2196 msgid "Diff is to big to display"
2197 msgstr ""
2198
2199 #: rhodecode/templates/files/files.html:37
2200 #: rhodecode/templates/files/files_annotate.html:31
2201 #: rhodecode/templates/files/files_edit.html:39
2202 msgid "Location"
2203 msgstr "位置"
2204
2205 #: rhodecode/templates/files/files.html:46
2206 msgid "Go back"
2207 msgstr ""
2208
2209 #: rhodecode/templates/files/files.html:47
2210 msgid "No files at given path"
2211 msgstr ""
2212
2213 #: rhodecode/templates/files/files_annotate.html:4
2214 msgid "File annotate"
2215 msgstr "檔案註釋"
2216
2217 #: rhodecode/templates/files/files_annotate.html:12
2218 msgid "annotate"
2219 msgstr "註釋"
2220
2221 #: rhodecode/templates/files/files_annotate.html:33
2222 #: rhodecode/templates/files/files_browser.html:160
2223 #: rhodecode/templates/files/files_source.html:2
2224 msgid "Revision"
2225 msgstr "修訂"
2226
2227 #: rhodecode/templates/files/files_annotate.html:36
2228 #: rhodecode/templates/files/files_browser.html:158
2229 #: rhodecode/templates/files/files_source.html:7
2230 msgid "Size"
2231 msgstr "大小"
2232
2233 #: rhodecode/templates/files/files_annotate.html:38
2234 #: rhodecode/templates/files/files_browser.html:159
2235 #: rhodecode/templates/files/files_source.html:9
2236 msgid "Mimetype"
2237 msgstr ""
2238
2239 #: rhodecode/templates/files/files_annotate.html:41
2240 msgid "show source"
2241 msgstr "顯示原始碼"
2242
2243 #: rhodecode/templates/files/files_annotate.html:43
2244 #: rhodecode/templates/files/files_annotate.html:78
2245 #: rhodecode/templates/files/files_source.html:14
2246 #: rhodecode/templates/files/files_source.html:51
2247 msgid "show as raw"
2248 msgstr "顯示原始文件"
2249
2250 #: rhodecode/templates/files/files_annotate.html:45
2251 #: rhodecode/templates/files/files_source.html:16
2252 msgid "download as raw"
2253 msgstr "下載原始文件"
2254
2255 #: rhodecode/templates/files/files_annotate.html:54
2256 #: rhodecode/templates/files/files_source.html:25
2257 msgid "History"
2258 msgstr "歷史"
2259
2260 #: rhodecode/templates/files/files_annotate.html:73
2261 #: rhodecode/templates/files/files_source.html:46
2262 #, python-format
2263 msgid "Binary file (%s)"
2264 msgstr "二進位檔 (%s)"
2265
2266 #: rhodecode/templates/files/files_annotate.html:78
2267 #: rhodecode/templates/files/files_source.html:51
2268 msgid "File is too big to display"
2269 msgstr "顯示的檔案太大"
2270
2271 #: rhodecode/templates/files/files_browser.html:13
2272 msgid "view"
2273 msgstr "顯示"
2274
2275 #: rhodecode/templates/files/files_browser.html:14
2276 msgid "previous revision"
2277 msgstr "前一個修訂"
2278
2279 #: rhodecode/templates/files/files_browser.html:16
2280 msgid "next revision"
2281 msgstr "下一個修訂"
2282
2283 #: rhodecode/templates/files/files_browser.html:23
2284 msgid "follow current branch"
2285 msgstr ""
2286
2287 #: rhodecode/templates/files/files_browser.html:27
2288 msgid "search file list"
2289 msgstr "搜尋檔案列表"
2290
2291 #: rhodecode/templates/files/files_browser.html:32
2292 msgid "Loading file list..."
2293 msgstr "載入檔案列表..."
2294
2295 #: rhodecode/templates/files/files_browser.html:111
2296 msgid "search truncated"
2297 msgstr ""
2298
2299 #: rhodecode/templates/files/files_browser.html:122
2300 msgid "no matching files"
2301 msgstr "無符合的檔案"
2302
2303 #: rhodecode/templates/files/files_browser.html:161
2304 msgid "Last modified"
2305 msgstr "最後修改"
2306
2307 #: rhodecode/templates/files/files_browser.html:162
2308 msgid "Last commiter"
2309 msgstr "最後的遞交者"
2310
2311 #: rhodecode/templates/files/files_edit.html:4
2312 msgid "Edit file"
2313 msgstr "編輯檔案"
2314
2315 #: rhodecode/templates/files/files_edit.html:19
2316 msgid "edit file"
2317 msgstr "編輯檔案"
2318
2319 #: rhodecode/templates/files/files_edit.html:45
2320 #: rhodecode/templates/shortlog/shortlog_data.html:5
2321 msgid "commit message"
2322 msgstr "遞交資訊"
2323
2324 #: rhodecode/templates/files/files_edit.html:51
2325 msgid "Commit changes"
2326 msgstr "遞交修改"
2327
2328 #: rhodecode/templates/files/files_source.html:12
2329 msgid "show annotation"
2330 msgstr "險是註釋"
2331
2332 #: rhodecode/templates/files/files_source.html:153
2333 msgid "Selection link"
2334 msgstr ""
2335
2336 #: rhodecode/templates/followers/followers.html:13
2337 msgid "followers"
2338 msgstr "追蹤者"
2339
2340 #: rhodecode/templates/followers/followers_data.html:12
2341 msgid "Started following"
2342 msgstr "開始追蹤"
2343
2344 #: rhodecode/templates/forks/forks.html:13
2345 msgid "forks"
2346 msgstr "分支"
2347
2348 #: rhodecode/templates/forks/forks_data.html:17
2349 msgid "forked"
2350 msgstr "已建立分支"
2351
2352 #: rhodecode/templates/forks/forks_data.html:34
2353 msgid "There are no forks yet"
2354 msgstr "尚未有任何 fork"
2355
2356 #: rhodecode/templates/journal/journal.html:34
2357 msgid "Following"
2358 msgstr "已追蹤"
2359
2360 #: rhodecode/templates/journal/journal.html:41
2361 msgid "following user"
2362 msgstr "追蹤使用者"
2363
2364 #: rhodecode/templates/journal/journal.html:41
2365 msgid "user"
2366 msgstr "使用者"
2367
2368 #: rhodecode/templates/journal/journal.html:65
2369 msgid "You are not following any users or repositories"
2370 msgstr "您尚未追蹤任何使用者或版本庫"
2371
2372 #: rhodecode/templates/journal/journal_data.html:46
2373 msgid "No entries yet"
2374 msgstr ""
2375
2376 #: rhodecode/templates/journal/public_journal.html:17
2377 msgid "Public Journal"
2378 msgstr "開放日誌"
2379
2380 #: rhodecode/templates/search/search.html:7
2381 #: rhodecode/templates/search/search.html:26
2382 msgid "in repository: "
2383 msgstr "於版本庫:"
2384
2385 #: rhodecode/templates/search/search.html:9
2386 #: rhodecode/templates/search/search.html:28
2387 msgid "in all repositories"
2388 msgstr "於所有的版本庫"
2389
2390 #: rhodecode/templates/search/search.html:42
2391 msgid "Search term"
2392 msgstr "搜尋關鍵字"
2393
2394 #: rhodecode/templates/search/search.html:54
2395 msgid "Search in"
2396 msgstr "搜尋範圍"
2397
2398 #: rhodecode/templates/search/search.html:57
2399 msgid "File contents"
2400 msgstr "文件內容"
2401
2402 #: rhodecode/templates/search/search.html:59
2403 msgid "File names"
2404 msgstr "檔案名稱"
2405
2406 #: rhodecode/templates/search/search_content.html:20
2407 #: rhodecode/templates/search/search_path.html:15
2408 msgid "Permission denied"
2409 msgstr "權限不足"
2410
2411 #: rhodecode/templates/settings/repo_fork.html:5
2412 msgid "Fork"
2413 msgstr "分支"
2414
2415 #: rhodecode/templates/settings/repo_fork.html:31
2416 msgid "Fork name"
2417 msgstr "分支名稱"
2418
2419 #: rhodecode/templates/settings/repo_fork.html:55
2420 msgid "fork this repository"
2421 msgstr "fork 這個版本庫"
2422
2423 #: rhodecode/templates/shortlog/shortlog.html:5
2424 #: rhodecode/templates/summary/summary.html:666
2425 msgid "Shortlog"
2426 msgstr "簡短紀錄"
2427
2428 #: rhodecode/templates/shortlog/shortlog.html:14
2429 msgid "shortlog"
2430 msgstr "簡短紀錄"
2431
2432 #: rhodecode/templates/shortlog/shortlog_data.html:6
2433 msgid "age"
2434 msgstr ""
2435
2436 #: rhodecode/templates/summary/summary.html:12
2437 msgid "summary"
2438 msgstr "概況"
2439
2440 #: rhodecode/templates/summary/summary.html:79
2441 msgid "remote clone"
2442 msgstr "遠端複製"
2443
2444 #: rhodecode/templates/summary/summary.html:121
2445 msgid "by"
2446 msgstr ""
2447
2448 #: rhodecode/templates/summary/summary.html:128
2449 msgid "Clone url"
2450 msgstr "複製連結"
2451
2452 #: rhodecode/templates/summary/summary.html:137
2453 msgid "Trending source files"
2454 msgstr ""
2455
2456 #: rhodecode/templates/summary/summary.html:146
2457 msgid "Download"
2458 msgstr "下載"
2459
2460 #: rhodecode/templates/summary/summary.html:150
2461 msgid "There are no downloads yet"
2462 msgstr "沒有任何下載"
2463
2464 #: rhodecode/templates/summary/summary.html:152
2465 msgid "Downloads are disabled for this repository"
2466 msgstr "這個版本庫的下載已停用"
2467
2468 #: rhodecode/templates/summary/summary.html:154
2469 #: rhodecode/templates/summary/summary.html:320
2470 msgid "enable"
2471 msgstr "啟用"
2472
2473 #: rhodecode/templates/summary/summary.html:162
2474 #: rhodecode/templates/summary/summary.html:297
2475 #, python-format
2476 msgid "Download %s as %s"
2477 msgstr "下載 %s 為 %s"
2478
2479 #: rhodecode/templates/summary/summary.html:168
2480 msgid "Check this to download archive with subrepos"
2481 msgstr ""
2482
2483 #: rhodecode/templates/summary/summary.html:168
2484 msgid "with subrepos"
2485 msgstr ""
2486
2487 #: rhodecode/templates/summary/summary.html:176
2488 msgid "Feeds"
2489 msgstr ""
2490
2491 #: rhodecode/templates/summary/summary.html:257
2492 #: rhodecode/templates/summary/summary.html:684
2493 #: rhodecode/templates/summary/summary.html:695
2494 msgid "show more"
2495 msgstr "顯示更多"
2496
2497 #: rhodecode/templates/summary/summary.html:312
2498 msgid "Commit activity by day / author"
2499 msgstr ""
2500
2501 #: rhodecode/templates/summary/summary.html:324
2502 msgid "Loaded in"
2503 msgstr ""
2504
2505 #: rhodecode/templates/summary/summary.html:603
2506 msgid "commits"
2507 msgstr "遞交"
2508
2509 #: rhodecode/templates/summary/summary.html:604
2510 msgid "files added"
2511 msgstr "多個檔案新增"
2512
2513 #: rhodecode/templates/summary/summary.html:605
2514 msgid "files changed"
2515 msgstr "多個檔案修改"
2516
2517 #: rhodecode/templates/summary/summary.html:606
2518 msgid "files removed"
2519 msgstr "移除多個檔案"
2520
2521 #: rhodecode/templates/summary/summary.html:610
2522 msgid "file added"
2523 msgstr "檔案新增"
2524
2525 #: rhodecode/templates/summary/summary.html:611
2526 msgid "file changed"
2527 msgstr "檔案修改"
2528
2529 #: rhodecode/templates/summary/summary.html:612
2530 msgid "file removed"
2531 msgstr "移除檔案"
2532
@@ -1,20 +1,21 b''
1 List of contributors to RhodeCode project:
1 List of contributors to RhodeCode project:
2 Marcin Kuźmiński <marcin@python-works.com>
2 Marcin Kuźmiński <marcin@python-works.com>
3 Lukasz Balcerzak <lukaszbalcerzak@gmail.com>
3 Lukasz Balcerzak <lukaszbalcerzak@gmail.com>
4 Jason Harris <jason@jasonfharris.com>
4 Jason Harris <jason@jasonfharris.com>
5 Thayne Harbaugh <thayne@fusionio.com>
5 Thayne Harbaugh <thayne@fusionio.com>
6 cejones
6 cejones <>
7 Thomas Waldmann <tw-public@gmx.de>
7 Thomas Waldmann <tw-public@gmx.de>
8 Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
8 Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
9 Dmitri Kuznetsov
9 Dmitri Kuznetsov <>
10 Jared Bunting <jared.bunting@peachjean.com>
10 Jared Bunting <jared.bunting@peachjean.com>
11 Steve Romanow <slestak989@gmail.com>
11 Steve Romanow <slestak989@gmail.com>
12 Augosto Hermann <augusto.herrmann@planejamento.gov.br>
12 Augosto Hermann <augusto.herrmann@planejamento.gov.br>
13 Ankit Solanki <ankit.solanki@gmail.com>
13 Ankit Solanki <ankit.solanki@gmail.com>
14 Liad Shani <liadff@gmail.com>
14 Liad Shani <liadff@gmail.com>
15 Les Peabody <lpeabody@gmail.com>
15 Les Peabody <lpeabody@gmail.com>
16 Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
16 Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
17 Matt Zuba <matt.zuba@goodwillaz.org>
17 Matt Zuba <matt.zuba@goodwillaz.org>
18 Aras Pranckevicius <aras@unity3d.com>
18 Aras Pranckevicius <aras@unity3d.com>
19 Tony Bussieres <t.bussieres@gmail.com>
19 Tony Bussieres <t.bussieres@gmail.com>
20 Erwin Kroon <e.kroon@smartmetersolutions.nl> No newline at end of file
20 Erwin Kroon <e.kroon@smartmetersolutions.nl>
21 nansenat16 <nansenat16@null.tw> No newline at end of file
@@ -1,174 +1,174 b''
1 =========
1 =========
2 RhodeCode
2 RhodeCode
3 =========
3 =========
4
4
5 About
5 About
6 -----
6 -----
7
7
8 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
8 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
9 with a built in push/pull server and full text search and code-review.
9 with a built in push/pull server and full text search and code-review.
10 It works on http/https and has a built in permission/authentication system with
10 It works on http/https and has a built in permission/authentication system with
11 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also provides
11 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also provides
12 simple API so it's easy integrable with existing external systems.
12 simple API so it's easy integrable with existing external systems.
13
13
14 RhodeCode is similar in some respects to github_ or bitbucket_,
14 RhodeCode is similar in some respects to github_ or bitbucket_,
15 however RhodeCode can be run as standalone hosted application on your own server.
15 however RhodeCode can be run as standalone hosted application on your own server.
16 It is open source and donation ware and focuses more on providing a customized,
16 It is open source and donation ware and focuses more on providing a customized,
17 self administered interface for Mercurial_ and GIT_ repositories.
17 self administered interface for Mercurial_ and GIT_ repositories.
18 RhodeCode works on *nix systems and Windows it is powered by a vcs_ library
18 RhodeCode works on \*nix systems and Windows it is powered by a vcs_ library
19 that Lukasz Balcerzak and Marcin Kuzminski created to handle multiple
19 that Lukasz Balcerzak and Marcin Kuzminski created to handle multiple
20 different version control systems.
20 different version control systems.
21
21
22 RhodeCode uses `PEP386 versioning <http://www.python.org/dev/peps/pep-0386/>`_
22 RhodeCode uses `PEP386 versioning <http://www.python.org/dev/peps/pep-0386/>`_
23
23
24 Installation
24 Installation
25 ------------
25 ------------
26 Stable releases of RhodeCode are best installed via::
26 Stable releases of RhodeCode are best installed via::
27
27
28 easy_install rhodecode
28 easy_install rhodecode
29
29
30 Or::
30 Or::
31
31
32 pip install rhodecode
32 pip install rhodecode
33
33
34 Detailed instructions and links may be found on the Installation page.
34 Detailed instructions and links may be found on the Installation page.
35
35
36 Please visit http://packages.python.org/RhodeCode/installation.html for
36 Please visit http://packages.python.org/RhodeCode/installation.html for
37 more details
37 more details
38
38
39 RhodeCode demo
39 RhodeCode demo
40 --------------
40 --------------
41
41
42 http://demo.rhodecode.org
42 http://demo.rhodecode.org
43
43
44 The default access is anonymous but you can login to an administrative account
44 The default access is anonymous but you can login to an administrative account
45 using the following credentials:
45 using the following credentials:
46
46
47 - username: demo
47 - username: demo
48 - password: demo12
48 - password: demo12
49
49
50 Source code
50 Source code
51 -----------
51 -----------
52
52
53 The latest sources can be obtained from official RhodeCode instance
53 The latest sources can be obtained from official RhodeCode instance
54 https://secure.rhodecode.org
54 https://secure.rhodecode.org
55
55
56
56
57 MIRRORS:
57 MIRRORS:
58
58
59 Issue tracker and sources at bitbucket_
59 Issue tracker and sources at bitbucket_
60
60
61 http://bitbucket.org/marcinkuzminski/rhodecode
61 http://bitbucket.org/marcinkuzminski/rhodecode
62
62
63 Sources at github_
63 Sources at github_
64
64
65 https://github.com/marcinkuzminski/rhodecode
65 https://github.com/marcinkuzminski/rhodecode
66
66
67
67
68 RhodeCode Features
68 RhodeCode Features
69 ------------------
69 ------------------
70
70
71 - Has its own middleware to handle mercurial_ protocol requests.
71 - Has its own middleware to handle mercurial_ protocol requests.
72 Each request can be logged and authenticated.
72 Each request can be logged and authenticated.
73 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
73 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
74 Supports http/https and LDAP
74 Supports http/https and LDAP
75 - Full permissions (private/read/write/admin) and authentication per project.
75 - Full permissions (private/read/write/admin) and authentication per project.
76 One account for web interface and mercurial_ push/pull/clone operations.
76 One account for web interface and mercurial_ push/pull/clone operations.
77 - Have built in users groups for easier permission management
77 - Have built in users groups for easier permission management
78 - Repository groups let you group repos and manage them easier.
78 - Repository groups let you group repos and manage them easier.
79 - Users can fork other users repo. RhodeCode have also compare view to see
79 - Users can fork other users repo. RhodeCode have also compare view to see
80 combined changeset for all changeset made within single push.
80 combined changeset for all changeset made within single push.
81 - Build in commit-api let's you add, edit and commit files right from RhodeCode
81 - Build in commit-api let's you add, edit and commit files right from RhodeCode
82 interface using simple editor or upload form for binaries.
82 interface using simple editor or upload form for binaries.
83 - Mako templates let's you customize the look and feel of the application.
83 - Mako templates let's you customize the look and feel of the application.
84 - Beautiful diffs, annotations and source code browsing all colored by pygments.
84 - Beautiful diffs, annotations and source code browsing all colored by pygments.
85 Raw diffs are made in git-diff format, including git_ binary-patches
85 Raw diffs are made in git-diff format, including git_ binary-patches
86 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
86 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
87 - Admin interface with user/permission management. Admin activity journal, logs
87 - Admin interface with user/permission management. Admin activity journal, logs
88 pulls, pushes, forks, registrations and other actions made by all users.
88 pulls, pushes, forks, registrations and other actions made by all users.
89 - Server side forks. It is possible to fork a project and modify it freely
89 - Server side forks. It is possible to fork a project and modify it freely
90 without breaking the main repository. You can even write Your own hooks
90 without breaking the main repository. You can even write Your own hooks
91 and install them
91 and install them
92 - code review with notification system, inline commenting, all parsed using
92 - code review with notification system, inline commenting, all parsed using
93 rst syntax
93 rst syntax
94 - rst and markdown README support for repositories
94 - rst and markdown README support for repositories
95 - Full text search powered by Whoosh on the source files, and file names.
95 - Full text search powered by Whoosh on the source files, and file names.
96 Build in indexing daemons, with optional incremental index build
96 Build in indexing daemons, with optional incremental index build
97 (no external search servers required all in one application)
97 (no external search servers required all in one application)
98 - Setup project descriptions and info inside built in db for easy, non
98 - Setup project descriptions and info inside built in db for easy, non
99 file-system operations
99 file-system operations
100 - Intelligent cache with invalidation after push or project change, provides
100 - Intelligent cache with invalidation after push or project change, provides
101 high performance and always up to date data.
101 high performance and always up to date data.
102 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
102 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
103 - Optional async tasks for speed and performance using celery_
103 - Optional async tasks for speed and performance using celery_
104 - Backup scripts can do backup of whole app and send it over scp to desired
104 - Backup scripts can do backup of whole app and send it over scp to desired
105 location
105 location
106 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
106 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
107
107
108
108
109 Incoming / Plans
109 Incoming / Plans
110 ----------------
110 ----------------
111
111
112 - Finer granular permissions per branch, repo group or subrepo
112 - Finer granular permissions per branch, repo group or subrepo
113 - pull requests and web based merges
113 - pull requests and web based merges
114 - per line file history
114 - per line file history
115 - SSH based authentication with server side key management
115 - SSH based authentication with server side key management
116 - Commit based built in wiki system
116 - Commit based built in wiki system
117 - More statistics and graph (global annotation + some more statistics)
117 - More statistics and graph (global annotation + some more statistics)
118 - Other advancements as development continues (or you can of course make
118 - Other advancements as development continues (or you can of course make
119 additions and or requests)
119 additions and or requests)
120
120
121 License
121 License
122 -------
122 -------
123
123
124 ``RhodeCode`` is released under the GPLv3 license.
124 ``RhodeCode`` is released under the GPLv3 license.
125
125
126
126
127 Getting help
127 Getting help
128 ------------
128 ------------
129
129
130 Listed bellow are various support resources that should help.
130 Listed bellow are various support resources that should help.
131
131
132 .. note::
132 .. note::
133
133
134 Please try to read the documentation before posting any issues
134 Please try to read the documentation before posting any issues
135
135
136 - Join the `Google group <http://groups.google.com/group/rhodecode>`_ and ask
136 - Join the `Google group <http://groups.google.com/group/rhodecode>`_ and ask
137 any questions.
137 any questions.
138
138
139 - Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
139 - Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
140
140
141
141
142 - Join #rhodecode on FreeNode (irc.freenode.net)
142 - Join #rhodecode on FreeNode (irc.freenode.net)
143 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
143 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
144
144
145 - You can also follow me on twitter **@marcinkuzminski** where i often post some
145 - You can also follow me on twitter **@marcinkuzminski** where i often post some
146 news about RhodeCode
146 news about RhodeCode
147
147
148
148
149 Online documentation
149 Online documentation
150 --------------------
150 --------------------
151
151
152 Online documentation for the current version of RhodeCode is available at
152 Online documentation for the current version of RhodeCode is available at
153 - http://packages.python.org/RhodeCode/
153 - http://packages.python.org/RhodeCode/
154 - http://rhodecode.readthedocs.org/en/latest/index.html
154 - http://rhodecode.readthedocs.org/en/latest/index.html
155
155
156 You may also build the documentation for yourself - go into ``docs/`` and run::
156 You may also build the documentation for yourself - go into ``docs/`` and run::
157
157
158 make html
158 make html
159
159
160 (You need to have sphinx_ installed to build the documentation. If you don't
160 (You need to have sphinx_ installed to build the documentation. If you don't
161 have sphinx_ installed you can install it via the command:
161 have sphinx_ installed you can install it via the command:
162 ``easy_install sphinx``)
162 ``easy_install sphinx``)
163
163
164 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
164 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
165 .. _python: http://www.python.org/
165 .. _python: http://www.python.org/
166 .. _sphinx: http://sphinx.pocoo.org/
166 .. _sphinx: http://sphinx.pocoo.org/
167 .. _mercurial: http://mercurial.selenic.com/
167 .. _mercurial: http://mercurial.selenic.com/
168 .. _bitbucket: http://bitbucket.org/
168 .. _bitbucket: http://bitbucket.org/
169 .. _github: http://github.com/
169 .. _github: http://github.com/
170 .. _subversion: http://subversion.tigris.org/
170 .. _subversion: http://subversion.tigris.org/
171 .. _git: http://git-scm.com/
171 .. _git: http://git-scm.com/
172 .. _celery: http://celeryproject.org/
172 .. _celery: http://celeryproject.org/
173 .. _Sphinx: http://sphinx.pocoo.org/
173 .. _Sphinx: http://sphinx.pocoo.org/
174 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
174 .. _vcs: http://pypi.python.org/pypi/vcs
@@ -1,633 +1,661 b''
1 .. _changelog:
1 .. _changelog:
2
2
3 =========
3 =========
4 Changelog
4 Changelog
5 =========
5 =========
6
6
7 1.3.5 (**2012-XX-XX**)
7 1.4.0 (**2012-XX-XX**)
8 ----------------------
8 ----------------------
9
9
10 :status: in-progress
10 :status: in-progress
11 :branch: beta
11 :branch: beta
12
12
13 news
13 news
14 ++++
14 ++++
15
15
16 fixes
17 +++++
18
19 1.3.6 (**2012-05-16**)
20 ----------------------
21
22 news
23 ++++
24
25 - chinese traditional translation
26
27 fixes
28 +++++
29
30 - fixed no scm found warning
31 - fixed __future__ import error on rcextensions
32 - made simplejson required lib for speedup on JSON encoding
33 - fixes #449 bad regex could get more than revisions from parsing history
34
35 1.3.5 (**2012-05-10**)
36 ----------------------
37
38 news
39 ++++
40
16 - use ext_json for json module
41 - use ext_json for json module
17 - unified annotation view with file source view
42 - unified annotation view with file source view
18 - notification improvements, better inbox + css
43 - notification improvements, better inbox + css
19 - #419 don't strip passwords for login forms, make rhodecode
44 - #419 don't strip passwords for login forms, make rhodecode
20 more compatible with LDAP servers
45 more compatible with LDAP servers
21 - Added HTTP_X_FORWARDED_FOR as another method of extracting
46 - Added HTTP_X_FORWARDED_FOR as another method of extracting
22 IP for pull/push logs. - moved all to base controller
47 IP for pull/push logs. - moved all to base controller
23 - #415: Adding comment to changeset causes reload.
48 - #415: Adding comment to changeset causes reload.
24 Comments are now added via ajax and doesn't reload the page
49 Comments are now added via ajax and doesn't reload the page
25 - #374 LDAP config is discarded when LDAP can't be activated
50 - #374 LDAP config is discarded when LDAP can't be activated
26 - limited push/pull operations are now logged for git in the journal
51 - limited push/pull operations are now logged for git in the journal
27 - bumped mercurial to 2.2.X series
52 - bumped mercurial to 2.2.X series
28 - added support for displaying submodules in file-browser
53 - added support for displaying submodules in file-browser
54 - #421 added bookmarks in changelog view
29
55
30 fixes
56 fixes
31 +++++
57 +++++
32
58
33 - fixed dev-version marker for stable when served from source codes
59 - fixed dev-version marker for stable when served from source codes
34 - fixed missing permission checks on show forks page
60 - fixed missing permission checks on show forks page
35 - #418 cast to unicode fixes in notification objects
61 - #418 cast to unicode fixes in notification objects
36 - #426 fixed mention extracting regex
62 - #426 fixed mention extracting regex
37 - fixed remote-pulling for git remotes remopositories
63 - fixed remote-pulling for git remotes remopositories
38 - fixed #434: Error when accessing files or changesets of a git repository
64 - fixed #434: Error when accessing files or changesets of a git repository
39 with submodules
65 with submodules
66 - fixed issue with empty APIKEYS for users after registration ref. #438
67 - fixed issue with getting README files from git repositories
40
68
41 1.3.4 (**2012-03-28**)
69 1.3.4 (**2012-03-28**)
42 ----------------------
70 ----------------------
43
71
44 news
72 news
45 ++++
73 ++++
46
74
47 - Whoosh logging is now controlled by the .ini files logging setup
75 - Whoosh logging is now controlled by the .ini files logging setup
48 - added clone-url into edit form on /settings page
76 - added clone-url into edit form on /settings page
49 - added help text into repo add/edit forms
77 - added help text into repo add/edit forms
50 - created rcextensions module with additional mappings (ref #322) and
78 - created rcextensions module with additional mappings (ref #322) and
51 post push/pull/create repo hooks callbacks
79 post push/pull/create repo hooks callbacks
52 - implemented #377 Users view for his own permissions on account page
80 - implemented #377 Users view for his own permissions on account page
53 - #399 added inheritance of permissions for users group on repos groups
81 - #399 added inheritance of permissions for users group on repos groups
54 - #401 repository group is automatically pre-selected when adding repos
82 - #401 repository group is automatically pre-selected when adding repos
55 inside a repository group
83 inside a repository group
56 - added alternative HTTP 403 response when client failed to authenticate. Helps
84 - added alternative HTTP 403 response when client failed to authenticate. Helps
57 solving issues with Mercurial and LDAP
85 solving issues with Mercurial and LDAP
58 - #402 removed group prefix from repository name when listing repositories
86 - #402 removed group prefix from repository name when listing repositories
59 inside a group
87 inside a group
60 - added gravatars into permission view and permissions autocomplete
88 - added gravatars into permission view and permissions autocomplete
61 - #347 when running multiple RhodeCode instances, properly invalidates cache
89 - #347 when running multiple RhodeCode instances, properly invalidates cache
62 for all registered servers
90 for all registered servers
63
91
64 fixes
92 fixes
65 +++++
93 +++++
66
94
67 - fixed #390 cache invalidation problems on repos inside group
95 - fixed #390 cache invalidation problems on repos inside group
68 - fixed #385 clone by ID url was loosing proxy prefix in URL
96 - fixed #385 clone by ID url was loosing proxy prefix in URL
69 - fixed some unicode problems with waitress
97 - fixed some unicode problems with waitress
70 - fixed issue with escaping < and > in changeset commits
98 - fixed issue with escaping < and > in changeset commits
71 - fixed error occurring during recursive group creation in API
99 - fixed error occurring during recursive group creation in API
72 create_repo function
100 create_repo function
73 - fixed #393 py2.5 fixes for routes url generator
101 - fixed #393 py2.5 fixes for routes url generator
74 - fixed #397 Private repository groups shows up before login
102 - fixed #397 Private repository groups shows up before login
75 - fixed #396 fixed problems with revoking users in nested groups
103 - fixed #396 fixed problems with revoking users in nested groups
76 - fixed mysql unicode issues + specified InnoDB as default engine with
104 - fixed mysql unicode issues + specified InnoDB as default engine with
77 utf8 charset
105 utf8 charset
78 - #406 trim long branch/tag names in changelog to not break UI
106 - #406 trim long branch/tag names in changelog to not break UI
79
107
80 1.3.3 (**2012-03-02**)
108 1.3.3 (**2012-03-02**)
81 ----------------------
109 ----------------------
82
110
83 news
111 news
84 ++++
112 ++++
85
113
86
114
87 fixes
115 fixes
88 +++++
116 +++++
89
117
90 - fixed some python2.5 compatibility issues
118 - fixed some python2.5 compatibility issues
91 - fixed issues with removed repos was accidentally added as groups, after
119 - fixed issues with removed repos was accidentally added as groups, after
92 full rescan of paths
120 full rescan of paths
93 - fixes #376 Cannot edit user (using container auth)
121 - fixes #376 Cannot edit user (using container auth)
94 - fixes #378 Invalid image urls on changeset screen with proxy-prefix
122 - fixes #378 Invalid image urls on changeset screen with proxy-prefix
95 configuration
123 configuration
96 - fixed initial sorting of repos inside repo group
124 - fixed initial sorting of repos inside repo group
97 - fixes issue when user tried to resubmit same permission into user/user_groups
125 - fixes issue when user tried to resubmit same permission into user/user_groups
98 - bumped beaker version that fixes #375 leap error bug
126 - bumped beaker version that fixes #375 leap error bug
99 - fixed raw_changeset for git. It was generated with hg patch headers
127 - fixed raw_changeset for git. It was generated with hg patch headers
100 - fixed vcs issue with last_changeset for filenodes
128 - fixed vcs issue with last_changeset for filenodes
101 - fixed missing commit after hook delete
129 - fixed missing commit after hook delete
102 - fixed #372 issues with git operation detection that caused a security issue
130 - fixed #372 issues with git operation detection that caused a security issue
103 for git repos
131 for git repos
104
132
105 1.3.2 (**2012-02-28**)
133 1.3.2 (**2012-02-28**)
106 ----------------------
134 ----------------------
107
135
108 news
136 news
109 ++++
137 ++++
110
138
111
139
112 fixes
140 fixes
113 +++++
141 +++++
114
142
115 - fixed git protocol issues with repos-groups
143 - fixed git protocol issues with repos-groups
116 - fixed git remote repos validator that prevented from cloning remote git repos
144 - fixed git remote repos validator that prevented from cloning remote git repos
117 - fixes #370 ending slashes fixes for repo and groups
145 - fixes #370 ending slashes fixes for repo and groups
118 - fixes #368 improved git-protocol detection to handle other clients
146 - fixes #368 improved git-protocol detection to handle other clients
119 - fixes #366 When Setting Repository Group To Blank Repo Group Wont Be
147 - fixes #366 When Setting Repository Group To Blank Repo Group Wont Be
120 Moved To Root
148 Moved To Root
121 - fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys
149 - fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys
122 - fixed #373 missing cascade drop on user_group_to_perm table
150 - fixed #373 missing cascade drop on user_group_to_perm table
123
151
124 1.3.1 (**2012-02-27**)
152 1.3.1 (**2012-02-27**)
125 ----------------------
153 ----------------------
126
154
127 news
155 news
128 ++++
156 ++++
129
157
130
158
131 fixes
159 fixes
132 +++++
160 +++++
133
161
134 - redirection loop occurs when remember-me wasn't checked during login
162 - redirection loop occurs when remember-me wasn't checked during login
135 - fixes issues with git blob history generation
163 - fixes issues with git blob history generation
136 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
164 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
137
165
138 1.3.0 (**2012-02-26**)
166 1.3.0 (**2012-02-26**)
139 ----------------------
167 ----------------------
140
168
141 news
169 news
142 ++++
170 ++++
143
171
144 - code review, inspired by github code-comments
172 - code review, inspired by github code-comments
145 - #215 rst and markdown README files support
173 - #215 rst and markdown README files support
146 - #252 Container-based and proxy pass-through authentication support
174 - #252 Container-based and proxy pass-through authentication support
147 - #44 branch browser. Filtering of changelog by branches
175 - #44 branch browser. Filtering of changelog by branches
148 - mercurial bookmarks support
176 - mercurial bookmarks support
149 - new hover top menu, optimized to add maximum size for important views
177 - new hover top menu, optimized to add maximum size for important views
150 - configurable clone url template with possibility to specify protocol like
178 - configurable clone url template with possibility to specify protocol like
151 ssh:// or http:// and also manually alter other parts of clone_url.
179 ssh:// or http:// and also manually alter other parts of clone_url.
152 - enabled largefiles extension by default
180 - enabled largefiles extension by default
153 - optimized summary file pages and saved a lot of unused space in them
181 - optimized summary file pages and saved a lot of unused space in them
154 - #239 option to manually mark repository as fork
182 - #239 option to manually mark repository as fork
155 - #320 mapping of commit authors to RhodeCode users
183 - #320 mapping of commit authors to RhodeCode users
156 - #304 hashes are displayed using monospace font
184 - #304 hashes are displayed using monospace font
157 - diff configuration, toggle white lines and context lines
185 - diff configuration, toggle white lines and context lines
158 - #307 configurable diffs, whitespace toggle, increasing context lines
186 - #307 configurable diffs, whitespace toggle, increasing context lines
159 - sorting on branches, tags and bookmarks using YUI datatable
187 - sorting on branches, tags and bookmarks using YUI datatable
160 - improved file filter on files page
188 - improved file filter on files page
161 - implements #330 api method for listing nodes ar particular revision
189 - implements #330 api method for listing nodes ar particular revision
162 - #73 added linking issues in commit messages to chosen issue tracker url
190 - #73 added linking issues in commit messages to chosen issue tracker url
163 based on user defined regular expression
191 based on user defined regular expression
164 - added linking of changesets in commit messages
192 - added linking of changesets in commit messages
165 - new compact changelog with expandable commit messages
193 - new compact changelog with expandable commit messages
166 - firstname and lastname are optional in user creation
194 - firstname and lastname are optional in user creation
167 - #348 added post-create repository hook
195 - #348 added post-create repository hook
168 - #212 global encoding settings is now configurable from .ini files
196 - #212 global encoding settings is now configurable from .ini files
169 - #227 added repository groups permissions
197 - #227 added repository groups permissions
170 - markdown gets codehilite extensions
198 - markdown gets codehilite extensions
171 - new API methods, delete_repositories, grante/revoke permissions for groups
199 - new API methods, delete_repositories, grante/revoke permissions for groups
172 and repos
200 and repos
173
201
174
202
175 fixes
203 fixes
176 +++++
204 +++++
177
205
178 - rewrote dbsession management for atomic operations, and better error handling
206 - rewrote dbsession management for atomic operations, and better error handling
179 - fixed sorting of repo tables
207 - fixed sorting of repo tables
180 - #326 escape of special html entities in diffs
208 - #326 escape of special html entities in diffs
181 - normalized user_name => username in api attributes
209 - normalized user_name => username in api attributes
182 - fixes #298 ldap created users with mixed case emails created conflicts
210 - fixes #298 ldap created users with mixed case emails created conflicts
183 on saving a form
211 on saving a form
184 - fixes issue when owner of a repo couldn't revoke permissions for users
212 - fixes issue when owner of a repo couldn't revoke permissions for users
185 and groups
213 and groups
186 - fixes #271 rare JSON serialization problem with statistics
214 - fixes #271 rare JSON serialization problem with statistics
187 - fixes #337 missing validation check for conflicting names of a group with a
215 - fixes #337 missing validation check for conflicting names of a group with a
188 repositories group
216 repositories group
189 - #340 fixed session problem for mysql and celery tasks
217 - #340 fixed session problem for mysql and celery tasks
190 - fixed #331 RhodeCode mangles repository names if the a repository group
218 - fixed #331 RhodeCode mangles repository names if the a repository group
191 contains the "full path" to the repositories
219 contains the "full path" to the repositories
192 - #355 RhodeCode doesn't store encrypted LDAP passwords
220 - #355 RhodeCode doesn't store encrypted LDAP passwords
193
221
194 1.2.5 (**2012-01-28**)
222 1.2.5 (**2012-01-28**)
195 ----------------------
223 ----------------------
196
224
197 news
225 news
198 ++++
226 ++++
199
227
200 fixes
228 fixes
201 +++++
229 +++++
202
230
203 - #340 Celery complains about MySQL server gone away, added session cleanup
231 - #340 Celery complains about MySQL server gone away, added session cleanup
204 for celery tasks
232 for celery tasks
205 - #341 "scanning for repositories in None" log message during Rescan was missing
233 - #341 "scanning for repositories in None" log message during Rescan was missing
206 a parameter
234 a parameter
207 - fixed creating archives with subrepos. Some hooks were triggered during that
235 - fixed creating archives with subrepos. Some hooks were triggered during that
208 operation leading to crash.
236 operation leading to crash.
209 - fixed missing email in account page.
237 - fixed missing email in account page.
210 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
238 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
211 forking on windows impossible
239 forking on windows impossible
212
240
213 1.2.4 (**2012-01-19**)
241 1.2.4 (**2012-01-19**)
214 ----------------------
242 ----------------------
215
243
216 news
244 news
217 ++++
245 ++++
218
246
219 - RhodeCode is bundled with mercurial series 2.0.X by default, with
247 - RhodeCode is bundled with mercurial series 2.0.X by default, with
220 full support to largefiles extension. Enabled by default in new installations
248 full support to largefiles extension. Enabled by default in new installations
221 - #329 Ability to Add/Remove Groups to/from a Repository via AP
249 - #329 Ability to Add/Remove Groups to/from a Repository via AP
222 - added requires.txt file with requirements
250 - added requires.txt file with requirements
223
251
224 fixes
252 fixes
225 +++++
253 +++++
226
254
227 - fixes db session issues with celery when emailing admins
255 - fixes db session issues with celery when emailing admins
228 - #331 RhodeCode mangles repository names if the a repository group
256 - #331 RhodeCode mangles repository names if the a repository group
229 contains the "full path" to the repositories
257 contains the "full path" to the repositories
230 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
258 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
231 - DB session cleanup after hg protocol operations, fixes issues with
259 - DB session cleanup after hg protocol operations, fixes issues with
232 `mysql has gone away` errors
260 `mysql has gone away` errors
233 - #333 doc fixes for get_repo api function
261 - #333 doc fixes for get_repo api function
234 - #271 rare JSON serialization problem with statistics enabled
262 - #271 rare JSON serialization problem with statistics enabled
235 - #337 Fixes issues with validation of repository name conflicting with
263 - #337 Fixes issues with validation of repository name conflicting with
236 a group name. A proper message is now displayed.
264 a group name. A proper message is now displayed.
237 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
265 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
238 doesn't work
266 doesn't work
239 - #316 fixes issues with web description in hgrc files
267 - #316 fixes issues with web description in hgrc files
240
268
241 1.2.3 (**2011-11-02**)
269 1.2.3 (**2011-11-02**)
242 ----------------------
270 ----------------------
243
271
244 news
272 news
245 ++++
273 ++++
246
274
247 - added option to manage repos group for non admin users
275 - added option to manage repos group for non admin users
248 - added following API methods for get_users, create_user, get_users_groups,
276 - added following API methods for get_users, create_user, get_users_groups,
249 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
277 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
250 get_repo, create_repo, add_user_to_repo
278 get_repo, create_repo, add_user_to_repo
251 - implements #237 added password confirmation for my account
279 - implements #237 added password confirmation for my account
252 and admin edit user.
280 and admin edit user.
253 - implements #291 email notification for global events are now sent to all
281 - implements #291 email notification for global events are now sent to all
254 administrator users, and global config email.
282 administrator users, and global config email.
255
283
256 fixes
284 fixes
257 +++++
285 +++++
258
286
259 - added option for passing auth method for smtp mailer
287 - added option for passing auth method for smtp mailer
260 - #276 issue with adding a single user with id>10 to usergroups
288 - #276 issue with adding a single user with id>10 to usergroups
261 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
289 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
262 - #288 fixes managing of repos in a group for non admin user
290 - #288 fixes managing of repos in a group for non admin user
263
291
264 1.2.2 (**2011-10-17**)
292 1.2.2 (**2011-10-17**)
265 ----------------------
293 ----------------------
266
294
267 news
295 news
268 ++++
296 ++++
269
297
270 - #226 repo groups are available by path instead of numerical id
298 - #226 repo groups are available by path instead of numerical id
271
299
272 fixes
300 fixes
273 +++++
301 +++++
274
302
275 - #259 Groups with the same name but with different parent group
303 - #259 Groups with the same name but with different parent group
276 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
304 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
277 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
305 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
278 - #265 ldap save fails sometimes on converting attributes to booleans,
306 - #265 ldap save fails sometimes on converting attributes to booleans,
279 added getter and setter into model that will prevent from this on db model level
307 added getter and setter into model that will prevent from this on db model level
280 - fixed problems with timestamps issues #251 and #213
308 - fixed problems with timestamps issues #251 and #213
281 - fixes #266 RhodeCode allows to create repo with the same name and in
309 - fixes #266 RhodeCode allows to create repo with the same name and in
282 the same parent as group
310 the same parent as group
283 - fixes #245 Rescan of the repositories on Windows
311 - fixes #245 Rescan of the repositories on Windows
284 - fixes #248 cannot edit repos inside a group on windows
312 - fixes #248 cannot edit repos inside a group on windows
285 - fixes #219 forking problems on windows
313 - fixes #219 forking problems on windows
286
314
287 1.2.1 (**2011-10-08**)
315 1.2.1 (**2011-10-08**)
288 ----------------------
316 ----------------------
289
317
290 news
318 news
291 ++++
319 ++++
292
320
293
321
294 fixes
322 fixes
295 +++++
323 +++++
296
324
297 - fixed problems with basic auth and push problems
325 - fixed problems with basic auth and push problems
298 - gui fixes
326 - gui fixes
299 - fixed logger
327 - fixed logger
300
328
301 1.2.0 (**2011-10-07**)
329 1.2.0 (**2011-10-07**)
302 ----------------------
330 ----------------------
303
331
304 news
332 news
305 ++++
333 ++++
306
334
307 - implemented #47 repository groups
335 - implemented #47 repository groups
308 - implemented #89 Can setup google analytics code from settings menu
336 - implemented #89 Can setup google analytics code from settings menu
309 - implemented #91 added nicer looking archive urls with more download options
337 - implemented #91 added nicer looking archive urls with more download options
310 like tags, branches
338 like tags, branches
311 - implemented #44 into file browsing, and added follow branch option
339 - implemented #44 into file browsing, and added follow branch option
312 - implemented #84 downloads can be enabled/disabled for each repository
340 - implemented #84 downloads can be enabled/disabled for each repository
313 - anonymous repository can be cloned without having to pass default:default
341 - anonymous repository can be cloned without having to pass default:default
314 into clone url
342 into clone url
315 - fixed #90 whoosh indexer can index chooses repositories passed in command
343 - fixed #90 whoosh indexer can index chooses repositories passed in command
316 line
344 line
317 - extended journal with day aggregates and paging
345 - extended journal with day aggregates and paging
318 - implemented #107 source code lines highlight ranges
346 - implemented #107 source code lines highlight ranges
319 - implemented #93 customizable changelog on combined revision ranges -
347 - implemented #93 customizable changelog on combined revision ranges -
320 equivalent of githubs compare view
348 equivalent of githubs compare view
321 - implemented #108 extended and more powerful LDAP configuration
349 - implemented #108 extended and more powerful LDAP configuration
322 - implemented #56 users groups
350 - implemented #56 users groups
323 - major code rewrites optimized codes for speed and memory usage
351 - major code rewrites optimized codes for speed and memory usage
324 - raw and diff downloads are now in git format
352 - raw and diff downloads are now in git format
325 - setup command checks for write access to given path
353 - setup command checks for write access to given path
326 - fixed many issues with international characters and unicode. It uses utf8
354 - fixed many issues with international characters and unicode. It uses utf8
327 decode with replace to provide less errors even with non utf8 encoded strings
355 decode with replace to provide less errors even with non utf8 encoded strings
328 - #125 added API KEY access to feeds
356 - #125 added API KEY access to feeds
329 - #109 Repository can be created from external Mercurial link (aka. remote
357 - #109 Repository can be created from external Mercurial link (aka. remote
330 repository, and manually updated (via pull) from admin panel
358 repository, and manually updated (via pull) from admin panel
331 - beta git support - push/pull server + basic view for git repos
359 - beta git support - push/pull server + basic view for git repos
332 - added followers page and forks page
360 - added followers page and forks page
333 - server side file creation (with binary file upload interface)
361 - server side file creation (with binary file upload interface)
334 and edition with commits powered by codemirror
362 and edition with commits powered by codemirror
335 - #111 file browser file finder, quick lookup files on whole file tree
363 - #111 file browser file finder, quick lookup files on whole file tree
336 - added quick login sliding menu into main page
364 - added quick login sliding menu into main page
337 - changelog uses lazy loading of affected files details, in some scenarios
365 - changelog uses lazy loading of affected files details, in some scenarios
338 this can improve speed of changelog page dramatically especially for
366 this can improve speed of changelog page dramatically especially for
339 larger repositories.
367 larger repositories.
340 - implements #214 added support for downloading subrepos in download menu.
368 - implements #214 added support for downloading subrepos in download menu.
341 - Added basic API for direct operations on rhodecode via JSON
369 - Added basic API for direct operations on rhodecode via JSON
342 - Implemented advanced hook management
370 - Implemented advanced hook management
343
371
344 fixes
372 fixes
345 +++++
373 +++++
346
374
347 - fixed file browser bug, when switching into given form revision the url was
375 - fixed file browser bug, when switching into given form revision the url was
348 not changing
376 not changing
349 - fixed propagation to error controller on simplehg and simplegit middlewares
377 - fixed propagation to error controller on simplehg and simplegit middlewares
350 - fixed error when trying to make a download on empty repository
378 - fixed error when trying to make a download on empty repository
351 - fixed problem with '[' chars in commit messages in journal
379 - fixed problem with '[' chars in commit messages in journal
352 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
380 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
353 - journal fork fixes
381 - journal fork fixes
354 - removed issue with space inside renamed repository after deletion
382 - removed issue with space inside renamed repository after deletion
355 - fixed strange issue on formencode imports
383 - fixed strange issue on formencode imports
356 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
384 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
357 - #150 fixes for errors on repositories mapped in db but corrupted in
385 - #150 fixes for errors on repositories mapped in db but corrupted in
358 filesystem
386 filesystem
359 - fixed problem with ascendant characters in realm #181
387 - fixed problem with ascendant characters in realm #181
360 - fixed problem with sqlite file based database connection pool
388 - fixed problem with sqlite file based database connection pool
361 - whoosh indexer and code stats share the same dynamic extensions map
389 - whoosh indexer and code stats share the same dynamic extensions map
362 - fixes #188 - relationship delete of repo_to_perm entry on user removal
390 - fixes #188 - relationship delete of repo_to_perm entry on user removal
363 - fixes issue #189 Trending source files shows "show more" when no more exist
391 - fixes issue #189 Trending source files shows "show more" when no more exist
364 - fixes issue #197 Relative paths for pidlocks
392 - fixes issue #197 Relative paths for pidlocks
365 - fixes issue #198 password will require only 3 chars now for login form
393 - fixes issue #198 password will require only 3 chars now for login form
366 - fixes issue #199 wrong redirection for non admin users after creating a repository
394 - fixes issue #199 wrong redirection for non admin users after creating a repository
367 - fixes issues #202, bad db constraint made impossible to attach same group
395 - fixes issues #202, bad db constraint made impossible to attach same group
368 more than one time. Affects only mysql/postgres
396 more than one time. Affects only mysql/postgres
369 - fixes #218 os.kill patch for windows was missing sig param
397 - fixes #218 os.kill patch for windows was missing sig param
370 - improved rendering of dag (they are not trimmed anymore when number of
398 - improved rendering of dag (they are not trimmed anymore when number of
371 heads exceeds 5)
399 heads exceeds 5)
372
400
373 1.1.8 (**2011-04-12**)
401 1.1.8 (**2011-04-12**)
374 ----------------------
402 ----------------------
375
403
376 news
404 news
377 ++++
405 ++++
378
406
379 - improved windows support
407 - improved windows support
380
408
381 fixes
409 fixes
382 +++++
410 +++++
383
411
384 - fixed #140 freeze of python dateutil library, since new version is python2.x
412 - fixed #140 freeze of python dateutil library, since new version is python2.x
385 incompatible
413 incompatible
386 - setup-app will check for write permission in given path
414 - setup-app will check for write permission in given path
387 - cleaned up license info issue #149
415 - cleaned up license info issue #149
388 - fixes for issues #137,#116 and problems with unicode and accented characters.
416 - fixes for issues #137,#116 and problems with unicode and accented characters.
389 - fixes crashes on gravatar, when passed in email as unicode
417 - fixes crashes on gravatar, when passed in email as unicode
390 - fixed tooltip flickering problems
418 - fixed tooltip flickering problems
391 - fixed came_from redirection on windows
419 - fixed came_from redirection on windows
392 - fixed logging modules, and sql formatters
420 - fixed logging modules, and sql formatters
393 - windows fixes for os.kill issue #133
421 - windows fixes for os.kill issue #133
394 - fixes path splitting for windows issues #148
422 - fixes path splitting for windows issues #148
395 - fixed issue #143 wrong import on migration to 1.1.X
423 - fixed issue #143 wrong import on migration to 1.1.X
396 - fixed problems with displaying binary files, thanks to Thomas Waldmann
424 - fixed problems with displaying binary files, thanks to Thomas Waldmann
397 - removed name from archive files since it's breaking ui for long repo names
425 - removed name from archive files since it's breaking ui for long repo names
398 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
426 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
399 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
427 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
400 Thomas Waldmann
428 Thomas Waldmann
401 - fixed issue #166 summary pager was skipping 10 revisions on second page
429 - fixed issue #166 summary pager was skipping 10 revisions on second page
402
430
403
431
404 1.1.7 (**2011-03-23**)
432 1.1.7 (**2011-03-23**)
405 ----------------------
433 ----------------------
406
434
407 news
435 news
408 ++++
436 ++++
409
437
410 fixes
438 fixes
411 +++++
439 +++++
412
440
413 - fixed (again) #136 installation support for FreeBSD
441 - fixed (again) #136 installation support for FreeBSD
414
442
415
443
416 1.1.6 (**2011-03-21**)
444 1.1.6 (**2011-03-21**)
417 ----------------------
445 ----------------------
418
446
419 news
447 news
420 ++++
448 ++++
421
449
422 fixes
450 fixes
423 +++++
451 +++++
424
452
425 - fixed #136 installation support for FreeBSD
453 - fixed #136 installation support for FreeBSD
426 - RhodeCode will check for python version during installation
454 - RhodeCode will check for python version during installation
427
455
428 1.1.5 (**2011-03-17**)
456 1.1.5 (**2011-03-17**)
429 ----------------------
457 ----------------------
430
458
431 news
459 news
432 ++++
460 ++++
433
461
434 - basic windows support, by exchanging pybcrypt into sha256 for windows only
462 - basic windows support, by exchanging pybcrypt into sha256 for windows only
435 highly inspired by idea of mantis406
463 highly inspired by idea of mantis406
436
464
437 fixes
465 fixes
438 +++++
466 +++++
439
467
440 - fixed sorting by author in main page
468 - fixed sorting by author in main page
441 - fixed crashes with diffs on binary files
469 - fixed crashes with diffs on binary files
442 - fixed #131 problem with boolean values for LDAP
470 - fixed #131 problem with boolean values for LDAP
443 - fixed #122 mysql problems thanks to striker69
471 - fixed #122 mysql problems thanks to striker69
444 - fixed problem with errors on calling raw/raw_files/annotate functions
472 - fixed problem with errors on calling raw/raw_files/annotate functions
445 with unknown revisions
473 with unknown revisions
446 - fixed returned rawfiles attachment names with international character
474 - fixed returned rawfiles attachment names with international character
447 - cleaned out docs, big thanks to Jason Harris
475 - cleaned out docs, big thanks to Jason Harris
448
476
449 1.1.4 (**2011-02-19**)
477 1.1.4 (**2011-02-19**)
450 ----------------------
478 ----------------------
451
479
452 news
480 news
453 ++++
481 ++++
454
482
455 fixes
483 fixes
456 +++++
484 +++++
457
485
458 - fixed formencode import problem on settings page, that caused server crash
486 - fixed formencode import problem on settings page, that caused server crash
459 when that page was accessed as first after server start
487 when that page was accessed as first after server start
460 - journal fixes
488 - journal fixes
461 - fixed option to access repository just by entering http://server/<repo_name>
489 - fixed option to access repository just by entering http://server/<repo_name>
462
490
463 1.1.3 (**2011-02-16**)
491 1.1.3 (**2011-02-16**)
464 ----------------------
492 ----------------------
465
493
466 news
494 news
467 ++++
495 ++++
468
496
469 - implemented #102 allowing the '.' character in username
497 - implemented #102 allowing the '.' character in username
470 - added option to access repository just by entering http://server/<repo_name>
498 - added option to access repository just by entering http://server/<repo_name>
471 - celery task ignores result for better performance
499 - celery task ignores result for better performance
472
500
473 fixes
501 fixes
474 +++++
502 +++++
475
503
476 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
504 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
477 apollo13 and Johan Walles
505 apollo13 and Johan Walles
478 - small fixes in journal
506 - small fixes in journal
479 - fixed problems with getting setting for celery from .ini files
507 - fixed problems with getting setting for celery from .ini files
480 - registration, password reset and login boxes share the same title as main
508 - registration, password reset and login boxes share the same title as main
481 application now
509 application now
482 - fixed #113: to high permissions to fork repository
510 - fixed #113: to high permissions to fork repository
483 - fixed problem with '[' chars in commit messages in journal
511 - fixed problem with '[' chars in commit messages in journal
484 - removed issue with space inside renamed repository after deletion
512 - removed issue with space inside renamed repository after deletion
485 - db transaction fixes when filesystem repository creation failed
513 - db transaction fixes when filesystem repository creation failed
486 - fixed #106 relation issues on databases different than sqlite
514 - fixed #106 relation issues on databases different than sqlite
487 - fixed static files paths links to use of url() method
515 - fixed static files paths links to use of url() method
488
516
489 1.1.2 (**2011-01-12**)
517 1.1.2 (**2011-01-12**)
490 ----------------------
518 ----------------------
491
519
492 news
520 news
493 ++++
521 ++++
494
522
495
523
496 fixes
524 fixes
497 +++++
525 +++++
498
526
499 - fixes #98 protection against float division of percentage stats
527 - fixes #98 protection against float division of percentage stats
500 - fixed graph bug
528 - fixed graph bug
501 - forced webhelpers version since it was making troubles during installation
529 - forced webhelpers version since it was making troubles during installation
502
530
503 1.1.1 (**2011-01-06**)
531 1.1.1 (**2011-01-06**)
504 ----------------------
532 ----------------------
505
533
506 news
534 news
507 ++++
535 ++++
508
536
509 - added force https option into ini files for easier https usage (no need to
537 - added force https option into ini files for easier https usage (no need to
510 set server headers with this options)
538 set server headers with this options)
511 - small css updates
539 - small css updates
512
540
513 fixes
541 fixes
514 +++++
542 +++++
515
543
516 - fixed #96 redirect loop on files view on repositories without changesets
544 - fixed #96 redirect loop on files view on repositories without changesets
517 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
545 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
518 and server crashed with errors
546 and server crashed with errors
519 - fixed large tooltips problems on main page
547 - fixed large tooltips problems on main page
520 - fixed #92 whoosh indexer is more error proof
548 - fixed #92 whoosh indexer is more error proof
521
549
522 1.1.0 (**2010-12-18**)
550 1.1.0 (**2010-12-18**)
523 ----------------------
551 ----------------------
524
552
525 news
553 news
526 ++++
554 ++++
527
555
528 - rewrite of internals for vcs >=0.1.10
556 - rewrite of internals for vcs >=0.1.10
529 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
557 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
530 with older clients
558 with older clients
531 - anonymous access, authentication via ldap
559 - anonymous access, authentication via ldap
532 - performance upgrade for cached repos list - each repository has its own
560 - performance upgrade for cached repos list - each repository has its own
533 cache that's invalidated when needed.
561 cache that's invalidated when needed.
534 - performance upgrades on repositories with large amount of commits (20K+)
562 - performance upgrades on repositories with large amount of commits (20K+)
535 - main page quick filter for filtering repositories
563 - main page quick filter for filtering repositories
536 - user dashboards with ability to follow chosen repositories actions
564 - user dashboards with ability to follow chosen repositories actions
537 - sends email to admin on new user registration
565 - sends email to admin on new user registration
538 - added cache/statistics reset options into repository settings
566 - added cache/statistics reset options into repository settings
539 - more detailed action logger (based on hooks) with pushed changesets lists
567 - more detailed action logger (based on hooks) with pushed changesets lists
540 and options to disable those hooks from admin panel
568 and options to disable those hooks from admin panel
541 - introduced new enhanced changelog for merges that shows more accurate results
569 - introduced new enhanced changelog for merges that shows more accurate results
542 - new improved and faster code stats (based on pygments lexers mapping tables,
570 - new improved and faster code stats (based on pygments lexers mapping tables,
543 showing up to 10 trending sources for each repository. Additionally stats
571 showing up to 10 trending sources for each repository. Additionally stats
544 can be disabled in repository settings.
572 can be disabled in repository settings.
545 - gui optimizations, fixed application width to 1024px
573 - gui optimizations, fixed application width to 1024px
546 - added cut off (for large files/changesets) limit into config files
574 - added cut off (for large files/changesets) limit into config files
547 - whoosh, celeryd, upgrade moved to paster command
575 - whoosh, celeryd, upgrade moved to paster command
548 - other than sqlite database backends can be used
576 - other than sqlite database backends can be used
549
577
550 fixes
578 fixes
551 +++++
579 +++++
552
580
553 - fixes #61 forked repo was showing only after cache expired
581 - fixes #61 forked repo was showing only after cache expired
554 - fixes #76 no confirmation on user deletes
582 - fixes #76 no confirmation on user deletes
555 - fixes #66 Name field misspelled
583 - fixes #66 Name field misspelled
556 - fixes #72 block user removal when he owns repositories
584 - fixes #72 block user removal when he owns repositories
557 - fixes #69 added password confirmation fields
585 - fixes #69 added password confirmation fields
558 - fixes #87 RhodeCode crashes occasionally on updating repository owner
586 - fixes #87 RhodeCode crashes occasionally on updating repository owner
559 - fixes #82 broken annotations on files with more than 1 blank line at the end
587 - fixes #82 broken annotations on files with more than 1 blank line at the end
560 - a lot of fixes and tweaks for file browser
588 - a lot of fixes and tweaks for file browser
561 - fixed detached session issues
589 - fixed detached session issues
562 - fixed when user had no repos he would see all repos listed in my account
590 - fixed when user had no repos he would see all repos listed in my account
563 - fixed ui() instance bug when global hgrc settings was loaded for server
591 - fixed ui() instance bug when global hgrc settings was loaded for server
564 instance and all hgrc options were merged with our db ui() object
592 instance and all hgrc options were merged with our db ui() object
565 - numerous small bugfixes
593 - numerous small bugfixes
566
594
567 (special thanks for TkSoh for detailed feedback)
595 (special thanks for TkSoh for detailed feedback)
568
596
569
597
570 1.0.2 (**2010-11-12**)
598 1.0.2 (**2010-11-12**)
571 ----------------------
599 ----------------------
572
600
573 news
601 news
574 ++++
602 ++++
575
603
576 - tested under python2.7
604 - tested under python2.7
577 - bumped sqlalchemy and celery versions
605 - bumped sqlalchemy and celery versions
578
606
579 fixes
607 fixes
580 +++++
608 +++++
581
609
582 - fixed #59 missing graph.js
610 - fixed #59 missing graph.js
583 - fixed repo_size crash when repository had broken symlinks
611 - fixed repo_size crash when repository had broken symlinks
584 - fixed python2.5 crashes.
612 - fixed python2.5 crashes.
585
613
586
614
587 1.0.1 (**2010-11-10**)
615 1.0.1 (**2010-11-10**)
588 ----------------------
616 ----------------------
589
617
590 news
618 news
591 ++++
619 ++++
592
620
593 - small css updated
621 - small css updated
594
622
595 fixes
623 fixes
596 +++++
624 +++++
597
625
598 - fixed #53 python2.5 incompatible enumerate calls
626 - fixed #53 python2.5 incompatible enumerate calls
599 - fixed #52 disable mercurial extension for web
627 - fixed #52 disable mercurial extension for web
600 - fixed #51 deleting repositories don't delete it's dependent objects
628 - fixed #51 deleting repositories don't delete it's dependent objects
601
629
602
630
603 1.0.0 (**2010-11-02**)
631 1.0.0 (**2010-11-02**)
604 ----------------------
632 ----------------------
605
633
606 - security bugfix simplehg wasn't checking for permissions on commands
634 - security bugfix simplehg wasn't checking for permissions on commands
607 other than pull or push.
635 other than pull or push.
608 - fixed doubled messages after push or pull in admin journal
636 - fixed doubled messages after push or pull in admin journal
609 - templating and css corrections, fixed repo switcher on chrome, updated titles
637 - templating and css corrections, fixed repo switcher on chrome, updated titles
610 - admin menu accessible from options menu on repository view
638 - admin menu accessible from options menu on repository view
611 - permissions cached queries
639 - permissions cached queries
612
640
613 1.0.0rc4 (**2010-10-12**)
641 1.0.0rc4 (**2010-10-12**)
614 --------------------------
642 --------------------------
615
643
616 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
644 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
617 - removed cache_manager settings from sqlalchemy meta
645 - removed cache_manager settings from sqlalchemy meta
618 - added sqlalchemy cache settings to ini files
646 - added sqlalchemy cache settings to ini files
619 - validated password length and added second try of failure on paster setup-app
647 - validated password length and added second try of failure on paster setup-app
620 - fixed setup database destroy prompt even when there was no db
648 - fixed setup database destroy prompt even when there was no db
621
649
622
650
623 1.0.0rc3 (**2010-10-11**)
651 1.0.0rc3 (**2010-10-11**)
624 -------------------------
652 -------------------------
625
653
626 - fixed i18n during installation.
654 - fixed i18n during installation.
627
655
628 1.0.0rc2 (**2010-10-11**)
656 1.0.0rc2 (**2010-10-11**)
629 -------------------------
657 -------------------------
630
658
631 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
659 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
632 occure. After vcs is fixed it'll be put back again.
660 occure. After vcs is fixed it'll be put back again.
633 - templating/css rewrites, optimized css. No newline at end of file
661 - templating/css rewrites, optimized css.
@@ -1,94 +1,97 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.__init__
3 rhodecode.__init__
4 ~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~
5
5
6 RhodeCode, a web based repository management based on pylons
6 RhodeCode, a web based repository management based on pylons
7 versioning implementation: http://www.python.org/dev/peps/pep-0386/
7 versioning implementation: http://www.python.org/dev/peps/pep-0386/
8
8
9 :created_on: Apr 9, 2010
9 :created_on: Apr 9, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 import sys
26 import sys
27 import platform
27 import platform
28
28
29 VERSION = (1, 4, 0, 'b')
29 VERSION = (1, 4, 0, 'b')
30
30
31 try:
31 try:
32 from rhodecode.lib import get_current_revision
32 from rhodecode.lib import get_current_revision
33 _rev = get_current_revision()
33 _rev = get_current_revision()
34 if _rev and len(VERSION) > 3:
34 if _rev and len(VERSION) > 3:
35 VERSION += ('dev%s' % _rev[0],)
35 VERSION += ('dev%s' % _rev[0],)
36 except ImportError:
36 except ImportError:
37 pass
37 pass
38
38
39 __version__ = ('.'.join((str(each) for each in VERSION[:3])) +
39 __version__ = ('.'.join((str(each) for each in VERSION[:3])) +
40 '.'.join(VERSION[3:]))
40 '.'.join(VERSION[3:]))
41 __dbversion__ = 6 # defines current db version for migrations
41 __dbversion__ = 6 # defines current db version for migrations
42 __platform__ = platform.system()
42 __platform__ = platform.system()
43 __license__ = 'GPLv3'
43 __license__ = 'GPLv3'
44 __py_version__ = sys.version_info
44 __py_version__ = sys.version_info
45
45
46 PLATFORM_WIN = ('Windows')
46 PLATFORM_WIN = ('Windows')
47 PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS')
47 PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS')
48
48
49 is_windows = __platform__ in PLATFORM_WIN
50 is_unix = __platform__ in PLATFORM_OTHERS
51
49 requirements = [
52 requirements = [
50 "Pylons==1.0.0",
53 "Pylons==1.0.0",
51 "Beaker==1.6.3",
54 "Beaker==1.6.3",
52 "WebHelpers==1.3",
55 "WebHelpers==1.3",
53 "formencode==1.2.4",
56 "formencode==1.2.4",
54 "SQLAlchemy==0.7.6",
57 "SQLAlchemy==0.7.6",
55 "Mako==0.7.0",
58 "Mako==0.7.0",
56 "pygments>=1.4",
59 "pygments>=1.4",
57 "whoosh>=2.4.0,<2.5",
60 "whoosh>=2.4.0,<2.5",
58 "celery>=2.2.5,<2.3",
61 "celery>=2.2.5,<2.3",
59 "babel",
62 "babel",
60 "python-dateutil>=1.5.0,<2.0.0",
63 "python-dateutil>=1.5.0,<2.0.0",
61 "dulwich>=0.8.5,<0.9.0",
64 "dulwich>=0.8.5,<0.9.0",
62 "webob==1.0.8",
65 "webob==1.0.8",
63 "markdown==2.1.1",
66 "markdown==2.1.1",
64 "docutils==0.8.1",
67 "docutils==0.8.1",
68 "simplejson==2.5.2",
65 ]
69 ]
66
70
67 if __py_version__ < (2, 6):
71 if __py_version__ < (2, 6):
68 requirements.append("simplejson")
69 requirements.append("pysqlite")
72 requirements.append("pysqlite")
70
73
71 if __platform__ in PLATFORM_WIN:
74 if is_windows:
72 requirements.append("mercurial>=2.2.1,<2.3")
75 requirements.append("mercurial>=2.2.1,<2.3")
73 else:
76 else:
74 requirements.append("py-bcrypt")
77 requirements.append("py-bcrypt")
75 requirements.append("mercurial>=2.2.1,<2.3")
78 requirements.append("mercurial>=2.2.1,<2.3")
76
79
77
80
78 def get_version():
81 def get_version():
79 """Returns shorter version (digit parts only) as string."""
82 """Returns shorter version (digit parts only) as string."""
80
83
81 return '.'.join((str(each) for each in VERSION[:3]))
84 return '.'.join((str(each) for each in VERSION[:3]))
82
85
83 BACKENDS = {
86 BACKENDS = {
84 'hg': 'Mercurial repository',
87 'hg': 'Mercurial repository',
85 'git': 'Git repository',
88 'git': 'Git repository',
86 }
89 }
87
90
88 CELERY_ON = False
91 CELERY_ON = False
89
92
90 # link to config for pylons
93 # link to config for pylons
91 CONFIG = {}
94 CONFIG = {}
92
95
93 # Linked module for extensions
96 # Linked module for extensions
94 EXTENSIONS = {}
97 EXTENSIONS = {}
@@ -1,79 +1,81 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.config.rcextensions.make_rcextensions
3 rhodecode.config.rcextensions.make_rcextensions
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Whoosh indexing module for RhodeCode
6 Whoosh indexing module for RhodeCode
7
7
8 :created_on: Mar 6, 2012
8 :created_on: Mar 6, 2012
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 from __future__ import with_statement
26
25 import os
27 import os
26 import sys
28 import sys
27 import pkg_resources
29 import pkg_resources
28 import traceback
30 import traceback
29 import logging
31 import logging
30 from os.path import dirname as dn, join as jn
32 from os.path import dirname as dn, join as jn
31
33
32 #to get the rhodecode import
34 #to get the rhodecode import
33 sys.path.append(dn(dn(dn(os.path.realpath(__file__)))))
35 sys.path.append(dn(dn(dn(os.path.realpath(__file__)))))
34
36
35 from rhodecode.lib.utils import BasePasterCommand, Command, ask_ok
37 from rhodecode.lib.utils import BasePasterCommand, Command, ask_ok
36
38
37 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
38
40
39
41
40 class MakeRcExt(BasePasterCommand):
42 class MakeRcExt(BasePasterCommand):
41
43
42 max_args = 1
44 max_args = 1
43 min_args = 1
45 min_args = 1
44
46
45 usage = "CONFIG_FILE"
47 usage = "CONFIG_FILE"
46 summary = "Creates additional extensions for rhodecode"
48 summary = "Creates additional extensions for rhodecode"
47 group_name = "RhodeCode"
49 group_name = "RhodeCode"
48 takes_config_file = -1
50 takes_config_file = -1
49 parser = Command.standard_parser(verbose=True)
51 parser = Command.standard_parser(verbose=True)
50
52
51 def command(self):
53 def command(self):
52 logging.config.fileConfig(self.path_to_ini_file)
54 logging.config.fileConfig(self.path_to_ini_file)
53 from pylons import config
55 from pylons import config
54
56
55 def _make_file(ext_file):
57 def _make_file(ext_file):
56 bdir = os.path.split(ext_file)[0]
58 bdir = os.path.split(ext_file)[0]
57 if not os.path.isdir(bdir):
59 if not os.path.isdir(bdir):
58 os.makedirs(bdir)
60 os.makedirs(bdir)
59 with open(ext_file, 'wb') as f:
61 with open(ext_file, 'wb') as f:
60 f.write(tmpl)
62 f.write(tmpl)
61 log.info('Writen new extensions file to %s' % ext_file)
63 log.info('Writen new extensions file to %s' % ext_file)
62
64
63 here = config['here']
65 here = config['here']
64 tmpl = pkg_resources.resource_string(
66 tmpl = pkg_resources.resource_string(
65 'rhodecode', jn('config', 'rcextensions', '__init__.py')
67 'rhodecode', jn('config', 'rcextensions', '__init__.py')
66 )
68 )
67 ext_file = jn(here, 'rcextensions', '__init__.py')
69 ext_file = jn(here, 'rcextensions', '__init__.py')
68 if os.path.exists(ext_file):
70 if os.path.exists(ext_file):
69 msg = ('Extension file already exists, do you want '
71 msg = ('Extension file already exists, do you want '
70 'to overwrite it ? [y/n]')
72 'to overwrite it ? [y/n]')
71 if ask_ok(msg):
73 if ask_ok(msg):
72 _make_file(ext_file)
74 _make_file(ext_file)
73 else:
75 else:
74 log.info('nothing done...')
76 log.info('nothing done...')
75 else:
77 else:
76 _make_file(ext_file)
78 _make_file(ext_file)
77
79
78 def update_parser(self):
80 def update_parser(self):
79 pass
81 pass
@@ -1,486 +1,486 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.files
3 rhodecode.controllers.files
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Files controller for RhodeCode
6 Files controller for RhodeCode
7
7
8 :created_on: Apr 21, 2010
8 :created_on: Apr 21, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import os
26 import os
27 import logging
27 import logging
28 import traceback
28 import traceback
29 import tempfile
29
30
30 from pylons import request, response, tmpl_context as c, url
31 from pylons import request, response, tmpl_context as c, url
31 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
33 from pylons.decorators import jsonify
34 from pylons.decorators import jsonify
34
35
35 from rhodecode.lib import diffs
36 from rhodecode.lib import diffs
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
37
38
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str
40 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str
40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 from rhodecode.lib.base import BaseRepoController, render
42 from rhodecode.lib.base import BaseRepoController, render
42 from rhodecode.lib.utils import EmptyChangeset
43 from rhodecode.lib.utils import EmptyChangeset
43 from rhodecode.lib.vcs.conf import settings
44 from rhodecode.lib.vcs.conf import settings
44 from rhodecode.lib.vcs.exceptions import RepositoryError, \
45 from rhodecode.lib.vcs.exceptions import RepositoryError, \
45 ChangesetDoesNotExistError, EmptyRepositoryError, \
46 ChangesetDoesNotExistError, EmptyRepositoryError, \
46 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
47 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
47 from rhodecode.lib.vcs.nodes import FileNode
48 from rhodecode.lib.vcs.nodes import FileNode
48
49
49 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.scm import ScmModel
51 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.db import Repository
51
53
52 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
54 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
53 _context_url, get_line_ctx, get_ignore_ws
55 _context_url, get_line_ctx, get_ignore_ws
54
56
55
57
56 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
57
59
58
60
59 class FilesController(BaseRepoController):
61 class FilesController(BaseRepoController):
60
62
61 @LoginRequired()
63 @LoginRequired()
62 def __before__(self):
64 def __before__(self):
63 super(FilesController, self).__before__()
65 super(FilesController, self).__before__()
64 c.cut_off_limit = self.cut_off_limit
66 c.cut_off_limit = self.cut_off_limit
65
67
66 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
68 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
67 """
69 """
68 Safe way to get changeset if error occur it redirects to tip with
70 Safe way to get changeset if error occur it redirects to tip with
69 proper message
71 proper message
70
72
71 :param rev: revision to fetch
73 :param rev: revision to fetch
72 :param repo_name: repo name to redirect after
74 :param repo_name: repo name to redirect after
73 """
75 """
74
76
75 try:
77 try:
76 return c.rhodecode_repo.get_changeset(rev)
78 return c.rhodecode_repo.get_changeset(rev)
77 except EmptyRepositoryError, e:
79 except EmptyRepositoryError, e:
78 if not redirect_after:
80 if not redirect_after:
79 return None
81 return None
80 url_ = url('files_add_home',
82 url_ = url('files_add_home',
81 repo_name=c.repo_name,
83 repo_name=c.repo_name,
82 revision=0, f_path='')
84 revision=0, f_path='')
83 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
85 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
84 h.flash(h.literal(_('There are no files yet %s' % add_new)),
86 h.flash(h.literal(_('There are no files yet %s' % add_new)),
85 category='warning')
87 category='warning')
86 redirect(h.url('summary_home', repo_name=repo_name))
88 redirect(h.url('summary_home', repo_name=repo_name))
87
89
88 except RepositoryError, e:
90 except RepositoryError, e:
89 h.flash(str(e), category='warning')
91 h.flash(str(e), category='warning')
90 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
92 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
91
93
92 def __get_filenode_or_redirect(self, repo_name, cs, path):
94 def __get_filenode_or_redirect(self, repo_name, cs, path):
93 """
95 """
94 Returns file_node, if error occurs or given path is directory,
96 Returns file_node, if error occurs or given path is directory,
95 it'll redirect to top level path
97 it'll redirect to top level path
96
98
97 :param repo_name: repo_name
99 :param repo_name: repo_name
98 :param cs: given changeset
100 :param cs: given changeset
99 :param path: path to lookup
101 :param path: path to lookup
100 """
102 """
101
103
102 try:
104 try:
103 file_node = cs.get_node(path)
105 file_node = cs.get_node(path)
104 if file_node.is_dir():
106 if file_node.is_dir():
105 raise RepositoryError('given path is a directory')
107 raise RepositoryError('given path is a directory')
106 except RepositoryError, e:
108 except RepositoryError, e:
107 h.flash(str(e), category='warning')
109 h.flash(str(e), category='warning')
108 redirect(h.url('files_home', repo_name=repo_name,
110 redirect(h.url('files_home', repo_name=repo_name,
109 revision=cs.raw_id))
111 revision=cs.raw_id))
110
112
111 return file_node
113 return file_node
112
114
113 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
115 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
114 'repository.admin')
116 'repository.admin')
115 def index(self, repo_name, revision, f_path, annotate=False):
117 def index(self, repo_name, revision, f_path, annotate=False):
116 # redirect to given revision from form if given
118 # redirect to given revision from form if given
117 post_revision = request.POST.get('at_rev', None)
119 post_revision = request.POST.get('at_rev', None)
118 if post_revision:
120 if post_revision:
119 cs = self.__get_cs_or_redirect(post_revision, repo_name)
121 cs = self.__get_cs_or_redirect(post_revision, repo_name)
120 redirect(url('files_home', repo_name=c.repo_name,
122 redirect(url('files_home', repo_name=c.repo_name,
121 revision=cs.raw_id, f_path=f_path))
123 revision=cs.raw_id, f_path=f_path))
122
124
123 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
125 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
124 c.branch = request.GET.get('branch', None)
126 c.branch = request.GET.get('branch', None)
125 c.f_path = f_path
127 c.f_path = f_path
126 c.annotate = annotate
128 c.annotate = annotate
127 cur_rev = c.changeset.revision
129 cur_rev = c.changeset.revision
128
130
129 # prev link
131 # prev link
130 try:
132 try:
131 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
133 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
132 c.url_prev = url('files_home', repo_name=c.repo_name,
134 c.url_prev = url('files_home', repo_name=c.repo_name,
133 revision=prev_rev.raw_id, f_path=f_path)
135 revision=prev_rev.raw_id, f_path=f_path)
134 if c.branch:
136 if c.branch:
135 c.url_prev += '?branch=%s' % c.branch
137 c.url_prev += '?branch=%s' % c.branch
136 except (ChangesetDoesNotExistError, VCSError):
138 except (ChangesetDoesNotExistError, VCSError):
137 c.url_prev = '#'
139 c.url_prev = '#'
138
140
139 # next link
141 # next link
140 try:
142 try:
141 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
143 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
142 c.url_next = url('files_home', repo_name=c.repo_name,
144 c.url_next = url('files_home', repo_name=c.repo_name,
143 revision=next_rev.raw_id, f_path=f_path)
145 revision=next_rev.raw_id, f_path=f_path)
144 if c.branch:
146 if c.branch:
145 c.url_next += '?branch=%s' % c.branch
147 c.url_next += '?branch=%s' % c.branch
146 except (ChangesetDoesNotExistError, VCSError):
148 except (ChangesetDoesNotExistError, VCSError):
147 c.url_next = '#'
149 c.url_next = '#'
148
150
149 # files or dirs
151 # files or dirs
150 try:
152 try:
151 c.file = c.changeset.get_node(f_path)
153 c.file = c.changeset.get_node(f_path)
152
154
153 if c.file.is_file():
155 if c.file.is_file():
154 c.file_history = self._get_node_history(c.changeset, f_path)
156 c.file_history = self._get_node_history(c.changeset, f_path)
155 else:
157 else:
156 c.file_history = []
158 c.file_history = []
157 except RepositoryError, e:
159 except RepositoryError, e:
158 h.flash(str(e), category='warning')
160 h.flash(str(e), category='warning')
159 redirect(h.url('files_home', repo_name=repo_name,
161 redirect(h.url('files_home', repo_name=repo_name,
160 revision=revision))
162 revision=revision))
161
163
162 return render('files/files.html')
164 return render('files/files.html')
163
165
164 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
166 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
165 'repository.admin')
167 'repository.admin')
166 def rawfile(self, repo_name, revision, f_path):
168 def rawfile(self, repo_name, revision, f_path):
167 cs = self.__get_cs_or_redirect(revision, repo_name)
169 cs = self.__get_cs_or_redirect(revision, repo_name)
168 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
170 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
169
171
170 response.content_disposition = 'attachment; filename=%s' % \
172 response.content_disposition = 'attachment; filename=%s' % \
171 safe_str(f_path.split(os.sep)[-1])
173 safe_str(f_path.split(Repository.url_sep())[-1])
172
174
173 response.content_type = file_node.mimetype
175 response.content_type = file_node.mimetype
174 return file_node.content
176 return file_node.content
175
177
176 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
178 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
177 'repository.admin')
179 'repository.admin')
178 def raw(self, repo_name, revision, f_path):
180 def raw(self, repo_name, revision, f_path):
179 cs = self.__get_cs_or_redirect(revision, repo_name)
181 cs = self.__get_cs_or_redirect(revision, repo_name)
180 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
182 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
181
183
182 raw_mimetype_mapping = {
184 raw_mimetype_mapping = {
183 # map original mimetype to a mimetype used for "show as raw"
185 # map original mimetype to a mimetype used for "show as raw"
184 # you can also provide a content-disposition to override the
186 # you can also provide a content-disposition to override the
185 # default "attachment" disposition.
187 # default "attachment" disposition.
186 # orig_type: (new_type, new_dispo)
188 # orig_type: (new_type, new_dispo)
187
189
188 # show images inline:
190 # show images inline:
189 'image/x-icon': ('image/x-icon', 'inline'),
191 'image/x-icon': ('image/x-icon', 'inline'),
190 'image/png': ('image/png', 'inline'),
192 'image/png': ('image/png', 'inline'),
191 'image/gif': ('image/gif', 'inline'),
193 'image/gif': ('image/gif', 'inline'),
192 'image/jpeg': ('image/jpeg', 'inline'),
194 'image/jpeg': ('image/jpeg', 'inline'),
193 'image/svg+xml': ('image/svg+xml', 'inline'),
195 'image/svg+xml': ('image/svg+xml', 'inline'),
194 }
196 }
195
197
196 mimetype = file_node.mimetype
198 mimetype = file_node.mimetype
197 try:
199 try:
198 mimetype, dispo = raw_mimetype_mapping[mimetype]
200 mimetype, dispo = raw_mimetype_mapping[mimetype]
199 except KeyError:
201 except KeyError:
200 # we don't know anything special about this, handle it safely
202 # we don't know anything special about this, handle it safely
201 if file_node.is_binary:
203 if file_node.is_binary:
202 # do same as download raw for binary files
204 # do same as download raw for binary files
203 mimetype, dispo = 'application/octet-stream', 'attachment'
205 mimetype, dispo = 'application/octet-stream', 'attachment'
204 else:
206 else:
205 # do not just use the original mimetype, but force text/plain,
207 # do not just use the original mimetype, but force text/plain,
206 # otherwise it would serve text/html and that might be unsafe.
208 # otherwise it would serve text/html and that might be unsafe.
207 # Note: underlying vcs library fakes text/plain mimetype if the
209 # Note: underlying vcs library fakes text/plain mimetype if the
208 # mimetype can not be determined and it thinks it is not
210 # mimetype can not be determined and it thinks it is not
209 # binary.This might lead to erroneous text display in some
211 # binary.This might lead to erroneous text display in some
210 # cases, but helps in other cases, like with text files
212 # cases, but helps in other cases, like with text files
211 # without extension.
213 # without extension.
212 mimetype, dispo = 'text/plain', 'inline'
214 mimetype, dispo = 'text/plain', 'inline'
213
215
214 if dispo == 'attachment':
216 if dispo == 'attachment':
215 dispo = 'attachment; filename=%s' % \
217 dispo = 'attachment; filename=%s' % \
216 safe_str(f_path.split(os.sep)[-1])
218 safe_str(f_path.split(os.sep)[-1])
217
219
218 response.content_disposition = dispo
220 response.content_disposition = dispo
219 response.content_type = mimetype
221 response.content_type = mimetype
220 return file_node.content
222 return file_node.content
221
223
222 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
224 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
223 def edit(self, repo_name, revision, f_path):
225 def edit(self, repo_name, revision, f_path):
224 r_post = request.POST
226 r_post = request.POST
225
227
226 c.cs = self.__get_cs_or_redirect(revision, repo_name)
228 c.cs = self.__get_cs_or_redirect(revision, repo_name)
227 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
229 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
228
230
229 if c.file.is_binary:
231 if c.file.is_binary:
230 return redirect(url('files_home', repo_name=c.repo_name,
232 return redirect(url('files_home', repo_name=c.repo_name,
231 revision=c.cs.raw_id, f_path=f_path))
233 revision=c.cs.raw_id, f_path=f_path))
232
234
233 c.f_path = f_path
235 c.f_path = f_path
234
236
235 if r_post:
237 if r_post:
236
238
237 old_content = c.file.content
239 old_content = c.file.content
238 sl = old_content.splitlines(1)
240 sl = old_content.splitlines(1)
239 first_line = sl[0] if sl else ''
241 first_line = sl[0] if sl else ''
240 # modes: 0 - Unix, 1 - Mac, 2 - DOS
242 # modes: 0 - Unix, 1 - Mac, 2 - DOS
241 mode = detect_mode(first_line, 0)
243 mode = detect_mode(first_line, 0)
242 content = convert_line_endings(r_post.get('content'), mode)
244 content = convert_line_endings(r_post.get('content'), mode)
243
245
244 message = r_post.get('message') or (_('Edited %s via RhodeCode')
246 message = r_post.get('message') or (_('Edited %s via RhodeCode')
245 % (f_path))
247 % (f_path))
246 author = self.rhodecode_user.full_contact
248 author = self.rhodecode_user.full_contact
247
249
248 if content == old_content:
250 if content == old_content:
249 h.flash(_('No changes'),
251 h.flash(_('No changes'),
250 category='warning')
252 category='warning')
251 return redirect(url('changeset_home', repo_name=c.repo_name,
253 return redirect(url('changeset_home', repo_name=c.repo_name,
252 revision='tip'))
254 revision='tip'))
253
255
254 try:
256 try:
255 self.scm_model.commit_change(repo=c.rhodecode_repo,
257 self.scm_model.commit_change(repo=c.rhodecode_repo,
256 repo_name=repo_name, cs=c.cs,
258 repo_name=repo_name, cs=c.cs,
257 user=self.rhodecode_user,
259 user=self.rhodecode_user,
258 author=author, message=message,
260 author=author, message=message,
259 content=content, f_path=f_path)
261 content=content, f_path=f_path)
260 h.flash(_('Successfully committed to %s' % f_path),
262 h.flash(_('Successfully committed to %s' % f_path),
261 category='success')
263 category='success')
262
264
263 except Exception:
265 except Exception:
264 log.error(traceback.format_exc())
266 log.error(traceback.format_exc())
265 h.flash(_('Error occurred during commit'), category='error')
267 h.flash(_('Error occurred during commit'), category='error')
266 return redirect(url('changeset_home',
268 return redirect(url('changeset_home',
267 repo_name=c.repo_name, revision='tip'))
269 repo_name=c.repo_name, revision='tip'))
268
270
269 return render('files/files_edit.html')
271 return render('files/files_edit.html')
270
272
271 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
273 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
272 def add(self, repo_name, revision, f_path):
274 def add(self, repo_name, revision, f_path):
273 r_post = request.POST
275 r_post = request.POST
274 c.cs = self.__get_cs_or_redirect(revision, repo_name,
276 c.cs = self.__get_cs_or_redirect(revision, repo_name,
275 redirect_after=False)
277 redirect_after=False)
276 if c.cs is None:
278 if c.cs is None:
277 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
279 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
278
280
279 c.f_path = f_path
281 c.f_path = f_path
280
282
281 if r_post:
283 if r_post:
282 unix_mode = 0
284 unix_mode = 0
283 content = convert_line_endings(r_post.get('content'), unix_mode)
285 content = convert_line_endings(r_post.get('content'), unix_mode)
284
286
285 message = r_post.get('message') or (_('Added %s via RhodeCode')
287 message = r_post.get('message') or (_('Added %s via RhodeCode')
286 % (f_path))
288 % (f_path))
287 location = r_post.get('location')
289 location = r_post.get('location')
288 filename = r_post.get('filename')
290 filename = r_post.get('filename')
289 file_obj = r_post.get('upload_file', None)
291 file_obj = r_post.get('upload_file', None)
290
292
291 if file_obj is not None and hasattr(file_obj, 'filename'):
293 if file_obj is not None and hasattr(file_obj, 'filename'):
292 filename = file_obj.filename
294 filename = file_obj.filename
293 content = file_obj.file
295 content = file_obj.file
294
296
295 node_path = os.path.join(location, filename)
297 node_path = os.path.join(location, filename)
296 author = self.rhodecode_user.full_contact
298 author = self.rhodecode_user.full_contact
297
299
298 if not content:
300 if not content:
299 h.flash(_('No content'), category='warning')
301 h.flash(_('No content'), category='warning')
300 return redirect(url('changeset_home', repo_name=c.repo_name,
302 return redirect(url('changeset_home', repo_name=c.repo_name,
301 revision='tip'))
303 revision='tip'))
302 if not filename:
304 if not filename:
303 h.flash(_('No filename'), category='warning')
305 h.flash(_('No filename'), category='warning')
304 return redirect(url('changeset_home', repo_name=c.repo_name,
306 return redirect(url('changeset_home', repo_name=c.repo_name,
305 revision='tip'))
307 revision='tip'))
306
308
307 try:
309 try:
308 self.scm_model.create_node(repo=c.rhodecode_repo,
310 self.scm_model.create_node(repo=c.rhodecode_repo,
309 repo_name=repo_name, cs=c.cs,
311 repo_name=repo_name, cs=c.cs,
310 user=self.rhodecode_user,
312 user=self.rhodecode_user,
311 author=author, message=message,
313 author=author, message=message,
312 content=content, f_path=node_path)
314 content=content, f_path=node_path)
313 h.flash(_('Successfully committed to %s' % node_path),
315 h.flash(_('Successfully committed to %s' % node_path),
314 category='success')
316 category='success')
315 except NodeAlreadyExistsError, e:
317 except NodeAlreadyExistsError, e:
316 h.flash(_(e), category='error')
318 h.flash(_(e), category='error')
317 except Exception:
319 except Exception:
318 log.error(traceback.format_exc())
320 log.error(traceback.format_exc())
319 h.flash(_('Error occurred during commit'), category='error')
321 h.flash(_('Error occurred during commit'), category='error')
320 return redirect(url('changeset_home',
322 return redirect(url('changeset_home',
321 repo_name=c.repo_name, revision='tip'))
323 repo_name=c.repo_name, revision='tip'))
322
324
323 return render('files/files_add.html')
325 return render('files/files_add.html')
324
326
325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
327 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 'repository.admin')
328 'repository.admin')
327 def archivefile(self, repo_name, fname):
329 def archivefile(self, repo_name, fname):
328
330
329 fileformat = None
331 fileformat = None
330 revision = None
332 revision = None
331 ext = None
333 ext = None
332 subrepos = request.GET.get('subrepos') == 'true'
334 subrepos = request.GET.get('subrepos') == 'true'
333
335
334 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
336 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
335 archive_spec = fname.split(ext_data[1])
337 archive_spec = fname.split(ext_data[1])
336 if len(archive_spec) == 2 and archive_spec[1] == '':
338 if len(archive_spec) == 2 and archive_spec[1] == '':
337 fileformat = a_type or ext_data[1]
339 fileformat = a_type or ext_data[1]
338 revision = archive_spec[0]
340 revision = archive_spec[0]
339 ext = ext_data[1]
341 ext = ext_data[1]
340
342
341 try:
343 try:
342 dbrepo = RepoModel().get_by_repo_name(repo_name)
344 dbrepo = RepoModel().get_by_repo_name(repo_name)
343 if dbrepo.enable_downloads is False:
345 if dbrepo.enable_downloads is False:
344 return _('downloads disabled')
346 return _('downloads disabled')
345
347
346 if c.rhodecode_repo.alias == 'hg':
348 if c.rhodecode_repo.alias == 'hg':
347 # patch and reset hooks section of UI config to not run any
349 # patch and reset hooks section of UI config to not run any
348 # hooks on fetching archives with subrepos
350 # hooks on fetching archives with subrepos
349 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
351 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
350 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
352 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
351
353
352 cs = c.rhodecode_repo.get_changeset(revision)
354 cs = c.rhodecode_repo.get_changeset(revision)
353 content_type = settings.ARCHIVE_SPECS[fileformat][0]
355 content_type = settings.ARCHIVE_SPECS[fileformat][0]
354 except ChangesetDoesNotExistError:
356 except ChangesetDoesNotExistError:
355 return _('Unknown revision %s') % revision
357 return _('Unknown revision %s') % revision
356 except EmptyRepositoryError:
358 except EmptyRepositoryError:
357 return _('Empty repository')
359 return _('Empty repository')
358 except (ImproperArchiveTypeError, KeyError):
360 except (ImproperArchiveTypeError, KeyError):
359 return _('Unknown archive type')
361 return _('Unknown archive type')
360
362
363 archive = tempfile.NamedTemporaryFile(mode='w+r+b', delete=False)
364 cs.fill_archive(stream=archive, kind=fileformat, subrepos=subrepos)
365 archive.close()
361 response.content_type = content_type
366 response.content_type = content_type
362 response.content_disposition = 'attachment; filename=%s-%s%s' \
367 response.content_disposition = 'attachment; filename=%s-%s%s' \
363 % (repo_name, revision, ext)
368 % (repo_name, revision[:12], ext)
364
369 response.content_length = str(os.path.getsize(archive.name))
365 import tempfile
366 archive = tempfile.mkstemp()[1]
367 t = open(archive, 'wb')
368 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
369
370
370 def get_chunked_archive(archive):
371 def get_chunked_archive(tmpfile):
371 stream = open(archive, 'rb')
372 while True:
372 while True:
373 data = stream.read(4096)
373 data = tmpfile.read(16 * 1024)
374 if not data:
374 if not data:
375 os.remove(archive)
375 tmpfile.close()
376 os.unlink(tmpfile.name)
376 break
377 break
377 yield data
378 yield data
378
379 return get_chunked_archive(tmpfile=open(archive.name,'rb'))
379 return get_chunked_archive(archive)
380
380
381 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
381 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
382 'repository.admin')
382 'repository.admin')
383 def diff(self, repo_name, f_path):
383 def diff(self, repo_name, f_path):
384 ignore_whitespace = request.GET.get('ignorews') == '1'
384 ignore_whitespace = request.GET.get('ignorews') == '1'
385 line_context = request.GET.get('context', 3)
385 line_context = request.GET.get('context', 3)
386 diff1 = request.GET.get('diff1', '')
386 diff1 = request.GET.get('diff1', '')
387 diff2 = request.GET.get('diff2', '')
387 diff2 = request.GET.get('diff2', '')
388 c.action = request.GET.get('diff')
388 c.action = request.GET.get('diff')
389 c.no_changes = diff1 == diff2
389 c.no_changes = diff1 == diff2
390 c.f_path = f_path
390 c.f_path = f_path
391 c.big_diff = False
391 c.big_diff = False
392 c.anchor_url = anchor_url
392 c.anchor_url = anchor_url
393 c.ignorews_url = _ignorews_url
393 c.ignorews_url = _ignorews_url
394 c.context_url = _context_url
394 c.context_url = _context_url
395 c.changes = OrderedDict()
395 c.changes = OrderedDict()
396 c.changes[diff2] = []
396 c.changes[diff2] = []
397 try:
397 try:
398 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
398 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
399 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
399 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
400 node1 = c.changeset_1.get_node(f_path)
400 node1 = c.changeset_1.get_node(f_path)
401 else:
401 else:
402 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
402 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
403 node1 = FileNode('.', '', changeset=c.changeset_1)
403 node1 = FileNode('.', '', changeset=c.changeset_1)
404
404
405 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
405 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
406 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
406 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
407 node2 = c.changeset_2.get_node(f_path)
407 node2 = c.changeset_2.get_node(f_path)
408 else:
408 else:
409 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
409 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
410 node2 = FileNode('.', '', changeset=c.changeset_2)
410 node2 = FileNode('.', '', changeset=c.changeset_2)
411 except RepositoryError:
411 except RepositoryError:
412 return redirect(url('files_home', repo_name=c.repo_name,
412 return redirect(url('files_home', repo_name=c.repo_name,
413 f_path=f_path))
413 f_path=f_path))
414
414
415 if c.action == 'download':
415 if c.action == 'download':
416 _diff = diffs.get_gitdiff(node1, node2,
416 _diff = diffs.get_gitdiff(node1, node2,
417 ignore_whitespace=ignore_whitespace,
417 ignore_whitespace=ignore_whitespace,
418 context=line_context)
418 context=line_context)
419 diff = diffs.DiffProcessor(_diff, format='gitdiff')
419 diff = diffs.DiffProcessor(_diff, format='gitdiff')
420
420
421 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
421 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
422 response.content_type = 'text/plain'
422 response.content_type = 'text/plain'
423 response.content_disposition = (
423 response.content_disposition = (
424 'attachment; filename=%s' % diff_name
424 'attachment; filename=%s' % diff_name
425 )
425 )
426 return diff.raw_diff()
426 return diff.raw_diff()
427
427
428 elif c.action == 'raw':
428 elif c.action == 'raw':
429 _diff = diffs.get_gitdiff(node1, node2,
429 _diff = diffs.get_gitdiff(node1, node2,
430 ignore_whitespace=ignore_whitespace,
430 ignore_whitespace=ignore_whitespace,
431 context=line_context)
431 context=line_context)
432 diff = diffs.DiffProcessor(_diff, format='gitdiff')
432 diff = diffs.DiffProcessor(_diff, format='gitdiff')
433 response.content_type = 'text/plain'
433 response.content_type = 'text/plain'
434 return diff.raw_diff()
434 return diff.raw_diff()
435
435
436 else:
436 else:
437 fid = h.FID(diff2, node2.path)
437 fid = h.FID(diff2, node2.path)
438 line_context_lcl = get_line_ctx(fid, request.GET)
438 line_context_lcl = get_line_ctx(fid, request.GET)
439 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
439 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
440
440
441 lim = request.GET.get('fulldiff') or self.cut_off_limit
441 lim = request.GET.get('fulldiff') or self.cut_off_limit
442 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
442 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
443 filenode_new=node2,
443 filenode_new=node2,
444 cut_off_limit=lim,
444 cut_off_limit=lim,
445 ignore_whitespace=ign_whitespace_lcl,
445 ignore_whitespace=ign_whitespace_lcl,
446 line_context=line_context_lcl,
446 line_context=line_context_lcl,
447 enable_comments=False)
447 enable_comments=False)
448
448
449 c.changes = [('', node2, diff, cs1, cs2, st,)]
449 c.changes = [('', node2, diff, cs1, cs2, st,)]
450
450
451 return render('files/file_diff.html')
451 return render('files/file_diff.html')
452
452
453 def _get_node_history(self, cs, f_path):
453 def _get_node_history(self, cs, f_path):
454 changesets = cs.get_file_history(f_path)
454 changesets = cs.get_file_history(f_path)
455 hist_l = []
455 hist_l = []
456
456
457 changesets_group = ([], _("Changesets"))
457 changesets_group = ([], _("Changesets"))
458 branches_group = ([], _("Branches"))
458 branches_group = ([], _("Branches"))
459 tags_group = ([], _("Tags"))
459 tags_group = ([], _("Tags"))
460 _hg = cs.repository.alias == 'hg'
460 _hg = cs.repository.alias == 'hg'
461 for chs in changesets:
461 for chs in changesets:
462 _branch = '(%s)' % chs.branch if _hg else ''
462 _branch = '(%s)' % chs.branch if _hg else ''
463 n_desc = 'r%s:%s %s' % (chs.revision, chs.short_id, _branch)
463 n_desc = 'r%s:%s %s' % (chs.revision, chs.short_id, _branch)
464 changesets_group[0].append((chs.raw_id, n_desc,))
464 changesets_group[0].append((chs.raw_id, n_desc,))
465
465
466 hist_l.append(changesets_group)
466 hist_l.append(changesets_group)
467
467
468 for name, chs in c.rhodecode_repo.branches.items():
468 for name, chs in c.rhodecode_repo.branches.items():
469 branches_group[0].append((chs, name),)
469 branches_group[0].append((chs, name),)
470 hist_l.append(branches_group)
470 hist_l.append(branches_group)
471
471
472 for name, chs in c.rhodecode_repo.tags.items():
472 for name, chs in c.rhodecode_repo.tags.items():
473 tags_group[0].append((chs, name),)
473 tags_group[0].append((chs, name),)
474 hist_l.append(tags_group)
474 hist_l.append(tags_group)
475
475
476 return hist_l
476 return hist_l
477
477
478 @jsonify
478 @jsonify
479 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
479 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
480 'repository.admin')
480 'repository.admin')
481 def nodelist(self, repo_name, revision, f_path):
481 def nodelist(self, repo_name, revision, f_path):
482 if request.environ.get('HTTP_X_PARTIAL_XHR'):
482 if request.environ.get('HTTP_X_PARTIAL_XHR'):
483 cs = self.__get_cs_or_redirect(revision, repo_name)
483 cs = self.__get_cs_or_redirect(revision, repo_name)
484 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
484 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
485 flat=False)
485 flat=False)
486 return _d + _f
486 return _d + _f
@@ -1,237 +1,240 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.summary
3 rhodecode.controllers.summary
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Summary controller for Rhodecode
6 Summary controller for Rhodecode
7
7
8 :created_on: Apr 18, 2010
8 :created_on: Apr 18, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import traceback
26 import traceback
27 import calendar
27 import calendar
28 import logging
28 import logging
29 import urllib
29 import urllib
30 from time import mktime
30 from time import mktime
31 from datetime import timedelta, date
31 from datetime import timedelta, date
32 from urlparse import urlparse
32 from urlparse import urlparse
33 from rhodecode.lib.compat import product
33 from rhodecode.lib.compat import product
34
34
35 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
35 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
36 NodeDoesNotExistError
36 NodeDoesNotExistError
37
37
38 from pylons import tmpl_context as c, request, url, config
38 from pylons import tmpl_context as c, request, url, config
39 from pylons.i18n.translation import _
39 from pylons.i18n.translation import _
40
40
41 from beaker.cache import cache_region, region_invalidate
41 from beaker.cache import cache_region, region_invalidate
42
42
43 from rhodecode.config.conf import ALL_READMES, ALL_EXTS, LANGUAGES_EXTENSIONS_MAP
43 from rhodecode.config.conf import ALL_READMES, ALL_EXTS, LANGUAGES_EXTENSIONS_MAP
44 from rhodecode.model.db import Statistics, CacheInvalidation
44 from rhodecode.model.db import Statistics, CacheInvalidation
45 from rhodecode.lib.utils2 import safe_unicode
45 from rhodecode.lib.utils2 import safe_unicode
46 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
46 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
47 from rhodecode.lib.base import BaseRepoController, render
47 from rhodecode.lib.base import BaseRepoController, render
48 from rhodecode.lib.utils import EmptyChangeset
48 from rhodecode.lib.utils import EmptyChangeset
49 from rhodecode.lib.markup_renderer import MarkupRenderer
49 from rhodecode.lib.markup_renderer import MarkupRenderer
50 from rhodecode.lib.celerylib import run_task
50 from rhodecode.lib.celerylib import run_task
51 from rhodecode.lib.celerylib.tasks import get_commits_stats
51 from rhodecode.lib.celerylib.tasks import get_commits_stats
52 from rhodecode.lib.helpers import RepoPage
52 from rhodecode.lib.helpers import RepoPage
53 from rhodecode.lib.compat import json, OrderedDict
53 from rhodecode.lib.compat import json, OrderedDict
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57 README_FILES = [''.join([x[0][0], x[1][0]]) for x in
57 README_FILES = [''.join([x[0][0], x[1][0]]) for x in
58 sorted(list(product(ALL_READMES, ALL_EXTS)),
58 sorted(list(product(ALL_READMES, ALL_EXTS)),
59 key=lambda y:y[0][1] + y[1][1])]
59 key=lambda y:y[0][1] + y[1][1])]
60
60
61
61
62 class SummaryController(BaseRepoController):
62 class SummaryController(BaseRepoController):
63
63
64 @LoginRequired()
64 @LoginRequired()
65 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
65 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
66 'repository.admin')
66 'repository.admin')
67 def __before__(self):
67 def __before__(self):
68 super(SummaryController, self).__before__()
68 super(SummaryController, self).__before__()
69
69
70 def index(self, repo_name):
70 def index(self, repo_name):
71 c.dbrepo = dbrepo = c.rhodecode_db_repo
71 c.dbrepo = dbrepo = c.rhodecode_db_repo
72 c.following = self.scm_model.is_following_repo(repo_name,
72 c.following = self.scm_model.is_following_repo(repo_name,
73 self.rhodecode_user.user_id)
73 self.rhodecode_user.user_id)
74
74
75 def url_generator(**kw):
75 def url_generator(**kw):
76 return url('shortlog_home', repo_name=repo_name, size=10, **kw)
76 return url('shortlog_home', repo_name=repo_name, size=10, **kw)
77
77
78 c.repo_changesets = RepoPage(c.rhodecode_repo, page=1,
78 c.repo_changesets = RepoPage(c.rhodecode_repo, page=1,
79 items_per_page=10, url=url_generator)
79 items_per_page=10, url=url_generator)
80
80
81 if self.rhodecode_user.username == 'default':
81 if self.rhodecode_user.username == 'default':
82 # for default(anonymous) user we don't need to pass credentials
82 # for default(anonymous) user we don't need to pass credentials
83 username = ''
83 username = ''
84 password = ''
84 password = ''
85 else:
85 else:
86 username = str(self.rhodecode_user.username)
86 username = str(self.rhodecode_user.username)
87 password = '@'
87 password = '@'
88
88
89 parsed_url = urlparse(url.current(qualified=True))
89 parsed_url = urlparse(url.current(qualified=True))
90
90
91 default_clone_uri = '{scheme}://{user}{pass}{netloc}{path}'
91 default_clone_uri = '{scheme}://{user}{pass}{netloc}{path}'
92
92
93 uri_tmpl = config.get('clone_uri', default_clone_uri)
93 uri_tmpl = config.get('clone_uri', default_clone_uri)
94 uri_tmpl = uri_tmpl.replace('{', '%(').replace('}', ')s')
94 uri_tmpl = uri_tmpl.replace('{', '%(').replace('}', ')s')
95 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
95 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
96 uri_dict = {
96 uri_dict = {
97 'user': username,
97 'user': username,
98 'pass': password,
98 'pass': password,
99 'scheme': parsed_url.scheme,
99 'scheme': parsed_url.scheme,
100 'netloc': parsed_url.netloc,
100 'netloc': parsed_url.netloc,
101 'path': decoded_path
101 'path': decoded_path
102 }
102 }
103
103
104 uri = uri_tmpl % uri_dict
104 uri = uri_tmpl % uri_dict
105 # generate another clone url by id
105 # generate another clone url by id
106 uri_dict.update(
106 uri_dict.update(
107 {'path': decoded_path.replace(repo_name, '_%s' % c.dbrepo.repo_id)}
107 {'path': decoded_path.replace(repo_name, '_%s' % c.dbrepo.repo_id)}
108 )
108 )
109 uri_id = uri_tmpl % uri_dict
109 uri_id = uri_tmpl % uri_dict
110
110
111 c.clone_repo_url = uri
111 c.clone_repo_url = uri
112 c.clone_repo_url_id = uri_id
112 c.clone_repo_url_id = uri_id
113 c.repo_tags = OrderedDict()
113 c.repo_tags = OrderedDict()
114 for name, hash_ in c.rhodecode_repo.tags.items()[:10]:
114 for name, hash_ in c.rhodecode_repo.tags.items()[:10]:
115 try:
115 try:
116 c.repo_tags[name] = c.rhodecode_repo.get_changeset(hash_)
116 c.repo_tags[name] = c.rhodecode_repo.get_changeset(hash_)
117 except ChangesetError:
117 except ChangesetError:
118 c.repo_tags[name] = EmptyChangeset(hash_)
118 c.repo_tags[name] = EmptyChangeset(hash_)
119
119
120 c.repo_branches = OrderedDict()
120 c.repo_branches = OrderedDict()
121 for name, hash_ in c.rhodecode_repo.branches.items()[:10]:
121 for name, hash_ in c.rhodecode_repo.branches.items()[:10]:
122 try:
122 try:
123 c.repo_branches[name] = c.rhodecode_repo.get_changeset(hash_)
123 c.repo_branches[name] = c.rhodecode_repo.get_changeset(hash_)
124 except ChangesetError:
124 except ChangesetError:
125 c.repo_branches[name] = EmptyChangeset(hash_)
125 c.repo_branches[name] = EmptyChangeset(hash_)
126
126
127 td = date.today() + timedelta(days=1)
127 td = date.today() + timedelta(days=1)
128 td_1m = td - timedelta(days=calendar.mdays[td.month])
128 td_1m = td - timedelta(days=calendar.mdays[td.month])
129 td_1y = td - timedelta(days=365)
129 td_1y = td - timedelta(days=365)
130
130
131 ts_min_m = mktime(td_1m.timetuple())
131 ts_min_m = mktime(td_1m.timetuple())
132 ts_min_y = mktime(td_1y.timetuple())
132 ts_min_y = mktime(td_1y.timetuple())
133 ts_max_y = mktime(td.timetuple())
133 ts_max_y = mktime(td.timetuple())
134
134
135 if dbrepo.enable_statistics:
135 if dbrepo.enable_statistics:
136 c.show_stats = True
136 c.show_stats = True
137 c.no_data_msg = _('No data loaded yet')
137 c.no_data_msg = _('No data loaded yet')
138 run_task(get_commits_stats, c.dbrepo.repo_name, ts_min_y, ts_max_y)
138 run_task(get_commits_stats, c.dbrepo.repo_name, ts_min_y, ts_max_y)
139 else:
139 else:
140 c.show_stats = False
140 c.show_stats = False
141 c.no_data_msg = _('Statistics are disabled for this repository')
141 c.no_data_msg = _('Statistics are disabled for this repository')
142 c.ts_min = ts_min_m
142 c.ts_min = ts_min_m
143 c.ts_max = ts_max_y
143 c.ts_max = ts_max_y
144
144
145 stats = self.sa.query(Statistics)\
145 stats = self.sa.query(Statistics)\
146 .filter(Statistics.repository == dbrepo)\
146 .filter(Statistics.repository == dbrepo)\
147 .scalar()
147 .scalar()
148
148
149 c.stats_percentage = 0
149 c.stats_percentage = 0
150
150
151 if stats and stats.languages:
151 if stats and stats.languages:
152 c.no_data = False is dbrepo.enable_statistics
152 c.no_data = False is dbrepo.enable_statistics
153 lang_stats_d = json.loads(stats.languages)
153 lang_stats_d = json.loads(stats.languages)
154 c.commit_data = stats.commit_activity
154 c.commit_data = stats.commit_activity
155 c.overview_data = stats.commit_activity_combined
155 c.overview_data = stats.commit_activity_combined
156
156
157 lang_stats = ((x, {"count": y,
157 lang_stats = ((x, {"count": y,
158 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
158 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
159 for x, y in lang_stats_d.items())
159 for x, y in lang_stats_d.items())
160
160
161 c.trending_languages = json.dumps(
161 c.trending_languages = json.dumps(
162 sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
162 sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
163 )
163 )
164 last_rev = stats.stat_on_revision + 1
164 last_rev = stats.stat_on_revision + 1
165 c.repo_last_rev = c.rhodecode_repo.count()\
165 c.repo_last_rev = c.rhodecode_repo.count()\
166 if c.rhodecode_repo.revisions else 0
166 if c.rhodecode_repo.revisions else 0
167 if last_rev == 0 or c.repo_last_rev == 0:
167 if last_rev == 0 or c.repo_last_rev == 0:
168 pass
168 pass
169 else:
169 else:
170 c.stats_percentage = '%.2f' % ((float((last_rev)) /
170 c.stats_percentage = '%.2f' % ((float((last_rev)) /
171 c.repo_last_rev) * 100)
171 c.repo_last_rev) * 100)
172 else:
172 else:
173 c.commit_data = json.dumps({})
173 c.commit_data = json.dumps({})
174 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]])
174 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]])
175 c.trending_languages = json.dumps({})
175 c.trending_languages = json.dumps({})
176 c.no_data = True
176 c.no_data = True
177
177
178 c.enable_downloads = dbrepo.enable_downloads
178 c.enable_downloads = dbrepo.enable_downloads
179 if c.enable_downloads:
179 if c.enable_downloads:
180 c.download_options = self._get_download_links(c.rhodecode_repo)
180 c.download_options = self._get_download_links(c.rhodecode_repo)
181
181
182 c.readme_data, c.readme_file = self.__get_readme_data(c.rhodecode_db_repo)
182 c.readme_data, c.readme_file = self.__get_readme_data(
183 c.rhodecode_db_repo.repo_name, c.rhodecode_repo
184 )
183 return render('summary/summary.html')
185 return render('summary/summary.html')
184
186
185 def __get_readme_data(self, repo):
187 def __get_readme_data(self, repo_name, repo):
186
188
187 @cache_region('long_term')
189 @cache_region('long_term')
188 def _get_readme_from_cache(key):
190 def _get_readme_from_cache(key):
189 readme_data = None
191 readme_data = None
190 readme_file = None
192 readme_file = None
191 log.debug('Fetching readme file')
193 log.debug('Fetching readme file')
192 try:
194 try:
193 cs = repo.get_changeset('tip')
195 cs = repo.get_changeset() # fetches TIP
194 renderer = MarkupRenderer()
196 renderer = MarkupRenderer()
195 for f in README_FILES:
197 for f in README_FILES:
196 try:
198 try:
197 readme = cs.get_node(f)
199 readme = cs.get_node(f)
198 readme_file = f
200 readme_file = f
199 readme_data = renderer.render(readme.content, f)
201 readme_data = renderer.render(readme.content, f)
200 log.debug('Found readme %s' % readme_file)
202 log.debug('Found readme %s' % readme_file)
201 break
203 break
202 except NodeDoesNotExistError:
204 except NodeDoesNotExistError:
203 continue
205 continue
204 except ChangesetError:
206 except ChangesetError:
207 log.error(traceback.format_exc())
205 pass
208 pass
206 except EmptyRepositoryError:
209 except EmptyRepositoryError:
207 pass
210 pass
208 except Exception:
211 except Exception:
209 log.error(traceback.format_exc())
212 log.error(traceback.format_exc())
210
213
211 return readme_data, readme_file
214 return readme_data, readme_file
212
215
213 key = repo.repo_name + '_README'
216 key = repo_name + '_README'
214 inv = CacheInvalidation.invalidate(key)
217 inv = CacheInvalidation.invalidate(key)
215 if inv is not None:
218 if inv is not None:
216 region_invalidate(_get_readme_from_cache, None, key)
219 region_invalidate(_get_readme_from_cache, None, key)
217 CacheInvalidation.set_valid(inv.cache_key)
220 CacheInvalidation.set_valid(inv.cache_key)
218 return _get_readme_from_cache(key)
221 return _get_readme_from_cache(key)
219
222
220 def _get_download_links(self, repo):
223 def _get_download_links(self, repo):
221
224
222 download_l = []
225 download_l = []
223
226
224 branches_group = ([], _("Branches"))
227 branches_group = ([], _("Branches"))
225 tags_group = ([], _("Tags"))
228 tags_group = ([], _("Tags"))
226
229
227 for name, chs in c.rhodecode_repo.branches.items():
230 for name, chs in c.rhodecode_repo.branches.items():
228 #chs = chs.split(':')[-1]
231 #chs = chs.split(':')[-1]
229 branches_group[0].append((chs, name),)
232 branches_group[0].append((chs, name),)
230 download_l.append(branches_group)
233 download_l.append(branches_group)
231
234
232 for name, chs in c.rhodecode_repo.tags.items():
235 for name, chs in c.rhodecode_repo.tags.items():
233 #chs = chs.split(':')[-1]
236 #chs = chs.split(':')[-1]
234 tags_group[0].append((chs, name),)
237 tags_group[0].append((chs, name),)
235 download_l.append(tags_group)
238 download_l.append(tags_group)
236
239
237 return download_l
240 return download_l
@@ -1,821 +1,821 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.auth
3 rhodecode.lib.auth
4 ~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~
5
5
6 authentication and permission libraries
6 authentication and permission libraries
7
7
8 :created_on: Apr 4, 2010
8 :created_on: Apr 4, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import random
26 import random
27 import logging
27 import logging
28 import traceback
28 import traceback
29 import hashlib
29 import hashlib
30
30
31 from tempfile import _RandomNameSequence
31 from tempfile import _RandomNameSequence
32 from decorator import decorator
32 from decorator import decorator
33
33
34 from pylons import config, url, request
34 from pylons import config, url, request
35 from pylons.controllers.util import abort, redirect
35 from pylons.controllers.util import abort, redirect
36 from pylons.i18n.translation import _
36 from pylons.i18n.translation import _
37
37
38 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
38 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
39 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
40
40
41 if __platform__ in PLATFORM_WIN:
41 if __platform__ in PLATFORM_WIN:
42 from hashlib import sha256
42 from hashlib import sha256
43 if __platform__ in PLATFORM_OTHERS:
43 if __platform__ in PLATFORM_OTHERS:
44 import bcrypt
44 import bcrypt
45
45
46 from rhodecode.lib.utils2 import str2bool, safe_unicode
46 from rhodecode.lib.utils2 import str2bool, safe_unicode
47 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
47 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
48 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
48 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
49 from rhodecode.lib.auth_ldap import AuthLdap
49 from rhodecode.lib.auth_ldap import AuthLdap
50
50
51 from rhodecode.model import meta
51 from rhodecode.model import meta
52 from rhodecode.model.user import UserModel
52 from rhodecode.model.user import UserModel
53 from rhodecode.model.db import Permission, RhodeCodeSetting, User
53 from rhodecode.model.db import Permission, RhodeCodeSetting, User
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class PasswordGenerator(object):
58 class PasswordGenerator(object):
59 """
59 """
60 This is a simple class for generating password from different sets of
60 This is a simple class for generating password from different sets of
61 characters
61 characters
62 usage::
62 usage::
63
63
64 passwd_gen = PasswordGenerator()
64 passwd_gen = PasswordGenerator()
65 #print 8-letter password containing only big and small letters
65 #print 8-letter password containing only big and small letters
66 of alphabet
66 of alphabet
67 print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
67 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
68 """
68 """
69 ALPHABETS_NUM = r'''1234567890'''
69 ALPHABETS_NUM = r'''1234567890'''
70 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
70 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
71 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
71 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
72 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
72 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
73 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
73 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
74 + ALPHABETS_NUM + ALPHABETS_SPECIAL
74 + ALPHABETS_NUM + ALPHABETS_SPECIAL
75 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
75 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
76 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
76 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
77 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
77 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
78 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
78 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
79
79
80 def __init__(self, passwd=''):
80 def __init__(self, passwd=''):
81 self.passwd = passwd
81 self.passwd = passwd
82
82
83 def gen_password(self, length, type_=None):
83 def gen_password(self, length, type_=None):
84 if type_ is None:
84 if type_ is None:
85 type_ = self.ALPHABETS_FULL
85 type_ = self.ALPHABETS_FULL
86 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
86 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
87 return self.passwd
87 return self.passwd
88
88
89
89
90 class RhodeCodeCrypto(object):
90 class RhodeCodeCrypto(object):
91
91
92 @classmethod
92 @classmethod
93 def hash_string(cls, str_):
93 def hash_string(cls, str_):
94 """
94 """
95 Cryptographic function used for password hashing based on pybcrypt
95 Cryptographic function used for password hashing based on pybcrypt
96 or pycrypto in windows
96 or pycrypto in windows
97
97
98 :param password: password to hash
98 :param password: password to hash
99 """
99 """
100 if __platform__ in PLATFORM_WIN:
100 if __platform__ in PLATFORM_WIN:
101 return sha256(str_).hexdigest()
101 return sha256(str_).hexdigest()
102 elif __platform__ in PLATFORM_OTHERS:
102 elif __platform__ in PLATFORM_OTHERS:
103 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
103 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
104 else:
104 else:
105 raise Exception('Unknown or unsupported platform %s' \
105 raise Exception('Unknown or unsupported platform %s' \
106 % __platform__)
106 % __platform__)
107
107
108 @classmethod
108 @classmethod
109 def hash_check(cls, password, hashed):
109 def hash_check(cls, password, hashed):
110 """
110 """
111 Checks matching password with it's hashed value, runs different
111 Checks matching password with it's hashed value, runs different
112 implementation based on platform it runs on
112 implementation based on platform it runs on
113
113
114 :param password: password
114 :param password: password
115 :param hashed: password in hashed form
115 :param hashed: password in hashed form
116 """
116 """
117
117
118 if __platform__ in PLATFORM_WIN:
118 if __platform__ in PLATFORM_WIN:
119 return sha256(password).hexdigest() == hashed
119 return sha256(password).hexdigest() == hashed
120 elif __platform__ in PLATFORM_OTHERS:
120 elif __platform__ in PLATFORM_OTHERS:
121 return bcrypt.hashpw(password, hashed) == hashed
121 return bcrypt.hashpw(password, hashed) == hashed
122 else:
122 else:
123 raise Exception('Unknown or unsupported platform %s' \
123 raise Exception('Unknown or unsupported platform %s' \
124 % __platform__)
124 % __platform__)
125
125
126
126
127 def get_crypt_password(password):
127 def get_crypt_password(password):
128 return RhodeCodeCrypto.hash_string(password)
128 return RhodeCodeCrypto.hash_string(password)
129
129
130
130
131 def check_password(password, hashed):
131 def check_password(password, hashed):
132 return RhodeCodeCrypto.hash_check(password, hashed)
132 return RhodeCodeCrypto.hash_check(password, hashed)
133
133
134
134
135 def generate_api_key(str_, salt=None):
135 def generate_api_key(str_, salt=None):
136 """
136 """
137 Generates API KEY from given string
137 Generates API KEY from given string
138
138
139 :param str_:
139 :param str_:
140 :param salt:
140 :param salt:
141 """
141 """
142
142
143 if salt is None:
143 if salt is None:
144 salt = _RandomNameSequence().next()
144 salt = _RandomNameSequence().next()
145
145
146 return hashlib.sha1(str_ + salt).hexdigest()
146 return hashlib.sha1(str_ + salt).hexdigest()
147
147
148
148
149 def authfunc(environ, username, password):
149 def authfunc(environ, username, password):
150 """
150 """
151 Dummy authentication wrapper function used in Mercurial and Git for
151 Dummy authentication wrapper function used in Mercurial and Git for
152 access control.
152 access control.
153
153
154 :param environ: needed only for using in Basic auth
154 :param environ: needed only for using in Basic auth
155 """
155 """
156 return authenticate(username, password)
156 return authenticate(username, password)
157
157
158
158
159 def authenticate(username, password):
159 def authenticate(username, password):
160 """
160 """
161 Authentication function used for access control,
161 Authentication function used for access control,
162 firstly checks for db authentication then if ldap is enabled for ldap
162 firstly checks for db authentication then if ldap is enabled for ldap
163 authentication, also creates ldap user if not in database
163 authentication, also creates ldap user if not in database
164
164
165 :param username: username
165 :param username: username
166 :param password: password
166 :param password: password
167 """
167 """
168
168
169 user_model = UserModel()
169 user_model = UserModel()
170 user = User.get_by_username(username)
170 user = User.get_by_username(username)
171
171
172 log.debug('Authenticating user using RhodeCode account')
172 log.debug('Authenticating user using RhodeCode account')
173 if user is not None and not user.ldap_dn:
173 if user is not None and not user.ldap_dn:
174 if user.active:
174 if user.active:
175 if user.username == 'default' and user.active:
175 if user.username == 'default' and user.active:
176 log.info('user %s authenticated correctly as anonymous user' %
176 log.info('user %s authenticated correctly as anonymous user' %
177 username)
177 username)
178 return True
178 return True
179
179
180 elif user.username == username and check_password(password,
180 elif user.username == username and check_password(password,
181 user.password):
181 user.password):
182 log.info('user %s authenticated correctly' % username)
182 log.info('user %s authenticated correctly' % username)
183 return True
183 return True
184 else:
184 else:
185 log.warning('user %s tried auth but is disabled' % username)
185 log.warning('user %s tried auth but is disabled' % username)
186
186
187 else:
187 else:
188 log.debug('Regular authentication failed')
188 log.debug('Regular authentication failed')
189 user_obj = User.get_by_username(username, case_insensitive=True)
189 user_obj = User.get_by_username(username, case_insensitive=True)
190
190
191 if user_obj is not None and not user_obj.ldap_dn:
191 if user_obj is not None and not user_obj.ldap_dn:
192 log.debug('this user already exists as non ldap')
192 log.debug('this user already exists as non ldap')
193 return False
193 return False
194
194
195 ldap_settings = RhodeCodeSetting.get_ldap_settings()
195 ldap_settings = RhodeCodeSetting.get_ldap_settings()
196 #======================================================================
196 #======================================================================
197 # FALLBACK TO LDAP AUTH IF ENABLE
197 # FALLBACK TO LDAP AUTH IF ENABLE
198 #======================================================================
198 #======================================================================
199 if str2bool(ldap_settings.get('ldap_active')):
199 if str2bool(ldap_settings.get('ldap_active')):
200 log.debug("Authenticating user using ldap")
200 log.debug("Authenticating user using ldap")
201 kwargs = {
201 kwargs = {
202 'server': ldap_settings.get('ldap_host', ''),
202 'server': ldap_settings.get('ldap_host', ''),
203 'base_dn': ldap_settings.get('ldap_base_dn', ''),
203 'base_dn': ldap_settings.get('ldap_base_dn', ''),
204 'port': ldap_settings.get('ldap_port'),
204 'port': ldap_settings.get('ldap_port'),
205 'bind_dn': ldap_settings.get('ldap_dn_user'),
205 'bind_dn': ldap_settings.get('ldap_dn_user'),
206 'bind_pass': ldap_settings.get('ldap_dn_pass'),
206 'bind_pass': ldap_settings.get('ldap_dn_pass'),
207 'tls_kind': ldap_settings.get('ldap_tls_kind'),
207 'tls_kind': ldap_settings.get('ldap_tls_kind'),
208 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
208 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
209 'ldap_filter': ldap_settings.get('ldap_filter'),
209 'ldap_filter': ldap_settings.get('ldap_filter'),
210 'search_scope': ldap_settings.get('ldap_search_scope'),
210 'search_scope': ldap_settings.get('ldap_search_scope'),
211 'attr_login': ldap_settings.get('ldap_attr_login'),
211 'attr_login': ldap_settings.get('ldap_attr_login'),
212 'ldap_version': 3,
212 'ldap_version': 3,
213 }
213 }
214 log.debug('Checking for ldap authentication')
214 log.debug('Checking for ldap authentication')
215 try:
215 try:
216 aldap = AuthLdap(**kwargs)
216 aldap = AuthLdap(**kwargs)
217 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
217 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
218 password)
218 password)
219 log.debug('Got ldap DN response %s' % user_dn)
219 log.debug('Got ldap DN response %s' % user_dn)
220
220
221 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
221 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
222 .get(k), [''])[0]
222 .get(k), [''])[0]
223
223
224 user_attrs = {
224 user_attrs = {
225 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
225 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
226 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
226 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
227 'email': get_ldap_attr('ldap_attr_email'),
227 'email': get_ldap_attr('ldap_attr_email'),
228 }
228 }
229
229
230 # don't store LDAP password since we don't need it. Override
230 # don't store LDAP password since we don't need it. Override
231 # with some random generated password
231 # with some random generated password
232 _password = PasswordGenerator().gen_password(length=8)
232 _password = PasswordGenerator().gen_password(length=8)
233 # create this user on the fly if it doesn't exist in rhodecode
233 # create this user on the fly if it doesn't exist in rhodecode
234 # database
234 # database
235 if user_model.create_ldap(username, _password, user_dn,
235 if user_model.create_ldap(username, _password, user_dn,
236 user_attrs):
236 user_attrs):
237 log.info('created new ldap user %s' % username)
237 log.info('created new ldap user %s' % username)
238
238
239 Session.commit()
239 Session.commit()
240 return True
240 return True
241 except (LdapUsernameError, LdapPasswordError,):
241 except (LdapUsernameError, LdapPasswordError,):
242 pass
242 pass
243 except (Exception,):
243 except (Exception,):
244 log.error(traceback.format_exc())
244 log.error(traceback.format_exc())
245 pass
245 pass
246 return False
246 return False
247
247
248
248
249 def login_container_auth(username):
249 def login_container_auth(username):
250 user = User.get_by_username(username)
250 user = User.get_by_username(username)
251 if user is None:
251 if user is None:
252 user_attrs = {
252 user_attrs = {
253 'name': username,
253 'name': username,
254 'lastname': None,
254 'lastname': None,
255 'email': None,
255 'email': None,
256 }
256 }
257 user = UserModel().create_for_container_auth(username, user_attrs)
257 user = UserModel().create_for_container_auth(username, user_attrs)
258 if not user:
258 if not user:
259 return None
259 return None
260 log.info('User %s was created by container authentication' % username)
260 log.info('User %s was created by container authentication' % username)
261
261
262 if not user.active:
262 if not user.active:
263 return None
263 return None
264
264
265 user.update_lastlogin()
265 user.update_lastlogin()
266 Session.commit()
266 Session.commit()
267
267
268 log.debug('User %s is now logged in by container authentication',
268 log.debug('User %s is now logged in by container authentication',
269 user.username)
269 user.username)
270 return user
270 return user
271
271
272
272
273 def get_container_username(environ, config):
273 def get_container_username(environ, config):
274 username = None
274 username = None
275
275
276 if str2bool(config.get('container_auth_enabled', False)):
276 if str2bool(config.get('container_auth_enabled', False)):
277 from paste.httpheaders import REMOTE_USER
277 from paste.httpheaders import REMOTE_USER
278 username = REMOTE_USER(environ)
278 username = REMOTE_USER(environ)
279
279
280 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
280 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
281 username = environ.get('HTTP_X_FORWARDED_USER')
281 username = environ.get('HTTP_X_FORWARDED_USER')
282
282
283 if username:
283 if username:
284 # Removing realm and domain from username
284 # Removing realm and domain from username
285 username = username.partition('@')[0]
285 username = username.partition('@')[0]
286 username = username.rpartition('\\')[2]
286 username = username.rpartition('\\')[2]
287 log.debug('Received username %s from container' % username)
287 log.debug('Received username %s from container' % username)
288
288
289 return username
289 return username
290
290
291
291
292 class CookieStoreWrapper(object):
292 class CookieStoreWrapper(object):
293
293
294 def __init__(self, cookie_store):
294 def __init__(self, cookie_store):
295 self.cookie_store = cookie_store
295 self.cookie_store = cookie_store
296
296
297 def __repr__(self):
297 def __repr__(self):
298 return 'CookieStore<%s>' % (self.cookie_store)
298 return 'CookieStore<%s>' % (self.cookie_store)
299
299
300 def get(self, key, other=None):
300 def get(self, key, other=None):
301 if isinstance(self.cookie_store, dict):
301 if isinstance(self.cookie_store, dict):
302 return self.cookie_store.get(key, other)
302 return self.cookie_store.get(key, other)
303 elif isinstance(self.cookie_store, AuthUser):
303 elif isinstance(self.cookie_store, AuthUser):
304 return self.cookie_store.__dict__.get(key, other)
304 return self.cookie_store.__dict__.get(key, other)
305
305
306
306
307 class AuthUser(object):
307 class AuthUser(object):
308 """
308 """
309 A simple object that handles all attributes of user in RhodeCode
309 A simple object that handles all attributes of user in RhodeCode
310
310
311 It does lookup based on API key,given user, or user present in session
311 It does lookup based on API key,given user, or user present in session
312 Then it fills all required information for such user. It also checks if
312 Then it fills all required information for such user. It also checks if
313 anonymous access is enabled and if so, it returns default user as logged
313 anonymous access is enabled and if so, it returns default user as logged
314 in
314 in
315 """
315 """
316
316
317 def __init__(self, user_id=None, api_key=None, username=None):
317 def __init__(self, user_id=None, api_key=None, username=None):
318
318
319 self.user_id = user_id
319 self.user_id = user_id
320 self.api_key = None
320 self.api_key = None
321 self.username = username
321 self.username = username
322
322
323 self.name = ''
323 self.name = ''
324 self.lastname = ''
324 self.lastname = ''
325 self.email = ''
325 self.email = ''
326 self.is_authenticated = False
326 self.is_authenticated = False
327 self.admin = False
327 self.admin = False
328 self.permissions = {}
328 self.permissions = {}
329 self._api_key = api_key
329 self._api_key = api_key
330 self.propagate_data()
330 self.propagate_data()
331 self._instance = None
331 self._instance = None
332
332
333 def propagate_data(self):
333 def propagate_data(self):
334 user_model = UserModel()
334 user_model = UserModel()
335 self.anonymous_user = User.get_by_username('default', cache=True)
335 self.anonymous_user = User.get_by_username('default', cache=True)
336 is_user_loaded = False
336 is_user_loaded = False
337
337
338 # try go get user by api key
338 # try go get user by api key
339 if self._api_key and self._api_key != self.anonymous_user.api_key:
339 if self._api_key and self._api_key != self.anonymous_user.api_key:
340 log.debug('Auth User lookup by API KEY %s' % self._api_key)
340 log.debug('Auth User lookup by API KEY %s' % self._api_key)
341 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
341 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
342 # lookup by userid
342 # lookup by userid
343 elif (self.user_id is not None and
343 elif (self.user_id is not None and
344 self.user_id != self.anonymous_user.user_id):
344 self.user_id != self.anonymous_user.user_id):
345 log.debug('Auth User lookup by USER ID %s' % self.user_id)
345 log.debug('Auth User lookup by USER ID %s' % self.user_id)
346 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
346 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
347 # lookup by username
347 # lookup by username
348 elif self.username and \
348 elif self.username and \
349 str2bool(config.get('container_auth_enabled', False)):
349 str2bool(config.get('container_auth_enabled', False)):
350
350
351 log.debug('Auth User lookup by USER NAME %s' % self.username)
351 log.debug('Auth User lookup by USER NAME %s' % self.username)
352 dbuser = login_container_auth(self.username)
352 dbuser = login_container_auth(self.username)
353 if dbuser is not None:
353 if dbuser is not None:
354 for k, v in dbuser.get_dict().items():
354 for k, v in dbuser.get_dict().items():
355 setattr(self, k, v)
355 setattr(self, k, v)
356 self.set_authenticated()
356 self.set_authenticated()
357 is_user_loaded = True
357 is_user_loaded = True
358 else:
358 else:
359 log.debug('No data in %s that could been used to log in' % self)
359 log.debug('No data in %s that could been used to log in' % self)
360
360
361 if not is_user_loaded:
361 if not is_user_loaded:
362 # if we cannot authenticate user try anonymous
362 # if we cannot authenticate user try anonymous
363 if self.anonymous_user.active is True:
363 if self.anonymous_user.active is True:
364 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
364 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
365 # then we set this user is logged in
365 # then we set this user is logged in
366 self.is_authenticated = True
366 self.is_authenticated = True
367 else:
367 else:
368 self.user_id = None
368 self.user_id = None
369 self.username = None
369 self.username = None
370 self.is_authenticated = False
370 self.is_authenticated = False
371
371
372 if not self.username:
372 if not self.username:
373 self.username = 'None'
373 self.username = 'None'
374
374
375 log.debug('Auth User is now %s' % self)
375 log.debug('Auth User is now %s' % self)
376 user_model.fill_perms(self)
376 user_model.fill_perms(self)
377
377
378 @property
378 @property
379 def is_admin(self):
379 def is_admin(self):
380 return self.admin
380 return self.admin
381
381
382 def __repr__(self):
382 def __repr__(self):
383 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
383 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
384 self.is_authenticated)
384 self.is_authenticated)
385
385
386 def set_authenticated(self, authenticated=True):
386 def set_authenticated(self, authenticated=True):
387 if self.user_id != self.anonymous_user.user_id:
387 if self.user_id != self.anonymous_user.user_id:
388 self.is_authenticated = authenticated
388 self.is_authenticated = authenticated
389
389
390 def get_cookie_store(self):
390 def get_cookie_store(self):
391 return {'username': self.username,
391 return {'username': self.username,
392 'user_id': self.user_id,
392 'user_id': self.user_id,
393 'is_authenticated': self.is_authenticated}
393 'is_authenticated': self.is_authenticated}
394
394
395 @classmethod
395 @classmethod
396 def from_cookie_store(cls, cookie_store):
396 def from_cookie_store(cls, cookie_store):
397 """
397 """
398 Creates AuthUser from a cookie store
398 Creates AuthUser from a cookie store
399
399
400 :param cls:
400 :param cls:
401 :param cookie_store:
401 :param cookie_store:
402 """
402 """
403 user_id = cookie_store.get('user_id')
403 user_id = cookie_store.get('user_id')
404 username = cookie_store.get('username')
404 username = cookie_store.get('username')
405 api_key = cookie_store.get('api_key')
405 api_key = cookie_store.get('api_key')
406 return AuthUser(user_id, api_key, username)
406 return AuthUser(user_id, api_key, username)
407
407
408
408
409 def set_available_permissions(config):
409 def set_available_permissions(config):
410 """
410 """
411 This function will propagate pylons globals with all available defined
411 This function will propagate pylons globals with all available defined
412 permission given in db. We don't want to check each time from db for new
412 permission given in db. We don't want to check each time from db for new
413 permissions since adding a new permission also requires application restart
413 permissions since adding a new permission also requires application restart
414 ie. to decorate new views with the newly created permission
414 ie. to decorate new views with the newly created permission
415
415
416 :param config: current pylons config instance
416 :param config: current pylons config instance
417
417
418 """
418 """
419 log.info('getting information about all available permissions')
419 log.info('getting information about all available permissions')
420 try:
420 try:
421 sa = meta.Session
421 sa = meta.Session
422 all_perms = sa.query(Permission).all()
422 all_perms = sa.query(Permission).all()
423 except Exception:
423 except Exception:
424 pass
424 pass
425 finally:
425 finally:
426 meta.Session.remove()
426 meta.Session.remove()
427
427
428 config['available_permissions'] = [x.permission_name for x in all_perms]
428 config['available_permissions'] = [x.permission_name for x in all_perms]
429
429
430
430
431 #==============================================================================
431 #==============================================================================
432 # CHECK DECORATORS
432 # CHECK DECORATORS
433 #==============================================================================
433 #==============================================================================
434 class LoginRequired(object):
434 class LoginRequired(object):
435 """
435 """
436 Must be logged in to execute this function else
436 Must be logged in to execute this function else
437 redirect to login page
437 redirect to login page
438
438
439 :param api_access: if enabled this checks only for valid auth token
439 :param api_access: if enabled this checks only for valid auth token
440 and grants access based on valid token
440 and grants access based on valid token
441 """
441 """
442
442
443 def __init__(self, api_access=False):
443 def __init__(self, api_access=False):
444 self.api_access = api_access
444 self.api_access = api_access
445
445
446 def __call__(self, func):
446 def __call__(self, func):
447 return decorator(self.__wrapper, func)
447 return decorator(self.__wrapper, func)
448
448
449 def __wrapper(self, func, *fargs, **fkwargs):
449 def __wrapper(self, func, *fargs, **fkwargs):
450 cls = fargs[0]
450 cls = fargs[0]
451 user = cls.rhodecode_user
451 user = cls.rhodecode_user
452
452
453 api_access_ok = False
453 api_access_ok = False
454 if self.api_access:
454 if self.api_access:
455 log.debug('Checking API KEY access for %s' % cls)
455 log.debug('Checking API KEY access for %s' % cls)
456 if user.api_key == request.GET.get('api_key'):
456 if user.api_key == request.GET.get('api_key'):
457 api_access_ok = True
457 api_access_ok = True
458 else:
458 else:
459 log.debug("API KEY token not valid")
459 log.debug("API KEY token not valid")
460 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
460 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
461 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
461 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
462 if user.is_authenticated or api_access_ok:
462 if user.is_authenticated or api_access_ok:
463 log.info('user %s is authenticated and granted access to %s' % (
463 log.info('user %s is authenticated and granted access to %s' % (
464 user.username, loc)
464 user.username, loc)
465 )
465 )
466 return func(*fargs, **fkwargs)
466 return func(*fargs, **fkwargs)
467 else:
467 else:
468 log.warn('user %s NOT authenticated on func: %s' % (
468 log.warn('user %s NOT authenticated on func: %s' % (
469 user, loc)
469 user, loc)
470 )
470 )
471 p = url.current()
471 p = url.current()
472
472
473 log.debug('redirecting to login page with %s' % p)
473 log.debug('redirecting to login page with %s' % p)
474 return redirect(url('login_home', came_from=p))
474 return redirect(url('login_home', came_from=p))
475
475
476
476
477 class NotAnonymous(object):
477 class NotAnonymous(object):
478 """
478 """
479 Must be logged in to execute this function else
479 Must be logged in to execute this function else
480 redirect to login page"""
480 redirect to login page"""
481
481
482 def __call__(self, func):
482 def __call__(self, func):
483 return decorator(self.__wrapper, func)
483 return decorator(self.__wrapper, func)
484
484
485 def __wrapper(self, func, *fargs, **fkwargs):
485 def __wrapper(self, func, *fargs, **fkwargs):
486 cls = fargs[0]
486 cls = fargs[0]
487 self.user = cls.rhodecode_user
487 self.user = cls.rhodecode_user
488
488
489 log.debug('Checking if user is not anonymous @%s' % cls)
489 log.debug('Checking if user is not anonymous @%s' % cls)
490
490
491 anonymous = self.user.username == 'default'
491 anonymous = self.user.username == 'default'
492
492
493 if anonymous:
493 if anonymous:
494 p = url.current()
494 p = url.current()
495
495
496 import rhodecode.lib.helpers as h
496 import rhodecode.lib.helpers as h
497 h.flash(_('You need to be a registered user to '
497 h.flash(_('You need to be a registered user to '
498 'perform this action'),
498 'perform this action'),
499 category='warning')
499 category='warning')
500 return redirect(url('login_home', came_from=p))
500 return redirect(url('login_home', came_from=p))
501 else:
501 else:
502 return func(*fargs, **fkwargs)
502 return func(*fargs, **fkwargs)
503
503
504
504
505 class PermsDecorator(object):
505 class PermsDecorator(object):
506 """Base class for controller decorators"""
506 """Base class for controller decorators"""
507
507
508 def __init__(self, *required_perms):
508 def __init__(self, *required_perms):
509 available_perms = config['available_permissions']
509 available_perms = config['available_permissions']
510 for perm in required_perms:
510 for perm in required_perms:
511 if perm not in available_perms:
511 if perm not in available_perms:
512 raise Exception("'%s' permission is not defined" % perm)
512 raise Exception("'%s' permission is not defined" % perm)
513 self.required_perms = set(required_perms)
513 self.required_perms = set(required_perms)
514 self.user_perms = None
514 self.user_perms = None
515
515
516 def __call__(self, func):
516 def __call__(self, func):
517 return decorator(self.__wrapper, func)
517 return decorator(self.__wrapper, func)
518
518
519 def __wrapper(self, func, *fargs, **fkwargs):
519 def __wrapper(self, func, *fargs, **fkwargs):
520 cls = fargs[0]
520 cls = fargs[0]
521 self.user = cls.rhodecode_user
521 self.user = cls.rhodecode_user
522 self.user_perms = self.user.permissions
522 self.user_perms = self.user.permissions
523 log.debug('checking %s permissions %s for %s %s',
523 log.debug('checking %s permissions %s for %s %s',
524 self.__class__.__name__, self.required_perms, cls, self.user)
524 self.__class__.__name__, self.required_perms, cls, self.user)
525
525
526 if self.check_permissions():
526 if self.check_permissions():
527 log.debug('Permission granted for %s %s' % (cls, self.user))
527 log.debug('Permission granted for %s %s' % (cls, self.user))
528 return func(*fargs, **fkwargs)
528 return func(*fargs, **fkwargs)
529
529
530 else:
530 else:
531 log.debug('Permission denied for %s %s' % (cls, self.user))
531 log.debug('Permission denied for %s %s' % (cls, self.user))
532 anonymous = self.user.username == 'default'
532 anonymous = self.user.username == 'default'
533
533
534 if anonymous:
534 if anonymous:
535 p = url.current()
535 p = url.current()
536
536
537 import rhodecode.lib.helpers as h
537 import rhodecode.lib.helpers as h
538 h.flash(_('You need to be a signed in to '
538 h.flash(_('You need to be a signed in to '
539 'view this page'),
539 'view this page'),
540 category='warning')
540 category='warning')
541 return redirect(url('login_home', came_from=p))
541 return redirect(url('login_home', came_from=p))
542
542
543 else:
543 else:
544 # redirect with forbidden ret code
544 # redirect with forbidden ret code
545 return abort(403)
545 return abort(403)
546
546
547 def check_permissions(self):
547 def check_permissions(self):
548 """Dummy function for overriding"""
548 """Dummy function for overriding"""
549 raise Exception('You have to write this function in child class')
549 raise Exception('You have to write this function in child class')
550
550
551
551
552 class HasPermissionAllDecorator(PermsDecorator):
552 class HasPermissionAllDecorator(PermsDecorator):
553 """
553 """
554 Checks for access permission for all given predicates. All of them
554 Checks for access permission for all given predicates. All of them
555 have to be meet in order to fulfill the request
555 have to be meet in order to fulfill the request
556 """
556 """
557
557
558 def check_permissions(self):
558 def check_permissions(self):
559 if self.required_perms.issubset(self.user_perms.get('global')):
559 if self.required_perms.issubset(self.user_perms.get('global')):
560 return True
560 return True
561 return False
561 return False
562
562
563
563
564 class HasPermissionAnyDecorator(PermsDecorator):
564 class HasPermissionAnyDecorator(PermsDecorator):
565 """
565 """
566 Checks for access permission for any of given predicates. In order to
566 Checks for access permission for any of given predicates. In order to
567 fulfill the request any of predicates must be meet
567 fulfill the request any of predicates must be meet
568 """
568 """
569
569
570 def check_permissions(self):
570 def check_permissions(self):
571 if self.required_perms.intersection(self.user_perms.get('global')):
571 if self.required_perms.intersection(self.user_perms.get('global')):
572 return True
572 return True
573 return False
573 return False
574
574
575
575
576 class HasRepoPermissionAllDecorator(PermsDecorator):
576 class HasRepoPermissionAllDecorator(PermsDecorator):
577 """
577 """
578 Checks for access permission for all given predicates for specific
578 Checks for access permission for all given predicates for specific
579 repository. All of them have to be meet in order to fulfill the request
579 repository. All of them have to be meet in order to fulfill the request
580 """
580 """
581
581
582 def check_permissions(self):
582 def check_permissions(self):
583 repo_name = get_repo_slug(request)
583 repo_name = get_repo_slug(request)
584 try:
584 try:
585 user_perms = set([self.user_perms['repositories'][repo_name]])
585 user_perms = set([self.user_perms['repositories'][repo_name]])
586 except KeyError:
586 except KeyError:
587 return False
587 return False
588 if self.required_perms.issubset(user_perms):
588 if self.required_perms.issubset(user_perms):
589 return True
589 return True
590 return False
590 return False
591
591
592
592
593 class HasRepoPermissionAnyDecorator(PermsDecorator):
593 class HasRepoPermissionAnyDecorator(PermsDecorator):
594 """
594 """
595 Checks for access permission for any of given predicates for specific
595 Checks for access permission for any of given predicates for specific
596 repository. In order to fulfill the request any of predicates must be meet
596 repository. In order to fulfill the request any of predicates must be meet
597 """
597 """
598
598
599 def check_permissions(self):
599 def check_permissions(self):
600 repo_name = get_repo_slug(request)
600 repo_name = get_repo_slug(request)
601
601
602 try:
602 try:
603 user_perms = set([self.user_perms['repositories'][repo_name]])
603 user_perms = set([self.user_perms['repositories'][repo_name]])
604 except KeyError:
604 except KeyError:
605 return False
605 return False
606
606
607 if self.required_perms.intersection(user_perms):
607 if self.required_perms.intersection(user_perms):
608 return True
608 return True
609 return False
609 return False
610
610
611
611
612 class HasReposGroupPermissionAllDecorator(PermsDecorator):
612 class HasReposGroupPermissionAllDecorator(PermsDecorator):
613 """
613 """
614 Checks for access permission for all given predicates for specific
614 Checks for access permission for all given predicates for specific
615 repository. All of them have to be meet in order to fulfill the request
615 repository. All of them have to be meet in order to fulfill the request
616 """
616 """
617
617
618 def check_permissions(self):
618 def check_permissions(self):
619 group_name = get_repos_group_slug(request)
619 group_name = get_repos_group_slug(request)
620 try:
620 try:
621 user_perms = set([self.user_perms['repositories_groups'][group_name]])
621 user_perms = set([self.user_perms['repositories_groups'][group_name]])
622 except KeyError:
622 except KeyError:
623 return False
623 return False
624 if self.required_perms.issubset(user_perms):
624 if self.required_perms.issubset(user_perms):
625 return True
625 return True
626 return False
626 return False
627
627
628
628
629 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
629 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
630 """
630 """
631 Checks for access permission for any of given predicates for specific
631 Checks for access permission for any of given predicates for specific
632 repository. In order to fulfill the request any of predicates must be meet
632 repository. In order to fulfill the request any of predicates must be meet
633 """
633 """
634
634
635 def check_permissions(self):
635 def check_permissions(self):
636 group_name = get_repos_group_slug(request)
636 group_name = get_repos_group_slug(request)
637
637
638 try:
638 try:
639 user_perms = set([self.user_perms['repositories_groups'][group_name]])
639 user_perms = set([self.user_perms['repositories_groups'][group_name]])
640 except KeyError:
640 except KeyError:
641 return False
641 return False
642 if self.required_perms.intersection(user_perms):
642 if self.required_perms.intersection(user_perms):
643 return True
643 return True
644 return False
644 return False
645
645
646
646
647 #==============================================================================
647 #==============================================================================
648 # CHECK FUNCTIONS
648 # CHECK FUNCTIONS
649 #==============================================================================
649 #==============================================================================
650 class PermsFunction(object):
650 class PermsFunction(object):
651 """Base function for other check functions"""
651 """Base function for other check functions"""
652
652
653 def __init__(self, *perms):
653 def __init__(self, *perms):
654 available_perms = config['available_permissions']
654 available_perms = config['available_permissions']
655
655
656 for perm in perms:
656 for perm in perms:
657 if perm not in available_perms:
657 if perm not in available_perms:
658 raise Exception("'%s' permission is not defined" % perm)
658 raise Exception("'%s' permission is not defined" % perm)
659 self.required_perms = set(perms)
659 self.required_perms = set(perms)
660 self.user_perms = None
660 self.user_perms = None
661 self.repo_name = None
661 self.repo_name = None
662 self.group_name = None
662 self.group_name = None
663
663
664 def __call__(self, check_Location=''):
664 def __call__(self, check_Location=''):
665 user = request.user
665 user = request.user
666 cls_name = self.__class__.__name__
666 cls_name = self.__class__.__name__
667 check_scope = {
667 check_scope = {
668 'HasPermissionAll': '',
668 'HasPermissionAll': '',
669 'HasPermissionAny': '',
669 'HasPermissionAny': '',
670 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
670 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
671 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
671 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
672 'HasReposGroupPermissionAll': 'group:%s' % self.group_name,
672 'HasReposGroupPermissionAll': 'group:%s' % self.group_name,
673 'HasReposGroupPermissionAny': 'group:%s' % self.group_name,
673 'HasReposGroupPermissionAny': 'group:%s' % self.group_name,
674 }.get(cls_name, '?')
674 }.get(cls_name, '?')
675 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
675 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
676 self.required_perms, user, check_scope,
676 self.required_perms, user, check_scope,
677 check_Location or 'unspecified location')
677 check_Location or 'unspecified location')
678 if not user:
678 if not user:
679 log.debug('Empty request user')
679 log.debug('Empty request user')
680 return False
680 return False
681 self.user_perms = user.permissions
681 self.user_perms = user.permissions
682 if self.check_permissions():
682 if self.check_permissions():
683 log.debug('Permission granted for user: %s @ %s', user,
683 log.debug('Permission granted for user: %s @ %s', user,
684 check_Location or 'unspecified location')
684 check_Location or 'unspecified location')
685 return True
685 return True
686
686
687 else:
687 else:
688 log.debug('Permission denied for user: %s @ %s', user,
688 log.debug('Permission denied for user: %s @ %s', user,
689 check_Location or 'unspecified location')
689 check_Location or 'unspecified location')
690 return False
690 return False
691
691
692 def check_permissions(self):
692 def check_permissions(self):
693 """Dummy function for overriding"""
693 """Dummy function for overriding"""
694 raise Exception('You have to write this function in child class')
694 raise Exception('You have to write this function in child class')
695
695
696
696
697 class HasPermissionAll(PermsFunction):
697 class HasPermissionAll(PermsFunction):
698 def check_permissions(self):
698 def check_permissions(self):
699 if self.required_perms.issubset(self.user_perms.get('global')):
699 if self.required_perms.issubset(self.user_perms.get('global')):
700 return True
700 return True
701 return False
701 return False
702
702
703
703
704 class HasPermissionAny(PermsFunction):
704 class HasPermissionAny(PermsFunction):
705 def check_permissions(self):
705 def check_permissions(self):
706 if self.required_perms.intersection(self.user_perms.get('global')):
706 if self.required_perms.intersection(self.user_perms.get('global')):
707 return True
707 return True
708 return False
708 return False
709
709
710
710
711 class HasRepoPermissionAll(PermsFunction):
711 class HasRepoPermissionAll(PermsFunction):
712 def __call__(self, repo_name=None, check_Location=''):
712 def __call__(self, repo_name=None, check_Location=''):
713 self.repo_name = repo_name
713 self.repo_name = repo_name
714 return super(HasRepoPermissionAll, self).__call__(check_Location)
714 return super(HasRepoPermissionAll, self).__call__(check_Location)
715
715
716 def check_permissions(self):
716 def check_permissions(self):
717 if not self.repo_name:
717 if not self.repo_name:
718 self.repo_name = get_repo_slug(request)
718 self.repo_name = get_repo_slug(request)
719
719
720 try:
720 try:
721 self._user_perms = set(
721 self._user_perms = set(
722 [self.user_perms['repositories'][self.repo_name]]
722 [self.user_perms['repositories'][self.repo_name]]
723 )
723 )
724 except KeyError:
724 except KeyError:
725 return False
725 return False
726 if self.required_perms.issubset(self._user_perms):
726 if self.required_perms.issubset(self._user_perms):
727 return True
727 return True
728 return False
728 return False
729
729
730
730
731 class HasRepoPermissionAny(PermsFunction):
731 class HasRepoPermissionAny(PermsFunction):
732 def __call__(self, repo_name=None, check_Location=''):
732 def __call__(self, repo_name=None, check_Location=''):
733 self.repo_name = repo_name
733 self.repo_name = repo_name
734 return super(HasRepoPermissionAny, self).__call__(check_Location)
734 return super(HasRepoPermissionAny, self).__call__(check_Location)
735
735
736 def check_permissions(self):
736 def check_permissions(self):
737 if not self.repo_name:
737 if not self.repo_name:
738 self.repo_name = get_repo_slug(request)
738 self.repo_name = get_repo_slug(request)
739
739
740 try:
740 try:
741 self._user_perms = set(
741 self._user_perms = set(
742 [self.user_perms['repositories'][self.repo_name]]
742 [self.user_perms['repositories'][self.repo_name]]
743 )
743 )
744 except KeyError:
744 except KeyError:
745 return False
745 return False
746 if self.required_perms.intersection(self._user_perms):
746 if self.required_perms.intersection(self._user_perms):
747 return True
747 return True
748 return False
748 return False
749
749
750
750
751 class HasReposGroupPermissionAny(PermsFunction):
751 class HasReposGroupPermissionAny(PermsFunction):
752 def __call__(self, group_name=None, check_Location=''):
752 def __call__(self, group_name=None, check_Location=''):
753 self.group_name = group_name
753 self.group_name = group_name
754 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
754 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
755
755
756 def check_permissions(self):
756 def check_permissions(self):
757 try:
757 try:
758 self._user_perms = set(
758 self._user_perms = set(
759 [self.user_perms['repositories_groups'][self.group_name]]
759 [self.user_perms['repositories_groups'][self.group_name]]
760 )
760 )
761 except KeyError:
761 except KeyError:
762 return False
762 return False
763 if self.required_perms.intersection(self._user_perms):
763 if self.required_perms.intersection(self._user_perms):
764 return True
764 return True
765 return False
765 return False
766
766
767
767
768 class HasReposGroupPermissionAll(PermsFunction):
768 class HasReposGroupPermissionAll(PermsFunction):
769 def __call__(self, group_name=None, check_Location=''):
769 def __call__(self, group_name=None, check_Location=''):
770 self.group_name = group_name
770 self.group_name = group_name
771 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
771 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
772
772
773 def check_permissions(self):
773 def check_permissions(self):
774 try:
774 try:
775 self._user_perms = set(
775 self._user_perms = set(
776 [self.user_perms['repositories_groups'][self.group_name]]
776 [self.user_perms['repositories_groups'][self.group_name]]
777 )
777 )
778 except KeyError:
778 except KeyError:
779 return False
779 return False
780 if self.required_perms.issubset(self._user_perms):
780 if self.required_perms.issubset(self._user_perms):
781 return True
781 return True
782 return False
782 return False
783
783
784
784
785 #==============================================================================
785 #==============================================================================
786 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
786 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
787 #==============================================================================
787 #==============================================================================
788 class HasPermissionAnyMiddleware(object):
788 class HasPermissionAnyMiddleware(object):
789 def __init__(self, *perms):
789 def __init__(self, *perms):
790 self.required_perms = set(perms)
790 self.required_perms = set(perms)
791
791
792 def __call__(self, user, repo_name):
792 def __call__(self, user, repo_name):
793 # repo_name MUST be unicode, since we handle keys in permission
793 # repo_name MUST be unicode, since we handle keys in permission
794 # dict by unicode
794 # dict by unicode
795 repo_name = safe_unicode(repo_name)
795 repo_name = safe_unicode(repo_name)
796 usr = AuthUser(user.user_id)
796 usr = AuthUser(user.user_id)
797 try:
797 try:
798 self.user_perms = set([usr.permissions['repositories'][repo_name]])
798 self.user_perms = set([usr.permissions['repositories'][repo_name]])
799 except Exception:
799 except Exception:
800 log.error('Exception while accessing permissions %s' %
800 log.error('Exception while accessing permissions %s' %
801 traceback.format_exc())
801 traceback.format_exc())
802 self.user_perms = set()
802 self.user_perms = set()
803 self.username = user.username
803 self.username = user.username
804 self.repo_name = repo_name
804 self.repo_name = repo_name
805 return self.check_permissions()
805 return self.check_permissions()
806
806
807 def check_permissions(self):
807 def check_permissions(self):
808 log.debug('checking mercurial protocol '
808 log.debug('checking mercurial protocol '
809 'permissions %s for user:%s repository:%s', self.user_perms,
809 'permissions %s for user:%s repository:%s', self.user_perms,
810 self.username, self.repo_name)
810 self.username, self.repo_name)
811 if self.required_perms.intersection(self.user_perms):
811 if self.required_perms.intersection(self.user_perms):
812 log.debug('permission granted for user:%s on repo:%s' % (
812 log.debug('permission granted for user:%s on repo:%s' % (
813 self.username, self.repo_name
813 self.username, self.repo_name
814 )
814 )
815 )
815 )
816 return True
816 return True
817 log.debug('permission denied for user:%s on repo:%s' % (
817 log.debug('permission denied for user:%s on repo:%s' % (
818 self.username, self.repo_name
818 self.username, self.repo_name
819 )
819 )
820 )
820 )
821 return False
821 return False
@@ -1,104 +1,106 b''
1 import datetime
1 import datetime
2 import functools
2 import functools
3 import decimal
3 import decimal
4
4
5 __all__ = ['json', 'simplejson', 'stdjson']
5 __all__ = ['json', 'simplejson', 'stdjson']
6
6
7
7
8 def _is_aware(value):
8 def _is_aware(value):
9 """
9 """
10 Determines if a given datetime.time is aware.
10 Determines if a given datetime.time is aware.
11
11
12 The logic is described in Python's docs:
12 The logic is described in Python's docs:
13 http://docs.python.org/library/datetime.html#datetime.tzinfo
13 http://docs.python.org/library/datetime.html#datetime.tzinfo
14 """
14 """
15 return (value.tzinfo is not None
15 return (value.tzinfo is not None
16 and value.tzinfo.utcoffset(value) is not None)
16 and value.tzinfo.utcoffset(value) is not None)
17
17
18
18
19 def _obj_dump(obj):
19 def _obj_dump(obj):
20 """
20 """
21 Custom function for dumping objects to JSON, if obj has __json__ attribute
21 Custom function for dumping objects to JSON, if obj has __json__ attribute
22 or method defined it will be used for serialization
22 or method defined it will be used for serialization
23
23
24 :param obj:
24 :param obj:
25 """
25 """
26
26
27 if isinstance(obj, complex):
27 if isinstance(obj, complex):
28 return [obj.real, obj.imag]
28 return [obj.real, obj.imag]
29 # See "Date Time String Format" in the ECMA-262 specification.
29 # See "Date Time String Format" in the ECMA-262 specification.
30 # some code borrowed from django 1.4
30 # some code borrowed from django 1.4
31 elif isinstance(obj, datetime.datetime):
31 elif isinstance(obj, datetime.datetime):
32 r = obj.isoformat()
32 r = obj.isoformat()
33 if obj.microsecond:
33 if obj.microsecond:
34 r = r[:23] + r[26:]
34 r = r[:23] + r[26:]
35 if r.endswith('+00:00'):
35 if r.endswith('+00:00'):
36 r = r[:-6] + 'Z'
36 r = r[:-6] + 'Z'
37 return r
37 return r
38 elif isinstance(obj, datetime.date):
38 elif isinstance(obj, datetime.date):
39 return obj.isoformat()
39 return obj.isoformat()
40 elif isinstance(obj, decimal.Decimal):
40 elif isinstance(obj, decimal.Decimal):
41 return str(obj)
41 return str(obj)
42 elif isinstance(obj, datetime.time):
42 elif isinstance(obj, datetime.time):
43 if _is_aware(obj):
43 if _is_aware(obj):
44 raise ValueError("JSON can't represent timezone-aware times.")
44 raise ValueError("JSON can't represent timezone-aware times.")
45 r = obj.isoformat()
45 r = obj.isoformat()
46 if obj.microsecond:
46 if obj.microsecond:
47 r = r[:12]
47 r = r[:12]
48 return r
48 return r
49 elif isinstance(obj, set):
49 elif isinstance(obj, set):
50 return list(obj)
50 return list(obj)
51 elif hasattr(obj, '__json__'):
51 elif hasattr(obj, '__json__'):
52 if callable(obj.__json__):
52 if callable(obj.__json__):
53 return obj.__json__()
53 return obj.__json__()
54 else:
54 else:
55 return obj.__json__
55 return obj.__json__
56 else:
56 else:
57 raise NotImplementedError
57 raise NotImplementedError
58
58
59
59
60 # Import simplejson
60 # Import simplejson
61 try:
61 try:
62 # import simplejson initially
62 # import simplejson initially
63 import simplejson as _sj
63 import simplejson as _sj
64
64
65 def extended_encode(obj):
65 def extended_encode(obj):
66 try:
66 try:
67 return _obj_dump(obj)
67 return _obj_dump(obj)
68 except NotImplementedError:
68 except NotImplementedError:
69 pass
69 pass
70 raise TypeError("%r is not JSON serializable" % (obj,))
70 raise TypeError("%r is not JSON serializable" % (obj,))
71 # we handle decimals our own it makes unified behavior of json vs
71 # we handle decimals our own it makes unified behavior of json vs
72 # simplejson
72 # simplejson
73 _sj.dumps = functools.partial(_sj.dumps, default=extended_encode,
73 _sj.dumps = functools.partial(_sj.dumps, default=extended_encode,
74 use_decimal=False)
74 use_decimal=False)
75 _sj.dump = functools.partial(_sj.dump, default=extended_encode,
75 _sj.dump = functools.partial(_sj.dump, default=extended_encode,
76 use_decimal=False)
76 use_decimal=False)
77 simplejson = _sj
77 simplejson = _sj
78
78
79 except ImportError:
79 except ImportError:
80 # no simplejson set it to None
80 # no simplejson set it to None
81 _sj = None
81 _sj = None
82
82
83
83
84 # simplejson not found try out regular json module
84 try:
85 import json as _json
85 # simplejson not found try out regular json module
86
86 import json as _json
87
87
88 # extended JSON encoder for json
88 # extended JSON encoder for json
89 class ExtendedEncoder(_json.JSONEncoder):
89 class ExtendedEncoder(_json.JSONEncoder):
90 def default(self, obj):
90 def default(self, obj):
91 try:
91 try:
92 return _obj_dump(obj)
92 return _obj_dump(obj)
93 except NotImplementedError:
93 except NotImplementedError:
94 pass
94 pass
95 return _json.JSONEncoder.default(self, obj)
95 return _json.JSONEncoder.default(self, obj)
96 # monkey-patch JSON encoder to use extended version
96 # monkey-patch JSON encoder to use extended version
97 _json.dumps = functools.partial(_json.dumps, cls=ExtendedEncoder)
97 _json.dumps = functools.partial(_json.dumps, cls=ExtendedEncoder)
98 _json.dump = functools.partial(_json.dump, cls=ExtendedEncoder)
98 _json.dump = functools.partial(_json.dump, cls=ExtendedEncoder)
99 stdlib = _json
99 stdlib = _json
100 except ImportError:
101 _json = None
100
102
101 # set all available json modules
103 # set all available json modules
102 simplejson = _sj
104 simplejson = _sj
103 stdjson = _json
105 stdjson = _json
104 json = _sj if _sj else _json
106 json = _sj if _sj else _json
@@ -1,666 +1,666 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.utils
3 rhodecode.lib.utils
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Utilities library for RhodeCode
6 Utilities library for RhodeCode
7
7
8 :created_on: Apr 18, 2010
8 :created_on: Apr 18, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import os
26 import os
27 import re
27 import re
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import traceback
30 import traceback
31 import paste
31 import paste
32 import beaker
32 import beaker
33 import tarfile
33 import tarfile
34 import shutil
34 import shutil
35 from os.path import abspath
35 from os.path import abspath
36 from os.path import dirname as dn, join as jn
36 from os.path import dirname as dn, join as jn
37
37
38 from paste.script.command import Command, BadCommand
38 from paste.script.command import Command, BadCommand
39
39
40 from mercurial import ui, config
40 from mercurial import ui, config
41
41
42 from webhelpers.text import collapse, remove_formatting, strip_tags
42 from webhelpers.text import collapse, remove_formatting, strip_tags
43
43
44 from rhodecode.lib.vcs import get_backend
44 from rhodecode.lib.vcs import get_backend
45 from rhodecode.lib.vcs.backends.base import BaseChangeset
45 from rhodecode.lib.vcs.backends.base import BaseChangeset
46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 from rhodecode.lib.vcs.utils.helpers import get_scm
47 from rhodecode.lib.vcs.utils.helpers import get_scm
48 from rhodecode.lib.vcs.exceptions import VCSError
48 from rhodecode.lib.vcs.exceptions import VCSError
49
49
50 from rhodecode.lib.caching_query import FromCache
50 from rhodecode.lib.caching_query import FromCache
51
51
52 from rhodecode.model import meta
52 from rhodecode.model import meta
53 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
53 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
54 UserLog, RepoGroup, RhodeCodeSetting, UserRepoGroupToPerm,\
54 UserLog, RepoGroup, RhodeCodeSetting, UserRepoGroupToPerm,\
55 CacheInvalidation
55 CacheInvalidation
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.repos_group import ReposGroupModel
57 from rhodecode.model.repos_group import ReposGroupModel
58 from rhodecode.lib.utils2 import safe_str, safe_unicode
58 from rhodecode.lib.utils2 import safe_str, safe_unicode
59 from rhodecode.lib.vcs.utils.fakemod import create_module
59 from rhodecode.lib.vcs.utils.fakemod import create_module
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
63 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
64
64
65
65
66 def recursive_replace(str_, replace=' '):
66 def recursive_replace(str_, replace=' '):
67 """
67 """
68 Recursive replace of given sign to just one instance
68 Recursive replace of given sign to just one instance
69
69
70 :param str_: given string
70 :param str_: given string
71 :param replace: char to find and replace multiple instances
71 :param replace: char to find and replace multiple instances
72
72
73 Examples::
73 Examples::
74 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
74 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
75 'Mighty-Mighty-Bo-sstones'
75 'Mighty-Mighty-Bo-sstones'
76 """
76 """
77
77
78 if str_.find(replace * 2) == -1:
78 if str_.find(replace * 2) == -1:
79 return str_
79 return str_
80 else:
80 else:
81 str_ = str_.replace(replace * 2, replace)
81 str_ = str_.replace(replace * 2, replace)
82 return recursive_replace(str_, replace)
82 return recursive_replace(str_, replace)
83
83
84
84
85 def repo_name_slug(value):
85 def repo_name_slug(value):
86 """
86 """
87 Return slug of name of repository
87 Return slug of name of repository
88 This function is called on each creation/modification
88 This function is called on each creation/modification
89 of repository to prevent bad names in repo
89 of repository to prevent bad names in repo
90 """
90 """
91
91
92 slug = remove_formatting(value)
92 slug = remove_formatting(value)
93 slug = strip_tags(slug)
93 slug = strip_tags(slug)
94
94
95 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
95 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
96 slug = slug.replace(c, '-')
96 slug = slug.replace(c, '-')
97 slug = recursive_replace(slug, '-')
97 slug = recursive_replace(slug, '-')
98 slug = collapse(slug, '-')
98 slug = collapse(slug, '-')
99 return slug
99 return slug
100
100
101
101
102 def get_repo_slug(request):
102 def get_repo_slug(request):
103 _repo = request.environ['pylons.routes_dict'].get('repo_name')
103 _repo = request.environ['pylons.routes_dict'].get('repo_name')
104 if _repo:
104 if _repo:
105 _repo = _repo.rstrip('/')
105 _repo = _repo.rstrip('/')
106 return _repo
106 return _repo
107
107
108
108
109 def get_repos_group_slug(request):
109 def get_repos_group_slug(request):
110 _group = request.environ['pylons.routes_dict'].get('group_name')
110 _group = request.environ['pylons.routes_dict'].get('group_name')
111 if _group:
111 if _group:
112 _group = _group.rstrip('/')
112 _group = _group.rstrip('/')
113 return _group
113 return _group
114
114
115
115
116 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
116 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
117 """
117 """
118 Action logger for various actions made by users
118 Action logger for various actions made by users
119
119
120 :param user: user that made this action, can be a unique username string or
120 :param user: user that made this action, can be a unique username string or
121 object containing user_id attribute
121 object containing user_id attribute
122 :param action: action to log, should be on of predefined unique actions for
122 :param action: action to log, should be on of predefined unique actions for
123 easy translations
123 easy translations
124 :param repo: string name of repository or object containing repo_id,
124 :param repo: string name of repository or object containing repo_id,
125 that action was made on
125 that action was made on
126 :param ipaddr: optional ip address from what the action was made
126 :param ipaddr: optional ip address from what the action was made
127 :param sa: optional sqlalchemy session
127 :param sa: optional sqlalchemy session
128
128
129 """
129 """
130
130
131 if not sa:
131 if not sa:
132 sa = meta.Session
132 sa = meta.Session
133
133
134 try:
134 try:
135 if hasattr(user, 'user_id'):
135 if hasattr(user, 'user_id'):
136 user_obj = user
136 user_obj = user
137 elif isinstance(user, basestring):
137 elif isinstance(user, basestring):
138 user_obj = User.get_by_username(user)
138 user_obj = User.get_by_username(user)
139 else:
139 else:
140 raise Exception('You have to provide user object or username')
140 raise Exception('You have to provide user object or username')
141
141
142 if hasattr(repo, 'repo_id'):
142 if hasattr(repo, 'repo_id'):
143 repo_obj = Repository.get(repo.repo_id)
143 repo_obj = Repository.get(repo.repo_id)
144 repo_name = repo_obj.repo_name
144 repo_name = repo_obj.repo_name
145 elif isinstance(repo, basestring):
145 elif isinstance(repo, basestring):
146 repo_name = repo.lstrip('/')
146 repo_name = repo.lstrip('/')
147 repo_obj = Repository.get_by_repo_name(repo_name)
147 repo_obj = Repository.get_by_repo_name(repo_name)
148 else:
148 else:
149 raise Exception('You have to provide repository to action logger')
149 raise Exception('You have to provide repository to action logger')
150
150
151 user_log = UserLog()
151 user_log = UserLog()
152 user_log.user_id = user_obj.user_id
152 user_log.user_id = user_obj.user_id
153 user_log.action = action
153 user_log.action = safe_unicode(action)
154
154
155 user_log.repository_id = repo_obj.repo_id
155 user_log.repository_id = repo_obj.repo_id
156 user_log.repository_name = repo_name
156 user_log.repository_name = repo_name
157
157
158 user_log.action_date = datetime.datetime.now()
158 user_log.action_date = datetime.datetime.now()
159 user_log.user_ip = ipaddr
159 user_log.user_ip = ipaddr
160 sa.add(user_log)
160 sa.add(user_log)
161
161
162 log.info(
162 log.info(
163 'Adding user %s, action %s on %s' % (user_obj, action,
163 'Adding user %s, action %s on %s' % (user_obj, action,
164 safe_unicode(repo))
164 safe_unicode(repo))
165 )
165 )
166 if commit:
166 if commit:
167 sa.commit()
167 sa.commit()
168 except:
168 except:
169 log.error(traceback.format_exc())
169 log.error(traceback.format_exc())
170 raise
170 raise
171
171
172
172
173 def get_repos(path, recursive=False):
173 def get_repos(path, recursive=False):
174 """
174 """
175 Scans given path for repos and return (name,(type,path)) tuple
175 Scans given path for repos and return (name,(type,path)) tuple
176
176
177 :param path: path to scan for repositories
177 :param path: path to scan for repositories
178 :param recursive: recursive search and return names with subdirs in front
178 :param recursive: recursive search and return names with subdirs in front
179 """
179 """
180
180
181 # remove ending slash for better results
181 # remove ending slash for better results
182 path = path.rstrip(os.sep)
182 path = path.rstrip(os.sep)
183
183
184 def _get_repos(p):
184 def _get_repos(p):
185 if not os.access(p, os.W_OK):
185 if not os.access(p, os.W_OK):
186 return
186 return
187 for dirpath in os.listdir(p):
187 for dirpath in os.listdir(p):
188 if os.path.isfile(os.path.join(p, dirpath)):
188 if os.path.isfile(os.path.join(p, dirpath)):
189 continue
189 continue
190 cur_path = os.path.join(p, dirpath)
190 cur_path = os.path.join(p, dirpath)
191 try:
191 try:
192 scm_info = get_scm(cur_path)
192 scm_info = get_scm(cur_path)
193 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
193 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
194 except VCSError:
194 except VCSError:
195 if not recursive:
195 if not recursive:
196 continue
196 continue
197 #check if this dir containts other repos for recursive scan
197 #check if this dir containts other repos for recursive scan
198 rec_path = os.path.join(p, dirpath)
198 rec_path = os.path.join(p, dirpath)
199 if os.path.isdir(rec_path):
199 if os.path.isdir(rec_path):
200 for inner_scm in _get_repos(rec_path):
200 for inner_scm in _get_repos(rec_path):
201 yield inner_scm
201 yield inner_scm
202
202
203 return _get_repos(path)
203 return _get_repos(path)
204
204
205
205
206 def is_valid_repo(repo_name, base_path):
206 def is_valid_repo(repo_name, base_path):
207 """
207 """
208 Returns True if given path is a valid repository False otherwise
208 Returns True if given path is a valid repository False otherwise
209
209
210 :param repo_name:
210 :param repo_name:
211 :param base_path:
211 :param base_path:
212
212
213 :return True: if given path is a valid repository
213 :return True: if given path is a valid repository
214 """
214 """
215 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
215 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
216
216
217 try:
217 try:
218 get_scm(full_path)
218 get_scm(full_path)
219 return True
219 return True
220 except VCSError:
220 except VCSError:
221 return False
221 return False
222
222
223
223
224 def is_valid_repos_group(repos_group_name, base_path):
224 def is_valid_repos_group(repos_group_name, base_path):
225 """
225 """
226 Returns True if given path is a repos group False otherwise
226 Returns True if given path is a repos group False otherwise
227
227
228 :param repo_name:
228 :param repo_name:
229 :param base_path:
229 :param base_path:
230 """
230 """
231 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
231 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
232
232
233 # check if it's not a repo
233 # check if it's not a repo
234 if is_valid_repo(repos_group_name, base_path):
234 if is_valid_repo(repos_group_name, base_path):
235 return False
235 return False
236
236
237 # check if it's a valid path
237 # check if it's a valid path
238 if os.path.isdir(full_path):
238 if os.path.isdir(full_path):
239 return True
239 return True
240
240
241 return False
241 return False
242
242
243
243
244 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
244 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
245 while True:
245 while True:
246 ok = raw_input(prompt)
246 ok = raw_input(prompt)
247 if ok in ('y', 'ye', 'yes'):
247 if ok in ('y', 'ye', 'yes'):
248 return True
248 return True
249 if ok in ('n', 'no', 'nop', 'nope'):
249 if ok in ('n', 'no', 'nop', 'nope'):
250 return False
250 return False
251 retries = retries - 1
251 retries = retries - 1
252 if retries < 0:
252 if retries < 0:
253 raise IOError
253 raise IOError
254 print complaint
254 print complaint
255
255
256 #propagated from mercurial documentation
256 #propagated from mercurial documentation
257 ui_sections = ['alias', 'auth',
257 ui_sections = ['alias', 'auth',
258 'decode/encode', 'defaults',
258 'decode/encode', 'defaults',
259 'diff', 'email',
259 'diff', 'email',
260 'extensions', 'format',
260 'extensions', 'format',
261 'merge-patterns', 'merge-tools',
261 'merge-patterns', 'merge-tools',
262 'hooks', 'http_proxy',
262 'hooks', 'http_proxy',
263 'smtp', 'patch',
263 'smtp', 'patch',
264 'paths', 'profiling',
264 'paths', 'profiling',
265 'server', 'trusted',
265 'server', 'trusted',
266 'ui', 'web', ]
266 'ui', 'web', ]
267
267
268
268
269 def make_ui(read_from='file', path=None, checkpaths=True):
269 def make_ui(read_from='file', path=None, checkpaths=True):
270 """
270 """
271 A function that will read python rc files or database
271 A function that will read python rc files or database
272 and make an mercurial ui object from read options
272 and make an mercurial ui object from read options
273
273
274 :param path: path to mercurial config file
274 :param path: path to mercurial config file
275 :param checkpaths: check the path
275 :param checkpaths: check the path
276 :param read_from: read from 'file' or 'db'
276 :param read_from: read from 'file' or 'db'
277 """
277 """
278
278
279 baseui = ui.ui()
279 baseui = ui.ui()
280
280
281 # clean the baseui object
281 # clean the baseui object
282 baseui._ocfg = config.config()
282 baseui._ocfg = config.config()
283 baseui._ucfg = config.config()
283 baseui._ucfg = config.config()
284 baseui._tcfg = config.config()
284 baseui._tcfg = config.config()
285
285
286 if read_from == 'file':
286 if read_from == 'file':
287 if not os.path.isfile(path):
287 if not os.path.isfile(path):
288 log.debug('hgrc file is not present at %s skipping...' % path)
288 log.debug('hgrc file is not present at %s skipping...' % path)
289 return False
289 return False
290 log.debug('reading hgrc from %s' % path)
290 log.debug('reading hgrc from %s' % path)
291 cfg = config.config()
291 cfg = config.config()
292 cfg.read(path)
292 cfg.read(path)
293 for section in ui_sections:
293 for section in ui_sections:
294 for k, v in cfg.items(section):
294 for k, v in cfg.items(section):
295 log.debug('settings ui from file[%s]%s:%s' % (section, k, v))
295 log.debug('settings ui from file[%s]%s:%s' % (section, k, v))
296 baseui.setconfig(section, k, v)
296 baseui.setconfig(section, k, v)
297
297
298 elif read_from == 'db':
298 elif read_from == 'db':
299 sa = meta.Session
299 sa = meta.Session
300 ret = sa.query(RhodeCodeUi)\
300 ret = sa.query(RhodeCodeUi)\
301 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
301 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
302 .all()
302 .all()
303
303
304 hg_ui = ret
304 hg_ui = ret
305 for ui_ in hg_ui:
305 for ui_ in hg_ui:
306 if ui_.ui_active:
306 if ui_.ui_active:
307 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
307 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
308 ui_.ui_key, ui_.ui_value)
308 ui_.ui_key, ui_.ui_value)
309 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
309 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
310
310
311 meta.Session.remove()
311 meta.Session.remove()
312 return baseui
312 return baseui
313
313
314
314
315 def set_rhodecode_config(config):
315 def set_rhodecode_config(config):
316 """
316 """
317 Updates pylons config with new settings from database
317 Updates pylons config with new settings from database
318
318
319 :param config:
319 :param config:
320 """
320 """
321 hgsettings = RhodeCodeSetting.get_app_settings()
321 hgsettings = RhodeCodeSetting.get_app_settings()
322
322
323 for k, v in hgsettings.items():
323 for k, v in hgsettings.items():
324 config[k] = v
324 config[k] = v
325
325
326
326
327 def invalidate_cache(cache_key, *args):
327 def invalidate_cache(cache_key, *args):
328 """
328 """
329 Puts cache invalidation task into db for
329 Puts cache invalidation task into db for
330 further global cache invalidation
330 further global cache invalidation
331 """
331 """
332
332
333 from rhodecode.model.scm import ScmModel
333 from rhodecode.model.scm import ScmModel
334
334
335 if cache_key.startswith('get_repo_cached_'):
335 if cache_key.startswith('get_repo_cached_'):
336 name = cache_key.split('get_repo_cached_')[-1]
336 name = cache_key.split('get_repo_cached_')[-1]
337 ScmModel().mark_for_invalidation(name)
337 ScmModel().mark_for_invalidation(name)
338
338
339
339
340 class EmptyChangeset(BaseChangeset):
340 class EmptyChangeset(BaseChangeset):
341 """
341 """
342 An dummy empty changeset. It's possible to pass hash when creating
342 An dummy empty changeset. It's possible to pass hash when creating
343 an EmptyChangeset
343 an EmptyChangeset
344 """
344 """
345
345
346 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
346 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
347 alias=None):
347 alias=None):
348 self._empty_cs = cs
348 self._empty_cs = cs
349 self.revision = -1
349 self.revision = -1
350 self.message = ''
350 self.message = ''
351 self.author = ''
351 self.author = ''
352 self.date = ''
352 self.date = ''
353 self.repository = repo
353 self.repository = repo
354 self.requested_revision = requested_revision
354 self.requested_revision = requested_revision
355 self.alias = alias
355 self.alias = alias
356
356
357 @LazyProperty
357 @LazyProperty
358 def raw_id(self):
358 def raw_id(self):
359 """
359 """
360 Returns raw string identifying this changeset, useful for web
360 Returns raw string identifying this changeset, useful for web
361 representation.
361 representation.
362 """
362 """
363
363
364 return self._empty_cs
364 return self._empty_cs
365
365
366 @LazyProperty
366 @LazyProperty
367 def branch(self):
367 def branch(self):
368 return get_backend(self.alias).DEFAULT_BRANCH_NAME
368 return get_backend(self.alias).DEFAULT_BRANCH_NAME
369
369
370 @LazyProperty
370 @LazyProperty
371 def short_id(self):
371 def short_id(self):
372 return self.raw_id[:12]
372 return self.raw_id[:12]
373
373
374 def get_file_changeset(self, path):
374 def get_file_changeset(self, path):
375 return self
375 return self
376
376
377 def get_file_content(self, path):
377 def get_file_content(self, path):
378 return u''
378 return u''
379
379
380 def get_file_size(self, path):
380 def get_file_size(self, path):
381 return 0
381 return 0
382
382
383
383
384 def map_groups(path):
384 def map_groups(path):
385 """
385 """
386 Given a full path to a repository, create all nested groups that this
386 Given a full path to a repository, create all nested groups that this
387 repo is inside. This function creates parent-child relationships between
387 repo is inside. This function creates parent-child relationships between
388 groups and creates default perms for all new groups.
388 groups and creates default perms for all new groups.
389
389
390 :param paths: full path to repository
390 :param paths: full path to repository
391 """
391 """
392 sa = meta.Session
392 sa = meta.Session
393 groups = path.split(Repository.url_sep())
393 groups = path.split(Repository.url_sep())
394 parent = None
394 parent = None
395 group = None
395 group = None
396
396
397 # last element is repo in nested groups structure
397 # last element is repo in nested groups structure
398 groups = groups[:-1]
398 groups = groups[:-1]
399 rgm = ReposGroupModel(sa)
399 rgm = ReposGroupModel(sa)
400 for lvl, group_name in enumerate(groups):
400 for lvl, group_name in enumerate(groups):
401 group_name = '/'.join(groups[:lvl] + [group_name])
401 group_name = '/'.join(groups[:lvl] + [group_name])
402 group = RepoGroup.get_by_group_name(group_name)
402 group = RepoGroup.get_by_group_name(group_name)
403 desc = '%s group' % group_name
403 desc = '%s group' % group_name
404
404
405 # skip folders that are now removed repos
405 # skip folders that are now removed repos
406 if REMOVED_REPO_PAT.match(group_name):
406 if REMOVED_REPO_PAT.match(group_name):
407 break
407 break
408
408
409 if group is None:
409 if group is None:
410 log.debug('creating group level: %s group_name: %s' % (lvl,
410 log.debug('creating group level: %s group_name: %s' % (lvl,
411 group_name))
411 group_name))
412 group = RepoGroup(group_name, parent)
412 group = RepoGroup(group_name, parent)
413 group.group_description = desc
413 group.group_description = desc
414 sa.add(group)
414 sa.add(group)
415 rgm._create_default_perms(group)
415 rgm._create_default_perms(group)
416 sa.flush()
416 sa.flush()
417 parent = group
417 parent = group
418 return group
418 return group
419
419
420
420
421 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
421 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
422 """
422 """
423 maps all repos given in initial_repo_list, non existing repositories
423 maps all repos given in initial_repo_list, non existing repositories
424 are created, if remove_obsolete is True it also check for db entries
424 are created, if remove_obsolete is True it also check for db entries
425 that are not in initial_repo_list and removes them.
425 that are not in initial_repo_list and removes them.
426
426
427 :param initial_repo_list: list of repositories found by scanning methods
427 :param initial_repo_list: list of repositories found by scanning methods
428 :param remove_obsolete: check for obsolete entries in database
428 :param remove_obsolete: check for obsolete entries in database
429 """
429 """
430 from rhodecode.model.repo import RepoModel
430 from rhodecode.model.repo import RepoModel
431 sa = meta.Session
431 sa = meta.Session
432 rm = RepoModel()
432 rm = RepoModel()
433 user = sa.query(User).filter(User.admin == True).first()
433 user = sa.query(User).filter(User.admin == True).first()
434 if user is None:
434 if user is None:
435 raise Exception('Missing administrative account !')
435 raise Exception('Missing administrative account !')
436 added = []
436 added = []
437
437
438 for name, repo in initial_repo_list.items():
438 for name, repo in initial_repo_list.items():
439 group = map_groups(name)
439 group = map_groups(name)
440 if not rm.get_by_repo_name(name, cache=False):
440 if not rm.get_by_repo_name(name, cache=False):
441 log.info('repository %s not found creating default' % name)
441 log.info('repository %s not found creating default' % name)
442 added.append(name)
442 added.append(name)
443 form_data = {
443 form_data = {
444 'repo_name': name,
444 'repo_name': name,
445 'repo_name_full': name,
445 'repo_name_full': name,
446 'repo_type': repo.alias,
446 'repo_type': repo.alias,
447 'description': repo.description \
447 'description': repo.description \
448 if repo.description != 'unknown' else '%s repository' % name,
448 if repo.description != 'unknown' else '%s repository' % name,
449 'private': False,
449 'private': False,
450 'group_id': getattr(group, 'group_id', None)
450 'group_id': getattr(group, 'group_id', None)
451 }
451 }
452 rm.create(form_data, user, just_db=True)
452 rm.create(form_data, user, just_db=True)
453 sa.commit()
453 sa.commit()
454 removed = []
454 removed = []
455 if remove_obsolete:
455 if remove_obsolete:
456 # remove from database those repositories that are not in the filesystem
456 # remove from database those repositories that are not in the filesystem
457 for repo in sa.query(Repository).all():
457 for repo in sa.query(Repository).all():
458 if repo.repo_name not in initial_repo_list.keys():
458 if repo.repo_name not in initial_repo_list.keys():
459 log.debug("Removing non existing repository found in db %s" %
459 log.debug("Removing non existing repository found in db %s" %
460 repo.repo_name)
460 repo.repo_name)
461 removed.append(repo.repo_name)
461 removed.append(repo.repo_name)
462 sa.delete(repo)
462 sa.delete(repo)
463 sa.commit()
463 sa.commit()
464
464
465 # clear cache keys
465 # clear cache keys
466 log.debug("Clearing cache keys now...")
466 log.debug("Clearing cache keys now...")
467 CacheInvalidation.clear_cache()
467 CacheInvalidation.clear_cache()
468 sa.commit()
468 sa.commit()
469 return added, removed
469 return added, removed
470
470
471
471
472 # set cache regions for beaker so celery can utilise it
472 # set cache regions for beaker so celery can utilise it
473 def add_cache(settings):
473 def add_cache(settings):
474 cache_settings = {'regions': None}
474 cache_settings = {'regions': None}
475 for key in settings.keys():
475 for key in settings.keys():
476 for prefix in ['beaker.cache.', 'cache.']:
476 for prefix in ['beaker.cache.', 'cache.']:
477 if key.startswith(prefix):
477 if key.startswith(prefix):
478 name = key.split(prefix)[1].strip()
478 name = key.split(prefix)[1].strip()
479 cache_settings[name] = settings[key].strip()
479 cache_settings[name] = settings[key].strip()
480 if cache_settings['regions']:
480 if cache_settings['regions']:
481 for region in cache_settings['regions'].split(','):
481 for region in cache_settings['regions'].split(','):
482 region = region.strip()
482 region = region.strip()
483 region_settings = {}
483 region_settings = {}
484 for key, value in cache_settings.items():
484 for key, value in cache_settings.items():
485 if key.startswith(region):
485 if key.startswith(region):
486 region_settings[key.split('.')[1]] = value
486 region_settings[key.split('.')[1]] = value
487 region_settings['expire'] = int(region_settings.get('expire',
487 region_settings['expire'] = int(region_settings.get('expire',
488 60))
488 60))
489 region_settings.setdefault('lock_dir',
489 region_settings.setdefault('lock_dir',
490 cache_settings.get('lock_dir'))
490 cache_settings.get('lock_dir'))
491 region_settings.setdefault('data_dir',
491 region_settings.setdefault('data_dir',
492 cache_settings.get('data_dir'))
492 cache_settings.get('data_dir'))
493
493
494 if 'type' not in region_settings:
494 if 'type' not in region_settings:
495 region_settings['type'] = cache_settings.get('type',
495 region_settings['type'] = cache_settings.get('type',
496 'memory')
496 'memory')
497 beaker.cache.cache_regions[region] = region_settings
497 beaker.cache.cache_regions[region] = region_settings
498
498
499
499
500 def load_rcextensions(root_path):
500 def load_rcextensions(root_path):
501 import rhodecode
501 import rhodecode
502 from rhodecode.config import conf
502 from rhodecode.config import conf
503
503
504 path = os.path.join(root_path, 'rcextensions', '__init__.py')
504 path = os.path.join(root_path, 'rcextensions', '__init__.py')
505 if os.path.isfile(path):
505 if os.path.isfile(path):
506 rcext = create_module('rc', path)
506 rcext = create_module('rc', path)
507 EXT = rhodecode.EXTENSIONS = rcext
507 EXT = rhodecode.EXTENSIONS = rcext
508 log.debug('Found rcextensions now loading %s...' % rcext)
508 log.debug('Found rcextensions now loading %s...' % rcext)
509
509
510 # Additional mappings that are not present in the pygments lexers
510 # Additional mappings that are not present in the pygments lexers
511 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
511 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
512
512
513 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
513 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
514
514
515 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
515 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
516 log.debug('settings custom INDEX_EXTENSIONS')
516 log.debug('settings custom INDEX_EXTENSIONS')
517 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
517 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
518
518
519 #ADDITIONAL MAPPINGS
519 #ADDITIONAL MAPPINGS
520 log.debug('adding extra into INDEX_EXTENSIONS')
520 log.debug('adding extra into INDEX_EXTENSIONS')
521 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
521 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
522
522
523
523
524 #==============================================================================
524 #==============================================================================
525 # TEST FUNCTIONS AND CREATORS
525 # TEST FUNCTIONS AND CREATORS
526 #==============================================================================
526 #==============================================================================
527 def create_test_index(repo_location, config, full_index):
527 def create_test_index(repo_location, config, full_index):
528 """
528 """
529 Makes default test index
529 Makes default test index
530
530
531 :param config: test config
531 :param config: test config
532 :param full_index:
532 :param full_index:
533 """
533 """
534
534
535 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
535 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
536 from rhodecode.lib.pidlock import DaemonLock, LockHeld
536 from rhodecode.lib.pidlock import DaemonLock, LockHeld
537
537
538 repo_location = repo_location
538 repo_location = repo_location
539
539
540 index_location = os.path.join(config['app_conf']['index_dir'])
540 index_location = os.path.join(config['app_conf']['index_dir'])
541 if not os.path.exists(index_location):
541 if not os.path.exists(index_location):
542 os.makedirs(index_location)
542 os.makedirs(index_location)
543
543
544 try:
544 try:
545 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
545 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
546 WhooshIndexingDaemon(index_location=index_location,
546 WhooshIndexingDaemon(index_location=index_location,
547 repo_location=repo_location)\
547 repo_location=repo_location)\
548 .run(full_index=full_index)
548 .run(full_index=full_index)
549 l.release()
549 l.release()
550 except LockHeld:
550 except LockHeld:
551 pass
551 pass
552
552
553
553
554 def create_test_env(repos_test_path, config):
554 def create_test_env(repos_test_path, config):
555 """
555 """
556 Makes a fresh database and
556 Makes a fresh database and
557 install test repository into tmp dir
557 install test repository into tmp dir
558 """
558 """
559 from rhodecode.lib.db_manage import DbManage
559 from rhodecode.lib.db_manage import DbManage
560 from rhodecode.tests import HG_REPO, TESTS_TMP_PATH
560 from rhodecode.tests import HG_REPO, TESTS_TMP_PATH
561
561
562 # PART ONE create db
562 # PART ONE create db
563 dbconf = config['sqlalchemy.db1.url']
563 dbconf = config['sqlalchemy.db1.url']
564 log.debug('making test db %s' % dbconf)
564 log.debug('making test db %s' % dbconf)
565
565
566 # create test dir if it doesn't exist
566 # create test dir if it doesn't exist
567 if not os.path.isdir(repos_test_path):
567 if not os.path.isdir(repos_test_path):
568 log.debug('Creating testdir %s' % repos_test_path)
568 log.debug('Creating testdir %s' % repos_test_path)
569 os.makedirs(repos_test_path)
569 os.makedirs(repos_test_path)
570
570
571 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
571 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
572 tests=True)
572 tests=True)
573 dbmanage.create_tables(override=True)
573 dbmanage.create_tables(override=True)
574 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
574 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
575 dbmanage.create_default_user()
575 dbmanage.create_default_user()
576 dbmanage.admin_prompt()
576 dbmanage.admin_prompt()
577 dbmanage.create_permissions()
577 dbmanage.create_permissions()
578 dbmanage.populate_default_permissions()
578 dbmanage.populate_default_permissions()
579 Session.commit()
579 Session.commit()
580 # PART TWO make test repo
580 # PART TWO make test repo
581 log.debug('making test vcs repositories')
581 log.debug('making test vcs repositories')
582
582
583 idx_path = config['app_conf']['index_dir']
583 idx_path = config['app_conf']['index_dir']
584 data_path = config['app_conf']['cache_dir']
584 data_path = config['app_conf']['cache_dir']
585
585
586 #clean index and data
586 #clean index and data
587 if idx_path and os.path.exists(idx_path):
587 if idx_path and os.path.exists(idx_path):
588 log.debug('remove %s' % idx_path)
588 log.debug('remove %s' % idx_path)
589 shutil.rmtree(idx_path)
589 shutil.rmtree(idx_path)
590
590
591 if data_path and os.path.exists(data_path):
591 if data_path and os.path.exists(data_path):
592 log.debug('remove %s' % data_path)
592 log.debug('remove %s' % data_path)
593 shutil.rmtree(data_path)
593 shutil.rmtree(data_path)
594
594
595 #CREATE DEFAULT HG REPOSITORY
595 #CREATE DEFAULT HG REPOSITORY
596 cur_dir = dn(dn(abspath(__file__)))
596 cur_dir = dn(dn(abspath(__file__)))
597 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
597 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
598 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
598 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
599 tar.close()
599 tar.close()
600
600
601
601
602 #==============================================================================
602 #==============================================================================
603 # PASTER COMMANDS
603 # PASTER COMMANDS
604 #==============================================================================
604 #==============================================================================
605 class BasePasterCommand(Command):
605 class BasePasterCommand(Command):
606 """
606 """
607 Abstract Base Class for paster commands.
607 Abstract Base Class for paster commands.
608
608
609 The celery commands are somewhat aggressive about loading
609 The celery commands are somewhat aggressive about loading
610 celery.conf, and since our module sets the `CELERY_LOADER`
610 celery.conf, and since our module sets the `CELERY_LOADER`
611 environment variable to our loader, we have to bootstrap a bit and
611 environment variable to our loader, we have to bootstrap a bit and
612 make sure we've had a chance to load the pylons config off of the
612 make sure we've had a chance to load the pylons config off of the
613 command line, otherwise everything fails.
613 command line, otherwise everything fails.
614 """
614 """
615 min_args = 1
615 min_args = 1
616 min_args_error = "Please provide a paster config file as an argument."
616 min_args_error = "Please provide a paster config file as an argument."
617 takes_config_file = 1
617 takes_config_file = 1
618 requires_config_file = True
618 requires_config_file = True
619
619
620 def notify_msg(self, msg, log=False):
620 def notify_msg(self, msg, log=False):
621 """Make a notification to user, additionally if logger is passed
621 """Make a notification to user, additionally if logger is passed
622 it logs this action using given logger
622 it logs this action using given logger
623
623
624 :param msg: message that will be printed to user
624 :param msg: message that will be printed to user
625 :param log: logging instance, to use to additionally log this message
625 :param log: logging instance, to use to additionally log this message
626
626
627 """
627 """
628 if log and isinstance(log, logging):
628 if log and isinstance(log, logging):
629 log(msg)
629 log(msg)
630
630
631 def run(self, args):
631 def run(self, args):
632 """
632 """
633 Overrides Command.run
633 Overrides Command.run
634
634
635 Checks for a config file argument and loads it.
635 Checks for a config file argument and loads it.
636 """
636 """
637 if len(args) < self.min_args:
637 if len(args) < self.min_args:
638 raise BadCommand(
638 raise BadCommand(
639 self.min_args_error % {'min_args': self.min_args,
639 self.min_args_error % {'min_args': self.min_args,
640 'actual_args': len(args)})
640 'actual_args': len(args)})
641
641
642 # Decrement because we're going to lob off the first argument.
642 # Decrement because we're going to lob off the first argument.
643 # @@ This is hacky
643 # @@ This is hacky
644 self.min_args -= 1
644 self.min_args -= 1
645 self.bootstrap_config(args[0])
645 self.bootstrap_config(args[0])
646 self.update_parser()
646 self.update_parser()
647 return super(BasePasterCommand, self).run(args[1:])
647 return super(BasePasterCommand, self).run(args[1:])
648
648
649 def update_parser(self):
649 def update_parser(self):
650 """
650 """
651 Abstract method. Allows for the class's parser to be updated
651 Abstract method. Allows for the class's parser to be updated
652 before the superclass's `run` method is called. Necessary to
652 before the superclass's `run` method is called. Necessary to
653 allow options/arguments to be passed through to the underlying
653 allow options/arguments to be passed through to the underlying
654 celery command.
654 celery command.
655 """
655 """
656 raise NotImplementedError("Abstract Method.")
656 raise NotImplementedError("Abstract Method.")
657
657
658 def bootstrap_config(self, conf):
658 def bootstrap_config(self, conf):
659 """
659 """
660 Loads the pylons configuration.
660 Loads the pylons configuration.
661 """
661 """
662 from pylons import config as pylonsconfig
662 from pylons import config as pylonsconfig
663
663
664 self.path_to_ini_file = os.path.realpath(conf)
664 self.path_to_ini_file = os.path.realpath(conf)
665 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
665 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
666 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
666 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
@@ -1,408 +1,407 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.utils
3 rhodecode.lib.utils
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Some simple helper functions
6 Some simple helper functions
7
7
8 :created_on: Jan 5, 2011
8 :created_on: Jan 5, 2011
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import re
26 import re
27 from rhodecode.lib.vcs.utils.lazy import LazyProperty
27 from rhodecode.lib.vcs.utils.lazy import LazyProperty
28
28
29
29
30 def __get_lem():
30 def __get_lem():
31 """
31 """
32 Get language extension map based on what's inside pygments lexers
32 Get language extension map based on what's inside pygments lexers
33 """
33 """
34 from pygments import lexers
34 from pygments import lexers
35 from string import lower
35 from string import lower
36 from collections import defaultdict
36 from collections import defaultdict
37
37
38 d = defaultdict(lambda: [])
38 d = defaultdict(lambda: [])
39
39
40 def __clean(s):
40 def __clean(s):
41 s = s.lstrip('*')
41 s = s.lstrip('*')
42 s = s.lstrip('.')
42 s = s.lstrip('.')
43
43
44 if s.find('[') != -1:
44 if s.find('[') != -1:
45 exts = []
45 exts = []
46 start, stop = s.find('['), s.find(']')
46 start, stop = s.find('['), s.find(']')
47
47
48 for suffix in s[start + 1:stop]:
48 for suffix in s[start + 1:stop]:
49 exts.append(s[:s.find('[')] + suffix)
49 exts.append(s[:s.find('[')] + suffix)
50 return map(lower, exts)
50 return map(lower, exts)
51 else:
51 else:
52 return map(lower, [s])
52 return map(lower, [s])
53
53
54 for lx, t in sorted(lexers.LEXERS.items()):
54 for lx, t in sorted(lexers.LEXERS.items()):
55 m = map(__clean, t[-2])
55 m = map(__clean, t[-2])
56 if m:
56 if m:
57 m = reduce(lambda x, y: x + y, m)
57 m = reduce(lambda x, y: x + y, m)
58 for ext in m:
58 for ext in m:
59 desc = lx.replace('Lexer', '')
59 desc = lx.replace('Lexer', '')
60 d[ext].append(desc)
60 d[ext].append(desc)
61
61
62 return dict(d)
62 return dict(d)
63
63
64 def str2bool(_str):
64 def str2bool(_str):
65 """
65 """
66 returs True/False value from given string, it tries to translate the
66 returs True/False value from given string, it tries to translate the
67 string into boolean
67 string into boolean
68
68
69 :param _str: string value to translate into boolean
69 :param _str: string value to translate into boolean
70 :rtype: boolean
70 :rtype: boolean
71 :returns: boolean from given string
71 :returns: boolean from given string
72 """
72 """
73 if _str is None:
73 if _str is None:
74 return False
74 return False
75 if _str in (True, False):
75 if _str in (True, False):
76 return _str
76 return _str
77 _str = str(_str).strip().lower()
77 _str = str(_str).strip().lower()
78 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
78 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
79
79
80
80
81 def convert_line_endings(line, mode):
81 def convert_line_endings(line, mode):
82 """
82 """
83 Converts a given line "line end" accordingly to given mode
83 Converts a given line "line end" accordingly to given mode
84
84
85 Available modes are::
85 Available modes are::
86 0 - Unix
86 0 - Unix
87 1 - Mac
87 1 - Mac
88 2 - DOS
88 2 - DOS
89
89
90 :param line: given line to convert
90 :param line: given line to convert
91 :param mode: mode to convert to
91 :param mode: mode to convert to
92 :rtype: str
92 :rtype: str
93 :return: converted line according to mode
93 :return: converted line according to mode
94 """
94 """
95 from string import replace
95 from string import replace
96
96
97 if mode == 0:
97 if mode == 0:
98 line = replace(line, '\r\n', '\n')
98 line = replace(line, '\r\n', '\n')
99 line = replace(line, '\r', '\n')
99 line = replace(line, '\r', '\n')
100 elif mode == 1:
100 elif mode == 1:
101 line = replace(line, '\r\n', '\r')
101 line = replace(line, '\r\n', '\r')
102 line = replace(line, '\n', '\r')
102 line = replace(line, '\n', '\r')
103 elif mode == 2:
103 elif mode == 2:
104 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
104 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
105 return line
105 return line
106
106
107
107
108 def detect_mode(line, default):
108 def detect_mode(line, default):
109 """
109 """
110 Detects line break for given line, if line break couldn't be found
110 Detects line break for given line, if line break couldn't be found
111 given default value is returned
111 given default value is returned
112
112
113 :param line: str line
113 :param line: str line
114 :param default: default
114 :param default: default
115 :rtype: int
115 :rtype: int
116 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
116 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
117 """
117 """
118 if line.endswith('\r\n'):
118 if line.endswith('\r\n'):
119 return 2
119 return 2
120 elif line.endswith('\n'):
120 elif line.endswith('\n'):
121 return 0
121 return 0
122 elif line.endswith('\r'):
122 elif line.endswith('\r'):
123 return 1
123 return 1
124 else:
124 else:
125 return default
125 return default
126
126
127
127
128 def generate_api_key(username, salt=None):
128 def generate_api_key(username, salt=None):
129 """
129 """
130 Generates unique API key for given username, if salt is not given
130 Generates unique API key for given username, if salt is not given
131 it'll be generated from some random string
131 it'll be generated from some random string
132
132
133 :param username: username as string
133 :param username: username as string
134 :param salt: salt to hash generate KEY
134 :param salt: salt to hash generate KEY
135 :rtype: str
135 :rtype: str
136 :returns: sha1 hash from username+salt
136 :returns: sha1 hash from username+salt
137 """
137 """
138 from tempfile import _RandomNameSequence
138 from tempfile import _RandomNameSequence
139 import hashlib
139 import hashlib
140
140
141 if salt is None:
141 if salt is None:
142 salt = _RandomNameSequence().next()
142 salt = _RandomNameSequence().next()
143
143
144 return hashlib.sha1(username + salt).hexdigest()
144 return hashlib.sha1(username + salt).hexdigest()
145
145
146
146
147 def safe_unicode(str_, from_encoding=None):
147 def safe_unicode(str_, from_encoding=None):
148 """
148 """
149 safe unicode function. Does few trick to turn str_ into unicode
149 safe unicode function. Does few trick to turn str_ into unicode
150
150
151 In case of UnicodeDecode error we try to return it with encoding detected
151 In case of UnicodeDecode error we try to return it with encoding detected
152 by chardet library if it fails fallback to unicode with errors replaced
152 by chardet library if it fails fallback to unicode with errors replaced
153
153
154 :param str_: string to decode
154 :param str_: string to decode
155 :rtype: unicode
155 :rtype: unicode
156 :returns: unicode object
156 :returns: unicode object
157 """
157 """
158 if isinstance(str_, unicode):
158 if isinstance(str_, unicode):
159 return str_
159 return str_
160
160
161 if not from_encoding:
161 if not from_encoding:
162 import rhodecode
162 import rhodecode
163 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
163 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
164 from_encoding = DEFAULT_ENCODING
164 from_encoding = DEFAULT_ENCODING
165
165
166 try:
166 try:
167 return unicode(str_)
167 return unicode(str_)
168 except UnicodeDecodeError:
168 except UnicodeDecodeError:
169 pass
169 pass
170
170
171 try:
171 try:
172 return unicode(str_, from_encoding)
172 return unicode(str_, from_encoding)
173 except UnicodeDecodeError:
173 except UnicodeDecodeError:
174 pass
174 pass
175
175
176 try:
176 try:
177 import chardet
177 import chardet
178 encoding = chardet.detect(str_)['encoding']
178 encoding = chardet.detect(str_)['encoding']
179 if encoding is None:
179 if encoding is None:
180 raise Exception()
180 raise Exception()
181 return str_.decode(encoding)
181 return str_.decode(encoding)
182 except (ImportError, UnicodeDecodeError, Exception):
182 except (ImportError, UnicodeDecodeError, Exception):
183 return unicode(str_, from_encoding, 'replace')
183 return unicode(str_, from_encoding, 'replace')
184
184
185
185
186 def safe_str(unicode_, to_encoding=None):
186 def safe_str(unicode_, to_encoding=None):
187 """
187 """
188 safe str function. Does few trick to turn unicode_ into string
188 safe str function. Does few trick to turn unicode_ into string
189
189
190 In case of UnicodeEncodeError we try to return it with encoding detected
190 In case of UnicodeEncodeError we try to return it with encoding detected
191 by chardet library if it fails fallback to string with errors replaced
191 by chardet library if it fails fallback to string with errors replaced
192
192
193 :param unicode_: unicode to encode
193 :param unicode_: unicode to encode
194 :rtype: str
194 :rtype: str
195 :returns: str object
195 :returns: str object
196 """
196 """
197
197
198 # if it's not basestr cast to str
198 # if it's not basestr cast to str
199 if not isinstance(unicode_, basestring):
199 if not isinstance(unicode_, basestring):
200 return str(unicode_)
200 return str(unicode_)
201
201
202 if isinstance(unicode_, str):
202 if isinstance(unicode_, str):
203 return unicode_
203 return unicode_
204
204
205 if not to_encoding:
205 if not to_encoding:
206 import rhodecode
206 import rhodecode
207 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
207 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
208 to_encoding = DEFAULT_ENCODING
208 to_encoding = DEFAULT_ENCODING
209
209
210 try:
210 try:
211 return unicode_.encode(to_encoding)
211 return unicode_.encode(to_encoding)
212 except UnicodeEncodeError:
212 except UnicodeEncodeError:
213 pass
213 pass
214
214
215 try:
215 try:
216 import chardet
216 import chardet
217 encoding = chardet.detect(unicode_)['encoding']
217 encoding = chardet.detect(unicode_)['encoding']
218 print encoding
219 if encoding is None:
218 if encoding is None:
220 raise UnicodeEncodeError()
219 raise UnicodeEncodeError()
221
220
222 return unicode_.encode(encoding)
221 return unicode_.encode(encoding)
223 except (ImportError, UnicodeEncodeError):
222 except (ImportError, UnicodeEncodeError):
224 return unicode_.encode(to_encoding, 'replace')
223 return unicode_.encode(to_encoding, 'replace')
225
224
226 return safe_str
225 return safe_str
227
226
228
227
229 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
228 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
230 """
229 """
231 Custom engine_from_config functions that makes sure we use NullPool for
230 Custom engine_from_config functions that makes sure we use NullPool for
232 file based sqlite databases. This prevents errors on sqlite. This only
231 file based sqlite databases. This prevents errors on sqlite. This only
233 applies to sqlalchemy versions < 0.7.0
232 applies to sqlalchemy versions < 0.7.0
234
233
235 """
234 """
236 import sqlalchemy
235 import sqlalchemy
237 from sqlalchemy import engine_from_config as efc
236 from sqlalchemy import engine_from_config as efc
238 import logging
237 import logging
239
238
240 if int(sqlalchemy.__version__.split('.')[1]) < 7:
239 if int(sqlalchemy.__version__.split('.')[1]) < 7:
241
240
242 # This solution should work for sqlalchemy < 0.7.0, and should use
241 # This solution should work for sqlalchemy < 0.7.0, and should use
243 # proxy=TimerProxy() for execution time profiling
242 # proxy=TimerProxy() for execution time profiling
244
243
245 from sqlalchemy.pool import NullPool
244 from sqlalchemy.pool import NullPool
246 url = configuration[prefix + 'url']
245 url = configuration[prefix + 'url']
247
246
248 if url.startswith('sqlite'):
247 if url.startswith('sqlite'):
249 kwargs.update({'poolclass': NullPool})
248 kwargs.update({'poolclass': NullPool})
250 return efc(configuration, prefix, **kwargs)
249 return efc(configuration, prefix, **kwargs)
251 else:
250 else:
252 import time
251 import time
253 from sqlalchemy import event
252 from sqlalchemy import event
254 from sqlalchemy.engine import Engine
253 from sqlalchemy.engine import Engine
255
254
256 log = logging.getLogger('sqlalchemy.engine')
255 log = logging.getLogger('sqlalchemy.engine')
257 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
256 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
258 engine = efc(configuration, prefix, **kwargs)
257 engine = efc(configuration, prefix, **kwargs)
259
258
260 def color_sql(sql):
259 def color_sql(sql):
261 COLOR_SEQ = "\033[1;%dm"
260 COLOR_SEQ = "\033[1;%dm"
262 COLOR_SQL = YELLOW
261 COLOR_SQL = YELLOW
263 normal = '\x1b[0m'
262 normal = '\x1b[0m'
264 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
263 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
265
264
266 if configuration['debug']:
265 if configuration['debug']:
267 #attach events only for debug configuration
266 #attach events only for debug configuration
268
267
269 def before_cursor_execute(conn, cursor, statement,
268 def before_cursor_execute(conn, cursor, statement,
270 parameters, context, executemany):
269 parameters, context, executemany):
271 context._query_start_time = time.time()
270 context._query_start_time = time.time()
272 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
271 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
273
272
274
273
275 def after_cursor_execute(conn, cursor, statement,
274 def after_cursor_execute(conn, cursor, statement,
276 parameters, context, executemany):
275 parameters, context, executemany):
277 total = time.time() - context._query_start_time
276 total = time.time() - context._query_start_time
278 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
277 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
279
278
280 event.listen(engine, "before_cursor_execute",
279 event.listen(engine, "before_cursor_execute",
281 before_cursor_execute)
280 before_cursor_execute)
282 event.listen(engine, "after_cursor_execute",
281 event.listen(engine, "after_cursor_execute",
283 after_cursor_execute)
282 after_cursor_execute)
284
283
285 return engine
284 return engine
286
285
287
286
288 def age(curdate):
287 def age(curdate):
289 """
288 """
290 turns a datetime into an age string.
289 turns a datetime into an age string.
291
290
292 :param curdate: datetime object
291 :param curdate: datetime object
293 :rtype: unicode
292 :rtype: unicode
294 :returns: unicode words describing age
293 :returns: unicode words describing age
295 """
294 """
296
295
297 from datetime import datetime
296 from datetime import datetime
298 from webhelpers.date import time_ago_in_words
297 from webhelpers.date import time_ago_in_words
299
298
300 _ = lambda s: s
299 _ = lambda s: s
301
300
302 if not curdate:
301 if not curdate:
303 return ''
302 return ''
304
303
305 agescales = [(_(u"year"), 3600 * 24 * 365),
304 agescales = [(_(u"year"), 3600 * 24 * 365),
306 (_(u"month"), 3600 * 24 * 30),
305 (_(u"month"), 3600 * 24 * 30),
307 (_(u"day"), 3600 * 24),
306 (_(u"day"), 3600 * 24),
308 (_(u"hour"), 3600),
307 (_(u"hour"), 3600),
309 (_(u"minute"), 60),
308 (_(u"minute"), 60),
310 (_(u"second"), 1), ]
309 (_(u"second"), 1), ]
311
310
312 age = datetime.now() - curdate
311 age = datetime.now() - curdate
313 age_seconds = (age.days * agescales[2][1]) + age.seconds
312 age_seconds = (age.days * agescales[2][1]) + age.seconds
314 pos = 1
313 pos = 1
315 for scale in agescales:
314 for scale in agescales:
316 if scale[1] <= age_seconds:
315 if scale[1] <= age_seconds:
317 if pos == 6:
316 if pos == 6:
318 pos = 5
317 pos = 5
319 return '%s %s' % (time_ago_in_words(curdate,
318 return '%s %s' % (time_ago_in_words(curdate,
320 agescales[pos][0]), _('ago'))
319 agescales[pos][0]), _('ago'))
321 pos += 1
320 pos += 1
322
321
323 return _(u'just now')
322 return _(u'just now')
324
323
325
324
326 def uri_filter(uri):
325 def uri_filter(uri):
327 """
326 """
328 Removes user:password from given url string
327 Removes user:password from given url string
329
328
330 :param uri:
329 :param uri:
331 :rtype: unicode
330 :rtype: unicode
332 :returns: filtered list of strings
331 :returns: filtered list of strings
333 """
332 """
334 if not uri:
333 if not uri:
335 return ''
334 return ''
336
335
337 proto = ''
336 proto = ''
338
337
339 for pat in ('https://', 'http://'):
338 for pat in ('https://', 'http://'):
340 if uri.startswith(pat):
339 if uri.startswith(pat):
341 uri = uri[len(pat):]
340 uri = uri[len(pat):]
342 proto = pat
341 proto = pat
343 break
342 break
344
343
345 # remove passwords and username
344 # remove passwords and username
346 uri = uri[uri.find('@') + 1:]
345 uri = uri[uri.find('@') + 1:]
347
346
348 # get the port
347 # get the port
349 cred_pos = uri.find(':')
348 cred_pos = uri.find(':')
350 if cred_pos == -1:
349 if cred_pos == -1:
351 host, port = uri, None
350 host, port = uri, None
352 else:
351 else:
353 host, port = uri[:cred_pos], uri[cred_pos + 1:]
352 host, port = uri[:cred_pos], uri[cred_pos + 1:]
354
353
355 return filter(None, [proto, host, port])
354 return filter(None, [proto, host, port])
356
355
357
356
358 def credentials_filter(uri):
357 def credentials_filter(uri):
359 """
358 """
360 Returns a url with removed credentials
359 Returns a url with removed credentials
361
360
362 :param uri:
361 :param uri:
363 """
362 """
364
363
365 uri = uri_filter(uri)
364 uri = uri_filter(uri)
366 #check if we have port
365 #check if we have port
367 if len(uri) > 2 and uri[2]:
366 if len(uri) > 2 and uri[2]:
368 uri[2] = ':' + uri[2]
367 uri[2] = ':' + uri[2]
369
368
370 return ''.join(uri)
369 return ''.join(uri)
371
370
372
371
373 def get_changeset_safe(repo, rev):
372 def get_changeset_safe(repo, rev):
374 """
373 """
375 Safe version of get_changeset if this changeset doesn't exists for a
374 Safe version of get_changeset if this changeset doesn't exists for a
376 repo it returns a Dummy one instead
375 repo it returns a Dummy one instead
377
376
378 :param repo:
377 :param repo:
379 :param rev:
378 :param rev:
380 """
379 """
381 from rhodecode.lib.vcs.backends.base import BaseRepository
380 from rhodecode.lib.vcs.backends.base import BaseRepository
382 from rhodecode.lib.vcs.exceptions import RepositoryError
381 from rhodecode.lib.vcs.exceptions import RepositoryError
383 if not isinstance(repo, BaseRepository):
382 if not isinstance(repo, BaseRepository):
384 raise Exception('You must pass an Repository '
383 raise Exception('You must pass an Repository '
385 'object as first argument got %s', type(repo))
384 'object as first argument got %s', type(repo))
386
385
387 try:
386 try:
388 cs = repo.get_changeset(rev)
387 cs = repo.get_changeset(rev)
389 except RepositoryError:
388 except RepositoryError:
390 from rhodecode.lib.utils import EmptyChangeset
389 from rhodecode.lib.utils import EmptyChangeset
391 cs = EmptyChangeset(requested_revision=rev)
390 cs = EmptyChangeset(requested_revision=rev)
392 return cs
391 return cs
393
392
394
393
395 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
394 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
396
395
397
396
398 def extract_mentioned_users(s):
397 def extract_mentioned_users(s):
399 """
398 """
400 Returns unique usernames from given string s that have @mention
399 Returns unique usernames from given string s that have @mention
401
400
402 :param s: string to get mentions
401 :param s: string to get mentions
403 """
402 """
404 usrs = set()
403 usrs = set()
405 for username in re.findall(MENTIONS_REGEX, s):
404 for username in re.findall(MENTIONS_REGEX, s):
406 usrs.add(username)
405 usrs.add(username)
407
406
408 return sorted(list(usrs), key=lambda k: k.lower())
407 return sorted(list(usrs), key=lambda k: k.lower())
@@ -1,460 +1,460 b''
1 import re
1 import re
2 from itertools import chain
2 from itertools import chain
3 from dulwich import objects
3 from dulwich import objects
4 from subprocess import Popen, PIPE
4 from subprocess import Popen, PIPE
5 from rhodecode.lib.vcs.conf import settings
5 from rhodecode.lib.vcs.conf import settings
6 from rhodecode.lib.vcs.exceptions import RepositoryError
6 from rhodecode.lib.vcs.exceptions import RepositoryError
7 from rhodecode.lib.vcs.exceptions import ChangesetError
7 from rhodecode.lib.vcs.exceptions import ChangesetError
8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 from rhodecode.lib.vcs.exceptions import VCSError
9 from rhodecode.lib.vcs.exceptions import VCSError
10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 from rhodecode.lib.vcs.backends.base import BaseChangeset
12 from rhodecode.lib.vcs.backends.base import BaseChangeset
13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
14 RemovedFileNode, SubModuleNode
14 RemovedFileNode, SubModuleNode
15 from rhodecode.lib.vcs.utils import safe_unicode
15 from rhodecode.lib.vcs.utils import safe_unicode
16 from rhodecode.lib.vcs.utils import date_fromtimestamp
16 from rhodecode.lib.vcs.utils import date_fromtimestamp
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18
18
19
19
20 class GitChangeset(BaseChangeset):
20 class GitChangeset(BaseChangeset):
21 """
21 """
22 Represents state of the repository at single revision.
22 Represents state of the repository at single revision.
23 """
23 """
24
24
25 def __init__(self, repository, revision):
25 def __init__(self, repository, revision):
26 self._stat_modes = {}
26 self._stat_modes = {}
27 self.repository = repository
27 self.repository = repository
28 self.raw_id = revision
28 self.raw_id = revision
29 self.revision = repository.revisions.index(revision)
29 self.revision = repository.revisions.index(revision)
30
30
31 self.short_id = self.raw_id[:12]
31 self.short_id = self.raw_id[:12]
32 self.id = self.raw_id
32 self.id = self.raw_id
33 try:
33 try:
34 commit = self.repository._repo.get_object(self.raw_id)
34 commit = self.repository._repo.get_object(self.raw_id)
35 except KeyError:
35 except KeyError:
36 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
36 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
37 self._commit = commit
37 self._commit = commit
38 self._tree_id = commit.tree
38 self._tree_id = commit.tree
39
39
40 try:
40 try:
41 self.message = safe_unicode(commit.message[:-1])
41 self.message = safe_unicode(commit.message[:-1])
42 # Always strip last eol
42 # Always strip last eol
43 except UnicodeDecodeError:
43 except UnicodeDecodeError:
44 self.message = commit.message[:-1].decode(commit.encoding
44 self.message = commit.message[:-1].decode(commit.encoding
45 or 'utf-8')
45 or 'utf-8')
46 #self.branch = None
46 #self.branch = None
47 self.tags = []
47 self.tags = []
48 #tree = self.repository.get_object(self._tree_id)
48 #tree = self.repository.get_object(self._tree_id)
49 self.nodes = {}
49 self.nodes = {}
50 self._paths = {}
50 self._paths = {}
51
51
52 @LazyProperty
52 @LazyProperty
53 def author(self):
53 def author(self):
54 return safe_unicode(self._commit.committer)
54 return safe_unicode(self._commit.committer)
55
55
56 @LazyProperty
56 @LazyProperty
57 def date(self):
57 def date(self):
58 return date_fromtimestamp(self._commit.commit_time,
58 return date_fromtimestamp(self._commit.commit_time,
59 self._commit.commit_timezone)
59 self._commit.commit_timezone)
60
60
61 @LazyProperty
61 @LazyProperty
62 def status(self):
62 def status(self):
63 """
63 """
64 Returns modified, added, removed, deleted files for current changeset
64 Returns modified, added, removed, deleted files for current changeset
65 """
65 """
66 return self.changed, self.added, self.removed
66 return self.changed, self.added, self.removed
67
67
68 @LazyProperty
68 @LazyProperty
69 def branch(self):
69 def branch(self):
70
70
71 heads = self.repository._heads(reverse=False)
71 heads = self.repository._heads(reverse=False)
72
72
73 ref = heads.get(self.raw_id)
73 ref = heads.get(self.raw_id)
74 if ref:
74 if ref:
75 return safe_unicode(ref)
75 return safe_unicode(ref)
76
76
77 def _fix_path(self, path):
77 def _fix_path(self, path):
78 """
78 """
79 Paths are stored without trailing slash so we need to get rid off it if
79 Paths are stored without trailing slash so we need to get rid off it if
80 needed.
80 needed.
81 """
81 """
82 if path.endswith('/'):
82 if path.endswith('/'):
83 path = path.rstrip('/')
83 path = path.rstrip('/')
84 return path
84 return path
85
85
86 def _get_id_for_path(self, path):
86 def _get_id_for_path(self, path):
87
87
88 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
88 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
89 if not path in self._paths:
89 if not path in self._paths:
90 path = path.strip('/')
90 path = path.strip('/')
91 # set root tree
91 # set root tree
92 tree = self.repository._repo[self._commit.tree]
92 tree = self.repository._repo[self._commit.tree]
93 if path == '':
93 if path == '':
94 self._paths[''] = tree.id
94 self._paths[''] = tree.id
95 return tree.id
95 return tree.id
96 splitted = path.split('/')
96 splitted = path.split('/')
97 dirs, name = splitted[:-1], splitted[-1]
97 dirs, name = splitted[:-1], splitted[-1]
98 curdir = ''
98 curdir = ''
99
99
100 # initially extract things from root dir
100 # initially extract things from root dir
101 for item, stat, id in tree.iteritems():
101 for item, stat, id in tree.iteritems():
102 if curdir:
102 if curdir:
103 name = '/'.join((curdir, item))
103 name = '/'.join((curdir, item))
104 else:
104 else:
105 name = item
105 name = item
106 self._paths[name] = id
106 self._paths[name] = id
107 self._stat_modes[name] = stat
107 self._stat_modes[name] = stat
108
108
109 for dir in dirs:
109 for dir in dirs:
110 if curdir:
110 if curdir:
111 curdir = '/'.join((curdir, dir))
111 curdir = '/'.join((curdir, dir))
112 else:
112 else:
113 curdir = dir
113 curdir = dir
114 dir_id = None
114 dir_id = None
115 for item, stat, id in tree.iteritems():
115 for item, stat, id in tree.iteritems():
116 if dir == item:
116 if dir == item:
117 dir_id = id
117 dir_id = id
118 if dir_id:
118 if dir_id:
119 # Update tree
119 # Update tree
120 tree = self.repository._repo[dir_id]
120 tree = self.repository._repo[dir_id]
121 if not isinstance(tree, objects.Tree):
121 if not isinstance(tree, objects.Tree):
122 raise ChangesetError('%s is not a directory' % curdir)
122 raise ChangesetError('%s is not a directory' % curdir)
123 else:
123 else:
124 raise ChangesetError('%s have not been found' % curdir)
124 raise ChangesetError('%s have not been found' % curdir)
125
125
126 # cache all items from the given traversed tree
126 # cache all items from the given traversed tree
127 for item, stat, id in tree.iteritems():
127 for item, stat, id in tree.iteritems():
128 if curdir:
128 if curdir:
129 name = '/'.join((curdir, item))
129 name = '/'.join((curdir, item))
130 else:
130 else:
131 name = item
131 name = item
132 self._paths[name] = id
132 self._paths[name] = id
133 self._stat_modes[name] = stat
133 self._stat_modes[name] = stat
134 if not path in self._paths:
134 if not path in self._paths:
135 raise NodeDoesNotExistError("There is no file nor directory "
135 raise NodeDoesNotExistError("There is no file nor directory "
136 "at the given path %r at revision %r"
136 "at the given path %r at revision %r"
137 % (path, self.short_id))
137 % (path, self.short_id))
138 return self._paths[path]
138 return self._paths[path]
139
139
140 def _get_kind(self, path):
140 def _get_kind(self, path):
141 id = self._get_id_for_path(path)
141 id = self._get_id_for_path(path)
142 obj = self.repository._repo[id]
142 obj = self.repository._repo[id]
143 if isinstance(obj, objects.Blob):
143 if isinstance(obj, objects.Blob):
144 return NodeKind.FILE
144 return NodeKind.FILE
145 elif isinstance(obj, objects.Tree):
145 elif isinstance(obj, objects.Tree):
146 return NodeKind.DIR
146 return NodeKind.DIR
147
147
148 def _get_file_nodes(self):
148 def _get_file_nodes(self):
149 return chain(*(t[2] for t in self.walk()))
149 return chain(*(t[2] for t in self.walk()))
150
150
151 @LazyProperty
151 @LazyProperty
152 def parents(self):
152 def parents(self):
153 """
153 """
154 Returns list of parents changesets.
154 Returns list of parents changesets.
155 """
155 """
156 return [self.repository.get_changeset(parent)
156 return [self.repository.get_changeset(parent)
157 for parent in self._commit.parents]
157 for parent in self._commit.parents]
158
158
159 def next(self, branch=None):
159 def next(self, branch=None):
160
160
161 if branch and self.branch != branch:
161 if branch and self.branch != branch:
162 raise VCSError('Branch option used on changeset not belonging '
162 raise VCSError('Branch option used on changeset not belonging '
163 'to that branch')
163 'to that branch')
164
164
165 def _next(changeset, branch):
165 def _next(changeset, branch):
166 try:
166 try:
167 next_ = changeset.revision + 1
167 next_ = changeset.revision + 1
168 next_rev = changeset.repository.revisions[next_]
168 next_rev = changeset.repository.revisions[next_]
169 except IndexError:
169 except IndexError:
170 raise ChangesetDoesNotExistError
170 raise ChangesetDoesNotExistError
171 cs = changeset.repository.get_changeset(next_rev)
171 cs = changeset.repository.get_changeset(next_rev)
172
172
173 if branch and branch != cs.branch:
173 if branch and branch != cs.branch:
174 return _next(cs, branch)
174 return _next(cs, branch)
175
175
176 return cs
176 return cs
177
177
178 return _next(self, branch)
178 return _next(self, branch)
179
179
180 def prev(self, branch=None):
180 def prev(self, branch=None):
181 if branch and self.branch != branch:
181 if branch and self.branch != branch:
182 raise VCSError('Branch option used on changeset not belonging '
182 raise VCSError('Branch option used on changeset not belonging '
183 'to that branch')
183 'to that branch')
184
184
185 def _prev(changeset, branch):
185 def _prev(changeset, branch):
186 try:
186 try:
187 prev_ = changeset.revision - 1
187 prev_ = changeset.revision - 1
188 if prev_ < 0:
188 if prev_ < 0:
189 raise IndexError
189 raise IndexError
190 prev_rev = changeset.repository.revisions[prev_]
190 prev_rev = changeset.repository.revisions[prev_]
191 except IndexError:
191 except IndexError:
192 raise ChangesetDoesNotExistError
192 raise ChangesetDoesNotExistError
193
193
194 cs = changeset.repository.get_changeset(prev_rev)
194 cs = changeset.repository.get_changeset(prev_rev)
195
195
196 if branch and branch != cs.branch:
196 if branch and branch != cs.branch:
197 return _prev(cs, branch)
197 return _prev(cs, branch)
198
198
199 return cs
199 return cs
200
200
201 return _prev(self, branch)
201 return _prev(self, branch)
202
202
203 def get_file_mode(self, path):
203 def get_file_mode(self, path):
204 """
204 """
205 Returns stat mode of the file at the given ``path``.
205 Returns stat mode of the file at the given ``path``.
206 """
206 """
207 # ensure path is traversed
207 # ensure path is traversed
208 self._get_id_for_path(path)
208 self._get_id_for_path(path)
209 return self._stat_modes[path]
209 return self._stat_modes[path]
210
210
211 def get_file_content(self, path):
211 def get_file_content(self, path):
212 """
212 """
213 Returns content of the file at given ``path``.
213 Returns content of the file at given ``path``.
214 """
214 """
215 id = self._get_id_for_path(path)
215 id = self._get_id_for_path(path)
216 blob = self.repository._repo[id]
216 blob = self.repository._repo[id]
217 return blob.as_pretty_string()
217 return blob.as_pretty_string()
218
218
219 def get_file_size(self, path):
219 def get_file_size(self, path):
220 """
220 """
221 Returns size of the file at given ``path``.
221 Returns size of the file at given ``path``.
222 """
222 """
223 id = self._get_id_for_path(path)
223 id = self._get_id_for_path(path)
224 blob = self.repository._repo[id]
224 blob = self.repository._repo[id]
225 return blob.raw_length()
225 return blob.raw_length()
226
226
227 def get_file_changeset(self, path):
227 def get_file_changeset(self, path):
228 """
228 """
229 Returns last commit of the file at the given ``path``.
229 Returns last commit of the file at the given ``path``.
230 """
230 """
231 node = self.get_node(path)
231 node = self.get_node(path)
232 return node.history[0]
232 return node.history[0]
233
233
234 def get_file_history(self, path):
234 def get_file_history(self, path):
235 """
235 """
236 Returns history of file as reversed list of ``Changeset`` objects for
236 Returns history of file as reversed list of ``Changeset`` objects for
237 which file at given ``path`` has been modified.
237 which file at given ``path`` has been modified.
238
238
239 TODO: This function now uses os underlying 'git' and 'grep' commands
239 TODO: This function now uses os underlying 'git' and 'grep' commands
240 which is generally not good. Should be replaced with algorithm
240 which is generally not good. Should be replaced with algorithm
241 iterating commits.
241 iterating commits.
242 """
242 """
243 cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
243 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
244 self.id, path
244 self.id, path
245 )
245 )
246 so, se = self.repository.run_git_command(cmd)
246 so, se = self.repository.run_git_command(cmd)
247 ids = re.findall(r'\w{40}', so)
247 ids = re.findall(r'[0-9a-fA-F]{40}', so)
248 return [self.repository.get_changeset(id) for id in ids]
248 return [self.repository.get_changeset(id) for id in ids]
249
249
250 def get_file_annotate(self, path):
250 def get_file_annotate(self, path):
251 """
251 """
252 Returns a list of three element tuples with lineno,changeset and line
252 Returns a list of three element tuples with lineno,changeset and line
253
253
254 TODO: This function now uses os underlying 'git' command which is
254 TODO: This function now uses os underlying 'git' command which is
255 generally not good. Should be replaced with algorithm iterating
255 generally not good. Should be replaced with algorithm iterating
256 commits.
256 commits.
257 """
257 """
258 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
258 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
259 # -l ==> outputs long shas (and we need all 40 characters)
259 # -l ==> outputs long shas (and we need all 40 characters)
260 # --root ==> doesn't put '^' character for bounderies
260 # --root ==> doesn't put '^' character for bounderies
261 # -r sha ==> blames for the given revision
261 # -r sha ==> blames for the given revision
262 so, se = self.repository.run_git_command(cmd)
262 so, se = self.repository.run_git_command(cmd)
263 annotate = []
263 annotate = []
264 for i, blame_line in enumerate(so.split('\n')[:-1]):
264 for i, blame_line in enumerate(so.split('\n')[:-1]):
265 ln_no = i + 1
265 ln_no = i + 1
266 id, line = re.split(r' \(.+?\) ', blame_line, 1)
266 id, line = re.split(r' \(.+?\) ', blame_line, 1)
267 annotate.append((ln_no, self.repository.get_changeset(id), line))
267 annotate.append((ln_no, self.repository.get_changeset(id), line))
268 return annotate
268 return annotate
269
269
270 def fill_archive(self, stream=None, kind='tgz', prefix=None,
270 def fill_archive(self, stream=None, kind='tgz', prefix=None,
271 subrepos=False):
271 subrepos=False):
272 """
272 """
273 Fills up given stream.
273 Fills up given stream.
274
274
275 :param stream: file like object.
275 :param stream: file like object.
276 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
276 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
277 Default: ``tgz``.
277 Default: ``tgz``.
278 :param prefix: name of root directory in archive.
278 :param prefix: name of root directory in archive.
279 Default is repository name and changeset's raw_id joined with dash
279 Default is repository name and changeset's raw_id joined with dash
280 (``repo-tip.<KIND>``).
280 (``repo-tip.<KIND>``).
281 :param subrepos: include subrepos in this archive.
281 :param subrepos: include subrepos in this archive.
282
282
283 :raise ImproperArchiveTypeError: If given kind is wrong.
283 :raise ImproperArchiveTypeError: If given kind is wrong.
284 :raise VcsError: If given stream is None
284 :raise VcsError: If given stream is None
285
285
286 """
286 """
287 allowed_kinds = settings.ARCHIVE_SPECS.keys()
287 allowed_kinds = settings.ARCHIVE_SPECS.keys()
288 if kind not in allowed_kinds:
288 if kind not in allowed_kinds:
289 raise ImproperArchiveTypeError('Archive kind not supported use one'
289 raise ImproperArchiveTypeError('Archive kind not supported use one'
290 'of %s', allowed_kinds)
290 'of %s', allowed_kinds)
291
291
292 if prefix is None:
292 if prefix is None:
293 prefix = '%s-%s' % (self.repository.name, self.short_id)
293 prefix = '%s-%s' % (self.repository.name, self.short_id)
294 elif prefix.startswith('/'):
294 elif prefix.startswith('/'):
295 raise VCSError("Prefix cannot start with leading slash")
295 raise VCSError("Prefix cannot start with leading slash")
296 elif prefix.strip() == '':
296 elif prefix.strip() == '':
297 raise VCSError("Prefix cannot be empty")
297 raise VCSError("Prefix cannot be empty")
298
298
299 if kind == 'zip':
299 if kind == 'zip':
300 frmt = 'zip'
300 frmt = 'zip'
301 else:
301 else:
302 frmt = 'tar'
302 frmt = 'tar'
303 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
303 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
304 self.raw_id)
304 self.raw_id)
305 if kind == 'tgz':
305 if kind == 'tgz':
306 cmd += ' | gzip -9'
306 cmd += ' | gzip -9'
307 elif kind == 'tbz2':
307 elif kind == 'tbz2':
308 cmd += ' | bzip2 -9'
308 cmd += ' | bzip2 -9'
309
309
310 if stream is None:
310 if stream is None:
311 raise VCSError('You need to pass in a valid stream for filling'
311 raise VCSError('You need to pass in a valid stream for filling'
312 ' with archival data')
312 ' with archival data')
313 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
313 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
314 cwd=self.repository.path)
314 cwd=self.repository.path)
315
315
316 buffer_size = 1024 * 8
316 buffer_size = 1024 * 8
317 chunk = popen.stdout.read(buffer_size)
317 chunk = popen.stdout.read(buffer_size)
318 while chunk:
318 while chunk:
319 stream.write(chunk)
319 stream.write(chunk)
320 chunk = popen.stdout.read(buffer_size)
320 chunk = popen.stdout.read(buffer_size)
321 # Make sure all descriptors would be read
321 # Make sure all descriptors would be read
322 popen.communicate()
322 popen.communicate()
323
323
324 def get_nodes(self, path):
324 def get_nodes(self, path):
325 if self._get_kind(path) != NodeKind.DIR:
325 if self._get_kind(path) != NodeKind.DIR:
326 raise ChangesetError("Directory does not exist for revision %r at "
326 raise ChangesetError("Directory does not exist for revision %r at "
327 " %r" % (self.revision, path))
327 " %r" % (self.revision, path))
328 path = self._fix_path(path)
328 path = self._fix_path(path)
329 id = self._get_id_for_path(path)
329 id = self._get_id_for_path(path)
330 tree = self.repository._repo[id]
330 tree = self.repository._repo[id]
331 dirnodes = []
331 dirnodes = []
332 filenodes = []
332 filenodes = []
333 als = self.repository.alias
333 als = self.repository.alias
334 for name, stat, id in tree.iteritems():
334 for name, stat, id in tree.iteritems():
335 if objects.S_ISGITLINK(stat):
335 if objects.S_ISGITLINK(stat):
336 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
336 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
337 alias=als))
337 alias=als))
338 continue
338 continue
339
339
340 obj = self.repository._repo.get_object(id)
340 obj = self.repository._repo.get_object(id)
341 if path != '':
341 if path != '':
342 obj_path = '/'.join((path, name))
342 obj_path = '/'.join((path, name))
343 else:
343 else:
344 obj_path = name
344 obj_path = name
345 if obj_path not in self._stat_modes:
345 if obj_path not in self._stat_modes:
346 self._stat_modes[obj_path] = stat
346 self._stat_modes[obj_path] = stat
347 if isinstance(obj, objects.Tree):
347 if isinstance(obj, objects.Tree):
348 dirnodes.append(DirNode(obj_path, changeset=self))
348 dirnodes.append(DirNode(obj_path, changeset=self))
349 elif isinstance(obj, objects.Blob):
349 elif isinstance(obj, objects.Blob):
350 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
350 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
351 else:
351 else:
352 raise ChangesetError("Requested object should be Tree "
352 raise ChangesetError("Requested object should be Tree "
353 "or Blob, is %r" % type(obj))
353 "or Blob, is %r" % type(obj))
354 nodes = dirnodes + filenodes
354 nodes = dirnodes + filenodes
355 for node in nodes:
355 for node in nodes:
356 if not node.path in self.nodes:
356 if not node.path in self.nodes:
357 self.nodes[node.path] = node
357 self.nodes[node.path] = node
358 nodes.sort()
358 nodes.sort()
359 return nodes
359 return nodes
360
360
361 def get_node(self, path):
361 def get_node(self, path):
362 if isinstance(path, unicode):
362 if isinstance(path, unicode):
363 path = path.encode('utf-8')
363 path = path.encode('utf-8')
364 path = self._fix_path(path)
364 path = self._fix_path(path)
365 if not path in self.nodes:
365 if not path in self.nodes:
366 try:
366 try:
367 id_ = self._get_id_for_path(path)
367 id_ = self._get_id_for_path(path)
368 except ChangesetError:
368 except ChangesetError:
369 raise NodeDoesNotExistError("Cannot find one of parents' "
369 raise NodeDoesNotExistError("Cannot find one of parents' "
370 "directories for a given path: %s" % path)
370 "directories for a given path: %s" % path)
371
371
372 als = self.repository.alias
372 als = self.repository.alias
373 _GL = lambda m: m and objects.S_ISGITLINK(m)
373 _GL = lambda m: m and objects.S_ISGITLINK(m)
374 if _GL(self._stat_modes.get(path)):
374 if _GL(self._stat_modes.get(path)):
375 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
375 node = SubModuleNode(path, url=None, changeset=id_, alias=als)
376 else:
376 else:
377 obj = self.repository._repo.get_object(id_)
377 obj = self.repository._repo.get_object(id_)
378
378
379 if isinstance(obj, objects.Tree):
379 if isinstance(obj, objects.Tree):
380 if path == '':
380 if path == '':
381 node = RootNode(changeset=self)
381 node = RootNode(changeset=self)
382 else:
382 else:
383 node = DirNode(path, changeset=self)
383 node = DirNode(path, changeset=self)
384 node._tree = obj
384 node._tree = obj
385 elif isinstance(obj, objects.Blob):
385 elif isinstance(obj, objects.Blob):
386 node = FileNode(path, changeset=self)
386 node = FileNode(path, changeset=self)
387 node._blob = obj
387 node._blob = obj
388 else:
388 else:
389 raise NodeDoesNotExistError("There is no file nor directory "
389 raise NodeDoesNotExistError("There is no file nor directory "
390 "at the given path %r at revision %r"
390 "at the given path %r at revision %r"
391 % (path, self.short_id))
391 % (path, self.short_id))
392 # cache node
392 # cache node
393 self.nodes[path] = node
393 self.nodes[path] = node
394 return self.nodes[path]
394 return self.nodes[path]
395
395
396 @LazyProperty
396 @LazyProperty
397 def affected_files(self):
397 def affected_files(self):
398 """
398 """
399 Get's a fast accessible file changes for given changeset
399 Get's a fast accessible file changes for given changeset
400 """
400 """
401
401
402 return self.added + self.changed
402 return self.added + self.changed
403
403
404 @LazyProperty
404 @LazyProperty
405 def _diff_name_status(self):
405 def _diff_name_status(self):
406 output = []
406 output = []
407 for parent in self.parents:
407 for parent in self.parents:
408 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
408 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
409 so, se = self.repository.run_git_command(cmd)
409 so, se = self.repository.run_git_command(cmd)
410 output.append(so.strip())
410 output.append(so.strip())
411 return '\n'.join(output)
411 return '\n'.join(output)
412
412
413 def _get_paths_for_status(self, status):
413 def _get_paths_for_status(self, status):
414 """
414 """
415 Returns sorted list of paths for given ``status``.
415 Returns sorted list of paths for given ``status``.
416
416
417 :param status: one of: *added*, *modified* or *deleted*
417 :param status: one of: *added*, *modified* or *deleted*
418 """
418 """
419 paths = set()
419 paths = set()
420 char = status[0].upper()
420 char = status[0].upper()
421 for line in self._diff_name_status.splitlines():
421 for line in self._diff_name_status.splitlines():
422 if not line:
422 if not line:
423 continue
423 continue
424
424
425 if line.startswith(char):
425 if line.startswith(char):
426 splitted = line.split(char, 1)
426 splitted = line.split(char, 1)
427 if not len(splitted) == 2:
427 if not len(splitted) == 2:
428 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
428 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
429 "particularly that line: %s" % (self._diff_name_status,
429 "particularly that line: %s" % (self._diff_name_status,
430 line))
430 line))
431 _path = splitted[1].strip()
431 _path = splitted[1].strip()
432 paths.add(_path)
432 paths.add(_path)
433 return sorted(paths)
433 return sorted(paths)
434
434
435 @LazyProperty
435 @LazyProperty
436 def added(self):
436 def added(self):
437 """
437 """
438 Returns list of added ``FileNode`` objects.
438 Returns list of added ``FileNode`` objects.
439 """
439 """
440 if not self.parents:
440 if not self.parents:
441 return list(self._get_file_nodes())
441 return list(self._get_file_nodes())
442 return [self.get_node(path) for path in self._get_paths_for_status('added')]
442 return [self.get_node(path) for path in self._get_paths_for_status('added')]
443
443
444 @LazyProperty
444 @LazyProperty
445 def changed(self):
445 def changed(self):
446 """
446 """
447 Returns list of modified ``FileNode`` objects.
447 Returns list of modified ``FileNode`` objects.
448 """
448 """
449 if not self.parents:
449 if not self.parents:
450 return []
450 return []
451 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
451 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
452
452
453 @LazyProperty
453 @LazyProperty
454 def removed(self):
454 def removed(self):
455 """
455 """
456 Returns list of removed ``FileNode`` objects.
456 Returns list of removed ``FileNode`` objects.
457 """
457 """
458 if not self.parents:
458 if not self.parents:
459 return []
459 return []
460 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
460 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,355 +1,357 b''
1 import os
1 import os
2 import posixpath
2 import posixpath
3
3
4 from rhodecode.lib.vcs.backends.base import BaseChangeset
4 from rhodecode.lib.vcs.backends.base import BaseChangeset
5 from rhodecode.lib.vcs.conf import settings
5 from rhodecode.lib.vcs.conf import settings
6 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
6 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
7 ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
7 ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
8 from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
8 from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
9 ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
9 ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
10 RemovedFileNodesGenerator, RootNode, SubModuleNode
10 RemovedFileNodesGenerator, RootNode, SubModuleNode
11
11
12 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
12 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
13 from rhodecode.lib.vcs.utils.lazy import LazyProperty
13 from rhodecode.lib.vcs.utils.lazy import LazyProperty
14 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
14 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
15
15
16 from ...utils.hgcompat import archival, hex
16 from ...utils.hgcompat import archival, hex
17
17
18
18
19 class MercurialChangeset(BaseChangeset):
19 class MercurialChangeset(BaseChangeset):
20 """
20 """
21 Represents state of the repository at the single revision.
21 Represents state of the repository at the single revision.
22 """
22 """
23
23
24 def __init__(self, repository, revision):
24 def __init__(self, repository, revision):
25 self.repository = repository
25 self.repository = repository
26 self.raw_id = revision
26 self.raw_id = revision
27 self._ctx = repository._repo[revision]
27 self._ctx = repository._repo[revision]
28 self.revision = self._ctx._rev
28 self.revision = self._ctx._rev
29 self.nodes = {}
29 self.nodes = {}
30
30
31 @LazyProperty
31 @LazyProperty
32 def tags(self):
32 def tags(self):
33 return map(safe_unicode, self._ctx.tags())
33 return map(safe_unicode, self._ctx.tags())
34
34
35 @LazyProperty
35 @LazyProperty
36 def branch(self):
36 def branch(self):
37 return safe_unicode(self._ctx.branch())
37 return safe_unicode(self._ctx.branch())
38
38
39 @LazyProperty
39 @LazyProperty
40 def bookmarks(self):
41 return map(safe_unicode, self._ctx.bookmarks())
42
43 @LazyProperty
40 def message(self):
44 def message(self):
41 return safe_unicode(self._ctx.description())
45 return safe_unicode(self._ctx.description())
42
46
43 @LazyProperty
47 @LazyProperty
44 def author(self):
48 def author(self):
45 return safe_unicode(self._ctx.user())
49 return safe_unicode(self._ctx.user())
46
50
47 @LazyProperty
51 @LazyProperty
48 def date(self):
52 def date(self):
49 return date_fromtimestamp(*self._ctx.date())
53 return date_fromtimestamp(*self._ctx.date())
50
54
51 @LazyProperty
55 @LazyProperty
52 def status(self):
56 def status(self):
53 """
57 """
54 Returns modified, added, removed, deleted files for current changeset
58 Returns modified, added, removed, deleted files for current changeset
55 """
59 """
56 return self.repository._repo.status(self._ctx.p1().node(),
60 return self.repository._repo.status(self._ctx.p1().node(),
57 self._ctx.node())
61 self._ctx.node())
58
62
59 @LazyProperty
63 @LazyProperty
60 def _file_paths(self):
64 def _file_paths(self):
61 return list(self._ctx)
65 return list(self._ctx)
62
66
63 @LazyProperty
67 @LazyProperty
64 def _dir_paths(self):
68 def _dir_paths(self):
65 p = list(set(get_dirs_for_path(*self._file_paths)))
69 p = list(set(get_dirs_for_path(*self._file_paths)))
66 p.insert(0, '')
70 p.insert(0, '')
67 return p
71 return p
68
72
69 @LazyProperty
73 @LazyProperty
70 def _paths(self):
74 def _paths(self):
71 return self._dir_paths + self._file_paths
75 return self._dir_paths + self._file_paths
72
76
73 @LazyProperty
77 @LazyProperty
74 def id(self):
78 def id(self):
75 if self.last:
79 if self.last:
76 return u'tip'
80 return u'tip'
77 return self.short_id
81 return self.short_id
78
82
79 @LazyProperty
83 @LazyProperty
80 def short_id(self):
84 def short_id(self):
81 return self.raw_id[:12]
85 return self.raw_id[:12]
82
86
83 @LazyProperty
87 @LazyProperty
84 def parents(self):
88 def parents(self):
85 """
89 """
86 Returns list of parents changesets.
90 Returns list of parents changesets.
87 """
91 """
88 return [self.repository.get_changeset(parent.rev())
92 return [self.repository.get_changeset(parent.rev())
89 for parent in self._ctx.parents() if parent.rev() >= 0]
93 for parent in self._ctx.parents() if parent.rev() >= 0]
90
94
91 def next(self, branch=None):
95 def next(self, branch=None):
92
96
93 if branch and self.branch != branch:
97 if branch and self.branch != branch:
94 raise VCSError('Branch option used on changeset not belonging '
98 raise VCSError('Branch option used on changeset not belonging '
95 'to that branch')
99 'to that branch')
96
100
97 def _next(changeset, branch):
101 def _next(changeset, branch):
98 try:
102 try:
99 next_ = changeset.revision + 1
103 next_ = changeset.revision + 1
100 next_rev = changeset.repository.revisions[next_]
104 next_rev = changeset.repository.revisions[next_]
101 except IndexError:
105 except IndexError:
102 raise ChangesetDoesNotExistError
106 raise ChangesetDoesNotExistError
103 cs = changeset.repository.get_changeset(next_rev)
107 cs = changeset.repository.get_changeset(next_rev)
104
108
105 if branch and branch != cs.branch:
109 if branch and branch != cs.branch:
106 return _next(cs, branch)
110 return _next(cs, branch)
107
111
108 return cs
112 return cs
109
113
110 return _next(self, branch)
114 return _next(self, branch)
111
115
112 def prev(self, branch=None):
116 def prev(self, branch=None):
113 if branch and self.branch != branch:
117 if branch and self.branch != branch:
114 raise VCSError('Branch option used on changeset not belonging '
118 raise VCSError('Branch option used on changeset not belonging '
115 'to that branch')
119 'to that branch')
116
120
117 def _prev(changeset, branch):
121 def _prev(changeset, branch):
118 try:
122 try:
119 prev_ = changeset.revision - 1
123 prev_ = changeset.revision - 1
120 if prev_ < 0:
124 if prev_ < 0:
121 raise IndexError
125 raise IndexError
122 prev_rev = changeset.repository.revisions[prev_]
126 prev_rev = changeset.repository.revisions[prev_]
123 except IndexError:
127 except IndexError:
124 raise ChangesetDoesNotExistError
128 raise ChangesetDoesNotExistError
125
129
126 cs = changeset.repository.get_changeset(prev_rev)
130 cs = changeset.repository.get_changeset(prev_rev)
127
131
128 if branch and branch != cs.branch:
132 if branch and branch != cs.branch:
129 return _prev(cs, branch)
133 return _prev(cs, branch)
130
134
131 return cs
135 return cs
132
136
133 return _prev(self, branch)
137 return _prev(self, branch)
134
138
135 def _fix_path(self, path):
139 def _fix_path(self, path):
136 """
140 """
137 Paths are stored without trailing slash so we need to get rid off it if
141 Paths are stored without trailing slash so we need to get rid off it if
138 needed. Also mercurial keeps filenodes as str so we need to decode
142 needed. Also mercurial keeps filenodes as str so we need to decode
139 from unicode to str
143 from unicode to str
140 """
144 """
141 if path.endswith('/'):
145 if path.endswith('/'):
142 path = path.rstrip('/')
146 path = path.rstrip('/')
143
147
144 return safe_str(path)
148 return safe_str(path)
145
149
146 def _get_kind(self, path):
150 def _get_kind(self, path):
147 path = self._fix_path(path)
151 path = self._fix_path(path)
148 if path in self._file_paths:
152 if path in self._file_paths:
149 return NodeKind.FILE
153 return NodeKind.FILE
150 elif path in self._dir_paths:
154 elif path in self._dir_paths:
151 return NodeKind.DIR
155 return NodeKind.DIR
152 else:
156 else:
153 raise ChangesetError("Node does not exist at the given path %r"
157 raise ChangesetError("Node does not exist at the given path %r"
154 % (path))
158 % (path))
155
159
156 def _get_filectx(self, path):
160 def _get_filectx(self, path):
157 path = self._fix_path(path)
161 path = self._fix_path(path)
158 if self._get_kind(path) != NodeKind.FILE:
162 if self._get_kind(path) != NodeKind.FILE:
159 raise ChangesetError("File does not exist for revision %r at "
163 raise ChangesetError("File does not exist for revision %r at "
160 " %r" % (self.revision, path))
164 " %r" % (self.revision, path))
161 return self._ctx.filectx(path)
165 return self._ctx.filectx(path)
162
166
163 def _extract_submodules(self):
167 def _extract_submodules(self):
164 """
168 """
165 returns a dictionary with submodule information from substate file
169 returns a dictionary with submodule information from substate file
166 of hg repository
170 of hg repository
167 """
171 """
168 return self._ctx.substate
172 return self._ctx.substate
169
173
170 def get_file_mode(self, path):
174 def get_file_mode(self, path):
171 """
175 """
172 Returns stat mode of the file at the given ``path``.
176 Returns stat mode of the file at the given ``path``.
173 """
177 """
174 fctx = self._get_filectx(path)
178 fctx = self._get_filectx(path)
175 if 'x' in fctx.flags():
179 if 'x' in fctx.flags():
176 return 0100755
180 return 0100755
177 else:
181 else:
178 return 0100644
182 return 0100644
179
183
180 def get_file_content(self, path):
184 def get_file_content(self, path):
181 """
185 """
182 Returns content of the file at given ``path``.
186 Returns content of the file at given ``path``.
183 """
187 """
184 fctx = self._get_filectx(path)
188 fctx = self._get_filectx(path)
185 return fctx.data()
189 return fctx.data()
186
190
187 def get_file_size(self, path):
191 def get_file_size(self, path):
188 """
192 """
189 Returns size of the file at given ``path``.
193 Returns size of the file at given ``path``.
190 """
194 """
191 fctx = self._get_filectx(path)
195 fctx = self._get_filectx(path)
192 return fctx.size()
196 return fctx.size()
193
197
194 def get_file_changeset(self, path):
198 def get_file_changeset(self, path):
195 """
199 """
196 Returns last commit of the file at the given ``path``.
200 Returns last commit of the file at the given ``path``.
197 """
201 """
198 node = self.get_node(path)
202 node = self.get_node(path)
199 return node.history[0]
203 return node.history[0]
200
204
201 def get_file_history(self, path):
205 def get_file_history(self, path):
202 """
206 """
203 Returns history of file as reversed list of ``Changeset`` objects for
207 Returns history of file as reversed list of ``Changeset`` objects for
204 which file at given ``path`` has been modified.
208 which file at given ``path`` has been modified.
205 """
209 """
206 fctx = self._get_filectx(path)
210 fctx = self._get_filectx(path)
207 nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
211 nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
208 changesets = [self.repository.get_changeset(hex(node))
212 changesets = [self.repository.get_changeset(hex(node))
209 for node in reversed(nodes)]
213 for node in reversed(nodes)]
210 return changesets
214 return changesets
211
215
212 def get_file_annotate(self, path):
216 def get_file_annotate(self, path):
213 """
217 """
214 Returns a list of three element tuples with lineno,changeset and line
218 Returns a list of three element tuples with lineno,changeset and line
215 """
219 """
216 fctx = self._get_filectx(path)
220 fctx = self._get_filectx(path)
217 annotate = []
221 annotate = []
218 for i, annotate_data in enumerate(fctx.annotate()):
222 for i, annotate_data in enumerate(fctx.annotate()):
219 ln_no = i + 1
223 ln_no = i + 1
220 annotate.append((ln_no, self.repository\
224 annotate.append((ln_no, self.repository\
221 .get_changeset(hex(annotate_data[0].node())),
225 .get_changeset(hex(annotate_data[0].node())),
222 annotate_data[1],))
226 annotate_data[1],))
223
227
224 return annotate
228 return annotate
225
229
226 def fill_archive(self, stream=None, kind='tgz', prefix=None,
230 def fill_archive(self, stream=None, kind='tgz', prefix=None,
227 subrepos=False):
231 subrepos=False):
228 """
232 """
229 Fills up given stream.
233 Fills up given stream.
230
234
231 :param stream: file like object.
235 :param stream: file like object.
232 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
236 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
233 Default: ``tgz``.
237 Default: ``tgz``.
234 :param prefix: name of root directory in archive.
238 :param prefix: name of root directory in archive.
235 Default is repository name and changeset's raw_id joined with dash
239 Default is repository name and changeset's raw_id joined with dash
236 (``repo-tip.<KIND>``).
240 (``repo-tip.<KIND>``).
237 :param subrepos: include subrepos in this archive.
241 :param subrepos: include subrepos in this archive.
238
242
239 :raise ImproperArchiveTypeError: If given kind is wrong.
243 :raise ImproperArchiveTypeError: If given kind is wrong.
240 :raise VcsError: If given stream is None
244 :raise VcsError: If given stream is None
241 """
245 """
242
246
243 allowed_kinds = settings.ARCHIVE_SPECS.keys()
247 allowed_kinds = settings.ARCHIVE_SPECS.keys()
244 if kind not in allowed_kinds:
248 if kind not in allowed_kinds:
245 raise ImproperArchiveTypeError('Archive kind not supported use one'
249 raise ImproperArchiveTypeError('Archive kind not supported use one'
246 'of %s', allowed_kinds)
250 'of %s', allowed_kinds)
247
251
248 if stream is None:
252 if stream is None:
249 raise VCSError('You need to pass in a valid stream for filling'
253 raise VCSError('You need to pass in a valid stream for filling'
250 ' with archival data')
254 ' with archival data')
251
255
252 if prefix is None:
256 if prefix is None:
253 prefix = '%s-%s' % (self.repository.name, self.short_id)
257 prefix = '%s-%s' % (self.repository.name, self.short_id)
254 elif prefix.startswith('/'):
258 elif prefix.startswith('/'):
255 raise VCSError("Prefix cannot start with leading slash")
259 raise VCSError("Prefix cannot start with leading slash")
256 elif prefix.strip() == '':
260 elif prefix.strip() == '':
257 raise VCSError("Prefix cannot be empty")
261 raise VCSError("Prefix cannot be empty")
258
262
259 archival.archive(self.repository._repo, stream, self.raw_id,
263 archival.archive(self.repository._repo, stream, self.raw_id,
260 kind, prefix=prefix, subrepos=subrepos)
264 kind, prefix=prefix, subrepos=subrepos)
261
265
262 #stream.close()
263
264 if stream.closed and hasattr(stream, 'name'):
266 if stream.closed and hasattr(stream, 'name'):
265 stream = open(stream.name, 'rb')
267 stream = open(stream.name, 'rb')
266 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
268 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
267 stream = open(stream.name, 'rb')
269 stream = open(stream.name, 'rb')
268 else:
270 else:
269 stream.seek(0)
271 stream.seek(0)
270
272
271 def get_nodes(self, path):
273 def get_nodes(self, path):
272 """
274 """
273 Returns combined ``DirNode`` and ``FileNode`` objects list representing
275 Returns combined ``DirNode`` and ``FileNode`` objects list representing
274 state of changeset at the given ``path``. If node at the given ``path``
276 state of changeset at the given ``path``. If node at the given ``path``
275 is not instance of ``DirNode``, ChangesetError would be raised.
277 is not instance of ``DirNode``, ChangesetError would be raised.
276 """
278 """
277
279
278 if self._get_kind(path) != NodeKind.DIR:
280 if self._get_kind(path) != NodeKind.DIR:
279 raise ChangesetError("Directory does not exist for revision %r at "
281 raise ChangesetError("Directory does not exist for revision %r at "
280 " %r" % (self.revision, path))
282 " %r" % (self.revision, path))
281 path = self._fix_path(path)
283 path = self._fix_path(path)
282
284
283 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
285 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
284 if os.path.dirname(f) == path]
286 if os.path.dirname(f) == path]
285 dirs = path == '' and '' or [d for d in self._dir_paths
287 dirs = path == '' and '' or [d for d in self._dir_paths
286 if d and posixpath.dirname(d) == path]
288 if d and posixpath.dirname(d) == path]
287 dirnodes = [DirNode(d, changeset=self) for d in dirs
289 dirnodes = [DirNode(d, changeset=self) for d in dirs
288 if os.path.dirname(d) == path]
290 if os.path.dirname(d) == path]
289
291
290 als = self.repository.alias
292 als = self.repository.alias
291 for k, vals in self._extract_submodules().iteritems():
293 for k, vals in self._extract_submodules().iteritems():
292 #vals = url,rev,type
294 #vals = url,rev,type
293 loc = vals[0]
295 loc = vals[0]
294 cs = vals[1]
296 cs = vals[1]
295 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
297 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
296 alias=als))
298 alias=als))
297 nodes = dirnodes + filenodes
299 nodes = dirnodes + filenodes
298 # cache nodes
300 # cache nodes
299 for node in nodes:
301 for node in nodes:
300 self.nodes[node.path] = node
302 self.nodes[node.path] = node
301 nodes.sort()
303 nodes.sort()
302
304
303 return nodes
305 return nodes
304
306
305 def get_node(self, path):
307 def get_node(self, path):
306 """
308 """
307 Returns ``Node`` object from the given ``path``. If there is no node at
309 Returns ``Node`` object from the given ``path``. If there is no node at
308 the given ``path``, ``ChangesetError`` would be raised.
310 the given ``path``, ``ChangesetError`` would be raised.
309 """
311 """
310
312
311 path = self._fix_path(path)
313 path = self._fix_path(path)
312
314
313 if not path in self.nodes:
315 if not path in self.nodes:
314 if path in self._file_paths:
316 if path in self._file_paths:
315 node = FileNode(path, changeset=self)
317 node = FileNode(path, changeset=self)
316 elif path in self._dir_paths or path in self._dir_paths:
318 elif path in self._dir_paths or path in self._dir_paths:
317 if path == '':
319 if path == '':
318 node = RootNode(changeset=self)
320 node = RootNode(changeset=self)
319 else:
321 else:
320 node = DirNode(path, changeset=self)
322 node = DirNode(path, changeset=self)
321 else:
323 else:
322 raise NodeDoesNotExistError("There is no file nor directory "
324 raise NodeDoesNotExistError("There is no file nor directory "
323 "at the given path: %r at revision %r"
325 "at the given path: %r at revision %r"
324 % (path, self.short_id))
326 % (path, self.short_id))
325 # cache node
327 # cache node
326 self.nodes[path] = node
328 self.nodes[path] = node
327 return self.nodes[path]
329 return self.nodes[path]
328
330
329 @LazyProperty
331 @LazyProperty
330 def affected_files(self):
332 def affected_files(self):
331 """
333 """
332 Get's a fast accessible file changes for given changeset
334 Get's a fast accessible file changes for given changeset
333 """
335 """
334 return self._ctx.files()
336 return self._ctx.files()
335
337
336 @property
338 @property
337 def added(self):
339 def added(self):
338 """
340 """
339 Returns list of added ``FileNode`` objects.
341 Returns list of added ``FileNode`` objects.
340 """
342 """
341 return AddedFileNodesGenerator([n for n in self.status[1]], self)
343 return AddedFileNodesGenerator([n for n in self.status[1]], self)
342
344
343 @property
345 @property
344 def changed(self):
346 def changed(self):
345 """
347 """
346 Returns list of modified ``FileNode`` objects.
348 Returns list of modified ``FileNode`` objects.
347 """
349 """
348 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
350 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
349
351
350 @property
352 @property
351 def removed(self):
353 def removed(self):
352 """
354 """
353 Returns list of removed ``FileNode`` objects.
355 Returns list of removed ``FileNode`` objects.
354 """
356 """
355 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
357 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -1,139 +1,138 b''
1 """
1 """
2 This module provides some useful tools for ``vcs`` like annotate/diff html
2 This module provides some useful tools for ``vcs`` like annotate/diff html
3 output. It also includes some internal helpers.
3 output. It also includes some internal helpers.
4 """
4 """
5 import sys
5 import sys
6 import time
6 import time
7 import datetime
7 import datetime
8
8
9
9
10 def makedate():
10 def makedate():
11 lt = time.localtime()
11 lt = time.localtime()
12 if lt[8] == 1 and time.daylight:
12 if lt[8] == 1 and time.daylight:
13 tz = time.altzone
13 tz = time.altzone
14 else:
14 else:
15 tz = time.timezone
15 tz = time.timezone
16 return time.mktime(lt), tz
16 return time.mktime(lt), tz
17
17
18
18
19 def date_fromtimestamp(unixts, tzoffset=0):
19 def date_fromtimestamp(unixts, tzoffset=0):
20 """
20 """
21 Makes a local datetime object out of unix timestamp
21 Makes a local datetime object out of unix timestamp
22
22
23 :param unixts:
23 :param unixts:
24 :param tzoffset:
24 :param tzoffset:
25 """
25 """
26
26
27 return datetime.datetime.fromtimestamp(float(unixts))
27 return datetime.datetime.fromtimestamp(float(unixts))
28
28
29
29
30 def safe_unicode(str_, from_encoding=None):
30 def safe_unicode(str_, from_encoding=None):
31 """
31 """
32 safe unicode function. Does few trick to turn str_ into unicode
32 safe unicode function. Does few trick to turn str_ into unicode
33
33
34 In case of UnicodeDecode error we try to return it with encoding detected
34 In case of UnicodeDecode error we try to return it with encoding detected
35 by chardet library if it fails fallback to unicode with errors replaced
35 by chardet library if it fails fallback to unicode with errors replaced
36
36
37 :param str_: string to decode
37 :param str_: string to decode
38 :rtype: unicode
38 :rtype: unicode
39 :returns: unicode object
39 :returns: unicode object
40 """
40 """
41 if isinstance(str_, unicode):
41 if isinstance(str_, unicode):
42 return str_
42 return str_
43 if not from_encoding:
43 if not from_encoding:
44 import rhodecode
44 import rhodecode
45 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8')
45 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8')
46 from_encoding = DEFAULT_ENCODING
46 from_encoding = DEFAULT_ENCODING
47 try:
47 try:
48 return unicode(str_)
48 return unicode(str_)
49 except UnicodeDecodeError:
49 except UnicodeDecodeError:
50 pass
50 pass
51
51
52 try:
52 try:
53 return unicode(str_, from_encoding)
53 return unicode(str_, from_encoding)
54 except UnicodeDecodeError:
54 except UnicodeDecodeError:
55 pass
55 pass
56
56
57 try:
57 try:
58 import chardet
58 import chardet
59 encoding = chardet.detect(str_)['encoding']
59 encoding = chardet.detect(str_)['encoding']
60 if encoding is None:
60 if encoding is None:
61 raise Exception()
61 raise Exception()
62 return str_.decode(encoding)
62 return str_.decode(encoding)
63 except (ImportError, UnicodeDecodeError, Exception):
63 except (ImportError, UnicodeDecodeError, Exception):
64 return unicode(str_, from_encoding, 'replace')
64 return unicode(str_, from_encoding, 'replace')
65
65
66
66
67 def safe_str(unicode_, to_encoding=None):
67 def safe_str(unicode_, to_encoding=None):
68 """
68 """
69 safe str function. Does few trick to turn unicode_ into string
69 safe str function. Does few trick to turn unicode_ into string
70
70
71 In case of UnicodeEncodeError we try to return it with encoding detected
71 In case of UnicodeEncodeError we try to return it with encoding detected
72 by chardet library if it fails fallback to string with errors replaced
72 by chardet library if it fails fallback to string with errors replaced
73
73
74 :param unicode_: unicode to encode
74 :param unicode_: unicode to encode
75 :rtype: str
75 :rtype: str
76 :returns: str object
76 :returns: str object
77 """
77 """
78
78
79 if isinstance(unicode_, str):
79 if isinstance(unicode_, str):
80 return unicode_
80 return unicode_
81 if not to_encoding:
81 if not to_encoding:
82 import rhodecode
82 import rhodecode
83 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8')
83 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8')
84 to_encoding = DEFAULT_ENCODING
84 to_encoding = DEFAULT_ENCODING
85 try:
85 try:
86 return unicode_.encode(to_encoding)
86 return unicode_.encode(to_encoding)
87 except UnicodeEncodeError:
87 except UnicodeEncodeError:
88 pass
88 pass
89
89
90 try:
90 try:
91 import chardet
91 import chardet
92 encoding = chardet.detect(unicode_)['encoding']
92 encoding = chardet.detect(unicode_)['encoding']
93 print encoding
94 if encoding is None:
93 if encoding is None:
95 raise UnicodeEncodeError()
94 raise UnicodeEncodeError()
96
95
97 return unicode_.encode(encoding)
96 return unicode_.encode(encoding)
98 except (ImportError, UnicodeEncodeError):
97 except (ImportError, UnicodeEncodeError):
99 return unicode_.encode(to_encoding, 'replace')
98 return unicode_.encode(to_encoding, 'replace')
100
99
101 return safe_str
100 return safe_str
102
101
103
102
104 def author_email(author):
103 def author_email(author):
105 """
104 """
106 returns email address of given author.
105 returns email address of given author.
107 If any of <,> sign are found, it fallbacks to regex findall()
106 If any of <,> sign are found, it fallbacks to regex findall()
108 and returns first found result or empty string
107 and returns first found result or empty string
109
108
110 Regex taken from http://www.regular-expressions.info/email.html
109 Regex taken from http://www.regular-expressions.info/email.html
111 """
110 """
112 import re
111 import re
113 r = author.find('>')
112 r = author.find('>')
114 l = author.find('<')
113 l = author.find('<')
115
114
116 if l == -1 or r == -1:
115 if l == -1 or r == -1:
117 # fallback to regex match of email out of a string
116 # fallback to regex match of email out of a string
118 email_re = re.compile(r"""[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!"""
117 email_re = re.compile(r"""[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!"""
119 r"""#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z"""
118 r"""#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z"""
120 r"""0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]"""
119 r"""0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]"""
121 r"""*[a-z0-9])?""", re.IGNORECASE)
120 r"""*[a-z0-9])?""", re.IGNORECASE)
122 m = re.findall(email_re, author)
121 m = re.findall(email_re, author)
123 return m[0] if m else ''
122 return m[0] if m else ''
124
123
125 return author[l + 1:r].strip()
124 return author[l + 1:r].strip()
126
125
127
126
128 def author_name(author):
127 def author_name(author):
129 """
128 """
130 get name of author, or else username.
129 get name of author, or else username.
131 It'll try to find an email in the author string and just cut it off
130 It'll try to find an email in the author string and just cut it off
132 to get the username
131 to get the username
133 """
132 """
134
133
135 if not '@' in author:
134 if not '@' in author:
136 return author
135 return author
137 else:
136 else:
138 return author.replace(author_email(author), '').replace('<', '')\
137 return author.replace(author_email(author), '').replace('<', '')\
139 .replace('>', '').strip()
138 .replace('>', '').strip()
@@ -1,1356 +1,1357 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.db
3 rhodecode.model.db
4 ~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~
5
5
6 Database Models for RhodeCode
6 Database Models for RhodeCode
7
7
8 :created_on: Apr 08, 2010
8 :created_on: Apr 08, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import os
26 import os
27 import logging
27 import logging
28 import datetime
28 import datetime
29 import traceback
29 import traceback
30 from collections import defaultdict
30 from collections import defaultdict
31
31
32 from sqlalchemy import *
32 from sqlalchemy import *
33 from sqlalchemy.ext.hybrid import hybrid_property
33 from sqlalchemy.ext.hybrid import hybrid_property
34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 from beaker.cache import cache_region, region_invalidate
35 from beaker.cache import cache_region, region_invalidate
36
36
37 from pylons.i18n.translation import lazy_ugettext as _
37 from pylons.i18n.translation import lazy_ugettext as _
38
38
39 from rhodecode.lib.vcs import get_backend
39 from rhodecode.lib.vcs import get_backend
40 from rhodecode.lib.vcs.utils.helpers import get_scm
40 from rhodecode.lib.vcs.utils.helpers import get_scm
41 from rhodecode.lib.vcs.exceptions import VCSError
41 from rhodecode.lib.vcs.exceptions import VCSError
42 from rhodecode.lib.vcs.utils.lazy import LazyProperty
42 from rhodecode.lib.vcs.utils.lazy import LazyProperty
43
43
44 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
44 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
45 safe_unicode
45 safe_unicode
46 from rhodecode.lib.compat import json
46 from rhodecode.lib.compat import json
47 from rhodecode.lib.caching_query import FromCache
47 from rhodecode.lib.caching_query import FromCache
48
48
49 from rhodecode.model.meta import Base, Session
49 from rhodecode.model.meta import Base, Session
50 import hashlib
50 import hashlib
51
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55 #==============================================================================
55 #==============================================================================
56 # BASE CLASSES
56 # BASE CLASSES
57 #==============================================================================
57 #==============================================================================
58
58
59 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
59 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
60
60
61
61
62 class ModelSerializer(json.JSONEncoder):
62 class ModelSerializer(json.JSONEncoder):
63 """
63 """
64 Simple Serializer for JSON,
64 Simple Serializer for JSON,
65
65
66 usage::
66 usage::
67
67
68 to make object customized for serialization implement a __json__
68 to make object customized for serialization implement a __json__
69 method that will return a dict for serialization into json
69 method that will return a dict for serialization into json
70
70
71 example::
71 example::
72
72
73 class Task(object):
73 class Task(object):
74
74
75 def __init__(self, name, value):
75 def __init__(self, name, value):
76 self.name = name
76 self.name = name
77 self.value = value
77 self.value = value
78
78
79 def __json__(self):
79 def __json__(self):
80 return dict(name=self.name,
80 return dict(name=self.name,
81 value=self.value)
81 value=self.value)
82
82
83 """
83 """
84
84
85 def default(self, obj):
85 def default(self, obj):
86
86
87 if hasattr(obj, '__json__'):
87 if hasattr(obj, '__json__'):
88 return obj.__json__()
88 return obj.__json__()
89 else:
89 else:
90 return json.JSONEncoder.default(self, obj)
90 return json.JSONEncoder.default(self, obj)
91
91
92
92
93 class BaseModel(object):
93 class BaseModel(object):
94 """
94 """
95 Base Model for all classess
95 Base Model for all classess
96 """
96 """
97
97
98 @classmethod
98 @classmethod
99 def _get_keys(cls):
99 def _get_keys(cls):
100 """return column names for this model """
100 """return column names for this model """
101 return class_mapper(cls).c.keys()
101 return class_mapper(cls).c.keys()
102
102
103 def get_dict(self):
103 def get_dict(self):
104 """
104 """
105 return dict with keys and values corresponding
105 return dict with keys and values corresponding
106 to this model data """
106 to this model data """
107
107
108 d = {}
108 d = {}
109 for k in self._get_keys():
109 for k in self._get_keys():
110 d[k] = getattr(self, k)
110 d[k] = getattr(self, k)
111
111
112 # also use __json__() if present to get additional fields
112 # also use __json__() if present to get additional fields
113 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
113 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
114 d[k] = val
114 d[k] = val
115 return d
115 return d
116
116
117 def get_appstruct(self):
117 def get_appstruct(self):
118 """return list with keys and values tupples corresponding
118 """return list with keys and values tupples corresponding
119 to this model data """
119 to this model data """
120
120
121 l = []
121 l = []
122 for k in self._get_keys():
122 for k in self._get_keys():
123 l.append((k, getattr(self, k),))
123 l.append((k, getattr(self, k),))
124 return l
124 return l
125
125
126 def populate_obj(self, populate_dict):
126 def populate_obj(self, populate_dict):
127 """populate model with data from given populate_dict"""
127 """populate model with data from given populate_dict"""
128
128
129 for k in self._get_keys():
129 for k in self._get_keys():
130 if k in populate_dict:
130 if k in populate_dict:
131 setattr(self, k, populate_dict[k])
131 setattr(self, k, populate_dict[k])
132
132
133 @classmethod
133 @classmethod
134 def query(cls):
134 def query(cls):
135 return Session.query(cls)
135 return Session.query(cls)
136
136
137 @classmethod
137 @classmethod
138 def get(cls, id_):
138 def get(cls, id_):
139 if id_:
139 if id_:
140 return cls.query().get(id_)
140 return cls.query().get(id_)
141
141
142 @classmethod
142 @classmethod
143 def getAll(cls):
143 def getAll(cls):
144 return cls.query().all()
144 return cls.query().all()
145
145
146 @classmethod
146 @classmethod
147 def delete(cls, id_):
147 def delete(cls, id_):
148 obj = cls.query().get(id_)
148 obj = cls.query().get(id_)
149 Session.delete(obj)
149 Session.delete(obj)
150
150
151 def __repr__(self):
151 def __repr__(self):
152 if hasattr(self, '__unicode__'):
152 if hasattr(self, '__unicode__'):
153 # python repr needs to return str
153 # python repr needs to return str
154 return safe_str(self.__unicode__())
154 return safe_str(self.__unicode__())
155 return '<DB:%s>' % (self.__class__.__name__)
155 return '<DB:%s>' % (self.__class__.__name__)
156
156
157 class RhodeCodeSetting(Base, BaseModel):
157 class RhodeCodeSetting(Base, BaseModel):
158 __tablename__ = 'rhodecode_settings'
158 __tablename__ = 'rhodecode_settings'
159 __table_args__ = (
159 __table_args__ = (
160 UniqueConstraint('app_settings_name'),
160 UniqueConstraint('app_settings_name'),
161 {'extend_existing': True, 'mysql_engine': 'InnoDB',
161 {'extend_existing': True, 'mysql_engine': 'InnoDB',
162 'mysql_charset': 'utf8'}
162 'mysql_charset': 'utf8'}
163 )
163 )
164 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
164 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
165 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
165 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
166 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
166 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
167
167
168 def __init__(self, k='', v=''):
168 def __init__(self, k='', v=''):
169 self.app_settings_name = k
169 self.app_settings_name = k
170 self.app_settings_value = v
170 self.app_settings_value = v
171
171
172 @validates('_app_settings_value')
172 @validates('_app_settings_value')
173 def validate_settings_value(self, key, val):
173 def validate_settings_value(self, key, val):
174 assert type(val) == unicode
174 assert type(val) == unicode
175 return val
175 return val
176
176
177 @hybrid_property
177 @hybrid_property
178 def app_settings_value(self):
178 def app_settings_value(self):
179 v = self._app_settings_value
179 v = self._app_settings_value
180 if self.app_settings_name == 'ldap_active':
180 if self.app_settings_name == 'ldap_active':
181 v = str2bool(v)
181 v = str2bool(v)
182 return v
182 return v
183
183
184 @app_settings_value.setter
184 @app_settings_value.setter
185 def app_settings_value(self, val):
185 def app_settings_value(self, val):
186 """
186 """
187 Setter that will always make sure we use unicode in app_settings_value
187 Setter that will always make sure we use unicode in app_settings_value
188
188
189 :param val:
189 :param val:
190 """
190 """
191 self._app_settings_value = safe_unicode(val)
191 self._app_settings_value = safe_unicode(val)
192
192
193 def __unicode__(self):
193 def __unicode__(self):
194 return u"<%s('%s:%s')>" % (
194 return u"<%s('%s:%s')>" % (
195 self.__class__.__name__,
195 self.__class__.__name__,
196 self.app_settings_name, self.app_settings_value
196 self.app_settings_name, self.app_settings_value
197 )
197 )
198
198
199 @classmethod
199 @classmethod
200 def get_by_name(cls, ldap_key):
200 def get_by_name(cls, ldap_key):
201 return cls.query()\
201 return cls.query()\
202 .filter(cls.app_settings_name == ldap_key).scalar()
202 .filter(cls.app_settings_name == ldap_key).scalar()
203
203
204 @classmethod
204 @classmethod
205 def get_app_settings(cls, cache=False):
205 def get_app_settings(cls, cache=False):
206
206
207 ret = cls.query()
207 ret = cls.query()
208
208
209 if cache:
209 if cache:
210 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
210 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
211
211
212 if not ret:
212 if not ret:
213 raise Exception('Could not get application settings !')
213 raise Exception('Could not get application settings !')
214 settings = {}
214 settings = {}
215 for each in ret:
215 for each in ret:
216 settings['rhodecode_' + each.app_settings_name] = \
216 settings['rhodecode_' + each.app_settings_name] = \
217 each.app_settings_value
217 each.app_settings_value
218
218
219 return settings
219 return settings
220
220
221 @classmethod
221 @classmethod
222 def get_ldap_settings(cls, cache=False):
222 def get_ldap_settings(cls, cache=False):
223 ret = cls.query()\
223 ret = cls.query()\
224 .filter(cls.app_settings_name.startswith('ldap_')).all()
224 .filter(cls.app_settings_name.startswith('ldap_')).all()
225 fd = {}
225 fd = {}
226 for row in ret:
226 for row in ret:
227 fd.update({row.app_settings_name:row.app_settings_value})
227 fd.update({row.app_settings_name:row.app_settings_value})
228
228
229 return fd
229 return fd
230
230
231
231
232 class RhodeCodeUi(Base, BaseModel):
232 class RhodeCodeUi(Base, BaseModel):
233 __tablename__ = 'rhodecode_ui'
233 __tablename__ = 'rhodecode_ui'
234 __table_args__ = (
234 __table_args__ = (
235 UniqueConstraint('ui_key'),
235 UniqueConstraint('ui_key'),
236 {'extend_existing': True, 'mysql_engine': 'InnoDB',
236 {'extend_existing': True, 'mysql_engine': 'InnoDB',
237 'mysql_charset': 'utf8'}
237 'mysql_charset': 'utf8'}
238 )
238 )
239
239
240 HOOK_UPDATE = 'changegroup.update'
240 HOOK_UPDATE = 'changegroup.update'
241 HOOK_REPO_SIZE = 'changegroup.repo_size'
241 HOOK_REPO_SIZE = 'changegroup.repo_size'
242 HOOK_PUSH = 'pretxnchangegroup.push_logger'
242 HOOK_PUSH = 'pretxnchangegroup.push_logger'
243 HOOK_PULL = 'preoutgoing.pull_logger'
243 HOOK_PULL = 'preoutgoing.pull_logger'
244
244
245 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
245 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
246 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
246 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
247 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
247 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
248 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
248 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
249 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
249 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
250
250
251 @classmethod
251 @classmethod
252 def get_by_key(cls, key):
252 def get_by_key(cls, key):
253 return cls.query().filter(cls.ui_key == key)
253 return cls.query().filter(cls.ui_key == key)
254
254
255 @classmethod
255 @classmethod
256 def get_builtin_hooks(cls):
256 def get_builtin_hooks(cls):
257 q = cls.query()
257 q = cls.query()
258 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
258 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
259 cls.HOOK_REPO_SIZE,
259 cls.HOOK_REPO_SIZE,
260 cls.HOOK_PUSH, cls.HOOK_PULL]))
260 cls.HOOK_PUSH, cls.HOOK_PULL]))
261 return q.all()
261 return q.all()
262
262
263 @classmethod
263 @classmethod
264 def get_custom_hooks(cls):
264 def get_custom_hooks(cls):
265 q = cls.query()
265 q = cls.query()
266 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
266 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
267 cls.HOOK_REPO_SIZE,
267 cls.HOOK_REPO_SIZE,
268 cls.HOOK_PUSH, cls.HOOK_PULL]))
268 cls.HOOK_PUSH, cls.HOOK_PULL]))
269 q = q.filter(cls.ui_section == 'hooks')
269 q = q.filter(cls.ui_section == 'hooks')
270 return q.all()
270 return q.all()
271
271
272 @classmethod
272 @classmethod
273 def create_or_update_hook(cls, key, val):
273 def create_or_update_hook(cls, key, val):
274 new_ui = cls.get_by_key(key).scalar() or cls()
274 new_ui = cls.get_by_key(key).scalar() or cls()
275 new_ui.ui_section = 'hooks'
275 new_ui.ui_section = 'hooks'
276 new_ui.ui_active = True
276 new_ui.ui_active = True
277 new_ui.ui_key = key
277 new_ui.ui_key = key
278 new_ui.ui_value = val
278 new_ui.ui_value = val
279
279
280 Session.add(new_ui)
280 Session.add(new_ui)
281
281
282
282
283 class User(Base, BaseModel):
283 class User(Base, BaseModel):
284 __tablename__ = 'users'
284 __tablename__ = 'users'
285 __table_args__ = (
285 __table_args__ = (
286 UniqueConstraint('username'), UniqueConstraint('email'),
286 UniqueConstraint('username'), UniqueConstraint('email'),
287 {'extend_existing': True, 'mysql_engine': 'InnoDB',
287 {'extend_existing': True, 'mysql_engine': 'InnoDB',
288 'mysql_charset': 'utf8'}
288 'mysql_charset': 'utf8'}
289 )
289 )
290 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
290 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
291 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
291 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
292 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
292 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
293 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
293 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
294 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
294 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
295 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
295 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
296 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
296 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
297 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
297 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
298 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
298 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
299 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
299 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
300 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
300 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
301
301
302 user_log = relationship('UserLog', cascade='all')
302 user_log = relationship('UserLog', cascade='all')
303 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
303 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
304
304
305 repositories = relationship('Repository')
305 repositories = relationship('Repository')
306 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
306 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
307 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
307 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
308 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
308 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
309
309
310 group_member = relationship('UsersGroupMember', cascade='all')
310 group_member = relationship('UsersGroupMember', cascade='all')
311
311
312 notifications = relationship('UserNotification', cascade='all')
312 notifications = relationship('UserNotification', cascade='all')
313 # notifications assigned to this user
313 # notifications assigned to this user
314 user_created_notifications = relationship('Notification', cascade='all')
314 user_created_notifications = relationship('Notification', cascade='all')
315 # comments created by this user
315 # comments created by this user
316 user_comments = relationship('ChangesetComment', cascade='all')
316 user_comments = relationship('ChangesetComment', cascade='all')
317
317
318 @hybrid_property
318 @hybrid_property
319 def email(self):
319 def email(self):
320 return self._email
320 return self._email
321
321
322 @email.setter
322 @email.setter
323 def email(self, val):
323 def email(self, val):
324 self._email = val.lower() if val else None
324 self._email = val.lower() if val else None
325
325
326 @property
326 @property
327 def full_name(self):
327 def full_name(self):
328 return '%s %s' % (self.name, self.lastname)
328 return '%s %s' % (self.name, self.lastname)
329
329
330 @property
330 @property
331 def full_name_or_username(self):
331 def full_name_or_username(self):
332 return ('%s %s' % (self.name, self.lastname)
332 return ('%s %s' % (self.name, self.lastname)
333 if (self.name and self.lastname) else self.username)
333 if (self.name and self.lastname) else self.username)
334
334
335 @property
335 @property
336 def full_contact(self):
336 def full_contact(self):
337 return '%s %s <%s>' % (self.name, self.lastname, self.email)
337 return '%s %s <%s>' % (self.name, self.lastname, self.email)
338
338
339 @property
339 @property
340 def short_contact(self):
340 def short_contact(self):
341 return '%s %s' % (self.name, self.lastname)
341 return '%s %s' % (self.name, self.lastname)
342
342
343 @property
343 @property
344 def is_admin(self):
344 def is_admin(self):
345 return self.admin
345 return self.admin
346
346
347 def __unicode__(self):
347 def __unicode__(self):
348 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
348 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
349 self.user_id, self.username)
349 self.user_id, self.username)
350
350
351 @classmethod
351 @classmethod
352 def get_by_username(cls, username, case_insensitive=False, cache=False):
352 def get_by_username(cls, username, case_insensitive=False, cache=False):
353 if case_insensitive:
353 if case_insensitive:
354 q = cls.query().filter(cls.username.ilike(username))
354 q = cls.query().filter(cls.username.ilike(username))
355 else:
355 else:
356 q = cls.query().filter(cls.username == username)
356 q = cls.query().filter(cls.username == username)
357
357
358 if cache:
358 if cache:
359 q = q.options(FromCache(
359 q = q.options(FromCache(
360 "sql_cache_short",
360 "sql_cache_short",
361 "get_user_%s" % _hash_key(username)
361 "get_user_%s" % _hash_key(username)
362 )
362 )
363 )
363 )
364 return q.scalar()
364 return q.scalar()
365
365
366 @classmethod
366 @classmethod
367 def get_by_api_key(cls, api_key, cache=False):
367 def get_by_api_key(cls, api_key, cache=False):
368 q = cls.query().filter(cls.api_key == api_key)
368 q = cls.query().filter(cls.api_key == api_key)
369
369
370 if cache:
370 if cache:
371 q = q.options(FromCache("sql_cache_short",
371 q = q.options(FromCache("sql_cache_short",
372 "get_api_key_%s" % api_key))
372 "get_api_key_%s" % api_key))
373 return q.scalar()
373 return q.scalar()
374
374
375 @classmethod
375 @classmethod
376 def get_by_email(cls, email, case_insensitive=False, cache=False):
376 def get_by_email(cls, email, case_insensitive=False, cache=False):
377 if case_insensitive:
377 if case_insensitive:
378 q = cls.query().filter(cls.email.ilike(email))
378 q = cls.query().filter(cls.email.ilike(email))
379 else:
379 else:
380 q = cls.query().filter(cls.email == email)
380 q = cls.query().filter(cls.email == email)
381
381
382 if cache:
382 if cache:
383 q = q.options(FromCache("sql_cache_short",
383 q = q.options(FromCache("sql_cache_short",
384 "get_api_key_%s" % email))
384 "get_api_key_%s" % email))
385 return q.scalar()
385 return q.scalar()
386
386
387 def update_lastlogin(self):
387 def update_lastlogin(self):
388 """Update user lastlogin"""
388 """Update user lastlogin"""
389 self.last_login = datetime.datetime.now()
389 self.last_login = datetime.datetime.now()
390 Session.add(self)
390 Session.add(self)
391 log.debug('updated user %s lastlogin' % self.username)
391 log.debug('updated user %s lastlogin' % self.username)
392
392
393 def __json__(self):
393 def __json__(self):
394 return dict(
394 return dict(
395 user_id=self.user_id,
395 user_id=self.user_id,
396 first_name=self.name,
396 first_name=self.name,
397 last_name=self.lastname,
397 last_name=self.lastname,
398 email=self.email,
398 email=self.email,
399 full_name=self.full_name,
399 full_name=self.full_name,
400 full_name_or_username=self.full_name_or_username,
400 full_name_or_username=self.full_name_or_username,
401 short_contact=self.short_contact,
401 short_contact=self.short_contact,
402 full_contact=self.full_contact
402 full_contact=self.full_contact
403 )
403 )
404
404
405
405
406 class UserLog(Base, BaseModel):
406 class UserLog(Base, BaseModel):
407 __tablename__ = 'user_logs'
407 __tablename__ = 'user_logs'
408 __table_args__ = (
408 __table_args__ = (
409 {'extend_existing': True, 'mysql_engine': 'InnoDB',
409 {'extend_existing': True, 'mysql_engine': 'InnoDB',
410 'mysql_charset': 'utf8'},
410 'mysql_charset': 'utf8'},
411 )
411 )
412 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
412 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
413 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
413 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
415 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
415 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
416 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
416 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
417 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
417 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
418 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
418 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
419
419
420 @property
420 @property
421 def action_as_day(self):
421 def action_as_day(self):
422 return datetime.date(*self.action_date.timetuple()[:3])
422 return datetime.date(*self.action_date.timetuple()[:3])
423
423
424 user = relationship('User')
424 user = relationship('User')
425 repository = relationship('Repository', cascade='')
425 repository = relationship('Repository', cascade='')
426
426
427
427
428 class UsersGroup(Base, BaseModel):
428 class UsersGroup(Base, BaseModel):
429 __tablename__ = 'users_groups'
429 __tablename__ = 'users_groups'
430 __table_args__ = (
430 __table_args__ = (
431 {'extend_existing': True, 'mysql_engine': 'InnoDB',
431 {'extend_existing': True, 'mysql_engine': 'InnoDB',
432 'mysql_charset': 'utf8'},
432 'mysql_charset': 'utf8'},
433 )
433 )
434
434
435 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
435 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
436 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
436 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
437 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
437 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
438
438
439 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
439 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
440 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
440 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
441 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
441 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
442
442
443 def __unicode__(self):
443 def __unicode__(self):
444 return u'<userGroup(%s)>' % (self.users_group_name)
444 return u'<userGroup(%s)>' % (self.users_group_name)
445
445
446 @classmethod
446 @classmethod
447 def get_by_group_name(cls, group_name, cache=False,
447 def get_by_group_name(cls, group_name, cache=False,
448 case_insensitive=False):
448 case_insensitive=False):
449 if case_insensitive:
449 if case_insensitive:
450 q = cls.query().filter(cls.users_group_name.ilike(group_name))
450 q = cls.query().filter(cls.users_group_name.ilike(group_name))
451 else:
451 else:
452 q = cls.query().filter(cls.users_group_name == group_name)
452 q = cls.query().filter(cls.users_group_name == group_name)
453 if cache:
453 if cache:
454 q = q.options(FromCache(
454 q = q.options(FromCache(
455 "sql_cache_short",
455 "sql_cache_short",
456 "get_user_%s" % _hash_key(group_name)
456 "get_user_%s" % _hash_key(group_name)
457 )
457 )
458 )
458 )
459 return q.scalar()
459 return q.scalar()
460
460
461 @classmethod
461 @classmethod
462 def get(cls, users_group_id, cache=False):
462 def get(cls, users_group_id, cache=False):
463 users_group = cls.query()
463 users_group = cls.query()
464 if cache:
464 if cache:
465 users_group = users_group.options(FromCache("sql_cache_short",
465 users_group = users_group.options(FromCache("sql_cache_short",
466 "get_users_group_%s" % users_group_id))
466 "get_users_group_%s" % users_group_id))
467 return users_group.get(users_group_id)
467 return users_group.get(users_group_id)
468
468
469
469
470 class UsersGroupMember(Base, BaseModel):
470 class UsersGroupMember(Base, BaseModel):
471 __tablename__ = 'users_groups_members'
471 __tablename__ = 'users_groups_members'
472 __table_args__ = (
472 __table_args__ = (
473 {'extend_existing': True, 'mysql_engine': 'InnoDB',
473 {'extend_existing': True, 'mysql_engine': 'InnoDB',
474 'mysql_charset': 'utf8'},
474 'mysql_charset': 'utf8'},
475 )
475 )
476
476
477 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
477 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
478 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
478 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
479 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
479 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
480
480
481 user = relationship('User', lazy='joined')
481 user = relationship('User', lazy='joined')
482 users_group = relationship('UsersGroup')
482 users_group = relationship('UsersGroup')
483
483
484 def __init__(self, gr_id='', u_id=''):
484 def __init__(self, gr_id='', u_id=''):
485 self.users_group_id = gr_id
485 self.users_group_id = gr_id
486 self.user_id = u_id
486 self.user_id = u_id
487
487
488
488
489 class Repository(Base, BaseModel):
489 class Repository(Base, BaseModel):
490 __tablename__ = 'repositories'
490 __tablename__ = 'repositories'
491 __table_args__ = (
491 __table_args__ = (
492 UniqueConstraint('repo_name'),
492 UniqueConstraint('repo_name'),
493 {'extend_existing': True, 'mysql_engine': 'InnoDB',
493 {'extend_existing': True, 'mysql_engine': 'InnoDB',
494 'mysql_charset': 'utf8'},
494 'mysql_charset': 'utf8'},
495 )
495 )
496
496
497 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
497 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
498 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
498 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
499 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
499 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
500 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
500 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
501 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
501 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
502 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
502 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
503 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
503 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
504 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
504 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
505 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
505 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
506 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
506 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
507
507
508 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
508 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
509 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
509 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
510
510
511 user = relationship('User')
511 user = relationship('User')
512 fork = relationship('Repository', remote_side=repo_id)
512 fork = relationship('Repository', remote_side=repo_id)
513 group = relationship('RepoGroup')
513 group = relationship('RepoGroup')
514 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
514 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
515 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
515 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
516 stats = relationship('Statistics', cascade='all', uselist=False)
516 stats = relationship('Statistics', cascade='all', uselist=False)
517
517
518 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
518 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
519
519
520 logs = relationship('UserLog')
520 logs = relationship('UserLog')
521
521
522 def __unicode__(self):
522 def __unicode__(self):
523 return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id,
523 return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id,
524 self.repo_name)
524 self.repo_name)
525
525
526 @classmethod
526 @classmethod
527 def url_sep(cls):
527 def url_sep(cls):
528 return '/'
528 return '/'
529
529
530 @classmethod
530 @classmethod
531 def get_by_repo_name(cls, repo_name):
531 def get_by_repo_name(cls, repo_name):
532 q = Session.query(cls).filter(cls.repo_name == repo_name)
532 q = Session.query(cls).filter(cls.repo_name == repo_name)
533 q = q.options(joinedload(Repository.fork))\
533 q = q.options(joinedload(Repository.fork))\
534 .options(joinedload(Repository.user))\
534 .options(joinedload(Repository.user))\
535 .options(joinedload(Repository.group))
535 .options(joinedload(Repository.group))
536 return q.scalar()
536 return q.scalar()
537
537
538 @classmethod
538 @classmethod
539 def get_repo_forks(cls, repo_id):
539 def get_repo_forks(cls, repo_id):
540 return cls.query().filter(Repository.fork_id == repo_id)
540 return cls.query().filter(Repository.fork_id == repo_id)
541
541
542 @classmethod
542 @classmethod
543 def base_path(cls):
543 def base_path(cls):
544 """
544 """
545 Returns base path when all repos are stored
545 Returns base path when all repos are stored
546
546
547 :param cls:
547 :param cls:
548 """
548 """
549 q = Session.query(RhodeCodeUi)\
549 q = Session.query(RhodeCodeUi)\
550 .filter(RhodeCodeUi.ui_key == cls.url_sep())
550 .filter(RhodeCodeUi.ui_key == cls.url_sep())
551 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
551 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
552 return q.one().ui_value
552 return q.one().ui_value
553
553
554 @property
554 @property
555 def just_name(self):
555 def just_name(self):
556 return self.repo_name.split(Repository.url_sep())[-1]
556 return self.repo_name.split(Repository.url_sep())[-1]
557
557
558 @property
558 @property
559 def groups_with_parents(self):
559 def groups_with_parents(self):
560 groups = []
560 groups = []
561 if self.group is None:
561 if self.group is None:
562 return groups
562 return groups
563
563
564 cur_gr = self.group
564 cur_gr = self.group
565 groups.insert(0, cur_gr)
565 groups.insert(0, cur_gr)
566 while 1:
566 while 1:
567 gr = getattr(cur_gr, 'parent_group', None)
567 gr = getattr(cur_gr, 'parent_group', None)
568 cur_gr = cur_gr.parent_group
568 cur_gr = cur_gr.parent_group
569 if gr is None:
569 if gr is None:
570 break
570 break
571 groups.insert(0, gr)
571 groups.insert(0, gr)
572
572
573 return groups
573 return groups
574
574
575 @property
575 @property
576 def groups_and_repo(self):
576 def groups_and_repo(self):
577 return self.groups_with_parents, self.just_name
577 return self.groups_with_parents, self.just_name
578
578
579 @LazyProperty
579 @LazyProperty
580 def repo_path(self):
580 def repo_path(self):
581 """
581 """
582 Returns base full path for that repository means where it actually
582 Returns base full path for that repository means where it actually
583 exists on a filesystem
583 exists on a filesystem
584 """
584 """
585 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
585 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
586 Repository.url_sep())
586 Repository.url_sep())
587 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
587 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
588 return q.one().ui_value
588 return q.one().ui_value
589
589
590 @property
590 @property
591 def repo_full_path(self):
591 def repo_full_path(self):
592 p = [self.repo_path]
592 p = [self.repo_path]
593 # we need to split the name by / since this is how we store the
593 # we need to split the name by / since this is how we store the
594 # names in the database, but that eventually needs to be converted
594 # names in the database, but that eventually needs to be converted
595 # into a valid system path
595 # into a valid system path
596 p += self.repo_name.split(Repository.url_sep())
596 p += self.repo_name.split(Repository.url_sep())
597 return os.path.join(*p)
597 return os.path.join(*p)
598
598
599 def get_new_name(self, repo_name):
599 def get_new_name(self, repo_name):
600 """
600 """
601 returns new full repository name based on assigned group and new new
601 returns new full repository name based on assigned group and new new
602
602
603 :param group_name:
603 :param group_name:
604 """
604 """
605 path_prefix = self.group.full_path_splitted if self.group else []
605 path_prefix = self.group.full_path_splitted if self.group else []
606 return Repository.url_sep().join(path_prefix + [repo_name])
606 return Repository.url_sep().join(path_prefix + [repo_name])
607
607
608 @property
608 @property
609 def _ui(self):
609 def _ui(self):
610 """
610 """
611 Creates an db based ui object for this repository
611 Creates an db based ui object for this repository
612 """
612 """
613 from mercurial import ui
613 from mercurial import ui
614 from mercurial import config
614 from mercurial import config
615 baseui = ui.ui()
615 baseui = ui.ui()
616
616
617 #clean the baseui object
617 #clean the baseui object
618 baseui._ocfg = config.config()
618 baseui._ocfg = config.config()
619 baseui._ucfg = config.config()
619 baseui._ucfg = config.config()
620 baseui._tcfg = config.config()
620 baseui._tcfg = config.config()
621
621
622 ret = RhodeCodeUi.query()\
622 ret = RhodeCodeUi.query()\
623 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
623 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
624
624
625 hg_ui = ret
625 hg_ui = ret
626 for ui_ in hg_ui:
626 for ui_ in hg_ui:
627 if ui_.ui_active:
627 if ui_.ui_active:
628 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
628 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
629 ui_.ui_key, ui_.ui_value)
629 ui_.ui_key, ui_.ui_value)
630 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
630 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
631
631
632 return baseui
632 return baseui
633
633
634 @classmethod
634 @classmethod
635 def is_valid(cls, repo_name):
635 def is_valid(cls, repo_name):
636 """
636 """
637 returns True if given repo name is a valid filesystem repository
637 returns True if given repo name is a valid filesystem repository
638
638
639 :param cls:
639 :param cls:
640 :param repo_name:
640 :param repo_name:
641 """
641 """
642 from rhodecode.lib.utils import is_valid_repo
642 from rhodecode.lib.utils import is_valid_repo
643
643
644 return is_valid_repo(repo_name, cls.base_path())
644 return is_valid_repo(repo_name, cls.base_path())
645
645
646 #==========================================================================
646 #==========================================================================
647 # SCM PROPERTIES
647 # SCM PROPERTIES
648 #==========================================================================
648 #==========================================================================
649
649
650 def get_changeset(self, rev):
650 def get_changeset(self, rev=None):
651 return get_changeset_safe(self.scm_instance, rev)
651 return get_changeset_safe(self.scm_instance, rev)
652
652
653 @property
653 @property
654 def tip(self):
654 def tip(self):
655 return self.get_changeset('tip')
655 return self.get_changeset('tip')
656
656
657 @property
657 @property
658 def author(self):
658 def author(self):
659 return self.tip.author
659 return self.tip.author
660
660
661 @property
661 @property
662 def last_change(self):
662 def last_change(self):
663 return self.scm_instance.last_change
663 return self.scm_instance.last_change
664
664
665 def comments(self, revisions=None):
665 def comments(self, revisions=None):
666 """
666 """
667 Returns comments for this repository grouped by revisions
667 Returns comments for this repository grouped by revisions
668
668
669 :param revisions: filter query by revisions only
669 :param revisions: filter query by revisions only
670 """
670 """
671 cmts = ChangesetComment.query()\
671 cmts = ChangesetComment.query()\
672 .filter(ChangesetComment.repo == self)
672 .filter(ChangesetComment.repo == self)
673 if revisions:
673 if revisions:
674 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
674 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
675 grouped = defaultdict(list)
675 grouped = defaultdict(list)
676 for cmt in cmts.all():
676 for cmt in cmts.all():
677 grouped[cmt.revision].append(cmt)
677 grouped[cmt.revision].append(cmt)
678 return grouped
678 return grouped
679
679
680 def statuses(self, revisions=None):
680 def statuses(self, revisions=None):
681 """
681 """
682 Returns statuses for this repository
682 Returns statuses for this repository
683
683
684 :param revisions: list of revisions to get statuses for
684 :param revisions: list of revisions to get statuses for
685 :type revisions: list
685 :type revisions: list
686 """
686 """
687
687
688 statuses = ChangesetStatus.query()\
688 statuses = ChangesetStatus.query()\
689 .filter(ChangesetStatus.repo == self)
689 .filter(ChangesetStatus.repo == self)
690 if revisions:
690 if revisions:
691 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
691 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
692 grouped = {}
692 grouped = {}
693 for stat in statuses.all():
693 for stat in statuses.all():
694 grouped[stat.revision] = [str(stat.status), stat.status_lbl]
694 grouped[stat.revision] = [str(stat.status), stat.status_lbl]
695 return grouped
695 return grouped
696
696
697 #==========================================================================
697 #==========================================================================
698 # SCM CACHE INSTANCE
698 # SCM CACHE INSTANCE
699 #==========================================================================
699 #==========================================================================
700
700
701 @property
701 @property
702 def invalidate(self):
702 def invalidate(self):
703 return CacheInvalidation.invalidate(self.repo_name)
703 return CacheInvalidation.invalidate(self.repo_name)
704
704
705 def set_invalidate(self):
705 def set_invalidate(self):
706 """
706 """
707 set a cache for invalidation for this instance
707 set a cache for invalidation for this instance
708 """
708 """
709 CacheInvalidation.set_invalidate(self.repo_name)
709 CacheInvalidation.set_invalidate(self.repo_name)
710
710
711 @LazyProperty
711 @LazyProperty
712 def scm_instance(self):
712 def scm_instance(self):
713 return self.__get_instance()
713 return self.__get_instance()
714
714
715 @property
715 @property
716 def scm_instance_cached(self):
716 def scm_instance_cached(self):
717 @cache_region('long_term')
717 @cache_region('long_term')
718 def _c(repo_name):
718 def _c(repo_name):
719 return self.__get_instance()
719 return self.__get_instance()
720 rn = self.repo_name
720 rn = self.repo_name
721 log.debug('Getting cached instance of repo')
721 log.debug('Getting cached instance of repo')
722 inv = self.invalidate
722 inv = self.invalidate
723 if inv is not None:
723 if inv is not None:
724 region_invalidate(_c, None, rn)
724 region_invalidate(_c, None, rn)
725 # update our cache
725 # update our cache
726 CacheInvalidation.set_valid(inv.cache_key)
726 CacheInvalidation.set_valid(inv.cache_key)
727 return _c(rn)
727 return _c(rn)
728
728
729 def __get_instance(self):
729 def __get_instance(self):
730 repo_full_path = self.repo_full_path
730 repo_full_path = self.repo_full_path
731 try:
731 try:
732 alias = get_scm(repo_full_path)[0]
732 alias = get_scm(repo_full_path)[0]
733 log.debug('Creating instance of %s repository' % alias)
733 log.debug('Creating instance of %s repository' % alias)
734 backend = get_backend(alias)
734 backend = get_backend(alias)
735 except VCSError:
735 except VCSError:
736 log.error(traceback.format_exc())
736 log.error(traceback.format_exc())
737 log.error('Perhaps this repository is in db and not in '
737 log.error('Perhaps this repository is in db and not in '
738 'filesystem run rescan repositories with '
738 'filesystem run rescan repositories with '
739 '"destroy old data " option from admin panel')
739 '"destroy old data " option from admin panel')
740 return
740 return
741
741
742 if alias == 'hg':
742 if alias == 'hg':
743
743
744 repo = backend(safe_str(repo_full_path), create=False,
744 repo = backend(safe_str(repo_full_path), create=False,
745 baseui=self._ui)
745 baseui=self._ui)
746 # skip hidden web repository
746 # skip hidden web repository
747 if repo._get_hidden():
747 if repo._get_hidden():
748 return
748 return
749 else:
749 else:
750 repo = backend(repo_full_path, create=False)
750 repo = backend(repo_full_path, create=False)
751
751
752 return repo
752 return repo
753
753
754
754
755 class RepoGroup(Base, BaseModel):
755 class RepoGroup(Base, BaseModel):
756 __tablename__ = 'groups'
756 __tablename__ = 'groups'
757 __table_args__ = (
757 __table_args__ = (
758 UniqueConstraint('group_name', 'group_parent_id'),
758 UniqueConstraint('group_name', 'group_parent_id'),
759 CheckConstraint('group_id != group_parent_id'),
759 CheckConstraint('group_id != group_parent_id'),
760 {'extend_existing': True, 'mysql_engine': 'InnoDB',
760 {'extend_existing': True, 'mysql_engine': 'InnoDB',
761 'mysql_charset': 'utf8'},
761 'mysql_charset': 'utf8'},
762 )
762 )
763 __mapper_args__ = {'order_by': 'group_name'}
763 __mapper_args__ = {'order_by': 'group_name'}
764
764
765 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
765 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
766 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
766 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
767 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
767 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
768 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
768 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
769
769
770 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
770 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
771 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
771 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
772
772
773 parent_group = relationship('RepoGroup', remote_side=group_id)
773 parent_group = relationship('RepoGroup', remote_side=group_id)
774
774
775 def __init__(self, group_name='', parent_group=None):
775 def __init__(self, group_name='', parent_group=None):
776 self.group_name = group_name
776 self.group_name = group_name
777 self.parent_group = parent_group
777 self.parent_group = parent_group
778
778
779 def __unicode__(self):
779 def __unicode__(self):
780 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
780 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
781 self.group_name)
781 self.group_name)
782
782
783 @classmethod
783 @classmethod
784 def groups_choices(cls):
784 def groups_choices(cls):
785 from webhelpers.html import literal as _literal
785 from webhelpers.html import literal as _literal
786 repo_groups = [('', '')]
786 repo_groups = [('', '')]
787 sep = ' &raquo; '
787 sep = ' &raquo; '
788 _name = lambda k: _literal(sep.join(k))
788 _name = lambda k: _literal(sep.join(k))
789
789
790 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
790 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
791 for x in cls.query().all()])
791 for x in cls.query().all()])
792
792
793 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
793 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
794 return repo_groups
794 return repo_groups
795
795
796 @classmethod
796 @classmethod
797 def url_sep(cls):
797 def url_sep(cls):
798 return '/'
798 return '/'
799
799
800 @classmethod
800 @classmethod
801 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
801 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
802 if case_insensitive:
802 if case_insensitive:
803 gr = cls.query()\
803 gr = cls.query()\
804 .filter(cls.group_name.ilike(group_name))
804 .filter(cls.group_name.ilike(group_name))
805 else:
805 else:
806 gr = cls.query()\
806 gr = cls.query()\
807 .filter(cls.group_name == group_name)
807 .filter(cls.group_name == group_name)
808 if cache:
808 if cache:
809 gr = gr.options(FromCache(
809 gr = gr.options(FromCache(
810 "sql_cache_short",
810 "sql_cache_short",
811 "get_group_%s" % _hash_key(group_name)
811 "get_group_%s" % _hash_key(group_name)
812 )
812 )
813 )
813 )
814 return gr.scalar()
814 return gr.scalar()
815
815
816 @property
816 @property
817 def parents(self):
817 def parents(self):
818 parents_recursion_limit = 5
818 parents_recursion_limit = 5
819 groups = []
819 groups = []
820 if self.parent_group is None:
820 if self.parent_group is None:
821 return groups
821 return groups
822 cur_gr = self.parent_group
822 cur_gr = self.parent_group
823 groups.insert(0, cur_gr)
823 groups.insert(0, cur_gr)
824 cnt = 0
824 cnt = 0
825 while 1:
825 while 1:
826 cnt += 1
826 cnt += 1
827 gr = getattr(cur_gr, 'parent_group', None)
827 gr = getattr(cur_gr, 'parent_group', None)
828 cur_gr = cur_gr.parent_group
828 cur_gr = cur_gr.parent_group
829 if gr is None:
829 if gr is None:
830 break
830 break
831 if cnt == parents_recursion_limit:
831 if cnt == parents_recursion_limit:
832 # this will prevent accidental infinit loops
832 # this will prevent accidental infinit loops
833 log.error('group nested more than %s' %
833 log.error('group nested more than %s' %
834 parents_recursion_limit)
834 parents_recursion_limit)
835 break
835 break
836
836
837 groups.insert(0, gr)
837 groups.insert(0, gr)
838 return groups
838 return groups
839
839
840 @property
840 @property
841 def children(self):
841 def children(self):
842 return RepoGroup.query().filter(RepoGroup.parent_group == self)
842 return RepoGroup.query().filter(RepoGroup.parent_group == self)
843
843
844 @property
844 @property
845 def name(self):
845 def name(self):
846 return self.group_name.split(RepoGroup.url_sep())[-1]
846 return self.group_name.split(RepoGroup.url_sep())[-1]
847
847
848 @property
848 @property
849 def full_path(self):
849 def full_path(self):
850 return self.group_name
850 return self.group_name
851
851
852 @property
852 @property
853 def full_path_splitted(self):
853 def full_path_splitted(self):
854 return self.group_name.split(RepoGroup.url_sep())
854 return self.group_name.split(RepoGroup.url_sep())
855
855
856 @property
856 @property
857 def repositories(self):
857 def repositories(self):
858 return Repository.query()\
858 return Repository.query()\
859 .filter(Repository.group == self)\
859 .filter(Repository.group == self)\
860 .order_by(Repository.repo_name)
860 .order_by(Repository.repo_name)
861
861
862 @property
862 @property
863 def repositories_recursive_count(self):
863 def repositories_recursive_count(self):
864 cnt = self.repositories.count()
864 cnt = self.repositories.count()
865
865
866 def children_count(group):
866 def children_count(group):
867 cnt = 0
867 cnt = 0
868 for child in group.children:
868 for child in group.children:
869 cnt += child.repositories.count()
869 cnt += child.repositories.count()
870 cnt += children_count(child)
870 cnt += children_count(child)
871 return cnt
871 return cnt
872
872
873 return cnt + children_count(self)
873 return cnt + children_count(self)
874
874
875 def get_new_name(self, group_name):
875 def get_new_name(self, group_name):
876 """
876 """
877 returns new full group name based on parent and new name
877 returns new full group name based on parent and new name
878
878
879 :param group_name:
879 :param group_name:
880 """
880 """
881 path_prefix = (self.parent_group.full_path_splitted if
881 path_prefix = (self.parent_group.full_path_splitted if
882 self.parent_group else [])
882 self.parent_group else [])
883 return RepoGroup.url_sep().join(path_prefix + [group_name])
883 return RepoGroup.url_sep().join(path_prefix + [group_name])
884
884
885
885
886 class Permission(Base, BaseModel):
886 class Permission(Base, BaseModel):
887 __tablename__ = 'permissions'
887 __tablename__ = 'permissions'
888 __table_args__ = (
888 __table_args__ = (
889 {'extend_existing': True, 'mysql_engine': 'InnoDB',
889 {'extend_existing': True, 'mysql_engine': 'InnoDB',
890 'mysql_charset': 'utf8'},
890 'mysql_charset': 'utf8'},
891 )
891 )
892 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
892 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
893 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
893 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
894 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
894 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
895
895
896 def __unicode__(self):
896 def __unicode__(self):
897 return u"<%s('%s:%s')>" % (
897 return u"<%s('%s:%s')>" % (
898 self.__class__.__name__, self.permission_id, self.permission_name
898 self.__class__.__name__, self.permission_id, self.permission_name
899 )
899 )
900
900
901 @classmethod
901 @classmethod
902 def get_by_key(cls, key):
902 def get_by_key(cls, key):
903 return cls.query().filter(cls.permission_name == key).scalar()
903 return cls.query().filter(cls.permission_name == key).scalar()
904
904
905 @classmethod
905 @classmethod
906 def get_default_perms(cls, default_user_id):
906 def get_default_perms(cls, default_user_id):
907 q = Session.query(UserRepoToPerm, Repository, cls)\
907 q = Session.query(UserRepoToPerm, Repository, cls)\
908 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
908 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
909 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
909 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
910 .filter(UserRepoToPerm.user_id == default_user_id)
910 .filter(UserRepoToPerm.user_id == default_user_id)
911
911
912 return q.all()
912 return q.all()
913
913
914 @classmethod
914 @classmethod
915 def get_default_group_perms(cls, default_user_id):
915 def get_default_group_perms(cls, default_user_id):
916 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
916 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
917 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
917 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
918 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
918 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
919 .filter(UserRepoGroupToPerm.user_id == default_user_id)
919 .filter(UserRepoGroupToPerm.user_id == default_user_id)
920
920
921 return q.all()
921 return q.all()
922
922
923
923
924 class UserRepoToPerm(Base, BaseModel):
924 class UserRepoToPerm(Base, BaseModel):
925 __tablename__ = 'repo_to_perm'
925 __tablename__ = 'repo_to_perm'
926 __table_args__ = (
926 __table_args__ = (
927 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
927 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
928 {'extend_existing': True, 'mysql_engine': 'InnoDB',
928 {'extend_existing': True, 'mysql_engine': 'InnoDB',
929 'mysql_charset': 'utf8'}
929 'mysql_charset': 'utf8'}
930 )
930 )
931 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
931 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
932 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
932 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
933 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
933 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
934 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
934 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
935
935
936 user = relationship('User')
936 user = relationship('User')
937 repository = relationship('Repository')
937 repository = relationship('Repository')
938 permission = relationship('Permission')
938 permission = relationship('Permission')
939
939
940 @classmethod
940 @classmethod
941 def create(cls, user, repository, permission):
941 def create(cls, user, repository, permission):
942 n = cls()
942 n = cls()
943 n.user = user
943 n.user = user
944 n.repository = repository
944 n.repository = repository
945 n.permission = permission
945 n.permission = permission
946 Session.add(n)
946 Session.add(n)
947 return n
947 return n
948
948
949 def __unicode__(self):
949 def __unicode__(self):
950 return u'<user:%s => %s >' % (self.user, self.repository)
950 return u'<user:%s => %s >' % (self.user, self.repository)
951
951
952
952
953 class UserToPerm(Base, BaseModel):
953 class UserToPerm(Base, BaseModel):
954 __tablename__ = 'user_to_perm'
954 __tablename__ = 'user_to_perm'
955 __table_args__ = (
955 __table_args__ = (
956 UniqueConstraint('user_id', 'permission_id'),
956 UniqueConstraint('user_id', 'permission_id'),
957 {'extend_existing': True, 'mysql_engine': 'InnoDB',
957 {'extend_existing': True, 'mysql_engine': 'InnoDB',
958 'mysql_charset': 'utf8'}
958 'mysql_charset': 'utf8'}
959 )
959 )
960 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
960 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
961 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
961 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
962 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
962 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
963
963
964 user = relationship('User')
964 user = relationship('User')
965 permission = relationship('Permission', lazy='joined')
965 permission = relationship('Permission', lazy='joined')
966
966
967
967
968 class UsersGroupRepoToPerm(Base, BaseModel):
968 class UsersGroupRepoToPerm(Base, BaseModel):
969 __tablename__ = 'users_group_repo_to_perm'
969 __tablename__ = 'users_group_repo_to_perm'
970 __table_args__ = (
970 __table_args__ = (
971 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
971 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
972 {'extend_existing': True, 'mysql_engine': 'InnoDB',
972 {'extend_existing': True, 'mysql_engine': 'InnoDB',
973 'mysql_charset': 'utf8'}
973 'mysql_charset': 'utf8'}
974 )
974 )
975 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
975 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
976 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
976 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
977 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
977 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
978 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
978 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
979
979
980 users_group = relationship('UsersGroup')
980 users_group = relationship('UsersGroup')
981 permission = relationship('Permission')
981 permission = relationship('Permission')
982 repository = relationship('Repository')
982 repository = relationship('Repository')
983
983
984 @classmethod
984 @classmethod
985 def create(cls, users_group, repository, permission):
985 def create(cls, users_group, repository, permission):
986 n = cls()
986 n = cls()
987 n.users_group = users_group
987 n.users_group = users_group
988 n.repository = repository
988 n.repository = repository
989 n.permission = permission
989 n.permission = permission
990 Session.add(n)
990 Session.add(n)
991 return n
991 return n
992
992
993 def __unicode__(self):
993 def __unicode__(self):
994 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
994 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
995
995
996
996
997 class UsersGroupToPerm(Base, BaseModel):
997 class UsersGroupToPerm(Base, BaseModel):
998 __tablename__ = 'users_group_to_perm'
998 __tablename__ = 'users_group_to_perm'
999 __table_args__ = (
999 __table_args__ = (
1000 UniqueConstraint('users_group_id', 'permission_id',),
1000 UniqueConstraint('users_group_id', 'permission_id',),
1001 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1001 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1002 'mysql_charset': 'utf8'}
1002 'mysql_charset': 'utf8'}
1003 )
1003 )
1004 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1004 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1005 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1005 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1006 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1006 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1007
1007
1008 users_group = relationship('UsersGroup')
1008 users_group = relationship('UsersGroup')
1009 permission = relationship('Permission')
1009 permission = relationship('Permission')
1010
1010
1011
1011
1012 class UserRepoGroupToPerm(Base, BaseModel):
1012 class UserRepoGroupToPerm(Base, BaseModel):
1013 __tablename__ = 'user_repo_group_to_perm'
1013 __tablename__ = 'user_repo_group_to_perm'
1014 __table_args__ = (
1014 __table_args__ = (
1015 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1015 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1016 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1016 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1017 'mysql_charset': 'utf8'}
1017 'mysql_charset': 'utf8'}
1018 )
1018 )
1019
1019
1020 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1020 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1021 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1021 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1022 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1022 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1023 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1023 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1024
1024
1025 user = relationship('User')
1025 user = relationship('User')
1026 group = relationship('RepoGroup')
1026 group = relationship('RepoGroup')
1027 permission = relationship('Permission')
1027 permission = relationship('Permission')
1028
1028
1029
1029
1030 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1030 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1031 __tablename__ = 'users_group_repo_group_to_perm'
1031 __tablename__ = 'users_group_repo_group_to_perm'
1032 __table_args__ = (
1032 __table_args__ = (
1033 UniqueConstraint('users_group_id', 'group_id'),
1033 UniqueConstraint('users_group_id', 'group_id'),
1034 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1034 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1035 'mysql_charset': 'utf8'}
1035 'mysql_charset': 'utf8'}
1036 )
1036 )
1037
1037
1038 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)
1038 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)
1039 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1039 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1040 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1040 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1041 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1041 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1042
1042
1043 users_group = relationship('UsersGroup')
1043 users_group = relationship('UsersGroup')
1044 permission = relationship('Permission')
1044 permission = relationship('Permission')
1045 group = relationship('RepoGroup')
1045 group = relationship('RepoGroup')
1046
1046
1047
1047
1048 class Statistics(Base, BaseModel):
1048 class Statistics(Base, BaseModel):
1049 __tablename__ = 'statistics'
1049 __tablename__ = 'statistics'
1050 __table_args__ = (
1050 __table_args__ = (
1051 UniqueConstraint('repository_id'),
1051 UniqueConstraint('repository_id'),
1052 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1052 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1053 'mysql_charset': 'utf8'}
1053 'mysql_charset': 'utf8'}
1054 )
1054 )
1055 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1055 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1056 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1056 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1057 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1057 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1058 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1058 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1059 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1059 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1060 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1060 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1061
1061
1062 repository = relationship('Repository', single_parent=True)
1062 repository = relationship('Repository', single_parent=True)
1063
1063
1064
1064
1065 class UserFollowing(Base, BaseModel):
1065 class UserFollowing(Base, BaseModel):
1066 __tablename__ = 'user_followings'
1066 __tablename__ = 'user_followings'
1067 __table_args__ = (
1067 __table_args__ = (
1068 UniqueConstraint('user_id', 'follows_repository_id'),
1068 UniqueConstraint('user_id', 'follows_repository_id'),
1069 UniqueConstraint('user_id', 'follows_user_id'),
1069 UniqueConstraint('user_id', 'follows_user_id'),
1070 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1070 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1071 'mysql_charset': 'utf8'}
1071 'mysql_charset': 'utf8'}
1072 )
1072 )
1073
1073
1074 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1074 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1075 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1075 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1076 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1076 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1077 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1077 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1078 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1078 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1079
1079
1080 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1080 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1081
1081
1082 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1082 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1083 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1083 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1084
1084
1085 @classmethod
1085 @classmethod
1086 def get_repo_followers(cls, repo_id):
1086 def get_repo_followers(cls, repo_id):
1087 return cls.query().filter(cls.follows_repo_id == repo_id)
1087 return cls.query().filter(cls.follows_repo_id == repo_id)
1088
1088
1089
1089
1090 class CacheInvalidation(Base, BaseModel):
1090 class CacheInvalidation(Base, BaseModel):
1091 __tablename__ = 'cache_invalidation'
1091 __tablename__ = 'cache_invalidation'
1092 __table_args__ = (
1092 __table_args__ = (
1093 UniqueConstraint('cache_key'),
1093 UniqueConstraint('cache_key'),
1094 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1094 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1095 'mysql_charset': 'utf8'},
1095 'mysql_charset': 'utf8'},
1096 )
1096 )
1097 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1097 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1098 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1098 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1099 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1099 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1100 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1100 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1101
1101
1102 def __init__(self, cache_key, cache_args=''):
1102 def __init__(self, cache_key, cache_args=''):
1103 self.cache_key = cache_key
1103 self.cache_key = cache_key
1104 self.cache_args = cache_args
1104 self.cache_args = cache_args
1105 self.cache_active = False
1105 self.cache_active = False
1106
1106
1107 def __unicode__(self):
1107 def __unicode__(self):
1108 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1108 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1109 self.cache_id, self.cache_key)
1109 self.cache_id, self.cache_key)
1110 @classmethod
1110 @classmethod
1111 def clear_cache(cls):
1111 def clear_cache(cls):
1112 cls.query().delete()
1112 cls.query().delete()
1113
1113
1114 @classmethod
1114 @classmethod
1115 def _get_key(cls, key):
1115 def _get_key(cls, key):
1116 """
1116 """
1117 Wrapper for generating a key, together with a prefix
1117 Wrapper for generating a key, together with a prefix
1118
1118
1119 :param key:
1119 :param key:
1120 """
1120 """
1121 import rhodecode
1121 import rhodecode
1122 prefix = ''
1122 prefix = ''
1123 iid = rhodecode.CONFIG.get('instance_id')
1123 iid = rhodecode.CONFIG.get('instance_id')
1124 if iid:
1124 if iid:
1125 prefix = iid
1125 prefix = iid
1126 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1126 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1127
1127
1128 @classmethod
1128 @classmethod
1129 def get_by_key(cls, key):
1129 def get_by_key(cls, key):
1130 return cls.query().filter(cls.cache_key == key).scalar()
1130 return cls.query().filter(cls.cache_key == key).scalar()
1131
1131
1132 @classmethod
1132 @classmethod
1133 def _get_or_create_key(cls, key, prefix, org_key):
1133 def _get_or_create_key(cls, key, prefix, org_key):
1134 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1134 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1135 if not inv_obj:
1135 if not inv_obj:
1136 try:
1136 try:
1137 inv_obj = CacheInvalidation(key, org_key)
1137 inv_obj = CacheInvalidation(key, org_key)
1138 Session.add(inv_obj)
1138 Session.add(inv_obj)
1139 Session.commit()
1139 Session.commit()
1140 except Exception:
1140 except Exception:
1141 log.error(traceback.format_exc())
1141 log.error(traceback.format_exc())
1142 Session.rollback()
1142 Session.rollback()
1143 return inv_obj
1143 return inv_obj
1144
1144
1145 @classmethod
1145 @classmethod
1146 def invalidate(cls, key):
1146 def invalidate(cls, key):
1147 """
1147 """
1148 Returns Invalidation object if this given key should be invalidated
1148 Returns Invalidation object if this given key should be invalidated
1149 None otherwise. `cache_active = False` means that this cache
1149 None otherwise. `cache_active = False` means that this cache
1150 state is not valid and needs to be invalidated
1150 state is not valid and needs to be invalidated
1151
1151
1152 :param key:
1152 :param key:
1153 """
1153 """
1154
1154
1155 key, _prefix, _org_key = cls._get_key(key)
1155 key, _prefix, _org_key = cls._get_key(key)
1156 inv = cls._get_or_create_key(key, _prefix, _org_key)
1156 inv = cls._get_or_create_key(key, _prefix, _org_key)
1157
1157
1158 if inv and inv.cache_active is False:
1158 if inv and inv.cache_active is False:
1159 return inv
1159 return inv
1160
1160
1161 @classmethod
1161 @classmethod
1162 def set_invalidate(cls, key):
1162 def set_invalidate(cls, key):
1163 """
1163 """
1164 Mark this Cache key for invalidation
1164 Mark this Cache key for invalidation
1165
1165
1166 :param key:
1166 :param key:
1167 """
1167 """
1168
1168
1169 key, _prefix, _org_key = cls._get_key(key)
1169 key, _prefix, _org_key = cls._get_key(key)
1170 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1170 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1171 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1171 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1172 _org_key))
1172 _org_key))
1173 try:
1173 try:
1174 for inv_obj in inv_objs:
1174 for inv_obj in inv_objs:
1175 if inv_obj:
1175 if inv_obj:
1176 inv_obj.cache_active = False
1176 inv_obj.cache_active = False
1177
1177
1178 Session.add(inv_obj)
1178 Session.add(inv_obj)
1179 Session.commit()
1179 Session.commit()
1180 except Exception:
1180 except Exception:
1181 log.error(traceback.format_exc())
1181 log.error(traceback.format_exc())
1182 Session.rollback()
1182 Session.rollback()
1183
1183
1184 @classmethod
1184 @classmethod
1185 def set_valid(cls, key):
1185 def set_valid(cls, key):
1186 """
1186 """
1187 Mark this cache key as active and currently cached
1187 Mark this cache key as active and currently cached
1188
1188
1189 :param key:
1189 :param key:
1190 """
1190 """
1191 inv_obj = cls.get_by_key(key)
1191 inv_obj = cls.get_by_key(key)
1192 inv_obj.cache_active = True
1192 inv_obj.cache_active = True
1193 Session.add(inv_obj)
1193 Session.add(inv_obj)
1194 Session.commit()
1194 Session.commit()
1195
1195
1196
1196
1197 class ChangesetComment(Base, BaseModel):
1197 class ChangesetComment(Base, BaseModel):
1198 __tablename__ = 'changeset_comments'
1198 __tablename__ = 'changeset_comments'
1199 __table_args__ = (
1199 __table_args__ = (
1200 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1200 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1201 'mysql_charset': 'utf8'},
1201 'mysql_charset': 'utf8'},
1202 )
1202 )
1203 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1203 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1204 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1204 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1205 revision = Column('revision', String(40), nullable=False)
1205 revision = Column('revision', String(40), nullable=False)
1206 line_no = Column('line_no', Unicode(10), nullable=True)
1206 line_no = Column('line_no', Unicode(10), nullable=True)
1207 f_path = Column('f_path', Unicode(1000), nullable=True)
1207 f_path = Column('f_path', Unicode(1000), nullable=True)
1208 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1208 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1209 text = Column('text', Unicode(25000), nullable=False)
1209 text = Column('text', Unicode(25000), nullable=False)
1210 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1210 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1211
1211
1212 author = relationship('User', lazy='joined')
1212 author = relationship('User', lazy='joined')
1213 repo = relationship('Repository')
1213 repo = relationship('Repository')
1214
1214
1215 @classmethod
1215 @classmethod
1216 def get_users(cls, revision):
1216 def get_users(cls, revision):
1217 """
1217 """
1218 Returns user associated with this changesetComment. ie those
1218 Returns user associated with this changesetComment. ie those
1219 who actually commented
1219 who actually commented
1220
1220
1221 :param cls:
1221 :param cls:
1222 :param revision:
1222 :param revision:
1223 """
1223 """
1224 return Session.query(User)\
1224 return Session.query(User)\
1225 .filter(cls.revision == revision)\
1225 .filter(cls.revision == revision)\
1226 .join(ChangesetComment.author).all()
1226 .join(ChangesetComment.author).all()
1227
1227
1228
1228
1229 class ChangesetStatus(Base, BaseModel):
1229 class ChangesetStatus(Base, BaseModel):
1230 __tablename__ = 'changeset_statuses'
1230 __tablename__ = 'changeset_statuses'
1231 __table_args__ = (
1231 __table_args__ = (
1232 UniqueConstraint('repo_id', 'revision'),
1232 UniqueConstraint('repo_id', 'revision'),
1233 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1233 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1234 'mysql_charset': 'utf8'}
1234 'mysql_charset': 'utf8'}
1235 )
1235 )
1236
1236
1237 STATUSES = [
1237 STATUSES = [
1238 ('not_reviewed', _("Not Reviewed")), # (no icon) and default
1238 ('not_reviewed', _("Not Reviewed")), # (no icon) and default
1239 ('approved', _("Approved")),
1239 ('approved', _("Approved")),
1240 ('rejected', _("Rejected")),
1240 ('rejected', _("Rejected")),
1241 ('under_review', _("Under Review")),
1241 ('under_review', _("Under Review")),
1242 ]
1242 ]
1243 DEFAULT = STATUSES[0][0]
1243 DEFAULT = STATUSES[0][0]
1244
1244
1245 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1245 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1246 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1246 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1247 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1247 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1248 revision = Column('revision', String(40), nullable=False)
1248 revision = Column('revision', String(40), nullable=False)
1249 status = Column('status', String(128), nullable=False, default=DEFAULT)
1249 status = Column('status', String(128), nullable=False, default=DEFAULT)
1250 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1250 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1251
1251
1252 author = relationship('User', lazy='joined')
1252 author = relationship('User', lazy='joined')
1253 repo = relationship('Repository')
1253 repo = relationship('Repository')
1254
1254
1255 @property
1255 @property
1256 def status_lbl(self):
1256 def status_lbl(self):
1257 return dict(self.STATUSES).get(self.status)
1257 return dict(self.STATUSES).get(self.status)
1258
1258
1259
1259
1260 class ChangesetStatusHistory(Base, BaseModel):
1260 class ChangesetStatusHistory(Base, BaseModel):
1261 __tablename__ = 'changeset_statuses_history'
1261 __tablename__ = 'changeset_statuses_history'
1262 __table_args__ = (
1262 __table_args__ = (
1263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1264 'mysql_charset': 'utf8'}
1264 'mysql_charset': 'utf8'}
1265 )
1265 )
1266 #TODO: check if sqla has a nice history table implementation
1266 #TODO: check if sqla has a nice history table implementation
1267 changeset_status_id = Column('changeset_status_id', Integer(), ForeignKey('changeset_statuses.changeset_status_id'), nullable=False, primary_key=True)
1267 changeset_status_id = Column('changeset_status_id', Integer(), ForeignKey('changeset_statuses.changeset_status_id'), nullable=False, primary_key=True)
1268 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1268 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1269 status = Column('status', String(128), nullable=False)
1269 status = Column('status', String(128), nullable=False)
1270 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1270 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1271
1271
1272
1272
1273 class Notification(Base, BaseModel):
1273 class Notification(Base, BaseModel):
1274 __tablename__ = 'notifications'
1274 __tablename__ = 'notifications'
1275 __table_args__ = (
1275 __table_args__ = (
1276 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1276 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1277 'mysql_charset': 'utf8'},
1277 'mysql_charset': 'utf8'},
1278 )
1278 )
1279
1279
1280 TYPE_CHANGESET_COMMENT = u'cs_comment'
1280 TYPE_CHANGESET_COMMENT = u'cs_comment'
1281 TYPE_MESSAGE = u'message'
1281 TYPE_MESSAGE = u'message'
1282 TYPE_MENTION = u'mention'
1282 TYPE_MENTION = u'mention'
1283 TYPE_REGISTRATION = u'registration'
1283 TYPE_REGISTRATION = u'registration'
1284 TYPE_PULL_REQUEST = u'pull_request'
1284 TYPE_PULL_REQUEST = u'pull_request'
1285
1285
1286 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1286 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1287 subject = Column('subject', Unicode(512), nullable=True)
1287 subject = Column('subject', Unicode(512), nullable=True)
1288 body = Column('body', Unicode(50000), nullable=True)
1288 body = Column('body', Unicode(50000), nullable=True)
1289 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1289 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1290 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1290 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1291 type_ = Column('type', Unicode(256))
1291 type_ = Column('type', Unicode(256))
1292
1292
1293 created_by_user = relationship('User')
1293 created_by_user = relationship('User')
1294 notifications_to_users = relationship('UserNotification', lazy='joined',
1294 notifications_to_users = relationship('UserNotification', lazy='joined',
1295 cascade="all, delete, delete-orphan")
1295 cascade="all, delete, delete-orphan")
1296
1296
1297 @property
1297 @property
1298 def recipients(self):
1298 def recipients(self):
1299 return [x.user for x in UserNotification.query()\
1299 return [x.user for x in UserNotification.query()\
1300 .filter(UserNotification.notification == self).all()]
1300 .filter(UserNotification.notification == self)\
1301 .order_by(UserNotification.user).all()]
1301
1302
1302 @classmethod
1303 @classmethod
1303 def create(cls, created_by, subject, body, recipients, type_=None):
1304 def create(cls, created_by, subject, body, recipients, type_=None):
1304 if type_ is None:
1305 if type_ is None:
1305 type_ = Notification.TYPE_MESSAGE
1306 type_ = Notification.TYPE_MESSAGE
1306
1307
1307 notification = cls()
1308 notification = cls()
1308 notification.created_by_user = created_by
1309 notification.created_by_user = created_by
1309 notification.subject = subject
1310 notification.subject = subject
1310 notification.body = body
1311 notification.body = body
1311 notification.type_ = type_
1312 notification.type_ = type_
1312 notification.created_on = datetime.datetime.now()
1313 notification.created_on = datetime.datetime.now()
1313
1314
1314 for u in recipients:
1315 for u in recipients:
1315 assoc = UserNotification()
1316 assoc = UserNotification()
1316 assoc.notification = notification
1317 assoc.notification = notification
1317 u.notifications.append(assoc)
1318 u.notifications.append(assoc)
1318 Session.add(notification)
1319 Session.add(notification)
1319 return notification
1320 return notification
1320
1321
1321 @property
1322 @property
1322 def description(self):
1323 def description(self):
1323 from rhodecode.model.notification import NotificationModel
1324 from rhodecode.model.notification import NotificationModel
1324 return NotificationModel().make_description(self)
1325 return NotificationModel().make_description(self)
1325
1326
1326
1327
1327 class UserNotification(Base, BaseModel):
1328 class UserNotification(Base, BaseModel):
1328 __tablename__ = 'user_to_notification'
1329 __tablename__ = 'user_to_notification'
1329 __table_args__ = (
1330 __table_args__ = (
1330 UniqueConstraint('user_id', 'notification_id'),
1331 UniqueConstraint('user_id', 'notification_id'),
1331 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1332 'mysql_charset': 'utf8'}
1333 'mysql_charset': 'utf8'}
1333 )
1334 )
1334 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1335 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1335 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1336 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1336 read = Column('read', Boolean, default=False)
1337 read = Column('read', Boolean, default=False)
1337 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1338 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1338
1339
1339 user = relationship('User', lazy="joined")
1340 user = relationship('User', lazy="joined")
1340 notification = relationship('Notification', lazy="joined",
1341 notification = relationship('Notification', lazy="joined",
1341 order_by=lambda: Notification.created_on.desc(),)
1342 order_by=lambda: Notification.created_on.desc(),)
1342
1343
1343 def mark_as_read(self):
1344 def mark_as_read(self):
1344 self.read = True
1345 self.read = True
1345 Session.add(self)
1346 Session.add(self)
1346
1347
1347
1348
1348 class DbMigrateVersion(Base, BaseModel):
1349 class DbMigrateVersion(Base, BaseModel):
1349 __tablename__ = 'db_migrate_version'
1350 __tablename__ = 'db_migrate_version'
1350 __table_args__ = (
1351 __table_args__ = (
1351 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1352 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1352 'mysql_charset': 'utf8'},
1353 'mysql_charset': 'utf8'},
1353 )
1354 )
1354 repository_id = Column('repository_id', String(250), primary_key=True)
1355 repository_id = Column('repository_id', String(250), primary_key=True)
1355 repository_path = Column('repository_path', Text)
1356 repository_path = Column('repository_path', Text)
1356 version = Column('version', Integer)
1357 version = Column('version', Integer)
@@ -1,592 +1,589 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.user
3 rhodecode.model.user
4 ~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~
5
5
6 users model for RhodeCode
6 users model for RhodeCode
7
7
8 :created_on: Apr 9, 2010
8 :created_on: Apr 9, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from pylons import url
29 from pylons import url
30 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
31
31
32 from rhodecode.lib.utils2 import safe_unicode, generate_api_key
32 from rhodecode.lib.utils2 import safe_unicode, generate_api_key
33 from rhodecode.lib.caching_query import FromCache
33 from rhodecode.lib.caching_query import FromCache
34
34
35 from rhodecode.model import BaseModel
35 from rhodecode.model import BaseModel
36 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
36 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
37 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
37 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
38 Notification, RepoGroup, UserRepoGroupToPerm, UsersGroup,\
38 Notification, RepoGroup, UserRepoGroupToPerm, UsersGroup,\
39 UsersGroupRepoGroupToPerm
39 UsersGroupRepoGroupToPerm
40 from rhodecode.lib.exceptions import DefaultUserException, \
40 from rhodecode.lib.exceptions import DefaultUserException, \
41 UserOwnsReposException
41 UserOwnsReposException
42
42
43 from sqlalchemy.exc import DatabaseError
43 from sqlalchemy.exc import DatabaseError
44
44
45 from sqlalchemy.orm import joinedload
45 from sqlalchemy.orm import joinedload
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 PERM_WEIGHTS = {
50 PERM_WEIGHTS = {
51 'repository.none': 0,
51 'repository.none': 0,
52 'repository.read': 1,
52 'repository.read': 1,
53 'repository.write': 3,
53 'repository.write': 3,
54 'repository.admin': 4,
54 'repository.admin': 4,
55 'group.none': 0,
55 'group.none': 0,
56 'group.read': 1,
56 'group.read': 1,
57 'group.write': 3,
57 'group.write': 3,
58 'group.admin': 4,
58 'group.admin': 4,
59 }
59 }
60
60
61
61
62 class UserModel(BaseModel):
62 class UserModel(BaseModel):
63
63
64 def __get_user(self, user):
64 def __get_user(self, user):
65 return self._get_instance(User, user, callback=User.get_by_username)
65 return self._get_instance(User, user, callback=User.get_by_username)
66
66
67 def __get_perm(self, permission):
67 def __get_perm(self, permission):
68 return self._get_instance(Permission, permission,
68 return self._get_instance(Permission, permission,
69 callback=Permission.get_by_key)
69 callback=Permission.get_by_key)
70
70
71 def get(self, user_id, cache=False):
71 def get(self, user_id, cache=False):
72 user = self.sa.query(User)
72 user = self.sa.query(User)
73 if cache:
73 if cache:
74 user = user.options(FromCache("sql_cache_short",
74 user = user.options(FromCache("sql_cache_short",
75 "get_user_%s" % user_id))
75 "get_user_%s" % user_id))
76 return user.get(user_id)
76 return user.get(user_id)
77
77
78 def get_user(self, user):
78 def get_user(self, user):
79 return self.__get_user(user)
79 return self.__get_user(user)
80
80
81 def get_by_username(self, username, cache=False, case_insensitive=False):
81 def get_by_username(self, username, cache=False, case_insensitive=False):
82
82
83 if case_insensitive:
83 if case_insensitive:
84 user = self.sa.query(User).filter(User.username.ilike(username))
84 user = self.sa.query(User).filter(User.username.ilike(username))
85 else:
85 else:
86 user = self.sa.query(User)\
86 user = self.sa.query(User)\
87 .filter(User.username == username)
87 .filter(User.username == username)
88 if cache:
88 if cache:
89 user = user.options(FromCache("sql_cache_short",
89 user = user.options(FromCache("sql_cache_short",
90 "get_user_%s" % username))
90 "get_user_%s" % username))
91 return user.scalar()
91 return user.scalar()
92
92
93 def get_by_api_key(self, api_key, cache=False):
93 def get_by_api_key(self, api_key, cache=False):
94 return User.get_by_api_key(api_key, cache)
94 return User.get_by_api_key(api_key, cache)
95
95
96 def create(self, form_data):
96 def create(self, form_data):
97 try:
97 try:
98 new_user = User()
98 new_user = User()
99 for k, v in form_data.items():
99 for k, v in form_data.items():
100 setattr(new_user, k, v)
100 setattr(new_user, k, v)
101
101
102 new_user.api_key = generate_api_key(form_data['username'])
102 new_user.api_key = generate_api_key(form_data['username'])
103 self.sa.add(new_user)
103 self.sa.add(new_user)
104 return new_user
104 return new_user
105 except:
105 except:
106 log.error(traceback.format_exc())
106 log.error(traceback.format_exc())
107 raise
107 raise
108
108
109 def create_or_update(self, username, password, email, name, lastname,
109 def create_or_update(self, username, password, email, name, lastname,
110 active=True, admin=False, ldap_dn=None):
110 active=True, admin=False, ldap_dn=None):
111 """
111 """
112 Creates a new instance if not found, or updates current one
112 Creates a new instance if not found, or updates current one
113
113
114 :param username:
114 :param username:
115 :param password:
115 :param password:
116 :param email:
116 :param email:
117 :param active:
117 :param active:
118 :param name:
118 :param name:
119 :param lastname:
119 :param lastname:
120 :param active:
120 :param active:
121 :param admin:
121 :param admin:
122 :param ldap_dn:
122 :param ldap_dn:
123 """
123 """
124
124
125 from rhodecode.lib.auth import get_crypt_password
125 from rhodecode.lib.auth import get_crypt_password
126
126
127 log.debug('Checking for %s account in RhodeCode database' % username)
127 log.debug('Checking for %s account in RhodeCode database' % username)
128 user = User.get_by_username(username, case_insensitive=True)
128 user = User.get_by_username(username, case_insensitive=True)
129 if user is None:
129 if user is None:
130 log.debug('creating new user %s' % username)
130 log.debug('creating new user %s' % username)
131 new_user = User()
131 new_user = User()
132 else:
132 else:
133 log.debug('updating user %s' % username)
133 log.debug('updating user %s' % username)
134 new_user = user
134 new_user = user
135
135
136 try:
136 try:
137 new_user.username = username
137 new_user.username = username
138 new_user.admin = admin
138 new_user.admin = admin
139 new_user.password = get_crypt_password(password)
139 new_user.password = get_crypt_password(password)
140 new_user.api_key = generate_api_key(username)
140 new_user.api_key = generate_api_key(username)
141 new_user.email = email
141 new_user.email = email
142 new_user.active = active
142 new_user.active = active
143 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
143 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
144 new_user.name = name
144 new_user.name = name
145 new_user.lastname = lastname
145 new_user.lastname = lastname
146 self.sa.add(new_user)
146 self.sa.add(new_user)
147 return new_user
147 return new_user
148 except (DatabaseError,):
148 except (DatabaseError,):
149 log.error(traceback.format_exc())
149 log.error(traceback.format_exc())
150 raise
150 raise
151
151
152 def create_for_container_auth(self, username, attrs):
152 def create_for_container_auth(self, username, attrs):
153 """
153 """
154 Creates the given user if it's not already in the database
154 Creates the given user if it's not already in the database
155
155
156 :param username:
156 :param username:
157 :param attrs:
157 :param attrs:
158 """
158 """
159 if self.get_by_username(username, case_insensitive=True) is None:
159 if self.get_by_username(username, case_insensitive=True) is None:
160
160
161 # autogenerate email for container account without one
161 # autogenerate email for container account without one
162 generate_email = lambda usr: '%s@container_auth.account' % usr
162 generate_email = lambda usr: '%s@container_auth.account' % usr
163
163
164 try:
164 try:
165 new_user = User()
165 new_user = User()
166 new_user.username = username
166 new_user.username = username
167 new_user.password = None
167 new_user.password = None
168 new_user.api_key = generate_api_key(username)
168 new_user.api_key = generate_api_key(username)
169 new_user.email = attrs['email']
169 new_user.email = attrs['email']
170 new_user.active = attrs.get('active', True)
170 new_user.active = attrs.get('active', True)
171 new_user.name = attrs['name'] or generate_email(username)
171 new_user.name = attrs['name'] or generate_email(username)
172 new_user.lastname = attrs['lastname']
172 new_user.lastname = attrs['lastname']
173
173
174 self.sa.add(new_user)
174 self.sa.add(new_user)
175 return new_user
175 return new_user
176 except (DatabaseError,):
176 except (DatabaseError,):
177 log.error(traceback.format_exc())
177 log.error(traceback.format_exc())
178 self.sa.rollback()
178 self.sa.rollback()
179 raise
179 raise
180 log.debug('User %s already exists. Skipping creation of account'
180 log.debug('User %s already exists. Skipping creation of account'
181 ' for container auth.', username)
181 ' for container auth.', username)
182 return None
182 return None
183
183
184 def create_ldap(self, username, password, user_dn, attrs):
184 def create_ldap(self, username, password, user_dn, attrs):
185 """
185 """
186 Checks if user is in database, if not creates this user marked
186 Checks if user is in database, if not creates this user marked
187 as ldap user
187 as ldap user
188
188
189 :param username:
189 :param username:
190 :param password:
190 :param password:
191 :param user_dn:
191 :param user_dn:
192 :param attrs:
192 :param attrs:
193 """
193 """
194 from rhodecode.lib.auth import get_crypt_password
194 from rhodecode.lib.auth import get_crypt_password
195 log.debug('Checking for such ldap account in RhodeCode database')
195 log.debug('Checking for such ldap account in RhodeCode database')
196 if self.get_by_username(username, case_insensitive=True) is None:
196 if self.get_by_username(username, case_insensitive=True) is None:
197
197
198 # autogenerate email for ldap account without one
198 # autogenerate email for ldap account without one
199 generate_email = lambda usr: '%s@ldap.account' % usr
199 generate_email = lambda usr: '%s@ldap.account' % usr
200
200
201 try:
201 try:
202 new_user = User()
202 new_user = User()
203 username = username.lower()
203 username = username.lower()
204 # add ldap account always lowercase
204 # add ldap account always lowercase
205 new_user.username = username
205 new_user.username = username
206 new_user.password = get_crypt_password(password)
206 new_user.password = get_crypt_password(password)
207 new_user.api_key = generate_api_key(username)
207 new_user.api_key = generate_api_key(username)
208 new_user.email = attrs['email'] or generate_email(username)
208 new_user.email = attrs['email'] or generate_email(username)
209 new_user.active = attrs.get('active', True)
209 new_user.active = attrs.get('active', True)
210 new_user.ldap_dn = safe_unicode(user_dn)
210 new_user.ldap_dn = safe_unicode(user_dn)
211 new_user.name = attrs['name']
211 new_user.name = attrs['name']
212 new_user.lastname = attrs['lastname']
212 new_user.lastname = attrs['lastname']
213
213
214 self.sa.add(new_user)
214 self.sa.add(new_user)
215 return new_user
215 return new_user
216 except (DatabaseError,):
216 except (DatabaseError,):
217 log.error(traceback.format_exc())
217 log.error(traceback.format_exc())
218 self.sa.rollback()
218 self.sa.rollback()
219 raise
219 raise
220 log.debug('this %s user exists skipping creation of ldap account',
220 log.debug('this %s user exists skipping creation of ldap account',
221 username)
221 username)
222 return None
222 return None
223
223
224 def create_registration(self, form_data):
224 def create_registration(self, form_data):
225 from rhodecode.model.notification import NotificationModel
225 from rhodecode.model.notification import NotificationModel
226
226
227 try:
227 try:
228 new_user = User()
228 form_data['admin'] = False
229 for k, v in form_data.items():
229 new_user = self.create(form_data)
230 if k != 'admin':
231 setattr(new_user, k, v)
232
230
233 self.sa.add(new_user)
231 self.sa.add(new_user)
234 self.sa.flush()
232 self.sa.flush()
235
233
236 # notification to admins
234 # notification to admins
237 subject = _('new user registration')
235 subject = _('new user registration')
238 body = ('New user registration\n'
236 body = ('New user registration\n'
239 '---------------------\n'
237 '---------------------\n'
240 '- Username: %s\n'
238 '- Username: %s\n'
241 '- Full Name: %s\n'
239 '- Full Name: %s\n'
242 '- Email: %s\n')
240 '- Email: %s\n')
243 body = body % (new_user.username, new_user.full_name,
241 body = body % (new_user.username, new_user.full_name,
244 new_user.email)
242 new_user.email)
245 edit_url = url('edit_user', id=new_user.user_id, qualified=True)
243 edit_url = url('edit_user', id=new_user.user_id, qualified=True)
246 kw = {'registered_user_url': edit_url}
244 kw = {'registered_user_url': edit_url}
247 NotificationModel().create(created_by=new_user, subject=subject,
245 NotificationModel().create(created_by=new_user, subject=subject,
248 body=body, recipients=None,
246 body=body, recipients=None,
249 type_=Notification.TYPE_REGISTRATION,
247 type_=Notification.TYPE_REGISTRATION,
250 email_kwargs=kw)
248 email_kwargs=kw)
251
249
252 except:
250 except:
253 log.error(traceback.format_exc())
251 log.error(traceback.format_exc())
254 raise
252 raise
255
253
256 def update(self, user_id, form_data):
254 def update(self, user_id, form_data):
257 try:
255 try:
258 user = self.get(user_id, cache=False)
256 user = self.get(user_id, cache=False)
259 if user.username == 'default':
257 if user.username == 'default':
260 raise DefaultUserException(
258 raise DefaultUserException(
261 _("You can't Edit this user since it's"
259 _("You can't Edit this user since it's"
262 " crucial for entire application"))
260 " crucial for entire application"))
263
261
264 for k, v in form_data.items():
262 for k, v in form_data.items():
265 if k == 'new_password' and v != '':
263 if k == 'new_password' and v != '':
266 user.password = v
264 user.password = v
267 user.api_key = generate_api_key(user.username)
265 user.api_key = generate_api_key(user.username)
268 else:
266 else:
269 setattr(user, k, v)
267 setattr(user, k, v)
270
268
271 self.sa.add(user)
269 self.sa.add(user)
272 except:
270 except:
273 log.error(traceback.format_exc())
271 log.error(traceback.format_exc())
274 raise
272 raise
275
273
276 def update_my_account(self, user_id, form_data):
274 def update_my_account(self, user_id, form_data):
277 try:
275 try:
278 user = self.get(user_id, cache=False)
276 user = self.get(user_id, cache=False)
279 if user.username == 'default':
277 if user.username == 'default':
280 raise DefaultUserException(
278 raise DefaultUserException(
281 _("You can't Edit this user since it's"
279 _("You can't Edit this user since it's"
282 " crucial for entire application"))
280 " crucial for entire application"))
283 for k, v in form_data.items():
281 for k, v in form_data.items():
284 if k == 'new_password' and v != '':
282 if k == 'new_password' and v != '':
285 user.password = v
283 user.password = v
286 user.api_key = generate_api_key(user.username)
284 user.api_key = generate_api_key(user.username)
287 else:
285 else:
288 if k not in ['admin', 'active']:
286 if k not in ['admin', 'active']:
289 setattr(user, k, v)
287 setattr(user, k, v)
290
288
291 self.sa.add(user)
289 self.sa.add(user)
292 except:
290 except:
293 log.error(traceback.format_exc())
291 log.error(traceback.format_exc())
294 raise
292 raise
295
293
296 def delete(self, user):
294 def delete(self, user):
297 user = self.__get_user(user)
295 user = self.__get_user(user)
298
296
299 try:
297 try:
300 if user.username == 'default':
298 if user.username == 'default':
301 raise DefaultUserException(
299 raise DefaultUserException(
302 _(u"You can't remove this user since it's"
300 _(u"You can't remove this user since it's"
303 " crucial for entire application")
301 " crucial for entire application")
304 )
302 )
305 if user.repositories:
303 if user.repositories:
306 repos = [x.repo_name for x in user.repositories]
304 repos = [x.repo_name for x in user.repositories]
307 raise UserOwnsReposException(
305 raise UserOwnsReposException(
308 _(u'user "%s" still owns %s repositories and cannot be '
306 _(u'user "%s" still owns %s repositories and cannot be '
309 'removed. Switch owners or remove those repositories. %s')
307 'removed. Switch owners or remove those repositories. %s')
310 % (user.username, len(repos), ', '.join(repos))
308 % (user.username, len(repos), ', '.join(repos))
311 )
309 )
312 self.sa.delete(user)
310 self.sa.delete(user)
313 except:
311 except:
314 log.error(traceback.format_exc())
312 log.error(traceback.format_exc())
315 raise
313 raise
316
314
317 def reset_password_link(self, data):
315 def reset_password_link(self, data):
318 from rhodecode.lib.celerylib import tasks, run_task
316 from rhodecode.lib.celerylib import tasks, run_task
319 run_task(tasks.send_password_link, data['email'])
317 run_task(tasks.send_password_link, data['email'])
320
318
321 def reset_password(self, data):
319 def reset_password(self, data):
322 from rhodecode.lib.celerylib import tasks, run_task
320 from rhodecode.lib.celerylib import tasks, run_task
323 run_task(tasks.reset_user_password, data['email'])
321 run_task(tasks.reset_user_password, data['email'])
324
322
325 def fill_data(self, auth_user, user_id=None, api_key=None):
323 def fill_data(self, auth_user, user_id=None, api_key=None):
326 """
324 """
327 Fetches auth_user by user_id,or api_key if present.
325 Fetches auth_user by user_id,or api_key if present.
328 Fills auth_user attributes with those taken from database.
326 Fills auth_user attributes with those taken from database.
329 Additionally set's is_authenitated if lookup fails
327 Additionally set's is_authenitated if lookup fails
330 present in database
328 present in database
331
329
332 :param auth_user: instance of user to set attributes
330 :param auth_user: instance of user to set attributes
333 :param user_id: user id to fetch by
331 :param user_id: user id to fetch by
334 :param api_key: api key to fetch by
332 :param api_key: api key to fetch by
335 """
333 """
336 if user_id is None and api_key is None:
334 if user_id is None and api_key is None:
337 raise Exception('You need to pass user_id or api_key')
335 raise Exception('You need to pass user_id or api_key')
338
336
339 try:
337 try:
340 if api_key:
338 if api_key:
341 dbuser = self.get_by_api_key(api_key)
339 dbuser = self.get_by_api_key(api_key)
342 else:
340 else:
343 dbuser = self.get(user_id)
341 dbuser = self.get(user_id)
344
342
345 if dbuser is not None and dbuser.active:
343 if dbuser is not None and dbuser.active:
346 log.debug('filling %s data' % dbuser)
344 log.debug('filling %s data' % dbuser)
347 for k, v in dbuser.get_dict().items():
345 for k, v in dbuser.get_dict().items():
348 setattr(auth_user, k, v)
346 setattr(auth_user, k, v)
349 else:
347 else:
350 return False
348 return False
351
349
352 except:
350 except:
353 log.error(traceback.format_exc())
351 log.error(traceback.format_exc())
354 auth_user.is_authenticated = False
352 auth_user.is_authenticated = False
355 return False
353 return False
356
354
357 return True
355 return True
358
356
359 def fill_perms(self, user):
357 def fill_perms(self, user):
360 """
358 """
361 Fills user permission attribute with permissions taken from database
359 Fills user permission attribute with permissions taken from database
362 works for permissions given for repositories, and for permissions that
360 works for permissions given for repositories, and for permissions that
363 are granted to groups
361 are granted to groups
364
362
365 :param user: user instance to fill his perms
363 :param user: user instance to fill his perms
366 """
364 """
367 RK = 'repositories'
365 RK = 'repositories'
368 GK = 'repositories_groups'
366 GK = 'repositories_groups'
369 GLOBAL = 'global'
367 GLOBAL = 'global'
370 user.permissions[RK] = {}
368 user.permissions[RK] = {}
371 user.permissions[GK] = {}
369 user.permissions[GK] = {}
372 user.permissions[GLOBAL] = set()
370 user.permissions[GLOBAL] = set()
373
371
374 #======================================================================
372 #======================================================================
375 # fetch default permissions
373 # fetch default permissions
376 #======================================================================
374 #======================================================================
377 default_user = User.get_by_username('default', cache=True)
375 default_user = User.get_by_username('default', cache=True)
378 default_user_id = default_user.user_id
376 default_user_id = default_user.user_id
379
377
380 default_repo_perms = Permission.get_default_perms(default_user_id)
378 default_repo_perms = Permission.get_default_perms(default_user_id)
381 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
379 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
382
380
383 if user.is_admin:
381 if user.is_admin:
384 #==================================================================
382 #==================================================================
385 # admin user have all default rights for repositories
383 # admin user have all default rights for repositories
386 # and groups set to admin
384 # and groups set to admin
387 #==================================================================
385 #==================================================================
388 user.permissions[GLOBAL].add('hg.admin')
386 user.permissions[GLOBAL].add('hg.admin')
389
387
390 # repositories
388 # repositories
391 for perm in default_repo_perms:
389 for perm in default_repo_perms:
392 r_k = perm.UserRepoToPerm.repository.repo_name
390 r_k = perm.UserRepoToPerm.repository.repo_name
393 p = 'repository.admin'
391 p = 'repository.admin'
394 user.permissions[RK][r_k] = p
392 user.permissions[RK][r_k] = p
395
393
396 # repositories groups
394 # repositories groups
397 for perm in default_repo_groups_perms:
395 for perm in default_repo_groups_perms:
398 rg_k = perm.UserRepoGroupToPerm.group.group_name
396 rg_k = perm.UserRepoGroupToPerm.group.group_name
399 p = 'group.admin'
397 p = 'group.admin'
400 user.permissions[GK][rg_k] = p
398 user.permissions[GK][rg_k] = p
401 return user
399 return user
402
400
403 #==================================================================
401 #==================================================================
404 # set default permissions first for repositories and groups
402 # set default permissions first for repositories and groups
405 #==================================================================
403 #==================================================================
406 uid = user.user_id
404 uid = user.user_id
407
405
408 # default global permissions
406 # default global permissions
409 default_global_perms = self.sa.query(UserToPerm)\
407 default_global_perms = self.sa.query(UserToPerm)\
410 .filter(UserToPerm.user_id == default_user_id)
408 .filter(UserToPerm.user_id == default_user_id)
411
409
412 for perm in default_global_perms:
410 for perm in default_global_perms:
413 user.permissions[GLOBAL].add(perm.permission.permission_name)
411 user.permissions[GLOBAL].add(perm.permission.permission_name)
414
412
415 # defaults for repositories, taken from default user
413 # defaults for repositories, taken from default user
416 for perm in default_repo_perms:
414 for perm in default_repo_perms:
417 r_k = perm.UserRepoToPerm.repository.repo_name
415 r_k = perm.UserRepoToPerm.repository.repo_name
418 if perm.Repository.private and not (perm.Repository.user_id == uid):
416 if perm.Repository.private and not (perm.Repository.user_id == uid):
419 # disable defaults for private repos,
417 # disable defaults for private repos,
420 p = 'repository.none'
418 p = 'repository.none'
421 elif perm.Repository.user_id == uid:
419 elif perm.Repository.user_id == uid:
422 # set admin if owner
420 # set admin if owner
423 p = 'repository.admin'
421 p = 'repository.admin'
424 else:
422 else:
425 p = perm.Permission.permission_name
423 p = perm.Permission.permission_name
426
424
427 user.permissions[RK][r_k] = p
425 user.permissions[RK][r_k] = p
428
426
429 # defaults for repositories groups taken from default user permission
427 # defaults for repositories groups taken from default user permission
430 # on given group
428 # on given group
431 for perm in default_repo_groups_perms:
429 for perm in default_repo_groups_perms:
432 rg_k = perm.UserRepoGroupToPerm.group.group_name
430 rg_k = perm.UserRepoGroupToPerm.group.group_name
433 p = perm.Permission.permission_name
431 p = perm.Permission.permission_name
434 user.permissions[GK][rg_k] = p
432 user.permissions[GK][rg_k] = p
435
433
436 #==================================================================
434 #==================================================================
437 # overwrite defaults with user permissions if any found
435 # overwrite defaults with user permissions if any found
438 #==================================================================
436 #==================================================================
439
437
440 # user global permissions
438 # user global permissions
441 user_perms = self.sa.query(UserToPerm)\
439 user_perms = self.sa.query(UserToPerm)\
442 .options(joinedload(UserToPerm.permission))\
440 .options(joinedload(UserToPerm.permission))\
443 .filter(UserToPerm.user_id == uid).all()
441 .filter(UserToPerm.user_id == uid).all()
444
442
445 for perm in user_perms:
443 for perm in user_perms:
446 user.permissions[GLOBAL].add(perm.permission.permission_name)
444 user.permissions[GLOBAL].add(perm.permission.permission_name)
447
445
448 # user explicit permissions for repositories
446 # user explicit permissions for repositories
449 user_repo_perms = \
447 user_repo_perms = \
450 self.sa.query(UserRepoToPerm, Permission, Repository)\
448 self.sa.query(UserRepoToPerm, Permission, Repository)\
451 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
449 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
452 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
450 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
453 .filter(UserRepoToPerm.user_id == uid)\
451 .filter(UserRepoToPerm.user_id == uid)\
454 .all()
452 .all()
455
453
456 for perm in user_repo_perms:
454 for perm in user_repo_perms:
457 # set admin if owner
455 # set admin if owner
458 r_k = perm.UserRepoToPerm.repository.repo_name
456 r_k = perm.UserRepoToPerm.repository.repo_name
459 if perm.Repository.user_id == uid:
457 if perm.Repository.user_id == uid:
460 p = 'repository.admin'
458 p = 'repository.admin'
461 else:
459 else:
462 p = perm.Permission.permission_name
460 p = perm.Permission.permission_name
463 user.permissions[RK][r_k] = p
461 user.permissions[RK][r_k] = p
464
462
465 # USER GROUP
463 # USER GROUP
466 #==================================================================
464 #==================================================================
467 # check if user is part of user groups for this repository and
465 # check if user is part of user groups for this repository and
468 # fill in (or replace with higher) permissions
466 # fill in (or replace with higher) permissions
469 #==================================================================
467 #==================================================================
470
468
471 # users group global
469 # users group global
472 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
470 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
473 .options(joinedload(UsersGroupToPerm.permission))\
471 .options(joinedload(UsersGroupToPerm.permission))\
474 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
472 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
475 UsersGroupMember.users_group_id))\
473 UsersGroupMember.users_group_id))\
476 .filter(UsersGroupMember.user_id == uid).all()
474 .filter(UsersGroupMember.user_id == uid).all()
477
475
478 for perm in user_perms_from_users_groups:
476 for perm in user_perms_from_users_groups:
479 user.permissions[GLOBAL].add(perm.permission.permission_name)
477 user.permissions[GLOBAL].add(perm.permission.permission_name)
480
478
481 # users group for repositories permissions
479 # users group for repositories permissions
482 user_repo_perms_from_users_groups = \
480 user_repo_perms_from_users_groups = \
483 self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
481 self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
484 .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\
482 .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\
485 .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\
483 .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\
486 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\
484 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\
487 .filter(UsersGroupMember.user_id == uid)\
485 .filter(UsersGroupMember.user_id == uid)\
488 .all()
486 .all()
489
487
490 for perm in user_repo_perms_from_users_groups:
488 for perm in user_repo_perms_from_users_groups:
491 r_k = perm.UsersGroupRepoToPerm.repository.repo_name
489 r_k = perm.UsersGroupRepoToPerm.repository.repo_name
492 p = perm.Permission.permission_name
490 p = perm.Permission.permission_name
493 cur_perm = user.permissions[RK][r_k]
491 cur_perm = user.permissions[RK][r_k]
494 # overwrite permission only if it's greater than permission
492 # overwrite permission only if it's greater than permission
495 # given from other sources
493 # given from other sources
496 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
494 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
497 user.permissions[RK][r_k] = p
495 user.permissions[RK][r_k] = p
498
496
499 # REPO GROUP
497 # REPO GROUP
500 #==================================================================
498 #==================================================================
501 # get access for this user for repos group and override defaults
499 # get access for this user for repos group and override defaults
502 #==================================================================
500 #==================================================================
503
501
504 # user explicit permissions for repository
502 # user explicit permissions for repository
505 user_repo_groups_perms = \
503 user_repo_groups_perms = \
506 self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
504 self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
507 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
505 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
508 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
506 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
509 .filter(UserRepoGroupToPerm.user_id == uid)\
507 .filter(UserRepoGroupToPerm.user_id == uid)\
510 .all()
508 .all()
511
509
512 for perm in user_repo_groups_perms:
510 for perm in user_repo_groups_perms:
513 rg_k = perm.UserRepoGroupToPerm.group.group_name
511 rg_k = perm.UserRepoGroupToPerm.group.group_name
514 p = perm.Permission.permission_name
512 p = perm.Permission.permission_name
515 cur_perm = user.permissions[GK][rg_k]
513 cur_perm = user.permissions[GK][rg_k]
516 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
514 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
517 user.permissions[GK][rg_k] = p
515 user.permissions[GK][rg_k] = p
518
516
519 # REPO GROUP + USER GROUP
517 # REPO GROUP + USER GROUP
520 #==================================================================
518 #==================================================================
521 # check if user is part of user groups for this repo group and
519 # check if user is part of user groups for this repo group and
522 # fill in (or replace with higher) permissions
520 # fill in (or replace with higher) permissions
523 #==================================================================
521 #==================================================================
524
522
525 # users group for repositories permissions
523 # users group for repositories permissions
526 user_repo_group_perms_from_users_groups = \
524 user_repo_group_perms_from_users_groups = \
527 self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\
525 self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\
528 .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
526 .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
529 .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\
527 .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\
530 .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\
528 .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\
531 .filter(UsersGroupMember.user_id == uid)\
529 .filter(UsersGroupMember.user_id == uid)\
532 .all()
530 .all()
533
531
534 for perm in user_repo_group_perms_from_users_groups:
532 for perm in user_repo_group_perms_from_users_groups:
535 g_k = perm.UsersGroupRepoGroupToPerm.group.group_name
533 g_k = perm.UsersGroupRepoGroupToPerm.group.group_name
536 print perm, g_k
537 p = perm.Permission.permission_name
534 p = perm.Permission.permission_name
538 cur_perm = user.permissions[GK][g_k]
535 cur_perm = user.permissions[GK][g_k]
539 # overwrite permission only if it's greater than permission
536 # overwrite permission only if it's greater than permission
540 # given from other sources
537 # given from other sources
541 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
538 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
542 user.permissions[GK][g_k] = p
539 user.permissions[GK][g_k] = p
543
540
544 return user
541 return user
545
542
546 def has_perm(self, user, perm):
543 def has_perm(self, user, perm):
547 if not isinstance(perm, Permission):
544 if not isinstance(perm, Permission):
548 raise Exception('perm needs to be an instance of Permission class '
545 raise Exception('perm needs to be an instance of Permission class '
549 'got %s instead' % type(perm))
546 'got %s instead' % type(perm))
550
547
551 user = self.__get_user(user)
548 user = self.__get_user(user)
552
549
553 return UserToPerm.query().filter(UserToPerm.user == user)\
550 return UserToPerm.query().filter(UserToPerm.user == user)\
554 .filter(UserToPerm.permission == perm).scalar() is not None
551 .filter(UserToPerm.permission == perm).scalar() is not None
555
552
556 def grant_perm(self, user, perm):
553 def grant_perm(self, user, perm):
557 """
554 """
558 Grant user global permissions
555 Grant user global permissions
559
556
560 :param user:
557 :param user:
561 :param perm:
558 :param perm:
562 """
559 """
563 user = self.__get_user(user)
560 user = self.__get_user(user)
564 perm = self.__get_perm(perm)
561 perm = self.__get_perm(perm)
565 # if this permission is already granted skip it
562 # if this permission is already granted skip it
566 _perm = UserToPerm.query()\
563 _perm = UserToPerm.query()\
567 .filter(UserToPerm.user == user)\
564 .filter(UserToPerm.user == user)\
568 .filter(UserToPerm.permission == perm)\
565 .filter(UserToPerm.permission == perm)\
569 .scalar()
566 .scalar()
570 if _perm:
567 if _perm:
571 return
568 return
572 new = UserToPerm()
569 new = UserToPerm()
573 new.user = user
570 new.user = user
574 new.permission = perm
571 new.permission = perm
575 self.sa.add(new)
572 self.sa.add(new)
576
573
577 def revoke_perm(self, user, perm):
574 def revoke_perm(self, user, perm):
578 """
575 """
579 Revoke users global permissions
576 Revoke users global permissions
580
577
581 :param user:
578 :param user:
582 :param perm:
579 :param perm:
583 """
580 """
584 user = self.__get_user(user)
581 user = self.__get_user(user)
585 perm = self.__get_perm(perm)
582 perm = self.__get_perm(perm)
586
583
587 obj = UserToPerm.query()\
584 obj = UserToPerm.query()\
588 .filter(UserToPerm.user == user)\
585 .filter(UserToPerm.user == user)\
589 .filter(UserToPerm.permission == perm)\
586 .filter(UserToPerm.permission == perm)\
590 .scalar()
587 .scalar()
591 if obj:
588 if obj:
592 self.sa.delete(obj)
589 self.sa.delete(obj)
@@ -1,4422 +1,4426 b''
1 html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td
1 html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td
2 {
2 {
3 border: 0;
3 border: 0;
4 outline: 0;
4 outline: 0;
5 font-size: 100%;
5 font-size: 100%;
6 vertical-align: baseline;
6 vertical-align: baseline;
7 background: transparent;
7 background: transparent;
8 margin: 0;
8 margin: 0;
9 padding: 0;
9 padding: 0;
10 }
10 }
11
11
12 body {
12 body {
13 line-height: 1;
13 line-height: 1;
14 height: 100%;
14 height: 100%;
15 background: url("../images/background.png") repeat scroll 0 0 #B0B0B0;
15 background: url("../images/background.png") repeat scroll 0 0 #B0B0B0;
16 font-family: Lucida Grande, Verdana, Lucida Sans Regular,
16 font-family: Lucida Grande, Verdana, Lucida Sans Regular,
17 Lucida Sans Unicode, Arial, sans-serif; font-size : 12px;
17 Lucida Sans Unicode, Arial, sans-serif; font-size : 12px;
18 color: #000;
18 color: #000;
19 margin: 0;
19 margin: 0;
20 padding: 0;
20 padding: 0;
21 font-size: 12px;
21 font-size: 12px;
22 }
22 }
23
23
24 ol,ul {
24 ol,ul {
25 list-style: none;
25 list-style: none;
26 }
26 }
27
27
28 blockquote,q {
28 blockquote,q {
29 quotes: none;
29 quotes: none;
30 }
30 }
31
31
32 blockquote:before,blockquote:after,q:before,q:after {
32 blockquote:before,blockquote:after,q:before,q:after {
33 content: none;
33 content: none;
34 }
34 }
35
35
36 :focus {
36 :focus {
37 outline: 0;
37 outline: 0;
38 }
38 }
39
39
40 del {
40 del {
41 text-decoration: line-through;
41 text-decoration: line-through;
42 }
42 }
43
43
44 table {
44 table {
45 border-collapse: collapse;
45 border-collapse: collapse;
46 border-spacing: 0;
46 border-spacing: 0;
47 }
47 }
48
48
49 html {
49 html {
50 height: 100%;
50 height: 100%;
51 }
51 }
52
52
53 a {
53 a {
54 color: #003367;
54 color: #003367;
55 text-decoration: none;
55 text-decoration: none;
56 cursor: pointer;
56 cursor: pointer;
57 }
57 }
58
58
59 a:hover {
59 a:hover {
60 color: #316293;
60 color: #316293;
61 text-decoration: underline;
61 text-decoration: underline;
62 }
62 }
63
63
64 h1,h2,h3,h4,h5,h6 {
64 h1,h2,h3,h4,h5,h6 {
65 color: #292929;
65 color: #292929;
66 font-weight: 700;
66 font-weight: 700;
67 }
67 }
68
68
69 h1 {
69 h1 {
70 font-size: 22px;
70 font-size: 22px;
71 }
71 }
72
72
73 h2 {
73 h2 {
74 font-size: 20px;
74 font-size: 20px;
75 }
75 }
76
76
77 h3 {
77 h3 {
78 font-size: 18px;
78 font-size: 18px;
79 }
79 }
80
80
81 h4 {
81 h4 {
82 font-size: 16px;
82 font-size: 16px;
83 }
83 }
84
84
85 h5 {
85 h5 {
86 font-size: 14px;
86 font-size: 14px;
87 }
87 }
88
88
89 h6 {
89 h6 {
90 font-size: 11px;
90 font-size: 11px;
91 }
91 }
92
92
93 ul.circle {
93 ul.circle {
94 list-style-type: circle;
94 list-style-type: circle;
95 }
95 }
96
96
97 ul.disc {
97 ul.disc {
98 list-style-type: disc;
98 list-style-type: disc;
99 }
99 }
100
100
101 ul.square {
101 ul.square {
102 list-style-type: square;
102 list-style-type: square;
103 }
103 }
104
104
105 ol.lower-roman {
105 ol.lower-roman {
106 list-style-type: lower-roman;
106 list-style-type: lower-roman;
107 }
107 }
108
108
109 ol.upper-roman {
109 ol.upper-roman {
110 list-style-type: upper-roman;
110 list-style-type: upper-roman;
111 }
111 }
112
112
113 ol.lower-alpha {
113 ol.lower-alpha {
114 list-style-type: lower-alpha;
114 list-style-type: lower-alpha;
115 }
115 }
116
116
117 ol.upper-alpha {
117 ol.upper-alpha {
118 list-style-type: upper-alpha;
118 list-style-type: upper-alpha;
119 }
119 }
120
120
121 ol.decimal {
121 ol.decimal {
122 list-style-type: decimal;
122 list-style-type: decimal;
123 }
123 }
124
124
125 div.color {
125 div.color {
126 clear: both;
126 clear: both;
127 overflow: hidden;
127 overflow: hidden;
128 position: absolute;
128 position: absolute;
129 background: #FFF;
129 background: #FFF;
130 margin: 7px 0 0 60px;
130 margin: 7px 0 0 60px;
131 padding: 1px 1px 1px 0;
131 padding: 1px 1px 1px 0;
132 }
132 }
133
133
134 div.color a {
134 div.color a {
135 width: 15px;
135 width: 15px;
136 height: 15px;
136 height: 15px;
137 display: block;
137 display: block;
138 float: left;
138 float: left;
139 margin: 0 0 0 1px;
139 margin: 0 0 0 1px;
140 padding: 0;
140 padding: 0;
141 }
141 }
142
142
143 div.options {
143 div.options {
144 clear: both;
144 clear: both;
145 overflow: hidden;
145 overflow: hidden;
146 position: absolute;
146 position: absolute;
147 background: #FFF;
147 background: #FFF;
148 margin: 7px 0 0 162px;
148 margin: 7px 0 0 162px;
149 padding: 0;
149 padding: 0;
150 }
150 }
151
151
152 div.options a {
152 div.options a {
153 height: 1%;
153 height: 1%;
154 display: block;
154 display: block;
155 text-decoration: none;
155 text-decoration: none;
156 margin: 0;
156 margin: 0;
157 padding: 3px 8px;
157 padding: 3px 8px;
158 }
158 }
159
159
160 .top-left-rounded-corner {
160 .top-left-rounded-corner {
161 -webkit-border-top-left-radius: 8px;
161 -webkit-border-top-left-radius: 8px;
162 -khtml-border-radius-topleft: 8px;
162 -khtml-border-radius-topleft: 8px;
163 -moz-border-radius-topleft: 8px;
163 -moz-border-radius-topleft: 8px;
164 border-top-left-radius: 8px;
164 border-top-left-radius: 8px;
165 }
165 }
166
166
167 .top-right-rounded-corner {
167 .top-right-rounded-corner {
168 -webkit-border-top-right-radius: 8px;
168 -webkit-border-top-right-radius: 8px;
169 -khtml-border-radius-topright: 8px;
169 -khtml-border-radius-topright: 8px;
170 -moz-border-radius-topright: 8px;
170 -moz-border-radius-topright: 8px;
171 border-top-right-radius: 8px;
171 border-top-right-radius: 8px;
172 }
172 }
173
173
174 .bottom-left-rounded-corner {
174 .bottom-left-rounded-corner {
175 -webkit-border-bottom-left-radius: 8px;
175 -webkit-border-bottom-left-radius: 8px;
176 -khtml-border-radius-bottomleft: 8px;
176 -khtml-border-radius-bottomleft: 8px;
177 -moz-border-radius-bottomleft: 8px;
177 -moz-border-radius-bottomleft: 8px;
178 border-bottom-left-radius: 8px;
178 border-bottom-left-radius: 8px;
179 }
179 }
180
180
181 .bottom-right-rounded-corner {
181 .bottom-right-rounded-corner {
182 -webkit-border-bottom-right-radius: 8px;
182 -webkit-border-bottom-right-radius: 8px;
183 -khtml-border-radius-bottomright: 8px;
183 -khtml-border-radius-bottomright: 8px;
184 -moz-border-radius-bottomright: 8px;
184 -moz-border-radius-bottomright: 8px;
185 border-bottom-right-radius: 8px;
185 border-bottom-right-radius: 8px;
186 }
186 }
187
187
188 .top-left-rounded-corner-mid {
188 .top-left-rounded-corner-mid {
189 -webkit-border-top-left-radius: 4px;
189 -webkit-border-top-left-radius: 4px;
190 -khtml-border-radius-topleft: 4px;
190 -khtml-border-radius-topleft: 4px;
191 -moz-border-radius-topleft: 4px;
191 -moz-border-radius-topleft: 4px;
192 border-top-left-radius: 4px;
192 border-top-left-radius: 4px;
193 }
193 }
194
194
195 .top-right-rounded-corner-mid {
195 .top-right-rounded-corner-mid {
196 -webkit-border-top-right-radius: 4px;
196 -webkit-border-top-right-radius: 4px;
197 -khtml-border-radius-topright: 4px;
197 -khtml-border-radius-topright: 4px;
198 -moz-border-radius-topright: 4px;
198 -moz-border-radius-topright: 4px;
199 border-top-right-radius: 4px;
199 border-top-right-radius: 4px;
200 }
200 }
201
201
202 .bottom-left-rounded-corner-mid {
202 .bottom-left-rounded-corner-mid {
203 -webkit-border-bottom-left-radius: 4px;
203 -webkit-border-bottom-left-radius: 4px;
204 -khtml-border-radius-bottomleft: 4px;
204 -khtml-border-radius-bottomleft: 4px;
205 -moz-border-radius-bottomleft: 4px;
205 -moz-border-radius-bottomleft: 4px;
206 border-bottom-left-radius: 4px;
206 border-bottom-left-radius: 4px;
207 }
207 }
208
208
209 .bottom-right-rounded-corner-mid {
209 .bottom-right-rounded-corner-mid {
210 -webkit-border-bottom-right-radius: 4px;
210 -webkit-border-bottom-right-radius: 4px;
211 -khtml-border-radius-bottomright: 4px;
211 -khtml-border-radius-bottomright: 4px;
212 -moz-border-radius-bottomright: 4px;
212 -moz-border-radius-bottomright: 4px;
213 border-bottom-right-radius: 4px;
213 border-bottom-right-radius: 4px;
214 }
214 }
215
215
216 .help-block {
216 .help-block {
217 color: #999999;
217 color: #999999;
218 display: block;
218 display: block;
219 margin-bottom: 0;
219 margin-bottom: 0;
220 margin-top: 5px;
220 margin-top: 5px;
221 }
221 }
222
222
223 #header {
223 #header {
224 margin: 0;
224 margin: 0;
225 padding: 0 10px;
225 padding: 0 10px;
226 }
226 }
227
227
228 #header ul#logged-user {
228 #header ul#logged-user {
229 margin-bottom: 5px !important;
229 margin-bottom: 5px !important;
230 -webkit-border-radius: 0px 0px 8px 8px;
230 -webkit-border-radius: 0px 0px 8px 8px;
231 -khtml-border-radius: 0px 0px 8px 8px;
231 -khtml-border-radius: 0px 0px 8px 8px;
232 -moz-border-radius: 0px 0px 8px 8px;
232 -moz-border-radius: 0px 0px 8px 8px;
233 border-radius: 0px 0px 8px 8px;
233 border-radius: 0px 0px 8px 8px;
234 height: 37px;
234 height: 37px;
235 background-color: #003B76;
235 background-color: #003B76;
236 background-repeat: repeat-x;
236 background-repeat: repeat-x;
237 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
237 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
238 background-image: -moz-linear-gradient(top, #003b76, #00376e);
238 background-image: -moz-linear-gradient(top, #003b76, #00376e);
239 background-image: -ms-linear-gradient(top, #003b76, #00376e);
239 background-image: -ms-linear-gradient(top, #003b76, #00376e);
240 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
240 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
241 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
241 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
242 background-image: -o-linear-gradient(top, #003b76, #00376e);
242 background-image: -o-linear-gradient(top, #003b76, #00376e);
243 background-image: linear-gradient(top, #003b76, #00376e);
243 background-image: linear-gradient(top, #003b76, #00376e);
244 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',endColorstr='#00376e', GradientType=0 );
244 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',endColorstr='#00376e', GradientType=0 );
245 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
245 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
246 }
246 }
247
247
248 #header ul#logged-user li {
248 #header ul#logged-user li {
249 list-style: none;
249 list-style: none;
250 float: left;
250 float: left;
251 margin: 8px 0 0;
251 margin: 8px 0 0;
252 padding: 4px 12px;
252 padding: 4px 12px;
253 border-left: 1px solid #316293;
253 border-left: 1px solid #316293;
254 }
254 }
255
255
256 #header ul#logged-user li.first {
256 #header ul#logged-user li.first {
257 border-left: none;
257 border-left: none;
258 margin: 4px;
258 margin: 4px;
259 }
259 }
260
260
261 #header ul#logged-user li.first div.gravatar {
261 #header ul#logged-user li.first div.gravatar {
262 margin-top: -2px;
262 margin-top: -2px;
263 }
263 }
264
264
265 #header ul#logged-user li.first div.account {
265 #header ul#logged-user li.first div.account {
266 padding-top: 4px;
266 padding-top: 4px;
267 float: left;
267 float: left;
268 }
268 }
269
269
270 #header ul#logged-user li.last {
270 #header ul#logged-user li.last {
271 border-right: none;
271 border-right: none;
272 }
272 }
273
273
274 #header ul#logged-user li a {
274 #header ul#logged-user li a {
275 color: #fff;
275 color: #fff;
276 font-weight: 700;
276 font-weight: 700;
277 text-decoration: none;
277 text-decoration: none;
278 }
278 }
279
279
280 #header ul#logged-user li a:hover {
280 #header ul#logged-user li a:hover {
281 text-decoration: underline;
281 text-decoration: underline;
282 }
282 }
283
283
284 #header ul#logged-user li.highlight a {
284 #header ul#logged-user li.highlight a {
285 color: #fff;
285 color: #fff;
286 }
286 }
287
287
288 #header ul#logged-user li.highlight a:hover {
288 #header ul#logged-user li.highlight a:hover {
289 color: #FFF;
289 color: #FFF;
290 }
290 }
291
291
292 #header #header-inner {
292 #header #header-inner {
293 min-height: 44px;
293 min-height: 44px;
294 clear: both;
294 clear: both;
295 position: relative;
295 position: relative;
296 background-color: #003B76;
296 background-color: #003B76;
297 background-repeat: repeat-x;
297 background-repeat: repeat-x;
298 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
298 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
299 background-image: -moz-linear-gradient(top, #003b76, #00376e);
299 background-image: -moz-linear-gradient(top, #003b76, #00376e);
300 background-image: -ms-linear-gradient(top, #003b76, #00376e);
300 background-image: -ms-linear-gradient(top, #003b76, #00376e);
301 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76),color-stop(100%, #00376e) );
301 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76),color-stop(100%, #00376e) );
302 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
302 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
303 background-image: -o-linear-gradient(top, #003b76, #00376e);
303 background-image: -o-linear-gradient(top, #003b76, #00376e);
304 background-image: linear-gradient(top, #003b76, #00376e);
304 background-image: linear-gradient(top, #003b76, #00376e);
305 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',endColorstr='#00376e', GradientType=0 );
305 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',endColorstr='#00376e', GradientType=0 );
306 margin: 0;
306 margin: 0;
307 padding: 0;
307 padding: 0;
308 display: block;
308 display: block;
309 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
309 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
310 -webkit-border-radius: 4px 4px 4px 4px;
310 -webkit-border-radius: 4px 4px 4px 4px;
311 -khtml-border-radius: 4px 4px 4px 4px;
311 -khtml-border-radius: 4px 4px 4px 4px;
312 -moz-border-radius: 4px 4px 4px 4px;
312 -moz-border-radius: 4px 4px 4px 4px;
313 border-radius: 4px 4px 4px 4px;
313 border-radius: 4px 4px 4px 4px;
314 }
314 }
315 #header #header-inner.hover{
315 #header #header-inner.hover{
316 position: fixed !important;
316 position: fixed !important;
317 width: 100% !important;
317 width: 100% !important;
318 margin-left: -10px !important;
318 margin-left: -10px !important;
319 z-index: 10000;
319 z-index: 10000;
320 -webkit-border-radius: 0px 0px 0px 0px;
320 -webkit-border-radius: 0px 0px 0px 0px;
321 -khtml-border-radius: 0px 0px 0px 0px;
321 -khtml-border-radius: 0px 0px 0px 0px;
322 -moz-border-radius: 0px 0px 0px 0px;
322 -moz-border-radius: 0px 0px 0px 0px;
323 border-radius: 0px 0px 0px 0px;
323 border-radius: 0px 0px 0px 0px;
324 }
324 }
325
325
326 .ie7 #header #header-inner.hover,
326 .ie7 #header #header-inner.hover,
327 .ie8 #header #header-inner.hover,
327 .ie8 #header #header-inner.hover,
328 .ie9 #header #header-inner.hover
328 .ie9 #header #header-inner.hover
329 {
329 {
330 z-index: auto !important;
330 z-index: auto !important;
331 }
331 }
332
332
333 #header #header-inner #home a {
333 #header #header-inner #home a {
334 height: 40px;
334 height: 40px;
335 width: 46px;
335 width: 46px;
336 display: block;
336 display: block;
337 background: url("../images/button_home.png");
337 background: url("../images/button_home.png");
338 background-position: 0 0;
338 background-position: 0 0;
339 margin: 0;
339 margin: 0;
340 padding: 0;
340 padding: 0;
341 }
341 }
342
342
343 #header #header-inner #home a:hover {
343 #header #header-inner #home a:hover {
344 background-position: 0 -40px;
344 background-position: 0 -40px;
345 }
345 }
346
346
347 #header #header-inner #logo {
347 #header #header-inner #logo {
348 float: left;
348 float: left;
349 position: absolute;
349 position: absolute;
350 }
350 }
351
351
352 #header #header-inner #logo h1 {
352 #header #header-inner #logo h1 {
353 color: #FFF;
353 color: #FFF;
354 font-size: 20px;
354 font-size: 20px;
355 margin: 12px 0 0 13px;
355 margin: 12px 0 0 13px;
356 padding: 0;
356 padding: 0;
357 }
357 }
358
358
359 #header #header-inner #logo a {
359 #header #header-inner #logo a {
360 color: #fff;
360 color: #fff;
361 text-decoration: none;
361 text-decoration: none;
362 }
362 }
363
363
364 #header #header-inner #logo a:hover {
364 #header #header-inner #logo a:hover {
365 color: #bfe3ff;
365 color: #bfe3ff;
366 }
366 }
367
367
368 #header #header-inner #quick,#header #header-inner #quick ul {
368 #header #header-inner #quick,#header #header-inner #quick ul {
369 position: relative;
369 position: relative;
370 float: right;
370 float: right;
371 list-style-type: none;
371 list-style-type: none;
372 list-style-position: outside;
372 list-style-position: outside;
373 margin: 8px 8px 0 0;
373 margin: 8px 8px 0 0;
374 padding: 0;
374 padding: 0;
375 }
375 }
376
376
377 #header #header-inner #quick li {
377 #header #header-inner #quick li {
378 position: relative;
378 position: relative;
379 float: left;
379 float: left;
380 margin: 0 5px 0 0;
380 margin: 0 5px 0 0;
381 padding: 0;
381 padding: 0;
382 }
382 }
383
383
384 #header #header-inner #quick li a.menu_link {
384 #header #header-inner #quick li a.menu_link {
385 top: 0;
385 top: 0;
386 left: 0;
386 left: 0;
387 height: 1%;
387 height: 1%;
388 display: block;
388 display: block;
389 clear: both;
389 clear: both;
390 overflow: hidden;
390 overflow: hidden;
391 color: #FFF;
391 color: #FFF;
392 font-weight: 700;
392 font-weight: 700;
393 text-decoration: none;
393 text-decoration: none;
394 background: #369;
394 background: #369;
395 padding: 0;
395 padding: 0;
396 -webkit-border-radius: 4px 4px 4px 4px;
396 -webkit-border-radius: 4px 4px 4px 4px;
397 -khtml-border-radius: 4px 4px 4px 4px;
397 -khtml-border-radius: 4px 4px 4px 4px;
398 -moz-border-radius: 4px 4px 4px 4px;
398 -moz-border-radius: 4px 4px 4px 4px;
399 border-radius: 4px 4px 4px 4px;
399 border-radius: 4px 4px 4px 4px;
400 }
400 }
401
401
402 #header #header-inner #quick li span.short {
402 #header #header-inner #quick li span.short {
403 padding: 9px 6px 8px 6px;
403 padding: 9px 6px 8px 6px;
404 }
404 }
405
405
406 #header #header-inner #quick li span {
406 #header #header-inner #quick li span {
407 top: 0;
407 top: 0;
408 right: 0;
408 right: 0;
409 height: 1%;
409 height: 1%;
410 display: block;
410 display: block;
411 float: left;
411 float: left;
412 border-left: 1px solid #3f6f9f;
412 border-left: 1px solid #3f6f9f;
413 margin: 0;
413 margin: 0;
414 padding: 10px 12px 8px 10px;
414 padding: 10px 12px 8px 10px;
415 }
415 }
416
416
417 #header #header-inner #quick li span.normal {
417 #header #header-inner #quick li span.normal {
418 border: none;
418 border: none;
419 padding: 10px 12px 8px;
419 padding: 10px 12px 8px;
420 }
420 }
421
421
422 #header #header-inner #quick li span.icon {
422 #header #header-inner #quick li span.icon {
423 top: 0;
423 top: 0;
424 left: 0;
424 left: 0;
425 border-left: none;
425 border-left: none;
426 border-right: 1px solid #2e5c89;
426 border-right: 1px solid #2e5c89;
427 padding: 8px 6px 4px;
427 padding: 8px 6px 4px;
428 }
428 }
429
429
430 #header #header-inner #quick li span.icon_short {
430 #header #header-inner #quick li span.icon_short {
431 top: 0;
431 top: 0;
432 left: 0;
432 left: 0;
433 border-left: none;
433 border-left: none;
434 border-right: 1px solid #2e5c89;
434 border-right: 1px solid #2e5c89;
435 padding: 8px 6px 4px;
435 padding: 8px 6px 4px;
436 }
436 }
437
437
438 #header #header-inner #quick li span.icon img,#header #header-inner #quick li span.icon_short img
438 #header #header-inner #quick li span.icon img,#header #header-inner #quick li span.icon_short img
439 {
439 {
440 margin: 0px -2px 0px 0px;
440 margin: 0px -2px 0px 0px;
441 }
441 }
442
442
443 #header #header-inner #quick li a:hover {
443 #header #header-inner #quick li a:hover {
444 background: #4e4e4e no-repeat top left;
444 background: #4e4e4e no-repeat top left;
445 }
445 }
446
446
447 #header #header-inner #quick li a:hover span {
447 #header #header-inner #quick li a:hover span {
448 border-left: 1px solid #545454;
448 border-left: 1px solid #545454;
449 }
449 }
450
450
451 #header #header-inner #quick li a:hover span.icon,#header #header-inner #quick li a:hover span.icon_short
451 #header #header-inner #quick li a:hover span.icon,#header #header-inner #quick li a:hover span.icon_short
452 {
452 {
453 border-left: none;
453 border-left: none;
454 border-right: 1px solid #464646;
454 border-right: 1px solid #464646;
455 }
455 }
456
456
457 #header #header-inner #quick ul {
457 #header #header-inner #quick ul {
458 top: 29px;
458 top: 29px;
459 right: 0;
459 right: 0;
460 min-width: 200px;
460 min-width: 200px;
461 display: none;
461 display: none;
462 position: absolute;
462 position: absolute;
463 background: #FFF;
463 background: #FFF;
464 border: 1px solid #666;
464 border: 1px solid #666;
465 border-top: 1px solid #003367;
465 border-top: 1px solid #003367;
466 z-index: 100;
466 z-index: 100;
467 margin: 0px 0px 0px 0px;
467 margin: 0px 0px 0px 0px;
468 padding: 0;
468 padding: 0;
469 }
469 }
470
470
471 #header #header-inner #quick ul.repo_switcher {
471 #header #header-inner #quick ul.repo_switcher {
472 max-height: 275px;
472 max-height: 275px;
473 overflow-x: hidden;
473 overflow-x: hidden;
474 overflow-y: auto;
474 overflow-y: auto;
475 }
475 }
476
476
477 #header #header-inner #quick ul.repo_switcher li.qfilter_rs {
477 #header #header-inner #quick ul.repo_switcher li.qfilter_rs {
478 float: none;
478 float: none;
479 margin: 0;
479 margin: 0;
480 border-bottom: 2px solid #003367;
480 border-bottom: 2px solid #003367;
481 }
481 }
482
482
483 #header #header-inner #quick .repo_switcher_type {
483 #header #header-inner #quick .repo_switcher_type {
484 position: absolute;
484 position: absolute;
485 left: 0;
485 left: 0;
486 top: 9px;
486 top: 9px;
487 }
487 }
488
488
489 #header #header-inner #quick li ul li {
489 #header #header-inner #quick li ul li {
490 border-bottom: 1px solid #ddd;
490 border-bottom: 1px solid #ddd;
491 }
491 }
492
492
493 #header #header-inner #quick li ul li a {
493 #header #header-inner #quick li ul li a {
494 width: 182px;
494 width: 182px;
495 height: auto;
495 height: auto;
496 display: block;
496 display: block;
497 float: left;
497 float: left;
498 background: #FFF;
498 background: #FFF;
499 color: #003367;
499 color: #003367;
500 font-weight: 400;
500 font-weight: 400;
501 margin: 0;
501 margin: 0;
502 padding: 7px 9px;
502 padding: 7px 9px;
503 }
503 }
504
504
505 #header #header-inner #quick li ul li a:hover {
505 #header #header-inner #quick li ul li a:hover {
506 color: #000;
506 color: #000;
507 background: #FFF;
507 background: #FFF;
508 }
508 }
509
509
510 #header #header-inner #quick ul ul {
510 #header #header-inner #quick ul ul {
511 top: auto;
511 top: auto;
512 }
512 }
513
513
514 #header #header-inner #quick li ul ul {
514 #header #header-inner #quick li ul ul {
515 right: 200px;
515 right: 200px;
516 max-height: 275px;
516 max-height: 275px;
517 overflow: auto;
517 overflow: auto;
518 overflow-x: hidden;
518 overflow-x: hidden;
519 white-space: normal;
519 white-space: normal;
520 }
520 }
521
521
522 #header #header-inner #quick li ul li a.journal,#header #header-inner #quick li ul li a.journal:hover
522 #header #header-inner #quick li ul li a.journal,#header #header-inner #quick li ul li a.journal:hover
523 {
523 {
524 background: url("../images/icons/book.png") no-repeat scroll 4px 9px
524 background: url("../images/icons/book.png") no-repeat scroll 4px 9px
525 #FFF;
525 #FFF;
526 width: 167px;
526 width: 167px;
527 margin: 0;
527 margin: 0;
528 padding: 12px 9px 7px 24px;
528 padding: 12px 9px 7px 24px;
529 }
529 }
530
530
531 #header #header-inner #quick li ul li a.private_repo,#header #header-inner #quick li ul li a.private_repo:hover
531 #header #header-inner #quick li ul li a.private_repo,#header #header-inner #quick li ul li a.private_repo:hover
532 {
532 {
533 background: url("../images/icons/lock.png") no-repeat scroll 4px 9px
533 background: url("../images/icons/lock.png") no-repeat scroll 4px 9px
534 #FFF;
534 #FFF;
535 min-width: 167px;
535 min-width: 167px;
536 margin: 0;
536 margin: 0;
537 padding: 12px 9px 7px 24px;
537 padding: 12px 9px 7px 24px;
538 }
538 }
539
539
540 #header #header-inner #quick li ul li a.public_repo,#header #header-inner #quick li ul li a.public_repo:hover
540 #header #header-inner #quick li ul li a.public_repo,#header #header-inner #quick li ul li a.public_repo:hover
541 {
541 {
542 background: url("../images/icons/lock_open.png") no-repeat scroll 4px
542 background: url("../images/icons/lock_open.png") no-repeat scroll 4px
543 9px #FFF;
543 9px #FFF;
544 min-width: 167px;
544 min-width: 167px;
545 margin: 0;
545 margin: 0;
546 padding: 12px 9px 7px 24px;
546 padding: 12px 9px 7px 24px;
547 }
547 }
548
548
549 #header #header-inner #quick li ul li a.hg,#header #header-inner #quick li ul li a.hg:hover
549 #header #header-inner #quick li ul li a.hg,#header #header-inner #quick li ul li a.hg:hover
550 {
550 {
551 background: url("../images/icons/hgicon.png") no-repeat scroll 4px 9px
551 background: url("../images/icons/hgicon.png") no-repeat scroll 4px 9px
552 #FFF;
552 #FFF;
553 min-width: 167px;
553 min-width: 167px;
554 margin: 0 0 0 14px;
554 margin: 0 0 0 14px;
555 padding: 12px 9px 7px 24px;
555 padding: 12px 9px 7px 24px;
556 }
556 }
557
557
558 #header #header-inner #quick li ul li a.git,#header #header-inner #quick li ul li a.git:hover
558 #header #header-inner #quick li ul li a.git,#header #header-inner #quick li ul li a.git:hover
559 {
559 {
560 background: url("../images/icons/giticon.png") no-repeat scroll 4px 9px
560 background: url("../images/icons/giticon.png") no-repeat scroll 4px 9px
561 #FFF;
561 #FFF;
562 min-width: 167px;
562 min-width: 167px;
563 margin: 0 0 0 14px;
563 margin: 0 0 0 14px;
564 padding: 12px 9px 7px 24px;
564 padding: 12px 9px 7px 24px;
565 }
565 }
566
566
567 #header #header-inner #quick li ul li a.repos,#header #header-inner #quick li ul li a.repos:hover
567 #header #header-inner #quick li ul li a.repos,#header #header-inner #quick li ul li a.repos:hover
568 {
568 {
569 background: url("../images/icons/database_edit.png") no-repeat scroll
569 background: url("../images/icons/database_edit.png") no-repeat scroll
570 4px 9px #FFF;
570 4px 9px #FFF;
571 width: 167px;
571 width: 167px;
572 margin: 0;
572 margin: 0;
573 padding: 12px 9px 7px 24px;
573 padding: 12px 9px 7px 24px;
574 }
574 }
575
575
576 #header #header-inner #quick li ul li a.repos_groups,#header #header-inner #quick li ul li a.repos_groups:hover
576 #header #header-inner #quick li ul li a.repos_groups,#header #header-inner #quick li ul li a.repos_groups:hover
577 {
577 {
578 background: url("../images/icons/database_link.png") no-repeat scroll
578 background: url("../images/icons/database_link.png") no-repeat scroll
579 4px 9px #FFF;
579 4px 9px #FFF;
580 width: 167px;
580 width: 167px;
581 margin: 0;
581 margin: 0;
582 padding: 12px 9px 7px 24px;
582 padding: 12px 9px 7px 24px;
583 }
583 }
584
584
585 #header #header-inner #quick li ul li a.users,#header #header-inner #quick li ul li a.users:hover
585 #header #header-inner #quick li ul li a.users,#header #header-inner #quick li ul li a.users:hover
586 {
586 {
587 background: #FFF url("../images/icons/user_edit.png") no-repeat 4px 9px;
587 background: #FFF url("../images/icons/user_edit.png") no-repeat 4px 9px;
588 width: 167px;
588 width: 167px;
589 margin: 0;
589 margin: 0;
590 padding: 12px 9px 7px 24px;
590 padding: 12px 9px 7px 24px;
591 }
591 }
592
592
593 #header #header-inner #quick li ul li a.groups,#header #header-inner #quick li ul li a.groups:hover
593 #header #header-inner #quick li ul li a.groups,#header #header-inner #quick li ul li a.groups:hover
594 {
594 {
595 background: #FFF url("../images/icons/group_edit.png") no-repeat 4px 9px;
595 background: #FFF url("../images/icons/group_edit.png") no-repeat 4px 9px;
596 width: 167px;
596 width: 167px;
597 margin: 0;
597 margin: 0;
598 padding: 12px 9px 7px 24px;
598 padding: 12px 9px 7px 24px;
599 }
599 }
600
600
601 #header #header-inner #quick li ul li a.settings,#header #header-inner #quick li ul li a.settings:hover
601 #header #header-inner #quick li ul li a.settings,#header #header-inner #quick li ul li a.settings:hover
602 {
602 {
603 background: #FFF url("../images/icons/cog.png") no-repeat 4px 9px;
603 background: #FFF url("../images/icons/cog.png") no-repeat 4px 9px;
604 width: 167px;
604 width: 167px;
605 margin: 0;
605 margin: 0;
606 padding: 12px 9px 7px 24px;
606 padding: 12px 9px 7px 24px;
607 }
607 }
608
608
609 #header #header-inner #quick li ul li a.permissions,#header #header-inner #quick li ul li a.permissions:hover
609 #header #header-inner #quick li ul li a.permissions,#header #header-inner #quick li ul li a.permissions:hover
610 {
610 {
611 background: #FFF url("../images/icons/key.png") no-repeat 4px 9px;
611 background: #FFF url("../images/icons/key.png") no-repeat 4px 9px;
612 width: 167px;
612 width: 167px;
613 margin: 0;
613 margin: 0;
614 padding: 12px 9px 7px 24px;
614 padding: 12px 9px 7px 24px;
615 }
615 }
616
616
617 #header #header-inner #quick li ul li a.ldap,#header #header-inner #quick li ul li a.ldap:hover
617 #header #header-inner #quick li ul li a.ldap,#header #header-inner #quick li ul li a.ldap:hover
618 {
618 {
619 background: #FFF url("../images/icons/server_key.png") no-repeat 4px 9px;
619 background: #FFF url("../images/icons/server_key.png") no-repeat 4px 9px;
620 width: 167px;
620 width: 167px;
621 margin: 0;
621 margin: 0;
622 padding: 12px 9px 7px 24px;
622 padding: 12px 9px 7px 24px;
623 }
623 }
624
624
625 #header #header-inner #quick li ul li a.fork,#header #header-inner #quick li ul li a.fork:hover
625 #header #header-inner #quick li ul li a.fork,#header #header-inner #quick li ul li a.fork:hover
626 {
626 {
627 background: #FFF url("../images/icons/arrow_divide.png") no-repeat 4px
627 background: #FFF url("../images/icons/arrow_divide.png") no-repeat 4px
628 9px;
628 9px;
629 width: 167px;
629 width: 167px;
630 margin: 0;
630 margin: 0;
631 padding: 12px 9px 7px 24px;
631 padding: 12px 9px 7px 24px;
632 }
632 }
633
633
634 #header #header-inner #quick li ul li a.search,#header #header-inner #quick li ul li a.search:hover
634 #header #header-inner #quick li ul li a.search,#header #header-inner #quick li ul li a.search:hover
635 {
635 {
636 background: #FFF url("../images/icons/search_16.png") no-repeat 4px 9px;
636 background: #FFF url("../images/icons/search_16.png") no-repeat 4px 9px;
637 width: 167px;
637 width: 167px;
638 margin: 0;
638 margin: 0;
639 padding: 12px 9px 7px 24px;
639 padding: 12px 9px 7px 24px;
640 }
640 }
641
641
642 #header #header-inner #quick li ul li a.delete,#header #header-inner #quick li ul li a.delete:hover
642 #header #header-inner #quick li ul li a.delete,#header #header-inner #quick li ul li a.delete:hover
643 {
643 {
644 background: #FFF url("../images/icons/delete.png") no-repeat 4px 9px;
644 background: #FFF url("../images/icons/delete.png") no-repeat 4px 9px;
645 width: 167px;
645 width: 167px;
646 margin: 0;
646 margin: 0;
647 padding: 12px 9px 7px 24px;
647 padding: 12px 9px 7px 24px;
648 }
648 }
649
649
650 #header #header-inner #quick li ul li a.branches,#header #header-inner #quick li ul li a.branches:hover
650 #header #header-inner #quick li ul li a.branches,#header #header-inner #quick li ul li a.branches:hover
651 {
651 {
652 background: #FFF url("../images/icons/arrow_branch.png") no-repeat 4px
652 background: #FFF url("../images/icons/arrow_branch.png") no-repeat 4px
653 9px;
653 9px;
654 width: 167px;
654 width: 167px;
655 margin: 0;
655 margin: 0;
656 padding: 12px 9px 7px 24px;
656 padding: 12px 9px 7px 24px;
657 }
657 }
658
658
659 #header #header-inner #quick li ul li a.tags,
659 #header #header-inner #quick li ul li a.tags,
660 #header #header-inner #quick li ul li a.tags:hover{
660 #header #header-inner #quick li ul li a.tags:hover{
661 background: #FFF url("../images/icons/tag_blue.png") no-repeat 4px 9px;
661 background: #FFF url("../images/icons/tag_blue.png") no-repeat 4px 9px;
662 width: 167px;
662 width: 167px;
663 margin: 0;
663 margin: 0;
664 padding: 12px 9px 7px 24px;
664 padding: 12px 9px 7px 24px;
665 }
665 }
666
666
667 #header #header-inner #quick li ul li a.bookmarks,
667 #header #header-inner #quick li ul li a.bookmarks,
668 #header #header-inner #quick li ul li a.bookmarks:hover{
668 #header #header-inner #quick li ul li a.bookmarks:hover{
669 background: #FFF url("../images/icons/tag_green.png") no-repeat 4px 9px;
669 background: #FFF url("../images/icons/tag_green.png") no-repeat 4px 9px;
670 width: 167px;
670 width: 167px;
671 margin: 0;
671 margin: 0;
672 padding: 12px 9px 7px 24px;
672 padding: 12px 9px 7px 24px;
673 }
673 }
674
674
675 #header #header-inner #quick li ul li a.admin,
675 #header #header-inner #quick li ul li a.admin,
676 #header #header-inner #quick li ul li a.admin:hover{
676 #header #header-inner #quick li ul li a.admin:hover{
677 background: #FFF url("../images/icons/cog_edit.png") no-repeat 4px 9px;
677 background: #FFF url("../images/icons/cog_edit.png") no-repeat 4px 9px;
678 width: 167px;
678 width: 167px;
679 margin: 0;
679 margin: 0;
680 padding: 12px 9px 7px 24px;
680 padding: 12px 9px 7px 24px;
681 }
681 }
682
682
683 .groups_breadcrumbs a {
683 .groups_breadcrumbs a {
684 color: #fff;
684 color: #fff;
685 }
685 }
686
686
687 .groups_breadcrumbs a:hover {
687 .groups_breadcrumbs a:hover {
688 color: #bfe3ff;
688 color: #bfe3ff;
689 text-decoration: none;
689 text-decoration: none;
690 }
690 }
691
691
692 td.quick_repo_menu {
692 td.quick_repo_menu {
693 background: #FFF url("../images/vertical-indicator.png") 8px 50% no-repeat !important;
693 background: #FFF url("../images/vertical-indicator.png") 8px 50% no-repeat !important;
694 cursor: pointer;
694 cursor: pointer;
695 width: 8px;
695 width: 8px;
696 border: 1px solid transparent;
696 border: 1px solid transparent;
697 }
697 }
698
698
699 td.quick_repo_menu.active {
699 td.quick_repo_menu.active {
700 background: url("../images/dt-arrow-dn.png") no-repeat scroll 5px 50% #FFFFFF !important;
700 background: url("../images/dt-arrow-dn.png") no-repeat scroll 5px 50% #FFFFFF !important;
701 border: 1px solid #003367;
701 border: 1px solid #003367;
702 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
702 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
703 cursor: pointer;
703 cursor: pointer;
704 }
704 }
705
705
706 td.quick_repo_menu .menu_items {
706 td.quick_repo_menu .menu_items {
707 margin-top: 10px;
707 margin-top: 10px;
708 margin-left:-6px;
708 margin-left:-6px;
709 width: 150px;
709 width: 150px;
710 position: absolute;
710 position: absolute;
711 background-color: #FFF;
711 background-color: #FFF;
712 background: none repeat scroll 0 0 #FFFFFF;
712 background: none repeat scroll 0 0 #FFFFFF;
713 border-color: #003367 #666666 #666666;
713 border-color: #003367 #666666 #666666;
714 border-right: 1px solid #666666;
714 border-right: 1px solid #666666;
715 border-style: solid;
715 border-style: solid;
716 border-width: 1px;
716 border-width: 1px;
717 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
717 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
718 border-top-style: none;
718 border-top-style: none;
719 }
719 }
720
720
721 td.quick_repo_menu .menu_items li {
721 td.quick_repo_menu .menu_items li {
722 padding: 0 !important;
722 padding: 0 !important;
723 }
723 }
724
724
725 td.quick_repo_menu .menu_items a {
725 td.quick_repo_menu .menu_items a {
726 display: block;
726 display: block;
727 padding: 4px 12px 4px 8px;
727 padding: 4px 12px 4px 8px;
728 }
728 }
729
729
730 td.quick_repo_menu .menu_items a:hover {
730 td.quick_repo_menu .menu_items a:hover {
731 background-color: #EEE;
731 background-color: #EEE;
732 text-decoration: none;
732 text-decoration: none;
733 }
733 }
734
734
735 td.quick_repo_menu .menu_items .icon img {
735 td.quick_repo_menu .menu_items .icon img {
736 margin-bottom: -2px;
736 margin-bottom: -2px;
737 }
737 }
738
738
739 td.quick_repo_menu .menu_items.hidden {
739 td.quick_repo_menu .menu_items.hidden {
740 display: none;
740 display: none;
741 }
741 }
742
742
743 .yui-dt-first th {
743 .yui-dt-first th {
744 text-align: left;
744 text-align: left;
745 }
745 }
746
746
747 /*
747 /*
748 Copyright (c) 2011, Yahoo! Inc. All rights reserved.
748 Copyright (c) 2011, Yahoo! Inc. All rights reserved.
749 Code licensed under the BSD License:
749 Code licensed under the BSD License:
750 http://developer.yahoo.com/yui/license.html
750 http://developer.yahoo.com/yui/license.html
751 version: 2.9.0
751 version: 2.9.0
752 */
752 */
753 .yui-skin-sam .yui-dt-mask {
753 .yui-skin-sam .yui-dt-mask {
754 position: absolute;
754 position: absolute;
755 z-index: 9500;
755 z-index: 9500;
756 }
756 }
757 .yui-dt-tmp {
757 .yui-dt-tmp {
758 position: absolute;
758 position: absolute;
759 left: -9000px;
759 left: -9000px;
760 }
760 }
761 .yui-dt-scrollable .yui-dt-bd { overflow: auto }
761 .yui-dt-scrollable .yui-dt-bd { overflow: auto }
762 .yui-dt-scrollable .yui-dt-hd {
762 .yui-dt-scrollable .yui-dt-hd {
763 overflow: hidden;
763 overflow: hidden;
764 position: relative;
764 position: relative;
765 }
765 }
766 .yui-dt-scrollable .yui-dt-bd thead tr,
766 .yui-dt-scrollable .yui-dt-bd thead tr,
767 .yui-dt-scrollable .yui-dt-bd thead th {
767 .yui-dt-scrollable .yui-dt-bd thead th {
768 position: absolute;
768 position: absolute;
769 left: -1500px;
769 left: -1500px;
770 }
770 }
771 .yui-dt-scrollable tbody { -moz-outline: 0 }
771 .yui-dt-scrollable tbody { -moz-outline: 0 }
772 .yui-skin-sam thead .yui-dt-sortable { cursor: pointer }
772 .yui-skin-sam thead .yui-dt-sortable { cursor: pointer }
773 .yui-skin-sam thead .yui-dt-draggable { cursor: move }
773 .yui-skin-sam thead .yui-dt-draggable { cursor: move }
774 .yui-dt-coltarget {
774 .yui-dt-coltarget {
775 position: absolute;
775 position: absolute;
776 z-index: 999;
776 z-index: 999;
777 }
777 }
778 .yui-dt-hd { zoom: 1 }
778 .yui-dt-hd { zoom: 1 }
779 th.yui-dt-resizeable .yui-dt-resizerliner { position: relative }
779 th.yui-dt-resizeable .yui-dt-resizerliner { position: relative }
780 .yui-dt-resizer {
780 .yui-dt-resizer {
781 position: absolute;
781 position: absolute;
782 right: 0;
782 right: 0;
783 bottom: 0;
783 bottom: 0;
784 height: 100%;
784 height: 100%;
785 cursor: e-resize;
785 cursor: e-resize;
786 cursor: col-resize;
786 cursor: col-resize;
787 background-color: #CCC;
787 background-color: #CCC;
788 opacity: 0;
788 opacity: 0;
789 filter: alpha(opacity=0);
789 filter: alpha(opacity=0);
790 }
790 }
791 .yui-dt-resizerproxy {
791 .yui-dt-resizerproxy {
792 visibility: hidden;
792 visibility: hidden;
793 position: absolute;
793 position: absolute;
794 z-index: 9000;
794 z-index: 9000;
795 background-color: #CCC;
795 background-color: #CCC;
796 opacity: 0;
796 opacity: 0;
797 filter: alpha(opacity=0);
797 filter: alpha(opacity=0);
798 }
798 }
799 th.yui-dt-hidden .yui-dt-liner,
799 th.yui-dt-hidden .yui-dt-liner,
800 td.yui-dt-hidden .yui-dt-liner,
800 td.yui-dt-hidden .yui-dt-liner,
801 th.yui-dt-hidden .yui-dt-resizer { display: none }
801 th.yui-dt-hidden .yui-dt-resizer { display: none }
802 .yui-dt-editor,
802 .yui-dt-editor,
803 .yui-dt-editor-shim {
803 .yui-dt-editor-shim {
804 position: absolute;
804 position: absolute;
805 z-index: 9000;
805 z-index: 9000;
806 }
806 }
807 .yui-skin-sam .yui-dt table {
807 .yui-skin-sam .yui-dt table {
808 margin: 0;
808 margin: 0;
809 padding: 0;
809 padding: 0;
810 font-family: arial;
810 font-family: arial;
811 font-size: inherit;
811 font-size: inherit;
812 border-collapse: separate;
812 border-collapse: separate;
813 *border-collapse: collapse;
813 *border-collapse: collapse;
814 border-spacing: 0;
814 border-spacing: 0;
815 border: 1px solid #7f7f7f;
815 border: 1px solid #7f7f7f;
816 }
816 }
817 .yui-skin-sam .yui-dt thead { border-spacing: 0 }
817 .yui-skin-sam .yui-dt thead { border-spacing: 0 }
818 .yui-skin-sam .yui-dt caption {
818 .yui-skin-sam .yui-dt caption {
819 color: #000;
819 color: #000;
820 font-size: 85%;
820 font-size: 85%;
821 font-weight: normal;
821 font-weight: normal;
822 font-style: italic;
822 font-style: italic;
823 line-height: 1;
823 line-height: 1;
824 padding: 1em 0;
824 padding: 1em 0;
825 text-align: center;
825 text-align: center;
826 }
826 }
827 .yui-skin-sam .yui-dt th { background: #d8d8da url(../images/sprite.png) repeat-x 0 0 }
827 .yui-skin-sam .yui-dt th { background: #d8d8da url(../images/sprite.png) repeat-x 0 0 }
828 .yui-skin-sam .yui-dt th,
828 .yui-skin-sam .yui-dt th,
829 .yui-skin-sam .yui-dt th a {
829 .yui-skin-sam .yui-dt th a {
830 font-weight: normal;
830 font-weight: normal;
831 text-decoration: none;
831 text-decoration: none;
832 color: #000;
832 color: #000;
833 vertical-align: bottom;
833 vertical-align: bottom;
834 }
834 }
835 .yui-skin-sam .yui-dt th {
835 .yui-skin-sam .yui-dt th {
836 margin: 0;
836 margin: 0;
837 padding: 0;
837 padding: 0;
838 border: 0;
838 border: 0;
839 border-right: 1px solid #cbcbcb;
839 border-right: 1px solid #cbcbcb;
840 }
840 }
841 .yui-skin-sam .yui-dt tr.yui-dt-first td { border-top: 1px solid #7f7f7f }
841 .yui-skin-sam .yui-dt tr.yui-dt-first td { border-top: 1px solid #7f7f7f }
842 .yui-skin-sam .yui-dt th .yui-dt-liner { white-space: nowrap }
842 .yui-skin-sam .yui-dt th .yui-dt-liner { white-space: nowrap }
843 .yui-skin-sam .yui-dt-liner {
843 .yui-skin-sam .yui-dt-liner {
844 margin: 0;
844 margin: 0;
845 padding: 0;
845 padding: 0;
846 }
846 }
847 .yui-skin-sam .yui-dt-coltarget {
847 .yui-skin-sam .yui-dt-coltarget {
848 width: 5px;
848 width: 5px;
849 background-color: red;
849 background-color: red;
850 }
850 }
851 .yui-skin-sam .yui-dt td {
851 .yui-skin-sam .yui-dt td {
852 margin: 0;
852 margin: 0;
853 padding: 0;
853 padding: 0;
854 border: 0;
854 border: 0;
855 border-right: 1px solid #cbcbcb;
855 border-right: 1px solid #cbcbcb;
856 text-align: left;
856 text-align: left;
857 }
857 }
858 .yui-skin-sam .yui-dt-list td { border-right: 0 }
858 .yui-skin-sam .yui-dt-list td { border-right: 0 }
859 .yui-skin-sam .yui-dt-resizer { width: 6px }
859 .yui-skin-sam .yui-dt-resizer { width: 6px }
860 .yui-skin-sam .yui-dt-mask {
860 .yui-skin-sam .yui-dt-mask {
861 background-color: #000;
861 background-color: #000;
862 opacity: .25;
862 opacity: .25;
863 filter: alpha(opacity=25);
863 filter: alpha(opacity=25);
864 }
864 }
865 .yui-skin-sam .yui-dt-message { background-color: #FFF }
865 .yui-skin-sam .yui-dt-message { background-color: #FFF }
866 .yui-skin-sam .yui-dt-scrollable table { border: 0 }
866 .yui-skin-sam .yui-dt-scrollable table { border: 0 }
867 .yui-skin-sam .yui-dt-scrollable .yui-dt-hd {
867 .yui-skin-sam .yui-dt-scrollable .yui-dt-hd {
868 border-left: 1px solid #7f7f7f;
868 border-left: 1px solid #7f7f7f;
869 border-top: 1px solid #7f7f7f;
869 border-top: 1px solid #7f7f7f;
870 border-right: 1px solid #7f7f7f;
870 border-right: 1px solid #7f7f7f;
871 }
871 }
872 .yui-skin-sam .yui-dt-scrollable .yui-dt-bd {
872 .yui-skin-sam .yui-dt-scrollable .yui-dt-bd {
873 border-left: 1px solid #7f7f7f;
873 border-left: 1px solid #7f7f7f;
874 border-bottom: 1px solid #7f7f7f;
874 border-bottom: 1px solid #7f7f7f;
875 border-right: 1px solid #7f7f7f;
875 border-right: 1px solid #7f7f7f;
876 background-color: #FFF;
876 background-color: #FFF;
877 }
877 }
878 .yui-skin-sam .yui-dt-scrollable .yui-dt-data tr.yui-dt-last td { border-bottom: 1px solid #7f7f7f }
878 .yui-skin-sam .yui-dt-scrollable .yui-dt-data tr.yui-dt-last td { border-bottom: 1px solid #7f7f7f }
879 .yui-skin-sam th.yui-dt-asc,
879 .yui-skin-sam th.yui-dt-asc,
880 .yui-skin-sam th.yui-dt-desc { background: url(../images/sprite.png) repeat-x 0 -100px }
880 .yui-skin-sam th.yui-dt-desc { background: url(../images/sprite.png) repeat-x 0 -100px }
881 .yui-skin-sam th.yui-dt-sortable .yui-dt-label { margin-right: 10px }
881 .yui-skin-sam th.yui-dt-sortable .yui-dt-label { margin-right: 10px }
882 .yui-skin-sam th.yui-dt-asc .yui-dt-liner { background: url(../images/dt-arrow-up.png) no-repeat right }
882 .yui-skin-sam th.yui-dt-asc .yui-dt-liner { background: url(../images/dt-arrow-up.png) no-repeat right }
883 .yui-skin-sam th.yui-dt-desc .yui-dt-liner { background: url(../images/dt-arrow-dn.png) no-repeat right }
883 .yui-skin-sam th.yui-dt-desc .yui-dt-liner { background: url(../images/dt-arrow-dn.png) no-repeat right }
884 tbody .yui-dt-editable { cursor: pointer }
884 tbody .yui-dt-editable { cursor: pointer }
885 .yui-dt-editor {
885 .yui-dt-editor {
886 text-align: left;
886 text-align: left;
887 background-color: #f2f2f2;
887 background-color: #f2f2f2;
888 border: 1px solid #808080;
888 border: 1px solid #808080;
889 padding: 6px;
889 padding: 6px;
890 }
890 }
891 .yui-dt-editor label {
891 .yui-dt-editor label {
892 padding-left: 4px;
892 padding-left: 4px;
893 padding-right: 6px;
893 padding-right: 6px;
894 }
894 }
895 .yui-dt-editor .yui-dt-button {
895 .yui-dt-editor .yui-dt-button {
896 padding-top: 6px;
896 padding-top: 6px;
897 text-align: right;
897 text-align: right;
898 }
898 }
899 .yui-dt-editor .yui-dt-button button {
899 .yui-dt-editor .yui-dt-button button {
900 background: url(../images/sprite.png) repeat-x 0 0;
900 background: url(../images/sprite.png) repeat-x 0 0;
901 border: 1px solid #999;
901 border: 1px solid #999;
902 width: 4em;
902 width: 4em;
903 height: 1.8em;
903 height: 1.8em;
904 margin-left: 6px;
904 margin-left: 6px;
905 }
905 }
906 .yui-dt-editor .yui-dt-button button.yui-dt-default {
906 .yui-dt-editor .yui-dt-button button.yui-dt-default {
907 background: url(../images/sprite.png) repeat-x 0 -1400px;
907 background: url(../images/sprite.png) repeat-x 0 -1400px;
908 background-color: #5584e0;
908 background-color: #5584e0;
909 border: 1px solid #304369;
909 border: 1px solid #304369;
910 color: #FFF;
910 color: #FFF;
911 }
911 }
912 .yui-dt-editor .yui-dt-button button:hover {
912 .yui-dt-editor .yui-dt-button button:hover {
913 background: url(../images/sprite.png) repeat-x 0 -1300px;
913 background: url(../images/sprite.png) repeat-x 0 -1300px;
914 color: #000;
914 color: #000;
915 }
915 }
916 .yui-dt-editor .yui-dt-button button:active {
916 .yui-dt-editor .yui-dt-button button:active {
917 background: url(../images/sprite.png) repeat-x 0 -1700px;
917 background: url(../images/sprite.png) repeat-x 0 -1700px;
918 color: #000;
918 color: #000;
919 }
919 }
920 .yui-skin-sam tr.yui-dt-even { background-color: #FFF }
920 .yui-skin-sam tr.yui-dt-even { background-color: #FFF }
921 .yui-skin-sam tr.yui-dt-odd { background-color: #edf5ff }
921 .yui-skin-sam tr.yui-dt-odd { background-color: #edf5ff }
922 .yui-skin-sam tr.yui-dt-even td.yui-dt-asc,
922 .yui-skin-sam tr.yui-dt-even td.yui-dt-asc,
923 .yui-skin-sam tr.yui-dt-even td.yui-dt-desc { background-color: #edf5ff }
923 .yui-skin-sam tr.yui-dt-even td.yui-dt-desc { background-color: #edf5ff }
924 .yui-skin-sam tr.yui-dt-odd td.yui-dt-asc,
924 .yui-skin-sam tr.yui-dt-odd td.yui-dt-asc,
925 .yui-skin-sam tr.yui-dt-odd td.yui-dt-desc { background-color: #dbeaff }
925 .yui-skin-sam tr.yui-dt-odd td.yui-dt-desc { background-color: #dbeaff }
926 .yui-skin-sam .yui-dt-list tr.yui-dt-even { background-color: #FFF }
926 .yui-skin-sam .yui-dt-list tr.yui-dt-even { background-color: #FFF }
927 .yui-skin-sam .yui-dt-list tr.yui-dt-odd { background-color: #FFF }
927 .yui-skin-sam .yui-dt-list tr.yui-dt-odd { background-color: #FFF }
928 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-asc,
928 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-asc,
929 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-desc { background-color: #edf5ff }
929 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-desc { background-color: #edf5ff }
930 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-asc,
930 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-asc,
931 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-desc { background-color: #edf5ff }
931 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-desc { background-color: #edf5ff }
932 .yui-skin-sam th.yui-dt-highlighted,
932 .yui-skin-sam th.yui-dt-highlighted,
933 .yui-skin-sam th.yui-dt-highlighted a { background-color: #b2d2ff }
933 .yui-skin-sam th.yui-dt-highlighted a { background-color: #b2d2ff }
934 .yui-skin-sam tr.yui-dt-highlighted,
934 .yui-skin-sam tr.yui-dt-highlighted,
935 .yui-skin-sam tr.yui-dt-highlighted td.yui-dt-asc,
935 .yui-skin-sam tr.yui-dt-highlighted td.yui-dt-asc,
936 .yui-skin-sam tr.yui-dt-highlighted td.yui-dt-desc,
936 .yui-skin-sam tr.yui-dt-highlighted td.yui-dt-desc,
937 .yui-skin-sam tr.yui-dt-even td.yui-dt-highlighted,
937 .yui-skin-sam tr.yui-dt-even td.yui-dt-highlighted,
938 .yui-skin-sam tr.yui-dt-odd td.yui-dt-highlighted {
938 .yui-skin-sam tr.yui-dt-odd td.yui-dt-highlighted {
939 cursor: pointer;
939 cursor: pointer;
940 background-color: #b2d2ff;
940 background-color: #b2d2ff;
941 }
941 }
942 .yui-skin-sam .yui-dt-list th.yui-dt-highlighted,
942 .yui-skin-sam .yui-dt-list th.yui-dt-highlighted,
943 .yui-skin-sam .yui-dt-list th.yui-dt-highlighted a { background-color: #b2d2ff }
943 .yui-skin-sam .yui-dt-list th.yui-dt-highlighted a { background-color: #b2d2ff }
944 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted,
944 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted,
945 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-asc,
945 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-asc,
946 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-desc,
946 .yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-desc,
947 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-highlighted,
947 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-highlighted,
948 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-highlighted {
948 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-highlighted {
949 cursor: pointer;
949 cursor: pointer;
950 background-color: #b2d2ff;
950 background-color: #b2d2ff;
951 }
951 }
952 .yui-skin-sam th.yui-dt-selected,
952 .yui-skin-sam th.yui-dt-selected,
953 .yui-skin-sam th.yui-dt-selected a { background-color: #446cd7 }
953 .yui-skin-sam th.yui-dt-selected a { background-color: #446cd7 }
954 .yui-skin-sam tr.yui-dt-selected td,
954 .yui-skin-sam tr.yui-dt-selected td,
955 .yui-skin-sam tr.yui-dt-selected td.yui-dt-asc,
955 .yui-skin-sam tr.yui-dt-selected td.yui-dt-asc,
956 .yui-skin-sam tr.yui-dt-selected td.yui-dt-desc {
956 .yui-skin-sam tr.yui-dt-selected td.yui-dt-desc {
957 background-color: #426fd9;
957 background-color: #426fd9;
958 color: #FFF;
958 color: #FFF;
959 }
959 }
960 .yui-skin-sam tr.yui-dt-even td.yui-dt-selected,
960 .yui-skin-sam tr.yui-dt-even td.yui-dt-selected,
961 .yui-skin-sam tr.yui-dt-odd td.yui-dt-selected {
961 .yui-skin-sam tr.yui-dt-odd td.yui-dt-selected {
962 background-color: #446cd7;
962 background-color: #446cd7;
963 color: #FFF;
963 color: #FFF;
964 }
964 }
965 .yui-skin-sam .yui-dt-list th.yui-dt-selected,
965 .yui-skin-sam .yui-dt-list th.yui-dt-selected,
966 .yui-skin-sam .yui-dt-list th.yui-dt-selected a { background-color: #446cd7 }
966 .yui-skin-sam .yui-dt-list th.yui-dt-selected a { background-color: #446cd7 }
967 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td,
967 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td,
968 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-asc,
968 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-asc,
969 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-desc {
969 .yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-desc {
970 background-color: #426fd9;
970 background-color: #426fd9;
971 color: #FFF;
971 color: #FFF;
972 }
972 }
973 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-selected,
973 .yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-selected,
974 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-selected {
974 .yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-selected {
975 background-color: #446cd7;
975 background-color: #446cd7;
976 color: #FFF;
976 color: #FFF;
977 }
977 }
978 .yui-skin-sam .yui-dt-paginator {
978 .yui-skin-sam .yui-dt-paginator {
979 display: block;
979 display: block;
980 margin: 6px 0;
980 margin: 6px 0;
981 white-space: nowrap;
981 white-space: nowrap;
982 }
982 }
983 .yui-skin-sam .yui-dt-paginator .yui-dt-first,
983 .yui-skin-sam .yui-dt-paginator .yui-dt-first,
984 .yui-skin-sam .yui-dt-paginator .yui-dt-last,
984 .yui-skin-sam .yui-dt-paginator .yui-dt-last,
985 .yui-skin-sam .yui-dt-paginator .yui-dt-selected { padding: 2px 6px }
985 .yui-skin-sam .yui-dt-paginator .yui-dt-selected { padding: 2px 6px }
986 .yui-skin-sam .yui-dt-paginator a.yui-dt-first,
986 .yui-skin-sam .yui-dt-paginator a.yui-dt-first,
987 .yui-skin-sam .yui-dt-paginator a.yui-dt-last { text-decoration: none }
987 .yui-skin-sam .yui-dt-paginator a.yui-dt-last { text-decoration: none }
988 .yui-skin-sam .yui-dt-paginator .yui-dt-previous,
988 .yui-skin-sam .yui-dt-paginator .yui-dt-previous,
989 .yui-skin-sam .yui-dt-paginator .yui-dt-next { display: none }
989 .yui-skin-sam .yui-dt-paginator .yui-dt-next { display: none }
990 .yui-skin-sam a.yui-dt-page {
990 .yui-skin-sam a.yui-dt-page {
991 border: 1px solid #cbcbcb;
991 border: 1px solid #cbcbcb;
992 padding: 2px 6px;
992 padding: 2px 6px;
993 text-decoration: none;
993 text-decoration: none;
994 background-color: #fff;
994 background-color: #fff;
995 }
995 }
996 .yui-skin-sam .yui-dt-selected {
996 .yui-skin-sam .yui-dt-selected {
997 border: 1px solid #fff;
997 border: 1px solid #fff;
998 background-color: #fff;
998 background-color: #fff;
999 }
999 }
1000
1000
1001 #content #left {
1001 #content #left {
1002 left: 0;
1002 left: 0;
1003 width: 280px;
1003 width: 280px;
1004 position: absolute;
1004 position: absolute;
1005 }
1005 }
1006
1006
1007 #content #right {
1007 #content #right {
1008 margin: 0 60px 10px 290px;
1008 margin: 0 60px 10px 290px;
1009 }
1009 }
1010
1010
1011 #content div.box {
1011 #content div.box {
1012 clear: both;
1012 clear: both;
1013 overflow: hidden;
1013 overflow: hidden;
1014 background: #fff;
1014 background: #fff;
1015 margin: 0 0 10px;
1015 margin: 0 0 10px;
1016 padding: 0 0 10px;
1016 padding: 0 0 10px;
1017 -webkit-border-radius: 4px 4px 4px 4px;
1017 -webkit-border-radius: 4px 4px 4px 4px;
1018 -khtml-border-radius: 4px 4px 4px 4px;
1018 -khtml-border-radius: 4px 4px 4px 4px;
1019 -moz-border-radius: 4px 4px 4px 4px;
1019 -moz-border-radius: 4px 4px 4px 4px;
1020 border-radius: 4px 4px 4px 4px;
1020 border-radius: 4px 4px 4px 4px;
1021 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1021 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1022 }
1022 }
1023
1023
1024 #content div.box-left {
1024 #content div.box-left {
1025 width: 49%;
1025 width: 49%;
1026 clear: none;
1026 clear: none;
1027 float: left;
1027 float: left;
1028 margin: 0 0 10px;
1028 margin: 0 0 10px;
1029 }
1029 }
1030
1030
1031 #content div.box-right {
1031 #content div.box-right {
1032 width: 49%;
1032 width: 49%;
1033 clear: none;
1033 clear: none;
1034 float: right;
1034 float: right;
1035 margin: 0 0 10px;
1035 margin: 0 0 10px;
1036 }
1036 }
1037
1037
1038 #content div.box div.title {
1038 #content div.box div.title {
1039 clear: both;
1039 clear: both;
1040 overflow: hidden;
1040 overflow: hidden;
1041 background-color: #003B76;
1041 background-color: #003B76;
1042 background-repeat: repeat-x;
1042 background-repeat: repeat-x;
1043 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
1043 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
1044 background-image: -moz-linear-gradient(top, #003b76, #00376e);
1044 background-image: -moz-linear-gradient(top, #003b76, #00376e);
1045 background-image: -ms-linear-gradient(top, #003b76, #00376e);
1045 background-image: -ms-linear-gradient(top, #003b76, #00376e);
1046 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
1046 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
1047 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
1047 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
1048 background-image: -o-linear-gradient(top, #003b76, #00376e);
1048 background-image: -o-linear-gradient(top, #003b76, #00376e);
1049 background-image: linear-gradient(top, #003b76, #00376e);
1049 background-image: linear-gradient(top, #003b76, #00376e);
1050 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', endColorstr='#00376e', GradientType=0 );
1050 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', endColorstr='#00376e', GradientType=0 );
1051 margin: 0 0 20px;
1051 margin: 0 0 20px;
1052 padding: 0;
1052 padding: 0;
1053 }
1053 }
1054
1054
1055 #content div.box div.title h5 {
1055 #content div.box div.title h5 {
1056 float: left;
1056 float: left;
1057 border: none;
1057 border: none;
1058 color: #fff;
1058 color: #fff;
1059 text-transform: uppercase;
1059 text-transform: uppercase;
1060 margin: 0;
1060 margin: 0;
1061 padding: 11px 0 11px 10px;
1061 padding: 11px 0 11px 10px;
1062 }
1062 }
1063
1063
1064 #content div.box div.title .link-white{
1064 #content div.box div.title .link-white{
1065 color: #FFFFFF;
1065 color: #FFFFFF;
1066 }
1066 }
1067
1067
1068 #content div.box div.title ul.links li {
1068 #content div.box div.title ul.links li {
1069 list-style: none;
1069 list-style: none;
1070 float: left;
1070 float: left;
1071 margin: 0;
1071 margin: 0;
1072 padding: 0;
1072 padding: 0;
1073 }
1073 }
1074
1074
1075 #content div.box div.title ul.links li a {
1075 #content div.box div.title ul.links li a {
1076 border-left: 1px solid #316293;
1076 border-left: 1px solid #316293;
1077 color: #FFFFFF;
1077 color: #FFFFFF;
1078 display: block;
1078 display: block;
1079 float: left;
1079 float: left;
1080 font-size: 13px;
1080 font-size: 13px;
1081 font-weight: 700;
1081 font-weight: 700;
1082 height: 1%;
1082 height: 1%;
1083 margin: 0;
1083 margin: 0;
1084 padding: 11px 22px 12px;
1084 padding: 11px 22px 12px;
1085 text-decoration: none;
1085 text-decoration: none;
1086 }
1086 }
1087
1087
1088 #content div.box h1,#content div.box h2,#content div.box h3,#content div.box h4,#content div.box h5,#content div.box h6
1088 #content div.box h1,#content div.box h2,#content div.box h3,#content div.box h4,#content div.box h5,#content div.box h6
1089 {
1089 {
1090 clear: both;
1090 clear: both;
1091 overflow: hidden;
1091 overflow: hidden;
1092 border-bottom: 1px solid #DDD;
1092 border-bottom: 1px solid #DDD;
1093 margin: 10px 20px;
1093 margin: 10px 20px;
1094 padding: 0 0 15px;
1094 padding: 0 0 15px;
1095 }
1095 }
1096
1096
1097 #content div.box p {
1097 #content div.box p {
1098 color: #5f5f5f;
1098 color: #5f5f5f;
1099 font-size: 12px;
1099 font-size: 12px;
1100 line-height: 150%;
1100 line-height: 150%;
1101 margin: 0 24px 10px;
1101 margin: 0 24px 10px;
1102 padding: 0;
1102 padding: 0;
1103 }
1103 }
1104
1104
1105 #content div.box blockquote {
1105 #content div.box blockquote {
1106 border-left: 4px solid #DDD;
1106 border-left: 4px solid #DDD;
1107 color: #5f5f5f;
1107 color: #5f5f5f;
1108 font-size: 11px;
1108 font-size: 11px;
1109 line-height: 150%;
1109 line-height: 150%;
1110 margin: 0 34px;
1110 margin: 0 34px;
1111 padding: 0 0 0 14px;
1111 padding: 0 0 0 14px;
1112 }
1112 }
1113
1113
1114 #content div.box blockquote p {
1114 #content div.box blockquote p {
1115 margin: 10px 0;
1115 margin: 10px 0;
1116 padding: 0;
1116 padding: 0;
1117 }
1117 }
1118
1118
1119 #content div.box dl {
1119 #content div.box dl {
1120 margin: 10px 0px;
1120 margin: 10px 0px;
1121 }
1121 }
1122
1122
1123 #content div.box dt {
1123 #content div.box dt {
1124 font-size: 12px;
1124 font-size: 12px;
1125 margin: 0;
1125 margin: 0;
1126 }
1126 }
1127
1127
1128 #content div.box dd {
1128 #content div.box dd {
1129 font-size: 12px;
1129 font-size: 12px;
1130 margin: 0;
1130 margin: 0;
1131 padding: 8px 0 8px 15px;
1131 padding: 8px 0 8px 15px;
1132 }
1132 }
1133
1133
1134 #content div.box li {
1134 #content div.box li {
1135 font-size: 12px;
1135 font-size: 12px;
1136 padding: 4px 0;
1136 padding: 4px 0;
1137 }
1137 }
1138
1138
1139 #content div.box ul.disc,#content div.box ul.circle {
1139 #content div.box ul.disc,#content div.box ul.circle {
1140 margin: 10px 24px 10px 38px;
1140 margin: 10px 24px 10px 38px;
1141 }
1141 }
1142
1142
1143 #content div.box ul.square {
1143 #content div.box ul.square {
1144 margin: 10px 24px 10px 40px;
1144 margin: 10px 24px 10px 40px;
1145 }
1145 }
1146
1146
1147 #content div.box img.left {
1147 #content div.box img.left {
1148 border: none;
1148 border: none;
1149 float: left;
1149 float: left;
1150 margin: 10px 10px 10px 0;
1150 margin: 10px 10px 10px 0;
1151 }
1151 }
1152
1152
1153 #content div.box img.right {
1153 #content div.box img.right {
1154 border: none;
1154 border: none;
1155 float: right;
1155 float: right;
1156 margin: 10px 0 10px 10px;
1156 margin: 10px 0 10px 10px;
1157 }
1157 }
1158
1158
1159 #content div.box div.messages {
1159 #content div.box div.messages {
1160 clear: both;
1160 clear: both;
1161 overflow: hidden;
1161 overflow: hidden;
1162 margin: 0 20px;
1162 margin: 0 20px;
1163 padding: 0;
1163 padding: 0;
1164 }
1164 }
1165
1165
1166 #content div.box div.message {
1166 #content div.box div.message {
1167 clear: both;
1167 clear: both;
1168 overflow: hidden;
1168 overflow: hidden;
1169 margin: 0;
1169 margin: 0;
1170 padding: 5px 0;
1170 padding: 5px 0;
1171 white-space: pre-wrap;
1171 white-space: pre-wrap;
1172 }
1172 }
1173 #content div.box div.expand {
1173 #content div.box div.expand {
1174 width: 110%;
1174 width: 110%;
1175 height:14px;
1175 height:14px;
1176 font-size:10px;
1176 font-size:10px;
1177 text-align:center;
1177 text-align:center;
1178 cursor: pointer;
1178 cursor: pointer;
1179 color:#666;
1179 color:#666;
1180
1180
1181 background:-webkit-gradient(linear,0% 50%,100% 50%,color-stop(0%,rgba(255,255,255,0)),color-stop(100%,rgba(64,96,128,0.1)));
1181 background:-webkit-gradient(linear,0% 50%,100% 50%,color-stop(0%,rgba(255,255,255,0)),color-stop(100%,rgba(64,96,128,0.1)));
1182 background:-webkit-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1182 background:-webkit-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1183 background:-moz-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1183 background:-moz-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1184 background:-o-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1184 background:-o-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1185 background:-ms-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1185 background:-ms-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1186 background:linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1186 background:linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1));
1187
1187
1188 display: none;
1188 display: none;
1189 }
1189 }
1190 #content div.box div.expand .expandtext {
1190 #content div.box div.expand .expandtext {
1191 background-color: #ffffff;
1191 background-color: #ffffff;
1192 padding: 2px;
1192 padding: 2px;
1193 border-radius: 2px;
1193 border-radius: 2px;
1194 }
1194 }
1195
1195
1196 #content div.box div.message a {
1196 #content div.box div.message a {
1197 font-weight: 400 !important;
1197 font-weight: 400 !important;
1198 }
1198 }
1199
1199
1200 #content div.box div.message div.image {
1200 #content div.box div.message div.image {
1201 float: left;
1201 float: left;
1202 margin: 9px 0 0 5px;
1202 margin: 9px 0 0 5px;
1203 padding: 6px;
1203 padding: 6px;
1204 }
1204 }
1205
1205
1206 #content div.box div.message div.image img {
1206 #content div.box div.message div.image img {
1207 vertical-align: middle;
1207 vertical-align: middle;
1208 margin: 0;
1208 margin: 0;
1209 }
1209 }
1210
1210
1211 #content div.box div.message div.text {
1211 #content div.box div.message div.text {
1212 float: left;
1212 float: left;
1213 margin: 0;
1213 margin: 0;
1214 padding: 9px 6px;
1214 padding: 9px 6px;
1215 }
1215 }
1216
1216
1217 #content div.box div.message div.dismiss a {
1217 #content div.box div.message div.dismiss a {
1218 height: 16px;
1218 height: 16px;
1219 width: 16px;
1219 width: 16px;
1220 display: block;
1220 display: block;
1221 background: url("../images/icons/cross.png") no-repeat;
1221 background: url("../images/icons/cross.png") no-repeat;
1222 margin: 15px 14px 0 0;
1222 margin: 15px 14px 0 0;
1223 padding: 0;
1223 padding: 0;
1224 }
1224 }
1225
1225
1226 #content div.box div.message div.text h1,#content div.box div.message div.text h2,#content div.box div.message div.text h3,#content div.box div.message div.text h4,#content div.box div.message div.text h5,#content div.box div.message div.text h6
1226 #content div.box div.message div.text h1,#content div.box div.message div.text h2,#content div.box div.message div.text h3,#content div.box div.message div.text h4,#content div.box div.message div.text h5,#content div.box div.message div.text h6
1227 {
1227 {
1228 border: none;
1228 border: none;
1229 margin: 0;
1229 margin: 0;
1230 padding: 0;
1230 padding: 0;
1231 }
1231 }
1232
1232
1233 #content div.box div.message div.text span {
1233 #content div.box div.message div.text span {
1234 height: 1%;
1234 height: 1%;
1235 display: block;
1235 display: block;
1236 margin: 0;
1236 margin: 0;
1237 padding: 5px 0 0;
1237 padding: 5px 0 0;
1238 }
1238 }
1239
1239
1240 #content div.box div.message-error {
1240 #content div.box div.message-error {
1241 height: 1%;
1241 height: 1%;
1242 clear: both;
1242 clear: both;
1243 overflow: hidden;
1243 overflow: hidden;
1244 background: #FBE3E4;
1244 background: #FBE3E4;
1245 border: 1px solid #FBC2C4;
1245 border: 1px solid #FBC2C4;
1246 color: #860006;
1246 color: #860006;
1247 }
1247 }
1248
1248
1249 #content div.box div.message-error h6 {
1249 #content div.box div.message-error h6 {
1250 color: #860006;
1250 color: #860006;
1251 }
1251 }
1252
1252
1253 #content div.box div.message-warning {
1253 #content div.box div.message-warning {
1254 height: 1%;
1254 height: 1%;
1255 clear: both;
1255 clear: both;
1256 overflow: hidden;
1256 overflow: hidden;
1257 background: #FFF6BF;
1257 background: #FFF6BF;
1258 border: 1px solid #FFD324;
1258 border: 1px solid #FFD324;
1259 color: #5f5200;
1259 color: #5f5200;
1260 }
1260 }
1261
1261
1262 #content div.box div.message-warning h6 {
1262 #content div.box div.message-warning h6 {
1263 color: #5f5200;
1263 color: #5f5200;
1264 }
1264 }
1265
1265
1266 #content div.box div.message-notice {
1266 #content div.box div.message-notice {
1267 height: 1%;
1267 height: 1%;
1268 clear: both;
1268 clear: both;
1269 overflow: hidden;
1269 overflow: hidden;
1270 background: #8FBDE0;
1270 background: #8FBDE0;
1271 border: 1px solid #6BACDE;
1271 border: 1px solid #6BACDE;
1272 color: #003863;
1272 color: #003863;
1273 }
1273 }
1274
1274
1275 #content div.box div.message-notice h6 {
1275 #content div.box div.message-notice h6 {
1276 color: #003863;
1276 color: #003863;
1277 }
1277 }
1278
1278
1279 #content div.box div.message-success {
1279 #content div.box div.message-success {
1280 height: 1%;
1280 height: 1%;
1281 clear: both;
1281 clear: both;
1282 overflow: hidden;
1282 overflow: hidden;
1283 background: #E6EFC2;
1283 background: #E6EFC2;
1284 border: 1px solid #C6D880;
1284 border: 1px solid #C6D880;
1285 color: #4e6100;
1285 color: #4e6100;
1286 }
1286 }
1287
1287
1288 #content div.box div.message-success h6 {
1288 #content div.box div.message-success h6 {
1289 color: #4e6100;
1289 color: #4e6100;
1290 }
1290 }
1291
1291
1292 #content div.box div.form div.fields div.field {
1292 #content div.box div.form div.fields div.field {
1293 height: 1%;
1293 height: 1%;
1294 border-bottom: 1px solid #DDD;
1294 border-bottom: 1px solid #DDD;
1295 clear: both;
1295 clear: both;
1296 margin: 0;
1296 margin: 0;
1297 padding: 10px 0;
1297 padding: 10px 0;
1298 }
1298 }
1299
1299
1300 #content div.box div.form div.fields div.field-first {
1300 #content div.box div.form div.fields div.field-first {
1301 padding: 0 0 10px;
1301 padding: 0 0 10px;
1302 }
1302 }
1303
1303
1304 #content div.box div.form div.fields div.field-noborder {
1304 #content div.box div.form div.fields div.field-noborder {
1305 border-bottom: 0 !important;
1305 border-bottom: 0 !important;
1306 }
1306 }
1307
1307
1308 #content div.box div.form div.fields div.field span.error-message {
1308 #content div.box div.form div.fields div.field span.error-message {
1309 height: 1%;
1309 height: 1%;
1310 display: inline-block;
1310 display: inline-block;
1311 color: red;
1311 color: red;
1312 margin: 8px 0 0 4px;
1312 margin: 8px 0 0 4px;
1313 padding: 0;
1313 padding: 0;
1314 }
1314 }
1315
1315
1316 #content div.box div.form div.fields div.field span.success {
1316 #content div.box div.form div.fields div.field span.success {
1317 height: 1%;
1317 height: 1%;
1318 display: block;
1318 display: block;
1319 color: #316309;
1319 color: #316309;
1320 margin: 8px 0 0;
1320 margin: 8px 0 0;
1321 padding: 0;
1321 padding: 0;
1322 }
1322 }
1323
1323
1324 #content div.box div.form div.fields div.field div.label {
1324 #content div.box div.form div.fields div.field div.label {
1325 left: 70px;
1325 left: 70px;
1326 width: 155px;
1326 width: 155px;
1327 position: absolute;
1327 position: absolute;
1328 margin: 0;
1328 margin: 0;
1329 padding: 5px 0 0 0px;
1329 padding: 5px 0 0 0px;
1330 }
1330 }
1331
1331
1332 #content div.box div.form div.fields div.field div.label-summary {
1332 #content div.box div.form div.fields div.field div.label-summary {
1333 left: 30px;
1333 left: 30px;
1334 width: 155px;
1334 width: 155px;
1335 position: absolute;
1335 position: absolute;
1336 margin: 0;
1336 margin: 0;
1337 padding: 0px 0 0 0px;
1337 padding: 0px 0 0 0px;
1338 }
1338 }
1339
1339
1340 #content div.box-left div.form div.fields div.field div.label,
1340 #content div.box-left div.form div.fields div.field div.label,
1341 #content div.box-right div.form div.fields div.field div.label,
1341 #content div.box-right div.form div.fields div.field div.label,
1342 #content div.box-left div.form div.fields div.field div.label,
1342 #content div.box-left div.form div.fields div.field div.label,
1343 #content div.box-left div.form div.fields div.field div.label-summary,
1343 #content div.box-left div.form div.fields div.field div.label-summary,
1344 #content div.box-right div.form div.fields div.field div.label-summary,
1344 #content div.box-right div.form div.fields div.field div.label-summary,
1345 #content div.box-left div.form div.fields div.field div.label-summary
1345 #content div.box-left div.form div.fields div.field div.label-summary
1346 {
1346 {
1347 clear: both;
1347 clear: both;
1348 overflow: hidden;
1348 overflow: hidden;
1349 left: 0;
1349 left: 0;
1350 width: auto;
1350 width: auto;
1351 position: relative;
1351 position: relative;
1352 margin: 0;
1352 margin: 0;
1353 padding: 0 0 8px;
1353 padding: 0 0 8px;
1354 }
1354 }
1355
1355
1356 #content div.box div.form div.fields div.field div.label-select {
1356 #content div.box div.form div.fields div.field div.label-select {
1357 padding: 5px 0 0 5px;
1357 padding: 5px 0 0 5px;
1358 }
1358 }
1359
1359
1360 #content div.box-left div.form div.fields div.field div.label-select,
1360 #content div.box-left div.form div.fields div.field div.label-select,
1361 #content div.box-right div.form div.fields div.field div.label-select
1361 #content div.box-right div.form div.fields div.field div.label-select
1362 {
1362 {
1363 padding: 0 0 8px;
1363 padding: 0 0 8px;
1364 }
1364 }
1365
1365
1366 #content div.box-left div.form div.fields div.field div.label-textarea,
1366 #content div.box-left div.form div.fields div.field div.label-textarea,
1367 #content div.box-right div.form div.fields div.field div.label-textarea
1367 #content div.box-right div.form div.fields div.field div.label-textarea
1368 {
1368 {
1369 padding: 0 0 8px !important;
1369 padding: 0 0 8px !important;
1370 }
1370 }
1371
1371
1372 #content div.box div.form div.fields div.field div.label label,div.label label
1372 #content div.box div.form div.fields div.field div.label label,div.label label
1373 {
1373 {
1374 color: #393939;
1374 color: #393939;
1375 font-weight: 700;
1375 font-weight: 700;
1376 }
1376 }
1377 #content div.box div.form div.fields div.field div.label label,div.label-summary label
1377 #content div.box div.form div.fields div.field div.label label,div.label-summary label
1378 {
1378 {
1379 color: #393939;
1379 color: #393939;
1380 font-weight: 700;
1380 font-weight: 700;
1381 }
1381 }
1382 #content div.box div.form div.fields div.field div.input {
1382 #content div.box div.form div.fields div.field div.input {
1383 margin: 0 0 0 200px;
1383 margin: 0 0 0 200px;
1384 }
1384 }
1385
1385
1386 #content div.box div.form div.fields div.field div.input.summary {
1386 #content div.box div.form div.fields div.field div.input.summary {
1387 margin: 0 0 0 110px;
1387 margin: 0 0 0 110px;
1388 }
1388 }
1389 #content div.box div.form div.fields div.field div.input.summary-short {
1389 #content div.box div.form div.fields div.field div.input.summary-short {
1390 margin: 0 0 0 110px;
1390 margin: 0 0 0 110px;
1391 }
1391 }
1392 #content div.box div.form div.fields div.field div.file {
1392 #content div.box div.form div.fields div.field div.file {
1393 margin: 0 0 0 200px;
1393 margin: 0 0 0 200px;
1394 }
1394 }
1395
1395
1396 #content div.box-left div.form div.fields div.field div.input,#content div.box-right div.form div.fields div.field div.input
1396 #content div.box-left div.form div.fields div.field div.input,#content div.box-right div.form div.fields div.field div.input
1397 {
1397 {
1398 margin: 0 0 0 0px;
1398 margin: 0 0 0 0px;
1399 }
1399 }
1400
1400
1401 #content div.box div.form div.fields div.field div.input input {
1401 #content div.box div.form div.fields div.field div.input input {
1402 background: #FFF;
1402 background: #FFF;
1403 border-top: 1px solid #b3b3b3;
1403 border-top: 1px solid #b3b3b3;
1404 border-left: 1px solid #b3b3b3;
1404 border-left: 1px solid #b3b3b3;
1405 border-right: 1px solid #eaeaea;
1405 border-right: 1px solid #eaeaea;
1406 border-bottom: 1px solid #eaeaea;
1406 border-bottom: 1px solid #eaeaea;
1407 color: #000;
1407 color: #000;
1408 font-size: 11px;
1408 font-size: 11px;
1409 margin: 0;
1409 margin: 0;
1410 padding: 7px 7px 6px;
1410 padding: 7px 7px 6px;
1411 }
1411 }
1412
1412
1413 #content div.box div.form div.fields div.field div.input input#clone_url,
1413 #content div.box div.form div.fields div.field div.input input#clone_url,
1414 #content div.box div.form div.fields div.field div.input input#clone_url_id
1414 #content div.box div.form div.fields div.field div.input input#clone_url_id
1415 {
1415 {
1416 font-size: 16px;
1416 font-size: 16px;
1417 padding: 2px;
1417 padding: 2px;
1418 }
1418 }
1419
1419
1420 #content div.box div.form div.fields div.field div.file input {
1420 #content div.box div.form div.fields div.field div.file input {
1421 background: none repeat scroll 0 0 #FFFFFF;
1421 background: none repeat scroll 0 0 #FFFFFF;
1422 border-color: #B3B3B3 #EAEAEA #EAEAEA #B3B3B3;
1422 border-color: #B3B3B3 #EAEAEA #EAEAEA #B3B3B3;
1423 border-style: solid;
1423 border-style: solid;
1424 border-width: 1px;
1424 border-width: 1px;
1425 color: #000000;
1425 color: #000000;
1426 font-size: 11px;
1426 font-size: 11px;
1427 margin: 0;
1427 margin: 0;
1428 padding: 7px 7px 6px;
1428 padding: 7px 7px 6px;
1429 }
1429 }
1430
1430
1431 input.disabled {
1431 input.disabled {
1432 background-color: #F5F5F5 !important;
1432 background-color: #F5F5F5 !important;
1433 }
1433 }
1434 #content div.box div.form div.fields div.field div.input input.small {
1434 #content div.box div.form div.fields div.field div.input input.small {
1435 width: 30%;
1435 width: 30%;
1436 }
1436 }
1437
1437
1438 #content div.box div.form div.fields div.field div.input input.medium {
1438 #content div.box div.form div.fields div.field div.input input.medium {
1439 width: 55%;
1439 width: 55%;
1440 }
1440 }
1441
1441
1442 #content div.box div.form div.fields div.field div.input input.large {
1442 #content div.box div.form div.fields div.field div.input input.large {
1443 width: 85%;
1443 width: 85%;
1444 }
1444 }
1445
1445
1446 #content div.box div.form div.fields div.field div.input input.date {
1446 #content div.box div.form div.fields div.field div.input input.date {
1447 width: 177px;
1447 width: 177px;
1448 }
1448 }
1449
1449
1450 #content div.box div.form div.fields div.field div.input input.button {
1450 #content div.box div.form div.fields div.field div.input input.button {
1451 background: #D4D0C8;
1451 background: #D4D0C8;
1452 border-top: 1px solid #FFF;
1452 border-top: 1px solid #FFF;
1453 border-left: 1px solid #FFF;
1453 border-left: 1px solid #FFF;
1454 border-right: 1px solid #404040;
1454 border-right: 1px solid #404040;
1455 border-bottom: 1px solid #404040;
1455 border-bottom: 1px solid #404040;
1456 color: #000;
1456 color: #000;
1457 margin: 0;
1457 margin: 0;
1458 padding: 4px 8px;
1458 padding: 4px 8px;
1459 }
1459 }
1460
1460
1461 #content div.box div.form div.fields div.field div.textarea {
1461 #content div.box div.form div.fields div.field div.textarea {
1462 border-top: 1px solid #b3b3b3;
1462 border-top: 1px solid #b3b3b3;
1463 border-left: 1px solid #b3b3b3;
1463 border-left: 1px solid #b3b3b3;
1464 border-right: 1px solid #eaeaea;
1464 border-right: 1px solid #eaeaea;
1465 border-bottom: 1px solid #eaeaea;
1465 border-bottom: 1px solid #eaeaea;
1466 margin: 0 0 0 200px;
1466 margin: 0 0 0 200px;
1467 padding: 10px;
1467 padding: 10px;
1468 }
1468 }
1469
1469
1470 #content div.box div.form div.fields div.field div.textarea-editor {
1470 #content div.box div.form div.fields div.field div.textarea-editor {
1471 border: 1px solid #ddd;
1471 border: 1px solid #ddd;
1472 padding: 0;
1472 padding: 0;
1473 }
1473 }
1474
1474
1475 #content div.box div.form div.fields div.field div.textarea textarea {
1475 #content div.box div.form div.fields div.field div.textarea textarea {
1476 width: 100%;
1476 width: 100%;
1477 height: 220px;
1477 height: 220px;
1478 overflow: hidden;
1478 overflow: hidden;
1479 background: #FFF;
1479 background: #FFF;
1480 color: #000;
1480 color: #000;
1481 font-size: 11px;
1481 font-size: 11px;
1482 outline: none;
1482 outline: none;
1483 border-width: 0;
1483 border-width: 0;
1484 margin: 0;
1484 margin: 0;
1485 padding: 0;
1485 padding: 0;
1486 }
1486 }
1487
1487
1488 #content div.box-left div.form div.fields div.field div.textarea textarea,#content div.box-right div.form div.fields div.field div.textarea textarea
1488 #content div.box-left div.form div.fields div.field div.textarea textarea,#content div.box-right div.form div.fields div.field div.textarea textarea
1489 {
1489 {
1490 width: 100%;
1490 width: 100%;
1491 height: 100px;
1491 height: 100px;
1492 }
1492 }
1493
1493
1494 #content div.box div.form div.fields div.field div.textarea table {
1494 #content div.box div.form div.fields div.field div.textarea table {
1495 width: 100%;
1495 width: 100%;
1496 border: none;
1496 border: none;
1497 margin: 0;
1497 margin: 0;
1498 padding: 0;
1498 padding: 0;
1499 }
1499 }
1500
1500
1501 #content div.box div.form div.fields div.field div.textarea table td {
1501 #content div.box div.form div.fields div.field div.textarea table td {
1502 background: #DDD;
1502 background: #DDD;
1503 border: none;
1503 border: none;
1504 padding: 0;
1504 padding: 0;
1505 }
1505 }
1506
1506
1507 #content div.box div.form div.fields div.field div.textarea table td table
1507 #content div.box div.form div.fields div.field div.textarea table td table
1508 {
1508 {
1509 width: auto;
1509 width: auto;
1510 border: none;
1510 border: none;
1511 margin: 0;
1511 margin: 0;
1512 padding: 0;
1512 padding: 0;
1513 }
1513 }
1514
1514
1515 #content div.box div.form div.fields div.field div.textarea table td table td
1515 #content div.box div.form div.fields div.field div.textarea table td table td
1516 {
1516 {
1517 font-size: 11px;
1517 font-size: 11px;
1518 padding: 5px 5px 5px 0;
1518 padding: 5px 5px 5px 0;
1519 }
1519 }
1520
1520
1521 #content div.box div.form div.fields div.field input[type=text]:focus,#content div.box div.form div.fields div.field input[type=password]:focus,#content div.box div.form div.fields div.field input[type=file]:focus,#content div.box div.form div.fields div.field textarea:focus,#content div.box div.form div.fields div.field select:focus
1521 #content div.box div.form div.fields div.field input[type=text]:focus,#content div.box div.form div.fields div.field input[type=password]:focus,#content div.box div.form div.fields div.field input[type=file]:focus,#content div.box div.form div.fields div.field textarea:focus,#content div.box div.form div.fields div.field select:focus
1522 {
1522 {
1523 background: #f6f6f6;
1523 background: #f6f6f6;
1524 border-color: #666;
1524 border-color: #666;
1525 }
1525 }
1526
1526
1527 div.form div.fields div.field div.button {
1527 div.form div.fields div.field div.button {
1528 margin: 0;
1528 margin: 0;
1529 padding: 0 0 0 8px;
1529 padding: 0 0 0 8px;
1530 }
1530 }
1531 #content div.box table.noborder {
1531 #content div.box table.noborder {
1532 border: 1px solid transparent;
1532 border: 1px solid transparent;
1533 }
1533 }
1534
1534
1535 #content div.box table {
1535 #content div.box table {
1536 width: 100%;
1536 width: 100%;
1537 border-collapse: separate;
1537 border-collapse: separate;
1538 margin: 0;
1538 margin: 0;
1539 padding: 0;
1539 padding: 0;
1540 border: 1px solid #eee;
1540 border: 1px solid #eee;
1541 -webkit-border-radius: 4px;
1541 -webkit-border-radius: 4px;
1542 -moz-border-radius: 4px;
1542 -moz-border-radius: 4px;
1543 border-radius: 4px;
1543 border-radius: 4px;
1544 }
1544 }
1545
1545
1546 #content div.box table th {
1546 #content div.box table th {
1547 background: #eee;
1547 background: #eee;
1548 border-bottom: 1px solid #ddd;
1548 border-bottom: 1px solid #ddd;
1549 padding: 5px 0px 5px 5px;
1549 padding: 5px 0px 5px 5px;
1550 }
1550 }
1551
1551
1552 #content div.box table th.left {
1552 #content div.box table th.left {
1553 text-align: left;
1553 text-align: left;
1554 }
1554 }
1555
1555
1556 #content div.box table th.right {
1556 #content div.box table th.right {
1557 text-align: right;
1557 text-align: right;
1558 }
1558 }
1559
1559
1560 #content div.box table th.center {
1560 #content div.box table th.center {
1561 text-align: center;
1561 text-align: center;
1562 }
1562 }
1563
1563
1564 #content div.box table th.selected {
1564 #content div.box table th.selected {
1565 vertical-align: middle;
1565 vertical-align: middle;
1566 padding: 0;
1566 padding: 0;
1567 }
1567 }
1568
1568
1569 #content div.box table td {
1569 #content div.box table td {
1570 background: #fff;
1570 background: #fff;
1571 border-bottom: 1px solid #cdcdcd;
1571 border-bottom: 1px solid #cdcdcd;
1572 vertical-align: middle;
1572 vertical-align: middle;
1573 padding: 5px;
1573 padding: 5px;
1574 }
1574 }
1575
1575
1576 #content div.box table tr.selected td {
1576 #content div.box table tr.selected td {
1577 background: #FFC;
1577 background: #FFC;
1578 }
1578 }
1579
1579
1580 #content div.box table td.selected {
1580 #content div.box table td.selected {
1581 width: 3%;
1581 width: 3%;
1582 text-align: center;
1582 text-align: center;
1583 vertical-align: middle;
1583 vertical-align: middle;
1584 padding: 0;
1584 padding: 0;
1585 }
1585 }
1586
1586
1587 #content div.box table td.action {
1587 #content div.box table td.action {
1588 width: 45%;
1588 width: 45%;
1589 text-align: left;
1589 text-align: left;
1590 }
1590 }
1591
1591
1592 #content div.box table td.date {
1592 #content div.box table td.date {
1593 width: 33%;
1593 width: 33%;
1594 text-align: center;
1594 text-align: center;
1595 }
1595 }
1596
1596
1597 #content div.box div.action {
1597 #content div.box div.action {
1598 float: right;
1598 float: right;
1599 background: #FFF;
1599 background: #FFF;
1600 text-align: right;
1600 text-align: right;
1601 margin: 10px 0 0;
1601 margin: 10px 0 0;
1602 padding: 0;
1602 padding: 0;
1603 }
1603 }
1604
1604
1605 #content div.box div.action select {
1605 #content div.box div.action select {
1606 font-size: 11px;
1606 font-size: 11px;
1607 margin: 0;
1607 margin: 0;
1608 }
1608 }
1609
1609
1610 #content div.box div.action .ui-selectmenu {
1610 #content div.box div.action .ui-selectmenu {
1611 margin: 0;
1611 margin: 0;
1612 padding: 0;
1612 padding: 0;
1613 }
1613 }
1614
1614
1615 #content div.box div.pagination {
1615 #content div.box div.pagination {
1616 height: 1%;
1616 height: 1%;
1617 clear: both;
1617 clear: both;
1618 overflow: hidden;
1618 overflow: hidden;
1619 margin: 10px 0 0;
1619 margin: 10px 0 0;
1620 padding: 0;
1620 padding: 0;
1621 }
1621 }
1622
1622
1623 #content div.box div.pagination ul.pager {
1623 #content div.box div.pagination ul.pager {
1624 float: right;
1624 float: right;
1625 text-align: right;
1625 text-align: right;
1626 margin: 0;
1626 margin: 0;
1627 padding: 0;
1627 padding: 0;
1628 }
1628 }
1629
1629
1630 #content div.box div.pagination ul.pager li {
1630 #content div.box div.pagination ul.pager li {
1631 height: 1%;
1631 height: 1%;
1632 float: left;
1632 float: left;
1633 list-style: none;
1633 list-style: none;
1634 background: #ebebeb url("../images/pager.png") repeat-x;
1634 background: #ebebeb url("../images/pager.png") repeat-x;
1635 border-top: 1px solid #dedede;
1635 border-top: 1px solid #dedede;
1636 border-left: 1px solid #cfcfcf;
1636 border-left: 1px solid #cfcfcf;
1637 border-right: 1px solid #c4c4c4;
1637 border-right: 1px solid #c4c4c4;
1638 border-bottom: 1px solid #c4c4c4;
1638 border-bottom: 1px solid #c4c4c4;
1639 color: #4A4A4A;
1639 color: #4A4A4A;
1640 font-weight: 700;
1640 font-weight: 700;
1641 margin: 0 0 0 4px;
1641 margin: 0 0 0 4px;
1642 padding: 0;
1642 padding: 0;
1643 }
1643 }
1644
1644
1645 #content div.box div.pagination ul.pager li.separator {
1645 #content div.box div.pagination ul.pager li.separator {
1646 padding: 6px;
1646 padding: 6px;
1647 }
1647 }
1648
1648
1649 #content div.box div.pagination ul.pager li.current {
1649 #content div.box div.pagination ul.pager li.current {
1650 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1650 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1651 border-top: 1px solid #ccc;
1651 border-top: 1px solid #ccc;
1652 border-left: 1px solid #bebebe;
1652 border-left: 1px solid #bebebe;
1653 border-right: 1px solid #b1b1b1;
1653 border-right: 1px solid #b1b1b1;
1654 border-bottom: 1px solid #afafaf;
1654 border-bottom: 1px solid #afafaf;
1655 color: #515151;
1655 color: #515151;
1656 padding: 6px;
1656 padding: 6px;
1657 }
1657 }
1658
1658
1659 #content div.box div.pagination ul.pager li a {
1659 #content div.box div.pagination ul.pager li a {
1660 height: 1%;
1660 height: 1%;
1661 display: block;
1661 display: block;
1662 float: left;
1662 float: left;
1663 color: #515151;
1663 color: #515151;
1664 text-decoration: none;
1664 text-decoration: none;
1665 margin: 0;
1665 margin: 0;
1666 padding: 6px;
1666 padding: 6px;
1667 }
1667 }
1668
1668
1669 #content div.box div.pagination ul.pager li a:hover,#content div.box div.pagination ul.pager li a:active
1669 #content div.box div.pagination ul.pager li a:hover,#content div.box div.pagination ul.pager li a:active
1670 {
1670 {
1671 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1671 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1672 border-top: 1px solid #ccc;
1672 border-top: 1px solid #ccc;
1673 border-left: 1px solid #bebebe;
1673 border-left: 1px solid #bebebe;
1674 border-right: 1px solid #b1b1b1;
1674 border-right: 1px solid #b1b1b1;
1675 border-bottom: 1px solid #afafaf;
1675 border-bottom: 1px solid #afafaf;
1676 margin: -1px;
1676 margin: -1px;
1677 }
1677 }
1678
1678
1679 #content div.box div.pagination-wh {
1679 #content div.box div.pagination-wh {
1680 height: 1%;
1680 height: 1%;
1681 clear: both;
1681 clear: both;
1682 overflow: hidden;
1682 overflow: hidden;
1683 text-align: right;
1683 text-align: right;
1684 margin: 10px 0 0;
1684 margin: 10px 0 0;
1685 padding: 0;
1685 padding: 0;
1686 }
1686 }
1687
1687
1688 #content div.box div.pagination-right {
1688 #content div.box div.pagination-right {
1689 float: right;
1689 float: right;
1690 }
1690 }
1691
1691
1692 #content div.box div.pagination-wh a,#content div.box div.pagination-wh span.pager_dotdot
1692 #content div.box div.pagination-wh a,#content div.box div.pagination-wh span.pager_dotdot
1693 {
1693 {
1694 height: 1%;
1694 height: 1%;
1695 float: left;
1695 float: left;
1696 background: #ebebeb url("../images/pager.png") repeat-x;
1696 background: #ebebeb url("../images/pager.png") repeat-x;
1697 border-top: 1px solid #dedede;
1697 border-top: 1px solid #dedede;
1698 border-left: 1px solid #cfcfcf;
1698 border-left: 1px solid #cfcfcf;
1699 border-right: 1px solid #c4c4c4;
1699 border-right: 1px solid #c4c4c4;
1700 border-bottom: 1px solid #c4c4c4;
1700 border-bottom: 1px solid #c4c4c4;
1701 color: #4A4A4A;
1701 color: #4A4A4A;
1702 font-weight: 700;
1702 font-weight: 700;
1703 margin: 0 0 0 4px;
1703 margin: 0 0 0 4px;
1704 padding: 6px;
1704 padding: 6px;
1705 }
1705 }
1706
1706
1707 #content div.box div.pagination-wh span.pager_curpage {
1707 #content div.box div.pagination-wh span.pager_curpage {
1708 height: 1%;
1708 height: 1%;
1709 float: left;
1709 float: left;
1710 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1710 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1711 border-top: 1px solid #ccc;
1711 border-top: 1px solid #ccc;
1712 border-left: 1px solid #bebebe;
1712 border-left: 1px solid #bebebe;
1713 border-right: 1px solid #b1b1b1;
1713 border-right: 1px solid #b1b1b1;
1714 border-bottom: 1px solid #afafaf;
1714 border-bottom: 1px solid #afafaf;
1715 color: #515151;
1715 color: #515151;
1716 font-weight: 700;
1716 font-weight: 700;
1717 margin: 0 0 0 4px;
1717 margin: 0 0 0 4px;
1718 padding: 6px;
1718 padding: 6px;
1719 }
1719 }
1720
1720
1721 #content div.box div.pagination-wh a:hover,#content div.box div.pagination-wh a:active
1721 #content div.box div.pagination-wh a:hover,#content div.box div.pagination-wh a:active
1722 {
1722 {
1723 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1723 background: #b4b4b4 url("../images/pager_selected.png") repeat-x;
1724 border-top: 1px solid #ccc;
1724 border-top: 1px solid #ccc;
1725 border-left: 1px solid #bebebe;
1725 border-left: 1px solid #bebebe;
1726 border-right: 1px solid #b1b1b1;
1726 border-right: 1px solid #b1b1b1;
1727 border-bottom: 1px solid #afafaf;
1727 border-bottom: 1px solid #afafaf;
1728 text-decoration: none;
1728 text-decoration: none;
1729 }
1729 }
1730
1730
1731 #content div.box div.traffic div.legend {
1731 #content div.box div.traffic div.legend {
1732 clear: both;
1732 clear: both;
1733 overflow: hidden;
1733 overflow: hidden;
1734 border-bottom: 1px solid #ddd;
1734 border-bottom: 1px solid #ddd;
1735 margin: 0 0 10px;
1735 margin: 0 0 10px;
1736 padding: 0 0 10px;
1736 padding: 0 0 10px;
1737 }
1737 }
1738
1738
1739 #content div.box div.traffic div.legend h6 {
1739 #content div.box div.traffic div.legend h6 {
1740 float: left;
1740 float: left;
1741 border: none;
1741 border: none;
1742 margin: 0;
1742 margin: 0;
1743 padding: 0;
1743 padding: 0;
1744 }
1744 }
1745
1745
1746 #content div.box div.traffic div.legend li {
1746 #content div.box div.traffic div.legend li {
1747 list-style: none;
1747 list-style: none;
1748 float: left;
1748 float: left;
1749 font-size: 11px;
1749 font-size: 11px;
1750 margin: 0;
1750 margin: 0;
1751 padding: 0 8px 0 4px;
1751 padding: 0 8px 0 4px;
1752 }
1752 }
1753
1753
1754 #content div.box div.traffic div.legend li.visits {
1754 #content div.box div.traffic div.legend li.visits {
1755 border-left: 12px solid #edc240;
1755 border-left: 12px solid #edc240;
1756 }
1756 }
1757
1757
1758 #content div.box div.traffic div.legend li.pageviews {
1758 #content div.box div.traffic div.legend li.pageviews {
1759 border-left: 12px solid #afd8f8;
1759 border-left: 12px solid #afd8f8;
1760 }
1760 }
1761
1761
1762 #content div.box div.traffic table {
1762 #content div.box div.traffic table {
1763 width: auto;
1763 width: auto;
1764 }
1764 }
1765
1765
1766 #content div.box div.traffic table td {
1766 #content div.box div.traffic table td {
1767 background: transparent;
1767 background: transparent;
1768 border: none;
1768 border: none;
1769 padding: 2px 3px 3px;
1769 padding: 2px 3px 3px;
1770 }
1770 }
1771
1771
1772 #content div.box div.traffic table td.legendLabel {
1772 #content div.box div.traffic table td.legendLabel {
1773 padding: 0 3px 2px;
1773 padding: 0 3px 2px;
1774 }
1774 }
1775
1775
1776 #summary {
1776 #summary {
1777
1777
1778 }
1778 }
1779
1779
1780 #summary .desc {
1780 #summary .desc {
1781 white-space: pre;
1781 white-space: pre;
1782 width: 100%;
1782 width: 100%;
1783 }
1783 }
1784
1784
1785 #summary .repo_name {
1785 #summary .repo_name {
1786 font-size: 1.6em;
1786 font-size: 1.6em;
1787 font-weight: bold;
1787 font-weight: bold;
1788 vertical-align: baseline;
1788 vertical-align: baseline;
1789 clear: right
1789 clear: right
1790 }
1790 }
1791
1791
1792 #footer {
1792 #footer {
1793 clear: both;
1793 clear: both;
1794 overflow: hidden;
1794 overflow: hidden;
1795 text-align: right;
1795 text-align: right;
1796 margin: 0;
1796 margin: 0;
1797 padding: 0 10px 4px;
1797 padding: 0 10px 4px;
1798 margin: -10px 0 0;
1798 margin: -10px 0 0;
1799 }
1799 }
1800
1800
1801 #footer div#footer-inner {
1801 #footer div#footer-inner {
1802 background-color: #003B76;
1802 background-color: #003B76;
1803 background-repeat : repeat-x;
1803 background-repeat : repeat-x;
1804 background-image : -khtml-gradient( linear, left top, left bottom, from(#003B76), to(#00376E));
1804 background-image : -khtml-gradient( linear, left top, left bottom, from(#003B76), to(#00376E));
1805 background-image : -moz-linear-gradient(top, #003b76, #00376e);
1805 background-image : -moz-linear-gradient(top, #003b76, #00376e);
1806 background-image : -ms-linear-gradient( top, #003b76, #00376e);
1806 background-image : -ms-linear-gradient( top, #003b76, #00376e);
1807 background-image : -webkit-gradient( linear, left top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e));
1807 background-image : -webkit-gradient( linear, left top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e));
1808 background-image : -webkit-linear-gradient( top, #003b76, #00376e));
1808 background-image : -webkit-linear-gradient( top, #003b76, #00376e));
1809 background-image : -o-linear-gradient( top, #003b76, #00376e));
1809 background-image : -o-linear-gradient( top, #003b76, #00376e));
1810 background-image : linear-gradient( top, #003b76, #00376e);
1810 background-image : linear-gradient( top, #003b76, #00376e);
1811 filter :progid : DXImageTransform.Microsoft.gradient ( startColorstr = '#003b76', endColorstr = '#00376e', GradientType = 0);
1811 filter :progid : DXImageTransform.Microsoft.gradient ( startColorstr = '#003b76', endColorstr = '#00376e', GradientType = 0);
1812 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1812 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1813 -webkit-border-radius: 4px 4px 4px 4px;
1813 -webkit-border-radius: 4px 4px 4px 4px;
1814 -khtml-border-radius: 4px 4px 4px 4px;
1814 -khtml-border-radius: 4px 4px 4px 4px;
1815 -moz-border-radius: 4px 4px 4px 4px;
1815 -moz-border-radius: 4px 4px 4px 4px;
1816 border-radius: 4px 4px 4px 4px;
1816 border-radius: 4px 4px 4px 4px;
1817 }
1817 }
1818
1818
1819 #footer div#footer-inner p {
1819 #footer div#footer-inner p {
1820 padding: 15px 25px 15px 0;
1820 padding: 15px 25px 15px 0;
1821 color: #FFF;
1821 color: #FFF;
1822 font-weight: 700;
1822 font-weight: 700;
1823 }
1823 }
1824
1824
1825 #footer div#footer-inner .footer-link {
1825 #footer div#footer-inner .footer-link {
1826 float: left;
1826 float: left;
1827 padding-left: 10px;
1827 padding-left: 10px;
1828 }
1828 }
1829
1829
1830 #footer div#footer-inner .footer-link a,#footer div#footer-inner .footer-link-right a
1830 #footer div#footer-inner .footer-link a,#footer div#footer-inner .footer-link-right a
1831 {
1831 {
1832 color: #FFF;
1832 color: #FFF;
1833 }
1833 }
1834
1834
1835 #login div.title {
1835 #login div.title {
1836 width: 420px;
1836 width: 420px;
1837 clear: both;
1837 clear: both;
1838 overflow: hidden;
1838 overflow: hidden;
1839 position: relative;
1839 position: relative;
1840 background-color: #003B76;
1840 background-color: #003B76;
1841 background-repeat : repeat-x;
1841 background-repeat : repeat-x;
1842 background-image : -khtml-gradient( linear, left top, left bottom, from(#003B76), to(#00376E));
1842 background-image : -khtml-gradient( linear, left top, left bottom, from(#003B76), to(#00376E));
1843 background-image : -moz-linear-gradient( top, #003b76, #00376e);
1843 background-image : -moz-linear-gradient( top, #003b76, #00376e);
1844 background-image : -ms-linear-gradient( top, #003b76, #00376e);
1844 background-image : -ms-linear-gradient( top, #003b76, #00376e);
1845 background-image : -webkit-gradient( linear, left top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e));
1845 background-image : -webkit-gradient( linear, left top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e));
1846 background-image : -webkit-linear-gradient( top, #003b76, #00376e));
1846 background-image : -webkit-linear-gradient( top, #003b76, #00376e));
1847 background-image : -o-linear-gradient( top, #003b76, #00376e));
1847 background-image : -o-linear-gradient( top, #003b76, #00376e));
1848 background-image : linear-gradient( top, #003b76, #00376e);
1848 background-image : linear-gradient( top, #003b76, #00376e);
1849 filter : progid : DXImageTransform.Microsoft.gradient ( startColorstr = '#003b76', endColorstr = '#00376e', GradientType = 0);
1849 filter : progid : DXImageTransform.Microsoft.gradient ( startColorstr = '#003b76', endColorstr = '#00376e', GradientType = 0);
1850 margin: 0 auto;
1850 margin: 0 auto;
1851 padding: 0;
1851 padding: 0;
1852 }
1852 }
1853
1853
1854 #login div.inner {
1854 #login div.inner {
1855 width: 380px;
1855 width: 380px;
1856 background: #FFF url("../images/login.png") no-repeat top left;
1856 background: #FFF url("../images/login.png") no-repeat top left;
1857 border-top: none;
1857 border-top: none;
1858 border-bottom: none;
1858 border-bottom: none;
1859 margin: 0 auto;
1859 margin: 0 auto;
1860 padding: 20px;
1860 padding: 20px;
1861 }
1861 }
1862
1862
1863 #login div.form div.fields div.field div.label {
1863 #login div.form div.fields div.field div.label {
1864 width: 173px;
1864 width: 173px;
1865 float: left;
1865 float: left;
1866 text-align: right;
1866 text-align: right;
1867 margin: 2px 10px 0 0;
1867 margin: 2px 10px 0 0;
1868 padding: 5px 0 0 5px;
1868 padding: 5px 0 0 5px;
1869 }
1869 }
1870
1870
1871 #login div.form div.fields div.field div.input input {
1871 #login div.form div.fields div.field div.input input {
1872 width: 176px;
1872 width: 176px;
1873 background: #FFF;
1873 background: #FFF;
1874 border-top: 1px solid #b3b3b3;
1874 border-top: 1px solid #b3b3b3;
1875 border-left: 1px solid #b3b3b3;
1875 border-left: 1px solid #b3b3b3;
1876 border-right: 1px solid #eaeaea;
1876 border-right: 1px solid #eaeaea;
1877 border-bottom: 1px solid #eaeaea;
1877 border-bottom: 1px solid #eaeaea;
1878 color: #000;
1878 color: #000;
1879 font-size: 11px;
1879 font-size: 11px;
1880 margin: 0;
1880 margin: 0;
1881 padding: 7px 7px 6px;
1881 padding: 7px 7px 6px;
1882 }
1882 }
1883
1883
1884 #login div.form div.fields div.buttons {
1884 #login div.form div.fields div.buttons {
1885 clear: both;
1885 clear: both;
1886 overflow: hidden;
1886 overflow: hidden;
1887 border-top: 1px solid #DDD;
1887 border-top: 1px solid #DDD;
1888 text-align: right;
1888 text-align: right;
1889 margin: 0;
1889 margin: 0;
1890 padding: 10px 0 0;
1890 padding: 10px 0 0;
1891 }
1891 }
1892
1892
1893 #login div.form div.links {
1893 #login div.form div.links {
1894 clear: both;
1894 clear: both;
1895 overflow: hidden;
1895 overflow: hidden;
1896 margin: 10px 0 0;
1896 margin: 10px 0 0;
1897 padding: 0 0 2px;
1897 padding: 0 0 2px;
1898 }
1898 }
1899
1899
1900 .user-menu{
1900 .user-menu{
1901 margin: 0px !important;
1901 margin: 0px !important;
1902 float: left;
1902 float: left;
1903 }
1903 }
1904
1904
1905 .user-menu .container{
1905 .user-menu .container{
1906 padding:0px 4px 0px 4px;
1906 padding:0px 4px 0px 4px;
1907 margin: 0px 0px 0px 0px;
1907 margin: 0px 0px 0px 0px;
1908 }
1908 }
1909
1909
1910 .user-menu .gravatar{
1910 .user-menu .gravatar{
1911 margin: 0px 0px 0px 0px;
1911 margin: 0px 0px 0px 0px;
1912 cursor: pointer;
1912 cursor: pointer;
1913 }
1913 }
1914 .user-menu .gravatar.enabled{
1914 .user-menu .gravatar.enabled{
1915 background-color: #FDF784 !important;
1915 background-color: #FDF784 !important;
1916 }
1916 }
1917 .user-menu .gravatar:hover{
1917 .user-menu .gravatar:hover{
1918 background-color: #FDF784 !important;
1918 background-color: #FDF784 !important;
1919 }
1919 }
1920 #quick_login{
1920 #quick_login{
1921 min-height: 80px;
1921 min-height: 80px;
1922 margin: 37px 0 0 -251px;
1922 margin: 37px 0 0 -251px;
1923 padding: 4px;
1923 padding: 4px;
1924 position: absolute;
1924 position: absolute;
1925 width: 278px;
1925 width: 278px;
1926
1926
1927 background-repeat: repeat-x;
1927 background-repeat: repeat-x;
1928 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
1928 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
1929 background-image: -moz-linear-gradient(top, #003b76, #00376e);
1929 background-image: -moz-linear-gradient(top, #003b76, #00376e);
1930 background-image: -ms-linear-gradient(top, #003b76, #00376e);
1930 background-image: -ms-linear-gradient(top, #003b76, #00376e);
1931 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
1931 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
1932 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
1932 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
1933 background-image: -o-linear-gradient(top, #003b76, #00376e);
1933 background-image: -o-linear-gradient(top, #003b76, #00376e);
1934 background-image: linear-gradient(top, #003b76, #00376e);
1934 background-image: linear-gradient(top, #003b76, #00376e);
1935 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', endColorstr='#00376e', GradientType=0 );
1935 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', endColorstr='#00376e', GradientType=0 );
1936
1936
1937 z-index: 999;
1937 z-index: 999;
1938 -webkit-border-radius: 0px 0px 4px 4px;
1938 -webkit-border-radius: 0px 0px 4px 4px;
1939 -khtml-border-radius: 0px 0px 4px 4px;
1939 -khtml-border-radius: 0px 0px 4px 4px;
1940 -moz-border-radius: 0px 0px 4px 4px;
1940 -moz-border-radius: 0px 0px 4px 4px;
1941 border-radius: 0px 0px 4px 4px;
1941 border-radius: 0px 0px 4px 4px;
1942 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1942 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
1943 }
1943 }
1944 #quick_login h4{
1944 #quick_login h4{
1945 color: #fff;
1945 color: #fff;
1946 padding: 5px 0px 5px 14px;
1946 padding: 5px 0px 5px 14px;
1947 }
1947 }
1948
1948
1949 #quick_login .password_forgoten {
1949 #quick_login .password_forgoten {
1950 padding-right: 10px;
1950 padding-right: 10px;
1951 padding-top: 0px;
1951 padding-top: 0px;
1952 text-align: left;
1952 text-align: left;
1953 }
1953 }
1954
1954
1955 #quick_login .password_forgoten a {
1955 #quick_login .password_forgoten a {
1956 font-size: 10px;
1956 font-size: 10px;
1957 color: #fff;
1957 color: #fff;
1958 }
1958 }
1959
1959
1960 #quick_login .register {
1960 #quick_login .register {
1961 padding-right: 10px;
1961 padding-right: 10px;
1962 padding-top: 5px;
1962 padding-top: 5px;
1963 text-align: left;
1963 text-align: left;
1964 }
1964 }
1965
1965
1966 #quick_login .register a {
1966 #quick_login .register a {
1967 font-size: 10px;
1967 font-size: 10px;
1968 color: #fff;
1968 color: #fff;
1969 }
1969 }
1970
1970
1971 #quick_login .submit {
1971 #quick_login .submit {
1972 margin: -20px 0 0 0px;
1972 margin: -20px 0 0 0px;
1973 position: absolute;
1973 position: absolute;
1974 right: 15px;
1974 right: 15px;
1975 }
1975 }
1976
1976
1977 #quick_login .links_left{
1977 #quick_login .links_left{
1978 float: left;
1978 float: left;
1979 }
1979 }
1980 #quick_login .links_right{
1980 #quick_login .links_right{
1981 float: right;
1981 float: right;
1982 }
1982 }
1983 #quick_login .full_name{
1983 #quick_login .full_name{
1984 color: #FFFFFF;
1984 color: #FFFFFF;
1985 font-weight: bold;
1985 font-weight: bold;
1986 padding: 3px;
1986 padding: 3px;
1987 }
1987 }
1988 #quick_login .big_gravatar{
1988 #quick_login .big_gravatar{
1989 padding:4px 0px 0px 6px;
1989 padding:4px 0px 0px 6px;
1990 }
1990 }
1991 #quick_login .inbox{
1991 #quick_login .inbox{
1992 padding:4px 0px 0px 6px;
1992 padding:4px 0px 0px 6px;
1993 color: #FFFFFF;
1993 color: #FFFFFF;
1994 font-weight: bold;
1994 font-weight: bold;
1995 }
1995 }
1996 #quick_login .inbox a{
1996 #quick_login .inbox a{
1997 color: #FFFFFF;
1997 color: #FFFFFF;
1998 }
1998 }
1999 #quick_login .email,#quick_login .email a{
1999 #quick_login .email,#quick_login .email a{
2000 color: #FFFFFF;
2000 color: #FFFFFF;
2001 padding: 3px;
2001 padding: 3px;
2002
2002
2003 }
2003 }
2004 #quick_login .links .logout{
2004 #quick_login .links .logout{
2005
2005
2006 }
2006 }
2007
2007
2008 #quick_login div.form div.fields {
2008 #quick_login div.form div.fields {
2009 padding-top: 2px;
2009 padding-top: 2px;
2010 padding-left: 10px;
2010 padding-left: 10px;
2011 }
2011 }
2012
2012
2013 #quick_login div.form div.fields div.field {
2013 #quick_login div.form div.fields div.field {
2014 padding: 5px;
2014 padding: 5px;
2015 }
2015 }
2016
2016
2017 #quick_login div.form div.fields div.field div.label label {
2017 #quick_login div.form div.fields div.field div.label label {
2018 color: #fff;
2018 color: #fff;
2019 padding-bottom: 3px;
2019 padding-bottom: 3px;
2020 }
2020 }
2021
2021
2022 #quick_login div.form div.fields div.field div.input input {
2022 #quick_login div.form div.fields div.field div.input input {
2023 width: 236px;
2023 width: 236px;
2024 background: #FFF;
2024 background: #FFF;
2025 border-top: 1px solid #b3b3b3;
2025 border-top: 1px solid #b3b3b3;
2026 border-left: 1px solid #b3b3b3;
2026 border-left: 1px solid #b3b3b3;
2027 border-right: 1px solid #eaeaea;
2027 border-right: 1px solid #eaeaea;
2028 border-bottom: 1px solid #eaeaea;
2028 border-bottom: 1px solid #eaeaea;
2029 color: #000;
2029 color: #000;
2030 font-size: 11px;
2030 font-size: 11px;
2031 margin: 0;
2031 margin: 0;
2032 padding: 5px 7px 4px;
2032 padding: 5px 7px 4px;
2033 }
2033 }
2034
2034
2035 #quick_login div.form div.fields div.buttons {
2035 #quick_login div.form div.fields div.buttons {
2036 clear: both;
2036 clear: both;
2037 overflow: hidden;
2037 overflow: hidden;
2038 text-align: right;
2038 text-align: right;
2039 margin: 0;
2039 margin: 0;
2040 padding: 5px 14px 0px 5px;
2040 padding: 5px 14px 0px 5px;
2041 }
2041 }
2042
2042
2043 #quick_login div.form div.links {
2043 #quick_login div.form div.links {
2044 clear: both;
2044 clear: both;
2045 overflow: hidden;
2045 overflow: hidden;
2046 margin: 10px 0 0;
2046 margin: 10px 0 0;
2047 padding: 0 0 2px;
2047 padding: 0 0 2px;
2048 }
2048 }
2049
2049
2050 #quick_login ol.links{
2050 #quick_login ol.links{
2051 display: block;
2051 display: block;
2052 font-weight: bold;
2052 font-weight: bold;
2053 list-style: none outside none;
2053 list-style: none outside none;
2054 text-align: right;
2054 text-align: right;
2055 }
2055 }
2056 #quick_login ol.links li{
2056 #quick_login ol.links li{
2057 line-height: 27px;
2057 line-height: 27px;
2058 margin: 0;
2058 margin: 0;
2059 padding: 0;
2059 padding: 0;
2060 color: #fff;
2060 color: #fff;
2061 display: block;
2061 display: block;
2062 float:none !important;
2062 float:none !important;
2063 }
2063 }
2064
2064
2065 #quick_login ol.links li a{
2065 #quick_login ol.links li a{
2066 color: #fff;
2066 color: #fff;
2067 display: block;
2067 display: block;
2068 padding: 2px;
2068 padding: 2px;
2069 }
2069 }
2070 #quick_login ol.links li a:HOVER{
2070 #quick_login ol.links li a:HOVER{
2071 background-color: inherit !important;
2071 background-color: inherit !important;
2072 }
2072 }
2073
2073
2074 #register div.title {
2074 #register div.title {
2075 clear: both;
2075 clear: both;
2076 overflow: hidden;
2076 overflow: hidden;
2077 position: relative;
2077 position: relative;
2078 background-color: #003B76;
2078 background-color: #003B76;
2079 background-repeat: repeat-x;
2079 background-repeat: repeat-x;
2080 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
2080 background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
2081 background-image: -moz-linear-gradient(top, #003b76, #00376e);
2081 background-image: -moz-linear-gradient(top, #003b76, #00376e);
2082 background-image: -ms-linear-gradient(top, #003b76, #00376e);
2082 background-image: -ms-linear-gradient(top, #003b76, #00376e);
2083 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
2083 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
2084 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
2084 background-image: -webkit-linear-gradient(top, #003b76, #00376e);
2085 background-image: -o-linear-gradient(top, #003b76, #00376e);
2085 background-image: -o-linear-gradient(top, #003b76, #00376e);
2086 background-image: linear-gradient(top, #003b76, #00376e);
2086 background-image: linear-gradient(top, #003b76, #00376e);
2087 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',
2087 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',
2088 endColorstr='#00376e', GradientType=0 );
2088 endColorstr='#00376e', GradientType=0 );
2089 margin: 0 auto;
2089 margin: 0 auto;
2090 padding: 0;
2090 padding: 0;
2091 }
2091 }
2092
2092
2093 #register div.inner {
2093 #register div.inner {
2094 background: #FFF;
2094 background: #FFF;
2095 border-top: none;
2095 border-top: none;
2096 border-bottom: none;
2096 border-bottom: none;
2097 margin: 0 auto;
2097 margin: 0 auto;
2098 padding: 20px;
2098 padding: 20px;
2099 }
2099 }
2100
2100
2101 #register div.form div.fields div.field div.label {
2101 #register div.form div.fields div.field div.label {
2102 width: 135px;
2102 width: 135px;
2103 float: left;
2103 float: left;
2104 text-align: right;
2104 text-align: right;
2105 margin: 2px 10px 0 0;
2105 margin: 2px 10px 0 0;
2106 padding: 5px 0 0 5px;
2106 padding: 5px 0 0 5px;
2107 }
2107 }
2108
2108
2109 #register div.form div.fields div.field div.input input {
2109 #register div.form div.fields div.field div.input input {
2110 width: 300px;
2110 width: 300px;
2111 background: #FFF;
2111 background: #FFF;
2112 border-top: 1px solid #b3b3b3;
2112 border-top: 1px solid #b3b3b3;
2113 border-left: 1px solid #b3b3b3;
2113 border-left: 1px solid #b3b3b3;
2114 border-right: 1px solid #eaeaea;
2114 border-right: 1px solid #eaeaea;
2115 border-bottom: 1px solid #eaeaea;
2115 border-bottom: 1px solid #eaeaea;
2116 color: #000;
2116 color: #000;
2117 font-size: 11px;
2117 font-size: 11px;
2118 margin: 0;
2118 margin: 0;
2119 padding: 7px 7px 6px;
2119 padding: 7px 7px 6px;
2120 }
2120 }
2121
2121
2122 #register div.form div.fields div.buttons {
2122 #register div.form div.fields div.buttons {
2123 clear: both;
2123 clear: both;
2124 overflow: hidden;
2124 overflow: hidden;
2125 border-top: 1px solid #DDD;
2125 border-top: 1px solid #DDD;
2126 text-align: left;
2126 text-align: left;
2127 margin: 0;
2127 margin: 0;
2128 padding: 10px 0 0 150px;
2128 padding: 10px 0 0 150px;
2129 }
2129 }
2130
2130
2131 #register div.form div.activation_msg {
2131 #register div.form div.activation_msg {
2132 padding-top: 4px;
2132 padding-top: 4px;
2133 padding-bottom: 4px;
2133 padding-bottom: 4px;
2134 }
2134 }
2135
2135
2136 #journal .journal_day {
2136 #journal .journal_day {
2137 font-size: 20px;
2137 font-size: 20px;
2138 padding: 10px 0px;
2138 padding: 10px 0px;
2139 border-bottom: 2px solid #DDD;
2139 border-bottom: 2px solid #DDD;
2140 margin-left: 10px;
2140 margin-left: 10px;
2141 margin-right: 10px;
2141 margin-right: 10px;
2142 }
2142 }
2143
2143
2144 #journal .journal_container {
2144 #journal .journal_container {
2145 padding: 5px;
2145 padding: 5px;
2146 clear: both;
2146 clear: both;
2147 margin: 0px 5px 0px 10px;
2147 margin: 0px 5px 0px 10px;
2148 }
2148 }
2149
2149
2150 #journal .journal_action_container {
2150 #journal .journal_action_container {
2151 padding-left: 38px;
2151 padding-left: 38px;
2152 }
2152 }
2153
2153
2154 #journal .journal_user {
2154 #journal .journal_user {
2155 color: #747474;
2155 color: #747474;
2156 font-size: 14px;
2156 font-size: 14px;
2157 font-weight: bold;
2157 font-weight: bold;
2158 height: 30px;
2158 height: 30px;
2159 }
2159 }
2160
2160
2161 #journal .journal_icon {
2161 #journal .journal_icon {
2162 clear: both;
2162 clear: both;
2163 float: left;
2163 float: left;
2164 padding-right: 4px;
2164 padding-right: 4px;
2165 padding-top: 3px;
2165 padding-top: 3px;
2166 }
2166 }
2167
2167
2168 #journal .journal_action {
2168 #journal .journal_action {
2169 padding-top: 4px;
2169 padding-top: 4px;
2170 min-height: 2px;
2170 min-height: 2px;
2171 float: left
2171 float: left
2172 }
2172 }
2173
2173
2174 #journal .journal_action_params {
2174 #journal .journal_action_params {
2175 clear: left;
2175 clear: left;
2176 padding-left: 22px;
2176 padding-left: 22px;
2177 }
2177 }
2178
2178
2179 #journal .journal_repo {
2179 #journal .journal_repo {
2180 float: left;
2180 float: left;
2181 margin-left: 6px;
2181 margin-left: 6px;
2182 padding-top: 3px;
2182 padding-top: 3px;
2183 }
2183 }
2184
2184
2185 #journal .date {
2185 #journal .date {
2186 clear: both;
2186 clear: both;
2187 color: #777777;
2187 color: #777777;
2188 font-size: 11px;
2188 font-size: 11px;
2189 padding-left: 22px;
2189 padding-left: 22px;
2190 }
2190 }
2191
2191
2192 #journal .journal_repo .journal_repo_name {
2192 #journal .journal_repo .journal_repo_name {
2193 font-weight: bold;
2193 font-weight: bold;
2194 font-size: 1.1em;
2194 font-size: 1.1em;
2195 }
2195 }
2196
2196
2197 #journal .compare_view {
2197 #journal .compare_view {
2198 padding: 5px 0px 5px 0px;
2198 padding: 5px 0px 5px 0px;
2199 width: 95px;
2199 width: 95px;
2200 }
2200 }
2201
2201
2202 .journal_highlight {
2202 .journal_highlight {
2203 font-weight: bold;
2203 font-weight: bold;
2204 padding: 0 2px;
2204 padding: 0 2px;
2205 vertical-align: bottom;
2205 vertical-align: bottom;
2206 }
2206 }
2207
2207
2208 .trending_language_tbl,.trending_language_tbl td {
2208 .trending_language_tbl,.trending_language_tbl td {
2209 border: 0 !important;
2209 border: 0 !important;
2210 margin: 0 !important;
2210 margin: 0 !important;
2211 padding: 0 !important;
2211 padding: 0 !important;
2212 }
2212 }
2213
2213
2214 .trending_language_tbl,.trending_language_tbl tr {
2214 .trending_language_tbl,.trending_language_tbl tr {
2215 border-spacing: 1px;
2215 border-spacing: 1px;
2216 }
2216 }
2217
2217
2218 .trending_language {
2218 .trending_language {
2219 background-color: #003367;
2219 background-color: #003367;
2220 color: #FFF;
2220 color: #FFF;
2221 display: block;
2221 display: block;
2222 min-width: 20px;
2222 min-width: 20px;
2223 text-decoration: none;
2223 text-decoration: none;
2224 height: 12px;
2224 height: 12px;
2225 margin-bottom: 0px;
2225 margin-bottom: 0px;
2226 margin-left: 5px;
2226 margin-left: 5px;
2227 white-space: pre;
2227 white-space: pre;
2228 padding: 3px;
2228 padding: 3px;
2229 }
2229 }
2230
2230
2231 h3.files_location {
2231 h3.files_location {
2232 font-size: 1.8em;
2232 font-size: 1.8em;
2233 font-weight: 700;
2233 font-weight: 700;
2234 border-bottom: none !important;
2234 border-bottom: none !important;
2235 margin: 10px 0 !important;
2235 margin: 10px 0 !important;
2236 }
2236 }
2237
2237
2238 #files_data dl dt {
2238 #files_data dl dt {
2239 float: left;
2239 float: left;
2240 width: 60px;
2240 width: 60px;
2241 margin: 0 !important;
2241 margin: 0 !important;
2242 padding: 5px;
2242 padding: 5px;
2243 }
2243 }
2244
2244
2245 #files_data dl dd {
2245 #files_data dl dd {
2246 margin: 0 !important;
2246 margin: 0 !important;
2247 padding: 5px !important;
2247 padding: 5px !important;
2248 }
2248 }
2249
2249
2250 .tablerow0 {
2250 .tablerow0 {
2251 background-color: #F8F8F8;
2251 background-color: #F8F8F8;
2252 }
2252 }
2253
2253
2254 .tablerow1 {
2254 .tablerow1 {
2255 background-color: #FFFFFF;
2255 background-color: #FFFFFF;
2256 }
2256 }
2257
2257
2258 .changeset_id {
2258 .changeset_id {
2259 font-family: monospace;
2259 font-family: monospace;
2260 color: #666666;
2260 color: #666666;
2261 }
2261 }
2262
2262
2263 .changeset_hash {
2263 .changeset_hash {
2264 color: #000000;
2264 color: #000000;
2265 }
2265 }
2266
2266
2267 #changeset_content {
2267 #changeset_content {
2268 border-left: 1px solid #CCC;
2268 border-left: 1px solid #CCC;
2269 border-right: 1px solid #CCC;
2269 border-right: 1px solid #CCC;
2270 border-bottom: 1px solid #CCC;
2270 border-bottom: 1px solid #CCC;
2271 padding: 5px;
2271 padding: 5px;
2272 }
2272 }
2273
2273
2274 #changeset_compare_view_content {
2274 #changeset_compare_view_content {
2275 border: 1px solid #CCC;
2275 border: 1px solid #CCC;
2276 padding: 5px;
2276 padding: 5px;
2277 }
2277 }
2278
2278
2279 #changeset_content .container {
2279 #changeset_content .container {
2280 min-height: 100px;
2280 min-height: 100px;
2281 font-size: 1.2em;
2281 font-size: 1.2em;
2282 overflow: hidden;
2282 overflow: hidden;
2283 }
2283 }
2284
2284
2285 #changeset_compare_view_content .compare_view_commits {
2285 #changeset_compare_view_content .compare_view_commits {
2286 width: auto !important;
2286 width: auto !important;
2287 }
2287 }
2288
2288
2289 #changeset_compare_view_content .compare_view_commits td {
2289 #changeset_compare_view_content .compare_view_commits td {
2290 padding: 0px 0px 0px 12px !important;
2290 padding: 0px 0px 0px 12px !important;
2291 }
2291 }
2292
2292
2293 #changeset_content .container .right {
2293 #changeset_content .container .right {
2294 float: right;
2294 float: right;
2295 width: 20%;
2295 width: 20%;
2296 text-align: right;
2296 text-align: right;
2297 }
2297 }
2298
2298
2299 #changeset_content .container .left .message {
2299 #changeset_content .container .left .message {
2300 white-space: pre-wrap;
2300 white-space: pre-wrap;
2301 }
2301 }
2302 #changeset_content .container .left .message a:hover {
2302 #changeset_content .container .left .message a:hover {
2303 text-decoration: none;
2303 text-decoration: none;
2304 }
2304 }
2305 .cs_files .cur_cs {
2305 .cs_files .cur_cs {
2306 margin: 10px 2px;
2306 margin: 10px 2px;
2307 font-weight: bold;
2307 font-weight: bold;
2308 }
2308 }
2309
2309
2310 .cs_files .node {
2310 .cs_files .node {
2311 float: left;
2311 float: left;
2312 }
2312 }
2313
2313
2314 .cs_files .changes {
2314 .cs_files .changes {
2315 float: right;
2315 float: right;
2316 color:#003367;
2316 color:#003367;
2317
2317
2318 }
2318 }
2319
2319
2320 .cs_files .changes .added {
2320 .cs_files .changes .added {
2321 background-color: #BBFFBB;
2321 background-color: #BBFFBB;
2322 float: left;
2322 float: left;
2323 text-align: center;
2323 text-align: center;
2324 font-size: 9px;
2324 font-size: 9px;
2325 padding: 2px 0px 2px 0px;
2325 padding: 2px 0px 2px 0px;
2326 }
2326 }
2327
2327
2328 .cs_files .changes .deleted {
2328 .cs_files .changes .deleted {
2329 background-color: #FF8888;
2329 background-color: #FF8888;
2330 float: left;
2330 float: left;
2331 text-align: center;
2331 text-align: center;
2332 font-size: 9px;
2332 font-size: 9px;
2333 padding: 2px 0px 2px 0px;
2333 padding: 2px 0px 2px 0px;
2334 }
2334 }
2335
2335
2336 .cs_files .cs_added {
2336 .cs_files .cs_added {
2337 background: url("../images/icons/page_white_add.png") no-repeat scroll
2337 background: url("../images/icons/page_white_add.png") no-repeat scroll
2338 3px;
2338 3px;
2339 height: 16px;
2339 height: 16px;
2340 padding-left: 20px;
2340 padding-left: 20px;
2341 margin-top: 7px;
2341 margin-top: 7px;
2342 text-align: left;
2342 text-align: left;
2343 }
2343 }
2344
2344
2345 .cs_files .cs_changed {
2345 .cs_files .cs_changed {
2346 background: url("../images/icons/page_white_edit.png") no-repeat scroll
2346 background: url("../images/icons/page_white_edit.png") no-repeat scroll
2347 3px;
2347 3px;
2348 height: 16px;
2348 height: 16px;
2349 padding-left: 20px;
2349 padding-left: 20px;
2350 margin-top: 7px;
2350 margin-top: 7px;
2351 text-align: left;
2351 text-align: left;
2352 }
2352 }
2353
2353
2354 .cs_files .cs_removed {
2354 .cs_files .cs_removed {
2355 background: url("../images/icons/page_white_delete.png") no-repeat
2355 background: url("../images/icons/page_white_delete.png") no-repeat
2356 scroll 3px;
2356 scroll 3px;
2357 height: 16px;
2357 height: 16px;
2358 padding-left: 20px;
2358 padding-left: 20px;
2359 margin-top: 7px;
2359 margin-top: 7px;
2360 text-align: left;
2360 text-align: left;
2361 }
2361 }
2362
2362
2363 #graph {
2363 #graph {
2364 overflow: hidden;
2364 overflow: hidden;
2365 }
2365 }
2366
2366
2367 #graph_nodes {
2367 #graph_nodes {
2368 float: left;
2368 float: left;
2369 margin-right: -6px;
2369 margin-right: -6px;
2370 margin-top: 0px;
2370 margin-top: 0px;
2371 }
2371 }
2372
2372
2373 #graph_content {
2373 #graph_content {
2374 width: 80%;
2374 width: 80%;
2375 float: left;
2375 float: left;
2376 }
2376 }
2377
2377
2378 #graph_content .container_header {
2378 #graph_content .container_header {
2379 border-bottom: 1px solid #DDD;
2379 border-bottom: 1px solid #DDD;
2380 padding: 10px;
2380 padding: 10px;
2381 height: 25px;
2381 height: 25px;
2382 }
2382 }
2383
2383
2384 #graph_content #rev_range_container {
2384 #graph_content #rev_range_container {
2385 padding: 7px 20px;
2385 padding: 7px 20px;
2386 float: left;
2386 float: left;
2387 }
2387 }
2388
2388
2389 #graph_content .container {
2389 #graph_content .container {
2390 border-bottom: 1px solid #DDD;
2390 border-bottom: 1px solid #DDD;
2391 height: 56px;
2391 height: 56px;
2392 overflow: hidden;
2392 overflow: hidden;
2393 }
2393 }
2394
2394
2395 #graph_content .container .right {
2395 #graph_content .container .right {
2396 float: right;
2396 float: right;
2397 width: 23%;
2397 width: 23%;
2398 text-align: right;
2398 text-align: right;
2399 }
2399 }
2400
2400
2401 #graph_content .container .left {
2401 #graph_content .container .left {
2402 float: left;
2402 float: left;
2403 width: 25%;
2403 width: 25%;
2404 padding-left: 5px;
2404 padding-left: 5px;
2405 }
2405 }
2406
2406
2407 #graph_content .container .mid {
2407 #graph_content .container .mid {
2408 float: left;
2408 float: left;
2409 width: 49%;
2409 width: 49%;
2410 }
2410 }
2411
2411
2412
2412
2413 #graph_content .container .left .date {
2413 #graph_content .container .left .date {
2414 color: #666;
2414 color: #666;
2415 padding-left: 22px;
2415 padding-left: 22px;
2416 font-size: 10px;
2416 font-size: 10px;
2417 }
2417 }
2418
2418
2419 #graph_content .container .left .author {
2419 #graph_content .container .left .author {
2420 height: 22px;
2420 height: 22px;
2421 }
2421 }
2422
2422
2423 #graph_content .container .left .author .user {
2423 #graph_content .container .left .author .user {
2424 color: #444444;
2424 color: #444444;
2425 float: left;
2425 float: left;
2426 margin-left: -4px;
2426 margin-left: -4px;
2427 margin-top: 4px;
2427 margin-top: 4px;
2428 }
2428 }
2429
2429
2430 #graph_content .container .mid .message {
2430 #graph_content .container .mid .message {
2431 white-space: pre-wrap;
2431 white-space: pre-wrap;
2432 }
2432 }
2433
2433
2434 #graph_content .container .mid .message a:hover{
2434 #graph_content .container .mid .message a:hover{
2435 text-decoration: none;
2435 text-decoration: none;
2436 }
2436 }
2437 #content #graph_content .message .revision-link,
2437 #content #graph_content .message .revision-link,
2438 #changeset_content .container .message .revision-link
2438 #changeset_content .container .message .revision-link
2439 {
2439 {
2440 color:#3F6F9F;
2440 color:#3F6F9F;
2441 font-weight: bold !important;
2441 font-weight: bold !important;
2442 }
2442 }
2443
2443
2444 #content #graph_content .message .issue-tracker-link,
2444 #content #graph_content .message .issue-tracker-link,
2445 #changeset_content .container .message .issue-tracker-link{
2445 #changeset_content .container .message .issue-tracker-link{
2446 color:#3F6F9F;
2446 color:#3F6F9F;
2447 font-weight: bold !important;
2447 font-weight: bold !important;
2448 }
2448 }
2449
2449
2450 .right .changeset-status-container{
2450 .right .changeset-status-container{
2451 padding-right: 5px;
2451 padding-right: 5px;
2452 margin-top:1px;
2452 margin-top:1px;
2453 float:right;
2453 float:right;
2454 height:14px;
2454 height:14px;
2455 }
2455 }
2456 .code-header .changeset-status-container{
2456 .code-header .changeset-status-container{
2457 float:left;
2457 float:left;
2458 padding:2px 0px 0px 2px;
2458 padding:2px 0px 0px 2px;
2459 }
2459 }
2460 .right .changeset-status-container .changeset-status-lbl{
2460 .right .changeset-status-container .changeset-status-lbl{
2461 color: rgb(136, 136, 136);
2461 color: rgb(136, 136, 136);
2462 float: left;
2462 float: left;
2463 padding: 0px 4px 0px 0px;
2463 padding: 0px 4px 0px 0px;
2464 }
2464 }
2465 .code-header .changeset-status-container .changeset-status-lbl{
2465 .code-header .changeset-status-container .changeset-status-lbl{
2466 float: left;
2466 float: left;
2467 padding: 0px 4px 0px 0px;
2467 padding: 0px 4px 0px 0px;
2468 }
2468 }
2469 .right .changeset-status-container .changeset-status-ico{
2469 .right .changeset-status-container .changeset-status-ico{
2470 float: left;
2470 float: left;
2471 }
2471 }
2472 .code-header .changeset-status-container .changeset-status-ico, .container .changeset-status-ico{
2472 .code-header .changeset-status-container .changeset-status-ico, .container .changeset-status-ico{
2473 float: left;
2473 float: left;
2474 }
2474 }
2475 .right .comments-container{
2475 .right .comments-container{
2476 padding-right: 5px;
2476 padding-right: 5px;
2477 margin-top:1px;
2477 margin-top:1px;
2478 float:right;
2478 float:right;
2479 height:14px;
2479 height:14px;
2480 }
2480 }
2481
2481
2482 .right .comments-cnt{
2482 .right .comments-cnt{
2483 float: left;
2483 float: left;
2484 color: rgb(136, 136, 136);
2484 color: rgb(136, 136, 136);
2485 padding-right: 2px;
2485 padding-right: 2px;
2486 }
2486 }
2487
2487
2488 .right .changes{
2488 .right .changes{
2489 clear: both;
2489 clear: both;
2490 }
2490 }
2491
2491
2492 .right .changes .changed_total {
2492 .right .changes .changed_total {
2493 display: block;
2493 display: block;
2494 float: right;
2494 float: right;
2495 text-align: center;
2495 text-align: center;
2496 min-width: 45px;
2496 min-width: 45px;
2497 cursor: pointer;
2497 cursor: pointer;
2498 color: #444444;
2498 color: #444444;
2499 background: #FEA;
2499 background: #FEA;
2500 -webkit-border-radius: 0px 0px 0px 6px;
2500 -webkit-border-radius: 0px 0px 0px 6px;
2501 -moz-border-radius: 0px 0px 0px 6px;
2501 -moz-border-radius: 0px 0px 0px 6px;
2502 border-radius: 0px 0px 0px 6px;
2502 border-radius: 0px 0px 0px 6px;
2503 padding: 1px;
2503 padding: 1px;
2504 }
2504 }
2505
2505
2506 .right .changes .added,.changed,.removed {
2506 .right .changes .added,.changed,.removed {
2507 display: block;
2507 display: block;
2508 padding: 1px;
2508 padding: 1px;
2509 color: #444444;
2509 color: #444444;
2510 float: right;
2510 float: right;
2511 text-align: center;
2511 text-align: center;
2512 min-width: 15px;
2512 min-width: 15px;
2513 }
2513 }
2514
2514
2515 .right .changes .added {
2515 .right .changes .added {
2516 background: #CFC;
2516 background: #CFC;
2517 }
2517 }
2518
2518
2519 .right .changes .changed {
2519 .right .changes .changed {
2520 background: #FEA;
2520 background: #FEA;
2521 }
2521 }
2522
2522
2523 .right .changes .removed {
2523 .right .changes .removed {
2524 background: #FAA;
2524 background: #FAA;
2525 }
2525 }
2526
2526
2527 .right .merge {
2527 .right .merge {
2528 padding: 1px 3px 1px 3px;
2528 padding: 1px 3px 1px 3px;
2529 background-color: #fca062;
2529 background-color: #fca062;
2530 font-size: 10px;
2530 font-size: 10px;
2531 font-weight: bold;
2531 font-weight: bold;
2532 color: #ffffff;
2532 color: #ffffff;
2533 text-transform: uppercase;
2533 text-transform: uppercase;
2534 white-space: nowrap;
2534 white-space: nowrap;
2535 -webkit-border-radius: 3px;
2535 -webkit-border-radius: 3px;
2536 -moz-border-radius: 3px;
2536 -moz-border-radius: 3px;
2537 border-radius: 3px;
2537 border-radius: 3px;
2538 margin-right: 2px;
2538 margin-right: 2px;
2539 }
2539 }
2540
2540
2541 .right .parent {
2541 .right .parent {
2542 color: #666666;
2542 color: #666666;
2543 clear:both;
2543 clear:both;
2544 }
2544 }
2545 .right .logtags{
2545 .right .logtags{
2546 padding: 2px 2px 2px 2px;
2546 padding: 2px 2px 2px 2px;
2547 }
2547 }
2548 .right .logtags .branchtag,.right .logtags .tagtag,.right .logtags .booktag{
2549 margin: 0px 2px;
2550 }
2551
2548 .right .logtags .branchtag,.logtags .branchtag {
2552 .right .logtags .branchtag,.logtags .branchtag {
2549 padding: 1px 3px 1px 3px;
2553 padding: 1px 3px 1px 3px;
2550 background-color: #bfbfbf;
2554 background-color: #bfbfbf;
2551 font-size: 10px;
2555 font-size: 10px;
2552 font-weight: bold;
2556 font-weight: bold;
2553 color: #ffffff;
2557 color: #ffffff;
2554 text-transform: uppercase;
2558 text-transform: uppercase;
2555 white-space: nowrap;
2559 white-space: nowrap;
2556 -webkit-border-radius: 3px;
2560 -webkit-border-radius: 3px;
2557 -moz-border-radius: 3px;
2561 -moz-border-radius: 3px;
2558 border-radius: 3px;
2562 border-radius: 3px;
2559 }
2563 }
2560 .right .logtags .branchtag a:hover,.logtags .branchtag a{
2564 .right .logtags .branchtag a:hover,.logtags .branchtag a{
2561 color: #ffffff;
2565 color: #ffffff;
2562 }
2566 }
2563 .right .logtags .branchtag a:hover,.logtags .branchtag a:hover{
2567 .right .logtags .branchtag a:hover,.logtags .branchtag a:hover{
2564 text-decoration: none;
2568 text-decoration: none;
2565 color: #ffffff;
2569 color: #ffffff;
2566 }
2570 }
2567 .right .logtags .tagtag,.logtags .tagtag {
2571 .right .logtags .tagtag,.logtags .tagtag {
2568 padding: 1px 3px 1px 3px;
2572 padding: 1px 3px 1px 3px;
2569 background-color: #62cffc;
2573 background-color: #62cffc;
2570 font-size: 10px;
2574 font-size: 10px;
2571 font-weight: bold;
2575 font-weight: bold;
2572 color: #ffffff;
2576 color: #ffffff;
2573 text-transform: uppercase;
2577 text-transform: uppercase;
2574 white-space: nowrap;
2578 white-space: nowrap;
2575 -webkit-border-radius: 3px;
2579 -webkit-border-radius: 3px;
2576 -moz-border-radius: 3px;
2580 -moz-border-radius: 3px;
2577 border-radius: 3px;
2581 border-radius: 3px;
2578 }
2582 }
2579 .right .logtags .tagtag a:hover,.logtags .tagtag a{
2583 .right .logtags .tagtag a:hover,.logtags .tagtag a{
2580 color: #ffffff;
2584 color: #ffffff;
2581 }
2585 }
2582 .right .logtags .tagtag a:hover,.logtags .tagtag a:hover{
2586 .right .logtags .tagtag a:hover,.logtags .tagtag a:hover{
2583 text-decoration: none;
2587 text-decoration: none;
2584 color: #ffffff;
2588 color: #ffffff;
2585 }
2589 }
2586 .right .logbooks .bookbook,.logbooks .bookbook {
2590 .right .logbooks .bookbook,.logbooks .bookbook,.right .logtags .bookbook,.logtags .bookbook {
2587 padding: 1px 3px 2px;
2591 padding: 1px 3px 1px 3px;
2588 background-color: #46A546;
2592 background-color: #46A546;
2589 font-size: 9.75px;
2593 font-size: 10px;
2590 font-weight: bold;
2594 font-weight: bold;
2591 color: #ffffff;
2595 color: #ffffff;
2592 text-transform: uppercase;
2596 text-transform: uppercase;
2593 white-space: nowrap;
2597 white-space: nowrap;
2594 -webkit-border-radius: 3px;
2598 -webkit-border-radius: 3px;
2595 -moz-border-radius: 3px;
2599 -moz-border-radius: 3px;
2596 border-radius: 3px;
2600 border-radius: 3px;
2597 }
2601 }
2598 .right .logbooks .bookbook,.logbooks .bookbook a{
2602 .right .logbooks .bookbook,.logbooks .bookbook a,.right .logtags .bookbook,.logtags .bookbook a{
2599 color: #ffffff;
2603 color: #ffffff;
2600 }
2604 }
2601 .right .logbooks .bookbook,.logbooks .bookbook a:hover{
2605 .right .logbooks .bookbook,.logbooks .bookbook a:hover,.right .logtags .bookbook,.logtags .bookbook a:hover{
2602 text-decoration: none;
2606 text-decoration: none;
2603 color: #ffffff;
2607 color: #ffffff;
2604 }
2608 }
2605 div.browserblock {
2609 div.browserblock {
2606 overflow: hidden;
2610 overflow: hidden;
2607 border: 1px solid #ccc;
2611 border: 1px solid #ccc;
2608 background: #f8f8f8;
2612 background: #f8f8f8;
2609 font-size: 100%;
2613 font-size: 100%;
2610 line-height: 125%;
2614 line-height: 125%;
2611 padding: 0;
2615 padding: 0;
2612 -webkit-border-radius: 6px 6px 0px 0px;
2616 -webkit-border-radius: 6px 6px 0px 0px;
2613 -moz-border-radius: 6px 6px 0px 0px;
2617 -moz-border-radius: 6px 6px 0px 0px;
2614 border-radius: 6px 6px 0px 0px;
2618 border-radius: 6px 6px 0px 0px;
2615 }
2619 }
2616
2620
2617 div.browserblock .browser-header {
2621 div.browserblock .browser-header {
2618 background: #FFF;
2622 background: #FFF;
2619 padding: 10px 0px 15px 0px;
2623 padding: 10px 0px 15px 0px;
2620 width: 100%;
2624 width: 100%;
2621 }
2625 }
2622
2626
2623 div.browserblock .browser-nav {
2627 div.browserblock .browser-nav {
2624 float: left
2628 float: left
2625 }
2629 }
2626
2630
2627 div.browserblock .browser-branch {
2631 div.browserblock .browser-branch {
2628 float: left;
2632 float: left;
2629 }
2633 }
2630
2634
2631 div.browserblock .browser-branch label {
2635 div.browserblock .browser-branch label {
2632 color: #4A4A4A;
2636 color: #4A4A4A;
2633 vertical-align: text-top;
2637 vertical-align: text-top;
2634 }
2638 }
2635
2639
2636 div.browserblock .browser-header span {
2640 div.browserblock .browser-header span {
2637 margin-left: 5px;
2641 margin-left: 5px;
2638 font-weight: 700;
2642 font-weight: 700;
2639 }
2643 }
2640
2644
2641 div.browserblock .browser-search {
2645 div.browserblock .browser-search {
2642 clear: both;
2646 clear: both;
2643 padding: 8px 8px 0px 5px;
2647 padding: 8px 8px 0px 5px;
2644 height: 20px;
2648 height: 20px;
2645 }
2649 }
2646
2650
2647 div.browserblock #node_filter_box {
2651 div.browserblock #node_filter_box {
2648
2652
2649 }
2653 }
2650
2654
2651 div.browserblock .search_activate {
2655 div.browserblock .search_activate {
2652 float: left
2656 float: left
2653 }
2657 }
2654
2658
2655 div.browserblock .add_node {
2659 div.browserblock .add_node {
2656 float: left;
2660 float: left;
2657 padding-left: 5px;
2661 padding-left: 5px;
2658 }
2662 }
2659
2663
2660 div.browserblock .search_activate a:hover,div.browserblock .add_node a:hover
2664 div.browserblock .search_activate a:hover,div.browserblock .add_node a:hover
2661 {
2665 {
2662 text-decoration: none !important;
2666 text-decoration: none !important;
2663 }
2667 }
2664
2668
2665 div.browserblock .browser-body {
2669 div.browserblock .browser-body {
2666 background: #EEE;
2670 background: #EEE;
2667 border-top: 1px solid #CCC;
2671 border-top: 1px solid #CCC;
2668 }
2672 }
2669
2673
2670 table.code-browser {
2674 table.code-browser {
2671 border-collapse: collapse;
2675 border-collapse: collapse;
2672 width: 100%;
2676 width: 100%;
2673 }
2677 }
2674
2678
2675 table.code-browser tr {
2679 table.code-browser tr {
2676 margin: 3px;
2680 margin: 3px;
2677 }
2681 }
2678
2682
2679 table.code-browser thead th {
2683 table.code-browser thead th {
2680 background-color: #EEE;
2684 background-color: #EEE;
2681 height: 20px;
2685 height: 20px;
2682 font-size: 1.1em;
2686 font-size: 1.1em;
2683 font-weight: 700;
2687 font-weight: 700;
2684 text-align: left;
2688 text-align: left;
2685 padding-left: 10px;
2689 padding-left: 10px;
2686 }
2690 }
2687
2691
2688 table.code-browser tbody td {
2692 table.code-browser tbody td {
2689 padding-left: 10px;
2693 padding-left: 10px;
2690 height: 20px;
2694 height: 20px;
2691 }
2695 }
2692
2696
2693 table.code-browser .browser-file {
2697 table.code-browser .browser-file {
2694 background: url("../images/icons/document_16.png") no-repeat scroll 3px;
2698 background: url("../images/icons/document_16.png") no-repeat scroll 3px;
2695 height: 16px;
2699 height: 16px;
2696 padding-left: 20px;
2700 padding-left: 20px;
2697 text-align: left;
2701 text-align: left;
2698 }
2702 }
2699 .diffblock .changeset_header {
2703 .diffblock .changeset_header {
2700 height: 16px;
2704 height: 16px;
2701 }
2705 }
2702 .diffblock .changeset_file {
2706 .diffblock .changeset_file {
2703 background: url("../images/icons/file.png") no-repeat scroll 3px;
2707 background: url("../images/icons/file.png") no-repeat scroll 3px;
2704 text-align: left;
2708 text-align: left;
2705 float: left;
2709 float: left;
2706 padding: 2px 0px 2px 22px;
2710 padding: 2px 0px 2px 22px;
2707 }
2711 }
2708 .diffblock .diff-menu-wrapper{
2712 .diffblock .diff-menu-wrapper{
2709 float: left;
2713 float: left;
2710 }
2714 }
2711
2715
2712 .diffblock .diff-menu{
2716 .diffblock .diff-menu{
2713 position: absolute;
2717 position: absolute;
2714 background: none repeat scroll 0 0 #FFFFFF;
2718 background: none repeat scroll 0 0 #FFFFFF;
2715 border-color: #003367 #666666 #666666;
2719 border-color: #003367 #666666 #666666;
2716 border-right: 1px solid #666666;
2720 border-right: 1px solid #666666;
2717 border-style: solid solid solid;
2721 border-style: solid solid solid;
2718 border-width: 1px;
2722 border-width: 1px;
2719 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
2723 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
2720 margin-top:5px;
2724 margin-top:5px;
2721 margin-left:1px;
2725 margin-left:1px;
2722
2726
2723 }
2727 }
2724 .diffblock .diff-actions {
2728 .diffblock .diff-actions {
2725 padding: 2px 0px 0px 2px;
2729 padding: 2px 0px 0px 2px;
2726 float: left;
2730 float: left;
2727 }
2731 }
2728 .diffblock .diff-menu ul li {
2732 .diffblock .diff-menu ul li {
2729 padding: 0px 0px 0px 0px !important;
2733 padding: 0px 0px 0px 0px !important;
2730 }
2734 }
2731 .diffblock .diff-menu ul li a{
2735 .diffblock .diff-menu ul li a{
2732 display: block;
2736 display: block;
2733 padding: 3px 8px 3px 8px !important;
2737 padding: 3px 8px 3px 8px !important;
2734 }
2738 }
2735 .diffblock .diff-menu ul li a:hover{
2739 .diffblock .diff-menu ul li a:hover{
2736 text-decoration: none;
2740 text-decoration: none;
2737 background-color: #EEEEEE;
2741 background-color: #EEEEEE;
2738 }
2742 }
2739 table.code-browser .browser-dir {
2743 table.code-browser .browser-dir {
2740 background: url("../images/icons/folder_16.png") no-repeat scroll 3px;
2744 background: url("../images/icons/folder_16.png") no-repeat scroll 3px;
2741 height: 16px;
2745 height: 16px;
2742 padding-left: 20px;
2746 padding-left: 20px;
2743 text-align: left;
2747 text-align: left;
2744 }
2748 }
2745
2749
2746 table.code-browser .submodule-dir {
2750 table.code-browser .submodule-dir {
2747 background: url("../images/icons/disconnect.png") no-repeat scroll 3px;
2751 background: url("../images/icons/disconnect.png") no-repeat scroll 3px;
2748 height: 16px;
2752 height: 16px;
2749 padding-left: 20px;
2753 padding-left: 20px;
2750 text-align: left;
2754 text-align: left;
2751 }
2755 }
2752
2756
2753
2757
2754 .box .search {
2758 .box .search {
2755 clear: both;
2759 clear: both;
2756 overflow: hidden;
2760 overflow: hidden;
2757 margin: 0;
2761 margin: 0;
2758 padding: 0 20px 10px;
2762 padding: 0 20px 10px;
2759 }
2763 }
2760
2764
2761 .box .search div.search_path {
2765 .box .search div.search_path {
2762 background: none repeat scroll 0 0 #EEE;
2766 background: none repeat scroll 0 0 #EEE;
2763 border: 1px solid #CCC;
2767 border: 1px solid #CCC;
2764 color: blue;
2768 color: blue;
2765 margin-bottom: 10px;
2769 margin-bottom: 10px;
2766 padding: 10px 0;
2770 padding: 10px 0;
2767 }
2771 }
2768
2772
2769 .box .search div.search_path div.link {
2773 .box .search div.search_path div.link {
2770 font-weight: 700;
2774 font-weight: 700;
2771 margin-left: 25px;
2775 margin-left: 25px;
2772 }
2776 }
2773
2777
2774 .box .search div.search_path div.link a {
2778 .box .search div.search_path div.link a {
2775 color: #003367;
2779 color: #003367;
2776 cursor: pointer;
2780 cursor: pointer;
2777 text-decoration: none;
2781 text-decoration: none;
2778 }
2782 }
2779
2783
2780 #path_unlock {
2784 #path_unlock {
2781 color: red;
2785 color: red;
2782 font-size: 1.2em;
2786 font-size: 1.2em;
2783 padding-left: 4px;
2787 padding-left: 4px;
2784 }
2788 }
2785
2789
2786 .info_box span {
2790 .info_box span {
2787 margin-left: 3px;
2791 margin-left: 3px;
2788 margin-right: 3px;
2792 margin-right: 3px;
2789 }
2793 }
2790
2794
2791 .info_box .rev {
2795 .info_box .rev {
2792 color: #003367;
2796 color: #003367;
2793 font-size: 1.6em;
2797 font-size: 1.6em;
2794 font-weight: bold;
2798 font-weight: bold;
2795 vertical-align: sub;
2799 vertical-align: sub;
2796 }
2800 }
2797
2801
2798 .info_box input#at_rev,.info_box input#size {
2802 .info_box input#at_rev,.info_box input#size {
2799 background: #FFF;
2803 background: #FFF;
2800 border-top: 1px solid #b3b3b3;
2804 border-top: 1px solid #b3b3b3;
2801 border-left: 1px solid #b3b3b3;
2805 border-left: 1px solid #b3b3b3;
2802 border-right: 1px solid #eaeaea;
2806 border-right: 1px solid #eaeaea;
2803 border-bottom: 1px solid #eaeaea;
2807 border-bottom: 1px solid #eaeaea;
2804 color: #000;
2808 color: #000;
2805 font-size: 12px;
2809 font-size: 12px;
2806 margin: 0;
2810 margin: 0;
2807 padding: 1px 5px 1px;
2811 padding: 1px 5px 1px;
2808 }
2812 }
2809
2813
2810 .info_box input#view {
2814 .info_box input#view {
2811 text-align: center;
2815 text-align: center;
2812 padding: 4px 3px 2px 2px;
2816 padding: 4px 3px 2px 2px;
2813 }
2817 }
2814
2818
2815 .yui-overlay,.yui-panel-container {
2819 .yui-overlay,.yui-panel-container {
2816 visibility: hidden;
2820 visibility: hidden;
2817 position: absolute;
2821 position: absolute;
2818 z-index: 2;
2822 z-index: 2;
2819 }
2823 }
2820
2824
2821 .yui-tt {
2825 .yui-tt {
2822 visibility: hidden;
2826 visibility: hidden;
2823 position: absolute;
2827 position: absolute;
2824 color: #666;
2828 color: #666;
2825 background-color: #FFF;
2829 background-color: #FFF;
2826 border: 2px solid #003367;
2830 border: 2px solid #003367;
2827 font: 100% sans-serif;
2831 font: 100% sans-serif;
2828 width: auto;
2832 width: auto;
2829 opacity: 1px;
2833 opacity: 1px;
2830 padding: 8px;
2834 padding: 8px;
2831 white-space: pre-wrap;
2835 white-space: pre-wrap;
2832 -webkit-border-radius: 8px 8px 8px 8px;
2836 -webkit-border-radius: 8px 8px 8px 8px;
2833 -khtml-border-radius: 8px 8px 8px 8px;
2837 -khtml-border-radius: 8px 8px 8px 8px;
2834 -moz-border-radius: 8px 8px 8px 8px;
2838 -moz-border-radius: 8px 8px 8px 8px;
2835 border-radius: 8px 8px 8px 8px;
2839 border-radius: 8px 8px 8px 8px;
2836 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
2840 box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
2837 }
2841 }
2838
2842
2839 .ac {
2843 .ac {
2840 vertical-align: top;
2844 vertical-align: top;
2841 }
2845 }
2842
2846
2843 .ac .yui-ac {
2847 .ac .yui-ac {
2844 position: inherit;
2848 position: inherit;
2845 font-size: 100%;
2849 font-size: 100%;
2846 }
2850 }
2847
2851
2848 .ac .perm_ac {
2852 .ac .perm_ac {
2849 width: 20em;
2853 width: 20em;
2850 }
2854 }
2851
2855
2852 .ac .yui-ac-input {
2856 .ac .yui-ac-input {
2853 width: 100%;
2857 width: 100%;
2854 }
2858 }
2855
2859
2856 .ac .yui-ac-container {
2860 .ac .yui-ac-container {
2857 position: absolute;
2861 position: absolute;
2858 top: 1.6em;
2862 top: 1.6em;
2859 width: auto;
2863 width: auto;
2860 }
2864 }
2861
2865
2862 .ac .yui-ac-content {
2866 .ac .yui-ac-content {
2863 position: absolute;
2867 position: absolute;
2864 border: 1px solid gray;
2868 border: 1px solid gray;
2865 background: #fff;
2869 background: #fff;
2866 z-index: 9050;
2870 z-index: 9050;
2867
2871
2868 }
2872 }
2869
2873
2870 .ac .yui-ac-shadow {
2874 .ac .yui-ac-shadow {
2871 position: absolute;
2875 position: absolute;
2872 width: 100%;
2876 width: 100%;
2873 background: #000;
2877 background: #000;
2874 -moz-opacity: 0.1px;
2878 -moz-opacity: 0.1px;
2875 opacity: .10;
2879 opacity: .10;
2876 filter: alpha(opacity = 10);
2880 filter: alpha(opacity = 10);
2877 z-index: 9049;
2881 z-index: 9049;
2878 margin: .3em;
2882 margin: .3em;
2879 }
2883 }
2880
2884
2881 .ac .yui-ac-content ul {
2885 .ac .yui-ac-content ul {
2882 width: 100%;
2886 width: 100%;
2883 margin: 0;
2887 margin: 0;
2884 padding: 0;
2888 padding: 0;
2885 z-index: 9050;
2889 z-index: 9050;
2886 }
2890 }
2887
2891
2888 .ac .yui-ac-content li {
2892 .ac .yui-ac-content li {
2889 cursor: default;
2893 cursor: default;
2890 white-space: nowrap;
2894 white-space: nowrap;
2891 margin: 0;
2895 margin: 0;
2892 padding: 2px 5px;
2896 padding: 2px 5px;
2893 height: 18px;
2897 height: 18px;
2894 z-index: 9050;
2898 z-index: 9050;
2895 display: block;
2899 display: block;
2896 width: auto !important;
2900 width: auto !important;
2897 }
2901 }
2898
2902
2899 .ac .yui-ac-content li .ac-container-wrap{
2903 .ac .yui-ac-content li .ac-container-wrap{
2900 width: auto;
2904 width: auto;
2901 }
2905 }
2902
2906
2903 .ac .yui-ac-content li.yui-ac-prehighlight {
2907 .ac .yui-ac-content li.yui-ac-prehighlight {
2904 background: #B3D4FF;
2908 background: #B3D4FF;
2905 z-index: 9050;
2909 z-index: 9050;
2906 }
2910 }
2907
2911
2908 .ac .yui-ac-content li.yui-ac-highlight {
2912 .ac .yui-ac-content li.yui-ac-highlight {
2909 background: #556CB5;
2913 background: #556CB5;
2910 color: #FFF;
2914 color: #FFF;
2911 z-index: 9050;
2915 z-index: 9050;
2912 }
2916 }
2913 .ac .yui-ac-bd{
2917 .ac .yui-ac-bd{
2914 z-index: 9050;
2918 z-index: 9050;
2915 }
2919 }
2916
2920
2917 .follow {
2921 .follow {
2918 background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
2922 background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
2919 height: 16px;
2923 height: 16px;
2920 width: 20px;
2924 width: 20px;
2921 cursor: pointer;
2925 cursor: pointer;
2922 display: block;
2926 display: block;
2923 float: right;
2927 float: right;
2924 margin-top: 2px;
2928 margin-top: 2px;
2925 }
2929 }
2926
2930
2927 .following {
2931 .following {
2928 background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
2932 background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
2929 height: 16px;
2933 height: 16px;
2930 width: 20px;
2934 width: 20px;
2931 cursor: pointer;
2935 cursor: pointer;
2932 display: block;
2936 display: block;
2933 float: right;
2937 float: right;
2934 margin-top: 2px;
2938 margin-top: 2px;
2935 }
2939 }
2936
2940
2937 .currently_following {
2941 .currently_following {
2938 padding-left: 10px;
2942 padding-left: 10px;
2939 padding-bottom: 5px;
2943 padding-bottom: 5px;
2940 }
2944 }
2941
2945
2942 .add_icon {
2946 .add_icon {
2943 background: url("../images/icons/add.png") no-repeat scroll 3px;
2947 background: url("../images/icons/add.png") no-repeat scroll 3px;
2944 padding-left: 20px;
2948 padding-left: 20px;
2945 padding-top: 0px;
2949 padding-top: 0px;
2946 text-align: left;
2950 text-align: left;
2947 }
2951 }
2948
2952
2949 .edit_icon {
2953 .edit_icon {
2950 background: url("../images/icons/folder_edit.png") no-repeat scroll 3px;
2954 background: url("../images/icons/folder_edit.png") no-repeat scroll 3px;
2951 padding-left: 20px;
2955 padding-left: 20px;
2952 padding-top: 0px;
2956 padding-top: 0px;
2953 text-align: left;
2957 text-align: left;
2954 }
2958 }
2955
2959
2956 .delete_icon {
2960 .delete_icon {
2957 background: url("../images/icons/delete.png") no-repeat scroll 3px;
2961 background: url("../images/icons/delete.png") no-repeat scroll 3px;
2958 padding-left: 20px;
2962 padding-left: 20px;
2959 padding-top: 0px;
2963 padding-top: 0px;
2960 text-align: left;
2964 text-align: left;
2961 }
2965 }
2962
2966
2963 .refresh_icon {
2967 .refresh_icon {
2964 background: url("../images/icons/arrow_refresh.png") no-repeat scroll
2968 background: url("../images/icons/arrow_refresh.png") no-repeat scroll
2965 3px;
2969 3px;
2966 padding-left: 20px;
2970 padding-left: 20px;
2967 padding-top: 0px;
2971 padding-top: 0px;
2968 text-align: left;
2972 text-align: left;
2969 }
2973 }
2970
2974
2971 .pull_icon {
2975 .pull_icon {
2972 background: url("../images/icons/connect.png") no-repeat scroll 3px;
2976 background: url("../images/icons/connect.png") no-repeat scroll 3px;
2973 padding-left: 20px;
2977 padding-left: 20px;
2974 padding-top: 0px;
2978 padding-top: 0px;
2975 text-align: left;
2979 text-align: left;
2976 }
2980 }
2977
2981
2978 .rss_icon {
2982 .rss_icon {
2979 background: url("../images/icons/rss_16.png") no-repeat scroll 3px;
2983 background: url("../images/icons/rss_16.png") no-repeat scroll 3px;
2980 padding-left: 20px;
2984 padding-left: 20px;
2981 padding-top: 4px;
2985 padding-top: 4px;
2982 text-align: left;
2986 text-align: left;
2983 font-size: 8px
2987 font-size: 8px
2984 }
2988 }
2985
2989
2986 .atom_icon {
2990 .atom_icon {
2987 background: url("../images/icons/atom.png") no-repeat scroll 3px;
2991 background: url("../images/icons/atom.png") no-repeat scroll 3px;
2988 padding-left: 20px;
2992 padding-left: 20px;
2989 padding-top: 4px;
2993 padding-top: 4px;
2990 text-align: left;
2994 text-align: left;
2991 font-size: 8px
2995 font-size: 8px
2992 }
2996 }
2993
2997
2994 .archive_icon {
2998 .archive_icon {
2995 background: url("../images/icons/compress.png") no-repeat scroll 3px;
2999 background: url("../images/icons/compress.png") no-repeat scroll 3px;
2996 padding-left: 20px;
3000 padding-left: 20px;
2997 text-align: left;
3001 text-align: left;
2998 padding-top: 1px;
3002 padding-top: 1px;
2999 }
3003 }
3000
3004
3001 .start_following_icon {
3005 .start_following_icon {
3002 background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
3006 background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
3003 padding-left: 20px;
3007 padding-left: 20px;
3004 text-align: left;
3008 text-align: left;
3005 padding-top: 0px;
3009 padding-top: 0px;
3006 }
3010 }
3007
3011
3008 .stop_following_icon {
3012 .stop_following_icon {
3009 background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
3013 background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
3010 padding-left: 20px;
3014 padding-left: 20px;
3011 text-align: left;
3015 text-align: left;
3012 padding-top: 0px;
3016 padding-top: 0px;
3013 }
3017 }
3014
3018
3015 .action_button {
3019 .action_button {
3016 border: 0;
3020 border: 0;
3017 display: inline;
3021 display: inline;
3018 }
3022 }
3019
3023
3020 .action_button:hover {
3024 .action_button:hover {
3021 border: 0;
3025 border: 0;
3022 text-decoration: underline;
3026 text-decoration: underline;
3023 cursor: pointer;
3027 cursor: pointer;
3024 }
3028 }
3025
3029
3026 #switch_repos {
3030 #switch_repos {
3027 position: absolute;
3031 position: absolute;
3028 height: 25px;
3032 height: 25px;
3029 z-index: 1;
3033 z-index: 1;
3030 }
3034 }
3031
3035
3032 #switch_repos select {
3036 #switch_repos select {
3033 min-width: 150px;
3037 min-width: 150px;
3034 max-height: 250px;
3038 max-height: 250px;
3035 z-index: 1;
3039 z-index: 1;
3036 }
3040 }
3037
3041
3038 .breadcrumbs {
3042 .breadcrumbs {
3039 border: medium none;
3043 border: medium none;
3040 color: #FFF;
3044 color: #FFF;
3041 float: left;
3045 float: left;
3042 text-transform: uppercase;
3046 text-transform: uppercase;
3043 font-weight: 700;
3047 font-weight: 700;
3044 font-size: 14px;
3048 font-size: 14px;
3045 margin: 0;
3049 margin: 0;
3046 padding: 11px 0 11px 10px;
3050 padding: 11px 0 11px 10px;
3047 }
3051 }
3048
3052
3049 .breadcrumbs .hash {
3053 .breadcrumbs .hash {
3050 text-transform: none;
3054 text-transform: none;
3051 color: #fff;
3055 color: #fff;
3052 }
3056 }
3053
3057
3054 .breadcrumbs a {
3058 .breadcrumbs a {
3055 color: #FFF;
3059 color: #FFF;
3056 }
3060 }
3057
3061
3058 .flash_msg {
3062 .flash_msg {
3059
3063
3060 }
3064 }
3061
3065
3062 .flash_msg ul {
3066 .flash_msg ul {
3063
3067
3064 }
3068 }
3065
3069
3066 .error_msg {
3070 .error_msg {
3067 background-color: #c43c35;
3071 background-color: #c43c35;
3068 background-repeat: repeat-x;
3072 background-repeat: repeat-x;
3069 background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35) );
3073 background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35) );
3070 background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
3074 background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
3071 background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
3075 background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
3072 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35) );
3076 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35) );
3073 background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
3077 background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
3074 background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
3078 background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
3075 background-image: linear-gradient(top, #ee5f5b, #c43c35);
3079 background-image: linear-gradient(top, #ee5f5b, #c43c35);
3076 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b',endColorstr='#c43c35', GradientType=0 );
3080 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b',endColorstr='#c43c35', GradientType=0 );
3077 border-color: #c43c35 #c43c35 #882a25;
3081 border-color: #c43c35 #c43c35 #882a25;
3078 }
3082 }
3079
3083
3080 .warning_msg {
3084 .warning_msg {
3081 color: #404040 !important;
3085 color: #404040 !important;
3082 background-color: #eedc94;
3086 background-color: #eedc94;
3083 background-repeat: repeat-x;
3087 background-repeat: repeat-x;
3084 background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94) );
3088 background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94) );
3085 background-image: -moz-linear-gradient(top, #fceec1, #eedc94);
3089 background-image: -moz-linear-gradient(top, #fceec1, #eedc94);
3086 background-image: -ms-linear-gradient(top, #fceec1, #eedc94);
3090 background-image: -ms-linear-gradient(top, #fceec1, #eedc94);
3087 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94) );
3091 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94) );
3088 background-image: -webkit-linear-gradient(top, #fceec1, #eedc94);
3092 background-image: -webkit-linear-gradient(top, #fceec1, #eedc94);
3089 background-image: -o-linear-gradient(top, #fceec1, #eedc94);
3093 background-image: -o-linear-gradient(top, #fceec1, #eedc94);
3090 background-image: linear-gradient(top, #fceec1, #eedc94);
3094 background-image: linear-gradient(top, #fceec1, #eedc94);
3091 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0 );
3095 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0 );
3092 border-color: #eedc94 #eedc94 #e4c652;
3096 border-color: #eedc94 #eedc94 #e4c652;
3093 }
3097 }
3094
3098
3095 .success_msg {
3099 .success_msg {
3096 background-color: #57a957;
3100 background-color: #57a957;
3097 background-repeat: repeat-x !important;
3101 background-repeat: repeat-x !important;
3098 background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957) );
3102 background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957) );
3099 background-image: -moz-linear-gradient(top, #62c462, #57a957);
3103 background-image: -moz-linear-gradient(top, #62c462, #57a957);
3100 background-image: -ms-linear-gradient(top, #62c462, #57a957);
3104 background-image: -ms-linear-gradient(top, #62c462, #57a957);
3101 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957) );
3105 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957) );
3102 background-image: -webkit-linear-gradient(top, #62c462, #57a957);
3106 background-image: -webkit-linear-gradient(top, #62c462, #57a957);
3103 background-image: -o-linear-gradient(top, #62c462, #57a957);
3107 background-image: -o-linear-gradient(top, #62c462, #57a957);
3104 background-image: linear-gradient(top, #62c462, #57a957);
3108 background-image: linear-gradient(top, #62c462, #57a957);
3105 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0 );
3109 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0 );
3106 border-color: #57a957 #57a957 #3d773d;
3110 border-color: #57a957 #57a957 #3d773d;
3107 }
3111 }
3108
3112
3109 .notice_msg {
3113 .notice_msg {
3110 background-color: #339bb9;
3114 background-color: #339bb9;
3111 background-repeat: repeat-x;
3115 background-repeat: repeat-x;
3112 background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9) );
3116 background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9) );
3113 background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
3117 background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
3114 background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
3118 background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
3115 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9) );
3119 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9) );
3116 background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
3120 background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
3117 background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
3121 background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
3118 background-image: linear-gradient(top, #5bc0de, #339bb9);
3122 background-image: linear-gradient(top, #5bc0de, #339bb9);
3119 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0 );
3123 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0 );
3120 border-color: #339bb9 #339bb9 #22697d;
3124 border-color: #339bb9 #339bb9 #22697d;
3121 }
3125 }
3122
3126
3123 .success_msg,.error_msg,.notice_msg,.warning_msg {
3127 .success_msg,.error_msg,.notice_msg,.warning_msg {
3124 font-size: 12px;
3128 font-size: 12px;
3125 font-weight: 700;
3129 font-weight: 700;
3126 min-height: 14px;
3130 min-height: 14px;
3127 line-height: 14px;
3131 line-height: 14px;
3128 margin-bottom: 10px;
3132 margin-bottom: 10px;
3129 margin-top: 0;
3133 margin-top: 0;
3130 display: block;
3134 display: block;
3131 overflow: auto;
3135 overflow: auto;
3132 padding: 6px 10px 6px 10px;
3136 padding: 6px 10px 6px 10px;
3133 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3137 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3134 position: relative;
3138 position: relative;
3135 color: #FFF;
3139 color: #FFF;
3136 border-width: 1px;
3140 border-width: 1px;
3137 border-style: solid;
3141 border-style: solid;
3138 -webkit-border-radius: 4px;
3142 -webkit-border-radius: 4px;
3139 -moz-border-radius: 4px;
3143 -moz-border-radius: 4px;
3140 border-radius: 4px;
3144 border-radius: 4px;
3141 -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3145 -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3142 -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3146 -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3143 box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3147 box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
3144 }
3148 }
3145
3149
3146 #msg_close {
3150 #msg_close {
3147 background: transparent url("../icons/cross_grey_small.png") no-repeat scroll 0 0;
3151 background: transparent url("../icons/cross_grey_small.png") no-repeat scroll 0 0;
3148 cursor: pointer;
3152 cursor: pointer;
3149 height: 16px;
3153 height: 16px;
3150 position: absolute;
3154 position: absolute;
3151 right: 5px;
3155 right: 5px;
3152 top: 5px;
3156 top: 5px;
3153 width: 16px;
3157 width: 16px;
3154 }
3158 }
3155 div#legend_data{
3159 div#legend_data{
3156 padding-left:10px;
3160 padding-left:10px;
3157 }
3161 }
3158 div#legend_container table{
3162 div#legend_container table{
3159 border: none !important;
3163 border: none !important;
3160 }
3164 }
3161 div#legend_container table,div#legend_choices table {
3165 div#legend_container table,div#legend_choices table {
3162 width: auto !important;
3166 width: auto !important;
3163 }
3167 }
3164
3168
3165 table#permissions_manage {
3169 table#permissions_manage {
3166 width: 0 !important;
3170 width: 0 !important;
3167 }
3171 }
3168
3172
3169 table#permissions_manage span.private_repo_msg {
3173 table#permissions_manage span.private_repo_msg {
3170 font-size: 0.8em;
3174 font-size: 0.8em;
3171 opacity: 0.6px;
3175 opacity: 0.6px;
3172 }
3176 }
3173
3177
3174 table#permissions_manage td.private_repo_msg {
3178 table#permissions_manage td.private_repo_msg {
3175 font-size: 0.8em;
3179 font-size: 0.8em;
3176 }
3180 }
3177
3181
3178 table#permissions_manage tr#add_perm_input td {
3182 table#permissions_manage tr#add_perm_input td {
3179 vertical-align: middle;
3183 vertical-align: middle;
3180 }
3184 }
3181
3185
3182 div.gravatar {
3186 div.gravatar {
3183 background-color: #FFF;
3187 background-color: #FFF;
3184 float: left;
3188 float: left;
3185 margin-right: 0.7em;
3189 margin-right: 0.7em;
3186 padding: 1px 1px 1px 1px;
3190 padding: 1px 1px 1px 1px;
3187 line-height:0;
3191 line-height:0;
3188 -webkit-border-radius: 3px;
3192 -webkit-border-radius: 3px;
3189 -khtml-border-radius: 3px;
3193 -khtml-border-radius: 3px;
3190 -moz-border-radius: 3px;
3194 -moz-border-radius: 3px;
3191 border-radius: 3px;
3195 border-radius: 3px;
3192 }
3196 }
3193
3197
3194 div.gravatar img {
3198 div.gravatar img {
3195 -webkit-border-radius: 2px;
3199 -webkit-border-radius: 2px;
3196 -khtml-border-radius: 2px;
3200 -khtml-border-radius: 2px;
3197 -moz-border-radius: 2px;
3201 -moz-border-radius: 2px;
3198 border-radius: 2px;
3202 border-radius: 2px;
3199 }
3203 }
3200
3204
3201 #header,#content,#footer {
3205 #header,#content,#footer {
3202 min-width: 978px;
3206 min-width: 978px;
3203 }
3207 }
3204
3208
3205 #content {
3209 #content {
3206 clear: both;
3210 clear: both;
3207 overflow: hidden;
3211 overflow: hidden;
3208 padding: 54px 10px 14px 10px;
3212 padding: 54px 10px 14px 10px;
3209 }
3213 }
3210
3214
3211 #content div.box div.title div.search {
3215 #content div.box div.title div.search {
3212
3216
3213 border-left: 1px solid #316293;
3217 border-left: 1px solid #316293;
3214 }
3218 }
3215
3219
3216 #content div.box div.title div.search div.input input {
3220 #content div.box div.title div.search div.input input {
3217 border: 1px solid #316293;
3221 border: 1px solid #316293;
3218 }
3222 }
3219
3223
3220 .ui-btn{
3224 .ui-btn{
3221 color: #515151;
3225 color: #515151;
3222 background-color: #DADADA;
3226 background-color: #DADADA;
3223 background-repeat: repeat-x;
3227 background-repeat: repeat-x;
3224 background-image: -khtml-gradient(linear, left top, left bottom, from(#F4F4F4),to(#DADADA) );
3228 background-image: -khtml-gradient(linear, left top, left bottom, from(#F4F4F4),to(#DADADA) );
3225 background-image: -moz-linear-gradient(top, #F4F4F4, #DADADA);
3229 background-image: -moz-linear-gradient(top, #F4F4F4, #DADADA);
3226 background-image: -ms-linear-gradient(top, #F4F4F4, #DADADA);
3230 background-image: -ms-linear-gradient(top, #F4F4F4, #DADADA);
3227 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #F4F4F4),color-stop(100%, #DADADA) );
3231 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #F4F4F4),color-stop(100%, #DADADA) );
3228 background-image: -webkit-linear-gradient(top, #F4F4F4, #DADADA) );
3232 background-image: -webkit-linear-gradient(top, #F4F4F4, #DADADA) );
3229 background-image: -o-linear-gradient(top, #F4F4F4, #DADADA) );
3233 background-image: -o-linear-gradient(top, #F4F4F4, #DADADA) );
3230 background-image: linear-gradient(top, #F4F4F4, #DADADA);
3234 background-image: linear-gradient(top, #F4F4F4, #DADADA);
3231 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F4F4F4', endColorstr='#DADADA', GradientType=0);
3235 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F4F4F4', endColorstr='#DADADA', GradientType=0);
3232
3236
3233 border-top: 1px solid #DDD;
3237 border-top: 1px solid #DDD;
3234 border-left: 1px solid #c6c6c6;
3238 border-left: 1px solid #c6c6c6;
3235 border-right: 1px solid #DDD;
3239 border-right: 1px solid #DDD;
3236 border-bottom: 1px solid #c6c6c6;
3240 border-bottom: 1px solid #c6c6c6;
3237 color: #515151;
3241 color: #515151;
3238 outline: none;
3242 outline: none;
3239 margin: 0px 3px 3px 0px;
3243 margin: 0px 3px 3px 0px;
3240 -webkit-border-radius: 4px 4px 4px 4px !important;
3244 -webkit-border-radius: 4px 4px 4px 4px !important;
3241 -khtml-border-radius: 4px 4px 4px 4px !important;
3245 -khtml-border-radius: 4px 4px 4px 4px !important;
3242 -moz-border-radius: 4px 4px 4px 4px !important;
3246 -moz-border-radius: 4px 4px 4px 4px !important;
3243 border-radius: 4px 4px 4px 4px !important;
3247 border-radius: 4px 4px 4px 4px !important;
3244 cursor: pointer !important;
3248 cursor: pointer !important;
3245 padding: 3px 3px 3px 3px;
3249 padding: 3px 3px 3px 3px;
3246 background-position: 0 -15px;
3250 background-position: 0 -15px;
3247
3251
3248 }
3252 }
3249 .ui-btn.xsmall{
3253 .ui-btn.xsmall{
3250 padding: 1px 2px 1px 1px;
3254 padding: 1px 2px 1px 1px;
3251 }
3255 }
3252 .ui-btn.clone{
3256 .ui-btn.clone{
3253 padding: 5px 2px 6px 1px;
3257 padding: 5px 2px 6px 1px;
3254 margin: 0px -4px 3px 0px;
3258 margin: 0px -4px 3px 0px;
3255 -webkit-border-radius: 4px 0px 0px 4px !important;
3259 -webkit-border-radius: 4px 0px 0px 4px !important;
3256 -khtml-border-radius: 4px 0px 0px 4px !important;
3260 -khtml-border-radius: 4px 0px 0px 4px !important;
3257 -moz-border-radius: 4px 0px 0px 4px !important;
3261 -moz-border-radius: 4px 0px 0px 4px !important;
3258 border-radius: 4px 0px 0px 4px !important;
3262 border-radius: 4px 0px 0px 4px !important;
3259 width: 100px;
3263 width: 100px;
3260 text-align: center;
3264 text-align: center;
3261 float: left;
3265 float: left;
3262 position: absolute;
3266 position: absolute;
3263 }
3267 }
3264 .ui-btn:focus {
3268 .ui-btn:focus {
3265 outline: none;
3269 outline: none;
3266 }
3270 }
3267 .ui-btn:hover{
3271 .ui-btn:hover{
3268 background-position: 0 0px;
3272 background-position: 0 0px;
3269 text-decoration: none;
3273 text-decoration: none;
3270 color: #515151;
3274 color: #515151;
3271 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), 0 0 3px #FFFFFF !important;
3275 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), 0 0 3px #FFFFFF !important;
3272 }
3276 }
3273
3277
3274 .ui-btn.red{
3278 .ui-btn.red{
3275 color:#fff;
3279 color:#fff;
3276 background-color: #c43c35;
3280 background-color: #c43c35;
3277 background-repeat: repeat-x;
3281 background-repeat: repeat-x;
3278 background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));
3282 background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));
3279 background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
3283 background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
3280 background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
3284 background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
3281 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));
3285 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));
3282 background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
3286 background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
3283 background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
3287 background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
3284 background-image: linear-gradient(top, #ee5f5b, #c43c35);
3288 background-image: linear-gradient(top, #ee5f5b, #c43c35);
3285 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
3289 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
3286 border-color: #c43c35 #c43c35 #882a25;
3290 border-color: #c43c35 #c43c35 #882a25;
3287 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3291 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3288 }
3292 }
3289
3293
3290
3294
3291 .ui-btn.blue{
3295 .ui-btn.blue{
3292 background-color: #339bb9;
3296 background-color: #339bb9;
3293 background-repeat: repeat-x;
3297 background-repeat: repeat-x;
3294 background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));
3298 background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));
3295 background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
3299 background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
3296 background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
3300 background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
3297 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));
3301 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));
3298 background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
3302 background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
3299 background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
3303 background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
3300 background-image: linear-gradient(top, #5bc0de, #339bb9);
3304 background-image: linear-gradient(top, #5bc0de, #339bb9);
3301 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);
3305 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);
3302 border-color: #339bb9 #339bb9 #22697d;
3306 border-color: #339bb9 #339bb9 #22697d;
3303 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3307 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3304 }
3308 }
3305
3309
3306 .ui-btn.green{
3310 .ui-btn.green{
3307 background-color: #57a957;
3311 background-color: #57a957;
3308 background-repeat: repeat-x;
3312 background-repeat: repeat-x;
3309 background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
3313 background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
3310 background-image: -moz-linear-gradient(top, #62c462, #57a957);
3314 background-image: -moz-linear-gradient(top, #62c462, #57a957);
3311 background-image: -ms-linear-gradient(top, #62c462, #57a957);
3315 background-image: -ms-linear-gradient(top, #62c462, #57a957);
3312 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));
3316 background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));
3313 background-image: -webkit-linear-gradient(top, #62c462, #57a957);
3317 background-image: -webkit-linear-gradient(top, #62c462, #57a957);
3314 background-image: -o-linear-gradient(top, #62c462, #57a957);
3318 background-image: -o-linear-gradient(top, #62c462, #57a957);
3315 background-image: linear-gradient(top, #62c462, #57a957);
3319 background-image: linear-gradient(top, #62c462, #57a957);
3316 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);
3320 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);
3317 border-color: #57a957 #57a957 #3d773d;
3321 border-color: #57a957 #57a957 #3d773d;
3318 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3322 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
3319 }
3323 }
3320
3324
3321 ins,div.options a:hover {
3325 ins,div.options a:hover {
3322 text-decoration: none;
3326 text-decoration: none;
3323 }
3327 }
3324
3328
3325 img,
3329 img,
3326 #header #header-inner #quick li a:hover span.normal,
3330 #header #header-inner #quick li a:hover span.normal,
3327 #header #header-inner #quick li ul li.last,
3331 #header #header-inner #quick li ul li.last,
3328 #content div.box div.form div.fields div.field div.textarea table td table td a,
3332 #content div.box div.form div.fields div.field div.textarea table td table td a,
3329 #clone_url,
3333 #clone_url,
3330 #clone_url_id
3334 #clone_url_id
3331 {
3335 {
3332 border: none;
3336 border: none;
3333 }
3337 }
3334
3338
3335 img.icon,.right .merge img {
3339 img.icon,.right .merge img {
3336 vertical-align: bottom;
3340 vertical-align: bottom;
3337 }
3341 }
3338
3342
3339 #header ul#logged-user,#content div.box div.title ul.links,
3343 #header ul#logged-user,#content div.box div.title ul.links,
3340 #content div.box div.message div.dismiss,
3344 #content div.box div.message div.dismiss,
3341 #content div.box div.traffic div.legend ul
3345 #content div.box div.traffic div.legend ul
3342 {
3346 {
3343 float: right;
3347 float: right;
3344 margin: 0;
3348 margin: 0;
3345 padding: 0;
3349 padding: 0;
3346 }
3350 }
3347
3351
3348 #header #header-inner #home,#header #header-inner #logo,
3352 #header #header-inner #home,#header #header-inner #logo,
3349 #content div.box ul.left,#content div.box ol.left,
3353 #content div.box ul.left,#content div.box ol.left,
3350 #content div.box div.pagination-left,div#commit_history,
3354 #content div.box div.pagination-left,div#commit_history,
3351 div#legend_data,div#legend_container,div#legend_choices
3355 div#legend_data,div#legend_container,div#legend_choices
3352 {
3356 {
3353 float: left;
3357 float: left;
3354 }
3358 }
3355
3359
3356 #header #header-inner #quick li:hover ul ul,
3360 #header #header-inner #quick li:hover ul ul,
3357 #header #header-inner #quick li:hover ul ul ul,
3361 #header #header-inner #quick li:hover ul ul ul,
3358 #header #header-inner #quick li:hover ul ul ul ul,
3362 #header #header-inner #quick li:hover ul ul ul ul,
3359 #content #left #menu ul.closed,#content #left #menu li ul.collapsed,.yui-tt-shadow
3363 #content #left #menu ul.closed,#content #left #menu li ul.collapsed,.yui-tt-shadow
3360 {
3364 {
3361 display: none;
3365 display: none;
3362 }
3366 }
3363
3367
3364 #header #header-inner #quick li:hover ul,#header #header-inner #quick li li:hover ul,#header #header-inner #quick li li li:hover ul,#header #header-inner #quick li li li li:hover ul,#content #left #menu ul.opened,#content #left #menu li ul.expanded
3368 #header #header-inner #quick li:hover ul,#header #header-inner #quick li li:hover ul,#header #header-inner #quick li li li:hover ul,#header #header-inner #quick li li li li:hover ul,#content #left #menu ul.opened,#content #left #menu li ul.expanded
3365 {
3369 {
3366 display: block;
3370 display: block;
3367 }
3371 }
3368
3372
3369 #content div.graph {
3373 #content div.graph {
3370 padding: 0 10px 10px;
3374 padding: 0 10px 10px;
3371 }
3375 }
3372
3376
3373 #content div.box div.title ul.links li a:hover,#content div.box div.title ul.links li.ui-tabs-selected a
3377 #content div.box div.title ul.links li a:hover,#content div.box div.title ul.links li.ui-tabs-selected a
3374 {
3378 {
3375 color: #bfe3ff;
3379 color: #bfe3ff;
3376 }
3380 }
3377
3381
3378 #content div.box ol.lower-roman,#content div.box ol.upper-roman,#content div.box ol.lower-alpha,#content div.box ol.upper-alpha,#content div.box ol.decimal
3382 #content div.box ol.lower-roman,#content div.box ol.upper-roman,#content div.box ol.lower-alpha,#content div.box ol.upper-alpha,#content div.box ol.decimal
3379 {
3383 {
3380 margin: 10px 24px 10px 44px;
3384 margin: 10px 24px 10px 44px;
3381 }
3385 }
3382
3386
3383 #content div.box div.form,#content div.box div.table,#content div.box div.traffic
3387 #content div.box div.form,#content div.box div.table,#content div.box div.traffic
3384 {
3388 {
3385 clear: both;
3389 clear: both;
3386 overflow: hidden;
3390 overflow: hidden;
3387 margin: 0;
3391 margin: 0;
3388 padding: 0 20px 10px;
3392 padding: 0 20px 10px;
3389 }
3393 }
3390
3394
3391 #content div.box div.form div.fields,#login div.form,#login div.form div.fields,#register div.form,#register div.form div.fields
3395 #content div.box div.form div.fields,#login div.form,#login div.form div.fields,#register div.form,#register div.form div.fields
3392 {
3396 {
3393 clear: both;
3397 clear: both;
3394 overflow: hidden;
3398 overflow: hidden;
3395 margin: 0;
3399 margin: 0;
3396 padding: 0;
3400 padding: 0;
3397 }
3401 }
3398
3402
3399 #content div.box div.form div.fields div.field div.label span,#login div.form div.fields div.field div.label span,#register div.form div.fields div.field div.label span
3403 #content div.box div.form div.fields div.field div.label span,#login div.form div.fields div.field div.label span,#register div.form div.fields div.field div.label span
3400 {
3404 {
3401 height: 1%;
3405 height: 1%;
3402 display: block;
3406 display: block;
3403 color: #363636;
3407 color: #363636;
3404 margin: 0;
3408 margin: 0;
3405 padding: 2px 0 0;
3409 padding: 2px 0 0;
3406 }
3410 }
3407
3411
3408 #content div.box div.form div.fields div.field div.input input.error,#login div.form div.fields div.field div.input input.error,#register div.form div.fields div.field div.input input.error
3412 #content div.box div.form div.fields div.field div.input input.error,#login div.form div.fields div.field div.input input.error,#register div.form div.fields div.field div.input input.error
3409 {
3413 {
3410 background: #FBE3E4;
3414 background: #FBE3E4;
3411 border-top: 1px solid #e1b2b3;
3415 border-top: 1px solid #e1b2b3;
3412 border-left: 1px solid #e1b2b3;
3416 border-left: 1px solid #e1b2b3;
3413 border-right: 1px solid #FBC2C4;
3417 border-right: 1px solid #FBC2C4;
3414 border-bottom: 1px solid #FBC2C4;
3418 border-bottom: 1px solid #FBC2C4;
3415 }
3419 }
3416
3420
3417 #content div.box div.form div.fields div.field div.input input.success,#login div.form div.fields div.field div.input input.success,#register div.form div.fields div.field div.input input.success
3421 #content div.box div.form div.fields div.field div.input input.success,#login div.form div.fields div.field div.input input.success,#register div.form div.fields div.field div.input input.success
3418 {
3422 {
3419 background: #E6EFC2;
3423 background: #E6EFC2;
3420 border-top: 1px solid #cebb98;
3424 border-top: 1px solid #cebb98;
3421 border-left: 1px solid #cebb98;
3425 border-left: 1px solid #cebb98;
3422 border-right: 1px solid #c6d880;
3426 border-right: 1px solid #c6d880;
3423 border-bottom: 1px solid #c6d880;
3427 border-bottom: 1px solid #c6d880;
3424 }
3428 }
3425
3429
3426 #content div.box-left div.form div.fields div.field div.textarea,#content div.box-right div.form div.fields div.field div.textarea,#content div.box div.form div.fields div.field div.select select,#content div.box table th.selected input,#content div.box table td.selected input
3430 #content div.box-left div.form div.fields div.field div.textarea,#content div.box-right div.form div.fields div.field div.textarea,#content div.box div.form div.fields div.field div.select select,#content div.box table th.selected input,#content div.box table td.selected input
3427 {
3431 {
3428 margin: 0;
3432 margin: 0;
3429 }
3433 }
3430
3434
3431 #content div.box-left div.form div.fields div.field div.select,#content div.box-left div.form div.fields div.field div.checkboxes,#content div.box-left div.form div.fields div.field div.radios,#content div.box-right div.form div.fields div.field div.select,#content div.box-right div.form div.fields div.field div.checkboxes,#content div.box-right div.form div.fields div.field div.radios
3435 #content div.box-left div.form div.fields div.field div.select,#content div.box-left div.form div.fields div.field div.checkboxes,#content div.box-left div.form div.fields div.field div.radios,#content div.box-right div.form div.fields div.field div.select,#content div.box-right div.form div.fields div.field div.checkboxes,#content div.box-right div.form div.fields div.field div.radios
3432 {
3436 {
3433 margin: 0 0 0 0px !important;
3437 margin: 0 0 0 0px !important;
3434 padding: 0;
3438 padding: 0;
3435 }
3439 }
3436
3440
3437 #content div.box div.form div.fields div.field div.select,#content div.box div.form div.fields div.field div.checkboxes,#content div.box div.form div.fields div.field div.radios
3441 #content div.box div.form div.fields div.field div.select,#content div.box div.form div.fields div.field div.checkboxes,#content div.box div.form div.fields div.field div.radios
3438 {
3442 {
3439 margin: 0 0 0 200px;
3443 margin: 0 0 0 200px;
3440 padding: 0;
3444 padding: 0;
3441 }
3445 }
3442
3446
3443 #content div.box div.form div.fields div.field div.select a:hover,#content div.box div.form div.fields div.field div.select a.ui-selectmenu:hover,#content div.box div.action a:hover
3447 #content div.box div.form div.fields div.field div.select a:hover,#content div.box div.form div.fields div.field div.select a.ui-selectmenu:hover,#content div.box div.action a:hover
3444 {
3448 {
3445 color: #000;
3449 color: #000;
3446 text-decoration: none;
3450 text-decoration: none;
3447 }
3451 }
3448
3452
3449 #content div.box div.form div.fields div.field div.select a.ui-selectmenu-focus,#content div.box div.action a.ui-selectmenu-focus
3453 #content div.box div.form div.fields div.field div.select a.ui-selectmenu-focus,#content div.box div.action a.ui-selectmenu-focus
3450 {
3454 {
3451 border: 1px solid #666;
3455 border: 1px solid #666;
3452 }
3456 }
3453
3457
3454 #content div.box div.form div.fields div.field div.checkboxes div.checkbox,#content div.box div.form div.fields div.field div.radios div.radio
3458 #content div.box div.form div.fields div.field div.checkboxes div.checkbox,#content div.box div.form div.fields div.field div.radios div.radio
3455 {
3459 {
3456 clear: both;
3460 clear: both;
3457 overflow: hidden;
3461 overflow: hidden;
3458 margin: 0;
3462 margin: 0;
3459 padding: 8px 0 2px;
3463 padding: 8px 0 2px;
3460 }
3464 }
3461
3465
3462 #content div.box div.form div.fields div.field div.checkboxes div.checkbox input,#content div.box div.form div.fields div.field div.radios div.radio input
3466 #content div.box div.form div.fields div.field div.checkboxes div.checkbox input,#content div.box div.form div.fields div.field div.radios div.radio input
3463 {
3467 {
3464 float: left;
3468 float: left;
3465 margin: 0;
3469 margin: 0;
3466 }
3470 }
3467
3471
3468 #content div.box div.form div.fields div.field div.checkboxes div.checkbox label,#content div.box div.form div.fields div.field div.radios div.radio label
3472 #content div.box div.form div.fields div.field div.checkboxes div.checkbox label,#content div.box div.form div.fields div.field div.radios div.radio label
3469 {
3473 {
3470 height: 1%;
3474 height: 1%;
3471 display: block;
3475 display: block;
3472 float: left;
3476 float: left;
3473 margin: 2px 0 0 4px;
3477 margin: 2px 0 0 4px;
3474 }
3478 }
3475
3479
3476 div.form div.fields div.field div.button input,#content div.box div.form div.fields div.buttons input,div.form div.fields div.buttons input,#content div.box div.action div.button input
3480 div.form div.fields div.field div.button input,#content div.box div.form div.fields div.buttons input,div.form div.fields div.buttons input,#content div.box div.action div.button input
3477 {
3481 {
3478 color: #000;
3482 color: #000;
3479 font-size: 11px;
3483 font-size: 11px;
3480 font-weight: 700;
3484 font-weight: 700;
3481 margin: 0;
3485 margin: 0;
3482 }
3486 }
3483
3487
3484 input.ui-button {
3488 input.ui-button {
3485 background: #e5e3e3 url("../images/button.png") repeat-x;
3489 background: #e5e3e3 url("../images/button.png") repeat-x;
3486 border-top: 1px solid #DDD;
3490 border-top: 1px solid #DDD;
3487 border-left: 1px solid #c6c6c6;
3491 border-left: 1px solid #c6c6c6;
3488 border-right: 1px solid #DDD;
3492 border-right: 1px solid #DDD;
3489 border-bottom: 1px solid #c6c6c6;
3493 border-bottom: 1px solid #c6c6c6;
3490 color: #515151 !important;
3494 color: #515151 !important;
3491 outline: none;
3495 outline: none;
3492 margin: 0;
3496 margin: 0;
3493 padding: 6px 12px;
3497 padding: 6px 12px;
3494 -webkit-border-radius: 4px 4px 4px 4px;
3498 -webkit-border-radius: 4px 4px 4px 4px;
3495 -khtml-border-radius: 4px 4px 4px 4px;
3499 -khtml-border-radius: 4px 4px 4px 4px;
3496 -moz-border-radius: 4px 4px 4px 4px;
3500 -moz-border-radius: 4px 4px 4px 4px;
3497 border-radius: 4px 4px 4px 4px;
3501 border-radius: 4px 4px 4px 4px;
3498 box-shadow: 0 1px 0 #ececec;
3502 box-shadow: 0 1px 0 #ececec;
3499 cursor: pointer;
3503 cursor: pointer;
3500 }
3504 }
3501
3505
3502 input.ui-button:hover {
3506 input.ui-button:hover {
3503 background: #b4b4b4 url("../images/button_selected.png") repeat-x;
3507 background: #b4b4b4 url("../images/button_selected.png") repeat-x;
3504 border-top: 1px solid #ccc;
3508 border-top: 1px solid #ccc;
3505 border-left: 1px solid #bebebe;
3509 border-left: 1px solid #bebebe;
3506 border-right: 1px solid #b1b1b1;
3510 border-right: 1px solid #b1b1b1;
3507 border-bottom: 1px solid #afafaf;
3511 border-bottom: 1px solid #afafaf;
3508 }
3512 }
3509
3513
3510 div.form div.fields div.field div.highlight,#content div.box div.form div.fields div.buttons div.highlight
3514 div.form div.fields div.field div.highlight,#content div.box div.form div.fields div.buttons div.highlight
3511 {
3515 {
3512 display: inline;
3516 display: inline;
3513 }
3517 }
3514
3518
3515 #content div.box div.form div.fields div.buttons,div.form div.fields div.buttons
3519 #content div.box div.form div.fields div.buttons,div.form div.fields div.buttons
3516 {
3520 {
3517 margin: 10px 0 0 200px;
3521 margin: 10px 0 0 200px;
3518 padding: 0;
3522 padding: 0;
3519 }
3523 }
3520
3524
3521 #content div.box-left div.form div.fields div.buttons,#content div.box-right div.form div.fields div.buttons,div.box-left div.form div.fields div.buttons,div.box-right div.form div.fields div.buttons
3525 #content div.box-left div.form div.fields div.buttons,#content div.box-right div.form div.fields div.buttons,div.box-left div.form div.fields div.buttons,div.box-right div.form div.fields div.buttons
3522 {
3526 {
3523 margin: 10px 0 0;
3527 margin: 10px 0 0;
3524 }
3528 }
3525
3529
3526 #content div.box table td.user,#content div.box table td.address {
3530 #content div.box table td.user,#content div.box table td.address {
3527 width: 10%;
3531 width: 10%;
3528 text-align: center;
3532 text-align: center;
3529 }
3533 }
3530
3534
3531 #content div.box div.action div.button,#login div.form div.fields div.field div.input div.link,#register div.form div.fields div.field div.input div.link
3535 #content div.box div.action div.button,#login div.form div.fields div.field div.input div.link,#register div.form div.fields div.field div.input div.link
3532 {
3536 {
3533 text-align: right;
3537 text-align: right;
3534 margin: 6px 0 0;
3538 margin: 6px 0 0;
3535 padding: 0;
3539 padding: 0;
3536 }
3540 }
3537
3541
3538 #content div.box div.action div.button input.ui-state-hover,#login div.form div.fields div.buttons input.ui-state-hover,#register div.form div.fields div.buttons input.ui-state-hover
3542 #content div.box div.action div.button input.ui-state-hover,#login div.form div.fields div.buttons input.ui-state-hover,#register div.form div.fields div.buttons input.ui-state-hover
3539 {
3543 {
3540 background: #b4b4b4 url("../images/button_selected.png") repeat-x;
3544 background: #b4b4b4 url("../images/button_selected.png") repeat-x;
3541 border-top: 1px solid #ccc;
3545 border-top: 1px solid #ccc;
3542 border-left: 1px solid #bebebe;
3546 border-left: 1px solid #bebebe;
3543 border-right: 1px solid #b1b1b1;
3547 border-right: 1px solid #b1b1b1;
3544 border-bottom: 1px solid #afafaf;
3548 border-bottom: 1px solid #afafaf;
3545 color: #515151;
3549 color: #515151;
3546 margin: 0;
3550 margin: 0;
3547 padding: 6px 12px;
3551 padding: 6px 12px;
3548 }
3552 }
3549
3553
3550 #content div.box div.pagination div.results,#content div.box div.pagination-wh div.results
3554 #content div.box div.pagination div.results,#content div.box div.pagination-wh div.results
3551 {
3555 {
3552 text-align: left;
3556 text-align: left;
3553 float: left;
3557 float: left;
3554 margin: 0;
3558 margin: 0;
3555 padding: 0;
3559 padding: 0;
3556 }
3560 }
3557
3561
3558 #content div.box div.pagination div.results span,#content div.box div.pagination-wh div.results span
3562 #content div.box div.pagination div.results span,#content div.box div.pagination-wh div.results span
3559 {
3563 {
3560 height: 1%;
3564 height: 1%;
3561 display: block;
3565 display: block;
3562 float: left;
3566 float: left;
3563 background: #ebebeb url("../images/pager.png") repeat-x;
3567 background: #ebebeb url("../images/pager.png") repeat-x;
3564 border-top: 1px solid #dedede;
3568 border-top: 1px solid #dedede;
3565 border-left: 1px solid #cfcfcf;
3569 border-left: 1px solid #cfcfcf;
3566 border-right: 1px solid #c4c4c4;
3570 border-right: 1px solid #c4c4c4;
3567 border-bottom: 1px solid #c4c4c4;
3571 border-bottom: 1px solid #c4c4c4;
3568 color: #4A4A4A;
3572 color: #4A4A4A;
3569 font-weight: 700;
3573 font-weight: 700;
3570 margin: 0;
3574 margin: 0;
3571 padding: 6px 8px;
3575 padding: 6px 8px;
3572 }
3576 }
3573
3577
3574 #content div.box div.pagination ul.pager li.disabled,#content div.box div.pagination-wh a.disabled
3578 #content div.box div.pagination ul.pager li.disabled,#content div.box div.pagination-wh a.disabled
3575 {
3579 {
3576 color: #B4B4B4;
3580 color: #B4B4B4;
3577 padding: 6px;
3581 padding: 6px;
3578 }
3582 }
3579
3583
3580 #login,#register {
3584 #login,#register {
3581 width: 520px;
3585 width: 520px;
3582 margin: 10% auto 0;
3586 margin: 10% auto 0;
3583 padding: 0;
3587 padding: 0;
3584 }
3588 }
3585
3589
3586 #login div.color,#register div.color {
3590 #login div.color,#register div.color {
3587 clear: both;
3591 clear: both;
3588 overflow: hidden;
3592 overflow: hidden;
3589 background: #FFF;
3593 background: #FFF;
3590 margin: 10px auto 0;
3594 margin: 10px auto 0;
3591 padding: 3px 3px 3px 0;
3595 padding: 3px 3px 3px 0;
3592 }
3596 }
3593
3597
3594 #login div.color a,#register div.color a {
3598 #login div.color a,#register div.color a {
3595 width: 20px;
3599 width: 20px;
3596 height: 20px;
3600 height: 20px;
3597 display: block;
3601 display: block;
3598 float: left;
3602 float: left;
3599 margin: 0 0 0 3px;
3603 margin: 0 0 0 3px;
3600 padding: 0;
3604 padding: 0;
3601 }
3605 }
3602
3606
3603 #login div.title h5,#register div.title h5 {
3607 #login div.title h5,#register div.title h5 {
3604 color: #fff;
3608 color: #fff;
3605 margin: 10px;
3609 margin: 10px;
3606 padding: 0;
3610 padding: 0;
3607 }
3611 }
3608
3612
3609 #login div.form div.fields div.field,#register div.form div.fields div.field
3613 #login div.form div.fields div.field,#register div.form div.fields div.field
3610 {
3614 {
3611 clear: both;
3615 clear: both;
3612 overflow: hidden;
3616 overflow: hidden;
3613 margin: 0;
3617 margin: 0;
3614 padding: 0 0 10px;
3618 padding: 0 0 10px;
3615 }
3619 }
3616
3620
3617 #login div.form div.fields div.field span.error-message,#register div.form div.fields div.field span.error-message
3621 #login div.form div.fields div.field span.error-message,#register div.form div.fields div.field span.error-message
3618 {
3622 {
3619 height: 1%;
3623 height: 1%;
3620 display: block;
3624 display: block;
3621 color: red;
3625 color: red;
3622 margin: 8px 0 0;
3626 margin: 8px 0 0;
3623 padding: 0;
3627 padding: 0;
3624 max-width: 320px;
3628 max-width: 320px;
3625 }
3629 }
3626
3630
3627 #login div.form div.fields div.field div.label label,#register div.form div.fields div.field div.label label
3631 #login div.form div.fields div.field div.label label,#register div.form div.fields div.field div.label label
3628 {
3632 {
3629 color: #000;
3633 color: #000;
3630 font-weight: 700;
3634 font-weight: 700;
3631 }
3635 }
3632
3636
3633 #login div.form div.fields div.field div.input,#register div.form div.fields div.field div.input
3637 #login div.form div.fields div.field div.input,#register div.form div.fields div.field div.input
3634 {
3638 {
3635 float: left;
3639 float: left;
3636 margin: 0;
3640 margin: 0;
3637 padding: 0;
3641 padding: 0;
3638 }
3642 }
3639
3643
3640 #login div.form div.fields div.field div.checkbox,#register div.form div.fields div.field div.checkbox
3644 #login div.form div.fields div.field div.checkbox,#register div.form div.fields div.field div.checkbox
3641 {
3645 {
3642 margin: 0 0 0 184px;
3646 margin: 0 0 0 184px;
3643 padding: 0;
3647 padding: 0;
3644 }
3648 }
3645
3649
3646 #login div.form div.fields div.field div.checkbox label,#register div.form div.fields div.field div.checkbox label
3650 #login div.form div.fields div.field div.checkbox label,#register div.form div.fields div.field div.checkbox label
3647 {
3651 {
3648 color: #565656;
3652 color: #565656;
3649 font-weight: 700;
3653 font-weight: 700;
3650 }
3654 }
3651
3655
3652 #login div.form div.fields div.buttons input,#register div.form div.fields div.buttons input
3656 #login div.form div.fields div.buttons input,#register div.form div.fields div.buttons input
3653 {
3657 {
3654 color: #000;
3658 color: #000;
3655 font-size: 1em;
3659 font-size: 1em;
3656 font-weight: 700;
3660 font-weight: 700;
3657 margin: 0;
3661 margin: 0;
3658 }
3662 }
3659
3663
3660 #changeset_content .container .wrapper,#graph_content .container .wrapper
3664 #changeset_content .container .wrapper,#graph_content .container .wrapper
3661 {
3665 {
3662 width: 600px;
3666 width: 600px;
3663 }
3667 }
3664
3668
3665 #changeset_content .container .left {
3669 #changeset_content .container .left {
3666 float: left;
3670 float: left;
3667 width: 75%;
3671 width: 75%;
3668 padding-left: 5px;
3672 padding-left: 5px;
3669 }
3673 }
3670
3674
3671 #changeset_content .container .left .date,.ac .match {
3675 #changeset_content .container .left .date,.ac .match {
3672 font-weight: 700;
3676 font-weight: 700;
3673 padding-top: 5px;
3677 padding-top: 5px;
3674 padding-bottom: 5px;
3678 padding-bottom: 5px;
3675 }
3679 }
3676
3680
3677 div#legend_container table td,div#legend_choices table td {
3681 div#legend_container table td,div#legend_choices table td {
3678 border: none !important;
3682 border: none !important;
3679 height: 20px !important;
3683 height: 20px !important;
3680 padding: 0 !important;
3684 padding: 0 !important;
3681 }
3685 }
3682
3686
3683 .q_filter_box {
3687 .q_filter_box {
3684 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
3688 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
3685 -webkit-border-radius: 4px;
3689 -webkit-border-radius: 4px;
3686 -moz-border-radius: 4px;
3690 -moz-border-radius: 4px;
3687 border-radius: 4px;
3691 border-radius: 4px;
3688 border: 0 none;
3692 border: 0 none;
3689 color: #AAAAAA;
3693 color: #AAAAAA;
3690 margin-bottom: -4px;
3694 margin-bottom: -4px;
3691 margin-top: -4px;
3695 margin-top: -4px;
3692 padding-left: 3px;
3696 padding-left: 3px;
3693 }
3697 }
3694
3698
3695 #node_filter {
3699 #node_filter {
3696 border: 0px solid #545454;
3700 border: 0px solid #545454;
3697 color: #AAAAAA;
3701 color: #AAAAAA;
3698 padding-left: 3px;
3702 padding-left: 3px;
3699 }
3703 }
3700
3704
3701
3705
3702 .group_members_wrap{
3706 .group_members_wrap{
3703
3707
3704 }
3708 }
3705
3709
3706 .group_members .group_member{
3710 .group_members .group_member{
3707 height: 30px;
3711 height: 30px;
3708 padding:0px 0px 0px 10px;
3712 padding:0px 0px 0px 10px;
3709 }
3713 }
3710
3714
3711 /*README STYLE*/
3715 /*README STYLE*/
3712
3716
3713 div.readme {
3717 div.readme {
3714 padding:0px;
3718 padding:0px;
3715 }
3719 }
3716
3720
3717 div.readme h2 {
3721 div.readme h2 {
3718 font-weight: normal;
3722 font-weight: normal;
3719 }
3723 }
3720
3724
3721 div.readme .readme_box {
3725 div.readme .readme_box {
3722 background-color: #fafafa;
3726 background-color: #fafafa;
3723 }
3727 }
3724
3728
3725 div.readme .readme_box {
3729 div.readme .readme_box {
3726 clear:both;
3730 clear:both;
3727 overflow:hidden;
3731 overflow:hidden;
3728 margin:0;
3732 margin:0;
3729 padding:0 20px 10px;
3733 padding:0 20px 10px;
3730 }
3734 }
3731
3735
3732 div.readme .readme_box h1, div.readme .readme_box h2, div.readme .readme_box h3, div.readme .readme_box h4, div.readme .readme_box h5, div.readme .readme_box h6 {
3736 div.readme .readme_box h1, div.readme .readme_box h2, div.readme .readme_box h3, div.readme .readme_box h4, div.readme .readme_box h5, div.readme .readme_box h6 {
3733 border-bottom: 0 !important;
3737 border-bottom: 0 !important;
3734 margin: 0 !important;
3738 margin: 0 !important;
3735 padding: 0 !important;
3739 padding: 0 !important;
3736 line-height: 1.5em !important;
3740 line-height: 1.5em !important;
3737 }
3741 }
3738
3742
3739
3743
3740 div.readme .readme_box h1:first-child {
3744 div.readme .readme_box h1:first-child {
3741 padding-top: .25em !important;
3745 padding-top: .25em !important;
3742 }
3746 }
3743
3747
3744 div.readme .readme_box h2, div.readme .readme_box h3 {
3748 div.readme .readme_box h2, div.readme .readme_box h3 {
3745 margin: 1em 0 !important;
3749 margin: 1em 0 !important;
3746 }
3750 }
3747
3751
3748 div.readme .readme_box h2 {
3752 div.readme .readme_box h2 {
3749 margin-top: 1.5em !important;
3753 margin-top: 1.5em !important;
3750 border-top: 4px solid #e0e0e0 !important;
3754 border-top: 4px solid #e0e0e0 !important;
3751 padding-top: .5em !important;
3755 padding-top: .5em !important;
3752 }
3756 }
3753
3757
3754 div.readme .readme_box p {
3758 div.readme .readme_box p {
3755 color: black !important;
3759 color: black !important;
3756 margin: 1em 0 !important;
3760 margin: 1em 0 !important;
3757 line-height: 1.5em !important;
3761 line-height: 1.5em !important;
3758 }
3762 }
3759
3763
3760 div.readme .readme_box ul {
3764 div.readme .readme_box ul {
3761 list-style: disc !important;
3765 list-style: disc !important;
3762 margin: 1em 0 1em 2em !important;
3766 margin: 1em 0 1em 2em !important;
3763 }
3767 }
3764
3768
3765 div.readme .readme_box ol {
3769 div.readme .readme_box ol {
3766 list-style: decimal;
3770 list-style: decimal;
3767 margin: 1em 0 1em 2em !important;
3771 margin: 1em 0 1em 2em !important;
3768 }
3772 }
3769
3773
3770 div.readme .readme_box pre, code {
3774 div.readme .readme_box pre, code {
3771 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
3775 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
3772 }
3776 }
3773
3777
3774 div.readme .readme_box code {
3778 div.readme .readme_box code {
3775 font-size: 12px !important;
3779 font-size: 12px !important;
3776 background-color: ghostWhite !important;
3780 background-color: ghostWhite !important;
3777 color: #444 !important;
3781 color: #444 !important;
3778 padding: 0 .2em !important;
3782 padding: 0 .2em !important;
3779 border: 1px solid #dedede !important;
3783 border: 1px solid #dedede !important;
3780 }
3784 }
3781
3785
3782 div.readme .readme_box pre code {
3786 div.readme .readme_box pre code {
3783 padding: 0 !important;
3787 padding: 0 !important;
3784 font-size: 12px !important;
3788 font-size: 12px !important;
3785 background-color: #eee !important;
3789 background-color: #eee !important;
3786 border: none !important;
3790 border: none !important;
3787 }
3791 }
3788
3792
3789 div.readme .readme_box pre {
3793 div.readme .readme_box pre {
3790 margin: 1em 0;
3794 margin: 1em 0;
3791 font-size: 12px;
3795 font-size: 12px;
3792 background-color: #eee;
3796 background-color: #eee;
3793 border: 1px solid #ddd;
3797 border: 1px solid #ddd;
3794 padding: 5px;
3798 padding: 5px;
3795 color: #444;
3799 color: #444;
3796 overflow: auto;
3800 overflow: auto;
3797 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
3801 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
3798 -webkit-border-radius: 3px;
3802 -webkit-border-radius: 3px;
3799 -moz-border-radius: 3px;
3803 -moz-border-radius: 3px;
3800 border-radius: 3px;
3804 border-radius: 3px;
3801 }
3805 }
3802
3806
3803
3807
3804 /** RST STYLE **/
3808 /** RST STYLE **/
3805
3809
3806
3810
3807 div.rst-block {
3811 div.rst-block {
3808 padding:0px;
3812 padding:0px;
3809 }
3813 }
3810
3814
3811 div.rst-block h2 {
3815 div.rst-block h2 {
3812 font-weight: normal;
3816 font-weight: normal;
3813 }
3817 }
3814
3818
3815 div.rst-block {
3819 div.rst-block {
3816 background-color: #fafafa;
3820 background-color: #fafafa;
3817 }
3821 }
3818
3822
3819 div.rst-block {
3823 div.rst-block {
3820 clear:both;
3824 clear:both;
3821 overflow:hidden;
3825 overflow:hidden;
3822 margin:0;
3826 margin:0;
3823 padding:0 20px 10px;
3827 padding:0 20px 10px;
3824 }
3828 }
3825
3829
3826 div.rst-block h1, div.rst-block h2, div.rst-block h3, div.rst-block h4, div.rst-block h5, div.rst-block h6 {
3830 div.rst-block h1, div.rst-block h2, div.rst-block h3, div.rst-block h4, div.rst-block h5, div.rst-block h6 {
3827 border-bottom: 0 !important;
3831 border-bottom: 0 !important;
3828 margin: 0 !important;
3832 margin: 0 !important;
3829 padding: 0 !important;
3833 padding: 0 !important;
3830 line-height: 1.5em !important;
3834 line-height: 1.5em !important;
3831 }
3835 }
3832
3836
3833
3837
3834 div.rst-block h1:first-child {
3838 div.rst-block h1:first-child {
3835 padding-top: .25em !important;
3839 padding-top: .25em !important;
3836 }
3840 }
3837
3841
3838 div.rst-block h2, div.rst-block h3 {
3842 div.rst-block h2, div.rst-block h3 {
3839 margin: 1em 0 !important;
3843 margin: 1em 0 !important;
3840 }
3844 }
3841
3845
3842 div.rst-block h2 {
3846 div.rst-block h2 {
3843 margin-top: 1.5em !important;
3847 margin-top: 1.5em !important;
3844 border-top: 4px solid #e0e0e0 !important;
3848 border-top: 4px solid #e0e0e0 !important;
3845 padding-top: .5em !important;
3849 padding-top: .5em !important;
3846 }
3850 }
3847
3851
3848 div.rst-block p {
3852 div.rst-block p {
3849 color: black !important;
3853 color: black !important;
3850 margin: 1em 0 !important;
3854 margin: 1em 0 !important;
3851 line-height: 1.5em !important;
3855 line-height: 1.5em !important;
3852 }
3856 }
3853
3857
3854 div.rst-block ul {
3858 div.rst-block ul {
3855 list-style: disc !important;
3859 list-style: disc !important;
3856 margin: 1em 0 1em 2em !important;
3860 margin: 1em 0 1em 2em !important;
3857 }
3861 }
3858
3862
3859 div.rst-block ol {
3863 div.rst-block ol {
3860 list-style: decimal;
3864 list-style: decimal;
3861 margin: 1em 0 1em 2em !important;
3865 margin: 1em 0 1em 2em !important;
3862 }
3866 }
3863
3867
3864 div.rst-block pre, code {
3868 div.rst-block pre, code {
3865 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
3869 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
3866 }
3870 }
3867
3871
3868 div.rst-block code {
3872 div.rst-block code {
3869 font-size: 12px !important;
3873 font-size: 12px !important;
3870 background-color: ghostWhite !important;
3874 background-color: ghostWhite !important;
3871 color: #444 !important;
3875 color: #444 !important;
3872 padding: 0 .2em !important;
3876 padding: 0 .2em !important;
3873 border: 1px solid #dedede !important;
3877 border: 1px solid #dedede !important;
3874 }
3878 }
3875
3879
3876 div.rst-block pre code {
3880 div.rst-block pre code {
3877 padding: 0 !important;
3881 padding: 0 !important;
3878 font-size: 12px !important;
3882 font-size: 12px !important;
3879 background-color: #eee !important;
3883 background-color: #eee !important;
3880 border: none !important;
3884 border: none !important;
3881 }
3885 }
3882
3886
3883 div.rst-block pre {
3887 div.rst-block pre {
3884 margin: 1em 0;
3888 margin: 1em 0;
3885 font-size: 12px;
3889 font-size: 12px;
3886 background-color: #eee;
3890 background-color: #eee;
3887 border: 1px solid #ddd;
3891 border: 1px solid #ddd;
3888 padding: 5px;
3892 padding: 5px;
3889 color: #444;
3893 color: #444;
3890 overflow: auto;
3894 overflow: auto;
3891 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
3895 -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
3892 -webkit-border-radius: 3px;
3896 -webkit-border-radius: 3px;
3893 -moz-border-radius: 3px;
3897 -moz-border-radius: 3px;
3894 border-radius: 3px;
3898 border-radius: 3px;
3895 }
3899 }
3896
3900
3897
3901
3898 /** comment main **/
3902 /** comment main **/
3899 .comments {
3903 .comments {
3900 padding:10px 20px;
3904 padding:10px 20px;
3901 }
3905 }
3902
3906
3903 .comments .comment {
3907 .comments .comment {
3904 border: 1px solid #ddd;
3908 border: 1px solid #ddd;
3905 margin-top: 10px;
3909 margin-top: 10px;
3906 -webkit-border-radius: 4px;
3910 -webkit-border-radius: 4px;
3907 -moz-border-radius: 4px;
3911 -moz-border-radius: 4px;
3908 border-radius: 4px;
3912 border-radius: 4px;
3909 }
3913 }
3910
3914
3911 .comments .comment .meta {
3915 .comments .comment .meta {
3912 background: #f8f8f8;
3916 background: #f8f8f8;
3913 padding: 4px;
3917 padding: 4px;
3914 border-bottom: 1px solid #ddd;
3918 border-bottom: 1px solid #ddd;
3915 }
3919 }
3916
3920
3917 .comments .comment .meta img {
3921 .comments .comment .meta img {
3918 vertical-align: middle;
3922 vertical-align: middle;
3919 }
3923 }
3920
3924
3921 .comments .comment .meta .user {
3925 .comments .comment .meta .user {
3922 font-weight: bold;
3926 font-weight: bold;
3923 }
3927 }
3924
3928
3925 .comments .comment .meta .date {
3929 .comments .comment .meta .date {
3926 }
3930 }
3927
3931
3928 .comments .comment .text {
3932 .comments .comment .text {
3929 background-color: #FAFAFA;
3933 background-color: #FAFAFA;
3930 }
3934 }
3931 .comment .text div.rst-block p {
3935 .comment .text div.rst-block p {
3932 margin: 0.5em 0px !important;
3936 margin: 0.5em 0px !important;
3933 }
3937 }
3934
3938
3935 .comments .comments-number{
3939 .comments .comments-number{
3936 padding:0px 0px 10px 0px;
3940 padding:0px 0px 10px 0px;
3937 font-weight: bold;
3941 font-weight: bold;
3938 color: #666;
3942 color: #666;
3939 font-size: 16px;
3943 font-size: 16px;
3940 }
3944 }
3941
3945
3942 /** comment form **/
3946 /** comment form **/
3943
3947
3944 .status-block{
3948 .status-block{
3945 height:80px;
3949 height:80px;
3946 clear:both
3950 clear:both
3947 }
3951 }
3948
3952
3949 .comment-form .clearfix{
3953 .comment-form .clearfix{
3950 background: #EEE;
3954 background: #EEE;
3951 -webkit-border-radius: 4px;
3955 -webkit-border-radius: 4px;
3952 -moz-border-radius: 4px;
3956 -moz-border-radius: 4px;
3953 border-radius: 4px;
3957 border-radius: 4px;
3954 padding: 10px;
3958 padding: 10px;
3955 }
3959 }
3956
3960
3957 div.comment-form {
3961 div.comment-form {
3958 margin-top: 20px;
3962 margin-top: 20px;
3959 }
3963 }
3960
3964
3961 .comment-form strong {
3965 .comment-form strong {
3962 display: block;
3966 display: block;
3963 margin-bottom: 15px;
3967 margin-bottom: 15px;
3964 }
3968 }
3965
3969
3966 .comment-form textarea {
3970 .comment-form textarea {
3967 width: 100%;
3971 width: 100%;
3968 height: 100px;
3972 height: 100px;
3969 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
3973 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
3970 }
3974 }
3971
3975
3972 form.comment-form {
3976 form.comment-form {
3973 margin-top: 10px;
3977 margin-top: 10px;
3974 margin-left: 10px;
3978 margin-left: 10px;
3975 }
3979 }
3976
3980
3977 .comment-form-submit {
3981 .comment-form-submit {
3978 margin-top: 5px;
3982 margin-top: 5px;
3979 margin-left: 525px;
3983 margin-left: 525px;
3980 }
3984 }
3981
3985
3982 .file-comments {
3986 .file-comments {
3983 display: none;
3987 display: none;
3984 }
3988 }
3985
3989
3986 .comment-form .comment {
3990 .comment-form .comment {
3987 margin-left: 10px;
3991 margin-left: 10px;
3988 }
3992 }
3989
3993
3990 .comment-form .comment-help{
3994 .comment-form .comment-help{
3991 padding: 0px 0px 5px 0px;
3995 padding: 0px 0px 5px 0px;
3992 color: #666;
3996 color: #666;
3993 }
3997 }
3994
3998
3995 .comment-form .comment-button{
3999 .comment-form .comment-button{
3996 padding-top:5px;
4000 padding-top:5px;
3997 }
4001 }
3998
4002
3999 .add-another-button {
4003 .add-another-button {
4000 margin-left: 10px;
4004 margin-left: 10px;
4001 margin-top: 10px;
4005 margin-top: 10px;
4002 margin-bottom: 10px;
4006 margin-bottom: 10px;
4003 }
4007 }
4004
4008
4005 .comment .buttons {
4009 .comment .buttons {
4006 float: right;
4010 float: right;
4007 padding:2px 2px 0px 0px;
4011 padding:2px 2px 0px 0px;
4008 }
4012 }
4009
4013
4010
4014
4011 .show-inline-comments{
4015 .show-inline-comments{
4012 position: relative;
4016 position: relative;
4013 top:1px
4017 top:1px
4014 }
4018 }
4015
4019
4016 /** comment inline form **/
4020 /** comment inline form **/
4017 .comment-inline-form .overlay{
4021 .comment-inline-form .overlay{
4018 display: none;
4022 display: none;
4019 }
4023 }
4020 .comment-inline-form .overlay.submitting{
4024 .comment-inline-form .overlay.submitting{
4021 display:block;
4025 display:block;
4022 background: none repeat scroll 0 0 white;
4026 background: none repeat scroll 0 0 white;
4023 font-size: 16px;
4027 font-size: 16px;
4024 opacity: 0.5;
4028 opacity: 0.5;
4025 position: absolute;
4029 position: absolute;
4026 text-align: center;
4030 text-align: center;
4027 vertical-align: top;
4031 vertical-align: top;
4028
4032
4029 }
4033 }
4030 .comment-inline-form .overlay.submitting .overlay-text{
4034 .comment-inline-form .overlay.submitting .overlay-text{
4031 width:100%;
4035 width:100%;
4032 margin-top:5%;
4036 margin-top:5%;
4033 }
4037 }
4034
4038
4035 .comment-inline-form .clearfix{
4039 .comment-inline-form .clearfix{
4036 background: #EEE;
4040 background: #EEE;
4037 -webkit-border-radius: 4px;
4041 -webkit-border-radius: 4px;
4038 -moz-border-radius: 4px;
4042 -moz-border-radius: 4px;
4039 border-radius: 4px;
4043 border-radius: 4px;
4040 padding: 5px;
4044 padding: 5px;
4041 }
4045 }
4042
4046
4043 div.comment-inline-form {
4047 div.comment-inline-form {
4044 margin-top: 5px;
4048 margin-top: 5px;
4045 padding:2px 6px 8px 6px;
4049 padding:2px 6px 8px 6px;
4046
4050
4047 }
4051 }
4048
4052
4049 .comment-inline-form strong {
4053 .comment-inline-form strong {
4050 display: block;
4054 display: block;
4051 margin-bottom: 15px;
4055 margin-bottom: 15px;
4052 }
4056 }
4053
4057
4054 .comment-inline-form textarea {
4058 .comment-inline-form textarea {
4055 width: 100%;
4059 width: 100%;
4056 height: 100px;
4060 height: 100px;
4057 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
4061 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
4058 }
4062 }
4059
4063
4060 form.comment-inline-form {
4064 form.comment-inline-form {
4061 margin-top: 10px;
4065 margin-top: 10px;
4062 margin-left: 10px;
4066 margin-left: 10px;
4063 }
4067 }
4064
4068
4065 .comment-inline-form-submit {
4069 .comment-inline-form-submit {
4066 margin-top: 5px;
4070 margin-top: 5px;
4067 margin-left: 525px;
4071 margin-left: 525px;
4068 }
4072 }
4069
4073
4070 .file-comments {
4074 .file-comments {
4071 display: none;
4075 display: none;
4072 }
4076 }
4073
4077
4074 .comment-inline-form .comment {
4078 .comment-inline-form .comment {
4075 margin-left: 10px;
4079 margin-left: 10px;
4076 }
4080 }
4077
4081
4078 .comment-inline-form .comment-help{
4082 .comment-inline-form .comment-help{
4079 padding: 0px 0px 2px 0px;
4083 padding: 0px 0px 2px 0px;
4080 color: #666666;
4084 color: #666666;
4081 font-size: 10px;
4085 font-size: 10px;
4082 }
4086 }
4083
4087
4084 .comment-inline-form .comment-button{
4088 .comment-inline-form .comment-button{
4085 padding-top:5px;
4089 padding-top:5px;
4086 }
4090 }
4087
4091
4088 /** comment inline **/
4092 /** comment inline **/
4089 .inline-comments {
4093 .inline-comments {
4090 padding:10px 20px;
4094 padding:10px 20px;
4091 }
4095 }
4092
4096
4093 .inline-comments div.rst-block {
4097 .inline-comments div.rst-block {
4094 clear:both;
4098 clear:both;
4095 overflow:hidden;
4099 overflow:hidden;
4096 margin:0;
4100 margin:0;
4097 padding:0 20px 0px;
4101 padding:0 20px 0px;
4098 }
4102 }
4099 .inline-comments .comment {
4103 .inline-comments .comment {
4100 border: 1px solid #ddd;
4104 border: 1px solid #ddd;
4101 -webkit-border-radius: 4px;
4105 -webkit-border-radius: 4px;
4102 -moz-border-radius: 4px;
4106 -moz-border-radius: 4px;
4103 border-radius: 4px;
4107 border-radius: 4px;
4104 margin: 3px 3px 5px 5px;
4108 margin: 3px 3px 5px 5px;
4105 background-color: #FAFAFA;
4109 background-color: #FAFAFA;
4106 }
4110 }
4107 .inline-comments .add-comment {
4111 .inline-comments .add-comment {
4108 padding: 2px 4px 8px 5px;
4112 padding: 2px 4px 8px 5px;
4109 }
4113 }
4110
4114
4111 .inline-comments .comment-wrapp{
4115 .inline-comments .comment-wrapp{
4112 padding:1px;
4116 padding:1px;
4113 }
4117 }
4114 .inline-comments .comment .meta {
4118 .inline-comments .comment .meta {
4115 background: #f8f8f8;
4119 background: #f8f8f8;
4116 padding: 4px;
4120 padding: 4px;
4117 border-bottom: 1px solid #ddd;
4121 border-bottom: 1px solid #ddd;
4118 }
4122 }
4119
4123
4120 .inline-comments .comment .meta img {
4124 .inline-comments .comment .meta img {
4121 vertical-align: middle;
4125 vertical-align: middle;
4122 }
4126 }
4123
4127
4124 .inline-comments .comment .meta .user {
4128 .inline-comments .comment .meta .user {
4125 font-weight: bold;
4129 font-weight: bold;
4126 }
4130 }
4127
4131
4128 .inline-comments .comment .meta .date {
4132 .inline-comments .comment .meta .date {
4129 }
4133 }
4130
4134
4131 .inline-comments .comment .text {
4135 .inline-comments .comment .text {
4132 background-color: #FAFAFA;
4136 background-color: #FAFAFA;
4133 }
4137 }
4134
4138
4135 .inline-comments .comments-number{
4139 .inline-comments .comments-number{
4136 padding:0px 0px 10px 0px;
4140 padding:0px 0px 10px 0px;
4137 font-weight: bold;
4141 font-weight: bold;
4138 color: #666;
4142 color: #666;
4139 font-size: 16px;
4143 font-size: 16px;
4140 }
4144 }
4141 .inline-comments-button .add-comment{
4145 .inline-comments-button .add-comment{
4142 margin:2px 0px 8px 5px !important
4146 margin:2px 0px 8px 5px !important
4143 }
4147 }
4144
4148
4145
4149
4146 .notification-paginator{
4150 .notification-paginator{
4147 padding: 0px 0px 4px 16px;
4151 padding: 0px 0px 4px 16px;
4148 float: left;
4152 float: left;
4149 }
4153 }
4150
4154
4151 .notifications{
4155 .notifications{
4152 border-radius: 4px 4px 4px 4px;
4156 border-radius: 4px 4px 4px 4px;
4153 -webkit-border-radius: 4px;
4157 -webkit-border-radius: 4px;
4154 -moz-border-radius: 4px;
4158 -moz-border-radius: 4px;
4155 float: right;
4159 float: right;
4156 margin: 20px 0px 0px 0px;
4160 margin: 20px 0px 0px 0px;
4157 position: absolute;
4161 position: absolute;
4158 text-align: center;
4162 text-align: center;
4159 width: 26px;
4163 width: 26px;
4160 z-index: 1000;
4164 z-index: 1000;
4161 }
4165 }
4162 .notifications a{
4166 .notifications a{
4163 color:#888 !important;
4167 color:#888 !important;
4164 display: block;
4168 display: block;
4165 font-size: 10px;
4169 font-size: 10px;
4166 background-color: #DEDEDE !important;
4170 background-color: #DEDEDE !important;
4167 border-radius: 2px !important;
4171 border-radius: 2px !important;
4168 -webkit-border-radius: 2px !important;
4172 -webkit-border-radius: 2px !important;
4169 -moz-border-radius: 2px !important;
4173 -moz-border-radius: 2px !important;
4170 }
4174 }
4171 .notifications a:hover{
4175 .notifications a:hover{
4172 text-decoration: none !important;
4176 text-decoration: none !important;
4173 background-color: #EEEFFF !important;
4177 background-color: #EEEFFF !important;
4174 }
4178 }
4175 .notification-header{
4179 .notification-header{
4176 padding-top:6px;
4180 padding-top:6px;
4177 }
4181 }
4178 .notification-header .desc{
4182 .notification-header .desc{
4179 font-size: 16px;
4183 font-size: 16px;
4180 height: 24px;
4184 height: 24px;
4181 float: left
4185 float: left
4182 }
4186 }
4183 .notification-list .container.unread{
4187 .notification-list .container.unread{
4184 background: none repeat scroll 0 0 rgba(255, 255, 180, 0.6);
4188 background: none repeat scroll 0 0 rgba(255, 255, 180, 0.6);
4185 }
4189 }
4186 .notification-header .gravatar{
4190 .notification-header .gravatar{
4187 background: none repeat scroll 0 0 transparent;
4191 background: none repeat scroll 0 0 transparent;
4188 padding: 0px 0px 0px 8px;
4192 padding: 0px 0px 0px 8px;
4189 }
4193 }
4190 .notification-header .desc.unread{
4194 .notification-header .desc.unread{
4191 font-weight: bold;
4195 font-weight: bold;
4192 font-size: 17px;
4196 font-size: 17px;
4193 }
4197 }
4194 .notification-table{
4198 .notification-table{
4195 border: 1px solid #ccc;
4199 border: 1px solid #ccc;
4196 -webkit-border-radius: 6px 6px 6px 6px;
4200 -webkit-border-radius: 6px 6px 6px 6px;
4197 -moz-border-radius: 6px 6px 6px 6px;
4201 -moz-border-radius: 6px 6px 6px 6px;
4198 border-radius: 6px 6px 6px 6px;
4202 border-radius: 6px 6px 6px 6px;
4199 clear: both;
4203 clear: both;
4200 margin: 0px 20px 0px 20px;
4204 margin: 0px 20px 0px 20px;
4201 }
4205 }
4202 .notification-header .delete-notifications{
4206 .notification-header .delete-notifications{
4203 float: right;
4207 float: right;
4204 padding-top: 8px;
4208 padding-top: 8px;
4205 cursor: pointer;
4209 cursor: pointer;
4206 }
4210 }
4207 .notification-subject{
4211 .notification-subject{
4208 clear:both;
4212 clear:both;
4209 border-bottom: 1px solid #eee;
4213 border-bottom: 1px solid #eee;
4210 padding:5px 0px 5px 38px;
4214 padding:5px 0px 5px 38px;
4211 }
4215 }
4212
4216
4213 .notification-body{
4217 .notification-body{
4214 clear:both;
4218 clear:both;
4215 margin: 34px 2px 2px 8px
4219 margin: 34px 2px 2px 8px
4216 }
4220 }
4217
4221
4218 /****
4222 /****
4219 PERMS
4223 PERMS
4220 *****/
4224 *****/
4221 #perms .perms_section_head {
4225 #perms .perms_section_head {
4222 padding:10px 10px 10px 0px;
4226 padding:10px 10px 10px 0px;
4223 font-size:16px;
4227 font-size:16px;
4224 font-weight: bold;
4228 font-weight: bold;
4225 }
4229 }
4226
4230
4227 #perms .perm_tag{
4231 #perms .perm_tag{
4228 padding: 1px 3px 1px 3px;
4232 padding: 1px 3px 1px 3px;
4229 font-size: 10px;
4233 font-size: 10px;
4230 font-weight: bold;
4234 font-weight: bold;
4231 text-transform: uppercase;
4235 text-transform: uppercase;
4232 white-space: nowrap;
4236 white-space: nowrap;
4233 -webkit-border-radius: 3px;
4237 -webkit-border-radius: 3px;
4234 -moz-border-radius: 3px;
4238 -moz-border-radius: 3px;
4235 border-radius: 3px;
4239 border-radius: 3px;
4236 }
4240 }
4237
4241
4238 #perms .perm_tag.admin{
4242 #perms .perm_tag.admin{
4239 background-color: #B94A48;
4243 background-color: #B94A48;
4240 color: #ffffff;
4244 color: #ffffff;
4241 }
4245 }
4242
4246
4243 #perms .perm_tag.write{
4247 #perms .perm_tag.write{
4244 background-color: #B94A48;
4248 background-color: #B94A48;
4245 color: #ffffff;
4249 color: #ffffff;
4246 }
4250 }
4247
4251
4248 #perms .perm_tag.read{
4252 #perms .perm_tag.read{
4249 background-color: #468847;
4253 background-color: #468847;
4250 color: #ffffff;
4254 color: #ffffff;
4251 }
4255 }
4252
4256
4253 #perms .perm_tag.none{
4257 #perms .perm_tag.none{
4254 background-color: #bfbfbf;
4258 background-color: #bfbfbf;
4255 color: #ffffff;
4259 color: #ffffff;
4256 }
4260 }
4257
4261
4258 .perm-gravatar{
4262 .perm-gravatar{
4259 vertical-align:middle;
4263 vertical-align:middle;
4260 padding:2px;
4264 padding:2px;
4261 }
4265 }
4262 .perm-gravatar-ac{
4266 .perm-gravatar-ac{
4263 vertical-align:middle;
4267 vertical-align:middle;
4264 padding:2px;
4268 padding:2px;
4265 width: 14px;
4269 width: 14px;
4266 height: 14px;
4270 height: 14px;
4267 }
4271 }
4268
4272
4269 /*****************************************************************************
4273 /*****************************************************************************
4270 DIFFS CSS
4274 DIFFS CSS
4271 ******************************************************************************/
4275 ******************************************************************************/
4272
4276
4273 div.diffblock {
4277 div.diffblock {
4274 overflow: auto;
4278 overflow: auto;
4275 padding: 0px;
4279 padding: 0px;
4276 border: 1px solid #ccc;
4280 border: 1px solid #ccc;
4277 background: #f8f8f8;
4281 background: #f8f8f8;
4278 font-size: 100%;
4282 font-size: 100%;
4279 line-height: 100%;
4283 line-height: 100%;
4280 /* new */
4284 /* new */
4281 line-height: 125%;
4285 line-height: 125%;
4282 -webkit-border-radius: 6px 6px 0px 0px;
4286 -webkit-border-radius: 6px 6px 0px 0px;
4283 -moz-border-radius: 6px 6px 0px 0px;
4287 -moz-border-radius: 6px 6px 0px 0px;
4284 border-radius: 6px 6px 0px 0px;
4288 border-radius: 6px 6px 0px 0px;
4285 }
4289 }
4286 div.diffblock.margined{
4290 div.diffblock.margined{
4287 margin: 0px 20px 0px 20px;
4291 margin: 0px 20px 0px 20px;
4288 }
4292 }
4289 div.diffblock .code-header{
4293 div.diffblock .code-header{
4290 border-bottom: 1px solid #CCCCCC;
4294 border-bottom: 1px solid #CCCCCC;
4291 background: #EEEEEE;
4295 background: #EEEEEE;
4292 padding:10px 0 10px 0;
4296 padding:10px 0 10px 0;
4293 height: 14px;
4297 height: 14px;
4294 }
4298 }
4295 div.diffblock .code-header.cv{
4299 div.diffblock .code-header.cv{
4296 height: 34px;
4300 height: 34px;
4297 }
4301 }
4298 div.diffblock .code-header-title{
4302 div.diffblock .code-header-title{
4299 padding: 0px 0px 10px 5px !important;
4303 padding: 0px 0px 10px 5px !important;
4300 margin: 0 !important;
4304 margin: 0 !important;
4301 }
4305 }
4302 div.diffblock .code-header .hash{
4306 div.diffblock .code-header .hash{
4303 float: left;
4307 float: left;
4304 padding: 2px 0 0 2px;
4308 padding: 2px 0 0 2px;
4305 }
4309 }
4306 div.diffblock .code-header .date{
4310 div.diffblock .code-header .date{
4307 float:left;
4311 float:left;
4308 text-transform: uppercase;
4312 text-transform: uppercase;
4309 padding: 2px 0px 0px 2px;
4313 padding: 2px 0px 0px 2px;
4310 }
4314 }
4311 div.diffblock .code-header div{
4315 div.diffblock .code-header div{
4312 margin-left:4px;
4316 margin-left:4px;
4313 font-weight: bold;
4317 font-weight: bold;
4314 font-size: 14px;
4318 font-size: 14px;
4315 }
4319 }
4316 div.diffblock .code-body{
4320 div.diffblock .code-body{
4317 background: #FFFFFF;
4321 background: #FFFFFF;
4318 }
4322 }
4319 div.diffblock pre.raw{
4323 div.diffblock pre.raw{
4320 background: #FFFFFF;
4324 background: #FFFFFF;
4321 color:#000000;
4325 color:#000000;
4322 }
4326 }
4323 table.code-difftable{
4327 table.code-difftable{
4324 border-collapse: collapse;
4328 border-collapse: collapse;
4325 width: 99%;
4329 width: 99%;
4326 }
4330 }
4327 table.code-difftable td {
4331 table.code-difftable td {
4328 padding: 0 !important;
4332 padding: 0 !important;
4329 background: none !important;
4333 background: none !important;
4330 border:0 !important;
4334 border:0 !important;
4331 vertical-align: none !important;
4335 vertical-align: none !important;
4332 }
4336 }
4333 table.code-difftable .context{
4337 table.code-difftable .context{
4334 background:none repeat scroll 0 0 #DDE7EF;
4338 background:none repeat scroll 0 0 #DDE7EF;
4335 }
4339 }
4336 table.code-difftable .add{
4340 table.code-difftable .add{
4337 background:none repeat scroll 0 0 #DDFFDD;
4341 background:none repeat scroll 0 0 #DDFFDD;
4338 }
4342 }
4339 table.code-difftable .add ins{
4343 table.code-difftable .add ins{
4340 background:none repeat scroll 0 0 #AAFFAA;
4344 background:none repeat scroll 0 0 #AAFFAA;
4341 text-decoration:none;
4345 text-decoration:none;
4342 }
4346 }
4343 table.code-difftable .del{
4347 table.code-difftable .del{
4344 background:none repeat scroll 0 0 #FFDDDD;
4348 background:none repeat scroll 0 0 #FFDDDD;
4345 }
4349 }
4346 table.code-difftable .del del{
4350 table.code-difftable .del del{
4347 background:none repeat scroll 0 0 #FFAAAA;
4351 background:none repeat scroll 0 0 #FFAAAA;
4348 text-decoration:none;
4352 text-decoration:none;
4349 }
4353 }
4350
4354
4351 /** LINE NUMBERS **/
4355 /** LINE NUMBERS **/
4352 table.code-difftable .lineno{
4356 table.code-difftable .lineno{
4353
4357
4354 padding-left:2px;
4358 padding-left:2px;
4355 padding-right:2px;
4359 padding-right:2px;
4356 text-align:right;
4360 text-align:right;
4357 width:32px;
4361 width:32px;
4358 -moz-user-select:none;
4362 -moz-user-select:none;
4359 -webkit-user-select: none;
4363 -webkit-user-select: none;
4360 border-right: 1px solid #CCC !important;
4364 border-right: 1px solid #CCC !important;
4361 border-left: 0px solid #CCC !important;
4365 border-left: 0px solid #CCC !important;
4362 border-top: 0px solid #CCC !important;
4366 border-top: 0px solid #CCC !important;
4363 border-bottom: none !important;
4367 border-bottom: none !important;
4364 vertical-align: middle !important;
4368 vertical-align: middle !important;
4365
4369
4366 }
4370 }
4367 table.code-difftable .lineno.new {
4371 table.code-difftable .lineno.new {
4368 }
4372 }
4369 table.code-difftable .lineno.old {
4373 table.code-difftable .lineno.old {
4370 }
4374 }
4371 table.code-difftable .lineno a{
4375 table.code-difftable .lineno a{
4372 color:#747474 !important;
4376 color:#747474 !important;
4373 font:11px "Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace !important;
4377 font:11px "Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace !important;
4374 letter-spacing:-1px;
4378 letter-spacing:-1px;
4375 text-align:right;
4379 text-align:right;
4376 padding-right: 2px;
4380 padding-right: 2px;
4377 cursor: pointer;
4381 cursor: pointer;
4378 display: block;
4382 display: block;
4379 width: 32px;
4383 width: 32px;
4380 }
4384 }
4381
4385
4382 table.code-difftable .lineno-inline{
4386 table.code-difftable .lineno-inline{
4383 background:none repeat scroll 0 0 #FFF !important;
4387 background:none repeat scroll 0 0 #FFF !important;
4384 padding-left:2px;
4388 padding-left:2px;
4385 padding-right:2px;
4389 padding-right:2px;
4386 text-align:right;
4390 text-align:right;
4387 width:30px;
4391 width:30px;
4388 -moz-user-select:none;
4392 -moz-user-select:none;
4389 -webkit-user-select: none;
4393 -webkit-user-select: none;
4390 }
4394 }
4391
4395
4392 /** CODE **/
4396 /** CODE **/
4393 table.code-difftable .code {
4397 table.code-difftable .code {
4394 display: block;
4398 display: block;
4395 width: 100%;
4399 width: 100%;
4396 }
4400 }
4397 table.code-difftable .code td{
4401 table.code-difftable .code td{
4398 margin:0;
4402 margin:0;
4399 padding:0;
4403 padding:0;
4400 }
4404 }
4401 table.code-difftable .code pre{
4405 table.code-difftable .code pre{
4402 margin:0;
4406 margin:0;
4403 padding:0;
4407 padding:0;
4404 height: 17px;
4408 height: 17px;
4405 line-height: 17px;
4409 line-height: 17px;
4406 }
4410 }
4407
4411
4408
4412
4409 .diffblock.margined.comm .line .code:hover{
4413 .diffblock.margined.comm .line .code:hover{
4410 background-color:#FFFFCC !important;
4414 background-color:#FFFFCC !important;
4411 cursor: pointer !important;
4415 cursor: pointer !important;
4412 background-image:url("../images/icons/comment_add.png") !important;
4416 background-image:url("../images/icons/comment_add.png") !important;
4413 background-repeat:no-repeat !important;
4417 background-repeat:no-repeat !important;
4414 background-position: right !important;
4418 background-position: right !important;
4415 background-position: 0% 50% !important;
4419 background-position: 0% 50% !important;
4416 }
4420 }
4417 .diffblock.margined.comm .line .code.no-comment:hover{
4421 .diffblock.margined.comm .line .code.no-comment:hover{
4418 background-image: none !important;
4422 background-image: none !important;
4419 cursor: auto !important;
4423 cursor: auto !important;
4420 background-color: inherit !important;
4424 background-color: inherit !important;
4421
4425
4422 }
4426 }
@@ -1,237 +1,245 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.html"/>
3 <%inherit file="/base/base.html"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 ${c.repo_name} ${_('Changelog')} - ${c.rhodecode_name}
6 ${c.repo_name} ${_('Changelog')} - ${c.rhodecode_name}
7 </%def>
7 </%def>
8
8
9 <%def name="breadcrumbs_links()">
9 <%def name="breadcrumbs_links()">
10 ${h.link_to(u'Home',h.url('/'))}
10 ${h.link_to(u'Home',h.url('/'))}
11 &raquo;
11 &raquo;
12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 &raquo;
13 &raquo;
14 ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
14 ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
15 </%def>
15 </%def>
16
16
17 <%def name="page_nav()">
17 <%def name="page_nav()">
18 ${self.menu('changelog')}
18 ${self.menu('changelog')}
19 </%def>
19 </%def>
20
20
21 <%def name="main()">
21 <%def name="main()">
22 <div class="box">
22 <div class="box">
23 <!-- box / title -->
23 <!-- box / title -->
24 <div class="title">
24 <div class="title">
25 ${self.breadcrumbs()}
25 ${self.breadcrumbs()}
26 </div>
26 </div>
27 <div class="table">
27 <div class="table">
28 % if c.pagination:
28 % if c.pagination:
29 <div id="graph">
29 <div id="graph">
30 <div id="graph_nodes">
30 <div id="graph_nodes">
31 <canvas id="graph_canvas"></canvas>
31 <canvas id="graph_canvas"></canvas>
32 </div>
32 </div>
33 <div id="graph_content">
33 <div id="graph_content">
34 <div class="info_box" style="clear: both;padding: 10px 6px;vertical-align: right;text-align: right;"><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}" class="ui-btn small">${_('Open new pull request')}</a></div>
34 <div class="info_box" style="clear: both;padding: 10px 6px;vertical-align: right;text-align: right;"><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}" class="ui-btn small">${_('Open new pull request')}</a></div>
35 <div class="container_header">
35 <div class="container_header">
36 ${h.form(h.url.current(),method='get')}
36 ${h.form(h.url.current(),method='get')}
37 <div class="info_box" style="float:left">
37 <div class="info_box" style="float:left">
38 ${h.submit('set',_('Show'),class_="ui-btn")}
38 ${h.submit('set',_('Show'),class_="ui-btn")}
39 ${h.text('size',size=1,value=c.size)}
39 ${h.text('size',size=1,value=c.size)}
40 ${_('revisions')}
40 ${_('revisions')}
41 </div>
41 </div>
42 ${h.end_form()}
42 ${h.end_form()}
43 <div id="rev_range_container" style="display:none"></div>
43 <div id="rev_range_container" style="display:none"></div>
44 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
44 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
45 </div>
45 </div>
46
46
47 %for cnt,cs in enumerate(c.pagination):
47 %for cnt,cs in enumerate(c.pagination):
48 <div id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
48 <div id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
49 <div class="left">
49 <div class="left">
50 <div>
50 <div>
51 ${h.checkbox(cs.short_id,class_="changeset_range")}
51 ${h.checkbox(cs.short_id,class_="changeset_range")}
52 <span class="tooltip" title="${h.age(cs.date)}"><a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}"><span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span></a></span>
52 <span class="tooltip" title="${h.age(cs.date)}"><a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}"><span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span></a></span>
53 </div>
53 </div>
54 <div class="author">
54 <div class="author">
55 <div class="gravatar">
55 <div class="gravatar">
56 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
56 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
57 </div>
57 </div>
58 <div title="${cs.author}" class="user">${h.person(cs.author)}</div>
58 <div title="${cs.author}" class="user">${h.person(cs.author)}</div>
59 </div>
59 </div>
60 <div class="date">${cs.date}</div>
60 <div class="date">${cs.date}</div>
61 </div>
61 </div>
62 <div class="mid">
62 <div class="mid">
63 <div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
63 <div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
64 <div class="expand"><span class="expandtext">&darr; ${_('show more')} &darr;</span></div>
64 <div class="expand"><span class="expandtext">&darr; ${_('show more')} &darr;</span></div>
65 </div>
65 </div>
66 <div class="right">
66 <div class="right">
67 <div class="changes">
67 <div class="changes">
68 <div id="${cs.raw_id}" style="float:right;" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</div>
68 <div id="${cs.raw_id}" style="float:right;" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</div>
69 <div class="comments-container">
69 <div class="comments-container">
70 %if len(c.comments.get(cs.raw_id,[])) > 0:
70 %if len(c.comments.get(cs.raw_id,[])) > 0:
71 <div class="comments-cnt" title="${('comments')}">
71 <div class="comments-cnt" title="${('comments')}">
72 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
72 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
73 <div class="comments-cnt">${len(c.comments[cs.raw_id])}</div>
73 <div class="comments-cnt">${len(c.comments[cs.raw_id])}</div>
74 <img src="${h.url('/images/icons/comments.png')}">
74 <img src="${h.url('/images/icons/comments.png')}">
75 </a>
75 </a>
76 </div>
76 </div>
77 %endif
77 %endif
78 </div>
78 </div>
79 <div class="changeset-status-container">
79 <div class="changeset-status-container">
80 %if c.statuses.get(cs.raw_id):
80 %if c.statuses.get(cs.raw_id):
81 <div title="${_('Changeset status')}" class="changeset-status-lbl">${c.statuses.get(cs.raw_id)[1]}</div>
81 <div title="${_('Changeset status')}" class="changeset-status-lbl">${c.statuses.get(cs.raw_id)[1]}</div>
82 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" /></div>
82 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" /></div>
83 %endif
83 %endif
84 </div>
84 </div>
85 </div>
85 </div>
86 %if cs.parents:
86 %if cs.parents:
87 %for p_cs in reversed(cs.parents):
87 %for p_cs in reversed(cs.parents):
88 <div class="parent">${_('Parent')}
88 <div class="parent">${_('Parent')}
89 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
89 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
90 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
90 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
91 </div>
91 </div>
92 %endfor
92 %endfor
93 %else:
93 %else:
94 <div class="parent">${_('No parents')}</div>
94 <div class="parent">${_('No parents')}</div>
95 %endif
95 %endif
96
96
97 <span class="logtags">
97 <span class="logtags">
98 %if len(cs.parents)>1:
98 %if len(cs.parents)>1:
99 <span class="merge">${_('merge')}</span>
99 <span class="merge">${_('merge')}</span>
100 %endif
100 %endif
101 %if cs.branch:
101 %if cs.branch:
102 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
102 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
103 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
103 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
104 </span>
104 %endif
105 %endif
106 %if h.is_hg(c.rhodecode_repo):
107 %for book in cs.bookmarks:
108 <span class="bookbook" title="${'%s %s' % (_('bookmark'),book)}">
109 ${h.link_to(h.shorter(book),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
110 </span>
111 %endfor
112 %endif
105 %for tag in cs.tags:
113 %for tag in cs.tags:
106 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
114 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
107 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
115 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
108 %endfor
116 %endfor
109 </span>
117 </span>
110 </div>
118 </div>
111 </div>
119 </div>
112
120
113 %endfor
121 %endfor
114 <div class="pagination-wh pagination-left">
122 <div class="pagination-wh pagination-left">
115 ${c.pagination.pager('$link_previous ~2~ $link_next')}
123 ${c.pagination.pager('$link_previous ~2~ $link_next')}
116 </div>
124 </div>
117 </div>
125 </div>
118 </div>
126 </div>
119
127
120 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
128 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
121 <script type="text/javascript">
129 <script type="text/javascript">
122 YAHOO.util.Event.onDOMReady(function(){
130 YAHOO.util.Event.onDOMReady(function(){
123
131
124 //Monitor range checkboxes and build a link to changesets
132 //Monitor range checkboxes and build a link to changesets
125 //ranges
133 //ranges
126 var checkboxes = YUD.getElementsByClassName('changeset_range');
134 var checkboxes = YUD.getElementsByClassName('changeset_range');
127 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
135 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
128 YUE.on(checkboxes,'click',function(e){
136 YUE.on(checkboxes,'click',function(e){
129 var checked_checkboxes = [];
137 var checked_checkboxes = [];
130 for (pos in checkboxes){
138 for (pos in checkboxes){
131 if(checkboxes[pos].checked){
139 if(checkboxes[pos].checked){
132 checked_checkboxes.push(checkboxes[pos]);
140 checked_checkboxes.push(checkboxes[pos]);
133 }
141 }
134 }
142 }
135 if(checked_checkboxes.length>1){
143 if(checked_checkboxes.length>1){
136 var rev_end = checked_checkboxes[0].name;
144 var rev_end = checked_checkboxes[0].name;
137 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
145 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
138
146
139 var url = url_tmpl.replace('__REVRANGE__',
147 var url = url_tmpl.replace('__REVRANGE__',
140 rev_start+'...'+rev_end);
148 rev_start+'...'+rev_end);
141
149
142 var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
150 var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
143 link = link.replace('__S',rev_start);
151 link = link.replace('__S',rev_start);
144 link = link.replace('__E',rev_end);
152 link = link.replace('__E',rev_end);
145 YUD.get('rev_range_container').innerHTML = link;
153 YUD.get('rev_range_container').innerHTML = link;
146 YUD.setStyle('rev_range_container','display','');
154 YUD.setStyle('rev_range_container','display','');
147 }
155 }
148 else{
156 else{
149 YUD.setStyle('rev_range_container','display','none');
157 YUD.setStyle('rev_range_container','display','none');
150
158
151 }
159 }
152 });
160 });
153
161
154 var msgs = YUQ('.message');
162 var msgs = YUQ('.message');
155 // get first element height
163 // get first element height
156 var el = YUQ('#graph_content .container')[0];
164 var el = YUQ('#graph_content .container')[0];
157 var row_h = el.clientHeight;
165 var row_h = el.clientHeight;
158 for(var i=0;i<msgs.length;i++){
166 for(var i=0;i<msgs.length;i++){
159 var m = msgs[i];
167 var m = msgs[i];
160
168
161 var h = m.clientHeight;
169 var h = m.clientHeight;
162 var pad = YUD.getStyle(m,'padding');
170 var pad = YUD.getStyle(m,'padding');
163 if(h > row_h){
171 if(h > row_h){
164 var offset = row_h - (h+12);
172 var offset = row_h - (h+12);
165 YUD.setStyle(m.nextElementSibling,'display','block');
173 YUD.setStyle(m.nextElementSibling,'display','block');
166 YUD.setStyle(m.nextElementSibling,'margin-top',offset+'px');
174 YUD.setStyle(m.nextElementSibling,'margin-top',offset+'px');
167 };
175 };
168 }
176 }
169 YUE.on(YUQ('.expand'),'click',function(e){
177 YUE.on(YUQ('.expand'),'click',function(e){
170 var elem = e.currentTarget.parentNode.parentNode;
178 var elem = e.currentTarget.parentNode.parentNode;
171 YUD.setStyle(e.currentTarget,'display','none');
179 YUD.setStyle(e.currentTarget,'display','none');
172 YUD.setStyle(elem,'height','auto');
180 YUD.setStyle(elem,'height','auto');
173
181
174 //redraw the graph, max_w and jsdata are global vars
182 //redraw the graph, max_w and jsdata are global vars
175 set_canvas(max_w);
183 set_canvas(max_w);
176
184
177 var r = new BranchRenderer();
185 var r = new BranchRenderer();
178 r.render(jsdata,max_w);
186 r.render(jsdata,max_w);
179
187
180 })
188 })
181
189
182 // Fetch changeset details
190 // Fetch changeset details
183 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
191 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
184 var id = e.currentTarget.id
192 var id = e.currentTarget.id
185 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
193 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
186 var url = url.replace('__CS__',id);
194 var url = url.replace('__CS__',id);
187 ypjax(url,id,function(){tooltip_activate()});
195 ypjax(url,id,function(){tooltip_activate()});
188 });
196 });
189
197
190 // change branch filter
198 // change branch filter
191 YUE.on(YUD.get('branch_filter'),'change',function(e){
199 YUE.on(YUD.get('branch_filter'),'change',function(e){
192 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
200 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
193 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
201 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
194 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
202 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
195 var url = url.replace('__BRANCH__',selected_branch);
203 var url = url.replace('__BRANCH__',selected_branch);
196 if(selected_branch != ''){
204 if(selected_branch != ''){
197 window.location = url;
205 window.location = url;
198 }else{
206 }else{
199 window.location = url_main;
207 window.location = url_main;
200 }
208 }
201
209
202 });
210 });
203
211
204 function set_canvas(heads) {
212 function set_canvas(heads) {
205 var c = document.getElementById('graph_nodes');
213 var c = document.getElementById('graph_nodes');
206 var t = document.getElementById('graph_content');
214 var t = document.getElementById('graph_content');
207 canvas = document.getElementById('graph_canvas');
215 canvas = document.getElementById('graph_canvas');
208 var div_h = t.clientHeight;
216 var div_h = t.clientHeight;
209 c.style.height=div_h+'px';
217 c.style.height=div_h+'px';
210 canvas.setAttribute('height',div_h);
218 canvas.setAttribute('height',div_h);
211 c.style.height=max_w+'px';
219 c.style.height=max_w+'px';
212 canvas.setAttribute('width',max_w);
220 canvas.setAttribute('width',max_w);
213 };
221 };
214 var heads = 1;
222 var heads = 1;
215 var max_heads = 0;
223 var max_heads = 0;
216 var jsdata = ${c.jsdata|n};
224 var jsdata = ${c.jsdata|n};
217
225
218 for( var i=0;i<jsdata.length;i++){
226 for( var i=0;i<jsdata.length;i++){
219 var m = Math.max.apply(Math, jsdata[i][1]);
227 var m = Math.max.apply(Math, jsdata[i][1]);
220 if (m>max_heads){
228 if (m>max_heads){
221 max_heads = m;
229 max_heads = m;
222 }
230 }
223 }
231 }
224 var max_w = Math.max(100,max_heads*25);
232 var max_w = Math.max(100,max_heads*25);
225 set_canvas(max_w);
233 set_canvas(max_w);
226
234
227 var r = new BranchRenderer();
235 var r = new BranchRenderer();
228 r.render(jsdata,max_w);
236 r.render(jsdata,max_w);
229
237
230 });
238 });
231 </script>
239 </script>
232 %else:
240 %else:
233 ${_('There are no changes yet')}
241 ${_('There are no changes yet')}
234 %endif
242 %endif
235 </div>
243 </div>
236 </div>
244 </div>
237 </%def>
245 </%def>
@@ -1,109 +1,112 b''
1 """Pylons application test package
1 """Pylons application test package
2
2
3 This package assumes the Pylons environment is already loaded, such as
3 This package assumes the Pylons environment is already loaded, such as
4 when this script is imported from the `nosetests --with-pylons=test.ini`
4 when this script is imported from the `nosetests --with-pylons=test.ini`
5 command.
5 command.
6
6
7 This module initializes the application via ``websetup`` (`paster
7 This module initializes the application via ``websetup`` (`paster
8 setup-app`) and provides the base testing objects.
8 setup-app`) and provides the base testing objects.
9 """
9 """
10 import os
10 import os
11 import time
11 import time
12 import logging
12 import logging
13 from os.path import join as jn
13 from os.path import join as jn
14
14
15 from unittest import TestCase
15 from unittest import TestCase
16 from tempfile import _RandomNameSequence
16 from tempfile import _RandomNameSequence
17
17
18 from paste.deploy import loadapp
18 from paste.deploy import loadapp
19 from paste.script.appinstall import SetupCommand
19 from paste.script.appinstall import SetupCommand
20 from pylons import config, url
20 from pylons import config, url
21 from routes.util import URLGenerator
21 from routes.util import URLGenerator
22 from webtest import TestApp
22 from webtest import TestApp
23
23
24 from rhodecode import is_windows
24 from rhodecode.model.meta import Session
25 from rhodecode.model.meta import Session
25 from rhodecode.model.db import User
26 from rhodecode.model.db import User
26
27
27 import pylons.test
28 import pylons.test
28
29
29 os.environ['TZ'] = 'UTC'
30 os.environ['TZ'] = 'UTC'
30 time.tzset()
31 if not is_windows:
32 time.tzset()
31
33
32 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
33
35
34 __all__ = [
36 __all__ = [
35 'environ', 'url', 'TestController', 'TESTS_TMP_PATH', 'HG_REPO',
37 'environ', 'url', 'TestController', 'TESTS_TMP_PATH', 'HG_REPO',
36 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO', 'HG_FORK', 'GIT_FORK',
38 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO', 'HG_FORK', 'GIT_FORK',
37 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
39 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
38 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
40 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
39 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL'
41 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL'
40 ]
42 ]
41
43
42 # Invoke websetup with the current config file
44 # Invoke websetup with the current config file
43 # SetupCommand('setup-app').run([config_file])
45 # SetupCommand('setup-app').run([config_file])
44
46
45 ##RUNNING DESIRED TESTS
47 ##RUNNING DESIRED TESTS
46 # nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account
48 # nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account
47 # nosetests --pdb --pdb-failures
49 # nosetests --pdb --pdb-failures
48 environ = {}
50 environ = {}
49
51
50 #SOME GLOBALS FOR TESTS
52 #SOME GLOBALS FOR TESTS
51
53
52 TESTS_TMP_PATH = jn('/', 'tmp', 'rc_test_%s' % _RandomNameSequence().next())
54 TESTS_TMP_PATH = jn('/', 'tmp', 'rc_test_%s' % _RandomNameSequence().next())
53 TEST_USER_ADMIN_LOGIN = 'test_admin'
55 TEST_USER_ADMIN_LOGIN = 'test_admin'
54 TEST_USER_ADMIN_PASS = 'test12'
56 TEST_USER_ADMIN_PASS = 'test12'
55 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
57 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
56
58
57 TEST_USER_REGULAR_LOGIN = 'test_regular'
59 TEST_USER_REGULAR_LOGIN = 'test_regular'
58 TEST_USER_REGULAR_PASS = 'test12'
60 TEST_USER_REGULAR_PASS = 'test12'
59 TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com'
61 TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com'
60
62
61 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
63 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
62 TEST_USER_REGULAR2_PASS = 'test12'
64 TEST_USER_REGULAR2_PASS = 'test12'
63 TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com'
65 TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com'
64
66
65 HG_REPO = 'vcs_test_hg'
67 HG_REPO = 'vcs_test_hg'
66 GIT_REPO = 'vcs_test_git'
68 GIT_REPO = 'vcs_test_git'
67
69
68 NEW_HG_REPO = 'vcs_test_hg_new'
70 NEW_HG_REPO = 'vcs_test_hg_new'
69 NEW_GIT_REPO = 'vcs_test_git_new'
71 NEW_GIT_REPO = 'vcs_test_git_new'
70
72
71 HG_FORK = 'vcs_test_hg_fork'
73 HG_FORK = 'vcs_test_hg_fork'
72 GIT_FORK = 'vcs_test_git_fork'
74 GIT_FORK = 'vcs_test_git_fork'
73
75
76
74 class TestController(TestCase):
77 class TestController(TestCase):
75
78
76 def __init__(self, *args, **kwargs):
79 def __init__(self, *args, **kwargs):
77 wsgiapp = pylons.test.pylonsapp
80 wsgiapp = pylons.test.pylonsapp
78 config = wsgiapp.config
81 config = wsgiapp.config
79
82
80 self.app = TestApp(wsgiapp)
83 self.app = TestApp(wsgiapp)
81 url._push_object(URLGenerator(config['routes.map'], environ))
84 url._push_object(URLGenerator(config['routes.map'], environ))
82 self.Session = Session
85 self.Session = Session
83 self.index_location = config['app_conf']['index_dir']
86 self.index_location = config['app_conf']['index_dir']
84 TestCase.__init__(self, *args, **kwargs)
87 TestCase.__init__(self, *args, **kwargs)
85
88
86 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
89 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
87 password=TEST_USER_ADMIN_PASS):
90 password=TEST_USER_ADMIN_PASS):
88 self._logged_username = username
91 self._logged_username = username
89 response = self.app.post(url(controller='login', action='index'),
92 response = self.app.post(url(controller='login', action='index'),
90 {'username':username,
93 {'username':username,
91 'password':password})
94 'password':password})
92
95
93 if 'invalid user name' in response.body:
96 if 'invalid user name' in response.body:
94 self.fail('could not login using %s %s' % (username, password))
97 self.fail('could not login using %s %s' % (username, password))
95
98
96 self.assertEqual(response.status, '302 Found')
99 self.assertEqual(response.status, '302 Found')
97 ses = response.session['rhodecode_user']
100 ses = response.session['rhodecode_user']
98 self.assertEqual(ses.get('username'), username)
101 self.assertEqual(ses.get('username'), username)
99 response = response.follow()
102 response = response.follow()
100 self.assertEqual(ses.get('is_authenticated'), True)
103 self.assertEqual(ses.get('is_authenticated'), True)
101
104
102 return response.session['rhodecode_user']
105 return response.session['rhodecode_user']
103
106
104 def _get_logged_user(self):
107 def _get_logged_user(self):
105 return User.get_by_username(self._logged_username)
108 return User.get_by_username(self._logged_username)
106
109
107 def checkSessionFlash(self, response, msg):
110 def checkSessionFlash(self, response, msg):
108 self.assertTrue('flash' in response.session)
111 self.assertTrue('flash' in response.session)
109 self.assertTrue(msg in response.session['flash'][0][1])
112 self.assertTrue(msg in response.session['flash'][0][1])
@@ -1,315 +1,318 b''
1 from rhodecode.tests import *
1 from rhodecode.tests import *
2
2
3 ARCHIVE_SPECS = {
3 ARCHIVE_SPECS = {
4 '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
4 '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
5 '.tar.gz': ('application/x-gzip', 'tgz', ''),
5 '.tar.gz': ('application/x-gzip', 'tgz', ''),
6 '.zip': ('application/zip', 'zip', ''),
6 '.zip': ('application/zip', 'zip', ''),
7 }
7 }
8
8
9
9
10 class TestFilesController(TestController):
10 class TestFilesController(TestController):
11
11
12 def test_index(self):
12 def test_index(self):
13 self.log_user()
13 self.log_user()
14 response = self.app.get(url(controller='files', action='index',
14 response = self.app.get(url(controller='files', action='index',
15 repo_name=HG_REPO,
15 repo_name=HG_REPO,
16 revision='tip',
16 revision='tip',
17 f_path='/'))
17 f_path='/'))
18 # Test response...
18 # Test response...
19 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/docs">docs</a>')
19 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/docs">docs</a>')
20 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/tests">tests</a>')
20 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/tests">tests</a>')
21 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/vcs">vcs</a>')
21 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/vcs">vcs</a>')
22 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/.hgignore">.hgignore</a>')
22 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/.hgignore">.hgignore</a>')
23 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/MANIFEST.in">MANIFEST.in</a>')
23 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/MANIFEST.in">MANIFEST.in</a>')
24
24
25 def test_index_revision(self):
25 def test_index_revision(self):
26 self.log_user()
26 self.log_user()
27
27
28 response = self.app.get(
28 response = self.app.get(
29 url(controller='files', action='index',
29 url(controller='files', action='index',
30 repo_name=HG_REPO,
30 repo_name=HG_REPO,
31 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
31 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
32 f_path='/')
32 f_path='/')
33 )
33 )
34
34
35 #Test response...
35 #Test response...
36
36
37 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs">docs</a>')
37 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs">docs</a>')
38 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests">tests</a>')
38 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests">tests</a>')
39 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst">README.rst</a>')
39 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst">README.rst</a>')
40 response.mustcontain('1.1 KiB')
40 response.mustcontain('1.1 KiB')
41 response.mustcontain('text/x-python')
41 response.mustcontain('text/x-python')
42
42
43 def test_index_different_branch(self):
43 def test_index_different_branch(self):
44 self.log_user()
44 self.log_user()
45
45
46 response = self.app.get(url(controller='files', action='index',
46 response = self.app.get(url(controller='files', action='index',
47 repo_name=HG_REPO,
47 repo_name=HG_REPO,
48 revision='97e8b885c04894463c51898e14387d80c30ed1ee',
48 revision='97e8b885c04894463c51898e14387d80c30ed1ee',
49 f_path='/'))
49 f_path='/'))
50
50
51 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: git</a></span>""")
51 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: git</a></span>""")
52
52
53 def test_index_paging(self):
53 def test_index_paging(self):
54 self.log_user()
54 self.log_user()
55
55
56 for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
56 for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
57 (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
57 (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
58 (109, '75feb4c33e81186c87eac740cee2447330288412'),
58 (109, '75feb4c33e81186c87eac740cee2447330288412'),
59 (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
59 (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
60 (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
60 (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
61
61
62 response = self.app.get(url(controller='files', action='index',
62 response = self.app.get(url(controller='files', action='index',
63 repo_name=HG_REPO,
63 repo_name=HG_REPO,
64 revision=r[1],
64 revision=r[1],
65 f_path='/'))
65 f_path='/'))
66
66
67 response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
67 response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
68
68
69 def test_file_source(self):
69 def test_file_source(self):
70 self.log_user()
70 self.log_user()
71 response = self.app.get(url(controller='files', action='index',
71 response = self.app.get(url(controller='files', action='index',
72 repo_name=HG_REPO,
72 repo_name=HG_REPO,
73 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
73 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
74 f_path='vcs/nodes.py'))
74 f_path='vcs/nodes.py'))
75
75
76 #test or history
76 #test or history
77 response.mustcontain("""<optgroup label="Changesets">
77 response.mustcontain("""<optgroup label="Changesets">
78 <option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
78 <option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
79 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
79 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
80 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
80 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
81 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
81 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
82 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
82 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
83 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
83 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
84 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
84 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
85 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
85 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
86 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
86 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
87 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
87 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
88 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
88 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
89 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
89 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
90 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
90 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
91 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
91 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
92 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
92 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
93 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
93 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
94 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
94 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
95 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
95 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
96 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
96 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
97 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
97 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
98 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
98 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
99 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
99 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
100 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
100 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
101 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
101 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
102 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
102 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
103 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
103 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
104 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
104 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
105 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
105 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
106 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
106 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
107 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
107 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
108 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
108 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
109 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
109 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
110 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
110 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
111 </optgroup>
111 </optgroup>
112 <optgroup label="Branches">
112 <optgroup label="Branches">
113 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
113 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
114 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
114 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
115 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
115 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
116 </optgroup>
116 </optgroup>
117 <optgroup label="Tags">
117 <optgroup label="Tags">
118 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
118 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
119 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
119 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
120 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
120 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
121 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
121 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
122 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
122 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
123 </optgroup>
123 </optgroup>
124 """)
124 """)
125
125
126 response.mustcontain("""<div class="commit">merge</div>""")
126 response.mustcontain("""<div class="commit">merge</div>""")
127
127
128 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
128 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
129
129
130 def test_file_annotation(self):
130 def test_file_annotation(self):
131 self.log_user()
131 self.log_user()
132 response = self.app.get(url(controller='files', action='index',
132 response = self.app.get(url(controller='files', action='index',
133 repo_name=HG_REPO,
133 repo_name=HG_REPO,
134 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
134 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
135 f_path='vcs/nodes.py',
135 f_path='vcs/nodes.py',
136 annotate=True))
136 annotate=True))
137
137
138
138
139 response.mustcontain("""<optgroup label="Changesets">
139 response.mustcontain("""<optgroup label="Changesets">
140 <option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
140 <option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
141 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
141 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
142 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
142 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
143 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
143 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
144 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
144 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
145 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
145 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
146 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
146 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
147 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
147 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
148 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
148 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
149 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
149 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
150 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
150 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
151 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
151 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
152 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
152 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
153 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
153 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
154 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
154 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
155 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
155 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
156 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
156 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
157 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
157 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
158 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
158 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
159 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
159 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
160 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
160 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
161 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
161 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
162 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
162 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
163 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
163 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
164 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
164 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
165 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
165 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
166 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
166 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
167 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
167 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
168 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
168 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
169 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
169 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
170 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
170 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
171 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
171 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
172 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
172 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
173 </optgroup>
173 </optgroup>
174 <optgroup label="Branches">
174 <optgroup label="Branches">
175 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
175 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
176 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
176 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
177 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
177 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
178 </optgroup>
178 </optgroup>
179 <optgroup label="Tags">
179 <optgroup label="Tags">
180 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
180 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
181 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
181 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
182 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
182 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
183 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
183 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
184 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
184 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
185 </optgroup>""")
185 </optgroup>""")
186
186
187 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
187 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
188
188
189 def test_archival(self):
189 def test_archival(self):
190 self.log_user()
190 self.log_user()
191
191
192 for arch_ext, info in ARCHIVE_SPECS.items():
192 for arch_ext, info in ARCHIVE_SPECS.items():
193 short = '27cd5cce30c9%s' % arch_ext
193 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
194 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
194 filename = '%s-%s' % (HG_REPO, fname)
195 filename = '%s-%s' % (HG_REPO, short)
195
196 response = self.app.get(url(controller='files',
196 response = self.app.get(url(controller='files', action='archivefile',
197 action='archivefile',
197 repo_name=HG_REPO,
198 repo_name=HG_REPO,
198 fname=fname))
199 fname=fname))
199
200
200 assert response.status == '200 OK', 'wrong response code'
201 self.assertEqual(response.status, '200 OK')
201 assert response.response._headers.items() == [('Pragma', 'no-cache'),
202 self.assertEqual(response.response._headers.items(),
202 ('Cache-Control', 'no-cache'),
203 [('Pragma', 'no-cache'),
203 ('Content-Type', '%s; charset=utf-8' % info[0]),
204 ('Cache-Control', 'no-cache'),
204 ('Content-Disposition', 'attachment; filename=%s' % filename), ], 'wrong headers'
205 ('Content-Type', '%s; charset=utf-8' % info[0]),
206 ('Content-Disposition', 'attachment; filename=%s' % filename),
207 ]
208 )
205
209
206 def test_archival_wrong_ext(self):
210 def test_archival_wrong_ext(self):
207 self.log_user()
211 self.log_user()
208
212
209 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
213 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
210 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
214 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
211
215
212 response = self.app.get(url(controller='files', action='archivefile',
216 response = self.app.get(url(controller='files', action='archivefile',
213 repo_name=HG_REPO,
217 repo_name=HG_REPO,
214 fname=fname))
218 fname=fname))
215 assert 'Unknown archive type' in response.body
219 response.mustcontain('Unknown archive type')
216
217
220
218 def test_archival_wrong_revision(self):
221 def test_archival_wrong_revision(self):
219 self.log_user()
222 self.log_user()
220
223
221 for rev in ['00x000000', 'tar', 'wrong', '@##$@$424213232', '232dffcd']:
224 for rev in ['00x000000', 'tar', 'wrong', '@##$@$424213232', '232dffcd']:
222 fname = '%s.zip' % rev
225 fname = '%s.zip' % rev
223
226
224 response = self.app.get(url(controller='files', action='archivefile',
227 response = self.app.get(url(controller='files', action='archivefile',
225 repo_name=HG_REPO,
228 repo_name=HG_REPO,
226 fname=fname))
229 fname=fname))
227 assert 'Unknown revision' in response.body
230 response.mustcontain('Unknown revision')
228
231
229 #==========================================================================
232 #==========================================================================
230 # RAW FILE
233 # RAW FILE
231 #==========================================================================
234 #==========================================================================
232 def test_raw_file_ok(self):
235 def test_raw_file_ok(self):
233 self.log_user()
236 self.log_user()
234 response = self.app.get(url(controller='files', action='rawfile',
237 response = self.app.get(url(controller='files', action='rawfile',
235 repo_name=HG_REPO,
238 repo_name=HG_REPO,
236 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
239 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
237 f_path='vcs/nodes.py'))
240 f_path='vcs/nodes.py'))
238
241
239 assert response.content_disposition == "attachment; filename=nodes.py"
242 self.assertEqual(response.content_disposition, "attachment; filename=nodes.py")
240 assert response.content_type == "text/x-python"
243 self.assertEqual(response.content_type, "text/x-python")
241
244
242 def test_raw_file_wrong_cs(self):
245 def test_raw_file_wrong_cs(self):
243 self.log_user()
246 self.log_user()
244 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
247 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
245 f_path = 'vcs/nodes.py'
248 f_path = 'vcs/nodes.py'
246
249
247 response = self.app.get(url(controller='files', action='rawfile',
250 response = self.app.get(url(controller='files', action='rawfile',
248 repo_name=HG_REPO,
251 repo_name=HG_REPO,
249 revision=rev,
252 revision=rev,
250 f_path=f_path))
253 f_path=f_path))
251
254
252 msg = """Revision %r does not exist for this repository""" % (rev)
255 msg = """Revision %r does not exist for this repository""" % (rev)
253 self.checkSessionFlash(response, msg)
256 self.checkSessionFlash(response, msg)
254
257
255 msg = """%s""" % (HG_REPO)
258 msg = """%s""" % (HG_REPO)
256 self.checkSessionFlash(response, msg)
259 self.checkSessionFlash(response, msg)
257
260
258 def test_raw_file_wrong_f_path(self):
261 def test_raw_file_wrong_f_path(self):
259 self.log_user()
262 self.log_user()
260 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
263 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
261 f_path = 'vcs/ERRORnodes.py'
264 f_path = 'vcs/ERRORnodes.py'
262 response = self.app.get(url(controller='files', action='rawfile',
265 response = self.app.get(url(controller='files', action='rawfile',
263 repo_name=HG_REPO,
266 repo_name=HG_REPO,
264 revision=rev,
267 revision=rev,
265 f_path=f_path))
268 f_path=f_path))
266
269
267 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
270 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
268 self.checkSessionFlash(response, msg)
271 self.checkSessionFlash(response, msg)
269
272
270 #==========================================================================
273 #==========================================================================
271 # RAW RESPONSE - PLAIN
274 # RAW RESPONSE - PLAIN
272 #==========================================================================
275 #==========================================================================
273 def test_raw_ok(self):
276 def test_raw_ok(self):
274 self.log_user()
277 self.log_user()
275 response = self.app.get(url(controller='files', action='raw',
278 response = self.app.get(url(controller='files', action='raw',
276 repo_name=HG_REPO,
279 repo_name=HG_REPO,
277 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
280 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
278 f_path='vcs/nodes.py'))
281 f_path='vcs/nodes.py'))
279
282
280 assert response.content_type == "text/plain"
283 self.assertEqual(response.content_type, "text/plain")
281
284
282 def test_raw_wrong_cs(self):
285 def test_raw_wrong_cs(self):
283 self.log_user()
286 self.log_user()
284 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
287 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
285 f_path = 'vcs/nodes.py'
288 f_path = 'vcs/nodes.py'
286
289
287 response = self.app.get(url(controller='files', action='raw',
290 response = self.app.get(url(controller='files', action='raw',
288 repo_name=HG_REPO,
291 repo_name=HG_REPO,
289 revision=rev,
292 revision=rev,
290 f_path=f_path))
293 f_path=f_path))
291 msg = """Revision %r does not exist for this repository""" % (rev)
294 msg = """Revision %r does not exist for this repository""" % (rev)
292 self.checkSessionFlash(response, msg)
295 self.checkSessionFlash(response, msg)
293
296
294 msg = """%s""" % (HG_REPO)
297 msg = """%s""" % (HG_REPO)
295 self.checkSessionFlash(response, msg)
298 self.checkSessionFlash(response, msg)
296
299
297 def test_raw_wrong_f_path(self):
300 def test_raw_wrong_f_path(self):
298 self.log_user()
301 self.log_user()
299 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
302 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
300 f_path = 'vcs/ERRORnodes.py'
303 f_path = 'vcs/ERRORnodes.py'
301 response = self.app.get(url(controller='files', action='raw',
304 response = self.app.get(url(controller='files', action='raw',
302 repo_name=HG_REPO,
305 repo_name=HG_REPO,
303 revision=rev,
306 revision=rev,
304 f_path=f_path))
307 f_path=f_path))
305 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
308 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
306 self.checkSessionFlash(response, msg)
309 self.checkSessionFlash(response, msg)
307
310
308 def test_ajaxed_files_list(self):
311 def test_ajaxed_files_list(self):
309 self.log_user()
312 self.log_user()
310 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
313 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
311 response = self.app.get(
314 response = self.app.get(
312 url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev),
315 url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev),
313 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
316 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
314 )
317 )
315 response.mustcontain("vcs/web/simplevcs/views/repository.py")
318 response.mustcontain("vcs/web/simplevcs/views/repository.py")
@@ -1,268 +1,267 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from rhodecode.tests import *
2 from rhodecode.tests import *
3 from rhodecode.model.db import User, Notification
3 from rhodecode.model.db import User, Notification
4 from rhodecode.lib.utils2 import generate_api_key
4 from rhodecode.lib.utils2 import generate_api_key
5 from rhodecode.lib.auth import check_password
5 from rhodecode.lib.auth import check_password
6 from rhodecode.model.meta import Session
6 from rhodecode.model.meta import Session
7
7
8
8
9 class TestLoginController(TestController):
9 class TestLoginController(TestController):
10
10
11 def tearDown(self):
11 def tearDown(self):
12 for n in Notification.query().all():
12 for n in Notification.query().all():
13 Session.delete(n)
13 Session.delete(n)
14
14
15 Session.commit()
15 Session.commit()
16 self.assertEqual(Notification.query().all(), [])
16 self.assertEqual(Notification.query().all(), [])
17
17
18 def test_index(self):
18 def test_index(self):
19 response = self.app.get(url(controller='login', action='index'))
19 response = self.app.get(url(controller='login', action='index'))
20 self.assertEqual(response.status, '200 OK')
20 self.assertEqual(response.status, '200 OK')
21 # Test response...
21 # Test response...
22
22
23 def test_login_admin_ok(self):
23 def test_login_admin_ok(self):
24 response = self.app.post(url(controller='login', action='index'),
24 response = self.app.post(url(controller='login', action='index'),
25 {'username':'test_admin',
25 {'username':'test_admin',
26 'password':'test12'})
26 'password':'test12'})
27 self.assertEqual(response.status, '302 Found')
27 self.assertEqual(response.status, '302 Found')
28 self.assertEqual(response.session['rhodecode_user'].get('username') ,
28 self.assertEqual(response.session['rhodecode_user'].get('username') ,
29 'test_admin')
29 'test_admin')
30 response = response.follow()
30 response = response.follow()
31 self.assertTrue('%s repository' % HG_REPO in response.body)
31 self.assertTrue('%s repository' % HG_REPO in response.body)
32
32
33 def test_login_regular_ok(self):
33 def test_login_regular_ok(self):
34 response = self.app.post(url(controller='login', action='index'),
34 response = self.app.post(url(controller='login', action='index'),
35 {'username':'test_regular',
35 {'username':'test_regular',
36 'password':'test12'})
36 'password':'test12'})
37
37
38 self.assertEqual(response.status, '302 Found')
38 self.assertEqual(response.status, '302 Found')
39 self.assertEqual(response.session['rhodecode_user'].get('username') ,
39 self.assertEqual(response.session['rhodecode_user'].get('username') ,
40 'test_regular')
40 'test_regular')
41 response = response.follow()
41 response = response.follow()
42 self.assertTrue('%s repository' % HG_REPO in response.body)
42 self.assertTrue('%s repository' % HG_REPO in response.body)
43 self.assertTrue('<a title="Admin" href="/_admin">' not in response.body)
43 self.assertTrue('<a title="Admin" href="/_admin">' not in response.body)
44
44
45 def test_login_ok_came_from(self):
45 def test_login_ok_came_from(self):
46 test_came_from = '/_admin/users'
46 test_came_from = '/_admin/users'
47 response = self.app.post(url(controller='login', action='index',
47 response = self.app.post(url(controller='login', action='index',
48 came_from=test_came_from),
48 came_from=test_came_from),
49 {'username':'test_admin',
49 {'username':'test_admin',
50 'password':'test12'})
50 'password':'test12'})
51 self.assertEqual(response.status, '302 Found')
51 self.assertEqual(response.status, '302 Found')
52 response = response.follow()
52 response = response.follow()
53
53
54 self.assertEqual(response.status, '200 OK')
54 self.assertEqual(response.status, '200 OK')
55 self.assertTrue('Users administration' in response.body)
55 self.assertTrue('Users administration' in response.body)
56
56
57
58 def test_login_short_password(self):
57 def test_login_short_password(self):
59 response = self.app.post(url(controller='login', action='index'),
58 response = self.app.post(url(controller='login', action='index'),
60 {'username':'test_admin',
59 {'username':'test_admin',
61 'password':'as'})
60 'password':'as'})
62 self.assertEqual(response.status, '200 OK')
61 self.assertEqual(response.status, '200 OK')
63
62
64 self.assertTrue('Enter 3 characters or more' in response.body)
63 self.assertTrue('Enter 3 characters or more' in response.body)
65
64
66 def test_login_wrong_username_password(self):
65 def test_login_wrong_username_password(self):
67 response = self.app.post(url(controller='login', action='index'),
66 response = self.app.post(url(controller='login', action='index'),
68 {'username':'error',
67 {'username':'error',
69 'password':'test12'})
68 'password':'test12'})
70 self.assertEqual(response.status , '200 OK')
69 self.assertEqual(response.status , '200 OK')
71
70
72 self.assertTrue('invalid user name' in response.body)
71 self.assertTrue('invalid user name' in response.body)
73 self.assertTrue('invalid password' in response.body)
72 self.assertTrue('invalid password' in response.body)
74
73
75 #==========================================================================
74 #==========================================================================
76 # REGISTRATIONS
75 # REGISTRATIONS
77 #==========================================================================
76 #==========================================================================
78 def test_register(self):
77 def test_register(self):
79 response = self.app.get(url(controller='login', action='register'))
78 response = self.app.get(url(controller='login', action='register'))
80 self.assertTrue('Sign Up to RhodeCode' in response.body)
79 self.assertTrue('Sign Up to RhodeCode' in response.body)
81
80
82 def test_register_err_same_username(self):
81 def test_register_err_same_username(self):
83 response = self.app.post(url(controller='login', action='register'),
82 response = self.app.post(url(controller='login', action='register'),
84 {'username':'test_admin',
83 {'username':'test_admin',
85 'password':'test12',
84 'password':'test12',
86 'password_confirmation':'test12',
85 'password_confirmation':'test12',
87 'email':'goodmail@domain.com',
86 'email':'goodmail@domain.com',
88 'name':'test',
87 'name':'test',
89 'lastname':'test'})
88 'lastname':'test'})
90
89
91 self.assertEqual(response.status , '200 OK')
90 self.assertEqual(response.status , '200 OK')
92 self.assertTrue('This username already exists' in response.body)
91 self.assertTrue('This username already exists' in response.body)
93
92
94 def test_register_err_same_email(self):
93 def test_register_err_same_email(self):
95 response = self.app.post(url(controller='login', action='register'),
94 response = self.app.post(url(controller='login', action='register'),
96 {'username':'test_admin_0',
95 {'username':'test_admin_0',
97 'password':'test12',
96 'password':'test12',
98 'password_confirmation':'test12',
97 'password_confirmation':'test12',
99 'email':'test_admin@mail.com',
98 'email':'test_admin@mail.com',
100 'name':'test',
99 'name':'test',
101 'lastname':'test'})
100 'lastname':'test'})
102
101
103 self.assertEqual(response.status , '200 OK')
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 def test_register_err_same_email_case_sensitive(self):
105 def test_register_err_same_email_case_sensitive(self):
107 response = self.app.post(url(controller='login', action='register'),
106 response = self.app.post(url(controller='login', action='register'),
108 {'username':'test_admin_1',
107 {'username':'test_admin_1',
109 'password':'test12',
108 'password':'test12',
110 'password_confirmation':'test12',
109 'password_confirmation':'test12',
111 'email':'TesT_Admin@mail.COM',
110 'email':'TesT_Admin@mail.COM',
112 'name':'test',
111 'name':'test',
113 'lastname':'test'})
112 'lastname':'test'})
114 self.assertEqual(response.status , '200 OK')
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 def test_register_err_wrong_data(self):
116 def test_register_err_wrong_data(self):
118 response = self.app.post(url(controller='login', action='register'),
117 response = self.app.post(url(controller='login', action='register'),
119 {'username':'xs',
118 {'username':'xs',
120 'password':'test',
119 'password':'test',
121 'password_confirmation':'test',
120 'password_confirmation':'test',
122 'email':'goodmailm',
121 'email':'goodmailm',
123 'name':'test',
122 'name':'test',
124 'lastname':'test'})
123 'lastname':'test'})
125 self.assertEqual(response.status , '200 OK')
124 self.assertEqual(response.status , '200 OK')
126 assert 'An email address must contain a single @' in response.body
125 response.mustcontain('An email address must contain a single @')
127 assert 'Enter a value 6 characters long or more' in response.body
126 response.mustcontain('Enter a value 6 characters long or more')
128
129
127
130 def test_register_err_username(self):
128 def test_register_err_username(self):
131 response = self.app.post(url(controller='login', action='register'),
129 response = self.app.post(url(controller='login', action='register'),
132 {'username':'error user',
130 {'username':'error user',
133 'password':'test12',
131 'password':'test12',
134 'password_confirmation':'test12',
132 'password_confirmation':'test12',
135 'email':'goodmailm',
133 'email':'goodmailm',
136 'name':'test',
134 'name':'test',
137 'lastname':'test'})
135 'lastname':'test'})
138
136
139 self.assertEqual(response.status , '200 OK')
137 self.assertEqual(response.status , '200 OK')
140 assert 'An email address must contain a single @' in response.body
138 response.mustcontain('An email address must contain a single @')
141 assert ('Username may only contain '
139 response.mustcontain('Username may only contain '
142 'alphanumeric characters underscores, '
140 'alphanumeric characters underscores, '
143 'periods or dashes and must begin with '
141 'periods or dashes and must begin with '
144 'alphanumeric character') in response.body
142 'alphanumeric character')
145
143
146 def test_register_err_case_sensitive(self):
144 def test_register_err_case_sensitive(self):
147 response = self.app.post(url(controller='login', action='register'),
145 response = self.app.post(url(controller='login', action='register'),
148 {'username':'Test_Admin',
146 {'username':'Test_Admin',
149 'password':'test12',
147 'password':'test12',
150 'password_confirmation':'test12',
148 'password_confirmation':'test12',
151 'email':'goodmailm',
149 'email':'goodmailm',
152 'name':'test',
150 'name':'test',
153 'lastname':'test'})
151 'lastname':'test'})
154
152
155 self.assertEqual(response.status , '200 OK')
153 self.assertEqual(response.status , '200 OK')
156 self.assertTrue('An email address must contain a single @' in response.body)
154 self.assertTrue('An email address must contain a single @' in response.body)
157 self.assertTrue('This username already exists' in response.body)
155 self.assertTrue('This username already exists' in response.body)
158
156
159
160
161 def test_register_special_chars(self):
157 def test_register_special_chars(self):
162 response = self.app.post(url(controller='login', action='register'),
158 response = self.app.post(url(controller='login', action='register'),
163 {'username':'xxxaxn',
159 {'username':'xxxaxn',
164 'password':'ąćźżąśśśś',
160 'password':'ąćźżąśśśś',
165 'password_confirmation':'ąćźżąśśśś',
161 'password_confirmation':'ąćźżąśśśś',
166 'email':'goodmailm@test.plx',
162 'email':'goodmailm@test.plx',
167 'name':'test',
163 'name':'test',
168 'lastname':'test'})
164 'lastname':'test'})
169
165
170 self.assertEqual(response.status , '200 OK')
166 self.assertEqual(response.status , '200 OK')
171 self.assertTrue('Invalid characters in password' in response.body)
167 self.assertTrue('Invalid characters in password' in response.body)
172
168
173
174 def test_register_password_mismatch(self):
169 def test_register_password_mismatch(self):
175 response = self.app.post(url(controller='login', action='register'),
170 response = self.app.post(url(controller='login', action='register'),
176 {'username':'xs',
171 {'username':'xs',
177 'password':'123qwe',
172 'password':'123qwe',
178 'password_confirmation':'qwe123',
173 'password_confirmation':'qwe123',
179 'email':'goodmailm@test.plxa',
174 'email':'goodmailm@test.plxa',
180 'name':'test',
175 'name':'test',
181 'lastname':'test'})
176 'lastname':'test'})
182
177
183 self.assertEqual(response.status , '200 OK')
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 def test_register_ok(self):
181 def test_register_ok(self):
187 username = 'test_regular4'
182 username = 'test_regular4'
188 password = 'qweqwe'
183 password = 'qweqwe'
189 email = 'marcin@test.com'
184 email = 'marcin@test.com'
190 name = 'testname'
185 name = 'testname'
191 lastname = 'testlastname'
186 lastname = 'testlastname'
192
187
193 response = self.app.post(url(controller='login', action='register'),
188 response = self.app.post(url(controller='login', action='register'),
194 {'username':username,
189 {'username':username,
195 'password':password,
190 'password':password,
196 'password_confirmation':password,
191 'password_confirmation':password,
197 'email':email,
192 'email':email,
198 'name':name,
193 'name':name,
199 'lastname':lastname})
194 'lastname':lastname,
200 self.assertEqual(response.status , '302 Found')
195 'admin':True}) # This should be overriden
201 assert 'You have successfully registered into rhodecode' in response.session['flash'][0], 'No flash message about user registration'
196 self.assertEqual(response.status, '302 Found')
197 self.checkSessionFlash(response, 'You have successfully registered into rhodecode')
202
198
203 ret = self.Session.query(User).filter(User.username == 'test_regular4').one()
199 ret = self.Session.query(User).filter(User.username == 'test_regular4').one()
204 assert ret.username == username , 'field mismatch %s %s' % (ret.username, username)
200 self.assertEqual(ret.username, username)
205 assert check_password(password, ret.password) == True , 'password mismatch'
201 self.assertEqual(check_password(password, ret.password), True)
206 assert ret.email == email , 'field mismatch %s %s' % (ret.email, email)
202 self.assertEqual(ret.email, email)
207 assert ret.name == name , 'field mismatch %s %s' % (ret.name, name)
203 self.assertEqual(ret.name, name)
208 assert ret.lastname == lastname , 'field mismatch %s %s' % (ret.lastname, lastname)
204 self.assertEqual(ret.lastname, lastname)
209
205 self.assertNotEqual(ret.api_key, None)
206 self.assertEqual(ret.admin, False)
210
207
211 def test_forgot_password_wrong_mail(self):
208 def test_forgot_password_wrong_mail(self):
212 response = self.app.post(url(controller='login', action='password_reset'),
209 response = self.app.post(
213 {'email':'marcin@wrongmail.org', })
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 def test_forgot_password(self):
216 def test_forgot_password(self):
218 response = self.app.get(url(controller='login',
217 response = self.app.get(url(controller='login',
219 action='password_reset'))
218 action='password_reset'))
220 self.assertEqual(response.status , '200 OK')
219 self.assertEqual(response.status, '200 OK')
221
220
222 username = 'test_password_reset_1'
221 username = 'test_password_reset_1'
223 password = 'qweqwe'
222 password = 'qweqwe'
224 email = 'marcin@python-works.com'
223 email = 'marcin@python-works.com'
225 name = 'passwd'
224 name = 'passwd'
226 lastname = 'reset'
225 lastname = 'reset'
227
226
228 new = User()
227 new = User()
229 new.username = username
228 new.username = username
230 new.password = password
229 new.password = password
231 new.email = email
230 new.email = email
232 new.name = name
231 new.name = name
233 new.lastname = lastname
232 new.lastname = lastname
234 new.api_key = generate_api_key(username)
233 new.api_key = generate_api_key(username)
235 self.Session.add(new)
234 self.Session.add(new)
236 self.Session.commit()
235 self.Session.commit()
237
236
238 response = self.app.post(url(controller='login',
237 response = self.app.post(url(controller='login',
239 action='password_reset'),
238 action='password_reset'),
240 {'email':email, })
239 {'email':email, })
241
240
242 self.checkSessionFlash(response, 'Your password reset link was sent')
241 self.checkSessionFlash(response, 'Your password reset link was sent')
243
242
244 response = response.follow()
243 response = response.follow()
245
244
246 # BAD KEY
245 # BAD KEY
247
246
248 key = "bad"
247 key = "bad"
249 response = self.app.get(url(controller='login',
248 response = self.app.get(url(controller='login',
250 action='password_reset_confirmation',
249 action='password_reset_confirmation',
251 key=key))
250 key=key))
252 self.assertEqual(response.status, '302 Found')
251 self.assertEqual(response.status, '302 Found')
253 self.assertTrue(response.location.endswith(url('reset_password')))
252 self.assertTrue(response.location.endswith(url('reset_password')))
254
253
255 # GOOD KEY
254 # GOOD KEY
256
255
257 key = User.get_by_username(username).api_key
256 key = User.get_by_username(username).api_key
258 response = self.app.get(url(controller='login',
257 response = self.app.get(url(controller='login',
259 action='password_reset_confirmation',
258 action='password_reset_confirmation',
260 key=key))
259 key=key))
261 self.assertEqual(response.status, '302 Found')
260 self.assertEqual(response.status, '302 Found')
262 self.assertTrue(response.location.endswith(url('login_home')))
261 self.assertTrue(response.location.endswith(url('login_home')))
263
262
264 self.checkSessionFlash(response,
263 self.checkSessionFlash(response,
265 ('Your password reset was successful, '
264 ('Your password reset was successful, '
266 'new password has been sent to your email'))
265 'new password has been sent to your email'))
267
266
268 response = response.follow()
267 response = response.follow()
@@ -1,715 +1,715 b''
1 import os
1 import os
2 import unittest
2 import unittest
3 from rhodecode.tests import *
3 from rhodecode.tests import *
4
4
5 from rhodecode.model.repos_group import ReposGroupModel
5 from rhodecode.model.repos_group import ReposGroupModel
6 from rhodecode.model.repo import RepoModel
6 from rhodecode.model.repo import RepoModel
7 from rhodecode.model.db import RepoGroup, User, Notification, UserNotification, \
7 from rhodecode.model.db import RepoGroup, User, Notification, UserNotification, \
8 UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm
8 UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm,\
9 Repository
9 from sqlalchemy.exc import IntegrityError
10 from sqlalchemy.exc import IntegrityError
10 from rhodecode.model.user import UserModel
11 from rhodecode.model.user import UserModel
11
12
12 from rhodecode.model.meta import Session
13 from rhodecode.model.meta import Session
13 from rhodecode.model.notification import NotificationModel
14 from rhodecode.model.notification import NotificationModel
14 from rhodecode.model.users_group import UsersGroupModel
15 from rhodecode.model.users_group import UsersGroupModel
15 from rhodecode.lib.auth import AuthUser
16 from rhodecode.lib.auth import AuthUser
16
17
17
18
18 def _make_group(path, desc='desc', parent_id=None,
19 def _make_group(path, desc='desc', parent_id=None,
19 skip_if_exists=False):
20 skip_if_exists=False):
20
21
21 gr = RepoGroup.get_by_group_name(path)
22 gr = RepoGroup.get_by_group_name(path)
22 if gr and skip_if_exists:
23 if gr and skip_if_exists:
23 return gr
24 return gr
24
25
25 gr = ReposGroupModel().create(path, desc, parent_id)
26 gr = ReposGroupModel().create(path, desc, parent_id)
26 return gr
27 return gr
27
28
28
29
29 class TestReposGroups(unittest.TestCase):
30 class TestReposGroups(unittest.TestCase):
30
31
31 def setUp(self):
32 def setUp(self):
32 self.g1 = _make_group('test1', skip_if_exists=True)
33 self.g1 = _make_group('test1', skip_if_exists=True)
33 Session.commit()
34 Session.commit()
34 self.g2 = _make_group('test2', skip_if_exists=True)
35 self.g2 = _make_group('test2', skip_if_exists=True)
35 Session.commit()
36 Session.commit()
36 self.g3 = _make_group('test3', skip_if_exists=True)
37 self.g3 = _make_group('test3', skip_if_exists=True)
37 Session.commit()
38 Session.commit()
38
39
39 def tearDown(self):
40 def tearDown(self):
40 print 'out'
41 print 'out'
41
42
42 def __check_path(self, *path):
43 def __check_path(self, *path):
43 """
44 """
44 Checks the path for existance !
45 Checks the path for existance !
45 """
46 """
46 path = [TESTS_TMP_PATH] + list(path)
47 path = [TESTS_TMP_PATH] + list(path)
47 path = os.path.join(*path)
48 path = os.path.join(*path)
48 return os.path.isdir(path)
49 return os.path.isdir(path)
49
50
50 def _check_folders(self):
51 def _check_folders(self):
51 print os.listdir(TESTS_TMP_PATH)
52 print os.listdir(TESTS_TMP_PATH)
52
53
53 def __delete_group(self, id_):
54 def __delete_group(self, id_):
54 ReposGroupModel().delete(id_)
55 ReposGroupModel().delete(id_)
55
56
56 def __update_group(self, id_, path, desc='desc', parent_id=None):
57 def __update_group(self, id_, path, desc='desc', parent_id=None):
57 form_data = dict(
58 form_data = dict(
58 group_name=path,
59 group_name=path,
59 group_description=desc,
60 group_description=desc,
60 group_parent_id=parent_id,
61 group_parent_id=parent_id,
61 perms_updates=[],
62 perms_updates=[],
62 perms_new=[]
63 perms_new=[]
63 )
64 )
64 gr = ReposGroupModel().update(id_, form_data)
65 gr = ReposGroupModel().update(id_, form_data)
65 return gr
66 return gr
66
67
67 def test_create_group(self):
68 def test_create_group(self):
68 g = _make_group('newGroup')
69 g = _make_group('newGroup')
69 self.assertEqual(g.full_path, 'newGroup')
70 self.assertEqual(g.full_path, 'newGroup')
70
71
71 self.assertTrue(self.__check_path('newGroup'))
72 self.assertTrue(self.__check_path('newGroup'))
72
73
73 def test_create_same_name_group(self):
74 def test_create_same_name_group(self):
74 self.assertRaises(IntegrityError, lambda:_make_group('newGroup'))
75 self.assertRaises(IntegrityError, lambda:_make_group('newGroup'))
75 Session.rollback()
76 Session.rollback()
76
77
77 def test_same_subgroup(self):
78 def test_same_subgroup(self):
78 sg1 = _make_group('sub1', parent_id=self.g1.group_id)
79 sg1 = _make_group('sub1', parent_id=self.g1.group_id)
79 self.assertEqual(sg1.parent_group, self.g1)
80 self.assertEqual(sg1.parent_group, self.g1)
80 self.assertEqual(sg1.full_path, 'test1/sub1')
81 self.assertEqual(sg1.full_path, 'test1/sub1')
81 self.assertTrue(self.__check_path('test1', 'sub1'))
82 self.assertTrue(self.__check_path('test1', 'sub1'))
82
83
83 ssg1 = _make_group('subsub1', parent_id=sg1.group_id)
84 ssg1 = _make_group('subsub1', parent_id=sg1.group_id)
84 self.assertEqual(ssg1.parent_group, sg1)
85 self.assertEqual(ssg1.parent_group, sg1)
85 self.assertEqual(ssg1.full_path, 'test1/sub1/subsub1')
86 self.assertEqual(ssg1.full_path, 'test1/sub1/subsub1')
86 self.assertTrue(self.__check_path('test1', 'sub1', 'subsub1'))
87 self.assertTrue(self.__check_path('test1', 'sub1', 'subsub1'))
87
88
88 def test_remove_group(self):
89 def test_remove_group(self):
89 sg1 = _make_group('deleteme')
90 sg1 = _make_group('deleteme')
90 self.__delete_group(sg1.group_id)
91 self.__delete_group(sg1.group_id)
91
92
92 self.assertEqual(RepoGroup.get(sg1.group_id), None)
93 self.assertEqual(RepoGroup.get(sg1.group_id), None)
93 self.assertFalse(self.__check_path('deteteme'))
94 self.assertFalse(self.__check_path('deteteme'))
94
95
95 sg1 = _make_group('deleteme', parent_id=self.g1.group_id)
96 sg1 = _make_group('deleteme', parent_id=self.g1.group_id)
96 self.__delete_group(sg1.group_id)
97 self.__delete_group(sg1.group_id)
97
98
98 self.assertEqual(RepoGroup.get(sg1.group_id), None)
99 self.assertEqual(RepoGroup.get(sg1.group_id), None)
99 self.assertFalse(self.__check_path('test1', 'deteteme'))
100 self.assertFalse(self.__check_path('test1', 'deteteme'))
100
101
101 def test_rename_single_group(self):
102 def test_rename_single_group(self):
102 sg1 = _make_group('initial')
103 sg1 = _make_group('initial')
103
104
104 new_sg1 = self.__update_group(sg1.group_id, 'after')
105 new_sg1 = self.__update_group(sg1.group_id, 'after')
105 self.assertTrue(self.__check_path('after'))
106 self.assertTrue(self.__check_path('after'))
106 self.assertEqual(RepoGroup.get_by_group_name('initial'), None)
107 self.assertEqual(RepoGroup.get_by_group_name('initial'), None)
107
108
108 def test_update_group_parent(self):
109 def test_update_group_parent(self):
109
110
110 sg1 = _make_group('initial', parent_id=self.g1.group_id)
111 sg1 = _make_group('initial', parent_id=self.g1.group_id)
111
112
112 new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g1.group_id)
113 new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g1.group_id)
113 self.assertTrue(self.__check_path('test1', 'after'))
114 self.assertTrue(self.__check_path('test1', 'after'))
114 self.assertEqual(RepoGroup.get_by_group_name('test1/initial'), None)
115 self.assertEqual(RepoGroup.get_by_group_name('test1/initial'), None)
115
116
116 new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g3.group_id)
117 new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g3.group_id)
117 self.assertTrue(self.__check_path('test3', 'after'))
118 self.assertTrue(self.__check_path('test3', 'after'))
118 self.assertEqual(RepoGroup.get_by_group_name('test3/initial'), None)
119 self.assertEqual(RepoGroup.get_by_group_name('test3/initial'), None)
119
120
120 new_sg1 = self.__update_group(sg1.group_id, 'hello')
121 new_sg1 = self.__update_group(sg1.group_id, 'hello')
121 self.assertTrue(self.__check_path('hello'))
122 self.assertTrue(self.__check_path('hello'))
122
123
123 self.assertEqual(RepoGroup.get_by_group_name('hello'), new_sg1)
124 self.assertEqual(RepoGroup.get_by_group_name('hello'), new_sg1)
124
125
125 def test_subgrouping_with_repo(self):
126 def test_subgrouping_with_repo(self):
126
127
127 g1 = _make_group('g1')
128 g1 = _make_group('g1')
128 g2 = _make_group('g2')
129 g2 = _make_group('g2')
129
130
130 # create new repo
131 # create new repo
131 form_data = dict(repo_name='john',
132 form_data = dict(repo_name='john',
132 repo_name_full='john',
133 repo_name_full='john',
133 fork_name=None,
134 fork_name=None,
134 description=None,
135 description=None,
135 repo_group=None,
136 repo_group=None,
136 private=False,
137 private=False,
137 repo_type='hg',
138 repo_type='hg',
138 clone_uri=None)
139 clone_uri=None)
139 cur_user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
140 cur_user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
140 r = RepoModel().create(form_data, cur_user)
141 r = RepoModel().create(form_data, cur_user)
141
142
142 self.assertEqual(r.repo_name, 'john')
143 self.assertEqual(r.repo_name, 'john')
143
144
144 # put repo into group
145 # put repo into group
145 form_data = form_data
146 form_data = form_data
146 form_data['repo_group'] = g1.group_id
147 form_data['repo_group'] = g1.group_id
147 form_data['perms_new'] = []
148 form_data['perms_new'] = []
148 form_data['perms_updates'] = []
149 form_data['perms_updates'] = []
149 RepoModel().update(r.repo_name, form_data)
150 RepoModel().update(r.repo_name, form_data)
150 self.assertEqual(r.repo_name, 'g1/john')
151 self.assertEqual(r.repo_name, 'g1/john')
151
152
152 self.__update_group(g1.group_id, 'g1', parent_id=g2.group_id)
153 self.__update_group(g1.group_id, 'g1', parent_id=g2.group_id)
153 self.assertTrue(self.__check_path('g2', 'g1'))
154 self.assertTrue(self.__check_path('g2', 'g1'))
154
155
155 # test repo
156 # test repo
156 self.assertEqual(r.repo_name, os.path.join('g2', 'g1', r.just_name))
157 self.assertEqual(r.repo_name, RepoGroup.url_sep().join(['g2', 'g1', r.just_name]))
157
158
158
159 def test_move_to_root(self):
159 def test_move_to_root(self):
160 g1 = _make_group('t11')
160 g1 = _make_group('t11')
161 Session.commit()
161 Session.commit()
162 g2 = _make_group('t22',parent_id=g1.group_id)
162 g2 = _make_group('t22', parent_id=g1.group_id)
163 Session.commit()
163 Session.commit()
164
164
165 self.assertEqual(g2.full_path,'t11/t22')
165 self.assertEqual(g2.full_path, 't11/t22')
166 self.assertTrue(self.__check_path('t11', 't22'))
166 self.assertTrue(self.__check_path('t11', 't22'))
167
167
168 g2 = self.__update_group(g2.group_id, 'g22', parent_id=None)
168 g2 = self.__update_group(g2.group_id, 'g22', parent_id=None)
169 Session.commit()
169 Session.commit()
170
170
171 self.assertEqual(g2.group_name,'g22')
171 self.assertEqual(g2.group_name, 'g22')
172 # we moved out group from t1 to '' so it's full path should be 'g2'
172 # we moved out group from t1 to '' so it's full path should be 'g2'
173 self.assertEqual(g2.full_path,'g22')
173 self.assertEqual(g2.full_path, 'g22')
174 self.assertFalse(self.__check_path('t11', 't22'))
174 self.assertFalse(self.__check_path('t11', 't22'))
175 self.assertTrue(self.__check_path('g22'))
175 self.assertTrue(self.__check_path('g22'))
176
176
177
177
178 class TestUser(unittest.TestCase):
178 class TestUser(unittest.TestCase):
179 def __init__(self, methodName='runTest'):
179 def __init__(self, methodName='runTest'):
180 Session.remove()
180 Session.remove()
181 super(TestUser, self).__init__(methodName=methodName)
181 super(TestUser, self).__init__(methodName=methodName)
182
182
183 def test_create_and_remove(self):
183 def test_create_and_remove(self):
184 usr = UserModel().create_or_update(username=u'test_user', password=u'qweqwe',
184 usr = UserModel().create_or_update(username=u'test_user', password=u'qweqwe',
185 email=u'u232@rhodecode.org',
185 email=u'u232@rhodecode.org',
186 name=u'u1', lastname=u'u1')
186 name=u'u1', lastname=u'u1')
187 Session.commit()
187 Session.commit()
188 self.assertEqual(User.get_by_username(u'test_user'), usr)
188 self.assertEqual(User.get_by_username(u'test_user'), usr)
189
189
190 # make users group
190 # make users group
191 users_group = UsersGroupModel().create('some_example_group')
191 users_group = UsersGroupModel().create('some_example_group')
192 Session.commit()
192 Session.commit()
193
193
194 UsersGroupModel().add_user_to_group(users_group, usr)
194 UsersGroupModel().add_user_to_group(users_group, usr)
195 Session.commit()
195 Session.commit()
196
196
197 self.assertEqual(UsersGroup.get(users_group.users_group_id), users_group)
197 self.assertEqual(UsersGroup.get(users_group.users_group_id), users_group)
198 self.assertEqual(UsersGroupMember.query().count(), 1)
198 self.assertEqual(UsersGroupMember.query().count(), 1)
199 UserModel().delete(usr.user_id)
199 UserModel().delete(usr.user_id)
200 Session.commit()
200 Session.commit()
201
201
202 self.assertEqual(UsersGroupMember.query().all(), [])
202 self.assertEqual(UsersGroupMember.query().all(), [])
203
203
204
204
205 class TestNotifications(unittest.TestCase):
205 class TestNotifications(unittest.TestCase):
206
206
207 def __init__(self, methodName='runTest'):
207 def __init__(self, methodName='runTest'):
208 Session.remove()
208 Session.remove()
209 self.u1 = UserModel().create_or_update(username=u'u1',
209 self.u1 = UserModel().create_or_update(username=u'u1',
210 password=u'qweqwe',
210 password=u'qweqwe',
211 email=u'u1@rhodecode.org',
211 email=u'u1@rhodecode.org',
212 name=u'u1', lastname=u'u1')
212 name=u'u1', lastname=u'u1')
213 Session.commit()
213 Session.commit()
214 self.u1 = self.u1.user_id
214 self.u1 = self.u1.user_id
215
215
216 self.u2 = UserModel().create_or_update(username=u'u2',
216 self.u2 = UserModel().create_or_update(username=u'u2',
217 password=u'qweqwe',
217 password=u'qweqwe',
218 email=u'u2@rhodecode.org',
218 email=u'u2@rhodecode.org',
219 name=u'u2', lastname=u'u3')
219 name=u'u2', lastname=u'u3')
220 Session.commit()
220 Session.commit()
221 self.u2 = self.u2.user_id
221 self.u2 = self.u2.user_id
222
222
223 self.u3 = UserModel().create_or_update(username=u'u3',
223 self.u3 = UserModel().create_or_update(username=u'u3',
224 password=u'qweqwe',
224 password=u'qweqwe',
225 email=u'u3@rhodecode.org',
225 email=u'u3@rhodecode.org',
226 name=u'u3', lastname=u'u3')
226 name=u'u3', lastname=u'u3')
227 Session.commit()
227 Session.commit()
228 self.u3 = self.u3.user_id
228 self.u3 = self.u3.user_id
229
229
230 super(TestNotifications, self).__init__(methodName=methodName)
230 super(TestNotifications, self).__init__(methodName=methodName)
231
231
232 def _clean_notifications(self):
232 def _clean_notifications(self):
233 for n in Notification.query().all():
233 for n in Notification.query().all():
234 Session.delete(n)
234 Session.delete(n)
235
235
236 Session.commit()
236 Session.commit()
237 self.assertEqual(Notification.query().all(), [])
237 self.assertEqual(Notification.query().all(), [])
238
238
239 def tearDown(self):
239 def tearDown(self):
240 self._clean_notifications()
240 self._clean_notifications()
241
241
242 def test_create_notification(self):
242 def test_create_notification(self):
243 self.assertEqual([], Notification.query().all())
243 self.assertEqual([], Notification.query().all())
244 self.assertEqual([], UserNotification.query().all())
244 self.assertEqual([], UserNotification.query().all())
245
245
246 usrs = [self.u1, self.u2]
246 usrs = [self.u1, self.u2]
247 notification = NotificationModel().create(created_by=self.u1,
247 notification = NotificationModel().create(created_by=self.u1,
248 subject=u'subj', body=u'hi there',
248 subject=u'subj', body=u'hi there',
249 recipients=usrs)
249 recipients=usrs)
250 Session.commit()
250 Session.commit()
251 u1 = User.get(self.u1)
251 u1 = User.get(self.u1)
252 u2 = User.get(self.u2)
252 u2 = User.get(self.u2)
253 u3 = User.get(self.u3)
253 u3 = User.get(self.u3)
254 notifications = Notification.query().all()
254 notifications = Notification.query().all()
255 self.assertEqual(len(notifications), 1)
255 self.assertEqual(len(notifications), 1)
256
256
257 unotification = UserNotification.query()\
257 unotification = UserNotification.query()\
258 .filter(UserNotification.notification == notification).all()
258 .filter(UserNotification.notification == notification).all()
259
259
260 self.assertEqual(notifications[0].recipients, [u1, u2])
260 self.assertEqual(notifications[0].recipients, [u1, u2])
261 self.assertEqual(notification.notification_id,
261 self.assertEqual(notification.notification_id,
262 notifications[0].notification_id)
262 notifications[0].notification_id)
263 self.assertEqual(len(unotification), len(usrs))
263 self.assertEqual(len(unotification), len(usrs))
264 self.assertEqual([x.user.user_id for x in unotification], usrs)
264 self.assertEqual([x.user.user_id for x in unotification], usrs)
265
265
266 def test_user_notifications(self):
266 def test_user_notifications(self):
267 self.assertEqual([], Notification.query().all())
267 self.assertEqual([], Notification.query().all())
268 self.assertEqual([], UserNotification.query().all())
268 self.assertEqual([], UserNotification.query().all())
269
269
270 notification1 = NotificationModel().create(created_by=self.u1,
270 notification1 = NotificationModel().create(created_by=self.u1,
271 subject=u'subj', body=u'hi there1',
271 subject=u'subj', body=u'hi there1',
272 recipients=[self.u3])
272 recipients=[self.u3])
273 Session.commit()
273 Session.commit()
274 notification2 = NotificationModel().create(created_by=self.u1,
274 notification2 = NotificationModel().create(created_by=self.u1,
275 subject=u'subj', body=u'hi there2',
275 subject=u'subj', body=u'hi there2',
276 recipients=[self.u3])
276 recipients=[self.u3])
277 Session.commit()
277 Session.commit()
278 u3 = Session.query(User).get(self.u3)
278 u3 = Session.query(User).get(self.u3)
279
279
280 self.assertEqual(sorted([x.notification for x in u3.notifications]),
280 self.assertEqual(sorted([x.notification for x in u3.notifications]),
281 sorted([notification2, notification1]))
281 sorted([notification2, notification1]))
282
282
283 def test_delete_notifications(self):
283 def test_delete_notifications(self):
284 self.assertEqual([], Notification.query().all())
284 self.assertEqual([], Notification.query().all())
285 self.assertEqual([], UserNotification.query().all())
285 self.assertEqual([], UserNotification.query().all())
286
286
287 notification = NotificationModel().create(created_by=self.u1,
287 notification = NotificationModel().create(created_by=self.u1,
288 subject=u'title', body=u'hi there3',
288 subject=u'title', body=u'hi there3',
289 recipients=[self.u3, self.u1, self.u2])
289 recipients=[self.u3, self.u1, self.u2])
290 Session.commit()
290 Session.commit()
291 notifications = Notification.query().all()
291 notifications = Notification.query().all()
292 self.assertTrue(notification in notifications)
292 self.assertTrue(notification in notifications)
293
293
294 Notification.delete(notification.notification_id)
294 Notification.delete(notification.notification_id)
295 Session.commit()
295 Session.commit()
296
296
297 notifications = Notification.query().all()
297 notifications = Notification.query().all()
298 self.assertFalse(notification in notifications)
298 self.assertFalse(notification in notifications)
299
299
300 un = UserNotification.query().filter(UserNotification.notification
300 un = UserNotification.query().filter(UserNotification.notification
301 == notification).all()
301 == notification).all()
302 self.assertEqual(un, [])
302 self.assertEqual(un, [])
303
303
304 def test_delete_association(self):
304 def test_delete_association(self):
305
305
306 self.assertEqual([], Notification.query().all())
306 self.assertEqual([], Notification.query().all())
307 self.assertEqual([], UserNotification.query().all())
307 self.assertEqual([], UserNotification.query().all())
308
308
309 notification = NotificationModel().create(created_by=self.u1,
309 notification = NotificationModel().create(created_by=self.u1,
310 subject=u'title', body=u'hi there3',
310 subject=u'title', body=u'hi there3',
311 recipients=[self.u3, self.u1, self.u2])
311 recipients=[self.u3, self.u1, self.u2])
312 Session.commit()
312 Session.commit()
313
313
314 unotification = UserNotification.query()\
314 unotification = UserNotification.query()\
315 .filter(UserNotification.notification ==
315 .filter(UserNotification.notification ==
316 notification)\
316 notification)\
317 .filter(UserNotification.user_id == self.u3)\
317 .filter(UserNotification.user_id == self.u3)\
318 .scalar()
318 .scalar()
319
319
320 self.assertEqual(unotification.user_id, self.u3)
320 self.assertEqual(unotification.user_id, self.u3)
321
321
322 NotificationModel().delete(self.u3,
322 NotificationModel().delete(self.u3,
323 notification.notification_id)
323 notification.notification_id)
324 Session.commit()
324 Session.commit()
325
325
326 u3notification = UserNotification.query()\
326 u3notification = UserNotification.query()\
327 .filter(UserNotification.notification ==
327 .filter(UserNotification.notification ==
328 notification)\
328 notification)\
329 .filter(UserNotification.user_id == self.u3)\
329 .filter(UserNotification.user_id == self.u3)\
330 .scalar()
330 .scalar()
331
331
332 self.assertEqual(u3notification, None)
332 self.assertEqual(u3notification, None)
333
333
334 # notification object is still there
334 # notification object is still there
335 self.assertEqual(Notification.query().all(), [notification])
335 self.assertEqual(Notification.query().all(), [notification])
336
336
337 #u1 and u2 still have assignments
337 #u1 and u2 still have assignments
338 u1notification = UserNotification.query()\
338 u1notification = UserNotification.query()\
339 .filter(UserNotification.notification ==
339 .filter(UserNotification.notification ==
340 notification)\
340 notification)\
341 .filter(UserNotification.user_id == self.u1)\
341 .filter(UserNotification.user_id == self.u1)\
342 .scalar()
342 .scalar()
343 self.assertNotEqual(u1notification, None)
343 self.assertNotEqual(u1notification, None)
344 u2notification = UserNotification.query()\
344 u2notification = UserNotification.query()\
345 .filter(UserNotification.notification ==
345 .filter(UserNotification.notification ==
346 notification)\
346 notification)\
347 .filter(UserNotification.user_id == self.u2)\
347 .filter(UserNotification.user_id == self.u2)\
348 .scalar()
348 .scalar()
349 self.assertNotEqual(u2notification, None)
349 self.assertNotEqual(u2notification, None)
350
350
351 def test_notification_counter(self):
351 def test_notification_counter(self):
352 self._clean_notifications()
352 self._clean_notifications()
353 self.assertEqual([], Notification.query().all())
353 self.assertEqual([], Notification.query().all())
354 self.assertEqual([], UserNotification.query().all())
354 self.assertEqual([], UserNotification.query().all())
355
355
356 NotificationModel().create(created_by=self.u1,
356 NotificationModel().create(created_by=self.u1,
357 subject=u'title', body=u'hi there_delete',
357 subject=u'title', body=u'hi there_delete',
358 recipients=[self.u3, self.u1])
358 recipients=[self.u3, self.u1])
359 Session.commit()
359 Session.commit()
360
360
361 self.assertEqual(NotificationModel()
361 self.assertEqual(NotificationModel()
362 .get_unread_cnt_for_user(self.u1), 1)
362 .get_unread_cnt_for_user(self.u1), 1)
363 self.assertEqual(NotificationModel()
363 self.assertEqual(NotificationModel()
364 .get_unread_cnt_for_user(self.u2), 0)
364 .get_unread_cnt_for_user(self.u2), 0)
365 self.assertEqual(NotificationModel()
365 self.assertEqual(NotificationModel()
366 .get_unread_cnt_for_user(self.u3), 1)
366 .get_unread_cnt_for_user(self.u3), 1)
367
367
368 notification = NotificationModel().create(created_by=self.u1,
368 notification = NotificationModel().create(created_by=self.u1,
369 subject=u'title', body=u'hi there3',
369 subject=u'title', body=u'hi there3',
370 recipients=[self.u3, self.u1, self.u2])
370 recipients=[self.u3, self.u1, self.u2])
371 Session.commit()
371 Session.commit()
372
372
373 self.assertEqual(NotificationModel()
373 self.assertEqual(NotificationModel()
374 .get_unread_cnt_for_user(self.u1), 2)
374 .get_unread_cnt_for_user(self.u1), 2)
375 self.assertEqual(NotificationModel()
375 self.assertEqual(NotificationModel()
376 .get_unread_cnt_for_user(self.u2), 1)
376 .get_unread_cnt_for_user(self.u2), 1)
377 self.assertEqual(NotificationModel()
377 self.assertEqual(NotificationModel()
378 .get_unread_cnt_for_user(self.u3), 2)
378 .get_unread_cnt_for_user(self.u3), 2)
379
379
380
380
381 class TestUsers(unittest.TestCase):
381 class TestUsers(unittest.TestCase):
382
382
383 def __init__(self, methodName='runTest'):
383 def __init__(self, methodName='runTest'):
384 super(TestUsers, self).__init__(methodName=methodName)
384 super(TestUsers, self).__init__(methodName=methodName)
385
385
386 def setUp(self):
386 def setUp(self):
387 self.u1 = UserModel().create_or_update(username=u'u1',
387 self.u1 = UserModel().create_or_update(username=u'u1',
388 password=u'qweqwe',
388 password=u'qweqwe',
389 email=u'u1@rhodecode.org',
389 email=u'u1@rhodecode.org',
390 name=u'u1', lastname=u'u1')
390 name=u'u1', lastname=u'u1')
391
391
392 def tearDown(self):
392 def tearDown(self):
393 perm = Permission.query().all()
393 perm = Permission.query().all()
394 for p in perm:
394 for p in perm:
395 UserModel().revoke_perm(self.u1, p)
395 UserModel().revoke_perm(self.u1, p)
396
396
397 UserModel().delete(self.u1)
397 UserModel().delete(self.u1)
398 Session.commit()
398 Session.commit()
399
399
400 def test_add_perm(self):
400 def test_add_perm(self):
401 perm = Permission.query().all()[0]
401 perm = Permission.query().all()[0]
402 UserModel().grant_perm(self.u1, perm)
402 UserModel().grant_perm(self.u1, perm)
403 Session.commit()
403 Session.commit()
404 self.assertEqual(UserModel().has_perm(self.u1, perm), True)
404 self.assertEqual(UserModel().has_perm(self.u1, perm), True)
405
405
406 def test_has_perm(self):
406 def test_has_perm(self):
407 perm = Permission.query().all()
407 perm = Permission.query().all()
408 for p in perm:
408 for p in perm:
409 has_p = UserModel().has_perm(self.u1, p)
409 has_p = UserModel().has_perm(self.u1, p)
410 self.assertEqual(False, has_p)
410 self.assertEqual(False, has_p)
411
411
412 def test_revoke_perm(self):
412 def test_revoke_perm(self):
413 perm = Permission.query().all()[0]
413 perm = Permission.query().all()[0]
414 UserModel().grant_perm(self.u1, perm)
414 UserModel().grant_perm(self.u1, perm)
415 Session.commit()
415 Session.commit()
416 self.assertEqual(UserModel().has_perm(self.u1, perm), True)
416 self.assertEqual(UserModel().has_perm(self.u1, perm), True)
417
417
418 #revoke
418 #revoke
419 UserModel().revoke_perm(self.u1, perm)
419 UserModel().revoke_perm(self.u1, perm)
420 Session.commit()
420 Session.commit()
421 self.assertEqual(UserModel().has_perm(self.u1, perm), False)
421 self.assertEqual(UserModel().has_perm(self.u1, perm), False)
422
422
423
423
424 class TestPermissions(unittest.TestCase):
424 class TestPermissions(unittest.TestCase):
425 def __init__(self, methodName='runTest'):
425 def __init__(self, methodName='runTest'):
426 super(TestPermissions, self).__init__(methodName=methodName)
426 super(TestPermissions, self).__init__(methodName=methodName)
427
427
428 def setUp(self):
428 def setUp(self):
429 self.u1 = UserModel().create_or_update(
429 self.u1 = UserModel().create_or_update(
430 username=u'u1', password=u'qweqwe',
430 username=u'u1', password=u'qweqwe',
431 email=u'u1@rhodecode.org', name=u'u1', lastname=u'u1'
431 email=u'u1@rhodecode.org', name=u'u1', lastname=u'u1'
432 )
432 )
433 self.u2 = UserModel().create_or_update(
433 self.u2 = UserModel().create_or_update(
434 username=u'u2', password=u'qweqwe',
434 username=u'u2', password=u'qweqwe',
435 email=u'u2@rhodecode.org', name=u'u2', lastname=u'u2'
435 email=u'u2@rhodecode.org', name=u'u2', lastname=u'u2'
436 )
436 )
437 self.anon = User.get_by_username('default')
437 self.anon = User.get_by_username('default')
438 self.a1 = UserModel().create_or_update(
438 self.a1 = UserModel().create_or_update(
439 username=u'a1', password=u'qweqwe',
439 username=u'a1', password=u'qweqwe',
440 email=u'a1@rhodecode.org', name=u'a1', lastname=u'a1', admin=True
440 email=u'a1@rhodecode.org', name=u'a1', lastname=u'a1', admin=True
441 )
441 )
442 Session.commit()
442 Session.commit()
443
443
444 def tearDown(self):
444 def tearDown(self):
445 if hasattr(self, 'test_repo'):
445 if hasattr(self, 'test_repo'):
446 RepoModel().delete(repo=self.test_repo)
446 RepoModel().delete(repo=self.test_repo)
447 UserModel().delete(self.u1)
447 UserModel().delete(self.u1)
448 UserModel().delete(self.u2)
448 UserModel().delete(self.u2)
449 UserModel().delete(self.a1)
449 UserModel().delete(self.a1)
450 if hasattr(self, 'g1'):
450 if hasattr(self, 'g1'):
451 ReposGroupModel().delete(self.g1.group_id)
451 ReposGroupModel().delete(self.g1.group_id)
452 if hasattr(self, 'g2'):
452 if hasattr(self, 'g2'):
453 ReposGroupModel().delete(self.g2.group_id)
453 ReposGroupModel().delete(self.g2.group_id)
454
454
455 if hasattr(self, 'ug1'):
455 if hasattr(self, 'ug1'):
456 UsersGroupModel().delete(self.ug1, force=True)
456 UsersGroupModel().delete(self.ug1, force=True)
457
457
458 Session.commit()
458 Session.commit()
459
459
460 def test_default_perms_set(self):
460 def test_default_perms_set(self):
461 u1_auth = AuthUser(user_id=self.u1.user_id)
461 u1_auth = AuthUser(user_id=self.u1.user_id)
462 perms = {
462 perms = {
463 'repositories_groups': {},
463 'repositories_groups': {},
464 'global': set([u'hg.create.repository', u'repository.read',
464 'global': set([u'hg.create.repository', u'repository.read',
465 u'hg.register.manual_activate']),
465 u'hg.register.manual_activate']),
466 'repositories': {u'vcs_test_hg': u'repository.read'}
466 'repositories': {u'vcs_test_hg': u'repository.read'}
467 }
467 }
468 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
468 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
469 perms['repositories'][HG_REPO])
469 perms['repositories'][HG_REPO])
470 new_perm = 'repository.write'
470 new_perm = 'repository.write'
471 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm=new_perm)
471 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm=new_perm)
472 Session.commit()
472 Session.commit()
473
473
474 u1_auth = AuthUser(user_id=self.u1.user_id)
474 u1_auth = AuthUser(user_id=self.u1.user_id)
475 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], new_perm)
475 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], new_perm)
476
476
477 def test_default_admin_perms_set(self):
477 def test_default_admin_perms_set(self):
478 a1_auth = AuthUser(user_id=self.a1.user_id)
478 a1_auth = AuthUser(user_id=self.a1.user_id)
479 perms = {
479 perms = {
480 'repositories_groups': {},
480 'repositories_groups': {},
481 'global': set([u'hg.admin']),
481 'global': set([u'hg.admin']),
482 'repositories': {u'vcs_test_hg': u'repository.admin'}
482 'repositories': {u'vcs_test_hg': u'repository.admin'}
483 }
483 }
484 self.assertEqual(a1_auth.permissions['repositories'][HG_REPO],
484 self.assertEqual(a1_auth.permissions['repositories'][HG_REPO],
485 perms['repositories'][HG_REPO])
485 perms['repositories'][HG_REPO])
486 new_perm = 'repository.write'
486 new_perm = 'repository.write'
487 RepoModel().grant_user_permission(repo=HG_REPO, user=self.a1, perm=new_perm)
487 RepoModel().grant_user_permission(repo=HG_REPO, user=self.a1, perm=new_perm)
488 Session.commit()
488 Session.commit()
489 # cannot really downgrade admins permissions !? they still get's set as
489 # cannot really downgrade admins permissions !? they still get's set as
490 # admin !
490 # admin !
491 u1_auth = AuthUser(user_id=self.a1.user_id)
491 u1_auth = AuthUser(user_id=self.a1.user_id)
492 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
492 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
493 perms['repositories'][HG_REPO])
493 perms['repositories'][HG_REPO])
494
494
495 def test_default_group_perms(self):
495 def test_default_group_perms(self):
496 self.g1 = _make_group('test1', skip_if_exists=True)
496 self.g1 = _make_group('test1', skip_if_exists=True)
497 self.g2 = _make_group('test2', skip_if_exists=True)
497 self.g2 = _make_group('test2', skip_if_exists=True)
498 u1_auth = AuthUser(user_id=self.u1.user_id)
498 u1_auth = AuthUser(user_id=self.u1.user_id)
499 perms = {
499 perms = {
500 'repositories_groups': {u'test1': 'group.read', u'test2': 'group.read'},
500 'repositories_groups': {u'test1': 'group.read', u'test2': 'group.read'},
501 'global': set([u'hg.create.repository', u'repository.read', u'hg.register.manual_activate']),
501 'global': set([u'hg.create.repository', u'repository.read', u'hg.register.manual_activate']),
502 'repositories': {u'vcs_test_hg': u'repository.read'}
502 'repositories': {u'vcs_test_hg': u'repository.read'}
503 }
503 }
504 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
504 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
505 perms['repositories'][HG_REPO])
505 perms['repositories'][HG_REPO])
506 self.assertEqual(u1_auth.permissions['repositories_groups'],
506 self.assertEqual(u1_auth.permissions['repositories_groups'],
507 perms['repositories_groups'])
507 perms['repositories_groups'])
508
508
509 def test_default_admin_group_perms(self):
509 def test_default_admin_group_perms(self):
510 self.g1 = _make_group('test1', skip_if_exists=True)
510 self.g1 = _make_group('test1', skip_if_exists=True)
511 self.g2 = _make_group('test2', skip_if_exists=True)
511 self.g2 = _make_group('test2', skip_if_exists=True)
512 a1_auth = AuthUser(user_id=self.a1.user_id)
512 a1_auth = AuthUser(user_id=self.a1.user_id)
513 perms = {
513 perms = {
514 'repositories_groups': {u'test1': 'group.admin', u'test2': 'group.admin'},
514 'repositories_groups': {u'test1': 'group.admin', u'test2': 'group.admin'},
515 'global': set(['hg.admin']),
515 'global': set(['hg.admin']),
516 'repositories': {u'vcs_test_hg': 'repository.admin'}
516 'repositories': {u'vcs_test_hg': 'repository.admin'}
517 }
517 }
518
518
519 self.assertEqual(a1_auth.permissions['repositories'][HG_REPO],
519 self.assertEqual(a1_auth.permissions['repositories'][HG_REPO],
520 perms['repositories'][HG_REPO])
520 perms['repositories'][HG_REPO])
521 self.assertEqual(a1_auth.permissions['repositories_groups'],
521 self.assertEqual(a1_auth.permissions['repositories_groups'],
522 perms['repositories_groups'])
522 perms['repositories_groups'])
523
523
524 def test_propagated_permission_from_users_group(self):
524 def test_propagated_permission_from_users_group(self):
525 # make group
525 # make group
526 self.ug1 = UsersGroupModel().create('G1')
526 self.ug1 = UsersGroupModel().create('G1')
527 # add user to group
527 # add user to group
528 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
528 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
529
529
530 # set permission to lower
530 # set permission to lower
531 new_perm = 'repository.none'
531 new_perm = 'repository.none'
532 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm=new_perm)
532 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm=new_perm)
533 Session.commit()
533 Session.commit()
534 u1_auth = AuthUser(user_id=self.u1.user_id)
534 u1_auth = AuthUser(user_id=self.u1.user_id)
535 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
535 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
536 new_perm)
536 new_perm)
537
537
538 # grant perm for group this should override permission from user
538 # grant perm for group this should override permission from user
539 new_perm = 'repository.write'
539 new_perm = 'repository.write'
540 RepoModel().grant_users_group_permission(repo=HG_REPO,
540 RepoModel().grant_users_group_permission(repo=HG_REPO,
541 group_name=self.ug1,
541 group_name=self.ug1,
542 perm=new_perm)
542 perm=new_perm)
543 # check perms
543 # check perms
544 u1_auth = AuthUser(user_id=self.u1.user_id)
544 u1_auth = AuthUser(user_id=self.u1.user_id)
545 perms = {
545 perms = {
546 'repositories_groups': {},
546 'repositories_groups': {},
547 'global': set([u'hg.create.repository', u'repository.read',
547 'global': set([u'hg.create.repository', u'repository.read',
548 u'hg.register.manual_activate']),
548 u'hg.register.manual_activate']),
549 'repositories': {u'vcs_test_hg': u'repository.read'}
549 'repositories': {u'vcs_test_hg': u'repository.read'}
550 }
550 }
551 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
551 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
552 new_perm)
552 new_perm)
553 self.assertEqual(u1_auth.permissions['repositories_groups'],
553 self.assertEqual(u1_auth.permissions['repositories_groups'],
554 perms['repositories_groups'])
554 perms['repositories_groups'])
555
555
556 def test_propagated_permission_from_users_group_lower_weight(self):
556 def test_propagated_permission_from_users_group_lower_weight(self):
557 # make group
557 # make group
558 self.ug1 = UsersGroupModel().create('G1')
558 self.ug1 = UsersGroupModel().create('G1')
559 # add user to group
559 # add user to group
560 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
560 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
561
561
562 # set permission to lower
562 # set permission to lower
563 new_perm_h = 'repository.write'
563 new_perm_h = 'repository.write'
564 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1,
564 RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1,
565 perm=new_perm_h)
565 perm=new_perm_h)
566 Session.commit()
566 Session.commit()
567 u1_auth = AuthUser(user_id=self.u1.user_id)
567 u1_auth = AuthUser(user_id=self.u1.user_id)
568 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
568 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
569 new_perm_h)
569 new_perm_h)
570
570
571 # grant perm for group this should NOT override permission from user
571 # grant perm for group this should NOT override permission from user
572 # since it's lower than granted
572 # since it's lower than granted
573 new_perm_l = 'repository.read'
573 new_perm_l = 'repository.read'
574 RepoModel().grant_users_group_permission(repo=HG_REPO,
574 RepoModel().grant_users_group_permission(repo=HG_REPO,
575 group_name=self.ug1,
575 group_name=self.ug1,
576 perm=new_perm_l)
576 perm=new_perm_l)
577 # check perms
577 # check perms
578 u1_auth = AuthUser(user_id=self.u1.user_id)
578 u1_auth = AuthUser(user_id=self.u1.user_id)
579 perms = {
579 perms = {
580 'repositories_groups': {},
580 'repositories_groups': {},
581 'global': set([u'hg.create.repository', u'repository.read',
581 'global': set([u'hg.create.repository', u'repository.read',
582 u'hg.register.manual_activate']),
582 u'hg.register.manual_activate']),
583 'repositories': {u'vcs_test_hg': u'repository.write'}
583 'repositories': {u'vcs_test_hg': u'repository.write'}
584 }
584 }
585 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
585 self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
586 new_perm_h)
586 new_perm_h)
587 self.assertEqual(u1_auth.permissions['repositories_groups'],
587 self.assertEqual(u1_auth.permissions['repositories_groups'],
588 perms['repositories_groups'])
588 perms['repositories_groups'])
589
589
590 def test_repo_in_group_permissions(self):
590 def test_repo_in_group_permissions(self):
591 self.g1 = _make_group('group1', skip_if_exists=True)
591 self.g1 = _make_group('group1', skip_if_exists=True)
592 self.g2 = _make_group('group2', skip_if_exists=True)
592 self.g2 = _make_group('group2', skip_if_exists=True)
593 Session.commit()
593 Session.commit()
594 # both perms should be read !
594 # both perms should be read !
595 u1_auth = AuthUser(user_id=self.u1.user_id)
595 u1_auth = AuthUser(user_id=self.u1.user_id)
596 self.assertEqual(u1_auth.permissions['repositories_groups'],
596 self.assertEqual(u1_auth.permissions['repositories_groups'],
597 {u'group1': u'group.read', u'group2': u'group.read'})
597 {u'group1': u'group.read', u'group2': u'group.read'})
598
598
599 a1_auth = AuthUser(user_id=self.anon.user_id)
599 a1_auth = AuthUser(user_id=self.anon.user_id)
600 self.assertEqual(a1_auth.permissions['repositories_groups'],
600 self.assertEqual(a1_auth.permissions['repositories_groups'],
601 {u'group1': u'group.read', u'group2': u'group.read'})
601 {u'group1': u'group.read', u'group2': u'group.read'})
602
602
603 #Change perms to none for both groups
603 #Change perms to none for both groups
604 ReposGroupModel().grant_user_permission(repos_group=self.g1,
604 ReposGroupModel().grant_user_permission(repos_group=self.g1,
605 user=self.anon,
605 user=self.anon,
606 perm='group.none')
606 perm='group.none')
607 ReposGroupModel().grant_user_permission(repos_group=self.g2,
607 ReposGroupModel().grant_user_permission(repos_group=self.g2,
608 user=self.anon,
608 user=self.anon,
609 perm='group.none')
609 perm='group.none')
610
610
611
611
612 u1_auth = AuthUser(user_id=self.u1.user_id)
612 u1_auth = AuthUser(user_id=self.u1.user_id)
613 self.assertEqual(u1_auth.permissions['repositories_groups'],
613 self.assertEqual(u1_auth.permissions['repositories_groups'],
614 {u'group1': u'group.none', u'group2': u'group.none'})
614 {u'group1': u'group.none', u'group2': u'group.none'})
615
615
616 a1_auth = AuthUser(user_id=self.anon.user_id)
616 a1_auth = AuthUser(user_id=self.anon.user_id)
617 self.assertEqual(a1_auth.permissions['repositories_groups'],
617 self.assertEqual(a1_auth.permissions['repositories_groups'],
618 {u'group1': u'group.none', u'group2': u'group.none'})
618 {u'group1': u'group.none', u'group2': u'group.none'})
619
619
620 # add repo to group
620 # add repo to group
621 form_data = {
621 form_data = {
622 'repo_name':HG_REPO,
622 'repo_name':HG_REPO,
623 'repo_name_full':os.path.join(self.g1.group_name,HG_REPO),
623 'repo_name_full':RepoGroup.url_sep().join([self.g1.group_name,HG_REPO]),
624 'repo_type':'hg',
624 'repo_type':'hg',
625 'clone_uri':'',
625 'clone_uri':'',
626 'repo_group':self.g1.group_id,
626 'repo_group':self.g1.group_id,
627 'description':'desc',
627 'description':'desc',
628 'private':False
628 'private':False
629 }
629 }
630 self.test_repo = RepoModel().create(form_data, cur_user=self.u1)
630 self.test_repo = RepoModel().create(form_data, cur_user=self.u1)
631 Session.commit()
631 Session.commit()
632
632
633 u1_auth = AuthUser(user_id=self.u1.user_id)
633 u1_auth = AuthUser(user_id=self.u1.user_id)
634 self.assertEqual(u1_auth.permissions['repositories_groups'],
634 self.assertEqual(u1_auth.permissions['repositories_groups'],
635 {u'group1': u'group.none', u'group2': u'group.none'})
635 {u'group1': u'group.none', u'group2': u'group.none'})
636
636
637 a1_auth = AuthUser(user_id=self.anon.user_id)
637 a1_auth = AuthUser(user_id=self.anon.user_id)
638 self.assertEqual(a1_auth.permissions['repositories_groups'],
638 self.assertEqual(a1_auth.permissions['repositories_groups'],
639 {u'group1': u'group.none', u'group2': u'group.none'})
639 {u'group1': u'group.none', u'group2': u'group.none'})
640
640
641 #grant permission for u2 !
641 #grant permission for u2 !
642 ReposGroupModel().grant_user_permission(repos_group=self.g1,
642 ReposGroupModel().grant_user_permission(repos_group=self.g1,
643 user=self.u2,
643 user=self.u2,
644 perm='group.read')
644 perm='group.read')
645 ReposGroupModel().grant_user_permission(repos_group=self.g2,
645 ReposGroupModel().grant_user_permission(repos_group=self.g2,
646 user=self.u2,
646 user=self.u2,
647 perm='group.read')
647 perm='group.read')
648 Session.commit()
648 Session.commit()
649 self.assertNotEqual(self.u1, self.u2)
649 self.assertNotEqual(self.u1, self.u2)
650 #u1 and anon should have not change perms while u2 should !
650 #u1 and anon should have not change perms while u2 should !
651 u1_auth = AuthUser(user_id=self.u1.user_id)
651 u1_auth = AuthUser(user_id=self.u1.user_id)
652 self.assertEqual(u1_auth.permissions['repositories_groups'],
652 self.assertEqual(u1_auth.permissions['repositories_groups'],
653 {u'group1': u'group.none', u'group2': u'group.none'})
653 {u'group1': u'group.none', u'group2': u'group.none'})
654
654
655 u2_auth = AuthUser(user_id=self.u2.user_id)
655 u2_auth = AuthUser(user_id=self.u2.user_id)
656 self.assertEqual(u2_auth.permissions['repositories_groups'],
656 self.assertEqual(u2_auth.permissions['repositories_groups'],
657 {u'group1': u'group.read', u'group2': u'group.read'})
657 {u'group1': u'group.read', u'group2': u'group.read'})
658
658
659 a1_auth = AuthUser(user_id=self.anon.user_id)
659 a1_auth = AuthUser(user_id=self.anon.user_id)
660 self.assertEqual(a1_auth.permissions['repositories_groups'],
660 self.assertEqual(a1_auth.permissions['repositories_groups'],
661 {u'group1': u'group.none', u'group2': u'group.none'})
661 {u'group1': u'group.none', u'group2': u'group.none'})
662
662
663 def test_repo_group_user_as_user_group_member(self):
663 def test_repo_group_user_as_user_group_member(self):
664 # create Group1
664 # create Group1
665 self.g1 = _make_group('group1', skip_if_exists=True)
665 self.g1 = _make_group('group1', skip_if_exists=True)
666 Session.commit()
666 Session.commit()
667 a1_auth = AuthUser(user_id=self.anon.user_id)
667 a1_auth = AuthUser(user_id=self.anon.user_id)
668
668
669 self.assertEqual(a1_auth.permissions['repositories_groups'],
669 self.assertEqual(a1_auth.permissions['repositories_groups'],
670 {u'group1': u'group.read'})
670 {u'group1': u'group.read'})
671
671
672 # set default permission to none
672 # set default permission to none
673 ReposGroupModel().grant_user_permission(repos_group=self.g1,
673 ReposGroupModel().grant_user_permission(repos_group=self.g1,
674 user=self.anon,
674 user=self.anon,
675 perm='group.none')
675 perm='group.none')
676 # make group
676 # make group
677 self.ug1 = UsersGroupModel().create('G1')
677 self.ug1 = UsersGroupModel().create('G1')
678 # add user to group
678 # add user to group
679 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
679 UsersGroupModel().add_user_to_group(self.ug1, self.u1)
680 Session.commit()
680 Session.commit()
681
681
682 # check if user is in the group
682 # check if user is in the group
683 membrs = [x.user_id for x in UsersGroupModel().get(self.ug1.users_group_id).members]
683 membrs = [x.user_id for x in UsersGroupModel().get(self.ug1.users_group_id).members]
684 self.assertEqual(membrs, [self.u1.user_id])
684 self.assertEqual(membrs, [self.u1.user_id])
685 # add some user to that group
685 # add some user to that group
686
686
687 # check his permissions
687 # check his permissions
688 a1_auth = AuthUser(user_id=self.anon.user_id)
688 a1_auth = AuthUser(user_id=self.anon.user_id)
689 self.assertEqual(a1_auth.permissions['repositories_groups'],
689 self.assertEqual(a1_auth.permissions['repositories_groups'],
690 {u'group1': u'group.none'})
690 {u'group1': u'group.none'})
691
691
692 u1_auth = AuthUser(user_id=self.u1.user_id)
692 u1_auth = AuthUser(user_id=self.u1.user_id)
693 self.assertEqual(u1_auth.permissions['repositories_groups'],
693 self.assertEqual(u1_auth.permissions['repositories_groups'],
694 {u'group1': u'group.none'})
694 {u'group1': u'group.none'})
695
695
696 # grant ug1 read permissions for
696 # grant ug1 read permissions for
697 ReposGroupModel().grant_users_group_permission(repos_group=self.g1,
697 ReposGroupModel().grant_users_group_permission(repos_group=self.g1,
698 group_name=self.ug1,
698 group_name=self.ug1,
699 perm='group.read')
699 perm='group.read')
700 Session.commit()
700 Session.commit()
701 # check if the
701 # check if the
702 obj = Session.query(UsersGroupRepoGroupToPerm)\
702 obj = Session.query(UsersGroupRepoGroupToPerm)\
703 .filter(UsersGroupRepoGroupToPerm.group == self.g1)\
703 .filter(UsersGroupRepoGroupToPerm.group == self.g1)\
704 .filter(UsersGroupRepoGroupToPerm.users_group == self.ug1)\
704 .filter(UsersGroupRepoGroupToPerm.users_group == self.ug1)\
705 .scalar()
705 .scalar()
706 self.assertEqual(obj.permission.permission_name, 'group.read')
706 self.assertEqual(obj.permission.permission_name, 'group.read')
707
707
708 a1_auth = AuthUser(user_id=self.anon.user_id)
708 a1_auth = AuthUser(user_id=self.anon.user_id)
709
709
710 self.assertEqual(a1_auth.permissions['repositories_groups'],
710 self.assertEqual(a1_auth.permissions['repositories_groups'],
711 {u'group1': u'group.none'})
711 {u'group1': u'group.none'})
712
712
713 u1_auth = AuthUser(user_id=self.u1.user_id)
713 u1_auth = AuthUser(user_id=self.u1.user_id)
714 self.assertEqual(u1_auth.permissions['repositories_groups'],
714 self.assertEqual(u1_auth.permissions['repositories_groups'],
715 {u'group1': u'group.read'})
715 {u'group1': u'group.read'})
General Comments 0
You need to be logged in to leave comments. Login now