##// END OF EJS Templates
Added basic models for saving open pull requests...
marcink -
r2434:f2946967 codereview
parent child Browse files
Show More
@@ -0,0 +1,79 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.model.pull_reuquest
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 pull request model for RhodeCode
7
8 :created_on: Jun 6, 2012
9 :author: marcink
10 :copyright: (C) 2012-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 import logging
27 from pylons.i18n.translation import _
28
29 from rhodecode.lib import helpers as h
30 from rhodecode.model import BaseModel
31 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification
32 from rhodecode.model.notification import NotificationModel
33 from rhodecode.lib.utils2 import safe_unicode
34
35 log = logging.getLogger(__name__)
36
37
38 class PullRequestModel(BaseModel):
39
40 def create(self, created_by, org_repo, org_ref, other_repo,
41 other_ref, revisions, reviewers, title, description=None):
42
43 new = PullRequest()
44 new.org_repo = self._get_repo(org_repo)
45 new.org_ref = org_ref
46 new.other_repo = self._get_repo(other_repo)
47 new.other_ref = other_ref
48 new.revisions = revisions
49 new.title = title
50 new.description = description
51
52 self.sa.add(new)
53
54 #members
55 for member in reviewers:
56 _usr = self._get_user(member)
57 reviewer = PullRequestReviewers(_usr, new)
58 self.sa.add(reviewer)
59
60 #notification to reviewers
61 notif = NotificationModel()
62 created_by_user = self._get_user(created_by)
63 subject = safe_unicode(
64 h.link_to(
65 _('%(user)s wants you to review pull request #%(pr_id)s') % \
66 {'user': created_by_user.username,
67 'pr_id': new.pull_request_id},
68 h.url('pullrequest_show', repo_name=other_repo,
69 pull_request_id=new.pull_request_id,
70 qualified=True,
71 )
72 )
73 )
74 body = description
75 notif.create(created_by=created_by, subject=subject, body=body,
76 recipients=reviewers,
77 type_=Notification.TYPE_PULL_REQUEST,)
78
79 return new
@@ -0,0 +1,32 b''
1 <%inherit file="/base/base.html"/>
2
3 <%def name="title()">
4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
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 ${_('Pull request #%s') % c.pull_request.pull_request_id}
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 pull request ${c.pull_request} overview...
24
25 </div>
26
27 <script type="text/javascript">
28
29
30 </script>
31
32 </%def>
@@ -1,535 +1,546 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 '/{repo_name:.*}/pull-request/new',
440 controller='pullrequests', action='index',
441 conditions=dict(function=check_repo))
439 '/{repo_name:.*}/pull-request/new', controller='pullrequests',
440 action='index', conditions=dict(function=check_repo,
441 method=["GET"]))
442
443 rmap.connect('pullrequest',
444 '/{repo_name:.*}/pull-request/new', controller='pullrequests',
445 action='create', conditions=dict(function=check_repo,
446 method=["POST"]))
447
448 rmap.connect('pullrequest_show',
449 '/{repo_name:.*}/pull-request/{pull_request_id}',
450 controller='pullrequests',
451 action='show', conditions=dict(function=check_repo,
452 method=["GET"]))
442 453
443 454 rmap.connect('summary_home', '/{repo_name:.*}/summary',
444 455 controller='summary', conditions=dict(function=check_repo))
445 456
446 457 rmap.connect('shortlog_home', '/{repo_name:.*}/shortlog',
447 458 controller='shortlog', conditions=dict(function=check_repo))
448 459
449 460 rmap.connect('branches_home', '/{repo_name:.*}/branches',
450 461 controller='branches', conditions=dict(function=check_repo))
451 462
452 463 rmap.connect('tags_home', '/{repo_name:.*}/tags',
453 464 controller='tags', conditions=dict(function=check_repo))
454 465
455 466 rmap.connect('bookmarks_home', '/{repo_name:.*}/bookmarks',
456 467 controller='bookmarks', conditions=dict(function=check_repo))
457 468
458 469 rmap.connect('changelog_home', '/{repo_name:.*}/changelog',
459 470 controller='changelog', conditions=dict(function=check_repo))
460 471
461 472 rmap.connect('changelog_details', '/{repo_name:.*}/changelog_details/{cs}',
462 473 controller='changelog', action='changelog_details',
463 474 conditions=dict(function=check_repo))
464 475
465 476 rmap.connect('files_home', '/{repo_name:.*}/files/{revision}/{f_path:.*}',
466 477 controller='files', revision='tip', f_path='',
467 478 conditions=dict(function=check_repo))
468 479
469 480 rmap.connect('files_diff_home', '/{repo_name:.*}/diff/{f_path:.*}',
470 481 controller='files', action='diff', revision='tip', f_path='',
471 482 conditions=dict(function=check_repo))
472 483
473 484 rmap.connect('files_rawfile_home',
474 485 '/{repo_name:.*}/rawfile/{revision}/{f_path:.*}',
475 486 controller='files', action='rawfile', revision='tip',
476 487 f_path='', conditions=dict(function=check_repo))
477 488
478 489 rmap.connect('files_raw_home',
479 490 '/{repo_name:.*}/raw/{revision}/{f_path:.*}',
480 491 controller='files', action='raw', revision='tip', f_path='',
481 492 conditions=dict(function=check_repo))
482 493
483 494 rmap.connect('files_annotate_home',
484 495 '/{repo_name:.*}/annotate/{revision}/{f_path:.*}',
485 496 controller='files', action='index', revision='tip',
486 497 f_path='', annotate=True, conditions=dict(function=check_repo))
487 498
488 499 rmap.connect('files_edit_home',
489 500 '/{repo_name:.*}/edit/{revision}/{f_path:.*}',
490 501 controller='files', action='edit', revision='tip',
491 502 f_path='', conditions=dict(function=check_repo))
492 503
493 504 rmap.connect('files_add_home',
494 505 '/{repo_name:.*}/add/{revision}/{f_path:.*}',
495 506 controller='files', action='add', revision='tip',
496 507 f_path='', conditions=dict(function=check_repo))
497 508
498 509 rmap.connect('files_archive_home', '/{repo_name:.*}/archive/{fname}',
499 510 controller='files', action='archivefile',
500 511 conditions=dict(function=check_repo))
501 512
502 513 rmap.connect('files_nodelist_home',
503 514 '/{repo_name:.*}/nodelist/{revision}/{f_path:.*}',
504 515 controller='files', action='nodelist',
505 516 conditions=dict(function=check_repo))
506 517
507 518 rmap.connect('repo_settings_delete', '/{repo_name:.*}/settings',
508 519 controller='settings', action="delete",
509 520 conditions=dict(method=["DELETE"], function=check_repo))
510 521
511 522 rmap.connect('repo_settings_update', '/{repo_name:.*}/settings',
512 523 controller='settings', action="update",
513 524 conditions=dict(method=["PUT"], function=check_repo))
514 525
515 526 rmap.connect('repo_settings_home', '/{repo_name:.*}/settings',
516 527 controller='settings', action='index',
517 528 conditions=dict(function=check_repo))
518 529
519 530 rmap.connect('repo_fork_create_home', '/{repo_name:.*}/fork',
520 531 controller='forks', action='fork_create',
521 532 conditions=dict(function=check_repo, method=["POST"]))
522 533
523 534 rmap.connect('repo_fork_home', '/{repo_name:.*}/fork',
524 535 controller='forks', action='fork',
525 536 conditions=dict(function=check_repo))
526 537
527 538 rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
528 539 controller='forks', action='forks',
529 540 conditions=dict(function=check_repo))
530 541
531 542 rmap.connect('repo_followers_home', '/{repo_name:.*}/followers',
532 543 controller='followers', action='followers',
533 544 conditions=dict(function=check_repo))
534 545
535 546 return rmap
@@ -1,143 +1,145 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.compare
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 compare controller for pylons showoing differences between two
7 7 repos, branches, bookmarks or tips
8 8
9 9 :created_on: May 6, 2012
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 import binascii
29 29
30 30 from webob.exc import HTTPNotFound
31 31 from pylons import request, response, session, tmpl_context as c, url
32 32 from pylons.controllers.util import abort, redirect
33 33
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.base import BaseRepoController, render
36 36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 37 from rhodecode.lib import diffs
38 38
39 39 from rhodecode.model.db import Repository
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class CompareController(BaseRepoController):
45 45
46 46 @LoginRequired()
47 47 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
48 48 'repository.admin')
49 49 def __before__(self):
50 50 super(CompareController, self).__before__()
51 51
52 52 def _get_discovery(self, org_repo, org_ref, other_repo, other_ref):
53 53 from mercurial import discovery
54 54 other = org_repo._repo
55 55 repo = other_repo._repo
56 56 tip = other[org_ref[1]]
57 57 log.debug('Doing discovery for %s@%s vs %s@%s' % (
58 58 org_repo, org_ref, other_repo, other_ref)
59 59 )
60 60 log.debug('Filter heads are %s[%s]' % (tip, org_ref[1]))
61 61 tmp = discovery.findcommonincoming(
62 62 repo=repo, # other_repo we check for incoming
63 63 remote=other, # org_repo source for incoming
64 64 heads=[tip.node()],
65 65 force=False
66 66 )
67 67 return tmp
68 68
69 69 def _get_changesets(self, org_repo, org_ref, other_repo, other_ref, tmp):
70 70 changesets = []
71 71 #case two independent repos
72 72 if org_repo != other_repo:
73 73 common, incoming, rheads = tmp
74 74
75 75 if not incoming:
76 76 revs = []
77 77 else:
78 78 revs = org_repo._repo.changelog.findmissing(common, rheads)
79 79
80 80 for cs in reversed(map(binascii.hexlify, revs)):
81 81 changesets.append(org_repo.get_changeset(cs))
82 82 else:
83 83 revs = ['ancestors(%s) and not ancestors(%s)' % (org_ref[1],
84 84 other_ref[1])]
85 85 from mercurial import scmutil
86 86 out = scmutil.revrange(org_repo._repo, revs)
87 87 for cs in reversed(out):
88 88 changesets.append(org_repo.get_changeset(cs))
89 89
90 90 return changesets
91 91
92 92 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
93 93
94 94 org_repo = c.rhodecode_db_repo.repo_name
95 95 org_ref = (org_ref_type, org_ref)
96 96 other_ref = (other_ref_type, other_ref)
97 97 other_repo = request.GET.get('repo', org_repo)
98 98
99 99 c.swap_url = h.url('compare_url', repo_name=other_repo,
100 100 org_ref_type=other_ref[0], org_ref=other_ref[1],
101 101 other_ref_type=org_ref[0], other_ref=org_ref[1],
102 102 repo=org_repo)
103 103
104 104 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
105 105 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
106 106
107 107 if c.org_repo is None or c.other_repo is None:
108 108 log.error('Could not found repo %s or %s' % (org_repo, other_repo))
109 109 raise HTTPNotFound
110 110
111 111 discovery_data = self._get_discovery(org_repo.scm_instance,
112 112 org_ref,
113 113 other_repo.scm_instance,
114 114 other_ref)
115 115 c.cs_ranges = self._get_changesets(org_repo.scm_instance,
116 116 org_ref,
117 117 other_repo.scm_instance,
118 118 other_ref,
119 119 discovery_data)
120 120
121 121 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
122 122 c.cs_ranges])
123 # defines that we need hidden inputs with changesets
124 c.as_form = request.GET.get('as_form', False)
123 125 if request.environ.get('HTTP_X_PARTIAL_XHR'):
124 126 return render('compare/compare_cs.html')
125 127
126 128 c.org_ref = org_ref[1]
127 129 c.other_ref = other_ref[1]
128 130 # diff needs to have swapped org with other to generate proper diff
129 131 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
130 132 discovery_data)
131 133 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
132 134 _parsed = diff_processor.prepare()
133 135
134 136 c.files = []
135 137 c.changes = {}
136 138
137 139 for f in _parsed:
138 140 fid = h.FID('', f['filename'])
139 141 c.files.append([fid, f['operation'], f['filename'], f['stats']])
140 142 diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
141 143 c.changes[fid] = [f['operation'], f['filename'], diff]
142 144
143 145 return render('compare/compare_diff.html')
@@ -1,90 +1,133 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 27
28 28 from pylons import request, response, session, tmpl_context as c, url
29 29 from pylons.controllers.util import abort, redirect
30 30 from pylons.i18n.translation import _
31 31
32 32 from rhodecode.lib.base import BaseRepoController, render
33 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.model.db import User
34 from rhodecode.lib import helpers as h
35 from rhodecode.model.db import User, PullRequest
36 from rhodecode.model.pull_request import PullRequestModel
37 from rhodecode.model.meta import Session
35 38
36 39 log = logging.getLogger(__name__)
37 40
38 41
39 42 class PullrequestsController(BaseRepoController):
40 43
41 44 @LoginRequired()
42 45 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
43 46 'repository.admin')
44 47 def __before__(self):
45 48 super(PullrequestsController, self).__before__()
46 49
47 50 def _get_repo_refs(self, repo):
48 51 hist_l = []
49 52
50 53 branches_group = ([('branch:' + k, k) for k in repo.branches.keys()],
51 54 _("Branches"))
52 55 bookmarks_group = ([('book:' + k, k) for k in repo.bookmarks.keys()],
53 56 _("Bookmarks"))
54 57 tags_group = ([('tag:' + k, k) for k in repo.tags.keys()],
55 58 _("Tags"))
56 59
57 60 hist_l.append(bookmarks_group)
58 61 hist_l.append(branches_group)
59 62 hist_l.append(tags_group)
60 63
61 64 return hist_l
62 65
63 66 def index(self):
64 67 org_repo = c.rhodecode_db_repo
65 68 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
66 69 c.org_repos = []
67 70 c.other_repos = []
68 71 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
69 72 org_repo.user.username, c.repo_name))
70 73 )
71 74
72 75 c.other_refs = c.org_refs
73 76 c.other_repos.extend(c.org_repos)
74
77 c.default_pull_request = org_repo.repo_name
75 78 #gather forks and add to this list
76 79 for fork in org_repo.forks:
77 80 c.other_repos.append((fork.repo_name, '%s/%s' % (
78 81 fork.user.username, fork.repo_name))
79 82 )
80 83 #add parents of this fork also
84 if org_repo.parent:
85 c.default_pull_request = org_repo.parent.repo_name
81 86 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
82 87 org_repo.parent.user.username,
83 88 org_repo.parent.repo_name))
84 89 )
85 90
86 91 #TODO: maybe the owner should be default ?
87 92 c.review_members = []
88 c.available_members = [(x.user_id, x.username) for x in
89 User.query().filter(User.username != 'default').all()]
93 c.available_members = []
94 for u in User.query().filter(User.username != 'default').all():
95 uname = u.username
96 if org_repo.user == u:
97 uname = _('%s (owner)' % u.username)
98 # auto add owner to pull-request recipients
99 c.review_members.append([u.user_id, uname])
100 c.available_members.append([u.user_id, uname])
90 101 return render('/pullrequests/pullrequest.html')
102
103 def create(self, repo_name):
104 req_p = request.POST
105 org_repo = req_p['org_repo']
106 org_ref = req_p['org_ref']
107 other_repo = req_p['other_repo']
108 other_ref = req_p['other_ref']
109 revisions = req_p.getall('revisions')
110 reviewers = req_p.getall('review_members')
111 #TODO: wrap this into a FORM !!!
112
113 title = req_p['pullrequest_title']
114 description = req_p['pullrequest_desc']
115
116 try:
117 model = PullRequestModel()
118 model.create(self.rhodecode_user.user_id, org_repo,
119 org_ref, other_repo, other_ref, revisions,
120 reviewers, title, description)
121 Session.commit()
122 h.flash(_('Pull request send'), category='success')
123 except Exception:
124 raise
125 h.flash(_('Error occured during sending pull request'),
126 category='error')
127 log.error(traceback.format_exc())
128
129 return redirect(url('changelog_home', repo_name=repo_name))
130
131 def show(self, repo_name, pull_request_id):
132 c.pull_request = PullRequest.get(pull_request_id)
133 return render('/pullrequests/pullrequest_show.html')
@@ -1,157 +1,158 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.comment
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 comments model for RhodeCode
7 7
8 8 :created_on: Nov 11, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons.i18n.translation import _
30 30 from sqlalchemy.util.compat import defaultdict
31 31
32 32 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import ChangesetComment, User, Repository, Notification
36 36 from rhodecode.model.notification import NotificationModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class ChangesetCommentsModel(BaseModel):
42 42
43 43 def __get_changeset_comment(self, changeset_comment):
44 44 return self._get_instance(ChangesetComment, changeset_comment)
45 45
46 46 def _extract_mentions(self, s):
47 47 user_objects = []
48 48 for username in extract_mentioned_users(s):
49 49 user_obj = User.get_by_username(username, case_insensitive=True)
50 50 if user_obj:
51 51 user_objects.append(user_obj)
52 52 return user_objects
53 53
54 54 def create(self, text, repo_id, user_id, revision, f_path=None,
55 55 line_no=None, status_change=None):
56 56 """
57 57 Creates new comment for changeset. IF status_change is not none
58 58 this comment is associated with a status change of changeset
59 59
60 60 :param text:
61 61 :param repo_id:
62 62 :param user_id:
63 63 :param revision:
64 64 :param f_path:
65 65 :param line_no:
66 66 :param status_change:
67 67 """
68 68
69 69 if text:
70 70 repo = Repository.get(repo_id)
71 71 cs = repo.scm_instance.get_changeset(revision)
72 72 desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
73 73 author_email = cs.author_email
74 74 comment = ChangesetComment()
75 75 comment.repo = repo
76 76 comment.user_id = user_id
77 77 comment.revision = revision
78 78 comment.text = text
79 79 comment.f_path = f_path
80 80 comment.line_no = line_no
81 81
82 82 self.sa.add(comment)
83 83 self.sa.flush()
84 84 # make notification
85 85 line = ''
86 86 if line_no:
87 87 line = _('on line %s') % line_no
88 88 subj = safe_unicode(
89 89 h.link_to('Re commit: %(commit_desc)s %(line)s' % \
90 90 {'commit_desc': desc, 'line': line},
91 91 h.url('changeset_home', repo_name=repo.repo_name,
92 92 revision=revision,
93 93 anchor='comment-%s' % comment.comment_id,
94 94 qualified=True,
95 95 )
96 96 )
97 97 )
98 98
99 99 body = text
100 100
101 101 # get the current participants of this changeset
102 102 recipients = ChangesetComment.get_users(revision=revision)
103 103
104 104 # add changeset author if it's in rhodecode system
105 105 recipients += [User.get_by_email(author_email)]
106 106
107 107 # create notification objects, and emails
108 108 NotificationModel().create(
109 109 created_by=user_id, subject=subj, body=body,
110 110 recipients=recipients, type_=Notification.TYPE_CHANGESET_COMMENT,
111 111 email_kwargs={'status_change': status_change}
112 112 )
113 113
114 114 mention_recipients = set(self._extract_mentions(body))\
115 115 .difference(recipients)
116 116 if mention_recipients:
117 117 subj = _('[Mention]') + ' ' + subj
118 118 NotificationModel().create(
119 119 created_by=user_id, subject=subj, body=body,
120 120 recipients=mention_recipients,
121 type_=Notification.TYPE_CHANGESET_COMMENT
121 type_=Notification.TYPE_CHANGESET_COMMENT,
122 email_kwargs={'status_change': status_change}
122 123 )
123 124
124 125 return comment
125 126
126 127 def delete(self, comment):
127 128 """
128 129 Deletes given comment
129 130
130 131 :param comment_id:
131 132 """
132 133 comment = self.__get_changeset_comment(comment)
133 134 self.sa.delete(comment)
134 135
135 136 return comment
136 137
137 138 def get_comments(self, repo_id, revision):
138 139 return ChangesetComment.query()\
139 140 .filter(ChangesetComment.repo_id == repo_id)\
140 141 .filter(ChangesetComment.revision == revision)\
141 142 .filter(ChangesetComment.line_no == None)\
142 143 .filter(ChangesetComment.f_path == None).all()
143 144
144 145 def get_inline_comments(self, repo_id, revision):
145 146 comments = self.sa.query(ChangesetComment)\
146 147 .filter(ChangesetComment.repo_id == repo_id)\
147 148 .filter(ChangesetComment.revision == revision)\
148 149 .filter(ChangesetComment.line_no != None)\
149 150 .filter(ChangesetComment.f_path != None)\
150 151 .order_by(ChangesetComment.comment_id.asc())\
151 152 .all()
152 153
153 154 paths = defaultdict(lambda: defaultdict(list))
154 155
155 156 for co in comments:
156 157 paths[co.f_path][co.line_no].append(co)
157 158 return paths.items()
@@ -1,1471 +1,1527 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 1325 revision = Column('revision', String(40), nullable=False)
1326 1326 line_no = Column('line_no', Unicode(10), nullable=True)
1327 1327 f_path = Column('f_path', Unicode(1000), nullable=True)
1328 1328 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1329 1329 text = Column('text', Unicode(25000), nullable=False)
1330 1330 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1331 1331
1332 1332 author = relationship('User', lazy='joined')
1333 1333 repo = relationship('Repository')
1334 1334 status_change = relationship('ChangesetStatus', uselist=False)
1335 1335
1336 1336 @classmethod
1337 1337 def get_users(cls, revision):
1338 1338 """
1339 1339 Returns user associated with this changesetComment. ie those
1340 1340 who actually commented
1341 1341
1342 1342 :param cls:
1343 1343 :param revision:
1344 1344 """
1345 1345 return Session.query(User)\
1346 1346 .filter(cls.revision == revision)\
1347 1347 .join(ChangesetComment.author).all()
1348 1348
1349 1349
1350 1350 class ChangesetStatus(Base, BaseModel):
1351 1351 __tablename__ = 'changeset_statuses'
1352 1352 __table_args__ = (
1353 1353 UniqueConstraint('repo_id', 'revision', 'version'),
1354 1354 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1355 1355 'mysql_charset': 'utf8'}
1356 1356 )
1357 1357
1358 1358 STATUSES = [
1359 1359 ('not_reviewed', _("Not Reviewed")), # (no icon) and default
1360 1360 ('approved', _("Approved")),
1361 1361 ('rejected', _("Rejected")),
1362 1362 ('under_review', _("Under Review")),
1363 1363 ]
1364 1364 DEFAULT = STATUSES[0][0]
1365 1365
1366 1366 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1367 1367 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1368 1368 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1369 1369 revision = Column('revision', String(40), nullable=False)
1370 1370 status = Column('status', String(128), nullable=False, default=DEFAULT)
1371 1371 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1372 1372 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1373 1373 version = Column('version', Integer(), nullable=False, default=0)
1374 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1375
1374 1376 author = relationship('User', lazy='joined')
1375 1377 repo = relationship('Repository')
1376 1378 comment = relationship('ChangesetComment', lazy='joined')
1379 pull_request = relationship('PullRequest', lazy='joined')
1377 1380
1378 1381 @classmethod
1379 1382 def get_status_lbl(cls, value):
1380 1383 return dict(cls.STATUSES).get(value)
1381 1384
1382 1385 @property
1383 1386 def status_lbl(self):
1384 1387 return ChangesetStatus.get_status_lbl(self.status)
1385 1388
1386 1389
1390 class PullRequest(Base, BaseModel):
1391 __tablename__ = 'pull_requests'
1392 __table_args__ = (
1393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1394 'mysql_charset': 'utf8'},
1395 )
1396
1397 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1398 title = Column('title', Unicode(256), nullable=True)
1399 description = Column('description', Unicode(10240), nullable=True)
1400 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1401 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1402 org_ref = Column('org_ref', Unicode(256), nullable=False)
1403 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1404 other_ref = Column('other_ref', Unicode(256), nullable=False)
1405
1406 @hybrid_property
1407 def revisions(self):
1408 return self._revisions.split(':')
1409
1410 @revisions.setter
1411 def revisions(self, val):
1412 self._revisions = ':'.join(val)
1413
1414 reviewers = relationship('PullRequestReviewers')
1415 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1416 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1417
1418 def __json__(self):
1419 return dict(
1420 revisions=self.revisions
1421 )
1422
1423
1424 class PullRequestReviewers(Base, BaseModel):
1425 __tablename__ = 'pull_request_reviewers'
1426 __table_args__ = (
1427 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1428 'mysql_charset': 'utf8'},
1429 )
1430
1431 def __init__(self, user=None, pull_request=None):
1432 self.user = user
1433 self.pull_request = pull_request
1434
1435 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1436 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1437 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1438
1439 user = relationship('User')
1440 pull_request = relationship('PullRequest')
1441
1442
1387 1443 class Notification(Base, BaseModel):
1388 1444 __tablename__ = 'notifications'
1389 1445 __table_args__ = (
1390 1446 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1391 1447 'mysql_charset': 'utf8'},
1392 1448 )
1393 1449
1394 1450 TYPE_CHANGESET_COMMENT = u'cs_comment'
1395 1451 TYPE_MESSAGE = u'message'
1396 1452 TYPE_MENTION = u'mention'
1397 1453 TYPE_REGISTRATION = u'registration'
1398 1454 TYPE_PULL_REQUEST = u'pull_request'
1399 1455
1400 1456 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1401 1457 subject = Column('subject', Unicode(512), nullable=True)
1402 1458 body = Column('body', Unicode(50000), nullable=True)
1403 1459 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1404 1460 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1405 1461 type_ = Column('type', Unicode(256))
1406 1462
1407 1463 created_by_user = relationship('User')
1408 1464 notifications_to_users = relationship('UserNotification', lazy='joined',
1409 1465 cascade="all, delete, delete-orphan")
1410 1466
1411 1467 @property
1412 1468 def recipients(self):
1413 1469 return [x.user for x in UserNotification.query()\
1414 1470 .filter(UserNotification.notification == self)\
1415 1471 .order_by(UserNotification.user).all()]
1416 1472
1417 1473 @classmethod
1418 1474 def create(cls, created_by, subject, body, recipients, type_=None):
1419 1475 if type_ is None:
1420 1476 type_ = Notification.TYPE_MESSAGE
1421 1477
1422 1478 notification = cls()
1423 1479 notification.created_by_user = created_by
1424 1480 notification.subject = subject
1425 1481 notification.body = body
1426 1482 notification.type_ = type_
1427 1483 notification.created_on = datetime.datetime.now()
1428 1484
1429 1485 for u in recipients:
1430 1486 assoc = UserNotification()
1431 1487 assoc.notification = notification
1432 1488 u.notifications.append(assoc)
1433 1489 Session.add(notification)
1434 1490 return notification
1435 1491
1436 1492 @property
1437 1493 def description(self):
1438 1494 from rhodecode.model.notification import NotificationModel
1439 1495 return NotificationModel().make_description(self)
1440 1496
1441 1497
1442 1498 class UserNotification(Base, BaseModel):
1443 1499 __tablename__ = 'user_to_notification'
1444 1500 __table_args__ = (
1445 1501 UniqueConstraint('user_id', 'notification_id'),
1446 1502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1447 1503 'mysql_charset': 'utf8'}
1448 1504 )
1449 1505 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1450 1506 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1451 1507 read = Column('read', Boolean, default=False)
1452 1508 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1453 1509
1454 1510 user = relationship('User', lazy="joined")
1455 1511 notification = relationship('Notification', lazy="joined",
1456 1512 order_by=lambda: Notification.created_on.desc(),)
1457 1513
1458 1514 def mark_as_read(self):
1459 1515 self.read = True
1460 1516 Session.add(self)
1461 1517
1462 1518
1463 1519 class DbMigrateVersion(Base, BaseModel):
1464 1520 __tablename__ = 'db_migrate_version'
1465 1521 __table_args__ = (
1466 1522 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1467 1523 'mysql_charset': 'utf8'},
1468 1524 )
1469 1525 repository_id = Column('repository_id', String(250), primary_key=True)
1470 1526 repository_path = Column('repository_path', Text)
1471 1527 version = Column('version', Integer)
@@ -1,255 +1,256 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.notification
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Model for notifications
7 7
8 8
9 9 :created_on: Nov 20, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30 import datetime
31 31
32 32 from pylons.i18n.translation import _
33 33
34 34 import rhodecode
35 35 from rhodecode.config.conf import DATETIME_FORMAT
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.model import BaseModel
38 38 from rhodecode.model.db import Notification, User, UserNotification
39 39 from sqlalchemy.orm import joinedload
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class NotificationModel(BaseModel):
45 45
46 46 def __get_notification(self, notification):
47 47 if isinstance(notification, Notification):
48 48 return notification
49 49 elif isinstance(notification, (int, long)):
50 50 return Notification.get(notification)
51 51 else:
52 52 if notification:
53 53 raise Exception('notification must be int, long or Instance'
54 54 ' of Notification got %s' % type(notification))
55 55
56 56 def create(self, created_by, subject, body, recipients=None,
57 57 type_=Notification.TYPE_MESSAGE, with_email=True,
58 58 email_kwargs={}):
59 59 """
60 60
61 61 Creates notification of given type
62 62
63 63 :param created_by: int, str or User instance. User who created this
64 64 notification
65 65 :param subject:
66 66 :param body:
67 67 :param recipients: list of int, str or User objects, when None
68 68 is given send to all admins
69 69 :param type_: type of notification
70 70 :param with_email: send email with this notification
71 71 :param email_kwargs: additional dict to pass as args to email template
72 72 """
73 73 from rhodecode.lib.celerylib import tasks, run_task
74 74
75 75 if recipients and not getattr(recipients, '__iter__', False):
76 76 raise Exception('recipients must be a list of iterable')
77 77
78 78 created_by_obj = self._get_user(created_by)
79 79
80 80 if recipients:
81 81 recipients_objs = []
82 82 for u in recipients:
83 83 obj = self._get_user(u)
84 84 if obj:
85 85 recipients_objs.append(obj)
86 86 recipients_objs = set(recipients_objs)
87 87 log.debug('sending notifications %s to %s' % (
88 88 type_, recipients_objs)
89 89 )
90 90 else:
91 91 # empty recipients means to all admins
92 92 recipients_objs = User.query().filter(User.admin == True).all()
93 93 log.debug('sending notifications %s to admins: %s' % (
94 94 type_, recipients_objs)
95 95 )
96 96 notif = Notification.create(
97 97 created_by=created_by_obj, subject=subject,
98 98 body=body, recipients=recipients_objs, type_=type_
99 99 )
100 100
101 101 if with_email is False:
102 102 return notif
103 103
104 104 #don't send email to person who created this comment
105 105 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
106 106
107 107 # send email with notification to all other participants
108 108 for rec in rec_objs:
109 109 email_subject = NotificationModel().make_description(notif, False)
110 110 type_ = type_
111 111 email_body = body
112 112 ## this is passed into template
113 113 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
114 114 kwargs.update(email_kwargs)
115 115 email_body_html = EmailNotificationModel()\
116 116 .get_email_tmpl(type_, **kwargs)
117 117
118 118 run_task(tasks.send_email, rec.email, email_subject, email_body,
119 119 email_body_html)
120 120
121 121 return notif
122 122
123 123 def delete(self, user, notification):
124 124 # we don't want to remove actual notification just the assignment
125 125 try:
126 126 notification = self.__get_notification(notification)
127 127 user = self._get_user(user)
128 128 if notification and user:
129 129 obj = UserNotification.query()\
130 130 .filter(UserNotification.user == user)\
131 131 .filter(UserNotification.notification
132 132 == notification)\
133 133 .one()
134 134 self.sa.delete(obj)
135 135 return True
136 136 except Exception:
137 137 log.error(traceback.format_exc())
138 138 raise
139 139
140 140 def get_for_user(self, user, filter_=None):
141 141 """
142 142 Get mentions for given user, filter them if filter dict is given
143 143
144 144 :param user:
145 145 :type user:
146 146 :param filter:
147 147 """
148 148 user = self._get_user(user)
149 149
150 150 q = UserNotification.query()\
151 151 .filter(UserNotification.user == user)\
152 152 .join((Notification, UserNotification.notification_id ==
153 153 Notification.notification_id))
154 154
155 155 if filter_:
156 156 q = q.filter(Notification.type_ == filter_.get('type'))
157 157
158 158 return q.all()
159 159
160 160 def mark_all_read_for_user(self, user, filter_=None):
161 161 user = self._get_user(user)
162 162 q = UserNotification.query()\
163 163 .filter(UserNotification.user == user)\
164 164 .filter(UserNotification.read == False)\
165 165 .join((Notification, UserNotification.notification_id ==
166 166 Notification.notification_id))
167 167 if filter_:
168 168 q = q.filter(Notification.type_ == filter_.get('type'))
169 169
170 170 # this is a little inefficient but sqlalchemy doesn't support
171 171 # update on joined tables :(
172 172 for obj in q.all():
173 173 obj.read = True
174 174 self.sa.add(obj)
175 175
176 176 def get_unread_cnt_for_user(self, user):
177 177 user = self._get_user(user)
178 178 return UserNotification.query()\
179 179 .filter(UserNotification.read == False)\
180 180 .filter(UserNotification.user == user).count()
181 181
182 182 def get_unread_for_user(self, user):
183 183 user = self._get_user(user)
184 184 return [x.notification for x in UserNotification.query()\
185 185 .filter(UserNotification.read == False)\
186 186 .filter(UserNotification.user == user).all()]
187 187
188 188 def get_user_notification(self, user, notification):
189 189 user = self._get_user(user)
190 190 notification = self.__get_notification(notification)
191 191
192 192 return UserNotification.query()\
193 193 .filter(UserNotification.notification == notification)\
194 194 .filter(UserNotification.user == user).scalar()
195 195
196 196 def make_description(self, notification, show_age=True):
197 197 """
198 198 Creates a human readable description based on properties
199 199 of notification object
200 200 """
201 201
202 202 _map = {
203 203 notification.TYPE_CHANGESET_COMMENT: _('commented on commit'),
204 204 notification.TYPE_MESSAGE: _('sent message'),
205 205 notification.TYPE_MENTION: _('mentioned you'),
206 206 notification.TYPE_REGISTRATION: _('registered in RhodeCode'),
207 207 notification.TYPE_PULL_REQUEST: _('opened new pull request')
208 208 }
209 209
210 210 tmpl = "%(user)s %(action)s %(when)s"
211 211 if show_age:
212 212 when = h.age(notification.created_on)
213 213 else:
214 214 DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
215 215 when = DTF(notification.created_on)
216 216
217 217 data = dict(
218 218 user=notification.created_by_user.username,
219 219 action=_map[notification.type_], when=when,
220 220 )
221 221 return tmpl % data
222 222
223 223
224 224 class EmailNotificationModel(BaseModel):
225 225
226 226 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
227 227 TYPE_PASSWORD_RESET = 'passoword_link'
228 228 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
229 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
229 230 TYPE_DEFAULT = 'default'
230 231
231 232 def __init__(self):
232 233 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
233 234 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
234 235
235 236 self.email_types = {
236 237 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
237 238 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
238 239 self.TYPE_REGISTRATION: 'email_templates/registration.html',
239 240 self.TYPE_DEFAULT: 'email_templates/default.html'
240 241 }
241 242
242 243 def get_email_tmpl(self, type_, **kwargs):
243 244 """
244 245 return generated template for email based on given type
245 246
246 247 :param type_:
247 248 """
248 249
249 250 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
250 251 email_template = self._tmpl_lookup.get_template(base)
251 252 # translator inject
252 253 _kwargs = {'_': _}
253 254 _kwargs.update(kwargs)
254 255 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
255 256 return email_template.render(**_kwargs)
@@ -1,23 +1,27 b''
1 1 ## Changesets table !
2 2 <div class="container">
3 3 <table class="compare_view_commits noborder">
4 4 %if not c.cs_ranges:
5 5 <tr><td>${_('No changesets')}</td></tr>
6 6 %else:
7 7 %for cnt, cs in enumerate(c.cs_ranges):
8 8 <tr>
9 9 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),14)}"/></div></td>
10 10 <td>
11 11 %if cs.raw_id in c.statuses:
12 12 <div title="${c.statuses[cs.raw_id][1]}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cs.raw_id][0])}" /></div>
13 13 %endif
14 14 </td>
15 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</td>
15 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
16 %if c.as_form:
17 ${h.hidden('revisions',cs.raw_id)}
18 %endif
19 </td>
16 20 <td><div class="author">${h.person(cs.author)}</div></td>
17 21 <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
18 22 <td><div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name)}</div></td>
19 23 </tr>
20 24 %endfor
21 25 %endif
22 26 </table>
23 </div> No newline at end of file
27 </div>
@@ -1,77 +1,77 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${c.repo_name} ${_('Compare')} ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(u'Home',h.url('/'))}
10 10 &raquo;
11 11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 12 &raquo;
13 13 ${_('Compare')}
14 14 </%def>
15 15
16 16 <%def name="page_nav()">
17 17 ${self.menu('changelog')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21 <div class="box">
22 22 <!-- box / title -->
23 23 <div class="title">
24 24 ${self.breadcrumbs()}
25 25 </div>
26 26 <div class="table">
27 27 <div id="body" class="diffblock">
28 28 <div class="code-header cv">
29 29 <h3 class="code-header-title">${_('Compare View')}</h3>
30 30 <div>
31 31 ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)} <a href="${c.swap_url}">[swap]</a>
32 32 </div>
33 33 </div>
34 34 </div>
35 35 <div id="changeset_compare_view_content">
36 36 ##CS
37 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Changesets')}</div>
37 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Outgoing changesets')}</div>
38 38 <%include file="compare_cs.html" />
39 39
40 40 ## FILES
41 41 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
42 42 <div class="cs_files">
43 43 %for fid, change, f, stat in c.files:
44 44 <div class="cs_${change}">
45 45 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
46 46 <div class="changes">${h.fancy_file_stats(stat)}</div>
47 47 </div>
48 48 %endfor
49 49 </div>
50 50 </div>
51 51 </div>
52 52
53 53 ## diff block
54 54 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
55 55 %for fid, change, f, stat in c.files:
56 56 ${diff_block.diff_block_simple([c.changes[fid]])}
57 57 %endfor
58 58
59 59 <script type="text/javascript">
60 60
61 61 YUE.onDOMReady(function(){
62 62
63 63 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
64 64 var act = e.currentTarget.nextElementSibling;
65 65
66 66 if(YUD.hasClass(act,'active')){
67 67 YUD.removeClass(act,'active');
68 68 YUD.setStyle(act,'display','none');
69 69 }else{
70 70 YUD.addClass(act,'active');
71 71 YUD.setStyle(act,'display','');
72 72 }
73 73 });
74 74 })
75 75 </script>
76 76 </div>
77 77 </%def>
@@ -1,192 +1,192 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 ${c.repo_name} ${_('Pull request')}
4 ${c.repo_name} ${_('New pull request')}
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 ${_('Pull request')}
12 ${_('New pull request')}
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 ${h.form(url('#'),method='put', id='pull_request_form')}
23 <div style="float:left;padding:30px">
22 ${h.form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
23 <div style="float:left;padding:0px 30px 30px 30px">
24 <div style="padding:0px 5px 5px 5px">
25 <span>
26 <a id="refresh" href="#">
27 <img class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/>
28 ${_('refresh overview')}
29 </a>
30 </span>
31 </div>
24 32 ##ORG
25 33 <div style="float:left">
26 34 <div class="fork_user">
27 35 <div class="gravatar">
28 36 <img alt="gravatar" src="${h.gravatar_url(c.rhodecode_db_repo.user.email,24)}"/>
29 37 </div>
30 38 <span style="font-size: 20px">
31 39 ${h.select('org_repo','',c.org_repos,class_='refs')}:${h.select('org_ref','',c.org_refs,class_='refs')}
32 40 </span>
33 41 <div style="padding:5px 3px 3px 42px;">${c.rhodecode_db_repo.description}</div>
34 42 </div>
35 43 <div style="clear:both;padding-top: 10px"></div>
36 44 </div>
37 45 <div style="float:left;font-size:24px;padding:0px 20px">
38 46 <img height=32 width=32 src="${h.url('/images/arrow_right_64.png')}"/>
39 47 </div>
40 48
41 49 ##OTHER, most Probably the PARENT OF THIS FORK
42 50 <div style="float:left">
43 51 <div class="fork_user">
44 52 <div class="gravatar">
45 53 <img alt="gravatar" src="${h.gravatar_url(c.rhodecode_db_repo.user.email,24)}"/>
46 54 </div>
47 55 <span style="font-size: 20px">
48 ${h.select('other_repo','',c.other_repos,class_='refs')}:${h.select('other_ref','',c.other_refs,class_='refs')}
56 ${h.select('other_repo',c.default_pull_request ,c.other_repos,class_='refs')}:${h.select('other_ref','',c.other_refs,class_='refs')}
49 57 </span>
50 58 <div style="padding:5px 3px 3px 42px;">${c.rhodecode_db_repo.description}</div>
51 59 </div>
52 60 <div style="clear:both;padding-top: 10px"></div>
53 61 </div>
54 <div style="float:left;padding:5px 5px 5px 15px">
55 <span>
56 <a id="refresh" href="#">
57 <img class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/>
58 ${_('refresh overview')}
59 </a>
60 </span>
61 </div>
62 62 <div style="clear:both;padding-top: 10px"></div>
63 <div style="float:left" id="pull_request_overview">
64 </div>
63 ## overview pulled by ajax
64 <div style="float:left" id="pull_request_overview"></div>
65 65 <div style="float:left;clear:both;padding:10px 10px 10px 0px;display:none">
66 66 <a id="pull_request_overview_url" href="#">${_('Detailed compare view')}</a>
67 67 </div>
68 68 </div>
69 69 <div style="float:left; border-left:1px dashed #eee">
70 70 <h4>${_('Pull request reviewers')}</h4>
71 71 <div id="reviewers" style="padding:0px 0px 0px 15px">
72 72 ##TODO: make this nicer :)
73 73 <table class="table noborder">
74 74 <tr>
75 75 <td>
76 76 <div>
77 77 <div style="float:left">
78 <div class="text" style="padding: 0px 0px 6px;">${_('Choosen reviewers')}</div>
78 <div class="text" style="padding: 0px 0px 6px;">${_('Chosen reviewers')}</div>
79 79 ${h.select('review_members',[x[0] for x in c.review_members],c.review_members,multiple=True,size=8,style="min-width:210px")}
80 80 <div id="remove_all_elements" style="cursor:pointer;text-align:center">
81 81 ${_('Remove all elements')}
82 82 <img alt="remove" style="vertical-align:text-bottom" src="${h.url('/images/icons/arrow_right.png')}"/>
83 83 </div>
84 84 </div>
85 85 <div style="float:left;width:20px;padding-top:50px">
86 86 <img alt="add" id="add_element"
87 87 style="padding:2px;cursor:pointer"
88 88 src="${h.url('/images/icons/arrow_left.png')}"/>
89 89 <br />
90 90 <img alt="remove" id="remove_element"
91 91 style="padding:2px;cursor:pointer"
92 92 src="${h.url('/images/icons/arrow_right.png')}"/>
93 93 </div>
94 94 <div style="float:left">
95 95 <div class="text" style="padding: 0px 0px 6px;">${_('Available reviewers')}</div>
96 96 ${h.select('available_members',[],c.available_members,multiple=True,size=8,style="min-width:210px")}
97 97 <div id="add_all_elements" style="cursor:pointer;text-align:center">
98 98 <img alt="add" style="vertical-align:text-bottom" src="${h.url('/images/icons/arrow_left.png')}"/>
99 99 ${_('Add all elements')}
100 100 </div>
101 101 </div>
102 102 </div>
103 103 </td>
104 104 </tr>
105 105 </table>
106 106 </div>
107 107 </div>
108 108 <h3>${_('Create new pull request')}</h3>
109 109
110 110 <div class="form">
111 111 <!-- fields -->
112 112
113 113 <div class="fields">
114 114
115 115 <div class="field">
116 116 <div class="label">
117 117 <label for="pullrequest_title">${_('Title')}:</label>
118 118 </div>
119 119 <div class="input">
120 120 ${h.text('pullrequest_title',size=30)}
121 121 </div>
122 122 </div>
123 123
124 124 <div class="field">
125 125 <div class="label label-textarea">
126 126 <label for="pullrequest_desc">${_('description')}:</label>
127 127 </div>
128 128 <div class="textarea text-area editor">
129 129 ${h.textarea('pullrequest_desc',size=30)}
130 130 </div>
131 131 </div>
132 132
133 133 <div class="buttons">
134 134 ${h.submit('save',_('Send pull request'),class_="ui-button")}
135 135 ${h.reset('reset',_('Reset'),class_="ui-button")}
136 136 </div>
137 137 </div>
138 138 </div>
139 139 ${h.end_form()}
140 140
141 141 </div>
142 142
143 143 <script type="text/javascript">
144 144 MultiSelectWidget('review_members','available_members','pull_request_form');
145 145
146 146 var loadPreview = function(){
147 147 YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display','none');
148 148 var url = "${h.url('compare_url',
149 149 repo_name='org_repo',
150 150 org_ref_type='branch', org_ref='org_ref',
151 151 other_ref_type='branch', other_ref='other_ref',
152 repo='other_repo')}";
152 repo='other_repo',
153 as_form=True)}";
153 154
154 155 var select_refs = YUQ('#pull_request_form select.refs')
155 156
156 157 for(var i=0;i<select_refs.length;i++){
157 158 var select_ref = select_refs[i];
158 159 var select_ref_data = select_ref.value.split(':');
159 160 var key = null;
160 161 var val = null;
161 162 if(select_ref_data.length>1){
162 163 key = select_ref.name+"_type";
163 164 val = select_ref_data[0];
164 165 url = url.replace(key,val);
165 166
166 167 key = select_ref.name;
167 168 val = select_ref_data[1];
168 169 url = url.replace(key,val);
169 170
170 171 }else{
171 172 key = select_ref.name;
172 173 val = select_ref.value;
173 174 url = url.replace(key,val);
174 175 }
175 176 }
176 177
177 178 ypjax(url,'pull_request_overview', function(data){
178 179 YUD.get('pull_request_overview_url').href = url;
179 180 YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display','');
180 181 })
181 182 }
182 183 YUE.on('refresh','click',function(e){
183 184 loadPreview()
184 185 })
185 186
186 //lazy load after 0.5
187
187 //lazy load overview after 0.5s
188 188 setTimeout(loadPreview,500)
189 189
190 190 </script>
191 191
192 192 </%def>
@@ -1,7 +1,52 b''
1 1 from rhodecode.tests import *
2 2
3
3 4 class TestCompareController(TestController):
4 5
5 def test_index(self):
6 response = self.app.get(url(controller='compare', action='index'))
7 # Test response...
6 def test_index_tag(self):
7 self.log_user()
8 tag1='0.1.3'
9 tag2='0.1.2'
10 response = self.app.get(url(controller='compare', action='index',
11 repo_name=HG_REPO,
12 org_ref_type="tag",
13 org_ref=tag1,
14 other_ref_type="tag",
15 other_ref=tag2,
16 ))
17 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, tag1, HG_REPO, tag2))
18 ## outgoing changesets between tags
19 response.mustcontain('''<a href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
20 response.mustcontain('''<a href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
21 response.mustcontain('''<a href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
22 response.mustcontain('''<a href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % HG_REPO)
23 response.mustcontain('''<a href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
24 response.mustcontain('''<a href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
25 response.mustcontain('''<a href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
26
27 ## files diff
28 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--1c5cf9e91c12">docs/api/utils/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
29 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--e3305437df55">test_and_report.sh</a></div>''' % (HG_REPO, tag1, tag2))
30 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--c8e92ef85cd1">.hgignore</a></div>''' % (HG_REPO, tag1, tag2))
31 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--6e08b694d687">.hgtags</a></div>''' % (HG_REPO, tag1, tag2))
32 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2c14b00f3393">docs/api/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
33 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--430ccbc82bdf">vcs/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
34 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--9c390eb52cd6">vcs/backends/hg.py</a></div>''' % (HG_REPO, tag1, tag2))
35 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--ebb592c595c0">vcs/utils/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
36 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--7abc741b5052">vcs/utils/annotate.py</a></div>''' % (HG_REPO, tag1, tag2))
37 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2ef0ef106c56">vcs/utils/diffs.py</a></div>''' % (HG_REPO, tag1, tag2))
38 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--3150cb87d4b7">vcs/utils/lazy.py</a></div>''' % (HG_REPO, tag1, tag2))
39
40 def test_index_branch(self):
41 self.log_user()
42 response = self.app.get(url(controller='compare', action='index',
43 repo_name=HG_REPO,
44 org_ref_type="branch",
45 org_ref='default',
46 other_ref_type="branch",
47 other_ref='default',
48 ))
49
50 response.mustcontain('%s@default -> %s@default' % (HG_REPO, HG_REPO))
51 # branch are equal
52 response.mustcontain('<tr><td>No changesets</td></tr>')
@@ -1,7 +1,9 b''
1 1 from rhodecode.tests import *
2 2
3
3 4 class TestPullrequestsController(TestController):
4 5
5 6 def test_index(self):
6 response = self.app.get(url(controller='pullrequests', action='index'))
7 # Test response...
7 self.log_user()
8 response = self.app.get(url(controller='pullrequests', action='index',
9 repo_name=HG_REPO))
General Comments 0
You need to be logged in to leave comments. Login now