##// END OF EJS Templates
- pull request generates overview based on it's params...
marcink -
r2440:1bc579bc codereview
parent child Browse files
Show More
@@ -0,0 +1,31 b''
1 <%inherit file="/base/base.html"/>
2
3 <%def name="title()">
4 ${c.repo_name} ${_('All pull requests')}
5 </%def>
6
7 <%def name="breadcrumbs_links()">
8 ${h.link_to(u'Home',h.url('/'))}
9 &raquo;
10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
11 &raquo;
12 ${_('All pull requests')}
13 </%def>
14
15 <%def name="main()">
16
17 <div class="box">
18 <!-- box / title -->
19 <div class="title">
20 ${self.breadcrumbs()}
21 </div>
22
23 %for pr in c.pull_requests:
24 <a href="${h.url('pullrequest_show',repo_name=c.repo_name,pull_request_id=pr.pull_request_id)}">#${pr.pull_request_id}</a>
25 %endfor
26
27 </div>
28
29 <script type="text/javascript"></script>
30
31 </%def>
@@ -1,546 +1,552 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 rmap.connect('pullrequest_show_all',
455 '/{repo_name:.*}/pull-request',
456 controller='pullrequests',
457 action='show_all', conditions=dict(function=check_repo,
458 method=["GET"]))
459
454 460 rmap.connect('summary_home', '/{repo_name:.*}/summary',
455 461 controller='summary', conditions=dict(function=check_repo))
456 462
457 463 rmap.connect('shortlog_home', '/{repo_name:.*}/shortlog',
458 464 controller='shortlog', conditions=dict(function=check_repo))
459 465
460 466 rmap.connect('branches_home', '/{repo_name:.*}/branches',
461 467 controller='branches', conditions=dict(function=check_repo))
462 468
463 469 rmap.connect('tags_home', '/{repo_name:.*}/tags',
464 470 controller='tags', conditions=dict(function=check_repo))
465 471
466 472 rmap.connect('bookmarks_home', '/{repo_name:.*}/bookmarks',
467 473 controller='bookmarks', conditions=dict(function=check_repo))
468 474
469 475 rmap.connect('changelog_home', '/{repo_name:.*}/changelog',
470 476 controller='changelog', conditions=dict(function=check_repo))
471 477
472 478 rmap.connect('changelog_details', '/{repo_name:.*}/changelog_details/{cs}',
473 479 controller='changelog', action='changelog_details',
474 480 conditions=dict(function=check_repo))
475 481
476 482 rmap.connect('files_home', '/{repo_name:.*}/files/{revision}/{f_path:.*}',
477 483 controller='files', revision='tip', f_path='',
478 484 conditions=dict(function=check_repo))
479 485
480 486 rmap.connect('files_diff_home', '/{repo_name:.*}/diff/{f_path:.*}',
481 487 controller='files', action='diff', revision='tip', f_path='',
482 488 conditions=dict(function=check_repo))
483 489
484 490 rmap.connect('files_rawfile_home',
485 491 '/{repo_name:.*}/rawfile/{revision}/{f_path:.*}',
486 492 controller='files', action='rawfile', revision='tip',
487 493 f_path='', conditions=dict(function=check_repo))
488 494
489 495 rmap.connect('files_raw_home',
490 496 '/{repo_name:.*}/raw/{revision}/{f_path:.*}',
491 497 controller='files', action='raw', revision='tip', f_path='',
492 498 conditions=dict(function=check_repo))
493 499
494 500 rmap.connect('files_annotate_home',
495 501 '/{repo_name:.*}/annotate/{revision}/{f_path:.*}',
496 502 controller='files', action='index', revision='tip',
497 503 f_path='', annotate=True, conditions=dict(function=check_repo))
498 504
499 505 rmap.connect('files_edit_home',
500 506 '/{repo_name:.*}/edit/{revision}/{f_path:.*}',
501 507 controller='files', action='edit', revision='tip',
502 508 f_path='', conditions=dict(function=check_repo))
503 509
504 510 rmap.connect('files_add_home',
505 511 '/{repo_name:.*}/add/{revision}/{f_path:.*}',
506 512 controller='files', action='add', revision='tip',
507 513 f_path='', conditions=dict(function=check_repo))
508 514
509 515 rmap.connect('files_archive_home', '/{repo_name:.*}/archive/{fname}',
510 516 controller='files', action='archivefile',
511 517 conditions=dict(function=check_repo))
512 518
513 519 rmap.connect('files_nodelist_home',
514 520 '/{repo_name:.*}/nodelist/{revision}/{f_path:.*}',
515 521 controller='files', action='nodelist',
516 522 conditions=dict(function=check_repo))
517 523
518 524 rmap.connect('repo_settings_delete', '/{repo_name:.*}/settings',
519 525 controller='settings', action="delete",
520 526 conditions=dict(method=["DELETE"], function=check_repo))
521 527
522 528 rmap.connect('repo_settings_update', '/{repo_name:.*}/settings',
523 529 controller='settings', action="update",
524 530 conditions=dict(method=["PUT"], function=check_repo))
525 531
526 532 rmap.connect('repo_settings_home', '/{repo_name:.*}/settings',
527 533 controller='settings', action='index',
528 534 conditions=dict(function=check_repo))
529 535
530 536 rmap.connect('repo_fork_create_home', '/{repo_name:.*}/fork',
531 537 controller='forks', action='fork_create',
532 538 conditions=dict(function=check_repo, method=["POST"]))
533 539
534 540 rmap.connect('repo_fork_home', '/{repo_name:.*}/fork',
535 541 controller='forks', action='fork',
536 542 conditions=dict(function=check_repo))
537 543
538 544 rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
539 545 controller='forks', action='forks',
540 546 conditions=dict(function=check_repo))
541 547
542 548 rmap.connect('repo_followers_home', '/{repo_name:.*}/followers',
543 549 controller='followers', action='followers',
544 550 conditions=dict(function=check_repo))
545 551
546 552 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 for _, lines in c.inline_comments:
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 393 revision,
394 394 status,
395 395 c.rhodecode_user.user_id,
396 396 comm,
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,133 +1,257 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
29 from webob.exc import HTTPNotFound
27 30
28 31 from pylons import request, response, session, tmpl_context as c, url
29 32 from pylons.controllers.util import abort, redirect
30 33 from pylons.i18n.translation import _
31 34
32 35 from rhodecode.lib.base import BaseRepoController, render
33 36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 37 from rhodecode.lib import helpers as h
35 from rhodecode.model.db import User, PullRequest
38 from rhodecode.lib import diffs
39 from rhodecode.model.db import User, PullRequest, Repository, ChangesetStatus
36 40 from rhodecode.model.pull_request import PullRequestModel
37 41 from rhodecode.model.meta import Session
42 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.comment import ChangesetCommentsModel
44 from rhodecode.model.changeset_status import ChangesetStatusModel
38 45
39 46 log = logging.getLogger(__name__)
40 47
41 48
42 49 class PullrequestsController(BaseRepoController):
43 50
44 51 @LoginRequired()
45 52 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
46 53 'repository.admin')
47 54 def __before__(self):
48 55 super(PullrequestsController, self).__before__()
49 56
50 57 def _get_repo_refs(self, repo):
51 58 hist_l = []
52 59
53 branches_group = ([('branch:' + k, k) for k in repo.branches.keys()],
54 _("Branches"))
55 bookmarks_group = ([('book:' + k, k) for k in repo.bookmarks.keys()],
56 _("Bookmarks"))
57 tags_group = ([('tag:' + k, k) for k in repo.tags.keys()],
58 _("Tags"))
60 branches_group = ([('branch:%s:%s' % (k, v), k) for
61 k, v in repo.branches.iteritems()], _("Branches"))
62 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
63 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
64 tags_group = ([('tag:%s:%s' % (k, v), k) for
65 k, v in repo.tags.iteritems()], _("Tags"))
59 66
60 67 hist_l.append(bookmarks_group)
61 68 hist_l.append(branches_group)
62 69 hist_l.append(tags_group)
63 70
64 71 return hist_l
65 72
73 def show_all(self, repo_name):
74 c.pull_requests = PullRequestModel().get_all(repo_name)
75 c.repo_name = repo_name
76 return render('/pullrequests/pullrequest_show_all.html')
77
66 78 def index(self):
67 79 org_repo = c.rhodecode_db_repo
68 80 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
69 81 c.org_repos = []
70 82 c.other_repos = []
71 83 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
72 84 org_repo.user.username, c.repo_name))
73 85 )
74 86
75 87 c.other_refs = c.org_refs
76 88 c.other_repos.extend(c.org_repos)
77 89 c.default_pull_request = org_repo.repo_name
78 90 #gather forks and add to this list
79 91 for fork in org_repo.forks:
80 92 c.other_repos.append((fork.repo_name, '%s/%s' % (
81 93 fork.user.username, fork.repo_name))
82 94 )
83 95 #add parents of this fork also
84 96 if org_repo.parent:
85 97 c.default_pull_request = org_repo.parent.repo_name
86 98 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
87 99 org_repo.parent.user.username,
88 100 org_repo.parent.repo_name))
89 101 )
90 102
91 103 #TODO: maybe the owner should be default ?
92 104 c.review_members = []
93 105 c.available_members = []
94 106 for u in User.query().filter(User.username != 'default').all():
95 107 uname = u.username
96 108 if org_repo.user == u:
97 109 uname = _('%s (owner)' % u.username)
98 110 # auto add owner to pull-request recipients
99 111 c.review_members.append([u.user_id, uname])
100 112 c.available_members.append([u.user_id, uname])
101 113 return render('/pullrequests/pullrequest.html')
102 114
103 115 def create(self, repo_name):
104 116 req_p = request.POST
105 117 org_repo = req_p['org_repo']
106 118 org_ref = req_p['org_ref']
107 119 other_repo = req_p['other_repo']
108 120 other_ref = req_p['other_ref']
109 121 revisions = req_p.getall('revisions')
110 122 reviewers = req_p.getall('review_members')
111 123 #TODO: wrap this into a FORM !!!
112 124
113 125 title = req_p['pullrequest_title']
114 126 description = req_p['pullrequest_desc']
115 127
116 128 try:
117 129 model = PullRequestModel()
118 130 model.create(self.rhodecode_user.user_id, org_repo,
119 131 org_ref, other_repo, other_ref, revisions,
120 132 reviewers, title, description)
121 133 Session.commit()
122 134 h.flash(_('Pull request send'), category='success')
123 135 except Exception:
124 136 raise
125 137 h.flash(_('Error occured during sending pull request'),
126 138 category='error')
127 139 log.error(traceback.format_exc())
128 140
129 141 return redirect(url('changelog_home', repo_name=repo_name))
130 142
143 def _get_changesets(self, org_repo, org_ref, other_repo, other_ref, tmp):
144 changesets = []
145 #case two independent repos
146 if org_repo != other_repo:
147 common, incoming, rheads = tmp
148
149 if not incoming:
150 revs = []
151 else:
152 revs = org_repo._repo.changelog.findmissing(common, rheads)
153
154 for cs in reversed(map(binascii.hexlify, revs)):
155 changesets.append(org_repo.get_changeset(cs))
156 else:
157 revs = ['ancestors(%s) and not ancestors(%s)' % (org_ref[1],
158 other_ref[1])]
159 from mercurial import scmutil
160 out = scmutil.revrange(org_repo._repo, revs)
161 for cs in reversed(out):
162 changesets.append(org_repo.get_changeset(cs))
163
164 return changesets
165
166 def _get_discovery(self, org_repo, org_ref, other_repo, other_ref):
167 from mercurial import discovery
168 other = org_repo._repo
169 repo = other_repo._repo
170 tip = other[org_ref[1]]
171 log.debug('Doing discovery for %s@%s vs %s@%s' % (
172 org_repo, org_ref, other_repo, other_ref)
173 )
174 log.debug('Filter heads are %s[%s]' % (tip, org_ref[1]))
175 tmp = discovery.findcommonincoming(
176 repo=repo, # other_repo we check for incoming
177 remote=other, # org_repo source for incoming
178 heads=[tip.node()],
179 force=False
180 )
181 return tmp
182
183 def _compare(self, pull_request):
184
185 org_repo = pull_request.org_repo
186 org_ref_type, org_ref_, org_ref = pull_request.org_ref.split(':')
187 other_repo = pull_request.other_repo
188 other_ref_type, other_ref, other_ref_ = pull_request.other_ref.split(':')
189
190 org_ref = (org_ref_type, org_ref)
191 other_ref = (other_ref_type, other_ref)
192
193 c.org_repo = org_repo
194 c.other_repo = other_repo
195
196 discovery_data = self._get_discovery(org_repo.scm_instance,
197 org_ref,
198 other_repo.scm_instance,
199 other_ref)
200 c.cs_ranges = self._get_changesets(org_repo.scm_instance,
201 org_ref,
202 other_repo.scm_instance,
203 other_ref,
204 discovery_data)
205
206 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
207 c.cs_ranges])
208 # defines that we need hidden inputs with changesets
209 c.as_form = request.GET.get('as_form', False)
210 if request.environ.get('HTTP_X_PARTIAL_XHR'):
211 return render('compare/compare_cs.html')
212
213 c.org_ref = org_ref[1]
214 c.other_ref = other_ref[1]
215 # diff needs to have swapped org with other to generate proper diff
216 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
217 discovery_data)
218 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
219 _parsed = diff_processor.prepare()
220
221 c.files = []
222 c.changes = {}
223
224 for f in _parsed:
225 fid = h.FID('', f['filename'])
226 c.files.append([fid, f['operation'], f['filename'], f['stats']])
227 diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
228 c.changes[fid] = [f['operation'], f['filename'], diff]
229
131 230 def show(self, repo_name, pull_request_id):
231 repo_model = RepoModel()
232 c.users_array = repo_model.get_users_js()
233 c.users_groups_array = repo_model.get_users_groups_js()
132 234 c.pull_request = PullRequest.get(pull_request_id)
235 ##TODO: need more generic solution
236 self._compare(c.pull_request)
237
238 # inline comments
239 c.inline_cnt = 0
240 c.inline_comments = ChangesetCommentsModel()\
241 .get_inline_comments(c.rhodecode_db_repo.repo_id,
242 pull_request=pull_request_id)
243 # count inline comments
244 for __, lines in c.inline_comments:
245 for comments in lines.values():
246 c.inline_cnt += len(comments)
247 # comments
248 c.comments = ChangesetCommentsModel()\
249 .get_comments(c.rhodecode_db_repo.repo_id,
250 pull_request=pull_request_id)
251
252 # changeset(pull-request) statuse
253 c.current_changeset_status = ChangesetStatusModel()\
254 .get_status(c.rhodecode_db_repo.repo_id,
255 pull_request=pull_request_id)
256 c.changeset_statuses = ChangesetStatus.STATUSES
133 257 return render('/pullrequests/pullrequest_show.html')
@@ -1,217 +1,219 b''
1 1 """The base Controller API
2 2
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 6 import time
7 7 import traceback
8 8
9 9 from paste.auth.basic import AuthBasicAuthenticator
10 10 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
11 11 from paste.httpheaders import WWW_AUTHENTICATE
12 12
13 13 from pylons import config, tmpl_context as c, request, session, url
14 14 from pylons.controllers import WSGIController
15 15 from pylons.controllers.util import redirect
16 16 from pylons.templating import render_mako as render
17 17
18 18 from rhodecode import __version__, BACKENDS
19 19
20 20 from rhodecode.lib.utils2 import str2bool, safe_unicode
21 21 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
22 22 HasPermissionAnyMiddleware, CookieStoreWrapper
23 23 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
24 24 from rhodecode.model import meta
25 25
26 26 from rhodecode.model.db import Repository
27 27 from rhodecode.model.notification import NotificationModel
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 def _get_ip_addr(environ):
34 34 proxy_key = 'HTTP_X_REAL_IP'
35 35 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
36 36 def_key = 'REMOTE_ADDR'
37 37
38 38 return environ.get(proxy_key2,
39 39 environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
40 40 )
41 41
42 42
43 43 class BasicAuth(AuthBasicAuthenticator):
44 44
45 45 def __init__(self, realm, authfunc, auth_http_code=None):
46 46 self.realm = realm
47 47 self.authfunc = authfunc
48 48 self._rc_auth_http_code = auth_http_code
49 49
50 50 def build_authentication(self):
51 51 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
52 52 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
53 53 # return 403 if alternative http return code is specified in
54 54 # RhodeCode config
55 55 return HTTPForbidden(headers=head)
56 56 return HTTPUnauthorized(headers=head)
57 57
58 58
59 59 class BaseVCSController(object):
60 60
61 61 def __init__(self, application, config):
62 62 self.application = application
63 63 self.config = config
64 64 # base path of repo locations
65 65 self.basepath = self.config['base_path']
66 66 #authenticate this mercurial request using authfunc
67 67 self.authenticate = BasicAuth('', authfunc,
68 68 config.get('auth_ret_code'))
69 69 self.ipaddr = '0.0.0.0'
70 70
71 71 def _handle_request(self, environ, start_response):
72 72 raise NotImplementedError()
73 73
74 74 def _get_by_id(self, repo_name):
75 75 """
76 76 Get's a special pattern _<ID> from clone url and tries to replace it
77 77 with a repository_name for support of _<ID> non changable urls
78 78
79 79 :param repo_name:
80 80 """
81 81 try:
82 82 data = repo_name.split('/')
83 83 if len(data) >= 2:
84 84 by_id = data[1].split('_')
85 85 if len(by_id) == 2 and by_id[1].isdigit():
86 86 _repo_name = Repository.get(by_id[1]).repo_name
87 87 data[1] = _repo_name
88 88 except:
89 89 log.debug('Failed to extract repo_name from id %s' % (
90 90 traceback.format_exc()
91 91 )
92 92 )
93 93
94 94 return '/'.join(data)
95 95
96 96 def _invalidate_cache(self, repo_name):
97 97 """
98 98 Set's cache for this repository for invalidation on next access
99 99
100 100 :param repo_name: full repo name, also a cache key
101 101 """
102 102 invalidate_cache('get_repo_cached_%s' % repo_name)
103 103
104 104 def _check_permission(self, action, user, repo_name):
105 105 """
106 106 Checks permissions using action (push/pull) user and repository
107 107 name
108 108
109 109 :param action: push or pull action
110 110 :param user: user instance
111 111 :param repo_name: repository name
112 112 """
113 113 if action == 'push':
114 114 if not HasPermissionAnyMiddleware('repository.write',
115 115 'repository.admin')(user,
116 116 repo_name):
117 117 return False
118 118
119 119 else:
120 120 #any other action need at least read permission
121 121 if not HasPermissionAnyMiddleware('repository.read',
122 122 'repository.write',
123 123 'repository.admin')(user,
124 124 repo_name):
125 125 return False
126 126
127 127 return True
128 128
129 129 def _get_ip_addr(self, environ):
130 130 return _get_ip_addr(environ)
131 131
132 132 def __call__(self, environ, start_response):
133 133 start = time.time()
134 134 try:
135 135 return self._handle_request(environ, start_response)
136 136 finally:
137 137 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
138 138 log.debug('Request time: %.3fs' % (time.time() - start))
139 139 meta.Session.remove()
140 140
141 141
142 142 class BaseController(WSGIController):
143 143
144 144 def __before__(self):
145 145 c.rhodecode_version = __version__
146 146 c.rhodecode_instanceid = config.get('instance_id')
147 147 c.rhodecode_name = config.get('rhodecode_title')
148 148 c.use_gravatar = str2bool(config.get('use_gravatar'))
149 149 c.ga_code = config.get('rhodecode_ga_code')
150 150 c.repo_name = get_repo_slug(request)
151 151 c.backends = BACKENDS.keys()
152 152 c.unread_notifications = NotificationModel()\
153 153 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
154 154 self.cut_off_limit = int(config.get('cut_off_limit'))
155 155
156 156 self.sa = meta.Session
157 157 self.scm_model = ScmModel(self.sa)
158 158 self.ip_addr = ''
159 159
160 160 def __call__(self, environ, start_response):
161 161 """Invoke the Controller"""
162 162 # WSGIController.__call__ dispatches to the Controller method
163 163 # the request is routed to. This routing information is
164 164 # available in environ['pylons.routes_dict']
165 165 start = time.time()
166 166 try:
167 167 self.ip_addr = _get_ip_addr(environ)
168 168 # make sure that we update permissions each time we call controller
169 169 api_key = request.GET.get('api_key')
170 170 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
171 171 user_id = cookie_store.get('user_id', None)
172 172 username = get_container_username(environ, config)
173 173 auth_user = AuthUser(user_id, api_key, username)
174 174 request.user = auth_user
175 175 self.rhodecode_user = c.rhodecode_user = auth_user
176 176 if not self.rhodecode_user.is_authenticated and \
177 177 self.rhodecode_user.user_id is not None:
178 178 self.rhodecode_user.set_authenticated(
179 179 cookie_store.get('is_authenticated')
180 180 )
181 181 log.info('User: %s accessed %s' % (
182 182 auth_user, safe_unicode(environ.get('PATH_INFO')))
183 183 )
184 184 return WSGIController.__call__(self, environ, start_response)
185 185 finally:
186 186 log.info('Request to %s time: %.3fs' % (
187 187 safe_unicode(environ.get('PATH_INFO')), time.time() - start)
188 188 )
189 189 meta.Session.remove()
190 190
191 191
192 192 class BaseRepoController(BaseController):
193 193 """
194 194 Base class for controllers responsible for loading all needed data for
195 195 repository loaded items are
196 196
197 197 c.rhodecode_repo: instance of scm repository
198 198 c.rhodecode_db_repo: instance of db
199 199 c.repository_followers: number of followers
200 200 c.repository_forks: number of forks
201 201 """
202 202
203 203 def __before__(self):
204 204 super(BaseRepoController, self).__before__()
205 205 if c.repo_name:
206 206
207 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
207 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
208 208 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
209 209
210 210 if c.rhodecode_repo is None:
211 211 log.error('%s this repository is present in database but it '
212 212 'cannot be created as an scm instance', c.repo_name)
213 213
214 214 redirect(url('home'))
215 215
216 c.repository_followers = self.scm_model.get_followers(c.repo_name)
217 c.repository_forks = self.scm_model.get_forks(c.repo_name)
216 # some globals counter for menu
217 c.repository_followers = self.scm_model.get_followers(dbr)
218 c.repository_forks = self.scm_model.get_forks(dbr)
219 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr) No newline at end of file
@@ -1,94 +1,108 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 from rhodecode.model.db import ChangesetStatus
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 def get_status(self, repo, revision):
39 def __get_pull_request(self, pull_request):
40 return self._get_instance(PullRequest, pull_request)
41
42 def get_status(self, repo, revision=None, pull_request=None):
40 43 """
41 Returns status of changeset for given revision and version 0
42 versioning makes a history of statuses, and version == 0 is always the
43 current one
44 Returns latest status of changeset for given revision or for given
45 pull request. Statuses are versioned inside a table itself and
46 version == 0 is always the current one
44 47
45 48 :param repo:
46 49 :type repo:
47 :param revision: 40char hash
50 :param revision: 40char hash or None
48 51 :type revision: str
52 :param pull_request: pull_request reference
53 :type:
49 54 """
50 55 repo = self._get_repo(repo)
51 56
52 status = ChangesetStatus.query()\
57 q = ChangesetStatus.query()\
53 58 .filter(ChangesetStatus.repo == repo)\
54 .filter(ChangesetStatus.revision == revision)\
55 .filter(ChangesetStatus.version == 0).scalar()
59 .filter(ChangesetStatus.version == 0)
60
61 if revision:
62 q = q.filter(ChangesetStatus.revision == revision)
63 elif pull_request:
64 pull_request = self.__get_pull_request(pull_request)
65 q = q.filter(ChangesetStatus.pull_request == pull_request)
66 else:
67 raise Exception('Please specify revision or pull_request')
68
69 status = q.scalar()
56 70 status = status.status if status else status
57 71 st = status or ChangesetStatus.DEFAULT
58 72 return str(st)
59 73
60 74 def set_status(self, repo, revision, status, user, comment):
61 75 """
62 76 Creates new status for changeset or updates the old ones bumping their
63 77 version, leaving the current status at
64 78
65 79 :param repo:
66 80 :type repo:
67 81 :param revision:
68 82 :type revision:
69 83 :param status:
70 84 :type status:
71 85 :param user:
72 86 :type user:
73 87 :param comment:
74 88 :type comment:
75 89 """
76 90 repo = self._get_repo(repo)
77 91
78 92 cur_statuses = ChangesetStatus.query()\
79 93 .filter(ChangesetStatus.repo == repo)\
80 94 .filter(ChangesetStatus.revision == revision)\
81 95 .all()
82 96 if cur_statuses:
83 97 for st in cur_statuses:
84 98 st.version += 1
85 99 self.sa.add(st)
86 100 new_status = ChangesetStatus()
87 101 new_status.author = self._get_user(user)
88 102 new_status.repo = self._get_repo(repo)
89 103 new_status.status = status
90 104 new_status.revision = revision
91 105 new_status.comment = comment
92 106 self.sa.add(new_status)
93 107 return new_status
94 108
@@ -1,181 +1,188 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 from rhodecode.model.db import ChangesetComment, User, Repository, Notification
35 from rhodecode.model.db import ChangesetComment, User, Repository, \
36 Notification, PullRequest
36 37 from rhodecode.model.notification import NotificationModel
37 38
38 39 log = logging.getLogger(__name__)
39 40
40 41
41 42 class ChangesetCommentsModel(BaseModel):
42 43
43 44 def __get_changeset_comment(self, changeset_comment):
44 45 return self._get_instance(ChangesetComment, changeset_comment)
45 46
47 def __get_pull_request(self, pull_request):
48 return self._get_instance(PullRequest, pull_request)
49
46 50 def _extract_mentions(self, s):
47 51 user_objects = []
48 52 for username in extract_mentioned_users(s):
49 53 user_obj = User.get_by_username(username, case_insensitive=True)
50 54 if user_obj:
51 55 user_objects.append(user_obj)
52 56 return user_objects
53 57
54 58 def create(self, text, repo_id, user_id, revision, f_path=None,
55 59 line_no=None, status_change=None):
56 60 """
57 61 Creates new comment for changeset. IF status_change is not none
58 62 this comment is associated with a status change of changeset
59 63
60 64 :param text:
61 65 :param repo_id:
62 66 :param user_id:
63 67 :param revision:
64 68 :param f_path:
65 69 :param line_no:
66 70 :param status_change:
67 71 """
68 72
69 73 if text:
70 74 repo = Repository.get(repo_id)
71 75 cs = repo.scm_instance.get_changeset(revision)
72 76 desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
73 77 author_email = cs.author_email
74 78 comment = ChangesetComment()
75 79 comment.repo = repo
76 80 comment.user_id = user_id
77 81 comment.revision = revision
78 82 comment.text = text
79 83 comment.f_path = f_path
80 84 comment.line_no = line_no
81 85
82 86 self.sa.add(comment)
83 87 self.sa.flush()
84 88 # make notification
85 89 line = ''
86 90 if line_no:
87 91 line = _('on line %s') % line_no
88 92 subj = safe_unicode(
89 93 h.link_to('Re commit: %(commit_desc)s %(line)s' % \
90 94 {'commit_desc': desc, 'line': line},
91 95 h.url('changeset_home', repo_name=repo.repo_name,
92 96 revision=revision,
93 97 anchor='comment-%s' % comment.comment_id,
94 98 qualified=True,
95 99 )
96 100 )
97 101 )
98 102
99 103 body = text
100 104
101 105 # get the current participants of this changeset
102 106 recipients = ChangesetComment.get_users(revision=revision)
103 107
104 108 # add changeset author if it's in rhodecode system
105 109 recipients += [User.get_by_email(author_email)]
106 110
107 111 # create notification objects, and emails
108 112 NotificationModel().create(
109 113 created_by=user_id, subject=subj, body=body,
110 114 recipients=recipients, type_=Notification.TYPE_CHANGESET_COMMENT,
111 115 email_kwargs={'status_change': status_change}
112 116 )
113 117
114 118 mention_recipients = set(self._extract_mentions(body))\
115 119 .difference(recipients)
116 120 if mention_recipients:
117 121 subj = _('[Mention]') + ' ' + subj
118 122 NotificationModel().create(
119 123 created_by=user_id, subject=subj, body=body,
120 124 recipients=mention_recipients,
121 125 type_=Notification.TYPE_CHANGESET_COMMENT,
122 126 email_kwargs={'status_change': status_change}
123 127 )
124 128
125 129 return comment
126 130
127 131 def delete(self, comment):
128 132 """
129 133 Deletes given comment
130 134
131 135 :param comment_id:
132 136 """
133 137 comment = self.__get_changeset_comment(comment)
134 138 self.sa.delete(comment)
135 139
136 140 return comment
137 141
138 def get_comments(self, repo_id, revision=None, pull_request_id=None):
142 def get_comments(self, repo_id, revision=None, pull_request=None):
139 143 """
140 144 Get's main comments based on revision or pull_request_id
141 145
142 146 :param repo_id:
143 147 :type repo_id:
144 148 :param revision:
145 149 :type revision:
146 :param pull_request_id:
147 :type pull_request_id:
150 :param pull_request:
151 :type pull_request:
148 152 """
153
149 154 q = ChangesetComment.query()\
150 155 .filter(ChangesetComment.repo_id == repo_id)\
151 156 .filter(ChangesetComment.line_no == None)\
152 157 .filter(ChangesetComment.f_path == None)
153 158 if revision:
154 159 q = q.filter(ChangesetComment.revision == revision)
155 elif pull_request_id:
156 q = q.filter(ChangesetComment.pull_request_id == pull_request_id)
160 elif pull_request:
161 pull_request = self.__get_pull_request(pull_request)
162 q = q.filter(ChangesetComment.pull_request == pull_request)
157 163 else:
158 raise Exception('Please specify revision or pull_request_id')
164 raise Exception('Please specify revision or pull_request')
159 165 return q.all()
160 166
161 def get_inline_comments(self, repo_id, revision=None, pull_request_id=None):
167 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
162 168 q = self.sa.query(ChangesetComment)\
163 169 .filter(ChangesetComment.repo_id == repo_id)\
164 170 .filter(ChangesetComment.line_no != None)\
165 171 .filter(ChangesetComment.f_path != None)\
166 172 .order_by(ChangesetComment.comment_id.asc())\
167 173
168 174 if revision:
169 175 q = q.filter(ChangesetComment.revision == revision)
170 elif pull_request_id:
171 q = q.filter(ChangesetComment.pull_request_id == pull_request_id)
176 elif pull_request:
177 pull_request = self.__get_pull_request(pull_request)
178 q = q.filter(ChangesetComment.pull_request == pull_request)
172 179 else:
173 180 raise Exception('Please specify revision or pull_request_id')
174 181
175 182 comments = q.all()
176 183
177 184 paths = defaultdict(lambda: defaultdict(list))
178 185
179 186 for co in comments:
180 187 paths[co.f_path][co.line_no].append(co)
181 188 return paths.items()
@@ -1,1527 +1,1532 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 769 grouped[stat.revision] = [str(stat.status), stat.status_lbl]
770 770 return grouped
771 771
772 772 #==========================================================================
773 773 # SCM CACHE INSTANCE
774 774 #==========================================================================
775 775
776 776 @property
777 777 def invalidate(self):
778 778 return CacheInvalidation.invalidate(self.repo_name)
779 779
780 780 def set_invalidate(self):
781 781 """
782 782 set a cache for invalidation for this instance
783 783 """
784 784 CacheInvalidation.set_invalidate(self.repo_name)
785 785
786 786 @LazyProperty
787 787 def scm_instance(self):
788 788 return self.__get_instance()
789 789
790 790 def scm_instance_cached(self, cache_map=None):
791 791 @cache_region('long_term')
792 792 def _c(repo_name):
793 793 return self.__get_instance()
794 794 rn = self.repo_name
795 795 log.debug('Getting cached instance of repo')
796 796
797 797 if cache_map:
798 798 # get using prefilled cache_map
799 799 invalidate_repo = cache_map[self.repo_name]
800 800 if invalidate_repo:
801 801 invalidate_repo = (None if invalidate_repo.cache_active
802 802 else invalidate_repo)
803 803 else:
804 804 # get from invalidate
805 805 invalidate_repo = self.invalidate
806 806
807 807 if invalidate_repo is not None:
808 808 region_invalidate(_c, None, rn)
809 809 # update our cache
810 810 CacheInvalidation.set_valid(invalidate_repo.cache_key)
811 811 return _c(rn)
812 812
813 813 def __get_instance(self):
814 814 repo_full_path = self.repo_full_path
815 815 try:
816 816 alias = get_scm(repo_full_path)[0]
817 817 log.debug('Creating instance of %s repository' % alias)
818 818 backend = get_backend(alias)
819 819 except VCSError:
820 820 log.error(traceback.format_exc())
821 821 log.error('Perhaps this repository is in db and not in '
822 822 'filesystem run rescan repositories with '
823 823 '"destroy old data " option from admin panel')
824 824 return
825 825
826 826 if alias == 'hg':
827 827
828 828 repo = backend(safe_str(repo_full_path), create=False,
829 829 baseui=self._ui)
830 830 # skip hidden web repository
831 831 if repo._get_hidden():
832 832 return
833 833 else:
834 834 repo = backend(repo_full_path, create=False)
835 835
836 836 return repo
837 837
838 838
839 839 class RepoGroup(Base, BaseModel):
840 840 __tablename__ = 'groups'
841 841 __table_args__ = (
842 842 UniqueConstraint('group_name', 'group_parent_id'),
843 843 CheckConstraint('group_id != group_parent_id'),
844 844 {'extend_existing': True, 'mysql_engine': 'InnoDB',
845 845 'mysql_charset': 'utf8'},
846 846 )
847 847 __mapper_args__ = {'order_by': 'group_name'}
848 848
849 849 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
850 850 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
851 851 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
852 852 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
853 853
854 854 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
855 855 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
856 856
857 857 parent_group = relationship('RepoGroup', remote_side=group_id)
858 858
859 859 def __init__(self, group_name='', parent_group=None):
860 860 self.group_name = group_name
861 861 self.parent_group = parent_group
862 862
863 863 def __unicode__(self):
864 864 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
865 865 self.group_name)
866 866
867 867 @classmethod
868 868 def groups_choices(cls):
869 869 from webhelpers.html import literal as _literal
870 870 repo_groups = [('', '')]
871 871 sep = ' &raquo; '
872 872 _name = lambda k: _literal(sep.join(k))
873 873
874 874 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
875 875 for x in cls.query().all()])
876 876
877 877 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
878 878 return repo_groups
879 879
880 880 @classmethod
881 881 def url_sep(cls):
882 882 return URL_SEP
883 883
884 884 @classmethod
885 885 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
886 886 if case_insensitive:
887 887 gr = cls.query()\
888 888 .filter(cls.group_name.ilike(group_name))
889 889 else:
890 890 gr = cls.query()\
891 891 .filter(cls.group_name == group_name)
892 892 if cache:
893 893 gr = gr.options(FromCache(
894 894 "sql_cache_short",
895 895 "get_group_%s" % _hash_key(group_name)
896 896 )
897 897 )
898 898 return gr.scalar()
899 899
900 900 @property
901 901 def parents(self):
902 902 parents_recursion_limit = 5
903 903 groups = []
904 904 if self.parent_group is None:
905 905 return groups
906 906 cur_gr = self.parent_group
907 907 groups.insert(0, cur_gr)
908 908 cnt = 0
909 909 while 1:
910 910 cnt += 1
911 911 gr = getattr(cur_gr, 'parent_group', None)
912 912 cur_gr = cur_gr.parent_group
913 913 if gr is None:
914 914 break
915 915 if cnt == parents_recursion_limit:
916 916 # this will prevent accidental infinit loops
917 917 log.error('group nested more than %s' %
918 918 parents_recursion_limit)
919 919 break
920 920
921 921 groups.insert(0, gr)
922 922 return groups
923 923
924 924 @property
925 925 def children(self):
926 926 return RepoGroup.query().filter(RepoGroup.parent_group == self)
927 927
928 928 @property
929 929 def name(self):
930 930 return self.group_name.split(RepoGroup.url_sep())[-1]
931 931
932 932 @property
933 933 def full_path(self):
934 934 return self.group_name
935 935
936 936 @property
937 937 def full_path_splitted(self):
938 938 return self.group_name.split(RepoGroup.url_sep())
939 939
940 940 @property
941 941 def repositories(self):
942 942 return Repository.query()\
943 943 .filter(Repository.group == self)\
944 944 .order_by(Repository.repo_name)
945 945
946 946 @property
947 947 def repositories_recursive_count(self):
948 948 cnt = self.repositories.count()
949 949
950 950 def children_count(group):
951 951 cnt = 0
952 952 for child in group.children:
953 953 cnt += child.repositories.count()
954 954 cnt += children_count(child)
955 955 return cnt
956 956
957 957 return cnt + children_count(self)
958 958
959 959 def get_new_name(self, group_name):
960 960 """
961 961 returns new full group name based on parent and new name
962 962
963 963 :param group_name:
964 964 """
965 965 path_prefix = (self.parent_group.full_path_splitted if
966 966 self.parent_group else [])
967 967 return RepoGroup.url_sep().join(path_prefix + [group_name])
968 968
969 969
970 970 class Permission(Base, BaseModel):
971 971 __tablename__ = 'permissions'
972 972 __table_args__ = (
973 973 {'extend_existing': True, 'mysql_engine': 'InnoDB',
974 974 'mysql_charset': 'utf8'},
975 975 )
976 976 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
977 977 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
978 978 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
979 979
980 980 def __unicode__(self):
981 981 return u"<%s('%s:%s')>" % (
982 982 self.__class__.__name__, self.permission_id, self.permission_name
983 983 )
984 984
985 985 @classmethod
986 986 def get_by_key(cls, key):
987 987 return cls.query().filter(cls.permission_name == key).scalar()
988 988
989 989 @classmethod
990 990 def get_default_perms(cls, default_user_id):
991 991 q = Session.query(UserRepoToPerm, Repository, cls)\
992 992 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
993 993 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
994 994 .filter(UserRepoToPerm.user_id == default_user_id)
995 995
996 996 return q.all()
997 997
998 998 @classmethod
999 999 def get_default_group_perms(cls, default_user_id):
1000 1000 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
1001 1001 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1002 1002 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1003 1003 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1004 1004
1005 1005 return q.all()
1006 1006
1007 1007
1008 1008 class UserRepoToPerm(Base, BaseModel):
1009 1009 __tablename__ = 'repo_to_perm'
1010 1010 __table_args__ = (
1011 1011 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1012 1012 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1013 1013 'mysql_charset': 'utf8'}
1014 1014 )
1015 1015 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1016 1016 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1017 1017 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1018 1018 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1019 1019
1020 1020 user = relationship('User')
1021 1021 repository = relationship('Repository')
1022 1022 permission = relationship('Permission')
1023 1023
1024 1024 @classmethod
1025 1025 def create(cls, user, repository, permission):
1026 1026 n = cls()
1027 1027 n.user = user
1028 1028 n.repository = repository
1029 1029 n.permission = permission
1030 1030 Session.add(n)
1031 1031 return n
1032 1032
1033 1033 def __unicode__(self):
1034 1034 return u'<user:%s => %s >' % (self.user, self.repository)
1035 1035
1036 1036
1037 1037 class UserToPerm(Base, BaseModel):
1038 1038 __tablename__ = 'user_to_perm'
1039 1039 __table_args__ = (
1040 1040 UniqueConstraint('user_id', 'permission_id'),
1041 1041 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1042 1042 'mysql_charset': 'utf8'}
1043 1043 )
1044 1044 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1045 1045 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1046 1046 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1047 1047
1048 1048 user = relationship('User')
1049 1049 permission = relationship('Permission', lazy='joined')
1050 1050
1051 1051
1052 1052 class UsersGroupRepoToPerm(Base, BaseModel):
1053 1053 __tablename__ = 'users_group_repo_to_perm'
1054 1054 __table_args__ = (
1055 1055 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1056 1056 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1057 1057 'mysql_charset': 'utf8'}
1058 1058 )
1059 1059 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1060 1060 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1061 1061 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1062 1062 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1063 1063
1064 1064 users_group = relationship('UsersGroup')
1065 1065 permission = relationship('Permission')
1066 1066 repository = relationship('Repository')
1067 1067
1068 1068 @classmethod
1069 1069 def create(cls, users_group, repository, permission):
1070 1070 n = cls()
1071 1071 n.users_group = users_group
1072 1072 n.repository = repository
1073 1073 n.permission = permission
1074 1074 Session.add(n)
1075 1075 return n
1076 1076
1077 1077 def __unicode__(self):
1078 1078 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1079 1079
1080 1080
1081 1081 class UsersGroupToPerm(Base, BaseModel):
1082 1082 __tablename__ = 'users_group_to_perm'
1083 1083 __table_args__ = (
1084 1084 UniqueConstraint('users_group_id', 'permission_id',),
1085 1085 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1086 1086 'mysql_charset': 'utf8'}
1087 1087 )
1088 1088 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1089 1089 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1090 1090 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1091 1091
1092 1092 users_group = relationship('UsersGroup')
1093 1093 permission = relationship('Permission')
1094 1094
1095 1095
1096 1096 class UserRepoGroupToPerm(Base, BaseModel):
1097 1097 __tablename__ = 'user_repo_group_to_perm'
1098 1098 __table_args__ = (
1099 1099 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1100 1100 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1101 1101 'mysql_charset': 'utf8'}
1102 1102 )
1103 1103
1104 1104 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1105 1105 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1106 1106 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1107 1107 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1108 1108
1109 1109 user = relationship('User')
1110 1110 group = relationship('RepoGroup')
1111 1111 permission = relationship('Permission')
1112 1112
1113 1113
1114 1114 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1115 1115 __tablename__ = 'users_group_repo_group_to_perm'
1116 1116 __table_args__ = (
1117 1117 UniqueConstraint('users_group_id', 'group_id'),
1118 1118 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1119 1119 'mysql_charset': 'utf8'}
1120 1120 )
1121 1121
1122 1122 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 1123 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1124 1124 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1125 1125 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1126 1126
1127 1127 users_group = relationship('UsersGroup')
1128 1128 permission = relationship('Permission')
1129 1129 group = relationship('RepoGroup')
1130 1130
1131 1131
1132 1132 class Statistics(Base, BaseModel):
1133 1133 __tablename__ = 'statistics'
1134 1134 __table_args__ = (
1135 1135 UniqueConstraint('repository_id'),
1136 1136 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1137 1137 'mysql_charset': 'utf8'}
1138 1138 )
1139 1139 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1140 1140 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1141 1141 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1142 1142 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1143 1143 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1144 1144 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1145 1145
1146 1146 repository = relationship('Repository', single_parent=True)
1147 1147
1148 1148
1149 1149 class UserFollowing(Base, BaseModel):
1150 1150 __tablename__ = 'user_followings'
1151 1151 __table_args__ = (
1152 1152 UniqueConstraint('user_id', 'follows_repository_id'),
1153 1153 UniqueConstraint('user_id', 'follows_user_id'),
1154 1154 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1155 1155 'mysql_charset': 'utf8'}
1156 1156 )
1157 1157
1158 1158 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1159 1159 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1160 1160 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1161 1161 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1162 1162 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1163 1163
1164 1164 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1165 1165
1166 1166 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1167 1167 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1168 1168
1169 1169 @classmethod
1170 1170 def get_repo_followers(cls, repo_id):
1171 1171 return cls.query().filter(cls.follows_repo_id == repo_id)
1172 1172
1173 1173
1174 1174 class CacheInvalidation(Base, BaseModel):
1175 1175 __tablename__ = 'cache_invalidation'
1176 1176 __table_args__ = (
1177 1177 UniqueConstraint('cache_key'),
1178 1178 Index('key_idx', 'cache_key'),
1179 1179 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1180 1180 'mysql_charset': 'utf8'},
1181 1181 )
1182 1182 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1183 1183 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1184 1184 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1185 1185 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1186 1186
1187 1187 def __init__(self, cache_key, cache_args=''):
1188 1188 self.cache_key = cache_key
1189 1189 self.cache_args = cache_args
1190 1190 self.cache_active = False
1191 1191
1192 1192 def __unicode__(self):
1193 1193 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1194 1194 self.cache_id, self.cache_key)
1195 1195
1196 1196 @classmethod
1197 1197 def clear_cache(cls):
1198 1198 cls.query().delete()
1199 1199
1200 1200 @classmethod
1201 1201 def _get_key(cls, key):
1202 1202 """
1203 1203 Wrapper for generating a key, together with a prefix
1204 1204
1205 1205 :param key:
1206 1206 """
1207 1207 import rhodecode
1208 1208 prefix = ''
1209 1209 iid = rhodecode.CONFIG.get('instance_id')
1210 1210 if iid:
1211 1211 prefix = iid
1212 1212 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1213 1213
1214 1214 @classmethod
1215 1215 def get_by_key(cls, key):
1216 1216 return cls.query().filter(cls.cache_key == key).scalar()
1217 1217
1218 1218 @classmethod
1219 1219 def _get_or_create_key(cls, key, prefix, org_key):
1220 1220 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1221 1221 if not inv_obj:
1222 1222 try:
1223 1223 inv_obj = CacheInvalidation(key, org_key)
1224 1224 Session.add(inv_obj)
1225 1225 Session.commit()
1226 1226 except Exception:
1227 1227 log.error(traceback.format_exc())
1228 1228 Session.rollback()
1229 1229 return inv_obj
1230 1230
1231 1231 @classmethod
1232 1232 def invalidate(cls, key):
1233 1233 """
1234 1234 Returns Invalidation object if this given key should be invalidated
1235 1235 None otherwise. `cache_active = False` means that this cache
1236 1236 state is not valid and needs to be invalidated
1237 1237
1238 1238 :param key:
1239 1239 """
1240 1240
1241 1241 key, _prefix, _org_key = cls._get_key(key)
1242 1242 inv = cls._get_or_create_key(key, _prefix, _org_key)
1243 1243
1244 1244 if inv and inv.cache_active is False:
1245 1245 return inv
1246 1246
1247 1247 @classmethod
1248 1248 def set_invalidate(cls, key):
1249 1249 """
1250 1250 Mark this Cache key for invalidation
1251 1251
1252 1252 :param key:
1253 1253 """
1254 1254
1255 1255 key, _prefix, _org_key = cls._get_key(key)
1256 1256 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1257 1257 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1258 1258 _org_key))
1259 1259 try:
1260 1260 for inv_obj in inv_objs:
1261 1261 if inv_obj:
1262 1262 inv_obj.cache_active = False
1263 1263
1264 1264 Session.add(inv_obj)
1265 1265 Session.commit()
1266 1266 except Exception:
1267 1267 log.error(traceback.format_exc())
1268 1268 Session.rollback()
1269 1269
1270 1270 @classmethod
1271 1271 def set_valid(cls, key):
1272 1272 """
1273 1273 Mark this cache key as active and currently cached
1274 1274
1275 1275 :param key:
1276 1276 """
1277 1277 inv_obj = cls.get_by_key(key)
1278 1278 inv_obj.cache_active = True
1279 1279 Session.add(inv_obj)
1280 1280 Session.commit()
1281 1281
1282 1282 @classmethod
1283 1283 def get_cache_map(cls):
1284 1284
1285 1285 class cachemapdict(dict):
1286 1286
1287 1287 def __init__(self, *args, **kwargs):
1288 1288 fixkey = kwargs.get('fixkey')
1289 1289 if fixkey:
1290 1290 del kwargs['fixkey']
1291 1291 self.fixkey = fixkey
1292 1292 super(cachemapdict, self).__init__(*args, **kwargs)
1293 1293
1294 1294 def __getattr__(self, name):
1295 1295 key = name
1296 1296 if self.fixkey:
1297 1297 key, _prefix, _org_key = cls._get_key(key)
1298 1298 if key in self.__dict__:
1299 1299 return self.__dict__[key]
1300 1300 else:
1301 1301 return self[key]
1302 1302
1303 1303 def __getitem__(self, key):
1304 1304 if self.fixkey:
1305 1305 key, _prefix, _org_key = cls._get_key(key)
1306 1306 try:
1307 1307 return super(cachemapdict, self).__getitem__(key)
1308 1308 except KeyError:
1309 1309 return
1310 1310
1311 1311 cache_map = cachemapdict(fixkey=True)
1312 1312 for obj in cls.query().all():
1313 1313 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1314 1314 return cache_map
1315 1315
1316 1316
1317 1317 class ChangesetComment(Base, BaseModel):
1318 1318 __tablename__ = 'changeset_comments'
1319 1319 __table_args__ = (
1320 1320 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1321 1321 'mysql_charset': 'utf8'},
1322 1322 )
1323 1323 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1324 1324 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1325 revision = Column('revision', String(40), nullable=False)
1325 revision = Column('revision', String(40), nullable=True)
1326 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1326 1327 line_no = Column('line_no', Unicode(10), nullable=True)
1327 1328 f_path = Column('f_path', Unicode(1000), nullable=True)
1328 1329 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1329 1330 text = Column('text', Unicode(25000), nullable=False)
1330 1331 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1331 1332
1332 1333 author = relationship('User', lazy='joined')
1333 1334 repo = relationship('Repository')
1334 1335 status_change = relationship('ChangesetStatus', uselist=False)
1336 pull_request = relationship('PullRequest', lazy='joined')
1335 1337
1336 1338 @classmethod
1337 1339 def get_users(cls, revision):
1338 1340 """
1339 1341 Returns user associated with this changesetComment. ie those
1340 1342 who actually commented
1341 1343
1342 1344 :param cls:
1343 1345 :param revision:
1344 1346 """
1345 1347 return Session.query(User)\
1346 1348 .filter(cls.revision == revision)\
1347 1349 .join(ChangesetComment.author).all()
1348 1350
1349 1351
1350 1352 class ChangesetStatus(Base, BaseModel):
1351 1353 __tablename__ = 'changeset_statuses'
1352 1354 __table_args__ = (
1353 1355 UniqueConstraint('repo_id', 'revision', 'version'),
1354 1356 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1355 1357 'mysql_charset': 'utf8'}
1356 1358 )
1357 1359
1358 1360 STATUSES = [
1359 1361 ('not_reviewed', _("Not Reviewed")), # (no icon) and default
1360 1362 ('approved', _("Approved")),
1361 1363 ('rejected', _("Rejected")),
1362 1364 ('under_review', _("Under Review")),
1363 1365 ]
1364 1366 DEFAULT = STATUSES[0][0]
1365 1367
1366 1368 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1367 1369 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1368 1370 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1369 1371 revision = Column('revision', String(40), nullable=False)
1370 1372 status = Column('status', String(128), nullable=False, default=DEFAULT)
1371 1373 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1372 1374 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1373 1375 version = Column('version', Integer(), nullable=False, default=0)
1374 1376 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1375 1377
1376 1378 author = relationship('User', lazy='joined')
1377 1379 repo = relationship('Repository')
1378 1380 comment = relationship('ChangesetComment', lazy='joined')
1379 1381 pull_request = relationship('PullRequest', lazy='joined')
1380 1382
1381 1383 @classmethod
1382 1384 def get_status_lbl(cls, value):
1383 1385 return dict(cls.STATUSES).get(value)
1384 1386
1385 1387 @property
1386 1388 def status_lbl(self):
1387 1389 return ChangesetStatus.get_status_lbl(self.status)
1388 1390
1389 1391
1390 1392 class PullRequest(Base, BaseModel):
1391 1393 __tablename__ = 'pull_requests'
1392 1394 __table_args__ = (
1393 1395 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1394 1396 'mysql_charset': 'utf8'},
1395 1397 )
1396 1398
1397 1399 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1398 1400 title = Column('title', Unicode(256), nullable=True)
1399 1401 description = Column('description', Unicode(10240), nullable=True)
1402 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1403 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1400 1404 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1401 1405 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1402 1406 org_ref = Column('org_ref', Unicode(256), nullable=False)
1403 1407 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1404 1408 other_ref = Column('other_ref', Unicode(256), nullable=False)
1405 1409
1406 1410 @hybrid_property
1407 1411 def revisions(self):
1408 1412 return self._revisions.split(':')
1409 1413
1410 1414 @revisions.setter
1411 1415 def revisions(self, val):
1412 1416 self._revisions = ':'.join(val)
1413 1417
1418 author = relationship('User', lazy='joined')
1414 1419 reviewers = relationship('PullRequestReviewers')
1415 1420 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1416 1421 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1417 1422
1418 1423 def __json__(self):
1419 1424 return dict(
1420 1425 revisions=self.revisions
1421 1426 )
1422 1427
1423 1428
1424 1429 class PullRequestReviewers(Base, BaseModel):
1425 1430 __tablename__ = 'pull_request_reviewers'
1426 1431 __table_args__ = (
1427 1432 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1428 1433 'mysql_charset': 'utf8'},
1429 1434 )
1430 1435
1431 1436 def __init__(self, user=None, pull_request=None):
1432 1437 self.user = user
1433 1438 self.pull_request = pull_request
1434 1439
1435 1440 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1436 1441 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1437 1442 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1438 1443
1439 1444 user = relationship('User')
1440 1445 pull_request = relationship('PullRequest')
1441 1446
1442 1447
1443 1448 class Notification(Base, BaseModel):
1444 1449 __tablename__ = 'notifications'
1445 1450 __table_args__ = (
1446 1451 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1447 1452 'mysql_charset': 'utf8'},
1448 1453 )
1449 1454
1450 1455 TYPE_CHANGESET_COMMENT = u'cs_comment'
1451 1456 TYPE_MESSAGE = u'message'
1452 1457 TYPE_MENTION = u'mention'
1453 1458 TYPE_REGISTRATION = u'registration'
1454 1459 TYPE_PULL_REQUEST = u'pull_request'
1455 1460
1456 1461 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1457 1462 subject = Column('subject', Unicode(512), nullable=True)
1458 1463 body = Column('body', Unicode(50000), nullable=True)
1459 1464 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1460 1465 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1461 1466 type_ = Column('type', Unicode(256))
1462 1467
1463 1468 created_by_user = relationship('User')
1464 1469 notifications_to_users = relationship('UserNotification', lazy='joined',
1465 1470 cascade="all, delete, delete-orphan")
1466 1471
1467 1472 @property
1468 1473 def recipients(self):
1469 1474 return [x.user for x in UserNotification.query()\
1470 1475 .filter(UserNotification.notification == self)\
1471 1476 .order_by(UserNotification.user).all()]
1472 1477
1473 1478 @classmethod
1474 1479 def create(cls, created_by, subject, body, recipients, type_=None):
1475 1480 if type_ is None:
1476 1481 type_ = Notification.TYPE_MESSAGE
1477 1482
1478 1483 notification = cls()
1479 1484 notification.created_by_user = created_by
1480 1485 notification.subject = subject
1481 1486 notification.body = body
1482 1487 notification.type_ = type_
1483 1488 notification.created_on = datetime.datetime.now()
1484 1489
1485 1490 for u in recipients:
1486 1491 assoc = UserNotification()
1487 1492 assoc.notification = notification
1488 1493 u.notifications.append(assoc)
1489 1494 Session.add(notification)
1490 1495 return notification
1491 1496
1492 1497 @property
1493 1498 def description(self):
1494 1499 from rhodecode.model.notification import NotificationModel
1495 1500 return NotificationModel().make_description(self)
1496 1501
1497 1502
1498 1503 class UserNotification(Base, BaseModel):
1499 1504 __tablename__ = 'user_to_notification'
1500 1505 __table_args__ = (
1501 1506 UniqueConstraint('user_id', 'notification_id'),
1502 1507 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1503 1508 'mysql_charset': 'utf8'}
1504 1509 )
1505 1510 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1506 1511 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1507 1512 read = Column('read', Boolean, default=False)
1508 1513 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1509 1514
1510 1515 user = relationship('User', lazy="joined")
1511 1516 notification = relationship('Notification', lazy="joined",
1512 1517 order_by=lambda: Notification.created_on.desc(),)
1513 1518
1514 1519 def mark_as_read(self):
1515 1520 self.read = True
1516 1521 Session.add(self)
1517 1522
1518 1523
1519 1524 class DbMigrateVersion(Base, BaseModel):
1520 1525 __tablename__ = 'db_migrate_version'
1521 1526 __table_args__ = (
1522 1527 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1523 1528 'mysql_charset': 'utf8'},
1524 1529 )
1525 1530 repository_id = Column('repository_id', String(250), primary_key=True)
1526 1531 repository_path = Column('repository_path', Text)
1527 1532 version = Column('version', Integer)
@@ -1,79 +1,84 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.pull_reuquest
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull request model for RhodeCode
7 7
8 8 :created_on: Jun 6, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2012-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 from pylons.i18n.translation import _
28 28
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.model import BaseModel
31 31 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification
32 32 from rhodecode.model.notification import NotificationModel
33 33 from rhodecode.lib.utils2 import safe_unicode
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class PullRequestModel(BaseModel):
39 39
40 def get_all(self, repo):
41 repo = self._get_repo(repo)
42 return PullRequest.query().filter(PullRequest.other_repo == repo).all()
43
40 44 def create(self, created_by, org_repo, org_ref, other_repo,
41 45 other_ref, revisions, reviewers, title, description=None):
46 created_by_user = self._get_user(created_by)
42 47
43 48 new = PullRequest()
44 49 new.org_repo = self._get_repo(org_repo)
45 50 new.org_ref = org_ref
46 51 new.other_repo = self._get_repo(other_repo)
47 52 new.other_ref = other_ref
48 53 new.revisions = revisions
49 54 new.title = title
50 55 new.description = description
51
56 new.author = created_by_user
52 57 self.sa.add(new)
53 58
54 59 #members
55 60 for member in reviewers:
56 61 _usr = self._get_user(member)
57 62 reviewer = PullRequestReviewers(_usr, new)
58 63 self.sa.add(reviewer)
59 64
60 65 #notification to reviewers
61 66 notif = NotificationModel()
62 created_by_user = self._get_user(created_by)
67
63 68 subject = safe_unicode(
64 69 h.link_to(
65 70 _('%(user)s wants you to review pull request #%(pr_id)s') % \
66 71 {'user': created_by_user.username,
67 72 'pr_id': new.pull_request_id},
68 73 h.url('pullrequest_show', repo_name=other_repo,
69 74 pull_request_id=new.pull_request_id,
70 75 qualified=True,
71 76 )
72 77 )
73 78 )
74 79 body = description
75 80 notif.create(created_by=created_by, subject=subject, body=body,
76 81 recipients=reviewers,
77 82 type_=Notification.TYPE_PULL_REQUEST,)
78 83
79 84 return new
@@ -1,474 +1,476 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import time
27 27 import traceback
28 28 import logging
29 29 import cStringIO
30 30
31 31 from sqlalchemy import func
32 32
33 33 from rhodecode.lib.vcs import get_backend
34 34 from rhodecode.lib.vcs.exceptions import RepositoryError
35 35 from rhodecode.lib.vcs.utils.lazy import LazyProperty
36 36 from rhodecode.lib.vcs.nodes import FileNode
37 37
38 38 from rhodecode import BACKENDS
39 39 from rhodecode.lib import helpers as h
40 40 from rhodecode.lib.utils2 import safe_str, safe_unicode
41 41 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
42 42 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
43 43 action_logger, EmptyChangeset, REMOVED_REPO_PAT
44 44 from rhodecode.model import BaseModel
45 45 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
46 UserFollowing, UserLog, User, RepoGroup
46 UserFollowing, UserLog, User, RepoGroup, PullRequest
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class UserTemp(object):
52 52 def __init__(self, user_id):
53 53 self.user_id = user_id
54 54
55 55 def __repr__(self):
56 56 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
57 57
58 58
59 59 class RepoTemp(object):
60 60 def __init__(self, repo_id):
61 61 self.repo_id = repo_id
62 62
63 63 def __repr__(self):
64 64 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
65 65
66 66
67 67 class CachedRepoList(object):
68 68
69 69 def __init__(self, db_repo_list, repos_path, order_by=None):
70 70 self.db_repo_list = db_repo_list
71 71 self.repos_path = repos_path
72 72 self.order_by = order_by
73 73 self.reversed = (order_by or '').startswith('-')
74 74
75 75 def __len__(self):
76 76 return len(self.db_repo_list)
77 77
78 78 def __repr__(self):
79 79 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
80 80
81 81 def __iter__(self):
82 82 # pre-propagated cache_map to save executing select statements
83 83 # for each repo
84 84 cache_map = CacheInvalidation.get_cache_map()
85 85
86 86 for dbr in self.db_repo_list:
87 87 scmr = dbr.scm_instance_cached(cache_map)
88 88 # check permission at this level
89 89 if not HasRepoPermissionAny(
90 90 'repository.read', 'repository.write', 'repository.admin'
91 91 )(dbr.repo_name, 'get repo check'):
92 92 continue
93 93
94 94 if scmr is None:
95 95 log.error(
96 96 '%s this repository is present in database but it '
97 97 'cannot be created as an scm instance' % dbr.repo_name
98 98 )
99 99 continue
100 100
101 101 last_change = scmr.last_change
102 102 tip = h.get_changeset_safe(scmr, 'tip')
103 103
104 104 tmp_d = {}
105 105 tmp_d['name'] = dbr.repo_name
106 106 tmp_d['name_sort'] = tmp_d['name'].lower()
107 107 tmp_d['description'] = dbr.description
108 108 tmp_d['description_sort'] = tmp_d['description']
109 109 tmp_d['last_change'] = last_change
110 110 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
111 111 tmp_d['tip'] = tip.raw_id
112 112 tmp_d['tip_sort'] = tip.revision
113 113 tmp_d['rev'] = tip.revision
114 114 tmp_d['contact'] = dbr.user.full_contact
115 115 tmp_d['contact_sort'] = tmp_d['contact']
116 116 tmp_d['owner_sort'] = tmp_d['contact']
117 117 tmp_d['repo_archives'] = list(scmr._get_archives())
118 118 tmp_d['last_msg'] = tip.message
119 119 tmp_d['author'] = tip.author
120 120 tmp_d['dbrepo'] = dbr.get_dict()
121 121 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
122 122 yield tmp_d
123 123
124 124
125 125 class GroupList(object):
126 126
127 127 def __init__(self, db_repo_group_list):
128 128 self.db_repo_group_list = db_repo_group_list
129 129
130 130 def __len__(self):
131 131 return len(self.db_repo_group_list)
132 132
133 133 def __repr__(self):
134 134 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
135 135
136 136 def __iter__(self):
137 137 for dbgr in self.db_repo_group_list:
138 138 # check permission at this level
139 139 if not HasReposGroupPermissionAny(
140 140 'group.read', 'group.write', 'group.admin'
141 141 )(dbgr.group_name, 'get group repo check'):
142 142 continue
143 143
144 144 yield dbgr
145 145
146 146
147 147 class ScmModel(BaseModel):
148 148 """
149 149 Generic Scm Model
150 150 """
151 151
152 152 def __get_repo(self, instance):
153 153 cls = Repository
154 154 if isinstance(instance, cls):
155 155 return instance
156 156 elif isinstance(instance, int) or str(instance).isdigit():
157 157 return cls.get(instance)
158 158 elif isinstance(instance, basestring):
159 159 return cls.get_by_repo_name(instance)
160 160 elif instance:
161 161 raise Exception('given object must be int, basestr or Instance'
162 162 ' of %s got %s' % (type(cls), type(instance)))
163 163
164 164 @LazyProperty
165 165 def repos_path(self):
166 166 """
167 167 Get's the repositories root path from database
168 168 """
169 169
170 170 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
171 171
172 172 return q.ui_value
173 173
174 174 def repo_scan(self, repos_path=None):
175 175 """
176 176 Listing of repositories in given path. This path should not be a
177 177 repository itself. Return a dictionary of repository objects
178 178
179 179 :param repos_path: path to directory containing repositories
180 180 """
181 181
182 182 if repos_path is None:
183 183 repos_path = self.repos_path
184 184
185 185 log.info('scanning for repositories in %s' % repos_path)
186 186
187 187 baseui = make_ui('db')
188 188 repos = {}
189 189
190 190 for name, path in get_filesystem_repos(repos_path, recursive=True):
191 191 # skip removed repos
192 192 if REMOVED_REPO_PAT.match(name):
193 193 continue
194 194
195 195 # name need to be decomposed and put back together using the /
196 196 # since this is internal storage separator for rhodecode
197 197 name = Repository.url_sep().join(name.split(os.sep))
198 198
199 199 try:
200 200 if name in repos:
201 201 raise RepositoryError('Duplicate repository name %s '
202 202 'found in %s' % (name, path))
203 203 else:
204 204
205 205 klass = get_backend(path[0])
206 206
207 207 if path[0] == 'hg' and path[0] in BACKENDS.keys():
208 208 repos[name] = klass(safe_str(path[1]), baseui=baseui)
209 209
210 210 if path[0] == 'git' and path[0] in BACKENDS.keys():
211 211 repos[name] = klass(path[1])
212 212 except OSError:
213 213 continue
214 214
215 215 return repos
216 216
217 217 def get_repos(self, all_repos=None, sort_key=None):
218 218 """
219 219 Get all repos from db and for each repo create it's
220 220 backend instance and fill that backed with information from database
221 221
222 222 :param all_repos: list of repository names as strings
223 223 give specific repositories list, good for filtering
224 224 """
225 225 if all_repos is None:
226 226 all_repos = self.sa.query(Repository)\
227 227 .filter(Repository.group_id == None)\
228 228 .order_by(func.lower(Repository.repo_name)).all()
229 229
230 230 repo_iter = CachedRepoList(all_repos, repos_path=self.repos_path,
231 231 order_by=sort_key)
232 232
233 233 return repo_iter
234 234
235 235 def get_repos_groups(self, all_groups=None):
236 236 if all_groups is None:
237 237 all_groups = RepoGroup.query()\
238 238 .filter(RepoGroup.group_parent_id == None).all()
239 239 group_iter = GroupList(all_groups)
240 240
241 241 return group_iter
242 242
243 243 def mark_for_invalidation(self, repo_name):
244 244 """
245 245 Puts cache invalidation task into db for
246 246 further global cache invalidation
247 247
248 248 :param repo_name: this repo that should invalidation take place
249 249 """
250 250 CacheInvalidation.set_invalidate(repo_name)
251 251
252 252 def toggle_following_repo(self, follow_repo_id, user_id):
253 253
254 254 f = self.sa.query(UserFollowing)\
255 255 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
256 256 .filter(UserFollowing.user_id == user_id).scalar()
257 257
258 258 if f is not None:
259 259 try:
260 260 self.sa.delete(f)
261 261 action_logger(UserTemp(user_id),
262 262 'stopped_following_repo',
263 263 RepoTemp(follow_repo_id))
264 264 return
265 265 except:
266 266 log.error(traceback.format_exc())
267 267 raise
268 268
269 269 try:
270 270 f = UserFollowing()
271 271 f.user_id = user_id
272 272 f.follows_repo_id = follow_repo_id
273 273 self.sa.add(f)
274 274
275 275 action_logger(UserTemp(user_id),
276 276 'started_following_repo',
277 277 RepoTemp(follow_repo_id))
278 278 except:
279 279 log.error(traceback.format_exc())
280 280 raise
281 281
282 282 def toggle_following_user(self, follow_user_id, user_id):
283 283 f = self.sa.query(UserFollowing)\
284 284 .filter(UserFollowing.follows_user_id == follow_user_id)\
285 285 .filter(UserFollowing.user_id == user_id).scalar()
286 286
287 287 if f is not None:
288 288 try:
289 289 self.sa.delete(f)
290 290 return
291 291 except:
292 292 log.error(traceback.format_exc())
293 293 raise
294 294
295 295 try:
296 296 f = UserFollowing()
297 297 f.user_id = user_id
298 298 f.follows_user_id = follow_user_id
299 299 self.sa.add(f)
300 300 except:
301 301 log.error(traceback.format_exc())
302 302 raise
303 303
304 304 def is_following_repo(self, repo_name, user_id, cache=False):
305 305 r = self.sa.query(Repository)\
306 306 .filter(Repository.repo_name == repo_name).scalar()
307 307
308 308 f = self.sa.query(UserFollowing)\
309 309 .filter(UserFollowing.follows_repository == r)\
310 310 .filter(UserFollowing.user_id == user_id).scalar()
311 311
312 312 return f is not None
313 313
314 314 def is_following_user(self, username, user_id, cache=False):
315 315 u = User.get_by_username(username)
316 316
317 317 f = self.sa.query(UserFollowing)\
318 318 .filter(UserFollowing.follows_user == u)\
319 319 .filter(UserFollowing.user_id == user_id).scalar()
320 320
321 321 return f is not None
322 322
323 def get_followers(self, repo_id):
324 if not isinstance(repo_id, int):
325 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
323 def get_followers(self, repo):
324 repo = self._get_repo(repo)
326 325
327 326 return self.sa.query(UserFollowing)\
328 .filter(UserFollowing.follows_repo_id == repo_id).count()
327 .filter(UserFollowing.follows_repository == repo).count()
329 328
330 def get_forks(self, repo_id):
331 if not isinstance(repo_id, int):
332 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
329 def get_forks(self, repo):
330 repo = self._get_repo(repo)
331 return self.sa.query(Repository)\
332 .filter(Repository.fork == repo).count()
333 333
334 return self.sa.query(Repository)\
335 .filter(Repository.fork_id == repo_id).count()
334 def get_pull_requests(self, repo):
335 repo = self._get_repo(repo)
336 return self.sa.query(PullRequest)\
337 .filter(PullRequest.other_repo == repo).count()
336 338
337 339 def mark_as_fork(self, repo, fork, user):
338 340 repo = self.__get_repo(repo)
339 341 fork = self.__get_repo(fork)
340 342 repo.fork = fork
341 343 self.sa.add(repo)
342 344 return repo
343 345
344 346 def pull_changes(self, repo_name, username):
345 347 dbrepo = Repository.get_by_repo_name(repo_name)
346 348 clone_uri = dbrepo.clone_uri
347 349 if not clone_uri:
348 350 raise Exception("This repository doesn't have a clone uri")
349 351
350 352 repo = dbrepo.scm_instance
351 353 try:
352 354 extras = {
353 355 'ip': '',
354 356 'username': username,
355 357 'action': 'push_remote',
356 358 'repository': repo_name,
357 359 'scm': repo.alias,
358 360 }
359 361
360 362 # inject ui extra param to log this action via push logger
361 363 for k, v in extras.items():
362 364 repo._repo.ui.setconfig('rhodecode_extras', k, v)
363 365 if repo.alias == 'git':
364 366 repo.fetch(clone_uri)
365 367 else:
366 368 repo.pull(clone_uri)
367 369 self.mark_for_invalidation(repo_name)
368 370 except:
369 371 log.error(traceback.format_exc())
370 372 raise
371 373
372 374 def commit_change(self, repo, repo_name, cs, user, author, message,
373 375 content, f_path):
374 376
375 377 if repo.alias == 'hg':
376 378 from rhodecode.lib.vcs.backends.hg import \
377 379 MercurialInMemoryChangeset as IMC
378 380 elif repo.alias == 'git':
379 381 from rhodecode.lib.vcs.backends.git import \
380 382 GitInMemoryChangeset as IMC
381 383
382 384 # decoding here will force that we have proper encoded values
383 385 # in any other case this will throw exceptions and deny commit
384 386 content = safe_str(content)
385 387 path = safe_str(f_path)
386 388 # message and author needs to be unicode
387 389 # proper backend should then translate that into required type
388 390 message = safe_unicode(message)
389 391 author = safe_unicode(author)
390 392 m = IMC(repo)
391 393 m.change(FileNode(path, content))
392 394 tip = m.commit(message=message,
393 395 author=author,
394 396 parents=[cs], branch=cs.branch)
395 397
396 398 new_cs = tip.short_id
397 399 action = 'push_local:%s' % new_cs
398 400
399 401 action_logger(user, action, repo_name)
400 402
401 403 self.mark_for_invalidation(repo_name)
402 404
403 405 def create_node(self, repo, repo_name, cs, user, author, message, content,
404 406 f_path):
405 407 if repo.alias == 'hg':
406 408 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
407 409 elif repo.alias == 'git':
408 410 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
409 411 # decoding here will force that we have proper encoded values
410 412 # in any other case this will throw exceptions and deny commit
411 413
412 414 if isinstance(content, (basestring,)):
413 415 content = safe_str(content)
414 416 elif isinstance(content, (file, cStringIO.OutputType,)):
415 417 content = content.read()
416 418 else:
417 419 raise Exception('Content is of unrecognized type %s' % (
418 420 type(content)
419 421 ))
420 422
421 423 message = safe_unicode(message)
422 424 author = safe_unicode(author)
423 425 path = safe_str(f_path)
424 426 m = IMC(repo)
425 427
426 428 if isinstance(cs, EmptyChangeset):
427 429 # EmptyChangeset means we we're editing empty repository
428 430 parents = None
429 431 else:
430 432 parents = [cs]
431 433
432 434 m.add(FileNode(path, content=content))
433 435 tip = m.commit(message=message,
434 436 author=author,
435 437 parents=parents, branch=cs.branch)
436 438 new_cs = tip.short_id
437 439 action = 'push_local:%s' % new_cs
438 440
439 441 action_logger(user, action, repo_name)
440 442
441 443 self.mark_for_invalidation(repo_name)
442 444
443 445 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
444 446 """
445 447 recursive walk in root dir and return a set of all path in that dir
446 448 based on repository walk function
447 449
448 450 :param repo_name: name of repository
449 451 :param revision: revision for which to list nodes
450 452 :param root_path: root path to list
451 453 :param flat: return as a list, if False returns a dict with decription
452 454
453 455 """
454 456 _files = list()
455 457 _dirs = list()
456 458 try:
457 459 _repo = self.__get_repo(repo_name)
458 460 changeset = _repo.scm_instance.get_changeset(revision)
459 461 root_path = root_path.lstrip('/')
460 462 for topnode, dirs, files in changeset.walk(root_path):
461 463 for f in files:
462 464 _files.append(f.path if flat else {"name": f.path,
463 465 "type": "file"})
464 466 for d in dirs:
465 467 _dirs.append(d.path if flat else {"name": d.path,
466 468 "type": "dir"})
467 469 except RepositoryError:
468 470 log.debug(traceback.format_exc())
469 471 raise
470 472
471 473 return _dirs, _files
472 474
473 475 def get_unread_journal(self):
474 476 return self.sa.query(UserLog).count()
@@ -1,338 +1,346 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.html"/>
3 3
4 4 <!-- HEADER -->
5 5 <div id="header">
6 6 <div id="header-inner" class="title hover">
7 7 <div id="logo">
8 8 <h1><a href="${h.url('home')}">${c.rhodecode_name}</a></h1>
9 9 </div>
10 10 <!-- MENU -->
11 11 ${self.page_nav()}
12 12 <!-- END MENU -->
13 13 ${self.body()}
14 14 </div>
15 15 </div>
16 16 <!-- END HEADER -->
17 17
18 18 <!-- CONTENT -->
19 19 <div id="content">
20 20 <div class="flash_msg">
21 21 <% messages = h.flash.pop_messages() %>
22 22 % if messages:
23 23 <ul id="flash-messages">
24 24 % for message in messages:
25 25 <li class="${message.category}_msg">${message}</li>
26 26 % endfor
27 27 </ul>
28 28 % endif
29 29 </div>
30 30 <div id="main">
31 31 ${next.main()}
32 32 </div>
33 33 </div>
34 34 <!-- END CONTENT -->
35 35
36 36 <!-- FOOTER -->
37 37 <div id="footer">
38 38 <div id="footer-inner" class="title">
39 39 <div>
40 40 <p class="footer-link">
41 41 <a href="${h.url('bugtracker')}">${_('Submit a bug')}</a>
42 42 </p>
43 43 <p class="footer-link-right">
44 44 <a href="${h.url('rhodecode_official')}">RhodeCode${'-%s' % c.rhodecode_instanceid if c.rhodecode_instanceid else ''}</a>
45 45 ${c.rhodecode_version} &copy; 2010-${h.datetime.today().year} by Marcin Kuzminski
46 46 </p>
47 47 </div>
48 48 </div>
49 49 </div>
50 50 <!-- END FOOTER -->
51 51
52 52 ### MAKO DEFS ###
53 53 <%def name="page_nav()">
54 54 ${self.menu()}
55 55 </%def>
56 56
57 57 <%def name="breadcrumbs()">
58 58 <div class="breadcrumbs">
59 59 ${self.breadcrumbs_links()}
60 60 </div>
61 61 </%def>
62 62
63 63 <%def name="usermenu()">
64 64 <div class="user-menu">
65 65 <div class="container">
66 66 <div class="gravatar" id="quick_login_link">
67 67 <img alt="gravatar" src="${h.gravatar_url(c.rhodecode_user.email,24)}" />
68 68 </div>
69 69 %if c.rhodecode_user.username != 'default' and c.unread_notifications != 0:
70 70 <div class="notifications">
71 71 <a id="notification_counter" href="${h.url('notifications')}">${c.unread_notifications}</a>
72 72 </div>
73 73 %endif
74 74 </div>
75 75 <div id="quick_login" style="display:none">
76 76 %if c.rhodecode_user.username == 'default':
77 77 <h4>${_('Login to your account')}</h4>
78 78 ${h.form(h.url('login_home',came_from=h.url.current()))}
79 79 <div class="form">
80 80 <div class="fields">
81 81 <div class="field">
82 82 <div class="label">
83 83 <label for="username">${_('Username')}:</label>
84 84 </div>
85 85 <div class="input">
86 86 ${h.text('username',class_='focus',size=40)}
87 87 </div>
88 88
89 89 </div>
90 90 <div class="field">
91 91 <div class="label">
92 92 <label for="password">${_('Password')}:</label>
93 93 </div>
94 94 <div class="input">
95 95 ${h.password('password',class_='focus',size=40)}
96 96 </div>
97 97
98 98 </div>
99 99 <div class="buttons">
100 100 <div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
101 101 <div class="register">
102 102 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
103 103 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
104 104 %endif
105 105 </div>
106 106 <div class="submit">
107 107 ${h.submit('sign_in',_('Log In'),class_="ui-btn xsmall")}
108 108 </div>
109 109 </div>
110 110 </div>
111 111 </div>
112 112 ${h.end_form()}
113 113 %else:
114 114 <div class="links_left">
115 115 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
116 116 <div class="email">${c.rhodecode_user.email}</div>
117 117 <div class="big_gravatar"><img alt="gravatar" src="${h.gravatar_url(c.rhodecode_user.email,48)}" /></div>
118 118 <div class="inbox"><a href="${h.url('notifications')}">${_('Inbox')}: ${c.unread_notifications}</a></div>
119 119 </div>
120 120 <div class="links_right">
121 121 <ol class="links">
122 122 <li>${h.link_to(_(u'Home'),h.url('home'))}</li>
123 123 <li>${h.link_to(_(u'Journal'),h.url('journal'))}</li>
124 124 <li>${h.link_to(_(u'My account'),h.url('admin_settings_my_account'))}</li>
125 125 <li class="logout">${h.link_to(_(u'Log Out'),h.url('logout_home'))}</li>
126 126 </ol>
127 127 </div>
128 128 %endif
129 129 </div>
130 130 </div>
131 131 </%def>
132 132
133 133 <%def name="menu(current=None)">
134 134 <%
135 135 def is_current(selected):
136 136 if selected == current:
137 137 return h.literal('class="current"')
138 138 %>
139 139 %if current not in ['home','admin']:
140 140 ##REGULAR MENU
141 141 <ul id="quick">
142 142 <!-- repo switcher -->
143 143 <li>
144 144 <a class="menu_link" id="repo_switcher" title="${_('Switch repository')}" href="#">
145 145 <span class="icon">
146 146 <img src="${h.url('/images/icons/database.png')}" alt="${_('Products')}" />
147 147 </span>
148 148 <span>&darr;</span>
149 149 </a>
150 150 <ul id="repo_switcher_list" class="repo_switcher">
151 151 <li>
152 152 <a href="#">${_('loading...')}</a>
153 153 </li>
154 154 </ul>
155 155 </li>
156 156
157 157 <li ${is_current('summary')}>
158 158 <a class="menu_link" title="${_('Summary')}" href="${h.url('summary_home',repo_name=c.repo_name)}">
159 159 <span class="icon">
160 160 <img src="${h.url('/images/icons/clipboard_16.png')}" alt="${_('Summary')}" />
161 161 </span>
162 162 <span>${_('Summary')}</span>
163 163 </a>
164 164 </li>
165 165 <li ${is_current('changelog')}>
166 166 <a class="menu_link" title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=c.repo_name)}">
167 167 <span class="icon">
168 168 <img src="${h.url('/images/icons/time.png')}" alt="${_('Changelog')}" />
169 169 </span>
170 170 <span>${_('Changelog')}</span>
171 171 </a>
172 172 </li>
173 173
174 174 <li ${is_current('switch_to')}>
175 175 <a class="menu_link" id="branch_tag_switcher" title="${_('Switch to')}" href="#">
176 176 <span class="icon">
177 177 <img src="${h.url('/images/icons/arrow_switch.png')}" alt="${_('Switch to')}" />
178 178 </span>
179 179 <span>${_('Switch to')}</span>
180 180 </a>
181 181 <ul id="switch_to_list" class="switch_to">
182 182 <li><a href="#">${_('loading...')}</a></li>
183 183 </ul>
184 184 </li>
185 185 <li ${is_current('files')}>
186 186 <a class="menu_link" title="${_('Files')}" href="${h.url('files_home',repo_name=c.repo_name)}">
187 187 <span class="icon">
188 188 <img src="${h.url('/images/icons/file.png')}" alt="${_('Files')}" />
189 189 </span>
190 190 <span>${_('Files')}</span>
191 191 </a>
192 192 </li>
193 193
194 194 <li ${is_current('options')}>
195 195 <a class="menu_link" title="${_('Options')}" href="#">
196 196 <span class="icon">
197 197 <img src="${h.url('/images/icons/table_gear.png')}" alt="${_('Admin')}" />
198 198 </span>
199 199 <span>${_('Options')}</span>
200 200 </a>
201 201 <ul>
202 202 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
203 203 %if h.HasPermissionAll('hg.admin')('access settings on repository'):
204 204 <li>${h.link_to(_('settings'),h.url('edit_repo',repo_name=c.repo_name),class_='settings')}</li>
205 205 %else:
206 206 <li>${h.link_to(_('settings'),h.url('repo_settings_home',repo_name=c.repo_name),class_='settings')}</li>
207 207 %endif
208 208 %endif
209 209 <li>${h.link_to(_('fork'),h.url('repo_fork_home',repo_name=c.repo_name),class_='fork')}</li>
210 210 <li>${h.link_to(_('search'),h.url('search_repo',search_repo=c.repo_name),class_='search')}</li>
211 211
212 212 % if h.HasPermissionAll('hg.admin')('access admin main page'):
213 213 <li>
214 214 ${h.link_to(_('admin'),h.url('admin_home'),class_='admin')}
215 215 <%def name="admin_menu()">
216 216 <ul>
217 217 <li>${h.link_to(_('journal'),h.url('admin_home'),class_='journal')}</li>
218 218 <li>${h.link_to(_('repositories'),h.url('repos'),class_='repos')}</li>
219 219 <li>${h.link_to(_('repositories groups'),h.url('repos_groups'),class_='repos_groups')}</li>
220 220 <li>${h.link_to(_('users'),h.url('users'),class_='users')}</li>
221 221 <li>${h.link_to(_('users groups'),h.url('users_groups'),class_='groups')}</li>
222 222 <li>${h.link_to(_('permissions'),h.url('edit_permission',id='default'),class_='permissions')}</li>
223 223 <li>${h.link_to(_('ldap'),h.url('ldap_home'),class_='ldap')}</li>
224 224 <li class="last">${h.link_to(_('settings'),h.url('admin_settings'),class_='settings')}</li>
225 225 </ul>
226 226 </%def>
227 227
228 228 ${admin_menu()}
229 229 </li>
230 230 % endif
231 231 </ul>
232 232 </li>
233 233
234 234 <li>
235 235 <a class="menu_link" title="${_('Followers')}" href="${h.url('repo_followers_home',repo_name=c.repo_name)}">
236 236 <span class="icon_short">
237 237 <img src="${h.url('/images/icons/heart.png')}" alt="${_('Followers')}" />
238 238 </span>
239 239 <span id="current_followers_count" class="short">${c.repository_followers}</span>
240 240 </a>
241 241 </li>
242 242 <li>
243 243 <a class="menu_link" title="${_('Forks')}" href="${h.url('repo_forks_home',repo_name=c.repo_name)}">
244 244 <span class="icon_short">
245 245 <img src="${h.url('/images/icons/arrow_divide.png')}" alt="${_('Forks')}" />
246 246 </span>
247 247 <span class="short">${c.repository_forks}</span>
248 248 </a>
249 249 </li>
250 <li>
251 <a class="menu_link" title="${_('Pull requests')}" href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}">
252 <span class="icon_short">
253 <img src="${h.url('/images/icons/arrow_join.png')}" alt="${_('Pull requests')}" />
254 </span>
255 <span class="short">${c.repository_pull_requests}</span>
256 </a>
257 </li>
250 258 ${usermenu()}
251 259 </ul>
252 260 <script type="text/javascript">
253 261 YUE.on('repo_switcher','mouseover',function(){
254 262 function qfilter(){
255 263 var nodes = YUQ('ul#repo_switcher_list li a.repo_name');
256 264 var target = 'q_filter_rs';
257 265 var func = function(node){
258 266 return node.parentNode;
259 267 }
260 268 q_filter(target,nodes,func);
261 269 }
262 270 var loaded = YUD.hasClass('repo_switcher','loaded');
263 271 if(!loaded){
264 272 YUD.addClass('repo_switcher','loaded');
265 273 ypjax("${h.url('repo_switcher')}",'repo_switcher_list',
266 274 function(o){qfilter();},
267 275 function(o){YUD.removeClass('repo_switcher','loaded');}
268 276 ,null);
269 277 }
270 278 return false;
271 279 });
272 280
273 281 YUE.on('branch_tag_switcher','mouseover',function(){
274 282 var loaded = YUD.hasClass('branch_tag_switcher','loaded');
275 283 if(!loaded){
276 284 YUD.addClass('branch_tag_switcher','loaded');
277 285 ypjax("${h.url('branch_tag_switcher',repo_name=c.repo_name)}",'switch_to_list',
278 286 function(o){},
279 287 function(o){YUD.removeClass('branch_tag_switcher','loaded');}
280 288 ,null);
281 289 }
282 290 return false;
283 291 });
284 292 </script>
285 293 %else:
286 294 ##ROOT MENU
287 295 <ul id="quick">
288 296 <li>
289 297 <a class="menu_link" title="${_('Home')}" href="${h.url('home')}">
290 298 <span class="icon">
291 299 <img src="${h.url('/images/icons/home_16.png')}" alt="${_('Home')}" />
292 300 </span>
293 301 <span>${_('Home')}</span>
294 302 </a>
295 303 </li>
296 304 %if c.rhodecode_user.username != 'default':
297 305 <li>
298 306 <a class="menu_link" title="${_('Journal')}" href="${h.url('journal')}">
299 307 <span class="icon">
300 308 <img src="${h.url('/images/icons/book.png')}" alt="${_('Journal')}" />
301 309 </span>
302 310 <span>${_('Journal')}</span>
303 311 </a>
304 312 </li>
305 313 %else:
306 314 <li>
307 315 <a class="menu_link" title="${_('Public journal')}" href="${h.url('public_journal')}">
308 316 <span class="icon">
309 317 <img src="${h.url('/images/icons/book.png')}" alt="${_('Public journal')}" />
310 318 </span>
311 319 <span>${_('Public journal')}</span>
312 320 </a>
313 321 </li>
314 322 %endif
315 323 <li>
316 324 <a class="menu_link" title="${_('Search')}" href="${h.url('search')}">
317 325 <span class="icon">
318 326 <img src="${h.url('/images/icons/search_16.png')}" alt="${_('Search')}" />
319 327 </span>
320 328 <span>${_('Search')}</span>
321 329 </a>
322 330 </li>
323 331
324 332 %if h.HasPermissionAll('hg.admin')('access admin main page'):
325 333 <li ${is_current('admin')}>
326 334 <a class="menu_link" title="${_('Admin')}" href="${h.url('admin_home')}">
327 335 <span class="icon">
328 336 <img src="${h.url('/images/icons/cog_edit.png')}" alt="${_('Admin')}" />
329 337 </span>
330 338 <span>${_('Admin')}</span>
331 339 </a>
332 340 ${admin_menu()}
333 341 </li>
334 342 %endif
335 343 ${usermenu()}
336 344 </ul>
337 345 %endif
338 346 </%def>
@@ -1,32 +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 pull request ${c.pull_request} overview...
23 <h3>${_('Title')}: ${c.pull_request.title}</h3>
24 <div class="changeset-status-container" style="float:left;padding:0px 20px 20px 20px">
25 %if c.current_changeset_status:
26 <div title="${_('Changeset status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
27 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
28 %endif
29 </div>
30 <div style="padding:4px">
31 <div>${h.fmt_date(c.pull_request.created_on)}</div>
32 </div>
33
34 ##DIFF
35
36 <div class="table">
37 <div id="body" class="diffblock">
38 <div style="white-space:pre-wrap;padding:5px">${h.literal(c.pull_request.description)}</div>
39 </div>
40 <div id="changeset_compare_view_content">
41 ##CS
42 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Incoming changesets')}</div>
43 <%include file="/compare/compare_cs.html" />
44
45 ## FILES
46 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
47 <div class="cs_files">
48 %for fid, change, f, stat in c.files:
49 <div class="cs_${change}">
50 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
51 <div class="changes">${h.fancy_file_stats(stat)}</div>
52 </div>
53 %endfor
54 </div>
55 </div>
56 </div>
57 <script>
58 var _USERS_AC_DATA = ${c.users_array|n};
59 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
60 </script>
61
62 ## diff block
63 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
64 %for fid, change, f, stat in c.files:
65 ${diff_block.diff_block_simple([c.changes[fid]])}
66 %endfor
67
68 ## template for inline comment form
69 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
70 ##${comment.comment_inline_form(c.changeset)}
71
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)}
24 75
25 76 </div>
26 77
27 78 <script type="text/javascript">
28 79
29 80
30 81 </script>
31 82
32 83 </%def>
General Comments 0
You need to be logged in to leave comments. Login now