##// END OF EJS Templates
- added commenting to pull requests...
marcink -
r2443:fd0a8224 codereview
parent child Browse files
Show More
@@ -1,552 +1,558 b''
1 1 """
2 2 Routes configuration
3 3
4 4 The more specific and detailed routes should be defined first so they
5 5 may take precedent over the more generic routes. For more information
6 6 refer to the routes manual at http://routes.groovie.org/docs/
7 7 """
8 8 from __future__ import with_statement
9 9 from routes import Mapper
10 10
11 11 # prefix for non repository related links needs to be prefixed with `/`
12 12 ADMIN_PREFIX = '/_admin'
13 13
14 14
15 15 def make_map(config):
16 16 """Create, configure and return the routes Mapper"""
17 17 rmap = Mapper(directory=config['pylons.paths']['controllers'],
18 18 always_scan=config['debug'])
19 19 rmap.minimization = False
20 20 rmap.explicit = False
21 21
22 22 from rhodecode.lib.utils import is_valid_repo
23 23 from rhodecode.lib.utils import is_valid_repos_group
24 24
25 25 def check_repo(environ, match_dict):
26 26 """
27 27 check for valid repository for proper 404 handling
28 28
29 29 :param environ:
30 30 :param match_dict:
31 31 """
32 32 from rhodecode.model.db import Repository
33 33 repo_name = match_dict.get('repo_name')
34 34
35 35 try:
36 36 by_id = repo_name.split('_')
37 37 if len(by_id) == 2 and by_id[1].isdigit():
38 38 repo_name = Repository.get(by_id[1]).repo_name
39 39 match_dict['repo_name'] = repo_name
40 40 except:
41 41 pass
42 42
43 43 return is_valid_repo(repo_name, config['base_path'])
44 44
45 45 def check_group(environ, match_dict):
46 46 """
47 47 check for valid repositories group for proper 404 handling
48 48
49 49 :param environ:
50 50 :param match_dict:
51 51 """
52 52 repos_group_name = match_dict.get('group_name')
53 53
54 54 return is_valid_repos_group(repos_group_name, config['base_path'])
55 55
56 56 def check_int(environ, match_dict):
57 57 return match_dict.get('id').isdigit()
58 58
59 59 # The ErrorController route (handles 404/500 error pages); it should
60 60 # likely stay at the top, ensuring it can always be resolved
61 61 rmap.connect('/error/{action}', controller='error')
62 62 rmap.connect('/error/{action}/{id}', controller='error')
63 63
64 64 #==========================================================================
65 65 # CUSTOM ROUTES HERE
66 66 #==========================================================================
67 67
68 68 #MAIN PAGE
69 69 rmap.connect('home', '/', controller='home', action='index')
70 70 rmap.connect('repo_switcher', '/repos', controller='home',
71 71 action='repo_switcher')
72 72 rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*}',
73 73 controller='home', action='branch_tag_switcher')
74 74 rmap.connect('bugtracker',
75 75 "http://bitbucket.org/marcinkuzminski/rhodecode/issues",
76 76 _static=True)
77 77 rmap.connect('rst_help',
78 78 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
79 79 _static=True)
80 80 rmap.connect('rhodecode_official', "http://rhodecode.org", _static=True)
81 81
82 82 #ADMIN REPOSITORY REST ROUTES
83 83 with rmap.submapper(path_prefix=ADMIN_PREFIX,
84 84 controller='admin/repos') as m:
85 85 m.connect("repos", "/repos",
86 86 action="create", conditions=dict(method=["POST"]))
87 87 m.connect("repos", "/repos",
88 88 action="index", conditions=dict(method=["GET"]))
89 89 m.connect("formatted_repos", "/repos.{format}",
90 90 action="index",
91 91 conditions=dict(method=["GET"]))
92 92 m.connect("new_repo", "/repos/new",
93 93 action="new", conditions=dict(method=["GET"]))
94 94 m.connect("formatted_new_repo", "/repos/new.{format}",
95 95 action="new", conditions=dict(method=["GET"]))
96 96 m.connect("/repos/{repo_name:.*}",
97 97 action="update", conditions=dict(method=["PUT"],
98 98 function=check_repo))
99 99 m.connect("/repos/{repo_name:.*}",
100 100 action="delete", conditions=dict(method=["DELETE"],
101 101 function=check_repo))
102 102 m.connect("edit_repo", "/repos/{repo_name:.*}/edit",
103 103 action="edit", conditions=dict(method=["GET"],
104 104 function=check_repo))
105 105 m.connect("formatted_edit_repo", "/repos/{repo_name:.*}.{format}/edit",
106 106 action="edit", conditions=dict(method=["GET"],
107 107 function=check_repo))
108 108 m.connect("repo", "/repos/{repo_name:.*}",
109 109 action="show", conditions=dict(method=["GET"],
110 110 function=check_repo))
111 111 m.connect("formatted_repo", "/repos/{repo_name:.*}.{format}",
112 112 action="show", conditions=dict(method=["GET"],
113 113 function=check_repo))
114 114 #ajax delete repo perm user
115 115 m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*}",
116 116 action="delete_perm_user",
117 117 conditions=dict(method=["DELETE"], function=check_repo))
118 118
119 119 #ajax delete repo perm users_group
120 120 m.connect('delete_repo_users_group',
121 121 "/repos_delete_users_group/{repo_name:.*}",
122 122 action="delete_perm_users_group",
123 123 conditions=dict(method=["DELETE"], function=check_repo))
124 124
125 125 #settings actions
126 126 m.connect('repo_stats', "/repos_stats/{repo_name:.*}",
127 127 action="repo_stats", conditions=dict(method=["DELETE"],
128 128 function=check_repo))
129 129 m.connect('repo_cache', "/repos_cache/{repo_name:.*}",
130 130 action="repo_cache", conditions=dict(method=["DELETE"],
131 131 function=check_repo))
132 132 m.connect('repo_public_journal', "/repos_public_journal/{repo_name:.*}",
133 133 action="repo_public_journal", conditions=dict(method=["PUT"],
134 134 function=check_repo))
135 135 m.connect('repo_pull', "/repo_pull/{repo_name:.*}",
136 136 action="repo_pull", conditions=dict(method=["PUT"],
137 137 function=check_repo))
138 138 m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*}",
139 139 action="repo_as_fork", conditions=dict(method=["PUT"],
140 140 function=check_repo))
141 141
142 142 with rmap.submapper(path_prefix=ADMIN_PREFIX,
143 143 controller='admin/repos_groups') as m:
144 144 m.connect("repos_groups", "/repos_groups",
145 145 action="create", conditions=dict(method=["POST"]))
146 146 m.connect("repos_groups", "/repos_groups",
147 147 action="index", conditions=dict(method=["GET"]))
148 148 m.connect("formatted_repos_groups", "/repos_groups.{format}",
149 149 action="index", conditions=dict(method=["GET"]))
150 150 m.connect("new_repos_group", "/repos_groups/new",
151 151 action="new", conditions=dict(method=["GET"]))
152 152 m.connect("formatted_new_repos_group", "/repos_groups/new.{format}",
153 153 action="new", conditions=dict(method=["GET"]))
154 154 m.connect("update_repos_group", "/repos_groups/{id}",
155 155 action="update", conditions=dict(method=["PUT"],
156 156 function=check_int))
157 157 m.connect("delete_repos_group", "/repos_groups/{id}",
158 158 action="delete", conditions=dict(method=["DELETE"],
159 159 function=check_int))
160 160 m.connect("edit_repos_group", "/repos_groups/{id}/edit",
161 161 action="edit", conditions=dict(method=["GET"],
162 162 function=check_int))
163 163 m.connect("formatted_edit_repos_group",
164 164 "/repos_groups/{id}.{format}/edit",
165 165 action="edit", conditions=dict(method=["GET"],
166 166 function=check_int))
167 167 m.connect("repos_group", "/repos_groups/{id}",
168 168 action="show", conditions=dict(method=["GET"],
169 169 function=check_int))
170 170 m.connect("formatted_repos_group", "/repos_groups/{id}.{format}",
171 171 action="show", conditions=dict(method=["GET"],
172 172 function=check_int))
173 173 # ajax delete repos group perm user
174 174 m.connect('delete_repos_group_user_perm',
175 175 "/delete_repos_group_user_perm/{group_name:.*}",
176 176 action="delete_repos_group_user_perm",
177 177 conditions=dict(method=["DELETE"], function=check_group))
178 178
179 179 # ajax delete repos group perm users_group
180 180 m.connect('delete_repos_group_users_group_perm',
181 181 "/delete_repos_group_users_group_perm/{group_name:.*}",
182 182 action="delete_repos_group_users_group_perm",
183 183 conditions=dict(method=["DELETE"], function=check_group))
184 184
185 185 #ADMIN USER REST ROUTES
186 186 with rmap.submapper(path_prefix=ADMIN_PREFIX,
187 187 controller='admin/users') as m:
188 188 m.connect("users", "/users",
189 189 action="create", conditions=dict(method=["POST"]))
190 190 m.connect("users", "/users",
191 191 action="index", conditions=dict(method=["GET"]))
192 192 m.connect("formatted_users", "/users.{format}",
193 193 action="index", conditions=dict(method=["GET"]))
194 194 m.connect("new_user", "/users/new",
195 195 action="new", conditions=dict(method=["GET"]))
196 196 m.connect("formatted_new_user", "/users/new.{format}",
197 197 action="new", conditions=dict(method=["GET"]))
198 198 m.connect("update_user", "/users/{id}",
199 199 action="update", conditions=dict(method=["PUT"]))
200 200 m.connect("delete_user", "/users/{id}",
201 201 action="delete", conditions=dict(method=["DELETE"]))
202 202 m.connect("edit_user", "/users/{id}/edit",
203 203 action="edit", conditions=dict(method=["GET"]))
204 204 m.connect("formatted_edit_user",
205 205 "/users/{id}.{format}/edit",
206 206 action="edit", conditions=dict(method=["GET"]))
207 207 m.connect("user", "/users/{id}",
208 208 action="show", conditions=dict(method=["GET"]))
209 209 m.connect("formatted_user", "/users/{id}.{format}",
210 210 action="show", conditions=dict(method=["GET"]))
211 211
212 212 #EXTRAS USER ROUTES
213 213 m.connect("user_perm", "/users_perm/{id}",
214 214 action="update_perm", conditions=dict(method=["PUT"]))
215 215 m.connect("user_emails", "/users_emails/{id}",
216 216 action="add_email", conditions=dict(method=["PUT"]))
217 217 m.connect("user_emails_delete", "/users_emails/{id}",
218 218 action="delete_email", conditions=dict(method=["DELETE"]))
219 219
220 220 #ADMIN USERS GROUPS REST ROUTES
221 221 with rmap.submapper(path_prefix=ADMIN_PREFIX,
222 222 controller='admin/users_groups') as m:
223 223 m.connect("users_groups", "/users_groups",
224 224 action="create", conditions=dict(method=["POST"]))
225 225 m.connect("users_groups", "/users_groups",
226 226 action="index", conditions=dict(method=["GET"]))
227 227 m.connect("formatted_users_groups", "/users_groups.{format}",
228 228 action="index", conditions=dict(method=["GET"]))
229 229 m.connect("new_users_group", "/users_groups/new",
230 230 action="new", conditions=dict(method=["GET"]))
231 231 m.connect("formatted_new_users_group", "/users_groups/new.{format}",
232 232 action="new", conditions=dict(method=["GET"]))
233 233 m.connect("update_users_group", "/users_groups/{id}",
234 234 action="update", conditions=dict(method=["PUT"]))
235 235 m.connect("delete_users_group", "/users_groups/{id}",
236 236 action="delete", conditions=dict(method=["DELETE"]))
237 237 m.connect("edit_users_group", "/users_groups/{id}/edit",
238 238 action="edit", conditions=dict(method=["GET"]))
239 239 m.connect("formatted_edit_users_group",
240 240 "/users_groups/{id}.{format}/edit",
241 241 action="edit", conditions=dict(method=["GET"]))
242 242 m.connect("users_group", "/users_groups/{id}",
243 243 action="show", conditions=dict(method=["GET"]))
244 244 m.connect("formatted_users_group", "/users_groups/{id}.{format}",
245 245 action="show", conditions=dict(method=["GET"]))
246 246
247 247 #EXTRAS USER ROUTES
248 248 m.connect("users_group_perm", "/users_groups_perm/{id}",
249 249 action="update_perm", conditions=dict(method=["PUT"]))
250 250
251 251 #ADMIN GROUP REST ROUTES
252 252 rmap.resource('group', 'groups',
253 253 controller='admin/groups', path_prefix=ADMIN_PREFIX)
254 254
255 255 #ADMIN PERMISSIONS REST ROUTES
256 256 rmap.resource('permission', 'permissions',
257 257 controller='admin/permissions', path_prefix=ADMIN_PREFIX)
258 258
259 259 ##ADMIN LDAP SETTINGS
260 260 rmap.connect('ldap_settings', '%s/ldap' % ADMIN_PREFIX,
261 261 controller='admin/ldap_settings', action='ldap_settings',
262 262 conditions=dict(method=["POST"]))
263 263
264 264 rmap.connect('ldap_home', '%s/ldap' % ADMIN_PREFIX,
265 265 controller='admin/ldap_settings')
266 266
267 267 #ADMIN SETTINGS REST ROUTES
268 268 with rmap.submapper(path_prefix=ADMIN_PREFIX,
269 269 controller='admin/settings') as m:
270 270 m.connect("admin_settings", "/settings",
271 271 action="create", conditions=dict(method=["POST"]))
272 272 m.connect("admin_settings", "/settings",
273 273 action="index", conditions=dict(method=["GET"]))
274 274 m.connect("formatted_admin_settings", "/settings.{format}",
275 275 action="index", conditions=dict(method=["GET"]))
276 276 m.connect("admin_new_setting", "/settings/new",
277 277 action="new", conditions=dict(method=["GET"]))
278 278 m.connect("formatted_admin_new_setting", "/settings/new.{format}",
279 279 action="new", conditions=dict(method=["GET"]))
280 280 m.connect("/settings/{setting_id}",
281 281 action="update", conditions=dict(method=["PUT"]))
282 282 m.connect("/settings/{setting_id}",
283 283 action="delete", conditions=dict(method=["DELETE"]))
284 284 m.connect("admin_edit_setting", "/settings/{setting_id}/edit",
285 285 action="edit", conditions=dict(method=["GET"]))
286 286 m.connect("formatted_admin_edit_setting",
287 287 "/settings/{setting_id}.{format}/edit",
288 288 action="edit", conditions=dict(method=["GET"]))
289 289 m.connect("admin_setting", "/settings/{setting_id}",
290 290 action="show", conditions=dict(method=["GET"]))
291 291 m.connect("formatted_admin_setting", "/settings/{setting_id}.{format}",
292 292 action="show", conditions=dict(method=["GET"]))
293 293 m.connect("admin_settings_my_account", "/my_account",
294 294 action="my_account", conditions=dict(method=["GET"]))
295 295 m.connect("admin_settings_my_account_update", "/my_account_update",
296 296 action="my_account_update", conditions=dict(method=["PUT"]))
297 297 m.connect("admin_settings_create_repository", "/create_repository",
298 298 action="create_repository", conditions=dict(method=["GET"]))
299 299
300 300 #NOTIFICATION REST ROUTES
301 301 with rmap.submapper(path_prefix=ADMIN_PREFIX,
302 302 controller='admin/notifications') as m:
303 303 m.connect("notifications", "/notifications",
304 304 action="create", conditions=dict(method=["POST"]))
305 305 m.connect("notifications", "/notifications",
306 306 action="index", conditions=dict(method=["GET"]))
307 307 m.connect("notifications_mark_all_read", "/notifications/mark_all_read",
308 308 action="mark_all_read", conditions=dict(method=["GET"]))
309 309 m.connect("formatted_notifications", "/notifications.{format}",
310 310 action="index", conditions=dict(method=["GET"]))
311 311 m.connect("new_notification", "/notifications/new",
312 312 action="new", conditions=dict(method=["GET"]))
313 313 m.connect("formatted_new_notification", "/notifications/new.{format}",
314 314 action="new", conditions=dict(method=["GET"]))
315 315 m.connect("/notification/{notification_id}",
316 316 action="update", conditions=dict(method=["PUT"]))
317 317 m.connect("/notification/{notification_id}",
318 318 action="delete", conditions=dict(method=["DELETE"]))
319 319 m.connect("edit_notification", "/notification/{notification_id}/edit",
320 320 action="edit", conditions=dict(method=["GET"]))
321 321 m.connect("formatted_edit_notification",
322 322 "/notification/{notification_id}.{format}/edit",
323 323 action="edit", conditions=dict(method=["GET"]))
324 324 m.connect("notification", "/notification/{notification_id}",
325 325 action="show", conditions=dict(method=["GET"]))
326 326 m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
327 327 action="show", conditions=dict(method=["GET"]))
328 328
329 329 #ADMIN MAIN PAGES
330 330 with rmap.submapper(path_prefix=ADMIN_PREFIX,
331 331 controller='admin/admin') as m:
332 332 m.connect('admin_home', '', action='index')
333 333 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
334 334 action='add_repo')
335 335
336 336 #==========================================================================
337 337 # API V2
338 338 #==========================================================================
339 339 with rmap.submapper(path_prefix=ADMIN_PREFIX,
340 340 controller='api/api') as m:
341 341 m.connect('api', '/api')
342 342
343 343 #USER JOURNAL
344 344 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
345 345 controller='journal', action='index')
346 346 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
347 347 controller='journal', action='journal_rss')
348 348 rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
349 349 controller='journal', action='journal_atom')
350 350
351 351 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
352 352 controller='journal', action="public_journal")
353 353
354 354 rmap.connect('public_journal_rss', '%s/public_journal/rss' % ADMIN_PREFIX,
355 355 controller='journal', action="public_journal_rss")
356 356
357 357 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
358 358 controller='journal', action="public_journal_rss")
359 359
360 360 rmap.connect('public_journal_atom',
361 361 '%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
362 362 action="public_journal_atom")
363 363
364 364 rmap.connect('public_journal_atom_old',
365 365 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
366 366 action="public_journal_atom")
367 367
368 368 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
369 369 controller='journal', action='toggle_following',
370 370 conditions=dict(method=["POST"]))
371 371
372 372 #SEARCH
373 373 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
374 374 rmap.connect('search_repo', '%s/search/{search_repo:.*}' % ADMIN_PREFIX,
375 375 controller='search')
376 376
377 377 #LOGIN/LOGOUT/REGISTER/SIGN IN
378 378 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
379 379 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
380 380 action='logout')
381 381
382 382 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
383 383 action='register')
384 384
385 385 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
386 386 controller='login', action='password_reset')
387 387
388 388 rmap.connect('reset_password_confirmation',
389 389 '%s/password_reset_confirmation' % ADMIN_PREFIX,
390 390 controller='login', action='password_reset_confirmation')
391 391
392 392 #FEEDS
393 393 rmap.connect('rss_feed_home', '/{repo_name:.*}/feed/rss',
394 394 controller='feed', action='rss',
395 395 conditions=dict(function=check_repo))
396 396
397 397 rmap.connect('atom_feed_home', '/{repo_name:.*}/feed/atom',
398 398 controller='feed', action='atom',
399 399 conditions=dict(function=check_repo))
400 400
401 401 #==========================================================================
402 402 # REPOSITORY ROUTES
403 403 #==========================================================================
404 404 rmap.connect('summary_home', '/{repo_name:.*}',
405 405 controller='summary',
406 406 conditions=dict(function=check_repo))
407 407
408 408 rmap.connect('repos_group_home', '/{group_name:.*}',
409 409 controller='admin/repos_groups', action="show_by_name",
410 410 conditions=dict(function=check_group))
411 411
412 412 rmap.connect('changeset_home', '/{repo_name:.*}/changeset/{revision}',
413 413 controller='changeset', revision='tip',
414 414 conditions=dict(function=check_repo))
415 415
416 416 rmap.connect('changeset_comment',
417 417 '/{repo_name:.*}/changeset/{revision}/comment',
418 418 controller='changeset', revision='tip', action='comment',
419 419 conditions=dict(function=check_repo))
420 420
421 421 rmap.connect('changeset_comment_delete',
422 422 '/{repo_name:.*}/changeset/comment/{comment_id}/delete',
423 423 controller='changeset', action='delete_comment',
424 424 conditions=dict(function=check_repo, method=["DELETE"]))
425 425
426 426 rmap.connect('raw_changeset_home',
427 427 '/{repo_name:.*}/raw-changeset/{revision}',
428 428 controller='changeset', action='raw_changeset',
429 429 revision='tip', conditions=dict(function=check_repo))
430 430
431 431 rmap.connect('compare_url',
432 432 '/{repo_name:.*}/compare/{org_ref_type}@{org_ref}...{other_ref_type}@{other_ref}',
433 433 controller='compare', action='index',
434 434 conditions=dict(function=check_repo),
435 435 requirements=dict(org_ref_type='(branch|book|tag)',
436 436 other_ref_type='(branch|book|tag)'))
437 437
438 438 rmap.connect('pullrequest_home',
439 439 '/{repo_name:.*}/pull-request/new', controller='pullrequests',
440 440 action='index', conditions=dict(function=check_repo,
441 441 method=["GET"]))
442 442
443 443 rmap.connect('pullrequest',
444 444 '/{repo_name:.*}/pull-request/new', controller='pullrequests',
445 445 action='create', conditions=dict(function=check_repo,
446 446 method=["POST"]))
447 447
448 448 rmap.connect('pullrequest_show',
449 449 '/{repo_name:.*}/pull-request/{pull_request_id}',
450 450 controller='pullrequests',
451 451 action='show', conditions=dict(function=check_repo,
452 452 method=["GET"]))
453 453
454 454 rmap.connect('pullrequest_show_all',
455 455 '/{repo_name:.*}/pull-request',
456 456 controller='pullrequests',
457 457 action='show_all', conditions=dict(function=check_repo,
458 458 method=["GET"]))
459 459
460 rmap.connect('pullrequest_comment',
461 '/{repo_name:.*}/pull-request-comment/{pull_request_id}',
462 controller='pullrequests',
463 action='comment', conditions=dict(function=check_repo,
464 method=["POST"]))
465
460 466 rmap.connect('summary_home', '/{repo_name:.*}/summary',
461 467 controller='summary', conditions=dict(function=check_repo))
462 468
463 469 rmap.connect('shortlog_home', '/{repo_name:.*}/shortlog',
464 470 controller='shortlog', conditions=dict(function=check_repo))
465 471
466 472 rmap.connect('branches_home', '/{repo_name:.*}/branches',
467 473 controller='branches', conditions=dict(function=check_repo))
468 474
469 475 rmap.connect('tags_home', '/{repo_name:.*}/tags',
470 476 controller='tags', conditions=dict(function=check_repo))
471 477
472 478 rmap.connect('bookmarks_home', '/{repo_name:.*}/bookmarks',
473 479 controller='bookmarks', conditions=dict(function=check_repo))
474 480
475 481 rmap.connect('changelog_home', '/{repo_name:.*}/changelog',
476 482 controller='changelog', conditions=dict(function=check_repo))
477 483
478 484 rmap.connect('changelog_details', '/{repo_name:.*}/changelog_details/{cs}',
479 485 controller='changelog', action='changelog_details',
480 486 conditions=dict(function=check_repo))
481 487
482 488 rmap.connect('files_home', '/{repo_name:.*}/files/{revision}/{f_path:.*}',
483 489 controller='files', revision='tip', f_path='',
484 490 conditions=dict(function=check_repo))
485 491
486 492 rmap.connect('files_diff_home', '/{repo_name:.*}/diff/{f_path:.*}',
487 493 controller='files', action='diff', revision='tip', f_path='',
488 494 conditions=dict(function=check_repo))
489 495
490 496 rmap.connect('files_rawfile_home',
491 497 '/{repo_name:.*}/rawfile/{revision}/{f_path:.*}',
492 498 controller='files', action='rawfile', revision='tip',
493 499 f_path='', conditions=dict(function=check_repo))
494 500
495 501 rmap.connect('files_raw_home',
496 502 '/{repo_name:.*}/raw/{revision}/{f_path:.*}',
497 503 controller='files', action='raw', revision='tip', f_path='',
498 504 conditions=dict(function=check_repo))
499 505
500 506 rmap.connect('files_annotate_home',
501 507 '/{repo_name:.*}/annotate/{revision}/{f_path:.*}',
502 508 controller='files', action='index', revision='tip',
503 509 f_path='', annotate=True, conditions=dict(function=check_repo))
504 510
505 511 rmap.connect('files_edit_home',
506 512 '/{repo_name:.*}/edit/{revision}/{f_path:.*}',
507 513 controller='files', action='edit', revision='tip',
508 514 f_path='', conditions=dict(function=check_repo))
509 515
510 516 rmap.connect('files_add_home',
511 517 '/{repo_name:.*}/add/{revision}/{f_path:.*}',
512 518 controller='files', action='add', revision='tip',
513 519 f_path='', conditions=dict(function=check_repo))
514 520
515 521 rmap.connect('files_archive_home', '/{repo_name:.*}/archive/{fname}',
516 522 controller='files', action='archivefile',
517 523 conditions=dict(function=check_repo))
518 524
519 525 rmap.connect('files_nodelist_home',
520 526 '/{repo_name:.*}/nodelist/{revision}/{f_path:.*}',
521 527 controller='files', action='nodelist',
522 528 conditions=dict(function=check_repo))
523 529
524 530 rmap.connect('repo_settings_delete', '/{repo_name:.*}/settings',
525 531 controller='settings', action="delete",
526 532 conditions=dict(method=["DELETE"], function=check_repo))
527 533
528 534 rmap.connect('repo_settings_update', '/{repo_name:.*}/settings',
529 535 controller='settings', action="update",
530 536 conditions=dict(method=["PUT"], function=check_repo))
531 537
532 538 rmap.connect('repo_settings_home', '/{repo_name:.*}/settings',
533 539 controller='settings', action='index',
534 540 conditions=dict(function=check_repo))
535 541
536 542 rmap.connect('repo_fork_create_home', '/{repo_name:.*}/fork',
537 543 controller='forks', action='fork_create',
538 544 conditions=dict(function=check_repo, method=["POST"]))
539 545
540 546 rmap.connect('repo_fork_home', '/{repo_name:.*}/fork',
541 547 controller='forks', action='fork',
542 548 conditions=dict(function=check_repo))
543 549
544 550 rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
545 551 controller='forks', action='forks',
546 552 conditions=dict(function=check_repo))
547 553
548 554 rmap.connect('repo_followers_home', '/{repo_name:.*}/followers',
549 555 controller='followers', action='followers',
550 556 conditions=dict(function=check_repo))
551 557
552 558 return rmap
@@ -1,428 +1,428 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changeset
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changeset controller for pylons showoing changes beetween
7 7 revisions
8 8
9 9 :created_on: Apr 25, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28 from collections import defaultdict
29 29 from webob.exc import HTTPForbidden
30 30
31 31 from pylons import tmpl_context as c, url, request, response
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from pylons.decorators import jsonify
35 35
36 36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
37 37 ChangesetDoesNotExistError
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 42 from rhodecode.lib.base import BaseRepoController, render
43 43 from rhodecode.lib.utils import EmptyChangeset, action_logger
44 44 from rhodecode.lib.compat import OrderedDict
45 45 from rhodecode.lib import diffs
46 46 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 47 from rhodecode.model.comment import ChangesetCommentsModel
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.lib.diffs import wrapped_diff
51 51 from rhodecode.model.repo import RepoModel
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 def _update_with_GET(params, GET):
57 57 for k in ['diff1', 'diff2', 'diff']:
58 58 params[k] += GET.getall(k)
59 59
60 60
61 61 def anchor_url(revision, path, GET):
62 62 fid = h.FID(revision, path)
63 63 return h.url.current(anchor=fid, **dict(GET))
64 64
65 65
66 66 def get_ignore_ws(fid, GET):
67 67 ig_ws_global = GET.get('ignorews')
68 68 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
69 69 if ig_ws:
70 70 try:
71 71 return int(ig_ws[0].split(':')[-1])
72 72 except:
73 73 pass
74 74 return ig_ws_global
75 75
76 76
77 77 def _ignorews_url(GET, fileid=None):
78 78 fileid = str(fileid) if fileid else None
79 79 params = defaultdict(list)
80 80 _update_with_GET(params, GET)
81 81 lbl = _('show white space')
82 82 ig_ws = get_ignore_ws(fileid, GET)
83 83 ln_ctx = get_line_ctx(fileid, GET)
84 84 # global option
85 85 if fileid is None:
86 86 if ig_ws is None:
87 87 params['ignorews'] += [1]
88 88 lbl = _('ignore white space')
89 89 ctx_key = 'context'
90 90 ctx_val = ln_ctx
91 91 # per file options
92 92 else:
93 93 if ig_ws is None:
94 94 params[fileid] += ['WS:1']
95 95 lbl = _('ignore white space')
96 96
97 97 ctx_key = fileid
98 98 ctx_val = 'C:%s' % ln_ctx
99 99 # if we have passed in ln_ctx pass it along to our params
100 100 if ln_ctx:
101 101 params[ctx_key] += [ctx_val]
102 102
103 103 params['anchor'] = fileid
104 104 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
105 105 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
106 106
107 107
108 108 def get_line_ctx(fid, GET):
109 109 ln_ctx_global = GET.get('context')
110 110 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
111 111
112 112 if ln_ctx:
113 113 retval = ln_ctx[0].split(':')[-1]
114 114 else:
115 115 retval = ln_ctx_global
116 116
117 117 try:
118 118 return int(retval)
119 119 except:
120 120 return
121 121
122 122
123 123 def _context_url(GET, fileid=None):
124 124 """
125 125 Generates url for context lines
126 126
127 127 :param fileid:
128 128 """
129 129
130 130 fileid = str(fileid) if fileid else None
131 131 ig_ws = get_ignore_ws(fileid, GET)
132 132 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
133 133
134 134 params = defaultdict(list)
135 135 _update_with_GET(params, GET)
136 136
137 137 # global option
138 138 if fileid is None:
139 139 if ln_ctx > 0:
140 140 params['context'] += [ln_ctx]
141 141
142 142 if ig_ws:
143 143 ig_ws_key = 'ignorews'
144 144 ig_ws_val = 1
145 145
146 146 # per file option
147 147 else:
148 148 params[fileid] += ['C:%s' % ln_ctx]
149 149 ig_ws_key = fileid
150 150 ig_ws_val = 'WS:%s' % 1
151 151
152 152 if ig_ws:
153 153 params[ig_ws_key] += [ig_ws_val]
154 154
155 155 lbl = _('%s line context') % ln_ctx
156 156
157 157 params['anchor'] = fileid
158 158 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
159 159 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
160 160
161 161
162 162 class ChangesetController(BaseRepoController):
163 163
164 164 @LoginRequired()
165 165 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
166 166 'repository.admin')
167 167 def __before__(self):
168 168 super(ChangesetController, self).__before__()
169 169 c.affected_files_cut_off = 60
170 170 repo_model = RepoModel()
171 171 c.users_array = repo_model.get_users_js()
172 172 c.users_groups_array = repo_model.get_users_groups_js()
173 173
174 174 def index(self, revision):
175 175
176 176 c.anchor_url = anchor_url
177 177 c.ignorews_url = _ignorews_url
178 178 c.context_url = _context_url
179 179 limit_off = request.GET.get('fulldiff')
180 180 #get ranges of revisions if preset
181 181 rev_range = revision.split('...')[:2]
182 182 enable_comments = True
183 183 try:
184 184 if len(rev_range) == 2:
185 185 enable_comments = False
186 186 rev_start = rev_range[0]
187 187 rev_end = rev_range[1]
188 188 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
189 189 end=rev_end)
190 190 else:
191 191 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
192 192
193 193 c.cs_ranges = list(rev_ranges)
194 194 if not c.cs_ranges:
195 195 raise RepositoryError('Changeset range returned empty result')
196 196
197 197 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
198 198 log.error(traceback.format_exc())
199 199 h.flash(str(e), category='warning')
200 200 return redirect(url('home'))
201 201
202 202 c.changes = OrderedDict()
203 203
204 204 c.lines_added = 0 # count of lines added
205 205 c.lines_deleted = 0 # count of lines removes
206 206
207 207 cumulative_diff = 0
208 208 c.cut_off = False # defines if cut off limit is reached
209 209 c.changeset_statuses = ChangesetStatus.STATUSES
210 210 c.comments = []
211 211 c.statuses = []
212 212 c.inline_comments = []
213 213 c.inline_cnt = 0
214 214 # Iterate over ranges (default changeset view is always one changeset)
215 215 for changeset in c.cs_ranges:
216 216
217 217 c.statuses.extend([ChangesetStatusModel()\
218 218 .get_status(c.rhodecode_db_repo.repo_id,
219 219 changeset.raw_id)])
220 220
221 221 c.comments.extend(ChangesetCommentsModel()\
222 222 .get_comments(c.rhodecode_db_repo.repo_id,
223 223 revision=changeset.raw_id))
224 224 inlines = ChangesetCommentsModel()\
225 225 .get_inline_comments(c.rhodecode_db_repo.repo_id,
226 226 revision=changeset.raw_id)
227 227 c.inline_comments.extend(inlines)
228 228 c.changes[changeset.raw_id] = []
229 229 try:
230 230 changeset_parent = changeset.parents[0]
231 231 except IndexError:
232 232 changeset_parent = None
233 233
234 234 #==================================================================
235 235 # ADDED FILES
236 236 #==================================================================
237 237 for node in changeset.added:
238 238 fid = h.FID(revision, node.path)
239 239 line_context_lcl = get_line_ctx(fid, request.GET)
240 240 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
241 241 lim = self.cut_off_limit
242 242 if cumulative_diff > self.cut_off_limit:
243 243 lim = -1 if limit_off is None else None
244 244 size, cs1, cs2, diff, st = wrapped_diff(
245 245 filenode_old=None,
246 246 filenode_new=node,
247 247 cut_off_limit=lim,
248 248 ignore_whitespace=ign_whitespace_lcl,
249 249 line_context=line_context_lcl,
250 250 enable_comments=enable_comments
251 251 )
252 252 cumulative_diff += size
253 253 c.lines_added += st[0]
254 254 c.lines_deleted += st[1]
255 255 c.changes[changeset.raw_id].append(
256 256 ('added', node, diff, cs1, cs2, st)
257 257 )
258 258
259 259 #==================================================================
260 260 # CHANGED FILES
261 261 #==================================================================
262 262 for node in changeset.changed:
263 263 try:
264 264 filenode_old = changeset_parent.get_node(node.path)
265 265 except ChangesetError:
266 266 log.warning('Unable to fetch parent node for diff')
267 267 filenode_old = FileNode(node.path, '', EmptyChangeset())
268 268
269 269 fid = h.FID(revision, node.path)
270 270 line_context_lcl = get_line_ctx(fid, request.GET)
271 271 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
272 272 lim = self.cut_off_limit
273 273 if cumulative_diff > self.cut_off_limit:
274 274 lim = -1 if limit_off is None else None
275 275 size, cs1, cs2, diff, st = wrapped_diff(
276 276 filenode_old=filenode_old,
277 277 filenode_new=node,
278 278 cut_off_limit=lim,
279 279 ignore_whitespace=ign_whitespace_lcl,
280 280 line_context=line_context_lcl,
281 281 enable_comments=enable_comments
282 282 )
283 283 cumulative_diff += size
284 284 c.lines_added += st[0]
285 285 c.lines_deleted += st[1]
286 286 c.changes[changeset.raw_id].append(
287 287 ('changed', node, diff, cs1, cs2, st)
288 288 )
289 289 #==================================================================
290 290 # REMOVED FILES
291 291 #==================================================================
292 292 for node in changeset.removed:
293 293 c.changes[changeset.raw_id].append(
294 294 ('removed', node, None, None, None, (0, 0))
295 295 )
296 296
297 297 # count inline comments
298 298 for __, lines in c.inline_comments:
299 299 for comments in lines.values():
300 300 c.inline_cnt += len(comments)
301 301
302 302 if len(c.cs_ranges) == 1:
303 303 c.changeset = c.cs_ranges[0]
304 304 c.changes = c.changes[c.changeset.raw_id]
305 305
306 306 return render('changeset/changeset.html')
307 307 else:
308 308 return render('changeset/changeset_range.html')
309 309
310 310 def raw_changeset(self, revision):
311 311
312 312 method = request.GET.get('diff', 'show')
313 313 ignore_whitespace = request.GET.get('ignorews') == '1'
314 314 line_context = request.GET.get('context', 3)
315 315 try:
316 316 c.scm_type = c.rhodecode_repo.alias
317 317 c.changeset = c.rhodecode_repo.get_changeset(revision)
318 318 except RepositoryError:
319 319 log.error(traceback.format_exc())
320 320 return redirect(url('home'))
321 321 else:
322 322 try:
323 323 c.changeset_parent = c.changeset.parents[0]
324 324 except IndexError:
325 325 c.changeset_parent = None
326 326 c.changes = []
327 327
328 328 for node in c.changeset.added:
329 329 filenode_old = FileNode(node.path, '')
330 330 if filenode_old.is_binary or node.is_binary:
331 331 diff = _('binary file') + '\n'
332 332 else:
333 333 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
334 334 ignore_whitespace=ignore_whitespace,
335 335 context=line_context)
336 336 diff = diffs.DiffProcessor(f_gitdiff,
337 337 format='gitdiff').raw_diff()
338 338
339 339 cs1 = None
340 340 cs2 = node.changeset.raw_id
341 341 c.changes.append(('added', node, diff, cs1, cs2))
342 342
343 343 for node in c.changeset.changed:
344 344 filenode_old = c.changeset_parent.get_node(node.path)
345 345 if filenode_old.is_binary or node.is_binary:
346 346 diff = _('binary file')
347 347 else:
348 348 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
349 349 ignore_whitespace=ignore_whitespace,
350 350 context=line_context)
351 351 diff = diffs.DiffProcessor(f_gitdiff,
352 352 format='gitdiff').raw_diff()
353 353
354 354 cs1 = filenode_old.changeset.raw_id
355 355 cs2 = node.changeset.raw_id
356 356 c.changes.append(('changed', node, diff, cs1, cs2))
357 357
358 358 response.content_type = 'text/plain'
359 359
360 360 if method == 'download':
361 361 response.content_disposition = 'attachment; filename=%s.patch' \
362 362 % revision
363 363
364 364 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
365 365 for x in c.changeset.parents])
366 366
367 367 c.diffs = ''
368 368 for x in c.changes:
369 369 c.diffs += x[2]
370 370
371 371 return render('changeset/raw_changeset.html')
372 372
373 373 @jsonify
374 374 def comment(self, repo_name, revision):
375 375 status = request.POST.get('changeset_status')
376 376 change_status = request.POST.get('change_changeset_status')
377 377
378 378 comm = ChangesetCommentsModel().create(
379 379 text=request.POST.get('text'),
380 380 repo_id=c.rhodecode_db_repo.repo_id,
381 381 user_id=c.rhodecode_user.user_id,
382 382 revision=revision,
383 383 f_path=request.POST.get('f_path'),
384 384 line_no=request.POST.get('line'),
385 385 status_change=(ChangesetStatus.get_status_lbl(status)
386 386 if status and change_status else None)
387 387 )
388 388
389 389 # get status if set !
390 390 if status and change_status:
391 391 ChangesetStatusModel().set_status(
392 392 c.rhodecode_db_repo.repo_id,
393 revision,
394 393 status,
395 394 c.rhodecode_user.user_id,
396 395 comm,
396 revision=revision,
397 397 )
398 398 action_logger(self.rhodecode_user,
399 399 'user_commented_revision:%s' % revision,
400 400 c.rhodecode_db_repo, self.ip_addr, self.sa)
401 401
402 402 Session.commit()
403 403
404 404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 405 return redirect(h.url('changeset_home', repo_name=repo_name,
406 406 revision=revision))
407 407
408 408 data = {
409 409 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
410 410 }
411 411 if comm:
412 412 c.co = comm
413 413 data.update(comm.get_dict())
414 414 data.update({'rendered_text':
415 415 render('changeset/changeset_comment_block.html')})
416 416
417 417 return data
418 418
419 419 @jsonify
420 420 def delete_comment(self, repo_name, comment_id):
421 421 co = ChangesetComment.get(comment_id)
422 422 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
423 423 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
424 424 ChangesetCommentsModel().delete(comment=co)
425 425 Session.commit()
426 426 return True
427 427 else:
428 428 raise HTTPForbidden()
@@ -1,216 +1,264 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.pullrequests
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull requests controller for rhodecode for initializing pull requests
7 7
8 8 :created_on: May 7, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import traceback
27 import binascii
28 27
29 28 from webob.exc import HTTPNotFound
30 29
31 30 from pylons import request, response, session, tmpl_context as c, url
32 31 from pylons.controllers.util import abort, redirect
33 32 from pylons.i18n.translation import _
33 from pylons.decorators import jsonify
34 34
35 35 from rhodecode.lib.base import BaseRepoController, render
36 36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import diffs
39 from rhodecode.model.db import User, PullRequest, Repository, ChangesetStatus
39 from rhodecode.lib.utils import action_logger
40 from rhodecode.model.db import User, PullRequest, ChangesetStatus
40 41 from rhodecode.model.pull_request import PullRequestModel
41 42 from rhodecode.model.meta import Session
42 43 from rhodecode.model.repo import RepoModel
43 44 from rhodecode.model.comment import ChangesetCommentsModel
44 45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 46
46 47 log = logging.getLogger(__name__)
47 48
48 49
49 50 class PullrequestsController(BaseRepoController):
50 51
51 52 @LoginRequired()
52 53 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
53 54 'repository.admin')
54 55 def __before__(self):
55 56 super(PullrequestsController, self).__before__()
56 57
57 58 def _get_repo_refs(self, repo):
58 59 hist_l = []
59 60
60 61 branches_group = ([('branch:%s:%s' % (k, v), k) for
61 62 k, v in repo.branches.iteritems()], _("Branches"))
62 63 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
63 64 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
64 65 tags_group = ([('tag:%s:%s' % (k, v), k) for
65 66 k, v in repo.tags.iteritems()], _("Tags"))
66 67
67 68 hist_l.append(bookmarks_group)
68 69 hist_l.append(branches_group)
69 70 hist_l.append(tags_group)
70 71
71 72 return hist_l
72 73
73 74 def show_all(self, repo_name):
74 75 c.pull_requests = PullRequestModel().get_all(repo_name)
75 76 c.repo_name = repo_name
76 77 return render('/pullrequests/pullrequest_show_all.html')
77 78
78 79 def index(self):
79 80 org_repo = c.rhodecode_db_repo
80 81 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
81 82 c.org_repos = []
82 83 c.other_repos = []
83 84 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
84 85 org_repo.user.username, c.repo_name))
85 86 )
86 87
87 88 c.other_refs = c.org_refs
88 89 c.other_repos.extend(c.org_repos)
89 90 c.default_pull_request = org_repo.repo_name
90 91 #gather forks and add to this list
91 92 for fork in org_repo.forks:
92 93 c.other_repos.append((fork.repo_name, '%s/%s' % (
93 94 fork.user.username, fork.repo_name))
94 95 )
95 96 #add parents of this fork also
96 97 if org_repo.parent:
97 98 c.default_pull_request = org_repo.parent.repo_name
98 99 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
99 100 org_repo.parent.user.username,
100 101 org_repo.parent.repo_name))
101 102 )
102 103
103 104 #TODO: maybe the owner should be default ?
104 105 c.review_members = []
105 106 c.available_members = []
106 107 for u in User.query().filter(User.username != 'default').all():
107 108 uname = u.username
108 109 if org_repo.user == u:
109 110 uname = _('%s (owner)' % u.username)
110 111 # auto add owner to pull-request recipients
111 112 c.review_members.append([u.user_id, uname])
112 113 c.available_members.append([u.user_id, uname])
113 114 return render('/pullrequests/pullrequest.html')
114 115
115 116 def create(self, repo_name):
116 117 req_p = request.POST
117 118 org_repo = req_p['org_repo']
118 119 org_ref = req_p['org_ref']
119 120 other_repo = req_p['other_repo']
120 121 other_ref = req_p['other_ref']
121 122 revisions = req_p.getall('revisions')
122 123 reviewers = req_p.getall('review_members')
123 124 #TODO: wrap this into a FORM !!!
124 125
125 126 title = req_p['pullrequest_title']
126 127 description = req_p['pullrequest_desc']
127 128
128 129 try:
129 130 model = PullRequestModel()
130 131 model.create(self.rhodecode_user.user_id, org_repo,
131 132 org_ref, other_repo, other_ref, revisions,
132 133 reviewers, title, description)
133 134 Session.commit()
134 135 h.flash(_('Pull request send'), category='success')
135 136 except Exception:
136 137 raise
137 138 h.flash(_('Error occured during sending pull request'),
138 139 category='error')
139 140 log.error(traceback.format_exc())
140 141
141 142 return redirect(url('changelog_home', repo_name=repo_name))
142 143
143 144 def _load_compare_data(self, pull_request):
144 145 """
145 146 Load context data needed for generating compare diff
146 147
147 148 :param pull_request:
148 149 :type pull_request:
149 150 """
150 151
151 152 org_repo = pull_request.org_repo
152 153 org_ref_type, org_ref_, org_ref = pull_request.org_ref.split(':')
153 154 other_repo = pull_request.other_repo
154 155 other_ref_type, other_ref, other_ref_ = pull_request.other_ref.split(':')
155 156
156 157 org_ref = (org_ref_type, org_ref)
157 158 other_ref = (other_ref_type, other_ref)
158 159
159 160 c.org_repo = org_repo
160 161 c.other_repo = other_repo
161 162
162 163 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
163 164 org_repo, org_ref, other_repo, other_ref
164 165 )
165 166
166 167 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
167 168 c.cs_ranges])
168 169 # defines that we need hidden inputs with changesets
169 170 c.as_form = request.GET.get('as_form', False)
170 171
171 172 c.org_ref = org_ref[1]
172 173 c.other_ref = other_ref[1]
173 174 # diff needs to have swapped org with other to generate proper diff
174 175 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
175 176 discovery_data)
176 177 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
177 178 _parsed = diff_processor.prepare()
178 179
179 180 c.files = []
180 181 c.changes = {}
181 182
182 183 for f in _parsed:
183 184 fid = h.FID('', f['filename'])
184 185 c.files.append([fid, f['operation'], f['filename'], f['stats']])
185 186 diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
186 187 c.changes[fid] = [f['operation'], f['filename'], diff]
187 188
188 189 def show(self, repo_name, pull_request_id):
189 190 repo_model = RepoModel()
190 191 c.users_array = repo_model.get_users_js()
191 192 c.users_groups_array = repo_model.get_users_groups_js()
192 193 c.pull_request = PullRequest.get(pull_request_id)
193 194
194 195 # load compare data into template context
195 196 self._load_compare_data(c.pull_request)
196 197
197 198 # inline comments
198 199 c.inline_cnt = 0
199 200 c.inline_comments = ChangesetCommentsModel()\
200 201 .get_inline_comments(c.rhodecode_db_repo.repo_id,
201 202 pull_request=pull_request_id)
202 203 # count inline comments
203 204 for __, lines in c.inline_comments:
204 205 for comments in lines.values():
205 206 c.inline_cnt += len(comments)
206 207 # comments
207 208 c.comments = ChangesetCommentsModel()\
208 209 .get_comments(c.rhodecode_db_repo.repo_id,
209 210 pull_request=pull_request_id)
210 211
211 # changeset(pull-request) statuse
212 # changeset(pull-request) status
212 213 c.current_changeset_status = ChangesetStatusModel()\
213 .get_status(c.rhodecode_db_repo.repo_id,
214 pull_request=pull_request_id)
214 .get_status(c.pull_request.org_repo,
215 pull_request=c.pull_request)
215 216 c.changeset_statuses = ChangesetStatus.STATUSES
216 217 return render('/pullrequests/pullrequest_show.html')
218
219 @jsonify
220 def comment(self, repo_name, pull_request_id):
221
222 status = request.POST.get('changeset_status')
223 change_status = request.POST.get('change_changeset_status')
224
225 comm = ChangesetCommentsModel().create(
226 text=request.POST.get('text'),
227 repo_id=c.rhodecode_db_repo.repo_id,
228 user_id=c.rhodecode_user.user_id,
229 pull_request=pull_request_id,
230 f_path=request.POST.get('f_path'),
231 line_no=request.POST.get('line'),
232 status_change=(ChangesetStatus.get_status_lbl(status)
233 if status and change_status else None)
234 )
235
236 # get status if set !
237 if status and change_status:
238 ChangesetStatusModel().set_status(
239 c.rhodecode_db_repo.repo_id,
240 status,
241 c.rhodecode_user.user_id,
242 comm,
243 pull_request=pull_request_id
244 )
245 action_logger(self.rhodecode_user,
246 'user_commented_pull_request:%s' % pull_request_id,
247 c.rhodecode_db_repo, self.ip_addr, self.sa)
248
249 Session.commit()
250
251 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
252 return redirect(h.url('pullrequest_show', repo_name=repo_name,
253 pull_request_id=pull_request_id))
254
255 data = {
256 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
257 }
258 if comm:
259 c.co = comm
260 data.update(comm.get_dict())
261 data.update({'rendered_text':
262 render('changeset/changeset_comment_block.html')})
263
264 return data No newline at end of file
@@ -1,108 +1,139 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.changeset_status
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6
7 7 :created_on: Apr 30, 2012
8 8 :author: marcink
9 9 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 10 :license: GPLv3, see COPYING for more details.
11 11 """
12 12 # This program is free software: you can redistribute it and/or modify
13 13 # it under the terms of the GNU General Public License as published by
14 14 # the Free Software Foundation, either version 3 of the License, or
15 15 # (at your option) any later version.
16 16 #
17 17 # This program is distributed in the hope that it will be useful,
18 18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 20 # GNU General Public License for more details.
21 21 #
22 22 # You should have received a copy of the GNU General Public License
23 23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 24
25 25
26 26 import logging
27 27
28 28 from rhodecode.model import BaseModel
29 29 from rhodecode.model.db import ChangesetStatus, PullRequest
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class ChangesetStatusModel(BaseModel):
35 35
36 36 def __get_changeset_status(self, changeset_status):
37 37 return self._get_instance(ChangesetStatus, changeset_status)
38 38
39 39 def __get_pull_request(self, pull_request):
40 40 return self._get_instance(PullRequest, pull_request)
41 41
42 42 def get_status(self, repo, revision=None, pull_request=None):
43 43 """
44 44 Returns latest status of changeset for given revision or for given
45 45 pull request. Statuses are versioned inside a table itself and
46 46 version == 0 is always the current one
47 47
48 48 :param repo:
49 49 :type repo:
50 50 :param revision: 40char hash or None
51 51 :type revision: str
52 52 :param pull_request: pull_request reference
53 53 :type:
54 54 """
55 55 repo = self._get_repo(repo)
56 56
57 57 q = ChangesetStatus.query()\
58 58 .filter(ChangesetStatus.repo == repo)\
59 59 .filter(ChangesetStatus.version == 0)
60 60
61 61 if revision:
62 62 q = q.filter(ChangesetStatus.revision == revision)
63 63 elif pull_request:
64 64 pull_request = self.__get_pull_request(pull_request)
65 65 q = q.filter(ChangesetStatus.pull_request == pull_request)
66 66 else:
67 67 raise Exception('Please specify revision or pull_request')
68 68
69 status = q.scalar()
69 # need to use first here since there can be multiple statuses
70 # returned from pull_request
71 status = q.first()
70 72 status = status.status if status else status
71 73 st = status or ChangesetStatus.DEFAULT
72 74 return str(st)
73 75
74 def set_status(self, repo, revision, status, user, comment):
76 def set_status(self, repo, status, user, comment, revision=None,
77 pull_request=None):
75 78 """
76 79 Creates new status for changeset or updates the old ones bumping their
77 80 version, leaving the current status at
78 81
79 82 :param repo:
80 83 :type repo:
81 84 :param revision:
82 85 :type revision:
83 86 :param status:
84 87 :type status:
85 88 :param user:
86 89 :type user:
87 90 :param comment:
88 91 :type comment:
89 92 """
90 93 repo = self._get_repo(repo)
91 94
92 cur_statuses = ChangesetStatus.query()\
93 .filter(ChangesetStatus.repo == repo)\
94 .filter(ChangesetStatus.revision == revision)\
95 .all()
95 q = ChangesetStatus.query()
96
97 if revision:
98 q = q.filter(ChangesetStatus.repo == repo)
99 q = q.filter(ChangesetStatus.revision == revision)
100 elif pull_request:
101 pull_request = self.__get_pull_request(pull_request)
102 q = q.filter(ChangesetStatus.repo == pull_request.org_repo)
103 q = q.filter(ChangesetStatus.pull_request == pull_request)
104 cur_statuses = q.all()
105
96 106 if cur_statuses:
97 107 for st in cur_statuses:
98 108 st.version += 1
99 109 self.sa.add(st)
100 new_status = ChangesetStatus()
101 new_status.author = self._get_user(user)
102 new_status.repo = self._get_repo(repo)
103 new_status.status = status
104 new_status.revision = revision
105 new_status.comment = comment
106 self.sa.add(new_status)
107 return new_status
110
111 def _create_status(user, repo, status, comment, revision, pull_request):
112 new_status = ChangesetStatus()
113 new_status.author = self._get_user(user)
114 new_status.repo = self._get_repo(repo)
115 new_status.status = status
116 new_status.comment = comment
117 new_status.revision = revision
118 new_status.pull_request = pull_request
119 return new_status
108 120
121 if revision:
122 new_status = _create_status(user=user, repo=repo, status=status,
123 comment=comment, revision=revision,
124 pull_request=None)
125 self.sa.add(new_status)
126 return new_status
127 elif pull_request:
128 #pull request can have more than one revision associated to it
129 #we need to create new version for each one
130 new_statuses = []
131 repo = pull_request.org_repo
132 for rev in pull_request.revisions:
133 new_status = _create_status(user=user, repo=repo,
134 status=status, comment=comment,
135 revision=rev,
136 pull_request=pull_request)
137 new_statuses.append(new_status)
138 self.sa.add(new_status)
139 return new_statuses
@@ -1,188 +1,211 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.comment
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 comments model for RhodeCode
7 7
8 8 :created_on: Nov 11, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons.i18n.translation import _
30 30 from sqlalchemy.util.compat import defaultdict
31 31
32 32 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import ChangesetComment, User, Repository, \
36 36 Notification, PullRequest
37 37 from rhodecode.model.notification import NotificationModel
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class ChangesetCommentsModel(BaseModel):
43 43
44 44 def __get_changeset_comment(self, changeset_comment):
45 45 return self._get_instance(ChangesetComment, changeset_comment)
46 46
47 47 def __get_pull_request(self, pull_request):
48 48 return self._get_instance(PullRequest, pull_request)
49 49
50 50 def _extract_mentions(self, s):
51 51 user_objects = []
52 52 for username in extract_mentioned_users(s):
53 53 user_obj = User.get_by_username(username, case_insensitive=True)
54 54 if user_obj:
55 55 user_objects.append(user_obj)
56 56 return user_objects
57 57
58 def create(self, text, repo_id, user_id, revision, f_path=None,
59 line_no=None, status_change=None):
58 def create(self, text, repo_id, user_id, revision=None, pull_request=None,
59 f_path=None, line_no=None, status_change=None):
60 60 """
61 Creates new comment for changeset. IF status_change is not none
62 this comment is associated with a status change of changeset
61 Creates new comment for changeset or pull request.
62 IF status_change is not none this comment is associated with a
63 status change of changeset or changesets associated with pull request
63 64
64 65 :param text:
65 66 :param repo_id:
66 67 :param user_id:
67 68 :param revision:
69 :param pull_request:
68 70 :param f_path:
69 71 :param line_no:
70 72 :param status_change:
71 73 """
74 if not text:
75 return
72 76
73 if text:
74 repo = Repository.get(repo_id)
77 repo = Repository.get(repo_id)
78 comment = ChangesetComment()
79 comment.repo = repo
80 comment.user_id = user_id
81 comment.text = text
82 comment.f_path = f_path
83 comment.line_no = line_no
84
85 if revision:
75 86 cs = repo.scm_instance.get_changeset(revision)
76 87 desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
77 88 author_email = cs.author_email
78 comment = ChangesetComment()
79 comment.repo = repo
80 comment.user_id = user_id
81 89 comment.revision = revision
82 comment.text = text
83 comment.f_path = f_path
84 comment.line_no = line_no
90 elif pull_request:
91 pull_request = self.__get_pull_request(pull_request)
92 comment.pull_request = pull_request
93 desc = ''
94 else:
95 raise Exception('Please specify revision or pull_request_id')
85 96
86 self.sa.add(comment)
87 self.sa.flush()
88 # make notification
89 line = ''
97 self.sa.add(comment)
98 self.sa.flush()
99
100 # make notification
101 line = ''
102 body = text
103
104 #changeset
105 if revision:
90 106 if line_no:
91 107 line = _('on line %s') % line_no
92 108 subj = safe_unicode(
93 109 h.link_to('Re commit: %(commit_desc)s %(line)s' % \
94 110 {'commit_desc': desc, 'line': line},
95 111 h.url('changeset_home', repo_name=repo.repo_name,
96 112 revision=revision,
97 113 anchor='comment-%s' % comment.comment_id,
98 114 qualified=True,
99 115 )
100 116 )
101 117 )
102
103 body = text
104
118 notification_type = Notification.TYPE_CHANGESET_COMMENT
105 119 # get the current participants of this changeset
106 120 recipients = ChangesetComment.get_users(revision=revision)
107
108 121 # add changeset author if it's in rhodecode system
109 122 recipients += [User.get_by_email(author_email)]
123 #pull request
124 elif pull_request:
125 #TODO: make this something usefull
126 subj = 'commented on pull request something...'
127 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
128 # get the current participants of this pull request
129 recipients = ChangesetComment.get_users(pull_request_id=
130 pull_request.pull_request_id)
131 # add pull request author
132 recipients += [pull_request.author]
110 133
111 # create notification objects, and emails
134 # create notification objects, and emails
135 NotificationModel().create(
136 created_by=user_id, subject=subj, body=body,
137 recipients=recipients, type_=notification_type,
138 email_kwargs={'status_change': status_change}
139 )
140
141 mention_recipients = set(self._extract_mentions(body))\
142 .difference(recipients)
143 if mention_recipients:
144 subj = _('[Mention]') + ' ' + subj
112 145 NotificationModel().create(
113 created_by=user_id, subject=subj, body=body,
114 recipients=recipients, type_=Notification.TYPE_CHANGESET_COMMENT,
115 email_kwargs={'status_change': status_change}
146 created_by=user_id, subject=subj, body=body,
147 recipients=mention_recipients,
148 type_=notification_type,
149 email_kwargs={'status_change': status_change}
116 150 )
117 151
118 mention_recipients = set(self._extract_mentions(body))\
119 .difference(recipients)
120 if mention_recipients:
121 subj = _('[Mention]') + ' ' + subj
122 NotificationModel().create(
123 created_by=user_id, subject=subj, body=body,
124 recipients=mention_recipients,
125 type_=Notification.TYPE_CHANGESET_COMMENT,
126 email_kwargs={'status_change': status_change}
127 )
128
129 return comment
152 return comment
130 153
131 154 def delete(self, comment):
132 155 """
133 156 Deletes given comment
134 157
135 158 :param comment_id:
136 159 """
137 160 comment = self.__get_changeset_comment(comment)
138 161 self.sa.delete(comment)
139 162
140 163 return comment
141 164
142 165 def get_comments(self, repo_id, revision=None, pull_request=None):
143 166 """
144 167 Get's main comments based on revision or pull_request_id
145 168
146 169 :param repo_id:
147 170 :type repo_id:
148 171 :param revision:
149 172 :type revision:
150 173 :param pull_request:
151 174 :type pull_request:
152 175 """
153 176
154 177 q = ChangesetComment.query()\
155 178 .filter(ChangesetComment.repo_id == repo_id)\
156 179 .filter(ChangesetComment.line_no == None)\
157 180 .filter(ChangesetComment.f_path == None)
158 181 if revision:
159 182 q = q.filter(ChangesetComment.revision == revision)
160 183 elif pull_request:
161 184 pull_request = self.__get_pull_request(pull_request)
162 185 q = q.filter(ChangesetComment.pull_request == pull_request)
163 186 else:
164 187 raise Exception('Please specify revision or pull_request')
165 188 return q.all()
166 189
167 190 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
168 191 q = self.sa.query(ChangesetComment)\
169 192 .filter(ChangesetComment.repo_id == repo_id)\
170 193 .filter(ChangesetComment.line_no != None)\
171 194 .filter(ChangesetComment.f_path != None)\
172 195 .order_by(ChangesetComment.comment_id.asc())\
173 196
174 197 if revision:
175 198 q = q.filter(ChangesetComment.revision == revision)
176 199 elif pull_request:
177 200 pull_request = self.__get_pull_request(pull_request)
178 201 q = q.filter(ChangesetComment.pull_request == pull_request)
179 202 else:
180 203 raise Exception('Please specify revision or pull_request_id')
181 204
182 205 comments = q.all()
183 206
184 207 paths = defaultdict(lambda: defaultdict(list))
185 208
186 209 for co in comments:
187 210 paths[co.f_path][co.line_no].append(co)
188 211 return paths.items()
@@ -1,1532 +1,1542 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import hashlib
31 31 from collections import defaultdict
32 32
33 33 from sqlalchemy import *
34 34 from sqlalchemy.ext.hybrid import hybrid_property
35 35 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
36 36 from sqlalchemy.exc import DatabaseError
37 37 from beaker.cache import cache_region, region_invalidate
38 38
39 39 from pylons.i18n.translation import lazy_ugettext as _
40 40
41 41 from rhodecode.lib.vcs import get_backend
42 42 from rhodecode.lib.vcs.utils.helpers import get_scm
43 43 from rhodecode.lib.vcs.exceptions import VCSError
44 44 from rhodecode.lib.vcs.utils.lazy import LazyProperty
45 45
46 46 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
47 47 safe_unicode
48 48 from rhodecode.lib.compat import json
49 49 from rhodecode.lib.caching_query import FromCache
50 50
51 51 from rhodecode.model.meta import Base, Session
52 52
53 53
54 54 URL_SEP = '/'
55 55 log = logging.getLogger(__name__)
56 56
57 57 #==============================================================================
58 58 # BASE CLASSES
59 59 #==============================================================================
60 60
61 61 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
62 62
63 63
64 64 class ModelSerializer(json.JSONEncoder):
65 65 """
66 66 Simple Serializer for JSON,
67 67
68 68 usage::
69 69
70 70 to make object customized for serialization implement a __json__
71 71 method that will return a dict for serialization into json
72 72
73 73 example::
74 74
75 75 class Task(object):
76 76
77 77 def __init__(self, name, value):
78 78 self.name = name
79 79 self.value = value
80 80
81 81 def __json__(self):
82 82 return dict(name=self.name,
83 83 value=self.value)
84 84
85 85 """
86 86
87 87 def default(self, obj):
88 88
89 89 if hasattr(obj, '__json__'):
90 90 return obj.__json__()
91 91 else:
92 92 return json.JSONEncoder.default(self, obj)
93 93
94 94
95 95 class BaseModel(object):
96 96 """
97 97 Base Model for all classess
98 98 """
99 99
100 100 @classmethod
101 101 def _get_keys(cls):
102 102 """return column names for this model """
103 103 return class_mapper(cls).c.keys()
104 104
105 105 def get_dict(self):
106 106 """
107 107 return dict with keys and values corresponding
108 108 to this model data """
109 109
110 110 d = {}
111 111 for k in self._get_keys():
112 112 d[k] = getattr(self, k)
113 113
114 114 # also use __json__() if present to get additional fields
115 115 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
116 116 d[k] = val
117 117 return d
118 118
119 119 def get_appstruct(self):
120 120 """return list with keys and values tupples corresponding
121 121 to this model data """
122 122
123 123 l = []
124 124 for k in self._get_keys():
125 125 l.append((k, getattr(self, k),))
126 126 return l
127 127
128 128 def populate_obj(self, populate_dict):
129 129 """populate model with data from given populate_dict"""
130 130
131 131 for k in self._get_keys():
132 132 if k in populate_dict:
133 133 setattr(self, k, populate_dict[k])
134 134
135 135 @classmethod
136 136 def query(cls):
137 137 return Session.query(cls)
138 138
139 139 @classmethod
140 140 def get(cls, id_):
141 141 if id_:
142 142 return cls.query().get(id_)
143 143
144 144 @classmethod
145 145 def getAll(cls):
146 146 return cls.query().all()
147 147
148 148 @classmethod
149 149 def delete(cls, id_):
150 150 obj = cls.query().get(id_)
151 151 Session.delete(obj)
152 152
153 153 def __repr__(self):
154 154 if hasattr(self, '__unicode__'):
155 155 # python repr needs to return str
156 156 return safe_str(self.__unicode__())
157 157 return '<DB:%s>' % (self.__class__.__name__)
158 158
159 159
160 160 class RhodeCodeSetting(Base, BaseModel):
161 161 __tablename__ = 'rhodecode_settings'
162 162 __table_args__ = (
163 163 UniqueConstraint('app_settings_name'),
164 164 {'extend_existing': True, 'mysql_engine': 'InnoDB',
165 165 'mysql_charset': 'utf8'}
166 166 )
167 167 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
168 168 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
169 169 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
170 170
171 171 def __init__(self, k='', v=''):
172 172 self.app_settings_name = k
173 173 self.app_settings_value = v
174 174
175 175 @validates('_app_settings_value')
176 176 def validate_settings_value(self, key, val):
177 177 assert type(val) == unicode
178 178 return val
179 179
180 180 @hybrid_property
181 181 def app_settings_value(self):
182 182 v = self._app_settings_value
183 183 if self.app_settings_name == 'ldap_active':
184 184 v = str2bool(v)
185 185 return v
186 186
187 187 @app_settings_value.setter
188 188 def app_settings_value(self, val):
189 189 """
190 190 Setter that will always make sure we use unicode in app_settings_value
191 191
192 192 :param val:
193 193 """
194 194 self._app_settings_value = safe_unicode(val)
195 195
196 196 def __unicode__(self):
197 197 return u"<%s('%s:%s')>" % (
198 198 self.__class__.__name__,
199 199 self.app_settings_name, self.app_settings_value
200 200 )
201 201
202 202 @classmethod
203 203 def get_by_name(cls, ldap_key):
204 204 return cls.query()\
205 205 .filter(cls.app_settings_name == ldap_key).scalar()
206 206
207 207 @classmethod
208 208 def get_app_settings(cls, cache=False):
209 209
210 210 ret = cls.query()
211 211
212 212 if cache:
213 213 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
214 214
215 215 if not ret:
216 216 raise Exception('Could not get application settings !')
217 217 settings = {}
218 218 for each in ret:
219 219 settings['rhodecode_' + each.app_settings_name] = \
220 220 each.app_settings_value
221 221
222 222 return settings
223 223
224 224 @classmethod
225 225 def get_ldap_settings(cls, cache=False):
226 226 ret = cls.query()\
227 227 .filter(cls.app_settings_name.startswith('ldap_')).all()
228 228 fd = {}
229 229 for row in ret:
230 230 fd.update({row.app_settings_name: row.app_settings_value})
231 231
232 232 return fd
233 233
234 234
235 235 class RhodeCodeUi(Base, BaseModel):
236 236 __tablename__ = 'rhodecode_ui'
237 237 __table_args__ = (
238 238 UniqueConstraint('ui_key'),
239 239 {'extend_existing': True, 'mysql_engine': 'InnoDB',
240 240 'mysql_charset': 'utf8'}
241 241 )
242 242
243 243 HOOK_UPDATE = 'changegroup.update'
244 244 HOOK_REPO_SIZE = 'changegroup.repo_size'
245 245 HOOK_PUSH = 'changegroup.push_logger'
246 246 HOOK_PULL = 'preoutgoing.pull_logger'
247 247
248 248 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
249 249 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
250 250 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
251 251 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
252 252 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
253 253
254 254 @classmethod
255 255 def get_by_key(cls, key):
256 256 return cls.query().filter(cls.ui_key == key)
257 257
258 258 @classmethod
259 259 def get_builtin_hooks(cls):
260 260 q = cls.query()
261 261 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
262 262 cls.HOOK_REPO_SIZE,
263 263 cls.HOOK_PUSH, cls.HOOK_PULL]))
264 264 return q.all()
265 265
266 266 @classmethod
267 267 def get_custom_hooks(cls):
268 268 q = cls.query()
269 269 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
270 270 cls.HOOK_REPO_SIZE,
271 271 cls.HOOK_PUSH, cls.HOOK_PULL]))
272 272 q = q.filter(cls.ui_section == 'hooks')
273 273 return q.all()
274 274
275 275 @classmethod
276 276 def get_repos_location(cls):
277 277 return cls.get_by_key('/').one().ui_value
278 278
279 279 @classmethod
280 280 def create_or_update_hook(cls, key, val):
281 281 new_ui = cls.get_by_key(key).scalar() or cls()
282 282 new_ui.ui_section = 'hooks'
283 283 new_ui.ui_active = True
284 284 new_ui.ui_key = key
285 285 new_ui.ui_value = val
286 286
287 287 Session.add(new_ui)
288 288
289 289
290 290 class User(Base, BaseModel):
291 291 __tablename__ = 'users'
292 292 __table_args__ = (
293 293 UniqueConstraint('username'), UniqueConstraint('email'),
294 294 {'extend_existing': True, 'mysql_engine': 'InnoDB',
295 295 'mysql_charset': 'utf8'}
296 296 )
297 297 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
298 298 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
299 299 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
300 300 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
301 301 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
302 302 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
303 303 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
304 304 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
305 305 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
306 306 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
307 307 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
308 308
309 309 user_log = relationship('UserLog', cascade='all')
310 310 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
311 311
312 312 repositories = relationship('Repository')
313 313 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
314 314 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
315 315 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
316 316
317 317 group_member = relationship('UsersGroupMember', cascade='all')
318 318
319 319 notifications = relationship('UserNotification', cascade='all')
320 320 # notifications assigned to this user
321 321 user_created_notifications = relationship('Notification', cascade='all')
322 322 # comments created by this user
323 323 user_comments = relationship('ChangesetComment', cascade='all')
324 324
325 325 @hybrid_property
326 326 def email(self):
327 327 return self._email
328 328
329 329 @email.setter
330 330 def email(self, val):
331 331 self._email = val.lower() if val else None
332 332
333 333 @property
334 334 def full_name(self):
335 335 return '%s %s' % (self.name, self.lastname)
336 336
337 337 @property
338 338 def full_name_or_username(self):
339 339 return ('%s %s' % (self.name, self.lastname)
340 340 if (self.name and self.lastname) else self.username)
341 341
342 342 @property
343 343 def full_contact(self):
344 344 return '%s %s <%s>' % (self.name, self.lastname, self.email)
345 345
346 346 @property
347 347 def short_contact(self):
348 348 return '%s %s' % (self.name, self.lastname)
349 349
350 350 @property
351 351 def is_admin(self):
352 352 return self.admin
353 353
354 354 def __unicode__(self):
355 355 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
356 356 self.user_id, self.username)
357 357
358 358 @classmethod
359 359 def get_by_username(cls, username, case_insensitive=False, cache=False):
360 360 if case_insensitive:
361 361 q = cls.query().filter(cls.username.ilike(username))
362 362 else:
363 363 q = cls.query().filter(cls.username == username)
364 364
365 365 if cache:
366 366 q = q.options(FromCache(
367 367 "sql_cache_short",
368 368 "get_user_%s" % _hash_key(username)
369 369 )
370 370 )
371 371 return q.scalar()
372 372
373 373 @classmethod
374 374 def get_by_api_key(cls, api_key, cache=False):
375 375 q = cls.query().filter(cls.api_key == api_key)
376 376
377 377 if cache:
378 378 q = q.options(FromCache("sql_cache_short",
379 379 "get_api_key_%s" % api_key))
380 380 return q.scalar()
381 381
382 382 @classmethod
383 383 def get_by_email(cls, email, case_insensitive=False, cache=False):
384 384 if case_insensitive:
385 385 q = cls.query().filter(cls.email.ilike(email))
386 386 else:
387 387 q = cls.query().filter(cls.email == email)
388 388
389 389 if cache:
390 390 q = q.options(FromCache("sql_cache_short",
391 391 "get_email_key_%s" % email))
392 392
393 393 ret = q.scalar()
394 394 if ret is None:
395 395 q = UserEmailMap.query()
396 396 # try fetching in alternate email map
397 397 if case_insensitive:
398 398 q = q.filter(UserEmailMap.email.ilike(email))
399 399 else:
400 400 q = q.filter(UserEmailMap.email == email)
401 401 q = q.options(joinedload(UserEmailMap.user))
402 402 if cache:
403 403 q = q.options(FromCache("sql_cache_short",
404 404 "get_email_map_key_%s" % email))
405 405 ret = getattr(q.scalar(), 'user', None)
406 406
407 407 return ret
408 408
409 409 def update_lastlogin(self):
410 410 """Update user lastlogin"""
411 411 self.last_login = datetime.datetime.now()
412 412 Session.add(self)
413 413 log.debug('updated user %s lastlogin' % self.username)
414 414
415 415 def __json__(self):
416 416 return dict(
417 417 user_id=self.user_id,
418 418 first_name=self.name,
419 419 last_name=self.lastname,
420 420 email=self.email,
421 421 full_name=self.full_name,
422 422 full_name_or_username=self.full_name_or_username,
423 423 short_contact=self.short_contact,
424 424 full_contact=self.full_contact
425 425 )
426 426
427 427
428 428 class UserEmailMap(Base, BaseModel):
429 429 __tablename__ = 'user_email_map'
430 430 __table_args__ = (
431 431 UniqueConstraint('email'),
432 432 {'extend_existing': True, 'mysql_engine':'InnoDB',
433 433 'mysql_charset': 'utf8'}
434 434 )
435 435 __mapper_args__ = {}
436 436
437 437 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
438 438 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
439 439 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
440 440
441 441 user = relationship('User')
442 442
443 443 @validates('_email')
444 444 def validate_email(self, key, email):
445 445 # check if this email is not main one
446 446 main_email = Session.query(User).filter(User.email == email).scalar()
447 447 if main_email is not None:
448 448 raise AttributeError('email %s is present is user table' % email)
449 449 return email
450 450
451 451 @hybrid_property
452 452 def email(self):
453 453 return self._email
454 454
455 455 @email.setter
456 456 def email(self, val):
457 457 self._email = val.lower() if val else None
458 458
459 459
460 460 class UserLog(Base, BaseModel):
461 461 __tablename__ = 'user_logs'
462 462 __table_args__ = (
463 463 {'extend_existing': True, 'mysql_engine': 'InnoDB',
464 464 'mysql_charset': 'utf8'},
465 465 )
466 466 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
467 467 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
468 468 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
469 469 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
470 470 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
471 471 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
472 472 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
473 473
474 474 @property
475 475 def action_as_day(self):
476 476 return datetime.date(*self.action_date.timetuple()[:3])
477 477
478 478 user = relationship('User')
479 479 repository = relationship('Repository', cascade='')
480 480
481 481
482 482 class UsersGroup(Base, BaseModel):
483 483 __tablename__ = 'users_groups'
484 484 __table_args__ = (
485 485 {'extend_existing': True, 'mysql_engine': 'InnoDB',
486 486 'mysql_charset': 'utf8'},
487 487 )
488 488
489 489 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
490 490 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
491 491 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
492 492
493 493 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
494 494 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
495 495 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
496 496
497 497 def __unicode__(self):
498 498 return u'<userGroup(%s)>' % (self.users_group_name)
499 499
500 500 @classmethod
501 501 def get_by_group_name(cls, group_name, cache=False,
502 502 case_insensitive=False):
503 503 if case_insensitive:
504 504 q = cls.query().filter(cls.users_group_name.ilike(group_name))
505 505 else:
506 506 q = cls.query().filter(cls.users_group_name == group_name)
507 507 if cache:
508 508 q = q.options(FromCache(
509 509 "sql_cache_short",
510 510 "get_user_%s" % _hash_key(group_name)
511 511 )
512 512 )
513 513 return q.scalar()
514 514
515 515 @classmethod
516 516 def get(cls, users_group_id, cache=False):
517 517 users_group = cls.query()
518 518 if cache:
519 519 users_group = users_group.options(FromCache("sql_cache_short",
520 520 "get_users_group_%s" % users_group_id))
521 521 return users_group.get(users_group_id)
522 522
523 523
524 524 class UsersGroupMember(Base, BaseModel):
525 525 __tablename__ = 'users_groups_members'
526 526 __table_args__ = (
527 527 {'extend_existing': True, 'mysql_engine': 'InnoDB',
528 528 'mysql_charset': 'utf8'},
529 529 )
530 530
531 531 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
532 532 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
533 533 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
534 534
535 535 user = relationship('User', lazy='joined')
536 536 users_group = relationship('UsersGroup')
537 537
538 538 def __init__(self, gr_id='', u_id=''):
539 539 self.users_group_id = gr_id
540 540 self.user_id = u_id
541 541
542 542
543 543 class Repository(Base, BaseModel):
544 544 __tablename__ = 'repositories'
545 545 __table_args__ = (
546 546 UniqueConstraint('repo_name'),
547 547 {'extend_existing': True, 'mysql_engine': 'InnoDB',
548 548 'mysql_charset': 'utf8'},
549 549 )
550 550
551 551 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
552 552 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
553 553 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
554 554 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
555 555 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
556 556 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
557 557 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
558 558 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
559 559 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
560 560 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
561 561
562 562 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
563 563 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
564 564
565 565 user = relationship('User')
566 566 fork = relationship('Repository', remote_side=repo_id)
567 567 group = relationship('RepoGroup')
568 568 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
569 569 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
570 570 stats = relationship('Statistics', cascade='all', uselist=False)
571 571
572 572 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
573 573
574 574 logs = relationship('UserLog')
575 575 comments = relationship('ChangesetComment')
576 576
577 577 def __unicode__(self):
578 578 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
579 579 self.repo_name)
580 580
581 581 @classmethod
582 582 def url_sep(cls):
583 583 return URL_SEP
584 584
585 585 @classmethod
586 586 def get_by_repo_name(cls, repo_name):
587 587 q = Session.query(cls).filter(cls.repo_name == repo_name)
588 588 q = q.options(joinedload(Repository.fork))\
589 589 .options(joinedload(Repository.user))\
590 590 .options(joinedload(Repository.group))
591 591 return q.scalar()
592 592
593 593 @classmethod
594 594 def get_by_full_path(cls, repo_full_path):
595 595 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
596 596 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
597 597
598 598 @classmethod
599 599 def get_repo_forks(cls, repo_id):
600 600 return cls.query().filter(Repository.fork_id == repo_id)
601 601
602 602 @classmethod
603 603 def base_path(cls):
604 604 """
605 605 Returns base path when all repos are stored
606 606
607 607 :param cls:
608 608 """
609 609 q = Session.query(RhodeCodeUi)\
610 610 .filter(RhodeCodeUi.ui_key == cls.url_sep())
611 611 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
612 612 return q.one().ui_value
613 613
614 614 @property
615 615 def forks(self):
616 616 """
617 617 Return forks of this repo
618 618 """
619 619 return Repository.get_repo_forks(self.repo_id)
620 620
621 621 @property
622 622 def parent(self):
623 623 """
624 624 Returns fork parent
625 625 """
626 626 return self.fork
627 627
628 628 @property
629 629 def just_name(self):
630 630 return self.repo_name.split(Repository.url_sep())[-1]
631 631
632 632 @property
633 633 def groups_with_parents(self):
634 634 groups = []
635 635 if self.group is None:
636 636 return groups
637 637
638 638 cur_gr = self.group
639 639 groups.insert(0, cur_gr)
640 640 while 1:
641 641 gr = getattr(cur_gr, 'parent_group', None)
642 642 cur_gr = cur_gr.parent_group
643 643 if gr is None:
644 644 break
645 645 groups.insert(0, gr)
646 646
647 647 return groups
648 648
649 649 @property
650 650 def groups_and_repo(self):
651 651 return self.groups_with_parents, self.just_name
652 652
653 653 @LazyProperty
654 654 def repo_path(self):
655 655 """
656 656 Returns base full path for that repository means where it actually
657 657 exists on a filesystem
658 658 """
659 659 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
660 660 Repository.url_sep())
661 661 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
662 662 return q.one().ui_value
663 663
664 664 @property
665 665 def repo_full_path(self):
666 666 p = [self.repo_path]
667 667 # we need to split the name by / since this is how we store the
668 668 # names in the database, but that eventually needs to be converted
669 669 # into a valid system path
670 670 p += self.repo_name.split(Repository.url_sep())
671 671 return os.path.join(*p)
672 672
673 673 def get_new_name(self, repo_name):
674 674 """
675 675 returns new full repository name based on assigned group and new new
676 676
677 677 :param group_name:
678 678 """
679 679 path_prefix = self.group.full_path_splitted if self.group else []
680 680 return Repository.url_sep().join(path_prefix + [repo_name])
681 681
682 682 @property
683 683 def _ui(self):
684 684 """
685 685 Creates an db based ui object for this repository
686 686 """
687 687 from mercurial import ui
688 688 from mercurial import config
689 689 baseui = ui.ui()
690 690
691 691 #clean the baseui object
692 692 baseui._ocfg = config.config()
693 693 baseui._ucfg = config.config()
694 694 baseui._tcfg = config.config()
695 695
696 696 ret = RhodeCodeUi.query()\
697 697 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
698 698
699 699 hg_ui = ret
700 700 for ui_ in hg_ui:
701 701 if ui_.ui_active:
702 702 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
703 703 ui_.ui_key, ui_.ui_value)
704 704 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
705 705
706 706 return baseui
707 707
708 708 @classmethod
709 709 def is_valid(cls, repo_name):
710 710 """
711 711 returns True if given repo name is a valid filesystem repository
712 712
713 713 :param cls:
714 714 :param repo_name:
715 715 """
716 716 from rhodecode.lib.utils import is_valid_repo
717 717
718 718 return is_valid_repo(repo_name, cls.base_path())
719 719
720 720 #==========================================================================
721 721 # SCM PROPERTIES
722 722 #==========================================================================
723 723
724 724 def get_changeset(self, rev=None):
725 725 return get_changeset_safe(self.scm_instance, rev)
726 726
727 727 @property
728 728 def tip(self):
729 729 return self.get_changeset('tip')
730 730
731 731 @property
732 732 def author(self):
733 733 return self.tip.author
734 734
735 735 @property
736 736 def last_change(self):
737 737 return self.scm_instance.last_change
738 738
739 739 def comments(self, revisions=None):
740 740 """
741 741 Returns comments for this repository grouped by revisions
742 742
743 743 :param revisions: filter query by revisions only
744 744 """
745 745 cmts = ChangesetComment.query()\
746 746 .filter(ChangesetComment.repo == self)
747 747 if revisions:
748 748 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
749 749 grouped = defaultdict(list)
750 750 for cmt in cmts.all():
751 751 grouped[cmt.revision].append(cmt)
752 752 return grouped
753 753
754 754 def statuses(self, revisions=None):
755 755 """
756 756 Returns statuses for this repository
757 757
758 758 :param revisions: list of revisions to get statuses for
759 759 :type revisions: list
760 760 """
761 761
762 762 statuses = ChangesetStatus.query()\
763 763 .filter(ChangesetStatus.repo == self)\
764 764 .filter(ChangesetStatus.version == 0)
765 765 if revisions:
766 766 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
767 767 grouped = {}
768 768 for stat in statuses.all():
769 grouped[stat.revision] = [str(stat.status), stat.status_lbl]
769 pr_id = pr_repo = None
770 if stat.pull_request:
771 pr_id = stat.pull_request.pull_request_id
772 pr_repo = stat.pull_request.other_repo.repo_name
773 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
774 pr_id, pr_repo]
770 775 return grouped
771 776
772 777 #==========================================================================
773 778 # SCM CACHE INSTANCE
774 779 #==========================================================================
775 780
776 781 @property
777 782 def invalidate(self):
778 783 return CacheInvalidation.invalidate(self.repo_name)
779 784
780 785 def set_invalidate(self):
781 786 """
782 787 set a cache for invalidation for this instance
783 788 """
784 789 CacheInvalidation.set_invalidate(self.repo_name)
785 790
786 791 @LazyProperty
787 792 def scm_instance(self):
788 793 return self.__get_instance()
789 794
790 795 def scm_instance_cached(self, cache_map=None):
791 796 @cache_region('long_term')
792 797 def _c(repo_name):
793 798 return self.__get_instance()
794 799 rn = self.repo_name
795 800 log.debug('Getting cached instance of repo')
796 801
797 802 if cache_map:
798 803 # get using prefilled cache_map
799 804 invalidate_repo = cache_map[self.repo_name]
800 805 if invalidate_repo:
801 806 invalidate_repo = (None if invalidate_repo.cache_active
802 807 else invalidate_repo)
803 808 else:
804 809 # get from invalidate
805 810 invalidate_repo = self.invalidate
806 811
807 812 if invalidate_repo is not None:
808 813 region_invalidate(_c, None, rn)
809 814 # update our cache
810 815 CacheInvalidation.set_valid(invalidate_repo.cache_key)
811 816 return _c(rn)
812 817
813 818 def __get_instance(self):
814 819 repo_full_path = self.repo_full_path
815 820 try:
816 821 alias = get_scm(repo_full_path)[0]
817 822 log.debug('Creating instance of %s repository' % alias)
818 823 backend = get_backend(alias)
819 824 except VCSError:
820 825 log.error(traceback.format_exc())
821 826 log.error('Perhaps this repository is in db and not in '
822 827 'filesystem run rescan repositories with '
823 828 '"destroy old data " option from admin panel')
824 829 return
825 830
826 831 if alias == 'hg':
827 832
828 833 repo = backend(safe_str(repo_full_path), create=False,
829 834 baseui=self._ui)
830 835 # skip hidden web repository
831 836 if repo._get_hidden():
832 837 return
833 838 else:
834 839 repo = backend(repo_full_path, create=False)
835 840
836 841 return repo
837 842
838 843
839 844 class RepoGroup(Base, BaseModel):
840 845 __tablename__ = 'groups'
841 846 __table_args__ = (
842 847 UniqueConstraint('group_name', 'group_parent_id'),
843 848 CheckConstraint('group_id != group_parent_id'),
844 849 {'extend_existing': True, 'mysql_engine': 'InnoDB',
845 850 'mysql_charset': 'utf8'},
846 851 )
847 852 __mapper_args__ = {'order_by': 'group_name'}
848 853
849 854 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
850 855 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
851 856 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
852 857 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
853 858
854 859 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
855 860 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
856 861
857 862 parent_group = relationship('RepoGroup', remote_side=group_id)
858 863
859 864 def __init__(self, group_name='', parent_group=None):
860 865 self.group_name = group_name
861 866 self.parent_group = parent_group
862 867
863 868 def __unicode__(self):
864 869 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
865 870 self.group_name)
866 871
867 872 @classmethod
868 873 def groups_choices(cls):
869 874 from webhelpers.html import literal as _literal
870 875 repo_groups = [('', '')]
871 876 sep = ' &raquo; '
872 877 _name = lambda k: _literal(sep.join(k))
873 878
874 879 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
875 880 for x in cls.query().all()])
876 881
877 882 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
878 883 return repo_groups
879 884
880 885 @classmethod
881 886 def url_sep(cls):
882 887 return URL_SEP
883 888
884 889 @classmethod
885 890 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
886 891 if case_insensitive:
887 892 gr = cls.query()\
888 893 .filter(cls.group_name.ilike(group_name))
889 894 else:
890 895 gr = cls.query()\
891 896 .filter(cls.group_name == group_name)
892 897 if cache:
893 898 gr = gr.options(FromCache(
894 899 "sql_cache_short",
895 900 "get_group_%s" % _hash_key(group_name)
896 901 )
897 902 )
898 903 return gr.scalar()
899 904
900 905 @property
901 906 def parents(self):
902 907 parents_recursion_limit = 5
903 908 groups = []
904 909 if self.parent_group is None:
905 910 return groups
906 911 cur_gr = self.parent_group
907 912 groups.insert(0, cur_gr)
908 913 cnt = 0
909 914 while 1:
910 915 cnt += 1
911 916 gr = getattr(cur_gr, 'parent_group', None)
912 917 cur_gr = cur_gr.parent_group
913 918 if gr is None:
914 919 break
915 920 if cnt == parents_recursion_limit:
916 921 # this will prevent accidental infinit loops
917 922 log.error('group nested more than %s' %
918 923 parents_recursion_limit)
919 924 break
920 925
921 926 groups.insert(0, gr)
922 927 return groups
923 928
924 929 @property
925 930 def children(self):
926 931 return RepoGroup.query().filter(RepoGroup.parent_group == self)
927 932
928 933 @property
929 934 def name(self):
930 935 return self.group_name.split(RepoGroup.url_sep())[-1]
931 936
932 937 @property
933 938 def full_path(self):
934 939 return self.group_name
935 940
936 941 @property
937 942 def full_path_splitted(self):
938 943 return self.group_name.split(RepoGroup.url_sep())
939 944
940 945 @property
941 946 def repositories(self):
942 947 return Repository.query()\
943 948 .filter(Repository.group == self)\
944 949 .order_by(Repository.repo_name)
945 950
946 951 @property
947 952 def repositories_recursive_count(self):
948 953 cnt = self.repositories.count()
949 954
950 955 def children_count(group):
951 956 cnt = 0
952 957 for child in group.children:
953 958 cnt += child.repositories.count()
954 959 cnt += children_count(child)
955 960 return cnt
956 961
957 962 return cnt + children_count(self)
958 963
959 964 def get_new_name(self, group_name):
960 965 """
961 966 returns new full group name based on parent and new name
962 967
963 968 :param group_name:
964 969 """
965 970 path_prefix = (self.parent_group.full_path_splitted if
966 971 self.parent_group else [])
967 972 return RepoGroup.url_sep().join(path_prefix + [group_name])
968 973
969 974
970 975 class Permission(Base, BaseModel):
971 976 __tablename__ = 'permissions'
972 977 __table_args__ = (
973 978 {'extend_existing': True, 'mysql_engine': 'InnoDB',
974 979 'mysql_charset': 'utf8'},
975 980 )
976 981 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
977 982 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
978 983 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
979 984
980 985 def __unicode__(self):
981 986 return u"<%s('%s:%s')>" % (
982 987 self.__class__.__name__, self.permission_id, self.permission_name
983 988 )
984 989
985 990 @classmethod
986 991 def get_by_key(cls, key):
987 992 return cls.query().filter(cls.permission_name == key).scalar()
988 993
989 994 @classmethod
990 995 def get_default_perms(cls, default_user_id):
991 996 q = Session.query(UserRepoToPerm, Repository, cls)\
992 997 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
993 998 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
994 999 .filter(UserRepoToPerm.user_id == default_user_id)
995 1000
996 1001 return q.all()
997 1002
998 1003 @classmethod
999 1004 def get_default_group_perms(cls, default_user_id):
1000 1005 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
1001 1006 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1002 1007 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1003 1008 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1004 1009
1005 1010 return q.all()
1006 1011
1007 1012
1008 1013 class UserRepoToPerm(Base, BaseModel):
1009 1014 __tablename__ = 'repo_to_perm'
1010 1015 __table_args__ = (
1011 1016 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1012 1017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1013 1018 'mysql_charset': 'utf8'}
1014 1019 )
1015 1020 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1016 1021 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1017 1022 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1018 1023 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1019 1024
1020 1025 user = relationship('User')
1021 1026 repository = relationship('Repository')
1022 1027 permission = relationship('Permission')
1023 1028
1024 1029 @classmethod
1025 1030 def create(cls, user, repository, permission):
1026 1031 n = cls()
1027 1032 n.user = user
1028 1033 n.repository = repository
1029 1034 n.permission = permission
1030 1035 Session.add(n)
1031 1036 return n
1032 1037
1033 1038 def __unicode__(self):
1034 1039 return u'<user:%s => %s >' % (self.user, self.repository)
1035 1040
1036 1041
1037 1042 class UserToPerm(Base, BaseModel):
1038 1043 __tablename__ = 'user_to_perm'
1039 1044 __table_args__ = (
1040 1045 UniqueConstraint('user_id', 'permission_id'),
1041 1046 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1042 1047 'mysql_charset': 'utf8'}
1043 1048 )
1044 1049 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1045 1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1046 1051 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1047 1052
1048 1053 user = relationship('User')
1049 1054 permission = relationship('Permission', lazy='joined')
1050 1055
1051 1056
1052 1057 class UsersGroupRepoToPerm(Base, BaseModel):
1053 1058 __tablename__ = 'users_group_repo_to_perm'
1054 1059 __table_args__ = (
1055 1060 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1056 1061 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1057 1062 'mysql_charset': 'utf8'}
1058 1063 )
1059 1064 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1060 1065 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1061 1066 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1062 1067 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1063 1068
1064 1069 users_group = relationship('UsersGroup')
1065 1070 permission = relationship('Permission')
1066 1071 repository = relationship('Repository')
1067 1072
1068 1073 @classmethod
1069 1074 def create(cls, users_group, repository, permission):
1070 1075 n = cls()
1071 1076 n.users_group = users_group
1072 1077 n.repository = repository
1073 1078 n.permission = permission
1074 1079 Session.add(n)
1075 1080 return n
1076 1081
1077 1082 def __unicode__(self):
1078 1083 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1079 1084
1080 1085
1081 1086 class UsersGroupToPerm(Base, BaseModel):
1082 1087 __tablename__ = 'users_group_to_perm'
1083 1088 __table_args__ = (
1084 1089 UniqueConstraint('users_group_id', 'permission_id',),
1085 1090 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1086 1091 'mysql_charset': 'utf8'}
1087 1092 )
1088 1093 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1089 1094 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1090 1095 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1091 1096
1092 1097 users_group = relationship('UsersGroup')
1093 1098 permission = relationship('Permission')
1094 1099
1095 1100
1096 1101 class UserRepoGroupToPerm(Base, BaseModel):
1097 1102 __tablename__ = 'user_repo_group_to_perm'
1098 1103 __table_args__ = (
1099 1104 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1100 1105 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1101 1106 'mysql_charset': 'utf8'}
1102 1107 )
1103 1108
1104 1109 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1105 1110 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1106 1111 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1107 1112 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1108 1113
1109 1114 user = relationship('User')
1110 1115 group = relationship('RepoGroup')
1111 1116 permission = relationship('Permission')
1112 1117
1113 1118
1114 1119 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1115 1120 __tablename__ = 'users_group_repo_group_to_perm'
1116 1121 __table_args__ = (
1117 1122 UniqueConstraint('users_group_id', 'group_id'),
1118 1123 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1119 1124 'mysql_charset': 'utf8'}
1120 1125 )
1121 1126
1122 1127 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)
1123 1128 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1124 1129 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1125 1130 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1126 1131
1127 1132 users_group = relationship('UsersGroup')
1128 1133 permission = relationship('Permission')
1129 1134 group = relationship('RepoGroup')
1130 1135
1131 1136
1132 1137 class Statistics(Base, BaseModel):
1133 1138 __tablename__ = 'statistics'
1134 1139 __table_args__ = (
1135 1140 UniqueConstraint('repository_id'),
1136 1141 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1137 1142 'mysql_charset': 'utf8'}
1138 1143 )
1139 1144 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1140 1145 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1141 1146 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1142 1147 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1143 1148 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1144 1149 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1145 1150
1146 1151 repository = relationship('Repository', single_parent=True)
1147 1152
1148 1153
1149 1154 class UserFollowing(Base, BaseModel):
1150 1155 __tablename__ = 'user_followings'
1151 1156 __table_args__ = (
1152 1157 UniqueConstraint('user_id', 'follows_repository_id'),
1153 1158 UniqueConstraint('user_id', 'follows_user_id'),
1154 1159 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1155 1160 'mysql_charset': 'utf8'}
1156 1161 )
1157 1162
1158 1163 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1159 1164 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1160 1165 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1161 1166 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1162 1167 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1163 1168
1164 1169 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1165 1170
1166 1171 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1167 1172 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1168 1173
1169 1174 @classmethod
1170 1175 def get_repo_followers(cls, repo_id):
1171 1176 return cls.query().filter(cls.follows_repo_id == repo_id)
1172 1177
1173 1178
1174 1179 class CacheInvalidation(Base, BaseModel):
1175 1180 __tablename__ = 'cache_invalidation'
1176 1181 __table_args__ = (
1177 1182 UniqueConstraint('cache_key'),
1178 1183 Index('key_idx', 'cache_key'),
1179 1184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1180 1185 'mysql_charset': 'utf8'},
1181 1186 )
1182 1187 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1183 1188 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1184 1189 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1185 1190 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1186 1191
1187 1192 def __init__(self, cache_key, cache_args=''):
1188 1193 self.cache_key = cache_key
1189 1194 self.cache_args = cache_args
1190 1195 self.cache_active = False
1191 1196
1192 1197 def __unicode__(self):
1193 1198 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1194 1199 self.cache_id, self.cache_key)
1195 1200
1196 1201 @classmethod
1197 1202 def clear_cache(cls):
1198 1203 cls.query().delete()
1199 1204
1200 1205 @classmethod
1201 1206 def _get_key(cls, key):
1202 1207 """
1203 1208 Wrapper for generating a key, together with a prefix
1204 1209
1205 1210 :param key:
1206 1211 """
1207 1212 import rhodecode
1208 1213 prefix = ''
1209 1214 iid = rhodecode.CONFIG.get('instance_id')
1210 1215 if iid:
1211 1216 prefix = iid
1212 1217 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1213 1218
1214 1219 @classmethod
1215 1220 def get_by_key(cls, key):
1216 1221 return cls.query().filter(cls.cache_key == key).scalar()
1217 1222
1218 1223 @classmethod
1219 1224 def _get_or_create_key(cls, key, prefix, org_key):
1220 1225 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1221 1226 if not inv_obj:
1222 1227 try:
1223 1228 inv_obj = CacheInvalidation(key, org_key)
1224 1229 Session.add(inv_obj)
1225 1230 Session.commit()
1226 1231 except Exception:
1227 1232 log.error(traceback.format_exc())
1228 1233 Session.rollback()
1229 1234 return inv_obj
1230 1235
1231 1236 @classmethod
1232 1237 def invalidate(cls, key):
1233 1238 """
1234 1239 Returns Invalidation object if this given key should be invalidated
1235 1240 None otherwise. `cache_active = False` means that this cache
1236 1241 state is not valid and needs to be invalidated
1237 1242
1238 1243 :param key:
1239 1244 """
1240 1245
1241 1246 key, _prefix, _org_key = cls._get_key(key)
1242 1247 inv = cls._get_or_create_key(key, _prefix, _org_key)
1243 1248
1244 1249 if inv and inv.cache_active is False:
1245 1250 return inv
1246 1251
1247 1252 @classmethod
1248 1253 def set_invalidate(cls, key):
1249 1254 """
1250 1255 Mark this Cache key for invalidation
1251 1256
1252 1257 :param key:
1253 1258 """
1254 1259
1255 1260 key, _prefix, _org_key = cls._get_key(key)
1256 1261 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1257 1262 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1258 1263 _org_key))
1259 1264 try:
1260 1265 for inv_obj in inv_objs:
1261 1266 if inv_obj:
1262 1267 inv_obj.cache_active = False
1263 1268
1264 1269 Session.add(inv_obj)
1265 1270 Session.commit()
1266 1271 except Exception:
1267 1272 log.error(traceback.format_exc())
1268 1273 Session.rollback()
1269 1274
1270 1275 @classmethod
1271 1276 def set_valid(cls, key):
1272 1277 """
1273 1278 Mark this cache key as active and currently cached
1274 1279
1275 1280 :param key:
1276 1281 """
1277 1282 inv_obj = cls.get_by_key(key)
1278 1283 inv_obj.cache_active = True
1279 1284 Session.add(inv_obj)
1280 1285 Session.commit()
1281 1286
1282 1287 @classmethod
1283 1288 def get_cache_map(cls):
1284 1289
1285 1290 class cachemapdict(dict):
1286 1291
1287 1292 def __init__(self, *args, **kwargs):
1288 1293 fixkey = kwargs.get('fixkey')
1289 1294 if fixkey:
1290 1295 del kwargs['fixkey']
1291 1296 self.fixkey = fixkey
1292 1297 super(cachemapdict, self).__init__(*args, **kwargs)
1293 1298
1294 1299 def __getattr__(self, name):
1295 1300 key = name
1296 1301 if self.fixkey:
1297 1302 key, _prefix, _org_key = cls._get_key(key)
1298 1303 if key in self.__dict__:
1299 1304 return self.__dict__[key]
1300 1305 else:
1301 1306 return self[key]
1302 1307
1303 1308 def __getitem__(self, key):
1304 1309 if self.fixkey:
1305 1310 key, _prefix, _org_key = cls._get_key(key)
1306 1311 try:
1307 1312 return super(cachemapdict, self).__getitem__(key)
1308 1313 except KeyError:
1309 1314 return
1310 1315
1311 1316 cache_map = cachemapdict(fixkey=True)
1312 1317 for obj in cls.query().all():
1313 1318 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1314 1319 return cache_map
1315 1320
1316 1321
1317 1322 class ChangesetComment(Base, BaseModel):
1318 1323 __tablename__ = 'changeset_comments'
1319 1324 __table_args__ = (
1320 1325 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1321 1326 'mysql_charset': 'utf8'},
1322 1327 )
1323 1328 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1324 1329 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1325 1330 revision = Column('revision', String(40), nullable=True)
1326 1331 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1327 1332 line_no = Column('line_no', Unicode(10), nullable=True)
1328 1333 f_path = Column('f_path', Unicode(1000), nullable=True)
1329 1334 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1330 1335 text = Column('text', Unicode(25000), nullable=False)
1331 1336 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1332 1337
1333 1338 author = relationship('User', lazy='joined')
1334 1339 repo = relationship('Repository')
1335 1340 status_change = relationship('ChangesetStatus', uselist=False)
1336 1341 pull_request = relationship('PullRequest', lazy='joined')
1337 1342
1338 1343 @classmethod
1339 def get_users(cls, revision):
1344 def get_users(cls, revision=None, pull_request_id=None):
1340 1345 """
1341 Returns user associated with this changesetComment. ie those
1346 Returns user associated with this ChangesetComment. ie those
1342 1347 who actually commented
1343 1348
1344 1349 :param cls:
1345 1350 :param revision:
1346 1351 """
1347 return Session.query(User)\
1348 .filter(cls.revision == revision)\
1349 .join(ChangesetComment.author).all()
1352 q = Session.query(User)\
1353 .join(ChangesetComment.author)
1354 if revision:
1355 q = q.filter(cls.revision == revision)
1356 elif pull_request_id:
1357 q = q.filter(cls.pull_request_id == pull_request_id)
1358 return q.all()
1350 1359
1351 1360
1352 1361 class ChangesetStatus(Base, BaseModel):
1353 1362 __tablename__ = 'changeset_statuses'
1354 1363 __table_args__ = (
1355 1364 UniqueConstraint('repo_id', 'revision', 'version'),
1356 1365 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1357 1366 'mysql_charset': 'utf8'}
1358 1367 )
1359 1368
1360 1369 STATUSES = [
1361 1370 ('not_reviewed', _("Not Reviewed")), # (no icon) and default
1362 1371 ('approved', _("Approved")),
1363 1372 ('rejected', _("Rejected")),
1364 1373 ('under_review', _("Under Review")),
1365 1374 ]
1366 1375 DEFAULT = STATUSES[0][0]
1367 1376
1368 1377 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1369 1378 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1370 1379 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1371 1380 revision = Column('revision', String(40), nullable=False)
1372 1381 status = Column('status', String(128), nullable=False, default=DEFAULT)
1373 1382 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1374 1383 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1375 1384 version = Column('version', Integer(), nullable=False, default=0)
1376 1385 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1377 1386
1378 1387 author = relationship('User', lazy='joined')
1379 1388 repo = relationship('Repository')
1380 1389 comment = relationship('ChangesetComment', lazy='joined')
1381 1390 pull_request = relationship('PullRequest', lazy='joined')
1382 1391
1383 1392 @classmethod
1384 1393 def get_status_lbl(cls, value):
1385 1394 return dict(cls.STATUSES).get(value)
1386 1395
1387 1396 @property
1388 1397 def status_lbl(self):
1389 1398 return ChangesetStatus.get_status_lbl(self.status)
1390 1399
1391 1400
1392 1401 class PullRequest(Base, BaseModel):
1393 1402 __tablename__ = 'pull_requests'
1394 1403 __table_args__ = (
1395 1404 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1396 1405 'mysql_charset': 'utf8'},
1397 1406 )
1398 1407
1399 1408 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1400 1409 title = Column('title', Unicode(256), nullable=True)
1401 1410 description = Column('description', Unicode(10240), nullable=True)
1402 1411 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1403 1412 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1404 1413 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1405 1414 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1406 1415 org_ref = Column('org_ref', Unicode(256), nullable=False)
1407 1416 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1408 1417 other_ref = Column('other_ref', Unicode(256), nullable=False)
1409 1418
1410 1419 @hybrid_property
1411 1420 def revisions(self):
1412 1421 return self._revisions.split(':')
1413 1422
1414 1423 @revisions.setter
1415 1424 def revisions(self, val):
1416 1425 self._revisions = ':'.join(val)
1417 1426
1418 1427 author = relationship('User', lazy='joined')
1419 1428 reviewers = relationship('PullRequestReviewers')
1420 1429 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1421 1430 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1422 1431
1423 1432 def __json__(self):
1424 1433 return dict(
1425 1434 revisions=self.revisions
1426 1435 )
1427 1436
1428 1437
1429 1438 class PullRequestReviewers(Base, BaseModel):
1430 1439 __tablename__ = 'pull_request_reviewers'
1431 1440 __table_args__ = (
1432 1441 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1433 1442 'mysql_charset': 'utf8'},
1434 1443 )
1435 1444
1436 1445 def __init__(self, user=None, pull_request=None):
1437 1446 self.user = user
1438 1447 self.pull_request = pull_request
1439 1448
1440 1449 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1441 1450 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1442 1451 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1443 1452
1444 1453 user = relationship('User')
1445 1454 pull_request = relationship('PullRequest')
1446 1455
1447 1456
1448 1457 class Notification(Base, BaseModel):
1449 1458 __tablename__ = 'notifications'
1450 1459 __table_args__ = (
1451 1460 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1452 1461 'mysql_charset': 'utf8'},
1453 1462 )
1454 1463
1455 1464 TYPE_CHANGESET_COMMENT = u'cs_comment'
1456 1465 TYPE_MESSAGE = u'message'
1457 1466 TYPE_MENTION = u'mention'
1458 1467 TYPE_REGISTRATION = u'registration'
1459 1468 TYPE_PULL_REQUEST = u'pull_request'
1469 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1460 1470
1461 1471 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1462 1472 subject = Column('subject', Unicode(512), nullable=True)
1463 1473 body = Column('body', Unicode(50000), nullable=True)
1464 1474 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1465 1475 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1466 1476 type_ = Column('type', Unicode(256))
1467 1477
1468 1478 created_by_user = relationship('User')
1469 1479 notifications_to_users = relationship('UserNotification', lazy='joined',
1470 1480 cascade="all, delete, delete-orphan")
1471 1481
1472 1482 @property
1473 1483 def recipients(self):
1474 1484 return [x.user for x in UserNotification.query()\
1475 1485 .filter(UserNotification.notification == self)\
1476 1486 .order_by(UserNotification.user).all()]
1477 1487
1478 1488 @classmethod
1479 1489 def create(cls, created_by, subject, body, recipients, type_=None):
1480 1490 if type_ is None:
1481 1491 type_ = Notification.TYPE_MESSAGE
1482 1492
1483 1493 notification = cls()
1484 1494 notification.created_by_user = created_by
1485 1495 notification.subject = subject
1486 1496 notification.body = body
1487 1497 notification.type_ = type_
1488 1498 notification.created_on = datetime.datetime.now()
1489 1499
1490 1500 for u in recipients:
1491 1501 assoc = UserNotification()
1492 1502 assoc.notification = notification
1493 1503 u.notifications.append(assoc)
1494 1504 Session.add(notification)
1495 1505 return notification
1496 1506
1497 1507 @property
1498 1508 def description(self):
1499 1509 from rhodecode.model.notification import NotificationModel
1500 1510 return NotificationModel().make_description(self)
1501 1511
1502 1512
1503 1513 class UserNotification(Base, BaseModel):
1504 1514 __tablename__ = 'user_to_notification'
1505 1515 __table_args__ = (
1506 1516 UniqueConstraint('user_id', 'notification_id'),
1507 1517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1508 1518 'mysql_charset': 'utf8'}
1509 1519 )
1510 1520 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1511 1521 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1512 1522 read = Column('read', Boolean, default=False)
1513 1523 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1514 1524
1515 1525 user = relationship('User', lazy="joined")
1516 1526 notification = relationship('Notification', lazy="joined",
1517 1527 order_by=lambda: Notification.created_on.desc(),)
1518 1528
1519 1529 def mark_as_read(self):
1520 1530 self.read = True
1521 1531 Session.add(self)
1522 1532
1523 1533
1524 1534 class DbMigrateVersion(Base, BaseModel):
1525 1535 __tablename__ = 'db_migrate_version'
1526 1536 __table_args__ = (
1527 1537 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1528 1538 'mysql_charset': 'utf8'},
1529 1539 )
1530 1540 repository_id = Column('repository_id', String(250), primary_key=True)
1531 1541 repository_path = Column('repository_path', Text)
1532 1542 version = Column('version', Integer)
@@ -1,256 +1,258 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.notification
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Model for notifications
7 7
8 8
9 9 :created_on: Nov 20, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30 import datetime
31 31
32 32 from pylons.i18n.translation import _
33 33
34 34 import rhodecode
35 35 from rhodecode.config.conf import DATETIME_FORMAT
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.model import BaseModel
38 38 from rhodecode.model.db import Notification, User, UserNotification
39 39 from sqlalchemy.orm import joinedload
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class NotificationModel(BaseModel):
45 45
46 46 def __get_notification(self, notification):
47 47 if isinstance(notification, Notification):
48 48 return notification
49 49 elif isinstance(notification, (int, long)):
50 50 return Notification.get(notification)
51 51 else:
52 52 if notification:
53 53 raise Exception('notification must be int, long or Instance'
54 54 ' of Notification got %s' % type(notification))
55 55
56 56 def create(self, created_by, subject, body, recipients=None,
57 57 type_=Notification.TYPE_MESSAGE, with_email=True,
58 58 email_kwargs={}):
59 59 """
60 60
61 61 Creates notification of given type
62 62
63 63 :param created_by: int, str or User instance. User who created this
64 64 notification
65 65 :param subject:
66 66 :param body:
67 67 :param recipients: list of int, str or User objects, when None
68 68 is given send to all admins
69 69 :param type_: type of notification
70 70 :param with_email: send email with this notification
71 71 :param email_kwargs: additional dict to pass as args to email template
72 72 """
73 73 from rhodecode.lib.celerylib import tasks, run_task
74 74
75 75 if recipients and not getattr(recipients, '__iter__', False):
76 76 raise Exception('recipients must be a list of iterable')
77 77
78 78 created_by_obj = self._get_user(created_by)
79 79
80 80 if recipients:
81 81 recipients_objs = []
82 82 for u in recipients:
83 83 obj = self._get_user(u)
84 84 if obj:
85 85 recipients_objs.append(obj)
86 86 recipients_objs = set(recipients_objs)
87 87 log.debug('sending notifications %s to %s' % (
88 88 type_, recipients_objs)
89 89 )
90 90 else:
91 91 # empty recipients means to all admins
92 92 recipients_objs = User.query().filter(User.admin == True).all()
93 93 log.debug('sending notifications %s to admins: %s' % (
94 94 type_, recipients_objs)
95 95 )
96 96 notif = Notification.create(
97 97 created_by=created_by_obj, subject=subject,
98 98 body=body, recipients=recipients_objs, type_=type_
99 99 )
100 100
101 101 if with_email is False:
102 102 return notif
103 103
104 104 #don't send email to person who created this comment
105 105 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
106 106
107 107 # send email with notification to all other participants
108 108 for rec in rec_objs:
109 109 email_subject = NotificationModel().make_description(notif, False)
110 110 type_ = type_
111 111 email_body = body
112 112 ## this is passed into template
113 113 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
114 114 kwargs.update(email_kwargs)
115 115 email_body_html = EmailNotificationModel()\
116 116 .get_email_tmpl(type_, **kwargs)
117 117
118 118 run_task(tasks.send_email, rec.email, email_subject, email_body,
119 119 email_body_html)
120 120
121 121 return notif
122 122
123 123 def delete(self, user, notification):
124 124 # we don't want to remove actual notification just the assignment
125 125 try:
126 126 notification = self.__get_notification(notification)
127 127 user = self._get_user(user)
128 128 if notification and user:
129 129 obj = UserNotification.query()\
130 130 .filter(UserNotification.user == user)\
131 131 .filter(UserNotification.notification
132 132 == notification)\
133 133 .one()
134 134 self.sa.delete(obj)
135 135 return True
136 136 except Exception:
137 137 log.error(traceback.format_exc())
138 138 raise
139 139
140 140 def get_for_user(self, user, filter_=None):
141 141 """
142 142 Get mentions for given user, filter them if filter dict is given
143 143
144 144 :param user:
145 145 :type user:
146 146 :param filter:
147 147 """
148 148 user = self._get_user(user)
149 149
150 150 q = UserNotification.query()\
151 151 .filter(UserNotification.user == user)\
152 152 .join((Notification, UserNotification.notification_id ==
153 153 Notification.notification_id))
154 154
155 155 if filter_:
156 156 q = q.filter(Notification.type_ == filter_.get('type'))
157 157
158 158 return q.all()
159 159
160 160 def mark_all_read_for_user(self, user, filter_=None):
161 161 user = self._get_user(user)
162 162 q = UserNotification.query()\
163 163 .filter(UserNotification.user == user)\
164 164 .filter(UserNotification.read == False)\
165 165 .join((Notification, UserNotification.notification_id ==
166 166 Notification.notification_id))
167 167 if filter_:
168 168 q = q.filter(Notification.type_ == filter_.get('type'))
169 169
170 170 # this is a little inefficient but sqlalchemy doesn't support
171 171 # update on joined tables :(
172 172 for obj in q.all():
173 173 obj.read = True
174 174 self.sa.add(obj)
175 175
176 176 def get_unread_cnt_for_user(self, user):
177 177 user = self._get_user(user)
178 178 return UserNotification.query()\
179 179 .filter(UserNotification.read == False)\
180 180 .filter(UserNotification.user == user).count()
181 181
182 182 def get_unread_for_user(self, user):
183 183 user = self._get_user(user)
184 184 return [x.notification for x in UserNotification.query()\
185 185 .filter(UserNotification.read == False)\
186 186 .filter(UserNotification.user == user).all()]
187 187
188 188 def get_user_notification(self, user, notification):
189 189 user = self._get_user(user)
190 190 notification = self.__get_notification(notification)
191 191
192 192 return UserNotification.query()\
193 193 .filter(UserNotification.notification == notification)\
194 194 .filter(UserNotification.user == user).scalar()
195 195
196 196 def make_description(self, notification, show_age=True):
197 197 """
198 198 Creates a human readable description based on properties
199 199 of notification object
200 200 """
201
201 #alias
202 _n = notification
202 203 _map = {
203 notification.TYPE_CHANGESET_COMMENT: _('commented on commit'),
204 notification.TYPE_MESSAGE: _('sent message'),
205 notification.TYPE_MENTION: _('mentioned you'),
206 notification.TYPE_REGISTRATION: _('registered in RhodeCode'),
207 notification.TYPE_PULL_REQUEST: _('opened new pull request')
204 _n.TYPE_CHANGESET_COMMENT: _('commented on commit'),
205 _n.TYPE_MESSAGE: _('sent message'),
206 _n.TYPE_MENTION: _('mentioned you'),
207 _n.TYPE_REGISTRATION: _('registered in RhodeCode'),
208 _n.TYPE_PULL_REQUEST: _('opened new pull request'),
209 _n.TYPE_PULL_REQUEST_COMMENT: _('commented on pull request')
208 210 }
209 211
210 212 tmpl = "%(user)s %(action)s %(when)s"
211 213 if show_age:
212 214 when = h.age(notification.created_on)
213 215 else:
214 216 DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
215 217 when = DTF(notification.created_on)
216 218
217 219 data = dict(
218 220 user=notification.created_by_user.username,
219 221 action=_map[notification.type_], when=when,
220 222 )
221 223 return tmpl % data
222 224
223 225
224 226 class EmailNotificationModel(BaseModel):
225 227
226 228 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
227 229 TYPE_PASSWORD_RESET = 'passoword_link'
228 230 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
229 231 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
230 232 TYPE_DEFAULT = 'default'
231 233
232 234 def __init__(self):
233 235 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
234 236 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
235 237
236 238 self.email_types = {
237 239 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
238 240 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
239 241 self.TYPE_REGISTRATION: 'email_templates/registration.html',
240 242 self.TYPE_DEFAULT: 'email_templates/default.html'
241 243 }
242 244
243 245 def get_email_tmpl(self, type_, **kwargs):
244 246 """
245 247 return generated template for email based on given type
246 248
247 249 :param type_:
248 250 """
249 251
250 252 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
251 253 email_template = self._tmpl_lookup.get_template(base)
252 254 # translator inject
253 255 _kwargs = {'_': _}
254 256 _kwargs.update(kwargs)
255 257 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
256 258 return email_template.render(**_kwargs)
@@ -1,251 +1,257 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Changelog') % c.repo_name} - ${c.rhodecode_name}
7 7 </%def>
8 8
9 9 <%def name="breadcrumbs_links()">
10 10 ${h.link_to(u'Home',h.url('/'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 13 &raquo;
14 14 <% size = c.size if c.size <= c.total_cs else c.total_cs %>
15 15 ${_('Changelog')} - ${ungettext('showing %d out of %d revision', 'showing %d out of %d revisions', size) % (size, c.total_cs)}
16 16 </%def>
17 17
18 18 <%def name="page_nav()">
19 19 ${self.menu('changelog')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <div class="box">
24 24 <!-- box / title -->
25 25 <div class="title">
26 26 ${self.breadcrumbs()}
27 27 </div>
28 28 <div class="table">
29 29 % if c.pagination:
30 30 <div id="graph">
31 31 <div id="graph_nodes">
32 32 <canvas id="graph_canvas"></canvas>
33 33 </div>
34 34 <div id="graph_content">
35 35 <div class="info_box" style="clear: both;padding: 10px 6px;vertical-align: right;text-align: right;">
36 36 %if c.rhodecode_db_repo.fork:
37 37 <a title="${_('compare fork with %s' % c.rhodecode_db_repo.fork.repo_name)}" href="${h.url('compare_url',repo_name=c.repo_name,org_ref_type='branch',org_ref='default',other_ref_type='branch',other_ref='default',repo=c.rhodecode_db_repo.fork.repo_name)}" class="ui-btn small">${_('Compare fork')}</a>
38 38 %endif
39 39 <a href="${h.url('pullrequest_home',repo_name=c.repo_name)}" class="ui-btn small">${_('Open new pull request')}</a>
40 40 </div>
41 41 <div class="container_header">
42 42 ${h.form(h.url.current(),method='get')}
43 43 <div class="info_box" style="float:left">
44 44 ${h.submit('set',_('Show'),class_="ui-btn")}
45 45 ${h.text('size',size=1,value=c.size)}
46 46 ${_('revisions')}
47 47 </div>
48 48 ${h.end_form()}
49 49 <div id="rev_range_container" style="display:none"></div>
50 50 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
51 51 </div>
52 52
53 53 %for cnt,cs in enumerate(c.pagination):
54 54 <div id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
55 55 <div class="left">
56 56 <div>
57 57 ${h.checkbox(cs.short_id,class_="changeset_range")}
58 58 <span class="tooltip" title="${h.tooltip(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>
59 59 </div>
60 60 <div class="author">
61 61 <div class="gravatar">
62 62 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
63 63 </div>
64 64 <div title="${cs.author}" class="user">${h.shorter(h.person(cs.author),22)}</div>
65 65 </div>
66 66 <div class="date">${h.fmt_date(cs.date)}</div>
67 67 </div>
68 68 <div class="mid">
69 69 <div class="message">${h.urlify_commit(cs.message, c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
70 70 <div class="expand"><span class="expandtext">&darr; ${_('show more')} &darr;</span></div>
71 71 </div>
72 72 <div class="right">
73 73 <div class="changes">
74 74 <div id="${cs.raw_id}" style="float:right;" class="changed_total tooltip" title="${h.tooltip(_('Affected number of files, click to show more details'))}">${len(cs.affected_files)}</div>
75 75 <div class="comments-container">
76 76 %if len(c.comments.get(cs.raw_id,[])) > 0:
77 77 <div class="comments-cnt" title="${('comments')}">
78 78 <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)}">
79 79 <div class="comments-cnt">${len(c.comments[cs.raw_id])}</div>
80 80 <img src="${h.url('/images/icons/comments.png')}">
81 81 </a>
82 82 </div>
83 83 %endif
84 84 </div>
85 85 <div class="changeset-status-container">
86 86 %if c.statuses.get(cs.raw_id):
87 87 <div title="${_('Changeset status')}" class="changeset-status-lbl">${c.statuses.get(cs.raw_id)[1]}</div>
88 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" /></div>
88 <div class="changeset-status-ico">
89 %if c.statuses.get(cs.raw_id)[2]:
90 <a class="tooltip" title="${_('Click to open associated pull request')}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" /></a>
91 %else:
92 <img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" />
93 %endif
94 </div>
89 95 %endif
90 96 </div>
91 97 </div>
92 98 %if cs.parents:
93 99 %for p_cs in reversed(cs.parents):
94 100 <div class="parent">${_('Parent')}
95 101 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
96 102 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
97 103 </div>
98 104 %endfor
99 105 %else:
100 106 <div class="parent">${_('No parents')}</div>
101 107 %endif
102 108
103 109 <span class="logtags">
104 110 %if len(cs.parents)>1:
105 111 <span class="merge">${_('merge')}</span>
106 112 %endif
107 113 %if cs.branch:
108 114 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
109 115 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
110 116 </span>
111 117 %endif
112 118 %if h.is_hg(c.rhodecode_repo):
113 119 %for book in cs.bookmarks:
114 120 <span class="bookbook" title="${'%s %s' % (_('bookmark'),book)}">
115 121 ${h.link_to(h.shorter(book),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
116 122 </span>
117 123 %endfor
118 124 %endif
119 125 %for tag in cs.tags:
120 126 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
121 127 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
122 128 %endfor
123 129 </span>
124 130 </div>
125 131 </div>
126 132
127 133 %endfor
128 134 <div class="pagination-wh pagination-left">
129 135 ${c.pagination.pager('$link_previous ~2~ $link_next')}
130 136 </div>
131 137 </div>
132 138 </div>
133 139
134 140 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
135 141 <script type="text/javascript">
136 142 YAHOO.util.Event.onDOMReady(function(){
137 143
138 144 //Monitor range checkboxes and build a link to changesets
139 145 //ranges
140 146 var checkboxes = YUD.getElementsByClassName('changeset_range');
141 147 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
142 148 YUE.on(checkboxes,'click',function(e){
143 149 var checked_checkboxes = [];
144 150 for (pos in checkboxes){
145 151 if(checkboxes[pos].checked){
146 152 checked_checkboxes.push(checkboxes[pos]);
147 153 }
148 154 }
149 155 if(checked_checkboxes.length>1){
150 156 var rev_end = checked_checkboxes[0].name;
151 157 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
152 158
153 159 var url = url_tmpl.replace('__REVRANGE__',
154 160 rev_start+'...'+rev_end);
155 161
156 162 var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
157 163 link = link.replace('__S',rev_start);
158 164 link = link.replace('__E',rev_end);
159 165 YUD.get('rev_range_container').innerHTML = link;
160 166 YUD.setStyle('rev_range_container','display','');
161 167 }
162 168 else{
163 169 YUD.setStyle('rev_range_container','display','none');
164 170
165 171 }
166 172 });
167 173
168 174 var msgs = YUQ('.message');
169 175 // get first element height
170 176 var el = YUQ('#graph_content .container')[0];
171 177 var row_h = el.clientHeight;
172 178 for(var i=0;i<msgs.length;i++){
173 179 var m = msgs[i];
174 180
175 181 var h = m.clientHeight;
176 182 var pad = YUD.getStyle(m,'padding');
177 183 if(h > row_h){
178 184 var offset = row_h - (h+12);
179 185 YUD.setStyle(m.nextElementSibling,'display','block');
180 186 YUD.setStyle(m.nextElementSibling,'margin-top',offset+'px');
181 187 };
182 188 }
183 189 YUE.on(YUQ('.expand'),'click',function(e){
184 190 var elem = e.currentTarget.parentNode.parentNode;
185 191 YUD.setStyle(e.currentTarget,'display','none');
186 192 YUD.setStyle(elem,'height','auto');
187 193
188 194 //redraw the graph, max_w and jsdata are global vars
189 195 set_canvas(max_w);
190 196
191 197 var r = new BranchRenderer();
192 198 r.render(jsdata,max_w);
193 199
194 200 })
195 201
196 202 // Fetch changeset details
197 203 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
198 204 var id = e.currentTarget.id
199 205 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
200 206 var url = url.replace('__CS__',id);
201 207 ypjax(url,id,function(){tooltip_activate()});
202 208 });
203 209
204 210 // change branch filter
205 211 YUE.on(YUD.get('branch_filter'),'change',function(e){
206 212 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
207 213 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
208 214 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
209 215 var url = url.replace('__BRANCH__',selected_branch);
210 216 if(selected_branch != ''){
211 217 window.location = url;
212 218 }else{
213 219 window.location = url_main;
214 220 }
215 221
216 222 });
217 223
218 224 function set_canvas(heads) {
219 225 var c = document.getElementById('graph_nodes');
220 226 var t = document.getElementById('graph_content');
221 227 canvas = document.getElementById('graph_canvas');
222 228 var div_h = t.clientHeight;
223 229 c.style.height=div_h+'px';
224 230 canvas.setAttribute('height',div_h);
225 231 c.style.height=max_w+'px';
226 232 canvas.setAttribute('width',max_w);
227 233 };
228 234 var heads = 1;
229 235 var max_heads = 0;
230 236 var jsdata = ${c.jsdata|n};
231 237
232 238 for( var i=0;i<jsdata.length;i++){
233 239 var m = Math.max.apply(Math, jsdata[i][1]);
234 240 if (m>max_heads){
235 241 max_heads = m;
236 242 }
237 243 }
238 244 var max_w = Math.max(100,max_heads*25);
239 245 set_canvas(max_w);
240 246
241 247 var r = new BranchRenderer();
242 248 r.render(jsdata,max_w);
243 249
244 250 });
245 251 </script>
246 252 %else:
247 253 ${_('There are no changes yet')}
248 254 %endif
249 255 </div>
250 256 </div>
251 257 </%def>
@@ -1,83 +1,83 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${h.link_to(u'Home',h.url('/'))}
9 9 &raquo;
10 10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
11 11 &raquo;
12 12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
13 13 </%def>
14 14
15 15 <%def name="main()">
16 16
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22
23 23 <h3>${_('Title')}: ${c.pull_request.title}</h3>
24 24 <div class="changeset-status-container" style="float:left;padding:0px 20px 20px 20px">
25 25 %if c.current_changeset_status:
26 26 <div title="${_('Changeset status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
27 27 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
28 28 %endif
29 29 </div>
30 30 <div style="padding:4px">
31 31 <div>${h.fmt_date(c.pull_request.created_on)}</div>
32 32 </div>
33 33
34 34 ##DIFF
35 35
36 36 <div class="table">
37 37 <div id="body" class="diffblock">
38 38 <div style="white-space:pre-wrap;padding:5px">${h.literal(c.pull_request.description)}</div>
39 39 </div>
40 40 <div id="changeset_compare_view_content">
41 41 ##CS
42 42 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Incoming changesets')}</div>
43 43 <%include file="/compare/compare_cs.html" />
44 44
45 45 ## FILES
46 46 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
47 47 <div class="cs_files">
48 48 %for fid, change, f, stat in c.files:
49 49 <div class="cs_${change}">
50 50 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
51 51 <div class="changes">${h.fancy_file_stats(stat)}</div>
52 52 </div>
53 53 %endfor
54 54 </div>
55 55 </div>
56 56 </div>
57 57 <script>
58 58 var _USERS_AC_DATA = ${c.users_array|n};
59 59 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
60 60 </script>
61 61
62 62 ## diff block
63 63 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
64 64 %for fid, change, f, stat in c.files:
65 65 ${diff_block.diff_block_simple([c.changes[fid]])}
66 66 %endfor
67 67
68 68 ## template for inline comment form
69 69 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
70 70 ##${comment.comment_inline_form(c.changeset)}
71 71
72 72 ## render comments main comments form and it status
73 ##${comment.comments(h.url('pull_request_comment', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id),
74 ## c.current_changeset_status)}
73 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id),
74 c.current_changeset_status)}
75 75
76 76 </div>
77 77
78 78 <script type="text/javascript">
79 79
80 80
81 81 </script>
82 82
83 83 </%def>
General Comments 0
You need to be logged in to leave comments. Login now