##// END OF EJS Templates
notifications: ported views to pyramid
marcink -
r1920:d6d649b2 default
parent child Browse files
Show More
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -0,0 +1,198 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.httpexceptions import (
24 HTTPFound, HTTPNotFound, HTTPInternalServerError)
25 from pyramid.view import view_config
26
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
29
30 from rhodecode.lib import helpers as h
31 from rhodecode.lib.helpers import Page
32 from rhodecode.lib.utils2 import safe_int
33 from rhodecode.model.db import Notification
34 from rhodecode.model.notification import NotificationModel
35 from rhodecode.model.meta import Session
36
37
38 log = logging.getLogger(__name__)
39
40
41 class MyAccountNotificationsView(BaseAppView):
42
43 def load_default_context(self):
44 c = self._get_local_tmpl_context()
45 c.user = c.auth_user.get_instance()
46 self._register_global_c(c)
47 return c
48
49 def _has_permissions(self, notification):
50 def is_owner():
51 user_id = self._rhodecode_db_user.user_id
52 for user_notification in notification.notifications_to_users:
53 if user_notification.user.user_id == user_id:
54 return True
55 return False
56 return h.HasPermissionAny('hg.admin')() or is_owner()
57
58 @LoginRequired()
59 @NotAnonymous()
60 @view_config(
61 route_name='notifications_show_all', request_method='GET',
62 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
63 def notifications_show_all(self):
64 c = self.load_default_context()
65
66 c.unread_count = NotificationModel().get_unread_cnt_for_user(
67 self._rhodecode_db_user.user_id)
68
69 _current_filter = self.request.GET.getall('type') or ['unread']
70
71 notifications = NotificationModel().get_for_user(
72 self._rhodecode_db_user.user_id,
73 filter_=_current_filter)
74
75 p = safe_int(self.request.GET.get('page', 1), 1)
76
77 def url_generator(**kw):
78 _query = self.request.GET.mixed()
79 _query.update(kw)
80 return self.request.current_route_path(_query=_query)
81
82 c.notifications = Page(notifications, page=p, items_per_page=10,
83 url=url_generator)
84
85 c.unread_type = 'unread'
86 c.all_type = 'all'
87 c.pull_request_type = Notification.TYPE_PULL_REQUEST
88 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
89 Notification.TYPE_PULL_REQUEST_COMMENT]
90
91 c.current_filter = 'unread' # default filter
92
93 if _current_filter == [c.pull_request_type]:
94 c.current_filter = 'pull_request'
95 elif _current_filter == c.comment_type:
96 c.current_filter = 'comment'
97 elif _current_filter == [c.unread_type]:
98 c.current_filter = 'unread'
99 elif _current_filter == [c.all_type]:
100 c.current_filter = 'all'
101 return self._get_template_context(c)
102
103 @LoginRequired()
104 @NotAnonymous()
105 @CSRFRequired()
106 @view_config(
107 route_name='notifications_mark_all_read', request_method='POST',
108 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
109 def notifications_mark_all_read(self):
110 NotificationModel().mark_all_read_for_user(
111 self._rhodecode_db_user.user_id,
112 filter_=self.request.GET.getall('type'))
113 Session().commit()
114 raise HTTPFound(h.route_path('notifications_show_all'))
115
116 @LoginRequired()
117 @NotAnonymous()
118 @view_config(
119 route_name='notifications_show', request_method='GET',
120 renderer='rhodecode:templates/admin/notifications/notifications_show.mako')
121 def notifications_show(self):
122 c = self.load_default_context()
123 notification_id = self.request.matchdict['notification_id']
124 notification = Notification.get_or_404(notification_id)
125
126 if not self._has_permissions(notification):
127 log.debug('User %s does not have permission to access notification',
128 self._rhodecode_user)
129 raise HTTPNotFound()
130
131 u_notification = NotificationModel().get_user_notification(
132 self._rhodecode_db_user.user_id, notification)
133 if not u_notification:
134 log.debug('User %s notification does not exist',
135 self._rhodecode_user)
136 raise HTTPNotFound()
137
138 # when opening this notification, mark it as read for this use
139 if not u_notification.read:
140 u_notification.mark_as_read()
141 Session().commit()
142
143 c.notification = notification
144
145 return self._get_template_context(c)
146
147 @LoginRequired()
148 @NotAnonymous()
149 @CSRFRequired()
150 @view_config(
151 route_name='notifications_update', request_method='POST',
152 renderer='json_ext')
153 def notification_update(self):
154 notification_id = self.request.matchdict['notification_id']
155 notification = Notification.get_or_404(notification_id)
156
157 if not self._has_permissions(notification):
158 log.debug('User %s does not have permission to access notification',
159 self._rhodecode_user)
160 raise HTTPNotFound()
161
162 try:
163 # updates notification read flag
164 NotificationModel().mark_read(
165 self._rhodecode_user.user_id, notification)
166 Session().commit()
167 return 'ok'
168 except Exception:
169 Session().rollback()
170 log.exception("Exception updating a notification item")
171
172 raise HTTPInternalServerError()
173
174 @LoginRequired()
175 @NotAnonymous()
176 @CSRFRequired()
177 @view_config(
178 route_name='notifications_delete', request_method='POST',
179 renderer='json_ext')
180 def notification_delete(self):
181 notification_id = self.request.matchdict['notification_id']
182 notification = Notification.get_or_404(notification_id)
183 if not self._has_permissions(notification):
184 log.debug('User %s does not have permission to access notification',
185 self._rhodecode_user)
186 raise HTTPNotFound()
187
188 try:
189 # deletes only notification2user
190 NotificationModel().delete(
191 self._rhodecode_user.user_id, notification)
192 Session().commit()
193 return 'ok'
194 except Exception:
195 Session().rollback()
196 log.exception("Exception deleting a notification item")
197
198 raise HTTPInternalServerError()
@@ -1,104 +1,126 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 def includeme(config):
26 26
27 27 config.add_route(
28 28 name='my_account_profile',
29 29 pattern=ADMIN_PREFIX + '/my_account/profile')
30 30
31 31 # my account edit details
32 32 config.add_route(
33 33 name='my_account_edit',
34 34 pattern=ADMIN_PREFIX + '/my_account/edit')
35 35 config.add_route(
36 36 name='my_account_update',
37 37 pattern=ADMIN_PREFIX + '/my_account/update')
38 38
39 39 # my account password
40 40 config.add_route(
41 41 name='my_account_password',
42 42 pattern=ADMIN_PREFIX + '/my_account/password')
43 43
44 44 config.add_route(
45 45 name='my_account_password_update',
46 46 pattern=ADMIN_PREFIX + '/my_account/password')
47 47
48 48 # my account tokens
49 49 config.add_route(
50 50 name='my_account_auth_tokens',
51 51 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
52 52 config.add_route(
53 53 name='my_account_auth_tokens_add',
54 54 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new')
55 55 config.add_route(
56 56 name='my_account_auth_tokens_delete',
57 57 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete')
58 58
59 59 # my account emails
60 60 config.add_route(
61 61 name='my_account_emails',
62 62 pattern=ADMIN_PREFIX + '/my_account/emails')
63 63 config.add_route(
64 64 name='my_account_emails_add',
65 65 pattern=ADMIN_PREFIX + '/my_account/emails/new')
66 66 config.add_route(
67 67 name='my_account_emails_delete',
68 68 pattern=ADMIN_PREFIX + '/my_account/emails/delete')
69 69
70 70 config.add_route(
71 71 name='my_account_repos',
72 72 pattern=ADMIN_PREFIX + '/my_account/repos')
73 73
74 74 config.add_route(
75 75 name='my_account_watched',
76 76 pattern=ADMIN_PREFIX + '/my_account/watched')
77 77
78 78 config.add_route(
79 79 name='my_account_perms',
80 80 pattern=ADMIN_PREFIX + '/my_account/perms')
81 81
82 82 config.add_route(
83 83 name='my_account_notifications',
84 84 pattern=ADMIN_PREFIX + '/my_account/notifications')
85 85
86 86 config.add_route(
87 87 name='my_account_notifications_toggle_visibility',
88 88 pattern=ADMIN_PREFIX + '/my_account/toggle_visibility')
89 89
90 90 # my account pull requests
91 91 config.add_route(
92 92 name='my_account_pullrequests',
93 93 pattern=ADMIN_PREFIX + '/my_account/pull_requests')
94 94 config.add_route(
95 95 name='my_account_pullrequests_data',
96 96 pattern=ADMIN_PREFIX + '/my_account/pull_requests/data')
97 97
98 # notifications
99 config.add_route(
100 name='notifications_show_all',
101 pattern=ADMIN_PREFIX + '/notifications')
102
103 # notifications
104 config.add_route(
105 name='notifications_mark_all_read',
106 pattern=ADMIN_PREFIX + '/notifications/mark_all_read')
107
108 config.add_route(
109 name='notifications_show',
110 pattern=ADMIN_PREFIX + '/notifications/{notification_id}')
111
112 config.add_route(
113 name='notifications_update',
114 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/update')
115
116 config.add_route(
117 name='notifications_delete',
118 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/delete')
119
98 120 # channelstream test
99 121 config.add_route(
100 122 name='my_account_notifications_test_channelstream',
101 123 pattern=ADMIN_PREFIX + '/my_account/test_channelstream')
102 124
103 125 # Scan module for configuration decorators.
104 126 config.scan()
@@ -1,172 +1,196 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 from rhodecode.tests import *
23 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.tests import (
25 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
26 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
24 27 from rhodecode.tests.fixture import Fixture
25 28
26 29 from rhodecode.model.db import Notification, User
27 30 from rhodecode.model.user import UserModel
28 31 from rhodecode.model.notification import NotificationModel
29 32 from rhodecode.model.meta import Session
30 33
31 34 fixture = Fixture()
32 35
33 36
34 class TestNotificationsController(TestController):
35 destroy_users = set()
37 def route_path(name, params=None, **kwargs):
38 import urllib
39 from rhodecode.apps._base import ADMIN_PREFIX
36 40
37 @classmethod
38 def teardown_class(cls):
39 fixture.destroy_users(cls.destroy_users)
41 base_url = {
42 'notifications_show_all': ADMIN_PREFIX + '/notifications',
43 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications/mark_all_read',
44 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
45 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
46 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
47
48 }[name].format(**kwargs)
49
50 if params:
51 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
52 return base_url
53
54
55 class TestNotificationsController(TestController):
40 56
41 57 def teardown_method(self, method):
42 58 for n in Notification.query().all():
43 59 inst = Notification.get(n.notification_id)
44 60 Session().delete(inst)
45 61 Session().commit()
46 62
47 def test_index(self):
48 u1 = UserModel().create_or_update(
49 username='u1', password='qweqwe', email='u1@rhodecode.org',
50 firstname='u1', lastname='u1')
51 u1 = u1.user_id
52 self.destroy_users.add('u1')
63 def test_show_all(self, user_util):
64 user = user_util.create_user(password='qweqwe')
65 user_id = user.user_id
66 self.log_user(user.username, 'qweqwe')
53 67
54 self.log_user('u1', 'qweqwe')
55
56 response = self.app.get(url('notifications'))
68 response = self.app.get(
69 route_path('notifications_show_all', params={'type': 'all'}))
57 70 response.mustcontain(
58 71 '<div class="table">No notifications here yet</div>')
59 72
60 cur_user = self._get_logged_user()
61 notif = NotificationModel().create(
62 created_by=u1, notification_subject=u'test_notification_1',
63 notification_body=u'notification_1', recipients=[cur_user])
73 notification = NotificationModel().create(
74 created_by=user_id, notification_subject=u'test_notification_1',
75 notification_body=u'notification_1', recipients=[user_id])
64 76 Session().commit()
65 response = self.app.get(url('notifications'))
66 response.mustcontain('id="notification_%s"' % notif.notification_id)
77 notification_id = notification.notification_id
78
79 response = self.app.get(route_path('notifications_show_all',
80 params={'type': 'all'}))
81 response.mustcontain('id="notification_%s"' % notification_id)
82
83 def test_show_unread(self, user_util):
84 user = user_util.create_user(password='qweqwe')
85 user_id = user.user_id
86 self.log_user(user.username, 'qweqwe')
87
88 response = self.app.get(route_path('notifications_show_all'))
89 response.mustcontain(
90 '<div class="table">No notifications here yet</div>')
91
92 notification = NotificationModel().create(
93 created_by=user_id, notification_subject=u'test_notification_1',
94 notification_body=u'notification_1', recipients=[user_id])
95
96 # mark the USER notification as unread
97 user_notification = NotificationModel().get_user_notification(
98 user_id, notification)
99 user_notification.read = False
100
101 Session().commit()
102 notification_id = notification.notification_id
103
104 response = self.app.get(route_path('notifications_show_all'))
105 response.mustcontain('id="notification_%s"' % notification_id)
106 response.mustcontain('<div class="desc unread')
67 107
68 108 @pytest.mark.parametrize('user,password', [
69 109 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
70 110 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
71 111 ])
72 def test_delete(self, user, password):
112 def test_delete(self, user, password, user_util):
73 113 self.log_user(user, password)
74 114 cur_user = self._get_logged_user()
75 115
76 u1 = UserModel().create_or_update(
77 username='u1', password='qweqwe',
78 email='u1@rhodecode.org', firstname='u1', lastname='u1')
79 u2 = UserModel().create_or_update(
80 username='u2', password='qweqwe', email='u2@rhodecode.org',
81 firstname='u2', lastname='u2')
82 self.destroy_users.add('u1')
83 self.destroy_users.add('u2')
116 u1 = user_util.create_user()
117 u2 = user_util.create_user()
84 118
85 119 # make notifications
86 120 notification = NotificationModel().create(
87 121 created_by=cur_user, notification_subject=u'test',
88 122 notification_body=u'hi there', recipients=[cur_user, u1, u2])
89 123 Session().commit()
90 124 u1 = User.get(u1.user_id)
91 125 u2 = User.get(u2.user_id)
92 126
93 127 # check DB
94 128 get_notif = lambda un: [x.notification for x in un]
95 129 assert get_notif(cur_user.notifications) == [notification]
96 130 assert get_notif(u1.notifications) == [notification]
97 131 assert get_notif(u2.notifications) == [notification]
98 132 cur_usr_id = cur_user.user_id
99 133
100 134 response = self.app.post(
101 url('notification', notification_id=notification.notification_id),
102 params={'_method': 'delete', 'csrf_token': self.csrf_token})
103 assert response.body == 'ok'
135 route_path('notifications_delete',
136 notification_id=notification.notification_id),
137 params={'csrf_token': self.csrf_token})
138 assert response.json == 'ok'
104 139
105 140 cur_user = User.get(cur_usr_id)
106 141 assert cur_user.notifications == []
107 142
108 143 @pytest.mark.parametrize('user,password', [
109 144 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
110 145 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
111 146 ])
112 def test_show(self, user, password):
147 def test_show(self, user, password, user_util):
113 148 self.log_user(user, password)
114 149 cur_user = self._get_logged_user()
115 u1 = UserModel().create_or_update(username='u1', password='qweqwe',
116 email='u1@rhodecode.org',
117 firstname='u1', lastname='u1')
118 u2 = UserModel().create_or_update(username='u2', password='qweqwe',
119 email='u2@rhodecode.org',
120 firstname='u2', lastname='u2')
121 self.destroy_users.add('u1')
122 self.destroy_users.add('u2')
150 u1 = user_util.create_user()
151 u2 = user_util.create_user()
123 152
124 153 subject = u'test'
125 154 notif_body = u'hi there'
126 155 notification = NotificationModel().create(
127 156 created_by=cur_user, notification_subject=subject,
128 157 notification_body=notif_body, recipients=[cur_user, u1, u2])
129 158
130 response = self.app.get(url(
131 'notification', notification_id=notification.notification_id))
159 response = self.app.get(
160 route_path('notifications_show',
161 notification_id=notification.notification_id))
132 162
133 163 response.mustcontain(subject)
134 164 response.mustcontain(notif_body)
135 165
136 166 @pytest.mark.parametrize('user,password', [
137 167 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
138 168 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
139 169 ])
140 def test_update(self, user, password):
170 def test_update(self, user, password, user_util):
141 171 self.log_user(user, password)
142 172 cur_user = self._get_logged_user()
143
144 u1 = UserModel().create_or_update(username='u1', password='qweqwe',
145 email='u1@rhodecode.org',
146 firstname='u1', lastname='u1')
147 u2 = UserModel().create_or_update(username='u2', password='qweqwe',
148 email='u2@rhodecode.org',
149 firstname='u2', lastname='u2')
150 self.destroy_users.add('u1')
151 self.destroy_users.add('u2')
173 u1 = user_util.create_user()
174 u2 = user_util.create_user()
152 175
153 176 # make notifications
154 177 recipients = [cur_user, u1, u2]
155 178 notification = NotificationModel().create(
156 179 created_by=cur_user, notification_subject=u'test',
157 180 notification_body=u'hi there', recipients=recipients)
158 181 Session().commit()
159 182
160 183 for u_obj in recipients:
161 184 # if it's current user, he has his message already read
162 185 read = u_obj.username == user
163 186 assert len(u_obj.notifications) == 1
164 187 assert u_obj.notifications[0].read == read
165 188
166 189 response = self.app.post(
167 url('notification', notification_id=notification.notification_id),
168 params={'_method': 'put', 'csrf_token': self.csrf_token})
169 assert response.body == 'ok'
190 route_path('notifications_update',
191 notification_id=notification.notification_id),
192 params={'csrf_token': self.csrf_token})
193 assert response.json == 'ok'
170 194
171 195 cur_user = self._get_logged_user()
172 assert True == cur_user.notifications[0].read
196 assert True is cur_user.notifications[0].read
1 NO CONTENT: file renamed from rhodecode/apps/my_account/views.py to rhodecode/apps/my_account/views/my_account.py
@@ -1,884 +1,870 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 # prefix for non repository related links needs to be prefixed with `/`
36 36 ADMIN_PREFIX = '/_admin'
37 37 STATIC_FILE_PREFIX = '/_static'
38 38
39 39 # Default requirements for URL parts
40 40 URL_NAME_REQUIREMENTS = {
41 41 # group name can have a slash in them, but they must not end with a slash
42 42 'group_name': r'.*?[^/]',
43 43 'repo_group_name': r'.*?[^/]',
44 44 # repo names can have a slash in them, but they must not end with a slash
45 45 'repo_name': r'.*?[^/]',
46 46 # file path eats up everything at the end
47 47 'f_path': r'.*',
48 48 # reference types
49 49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 51 }
52 52
53 53
54 54 def add_route_requirements(route_path, requirements):
55 55 """
56 56 Adds regex requirements to pyramid routes using a mapping dict
57 57
58 58 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
59 59 '/{action}/{id:\d+}'
60 60
61 61 """
62 62 for key, regex in requirements.items():
63 63 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
64 64 return route_path
65 65
66 66
67 67 class JSRoutesMapper(Mapper):
68 68 """
69 69 Wrapper for routes.Mapper to make pyroutes compatible url definitions
70 70 """
71 71 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
72 72 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
73 73 def __init__(self, *args, **kw):
74 74 super(JSRoutesMapper, self).__init__(*args, **kw)
75 75 self._jsroutes = []
76 76
77 77 def connect(self, *args, **kw):
78 78 """
79 79 Wrapper for connect to take an extra argument jsroute=True
80 80
81 81 :param jsroute: boolean, if True will add the route to the pyroutes list
82 82 """
83 83 if kw.pop('jsroute', False):
84 84 if not self._named_route_regex.match(args[0]):
85 85 raise Exception('only named routes can be added to pyroutes')
86 86 self._jsroutes.append(args[0])
87 87
88 88 super(JSRoutesMapper, self).connect(*args, **kw)
89 89
90 90 def _extract_route_information(self, route):
91 91 """
92 92 Convert a route into tuple(name, path, args), eg:
93 93 ('show_user', '/profile/%(username)s', ['username'])
94 94 """
95 95 routepath = route.routepath
96 96 def replace(matchobj):
97 97 if matchobj.group(1):
98 98 return "%%(%s)s" % matchobj.group(1).split(':')[0]
99 99 else:
100 100 return "%%(%s)s" % matchobj.group(2)
101 101
102 102 routepath = self._argument_prog.sub(replace, routepath)
103 103 return (
104 104 route.name,
105 105 routepath,
106 106 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
107 107 for arg in self._argument_prog.findall(route.routepath)]
108 108 )
109 109
110 110 def jsroutes(self):
111 111 """
112 112 Return a list of pyroutes.js compatible routes
113 113 """
114 114 for route_name in self._jsroutes:
115 115 yield self._extract_route_information(self._routenames[route_name])
116 116
117 117
118 118 def make_map(config):
119 119 """Create, configure and return the routes Mapper"""
120 120 rmap = JSRoutesMapper(
121 121 directory=config['pylons.paths']['controllers'],
122 122 always_scan=config['debug'])
123 123 rmap.minimization = False
124 124 rmap.explicit = False
125 125
126 126 from rhodecode.lib.utils2 import str2bool
127 127 from rhodecode.model import repo, repo_group
128 128
129 129 def check_repo(environ, match_dict):
130 130 """
131 131 check for valid repository for proper 404 handling
132 132
133 133 :param environ:
134 134 :param match_dict:
135 135 """
136 136 repo_name = match_dict.get('repo_name')
137 137
138 138 if match_dict.get('f_path'):
139 139 # fix for multiple initial slashes that causes errors
140 140 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
141 141 repo_model = repo.RepoModel()
142 142 by_name_match = repo_model.get_by_repo_name(repo_name)
143 143 # if we match quickly from database, short circuit the operation,
144 144 # and validate repo based on the type.
145 145 if by_name_match:
146 146 return True
147 147
148 148 by_id_match = repo_model.get_repo_by_id(repo_name)
149 149 if by_id_match:
150 150 repo_name = by_id_match.repo_name
151 151 match_dict['repo_name'] = repo_name
152 152 return True
153 153
154 154 return False
155 155
156 156 def check_group(environ, match_dict):
157 157 """
158 158 check for valid repository group path for proper 404 handling
159 159
160 160 :param environ:
161 161 :param match_dict:
162 162 """
163 163 repo_group_name = match_dict.get('group_name')
164 164 repo_group_model = repo_group.RepoGroupModel()
165 165 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
166 166 if by_name_match:
167 167 return True
168 168
169 169 return False
170 170
171 171 def check_user_group(environ, match_dict):
172 172 """
173 173 check for valid user group for proper 404 handling
174 174
175 175 :param environ:
176 176 :param match_dict:
177 177 """
178 178 return True
179 179
180 180 def check_int(environ, match_dict):
181 181 return match_dict.get('id').isdigit()
182 182
183 183
184 184 #==========================================================================
185 185 # CUSTOM ROUTES HERE
186 186 #==========================================================================
187 187
188 188 # ping and pylons error test
189 189 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
190 190 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
191 191
192 192 # ADMIN REPOSITORY ROUTES
193 193 with rmap.submapper(path_prefix=ADMIN_PREFIX,
194 194 controller='admin/repos') as m:
195 195 m.connect('repos', '/repos',
196 196 action='create', conditions={'method': ['POST']})
197 197 m.connect('repos', '/repos',
198 198 action='index', conditions={'method': ['GET']})
199 199 m.connect('new_repo', '/create_repository', jsroute=True,
200 200 action='create_repository', conditions={'method': ['GET']})
201 201 m.connect('delete_repo', '/repos/{repo_name}',
202 202 action='delete', conditions={'method': ['DELETE']},
203 203 requirements=URL_NAME_REQUIREMENTS)
204 204 m.connect('repo', '/repos/{repo_name}',
205 205 action='show', conditions={'method': ['GET'],
206 206 'function': check_repo},
207 207 requirements=URL_NAME_REQUIREMENTS)
208 208
209 209 # ADMIN REPOSITORY GROUPS ROUTES
210 210 with rmap.submapper(path_prefix=ADMIN_PREFIX,
211 211 controller='admin/repo_groups') as m:
212 212 m.connect('repo_groups', '/repo_groups',
213 213 action='create', conditions={'method': ['POST']})
214 214 m.connect('repo_groups', '/repo_groups',
215 215 action='index', conditions={'method': ['GET']})
216 216 m.connect('new_repo_group', '/repo_groups/new',
217 217 action='new', conditions={'method': ['GET']})
218 218 m.connect('update_repo_group', '/repo_groups/{group_name}',
219 219 action='update', conditions={'method': ['PUT'],
220 220 'function': check_group},
221 221 requirements=URL_NAME_REQUIREMENTS)
222 222
223 223 # EXTRAS REPO GROUP ROUTES
224 224 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
225 225 action='edit',
226 226 conditions={'method': ['GET'], 'function': check_group},
227 227 requirements=URL_NAME_REQUIREMENTS)
228 228 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
229 229 action='edit',
230 230 conditions={'method': ['PUT'], 'function': check_group},
231 231 requirements=URL_NAME_REQUIREMENTS)
232 232
233 233 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
234 234 action='edit_repo_group_advanced',
235 235 conditions={'method': ['GET'], 'function': check_group},
236 236 requirements=URL_NAME_REQUIREMENTS)
237 237 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
238 238 action='edit_repo_group_advanced',
239 239 conditions={'method': ['PUT'], 'function': check_group},
240 240 requirements=URL_NAME_REQUIREMENTS)
241 241
242 242 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
243 243 action='edit_repo_group_perms',
244 244 conditions={'method': ['GET'], 'function': check_group},
245 245 requirements=URL_NAME_REQUIREMENTS)
246 246 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
247 247 action='update_perms',
248 248 conditions={'method': ['PUT'], 'function': check_group},
249 249 requirements=URL_NAME_REQUIREMENTS)
250 250
251 251 m.connect('delete_repo_group', '/repo_groups/{group_name}',
252 252 action='delete', conditions={'method': ['DELETE'],
253 253 'function': check_group},
254 254 requirements=URL_NAME_REQUIREMENTS)
255 255
256 256 # ADMIN USER ROUTES
257 257 with rmap.submapper(path_prefix=ADMIN_PREFIX,
258 258 controller='admin/users') as m:
259 259 m.connect('users', '/users',
260 260 action='create', conditions={'method': ['POST']})
261 261 m.connect('new_user', '/users/new',
262 262 action='new', conditions={'method': ['GET']})
263 263 m.connect('update_user', '/users/{user_id}',
264 264 action='update', conditions={'method': ['PUT']})
265 265 m.connect('delete_user', '/users/{user_id}',
266 266 action='delete', conditions={'method': ['DELETE']})
267 267 m.connect('edit_user', '/users/{user_id}/edit',
268 268 action='edit', conditions={'method': ['GET']}, jsroute=True)
269 269 m.connect('user', '/users/{user_id}',
270 270 action='show', conditions={'method': ['GET']})
271 271 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
272 272 action='reset_password', conditions={'method': ['POST']})
273 273 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
274 274 action='create_personal_repo_group', conditions={'method': ['POST']})
275 275
276 276 # EXTRAS USER ROUTES
277 277 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
278 278 action='edit_advanced', conditions={'method': ['GET']})
279 279 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
280 280 action='update_advanced', conditions={'method': ['PUT']})
281 281
282 282 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
283 283 action='edit_global_perms', conditions={'method': ['GET']})
284 284 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
285 285 action='update_global_perms', conditions={'method': ['PUT']})
286 286
287 287 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
288 288 action='edit_perms_summary', conditions={'method': ['GET']})
289 289
290 290
291 291 # ADMIN USER GROUPS REST ROUTES
292 292 with rmap.submapper(path_prefix=ADMIN_PREFIX,
293 293 controller='admin/user_groups') as m:
294 294 m.connect('users_groups', '/user_groups',
295 295 action='create', conditions={'method': ['POST']})
296 296 m.connect('users_groups', '/user_groups',
297 297 action='index', conditions={'method': ['GET']})
298 298 m.connect('new_users_group', '/user_groups/new',
299 299 action='new', conditions={'method': ['GET']})
300 300 m.connect('update_users_group', '/user_groups/{user_group_id}',
301 301 action='update', conditions={'method': ['PUT']})
302 302 m.connect('delete_users_group', '/user_groups/{user_group_id}',
303 303 action='delete', conditions={'method': ['DELETE']})
304 304 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
305 305 action='edit', conditions={'method': ['GET']},
306 306 function=check_user_group)
307 307
308 308 # EXTRAS USER GROUP ROUTES
309 309 m.connect('edit_user_group_global_perms',
310 310 '/user_groups/{user_group_id}/edit/global_permissions',
311 311 action='edit_global_perms', conditions={'method': ['GET']})
312 312 m.connect('edit_user_group_global_perms',
313 313 '/user_groups/{user_group_id}/edit/global_permissions',
314 314 action='update_global_perms', conditions={'method': ['PUT']})
315 315 m.connect('edit_user_group_perms_summary',
316 316 '/user_groups/{user_group_id}/edit/permissions_summary',
317 317 action='edit_perms_summary', conditions={'method': ['GET']})
318 318
319 319 m.connect('edit_user_group_perms',
320 320 '/user_groups/{user_group_id}/edit/permissions',
321 321 action='edit_perms', conditions={'method': ['GET']})
322 322 m.connect('edit_user_group_perms',
323 323 '/user_groups/{user_group_id}/edit/permissions',
324 324 action='update_perms', conditions={'method': ['PUT']})
325 325
326 326 m.connect('edit_user_group_advanced',
327 327 '/user_groups/{user_group_id}/edit/advanced',
328 328 action='edit_advanced', conditions={'method': ['GET']})
329 329
330 330 m.connect('edit_user_group_advanced_sync',
331 331 '/user_groups/{user_group_id}/edit/advanced/sync',
332 332 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
333 333
334 334 m.connect('edit_user_group_members',
335 335 '/user_groups/{user_group_id}/edit/members', jsroute=True,
336 336 action='user_group_members', conditions={'method': ['GET']})
337 337
338 338 # ADMIN PERMISSIONS ROUTES
339 339 with rmap.submapper(path_prefix=ADMIN_PREFIX,
340 340 controller='admin/permissions') as m:
341 341 m.connect('admin_permissions_application', '/permissions/application',
342 342 action='permission_application_update', conditions={'method': ['POST']})
343 343 m.connect('admin_permissions_application', '/permissions/application',
344 344 action='permission_application', conditions={'method': ['GET']})
345 345
346 346 m.connect('admin_permissions_global', '/permissions/global',
347 347 action='permission_global_update', conditions={'method': ['POST']})
348 348 m.connect('admin_permissions_global', '/permissions/global',
349 349 action='permission_global', conditions={'method': ['GET']})
350 350
351 351 m.connect('admin_permissions_object', '/permissions/object',
352 352 action='permission_objects_update', conditions={'method': ['POST']})
353 353 m.connect('admin_permissions_object', '/permissions/object',
354 354 action='permission_objects', conditions={'method': ['GET']})
355 355
356 356 m.connect('admin_permissions_ips', '/permissions/ips',
357 357 action='permission_ips', conditions={'method': ['POST']})
358 358 m.connect('admin_permissions_ips', '/permissions/ips',
359 359 action='permission_ips', conditions={'method': ['GET']})
360 360
361 361 m.connect('admin_permissions_overview', '/permissions/overview',
362 362 action='permission_perms', conditions={'method': ['GET']})
363 363
364 364 # ADMIN DEFAULTS REST ROUTES
365 365 with rmap.submapper(path_prefix=ADMIN_PREFIX,
366 366 controller='admin/defaults') as m:
367 367 m.connect('admin_defaults_repositories', '/defaults/repositories',
368 368 action='update_repository_defaults', conditions={'method': ['POST']})
369 369 m.connect('admin_defaults_repositories', '/defaults/repositories',
370 370 action='index', conditions={'method': ['GET']})
371 371
372 372 # ADMIN SETTINGS ROUTES
373 373 with rmap.submapper(path_prefix=ADMIN_PREFIX,
374 374 controller='admin/settings') as m:
375 375
376 376 # default
377 377 m.connect('admin_settings', '/settings',
378 378 action='settings_global_update',
379 379 conditions={'method': ['POST']})
380 380 m.connect('admin_settings', '/settings',
381 381 action='settings_global', conditions={'method': ['GET']})
382 382
383 383 m.connect('admin_settings_vcs', '/settings/vcs',
384 384 action='settings_vcs_update',
385 385 conditions={'method': ['POST']})
386 386 m.connect('admin_settings_vcs', '/settings/vcs',
387 387 action='settings_vcs',
388 388 conditions={'method': ['GET']})
389 389 m.connect('admin_settings_vcs', '/settings/vcs',
390 390 action='delete_svn_pattern',
391 391 conditions={'method': ['DELETE']})
392 392
393 393 m.connect('admin_settings_mapping', '/settings/mapping',
394 394 action='settings_mapping_update',
395 395 conditions={'method': ['POST']})
396 396 m.connect('admin_settings_mapping', '/settings/mapping',
397 397 action='settings_mapping', conditions={'method': ['GET']})
398 398
399 399 m.connect('admin_settings_global', '/settings/global',
400 400 action='settings_global_update',
401 401 conditions={'method': ['POST']})
402 402 m.connect('admin_settings_global', '/settings/global',
403 403 action='settings_global', conditions={'method': ['GET']})
404 404
405 405 m.connect('admin_settings_visual', '/settings/visual',
406 406 action='settings_visual_update',
407 407 conditions={'method': ['POST']})
408 408 m.connect('admin_settings_visual', '/settings/visual',
409 409 action='settings_visual', conditions={'method': ['GET']})
410 410
411 411 m.connect('admin_settings_issuetracker',
412 412 '/settings/issue-tracker', action='settings_issuetracker',
413 413 conditions={'method': ['GET']})
414 414 m.connect('admin_settings_issuetracker_save',
415 415 '/settings/issue-tracker/save',
416 416 action='settings_issuetracker_save',
417 417 conditions={'method': ['POST']})
418 418 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
419 419 action='settings_issuetracker_test',
420 420 conditions={'method': ['POST']})
421 421 m.connect('admin_issuetracker_delete',
422 422 '/settings/issue-tracker/delete',
423 423 action='settings_issuetracker_delete',
424 424 conditions={'method': ['DELETE']})
425 425
426 426 m.connect('admin_settings_email', '/settings/email',
427 427 action='settings_email_update',
428 428 conditions={'method': ['POST']})
429 429 m.connect('admin_settings_email', '/settings/email',
430 430 action='settings_email', conditions={'method': ['GET']})
431 431
432 432 m.connect('admin_settings_hooks', '/settings/hooks',
433 433 action='settings_hooks_update',
434 434 conditions={'method': ['POST', 'DELETE']})
435 435 m.connect('admin_settings_hooks', '/settings/hooks',
436 436 action='settings_hooks', conditions={'method': ['GET']})
437 437
438 438 m.connect('admin_settings_search', '/settings/search',
439 439 action='settings_search', conditions={'method': ['GET']})
440 440
441 441 m.connect('admin_settings_supervisor', '/settings/supervisor',
442 442 action='settings_supervisor', conditions={'method': ['GET']})
443 443 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
444 444 action='settings_supervisor_log', conditions={'method': ['GET']})
445 445
446 446 m.connect('admin_settings_labs', '/settings/labs',
447 447 action='settings_labs_update',
448 448 conditions={'method': ['POST']})
449 449 m.connect('admin_settings_labs', '/settings/labs',
450 450 action='settings_labs', conditions={'method': ['GET']})
451 451
452 452 # ADMIN MY ACCOUNT
453 453 with rmap.submapper(path_prefix=ADMIN_PREFIX,
454 454 controller='admin/my_account') as m:
455 455
456 456 # NOTE(marcink): this needs to be kept for password force flag to be
457 457 # handled in pylons controllers, remove after full migration to pyramid
458 458 m.connect('my_account_password', '/my_account/password',
459 459 action='my_account_password', conditions={'method': ['GET']})
460 460
461 # NOTIFICATION REST ROUTES
462 with rmap.submapper(path_prefix=ADMIN_PREFIX,
463 controller='admin/notifications') as m:
464 m.connect('notifications', '/notifications',
465 action='index', conditions={'method': ['GET']})
466 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
467 action='mark_all_read', conditions={'method': ['POST']})
468 m.connect('/notifications/{notification_id}',
469 action='update', conditions={'method': ['PUT']})
470 m.connect('/notifications/{notification_id}',
471 action='delete', conditions={'method': ['DELETE']})
472 m.connect('notification', '/notifications/{notification_id}',
473 action='show', conditions={'method': ['GET']})
474
475 461 # USER JOURNAL
476 462 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
477 463 controller='journal', action='index')
478 464 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
479 465 controller='journal', action='journal_rss')
480 466 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
481 467 controller='journal', action='journal_atom')
482 468
483 469 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
484 470 controller='journal', action='public_journal')
485 471
486 472 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
487 473 controller='journal', action='public_journal_rss')
488 474
489 475 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
490 476 controller='journal', action='public_journal_rss')
491 477
492 478 rmap.connect('public_journal_atom',
493 479 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
494 480 action='public_journal_atom')
495 481
496 482 rmap.connect('public_journal_atom_old',
497 483 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
498 484 action='public_journal_atom')
499 485
500 486 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
501 487 controller='journal', action='toggle_following', jsroute=True,
502 488 conditions={'method': ['POST']})
503 489
504 490 #==========================================================================
505 491 # REPOSITORY ROUTES
506 492 #==========================================================================
507 493
508 494 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
509 495 controller='admin/repos', action='repo_creating',
510 496 requirements=URL_NAME_REQUIREMENTS)
511 497 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
512 498 controller='admin/repos', action='repo_check',
513 499 requirements=URL_NAME_REQUIREMENTS)
514 500
515 501 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
516 502 controller='changeset', revision='tip',
517 503 conditions={'function': check_repo},
518 504 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
519 505 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
520 506 controller='changeset', revision='tip', action='changeset_children',
521 507 conditions={'function': check_repo},
522 508 requirements=URL_NAME_REQUIREMENTS)
523 509 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
524 510 controller='changeset', revision='tip', action='changeset_parents',
525 511 conditions={'function': check_repo},
526 512 requirements=URL_NAME_REQUIREMENTS)
527 513
528 514 # repo edit options
529 515 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
530 516 controller='admin/repos', action='edit_fields',
531 517 conditions={'method': ['GET'], 'function': check_repo},
532 518 requirements=URL_NAME_REQUIREMENTS)
533 519 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
534 520 controller='admin/repos', action='create_repo_field',
535 521 conditions={'method': ['PUT'], 'function': check_repo},
536 522 requirements=URL_NAME_REQUIREMENTS)
537 523 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
538 524 controller='admin/repos', action='delete_repo_field',
539 525 conditions={'method': ['DELETE'], 'function': check_repo},
540 526 requirements=URL_NAME_REQUIREMENTS)
541 527
542 528 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
543 529 controller='admin/repos', action='toggle_locking',
544 530 conditions={'method': ['GET'], 'function': check_repo},
545 531 requirements=URL_NAME_REQUIREMENTS)
546 532
547 533 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
548 534 controller='admin/repos', action='edit_remote_form',
549 535 conditions={'method': ['GET'], 'function': check_repo},
550 536 requirements=URL_NAME_REQUIREMENTS)
551 537 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
552 538 controller='admin/repos', action='edit_remote',
553 539 conditions={'method': ['PUT'], 'function': check_repo},
554 540 requirements=URL_NAME_REQUIREMENTS)
555 541
556 542 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
557 543 controller='admin/repos', action='edit_statistics_form',
558 544 conditions={'method': ['GET'], 'function': check_repo},
559 545 requirements=URL_NAME_REQUIREMENTS)
560 546 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
561 547 controller='admin/repos', action='edit_statistics',
562 548 conditions={'method': ['PUT'], 'function': check_repo},
563 549 requirements=URL_NAME_REQUIREMENTS)
564 550 rmap.connect('repo_settings_issuetracker',
565 551 '/{repo_name}/settings/issue-tracker',
566 552 controller='admin/repos', action='repo_issuetracker',
567 553 conditions={'method': ['GET'], 'function': check_repo},
568 554 requirements=URL_NAME_REQUIREMENTS)
569 555 rmap.connect('repo_issuetracker_test',
570 556 '/{repo_name}/settings/issue-tracker/test',
571 557 controller='admin/repos', action='repo_issuetracker_test',
572 558 conditions={'method': ['POST'], 'function': check_repo},
573 559 requirements=URL_NAME_REQUIREMENTS)
574 560 rmap.connect('repo_issuetracker_delete',
575 561 '/{repo_name}/settings/issue-tracker/delete',
576 562 controller='admin/repos', action='repo_issuetracker_delete',
577 563 conditions={'method': ['DELETE'], 'function': check_repo},
578 564 requirements=URL_NAME_REQUIREMENTS)
579 565 rmap.connect('repo_issuetracker_save',
580 566 '/{repo_name}/settings/issue-tracker/save',
581 567 controller='admin/repos', action='repo_issuetracker_save',
582 568 conditions={'method': ['POST'], 'function': check_repo},
583 569 requirements=URL_NAME_REQUIREMENTS)
584 570 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
585 571 controller='admin/repos', action='repo_settings_vcs_update',
586 572 conditions={'method': ['POST'], 'function': check_repo},
587 573 requirements=URL_NAME_REQUIREMENTS)
588 574 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
589 575 controller='admin/repos', action='repo_settings_vcs',
590 576 conditions={'method': ['GET'], 'function': check_repo},
591 577 requirements=URL_NAME_REQUIREMENTS)
592 578 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
593 579 controller='admin/repos', action='repo_delete_svn_pattern',
594 580 conditions={'method': ['DELETE'], 'function': check_repo},
595 581 requirements=URL_NAME_REQUIREMENTS)
596 582 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
597 583 controller='admin/repos', action='repo_settings_pullrequest',
598 584 conditions={'method': ['GET', 'POST'], 'function': check_repo},
599 585 requirements=URL_NAME_REQUIREMENTS)
600 586
601 587 # still working url for backward compat.
602 588 rmap.connect('raw_changeset_home_depraced',
603 589 '/{repo_name}/raw-changeset/{revision}',
604 590 controller='changeset', action='changeset_raw',
605 591 revision='tip', conditions={'function': check_repo},
606 592 requirements=URL_NAME_REQUIREMENTS)
607 593
608 594 # new URLs
609 595 rmap.connect('changeset_raw_home',
610 596 '/{repo_name}/changeset-diff/{revision}',
611 597 controller='changeset', action='changeset_raw',
612 598 revision='tip', conditions={'function': check_repo},
613 599 requirements=URL_NAME_REQUIREMENTS)
614 600
615 601 rmap.connect('changeset_patch_home',
616 602 '/{repo_name}/changeset-patch/{revision}',
617 603 controller='changeset', action='changeset_patch',
618 604 revision='tip', conditions={'function': check_repo},
619 605 requirements=URL_NAME_REQUIREMENTS)
620 606
621 607 rmap.connect('changeset_download_home',
622 608 '/{repo_name}/changeset-download/{revision}',
623 609 controller='changeset', action='changeset_download',
624 610 revision='tip', conditions={'function': check_repo},
625 611 requirements=URL_NAME_REQUIREMENTS)
626 612
627 613 rmap.connect('changeset_comment',
628 614 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
629 615 controller='changeset', revision='tip', action='comment',
630 616 conditions={'function': check_repo},
631 617 requirements=URL_NAME_REQUIREMENTS)
632 618
633 619 rmap.connect('changeset_comment_preview',
634 620 '/{repo_name}/changeset/comment/preview', jsroute=True,
635 621 controller='changeset', action='preview_comment',
636 622 conditions={'function': check_repo, 'method': ['POST']},
637 623 requirements=URL_NAME_REQUIREMENTS)
638 624
639 625 rmap.connect('changeset_comment_delete',
640 626 '/{repo_name}/changeset/comment/{comment_id}/delete',
641 627 controller='changeset', action='delete_comment',
642 628 conditions={'function': check_repo, 'method': ['DELETE']},
643 629 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
644 630
645 631 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
646 632 controller='changeset', action='changeset_info',
647 633 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
648 634
649 635 rmap.connect('compare_home',
650 636 '/{repo_name}/compare',
651 637 controller='compare', action='index',
652 638 conditions={'function': check_repo},
653 639 requirements=URL_NAME_REQUIREMENTS)
654 640
655 641 rmap.connect('compare_url',
656 642 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
657 643 controller='compare', action='compare',
658 644 conditions={'function': check_repo},
659 645 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
660 646
661 647 rmap.connect('pullrequest_home',
662 648 '/{repo_name}/pull-request/new', controller='pullrequests',
663 649 action='index', conditions={'function': check_repo,
664 650 'method': ['GET']},
665 651 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
666 652
667 653 rmap.connect('pullrequest',
668 654 '/{repo_name}/pull-request/new', controller='pullrequests',
669 655 action='create', conditions={'function': check_repo,
670 656 'method': ['POST']},
671 657 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
672 658
673 659 rmap.connect('pullrequest_repo_refs',
674 660 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
675 661 controller='pullrequests',
676 662 action='get_repo_refs',
677 663 conditions={'function': check_repo, 'method': ['GET']},
678 664 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
679 665
680 666 rmap.connect('pullrequest_repo_destinations',
681 667 '/{repo_name}/pull-request/repo-destinations',
682 668 controller='pullrequests',
683 669 action='get_repo_destinations',
684 670 conditions={'function': check_repo, 'method': ['GET']},
685 671 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
686 672
687 673 rmap.connect('pullrequest_show',
688 674 '/{repo_name}/pull-request/{pull_request_id}',
689 675 controller='pullrequests',
690 676 action='show', conditions={'function': check_repo,
691 677 'method': ['GET']},
692 678 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
693 679
694 680 rmap.connect('pullrequest_update',
695 681 '/{repo_name}/pull-request/{pull_request_id}',
696 682 controller='pullrequests',
697 683 action='update', conditions={'function': check_repo,
698 684 'method': ['PUT']},
699 685 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
700 686
701 687 rmap.connect('pullrequest_merge',
702 688 '/{repo_name}/pull-request/{pull_request_id}',
703 689 controller='pullrequests',
704 690 action='merge', conditions={'function': check_repo,
705 691 'method': ['POST']},
706 692 requirements=URL_NAME_REQUIREMENTS)
707 693
708 694 rmap.connect('pullrequest_delete',
709 695 '/{repo_name}/pull-request/{pull_request_id}',
710 696 controller='pullrequests',
711 697 action='delete', conditions={'function': check_repo,
712 698 'method': ['DELETE']},
713 699 requirements=URL_NAME_REQUIREMENTS)
714 700
715 701 rmap.connect('pullrequest_comment',
716 702 '/{repo_name}/pull-request-comment/{pull_request_id}',
717 703 controller='pullrequests',
718 704 action='comment', conditions={'function': check_repo,
719 705 'method': ['POST']},
720 706 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
721 707
722 708 rmap.connect('pullrequest_comment_delete',
723 709 '/{repo_name}/pull-request-comment/{comment_id}/delete',
724 710 controller='pullrequests', action='delete_comment',
725 711 conditions={'function': check_repo, 'method': ['DELETE']},
726 712 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
727 713
728 714 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
729 715 controller='changelog', conditions={'function': check_repo},
730 716 requirements=URL_NAME_REQUIREMENTS)
731 717
732 718 rmap.connect('changelog_file_home',
733 719 '/{repo_name}/changelog/{revision}/{f_path}',
734 720 controller='changelog', f_path=None,
735 721 conditions={'function': check_repo},
736 722 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
737 723
738 724 rmap.connect('changelog_elements', '/{repo_name}/changelog_details',
739 725 controller='changelog', action='changelog_elements',
740 726 conditions={'function': check_repo},
741 727 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
742 728
743 729 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
744 730 controller='files', revision='tip', f_path='',
745 731 conditions={'function': check_repo},
746 732 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
747 733
748 734 rmap.connect('files_home_simple_catchrev',
749 735 '/{repo_name}/files/{revision}',
750 736 controller='files', revision='tip', f_path='',
751 737 conditions={'function': check_repo},
752 738 requirements=URL_NAME_REQUIREMENTS)
753 739
754 740 rmap.connect('files_home_simple_catchall',
755 741 '/{repo_name}/files',
756 742 controller='files', revision='tip', f_path='',
757 743 conditions={'function': check_repo},
758 744 requirements=URL_NAME_REQUIREMENTS)
759 745
760 746 rmap.connect('files_history_home',
761 747 '/{repo_name}/history/{revision}/{f_path}',
762 748 controller='files', action='history', revision='tip', f_path='',
763 749 conditions={'function': check_repo},
764 750 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
765 751
766 752 rmap.connect('files_authors_home',
767 753 '/{repo_name}/authors/{revision}/{f_path}',
768 754 controller='files', action='authors', revision='tip', f_path='',
769 755 conditions={'function': check_repo},
770 756 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
771 757
772 758 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
773 759 controller='files', action='diff', f_path='',
774 760 conditions={'function': check_repo},
775 761 requirements=URL_NAME_REQUIREMENTS)
776 762
777 763 rmap.connect('files_diff_2way_home',
778 764 '/{repo_name}/diff-2way/{f_path}',
779 765 controller='files', action='diff_2way', f_path='',
780 766 conditions={'function': check_repo},
781 767 requirements=URL_NAME_REQUIREMENTS)
782 768
783 769 rmap.connect('files_rawfile_home',
784 770 '/{repo_name}/rawfile/{revision}/{f_path}',
785 771 controller='files', action='rawfile', revision='tip',
786 772 f_path='', conditions={'function': check_repo},
787 773 requirements=URL_NAME_REQUIREMENTS)
788 774
789 775 rmap.connect('files_raw_home',
790 776 '/{repo_name}/raw/{revision}/{f_path}',
791 777 controller='files', action='raw', revision='tip', f_path='',
792 778 conditions={'function': check_repo},
793 779 requirements=URL_NAME_REQUIREMENTS)
794 780
795 781 rmap.connect('files_render_home',
796 782 '/{repo_name}/render/{revision}/{f_path}',
797 783 controller='files', action='index', revision='tip', f_path='',
798 784 rendered=True, conditions={'function': check_repo},
799 785 requirements=URL_NAME_REQUIREMENTS)
800 786
801 787 rmap.connect('files_annotate_home',
802 788 '/{repo_name}/annotate/{revision}/{f_path}',
803 789 controller='files', action='index', revision='tip',
804 790 f_path='', annotate=True, conditions={'function': check_repo},
805 791 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
806 792
807 793 rmap.connect('files_annotate_previous',
808 794 '/{repo_name}/annotate-previous/{revision}/{f_path}',
809 795 controller='files', action='annotate_previous', revision='tip',
810 796 f_path='', annotate=True, conditions={'function': check_repo},
811 797 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
812 798
813 799 rmap.connect('files_edit',
814 800 '/{repo_name}/edit/{revision}/{f_path}',
815 801 controller='files', action='edit', revision='tip',
816 802 f_path='',
817 803 conditions={'function': check_repo, 'method': ['POST']},
818 804 requirements=URL_NAME_REQUIREMENTS)
819 805
820 806 rmap.connect('files_edit_home',
821 807 '/{repo_name}/edit/{revision}/{f_path}',
822 808 controller='files', action='edit_home', revision='tip',
823 809 f_path='', conditions={'function': check_repo},
824 810 requirements=URL_NAME_REQUIREMENTS)
825 811
826 812 rmap.connect('files_add',
827 813 '/{repo_name}/add/{revision}/{f_path}',
828 814 controller='files', action='add', revision='tip',
829 815 f_path='',
830 816 conditions={'function': check_repo, 'method': ['POST']},
831 817 requirements=URL_NAME_REQUIREMENTS)
832 818
833 819 rmap.connect('files_add_home',
834 820 '/{repo_name}/add/{revision}/{f_path}',
835 821 controller='files', action='add_home', revision='tip',
836 822 f_path='', conditions={'function': check_repo},
837 823 requirements=URL_NAME_REQUIREMENTS)
838 824
839 825 rmap.connect('files_delete',
840 826 '/{repo_name}/delete/{revision}/{f_path}',
841 827 controller='files', action='delete', revision='tip',
842 828 f_path='',
843 829 conditions={'function': check_repo, 'method': ['POST']},
844 830 requirements=URL_NAME_REQUIREMENTS)
845 831
846 832 rmap.connect('files_delete_home',
847 833 '/{repo_name}/delete/{revision}/{f_path}',
848 834 controller='files', action='delete_home', revision='tip',
849 835 f_path='', conditions={'function': check_repo},
850 836 requirements=URL_NAME_REQUIREMENTS)
851 837
852 838 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
853 839 controller='files', action='archivefile',
854 840 conditions={'function': check_repo},
855 841 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
856 842
857 843 rmap.connect('files_nodelist_home',
858 844 '/{repo_name}/nodelist/{revision}/{f_path}',
859 845 controller='files', action='nodelist',
860 846 conditions={'function': check_repo},
861 847 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
862 848
863 849 rmap.connect('files_nodetree_full',
864 850 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
865 851 controller='files', action='nodetree_full',
866 852 conditions={'function': check_repo},
867 853 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
868 854
869 855 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
870 856 controller='forks', action='fork_create',
871 857 conditions={'function': check_repo, 'method': ['POST']},
872 858 requirements=URL_NAME_REQUIREMENTS)
873 859
874 860 rmap.connect('repo_fork_home', '/{repo_name}/fork',
875 861 controller='forks', action='fork',
876 862 conditions={'function': check_repo},
877 863 requirements=URL_NAME_REQUIREMENTS)
878 864
879 865 rmap.connect('repo_forks_home', '/{repo_name}/forks',
880 866 controller='forks', action='forks',
881 867 conditions={'function': check_repo},
882 868 requirements=URL_NAME_REQUIREMENTS)
883 869
884 870 return rmap
@@ -1,379 +1,381 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Model for notifications
24 24 """
25 25
26 26
27 27 import logging
28 28 import traceback
29 29
30 30 from pylons.i18n.translation import _, ungettext
31 31 from sqlalchemy.sql.expression import false, true
32 32 from mako import exceptions
33 33
34 34 import rhodecode
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.utils import PartialRenderer
37 37 from rhodecode.model import BaseModel
38 38 from rhodecode.model.db import Notification, User, UserNotification
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.settings import SettingsModel
41 41 from rhodecode.translation import TranslationString
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class NotificationModel(BaseModel):
47 47
48 48 cls = Notification
49 49
50 50 def __get_notification(self, notification):
51 51 if isinstance(notification, Notification):
52 52 return notification
53 53 elif isinstance(notification, (int, long)):
54 54 return Notification.get(notification)
55 55 else:
56 56 if notification:
57 57 raise Exception('notification must be int, long or Instance'
58 58 ' of Notification got %s' % type(notification))
59 59
60 60 def create(
61 61 self, created_by, notification_subject, notification_body,
62 62 notification_type=Notification.TYPE_MESSAGE, recipients=None,
63 63 mention_recipients=None, with_email=True, email_kwargs=None):
64 64 """
65 65
66 66 Creates notification of given type
67 67
68 68 :param created_by: int, str or User instance. User who created this
69 69 notification
70 70 :param notification_subject: subject of notification itself
71 71 :param notification_body: body of notification text
72 72 :param notification_type: type of notification, based on that we
73 73 pick templates
74 74
75 75 :param recipients: list of int, str or User objects, when None
76 76 is given send to all admins
77 77 :param mention_recipients: list of int, str or User objects,
78 78 that were mentioned
79 79 :param with_email: send email with this notification
80 80 :param email_kwargs: dict with arguments to generate email
81 81 """
82 82
83 83 from rhodecode.lib.celerylib import tasks, run_task
84 84
85 85 if recipients and not getattr(recipients, '__iter__', False):
86 86 raise Exception('recipients must be an iterable object')
87 87
88 88 created_by_obj = self._get_user(created_by)
89 89 # default MAIN body if not given
90 90 email_kwargs = email_kwargs or {'body': notification_body}
91 91 mention_recipients = mention_recipients or set()
92 92
93 93 if not created_by_obj:
94 94 raise Exception('unknown user %s' % created_by)
95 95
96 96 if recipients is None:
97 97 # recipients is None means to all admins
98 98 recipients_objs = User.query().filter(User.admin == true()).all()
99 99 log.debug('sending notifications %s to admins: %s',
100 100 notification_type, recipients_objs)
101 101 else:
102 102 recipients_objs = []
103 103 for u in recipients:
104 104 obj = self._get_user(u)
105 105 if obj:
106 106 recipients_objs.append(obj)
107 107 else: # we didn't find this user, log the error and carry on
108 108 log.error('cannot notify unknown user %r', u)
109 109
110 110 recipients_objs = set(recipients_objs)
111 111 if not recipients_objs:
112 112 raise Exception('no valid recipients specified')
113 113
114 114 log.debug('sending notifications %s to %s',
115 115 notification_type, recipients_objs)
116 116
117 117 # add mentioned users into recipients
118 118 final_recipients = set(recipients_objs).union(mention_recipients)
119 119 notification = Notification.create(
120 120 created_by=created_by_obj, subject=notification_subject,
121 121 body=notification_body, recipients=final_recipients,
122 122 type_=notification_type
123 123 )
124 124
125 125 if not with_email: # skip sending email, and just create notification
126 126 return notification
127 127
128 128 # don't send email to person who created this comment
129 129 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
130 130
131 131 # now notify all recipients in question
132 132
133 133 for recipient in rec_objs.union(mention_recipients):
134 134 # inject current recipient
135 135 email_kwargs['recipient'] = recipient
136 136 email_kwargs['mention'] = recipient in mention_recipients
137 137 (subject, headers, email_body,
138 138 email_body_plaintext) = EmailNotificationModel().render_email(
139 139 notification_type, **email_kwargs)
140 140
141 141 log.debug(
142 142 'Creating notification email task for user:`%s`', recipient)
143 143 task = run_task(
144 144 tasks.send_email, recipient.email, subject,
145 145 email_body_plaintext, email_body)
146 146 log.debug('Created email task: %s', task)
147 147
148 148 return notification
149 149
150 150 def delete(self, user, notification):
151 151 # we don't want to remove actual notification just the assignment
152 152 try:
153 153 notification = self.__get_notification(notification)
154 154 user = self._get_user(user)
155 155 if notification and user:
156 156 obj = UserNotification.query()\
157 157 .filter(UserNotification.user == user)\
158 158 .filter(UserNotification.notification == notification)\
159 159 .one()
160 160 Session().delete(obj)
161 161 return True
162 162 except Exception:
163 163 log.error(traceback.format_exc())
164 164 raise
165 165
166 166 def get_for_user(self, user, filter_=None):
167 167 """
168 168 Get mentions for given user, filter them if filter dict is given
169
170 :param user:
171 :param filter:
172 169 """
173 170 user = self._get_user(user)
174 171
175 172 q = UserNotification.query()\
176 173 .filter(UserNotification.user == user)\
177 174 .join((
178 175 Notification, UserNotification.notification_id ==
179 176 Notification.notification_id))
180
181 if filter_:
177 if filter_ == ['all']:
178 q = q # no filter
179 elif filter_ == ['unread']:
180 q = q.filter(UserNotification.read == false())
181 elif filter_:
182 182 q = q.filter(Notification.type_.in_(filter_))
183 183
184 return q.all()
184 return q
185 185
186 186 def mark_read(self, user, notification):
187 187 try:
188 188 notification = self.__get_notification(notification)
189 189 user = self._get_user(user)
190 190 if notification and user:
191 191 obj = UserNotification.query()\
192 192 .filter(UserNotification.user == user)\
193 193 .filter(UserNotification.notification == notification)\
194 194 .one()
195 195 obj.read = True
196 196 Session().add(obj)
197 197 return True
198 198 except Exception:
199 199 log.error(traceback.format_exc())
200 200 raise
201 201
202 202 def mark_all_read_for_user(self, user, filter_=None):
203 203 user = self._get_user(user)
204 204 q = UserNotification.query()\
205 205 .filter(UserNotification.user == user)\
206 206 .filter(UserNotification.read == false())\
207 207 .join((
208 208 Notification, UserNotification.notification_id ==
209 209 Notification.notification_id))
210 if filter_:
210 if filter_ == ['unread']:
211 q = q.filter(UserNotification.read == false())
212 elif filter_:
211 213 q = q.filter(Notification.type_.in_(filter_))
212 214
213 215 # this is a little inefficient but sqlalchemy doesn't support
214 216 # update on joined tables :(
215 217 for obj in q.all():
216 218 obj.read = True
217 219 Session().add(obj)
218 220
219 221 def get_unread_cnt_for_user(self, user):
220 222 user = self._get_user(user)
221 223 return UserNotification.query()\
222 224 .filter(UserNotification.read == false())\
223 225 .filter(UserNotification.user == user).count()
224 226
225 227 def get_unread_for_user(self, user):
226 228 user = self._get_user(user)
227 229 return [x.notification for x in UserNotification.query()
228 230 .filter(UserNotification.read == false())
229 231 .filter(UserNotification.user == user).all()]
230 232
231 233 def get_user_notification(self, user, notification):
232 234 user = self._get_user(user)
233 235 notification = self.__get_notification(notification)
234 236
235 237 return UserNotification.query()\
236 238 .filter(UserNotification.notification == notification)\
237 239 .filter(UserNotification.user == user).scalar()
238 240
239 241 def make_description(self, notification, show_age=True, translate=None):
240 242 """
241 243 Creates a human readable description based on properties
242 244 of notification object
243 245 """
244 246
245 247 _map = {
246 248 notification.TYPE_CHANGESET_COMMENT: [
247 249 _('%(user)s commented on commit %(date_or_age)s'),
248 250 _('%(user)s commented on commit at %(date_or_age)s'),
249 251 ],
250 252 notification.TYPE_MESSAGE: [
251 253 _('%(user)s sent message %(date_or_age)s'),
252 254 _('%(user)s sent message at %(date_or_age)s'),
253 255 ],
254 256 notification.TYPE_MENTION: [
255 257 _('%(user)s mentioned you %(date_or_age)s'),
256 258 _('%(user)s mentioned you at %(date_or_age)s'),
257 259 ],
258 260 notification.TYPE_REGISTRATION: [
259 261 _('%(user)s registered in RhodeCode %(date_or_age)s'),
260 262 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
261 263 ],
262 264 notification.TYPE_PULL_REQUEST: [
263 265 _('%(user)s opened new pull request %(date_or_age)s'),
264 266 _('%(user)s opened new pull request at %(date_or_age)s'),
265 267 ],
266 268 notification.TYPE_PULL_REQUEST_COMMENT: [
267 269 _('%(user)s commented on pull request %(date_or_age)s'),
268 270 _('%(user)s commented on pull request at %(date_or_age)s'),
269 271 ],
270 272 }
271 273
272 274 templates = _map[notification.type_]
273 275
274 276 if show_age:
275 277 template = templates[0]
276 278 date_or_age = h.age(notification.created_on)
277 279 if translate:
278 280 date_or_age = translate(date_or_age)
279 281
280 282 if isinstance(date_or_age, TranslationString):
281 283 date_or_age = date_or_age.interpolate()
282 284
283 285 else:
284 286 template = templates[1]
285 287 date_or_age = h.format_date(notification.created_on)
286 288
287 289 return template % {
288 290 'user': notification.created_by_user.username,
289 291 'date_or_age': date_or_age,
290 292 }
291 293
292 294
293 295 class EmailNotificationModel(BaseModel):
294 296 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
295 297 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
296 298 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
297 299 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
298 300 TYPE_MAIN = Notification.TYPE_MESSAGE
299 301
300 302 TYPE_PASSWORD_RESET = 'password_reset'
301 303 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
302 304 TYPE_EMAIL_TEST = 'email_test'
303 305 TYPE_TEST = 'test'
304 306
305 307 email_types = {
306 308 TYPE_MAIN: 'email_templates/main.mako',
307 309 TYPE_TEST: 'email_templates/test.mako',
308 310 TYPE_EMAIL_TEST: 'email_templates/email_test.mako',
309 311 TYPE_REGISTRATION: 'email_templates/user_registration.mako',
310 312 TYPE_PASSWORD_RESET: 'email_templates/password_reset.mako',
311 313 TYPE_PASSWORD_RESET_CONFIRMATION: 'email_templates/password_reset_confirmation.mako',
312 314 TYPE_COMMIT_COMMENT: 'email_templates/commit_comment.mako',
313 315 TYPE_PULL_REQUEST: 'email_templates/pull_request_review.mako',
314 316 TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.mako',
315 317 }
316 318
317 319 def __init__(self):
318 320 """
319 321 Example usage::
320 322
321 323 (subject, headers, email_body,
322 324 email_body_plaintext) = EmailNotificationModel().render_email(
323 325 EmailNotificationModel.TYPE_TEST, **email_kwargs)
324 326
325 327 """
326 328 super(EmailNotificationModel, self).__init__()
327 329 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
328 330
329 331 def _update_kwargs_for_render(self, kwargs):
330 332 """
331 333 Inject params required for Mako rendering
332 334
333 335 :param kwargs:
334 336 """
335 337
336 338 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
337 339 instance_url = h.route_url('home')
338 340 _kwargs = {
339 341 'instance_url': instance_url,
340 342 'whitespace_filter': self.whitespace_filter
341 343 }
342 344 _kwargs.update(kwargs)
343 345 return _kwargs
344 346
345 347 def whitespace_filter(self, text):
346 348 return text.replace('\n', '').replace('\t', '')
347 349
348 350 def get_renderer(self, type_):
349 351 template_name = self.email_types[type_]
350 352 return PartialRenderer(template_name)
351 353
352 354 def render_email(self, type_, **kwargs):
353 355 """
354 356 renders template for email, and returns a tuple of
355 357 (subject, email_headers, email_html_body, email_plaintext_body)
356 358 """
357 359 # translator and helpers inject
358 360 _kwargs = self._update_kwargs_for_render(kwargs)
359 361
360 362 email_template = self.get_renderer(type_)
361 363
362 364 subject = email_template.render('subject', **_kwargs)
363 365
364 366 try:
365 367 headers = email_template.render('headers', **_kwargs)
366 368 except AttributeError:
367 369 # it's not defined in template, ok we can skip it
368 370 headers = ''
369 371
370 372 try:
371 373 body_plaintext = email_template.render('body_plaintext', **_kwargs)
372 374 except AttributeError:
373 375 # it's not defined in template, ok we can skip it
374 376 body_plaintext = ''
375 377
376 378 # render WHOLE template
377 379 body = email_template.render(None, **_kwargs)
378 380
379 381 return subject, headers, body, body_plaintext
@@ -1,178 +1,183 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('new_repo', '/_admin/create_repository', []);
16 16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
17 17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
18 18 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
19 19 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
20 20 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
21 21 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
22 22 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
23 23 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
24 24 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
25 25 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
26 26 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
27 27 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
28 28 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
29 29 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
30 30 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
31 31 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
32 32 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
33 33 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
34 34 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
35 35 pyroutes.register('changelog_elements', '/%(repo_name)s/changelog_details', ['repo_name']);
36 36 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
37 37 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
38 38 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
39 39 pyroutes.register('files_annotate_home', '/%(repo_name)s/annotate/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
40 40 pyroutes.register('files_annotate_previous', '/%(repo_name)s/annotate-previous/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
41 41 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
42 42 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
43 43 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
44 44 pyroutes.register('favicon', '/favicon.ico', []);
45 45 pyroutes.register('robots', '/robots.txt', []);
46 46 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
47 47 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
48 48 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
49 49 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
50 50 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
51 51 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
52 52 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/settings/integrations', ['repo_group_name']);
53 53 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
54 54 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
55 55 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
56 56 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
57 57 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
58 58 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
59 59 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
60 60 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
61 61 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
62 62 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
63 63 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
64 64 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
65 65 pyroutes.register('admin_home', '/_admin', []);
66 66 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
67 67 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
68 68 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
69 69 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
70 70 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
71 71 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
72 72 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
73 73 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
74 74 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
75 75 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
76 76 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
77 77 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
78 78 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
79 79 pyroutes.register('users', '/_admin/users', []);
80 80 pyroutes.register('users_data', '/_admin/users_data', []);
81 81 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
82 82 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
83 83 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
84 84 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
85 85 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
86 86 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
87 87 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
88 88 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
89 89 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
90 90 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
91 91 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
92 92 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
93 93 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
94 94 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
95 95 pyroutes.register('channelstream_proxy', '/_channelstream', []);
96 96 pyroutes.register('login', '/_admin/login', []);
97 97 pyroutes.register('logout', '/_admin/logout', []);
98 98 pyroutes.register('register', '/_admin/register', []);
99 99 pyroutes.register('reset_password', '/_admin/password_reset', []);
100 100 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
101 101 pyroutes.register('home', '/', []);
102 102 pyroutes.register('user_autocomplete_data', '/_users', []);
103 103 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
104 104 pyroutes.register('repo_list_data', '/_repos', []);
105 105 pyroutes.register('goto_switcher_data', '/_goto_data', []);
106 106 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
107 107 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
108 108 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
109 109 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
110 110 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
111 111 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
112 112 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
113 113 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
114 114 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
115 115 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
116 116 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
117 117 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
118 118 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
119 119 pyroutes.register('changeset_children', '/%(repo_name)s/changeset_children/%(revision)s', ['repo_name', 'revision']);
120 120 pyroutes.register('changeset_parents', '/%(repo_name)s/changeset_parents/%(revision)s', ['repo_name', 'revision']);
121 121 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
122 122 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
123 123 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
124 124 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
125 125 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
126 126 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
127 127 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
128 128 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
129 129 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
130 130 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
131 131 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
132 132 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
133 133 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
134 134 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
135 135 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
136 136 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
137 137 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
138 138 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
139 139 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
140 140 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
141 141 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
142 142 pyroutes.register('search', '/_admin/search', []);
143 143 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
144 144 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
145 145 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
146 146 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
147 147 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
148 148 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
149 149 pyroutes.register('my_account_password_update', '/_admin/my_account/password', []);
150 150 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
151 151 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
152 152 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
153 153 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
154 154 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
155 155 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
156 156 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
157 157 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
158 158 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
159 159 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
160 160 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
161 161 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
162 162 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
163 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
164 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
165 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
166 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
167 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
163 168 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
164 169 pyroutes.register('gists_show', '/_admin/gists', []);
165 170 pyroutes.register('gists_new', '/_admin/gists/new', []);
166 171 pyroutes.register('gists_create', '/_admin/gists/create', []);
167 172 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
168 173 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
169 174 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
170 175 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
171 176 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
172 177 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
173 178 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
174 179 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
175 180 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
176 181 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
177 182 pyroutes.register('apiv2', '/_admin/api', []);
178 183 }
@@ -1,64 +1,66 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var _run_callbacks = function(callbacks){
20 20 if (callbacks !== undefined){
21 21 var _l = callbacks.length;
22 22 for (var i=0;i<_l;i++){
23 23 var func = callbacks[i];
24 24 if(typeof(func)=='function'){
25 25 try{
26 26 func();
27 27 }catch (err){};
28 28 }
29 29 }
30 30 }
31 31 };
32 32
33 var deleteNotification = function(url, notification_id,callbacks){
33 var deleteNotification = function(notification_id, callbacks){
34 34 var callback = function(o){
35 35 var obj = $("#notification_"+notification_id);
36 36 obj.remove();
37 37 _run_callbacks(callbacks);
38 38 };
39 var postData = {'_method': 'delete', 'csrf_token': CSRF_TOKEN};
40 var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
41 var request = $.post(sUrl, postData)
42 .done(callback)
43 .fail(function(data, textStatus, errorThrown){
44 alert("Error while deleting notification.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(this)[0].url));
45 });
39 var postData = {'csrf_token': CSRF_TOKEN};
40 var sUrl = pyroutes.url('notifications_delete', {'notification_id': notification_id});
41 var request =
42 $.post(sUrl, postData)
43 .done(callback)
44 .fail(function(data, textStatus, errorThrown){
45 alert("Error while deleting notification.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(this)[0].url));
46 });
46 47 };
47 48
48 var readNotification = function(url, notification_id,callbacks){
49 var readNotification = function(notification_id, callbacks){
49 50 var callback = function(o){
50 51 var obj = $("#notification_"+notification_id);
51 52 obj.removeClass('unread');
52 var r_button = $('.read-notification',obj)[0];
53 var r_button = $('.read-notification', obj)[0];
53 54 r_button.remove();
54 55
55 56 _run_callbacks(callbacks);
56 57 };
57 var postData = {'_method': 'put', 'csrf_token': CSRF_TOKEN};
58 var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
59 var request = $.post(sUrl, postData)
60 .done(callback)
61 .fail(function(data, textStatus, errorThrown){
62 alert("Error while saving notification.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(this)[0].url));
63 });
58 var postData = {'csrf_token': CSRF_TOKEN};
59 var sUrl = pyroutes.url('notifications_update', {'notification_id': notification_id});
60 var request =
61 $.post(sUrl, postData)
62 .done(callback)
63 .fail(function(data, textStatus, errorThrown){
64 alert("Error while saving notification.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(this)[0].url));
65 });
64 66 };
@@ -1,40 +1,46 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 %if c.notifications:
3 <%
4 unread = lambda n:{False:'unread'}.get(n)
5 %>
6 2
3 <div class="panel panel-default">
4 <div class="panel-heading">
5 <h3 class="panel-title">${_('My notifications')}</h3>
6 </div>
7
8 <div class="panel-body">
9 %if c.notifications:
7 10
8 <div class="notification-list notification-table">
9 %for notification in c.notifications:
10 <div id="notification_${notification.notification.notification_id}" class="container ${unread(notification.read)}">
11 <div class="notification-header">
12 <div class="desc ${unread(notification.read)}">
13 <a href="${h.url('notification', notification_id=notification.notification.notification_id)}">
14 ${base.gravatar(notification.notification.created_by_user.email, 16)}
15 ${notification.notification.description}
16 </a>
17 </div>
18 <div class="delete-notifications">
19 <span id="${notification.notification.notification_id}" class="delete-notification"><i class="icon-delete" ></i></span>
20 </div>
21 <div class="read-notifications">
22 %if not notification.read:
23 <span id="${notification.notification.notification_id}" class="read-notification"><i class="icon-ok" ></i></span>
24 %endif
25 </div>
26 </div>
27 <div class="notification-subject"></div>
28 </div>
29 %endfor
30 </div>
11 <div class="notification-list notification-table">
12 %for notification in c.notifications:
13 <div id="notification_${notification.notification.notification_id}" class="container ${'unread' if not notification.read else '' }">
14 <div class="notification-header">
15 <div class="desc ${'unread' if not notification.read else '' }">
16 <a href="${h.route_path('notifications_show', notification_id=notification.notification.notification_id)}">
17 ${base.gravatar(notification.notification.created_by_user.email, 16)}
18 ${notification.notification.description}
19 </a>
20 </div>
21 <div class="delete-notifications">
22 <span onclick="deleteNotification(${notification.notification.notification_id})" class="delete-notification tooltip" title="${_('Delete')}"><i class="icon-delete"></i></span>
23 </div>
24 <div class="read-notifications">
25 %if not notification.read:
26 <span onclick="readNotification(${notification.notification.notification_id})" class="read-notification tooltip" title="${_('Mark as read')}"><i class="icon-ok"></i></span>
27 %endif
28 </div>
29 </div>
30 <div class="notification-subject"></div>
31 </div>
32 %endfor
33 </div>
31 34
32 <div class="notification-paginator">
33 <div class="pagination-wh pagination-left">
34 ${c.notifications.pager('$link_previous ~2~ $link_next')}
35 <div class="notification-paginator">
36 <div class="pagination-wh pagination-left">
37 ${c.notifications.pager('$link_previous ~2~ $link_next')}
38 </div>
39 </div>
40
41 %else:
42 <div class="table">${_('No notifications here yet')}</div>
43 %endif
44
35 45 </div>
36 46 </div>
37
38 %else:
39 <div class="table">${_('No notifications here yet')}</div>
40 %endif
@@ -1,57 +1,50 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Show notification')} ${c.rhodecode_user.username}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 ${h.link_to(_('Notifications'), h.url('notifications'))}
12 ${h.link_to(_('My Notifications'), h.route_path('notifications_show_all'))}
13 13 &raquo;
14 14 ${_('Show notification')}
15 15 </%def>
16 16
17 17 <%def name="menu_bar_nav()">
18 18 ${self.menu_items(active='admin')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <div class="table">
28 28 <div id="notification_${c.notification.notification_id}" class="main-content-full">
29 29 <div class="notification-header">
30 30 ${self.gravatar(c.notification.created_by_user.email, 30)}
31 31 <div class="desc">
32 32 ${c.notification.description}
33 33 </div>
34 34 <div class="delete-notifications">
35 <span id="${c.notification.notification_id}" class="delete-notification action"><i class="icon-delete" ></i></span>
35 <span class="delete-notification tooltip" title="${_('Delete')}" onclick="deleteNotification(${c.notification.notification_id}, [function(){window.location=pyroutes.url('notifications_show_all')}])" class="delete-notification action"><i class="icon-delete" ></i></span>
36 36 </div>
37 37 </div>
38 38 <div class="notification-body">
39 39 <div class="notification-subject">
40 40 <h3>${_('Subject')}: ${c.notification.subject}</h3>
41 41 </div>
42 42 %if c.notification.body:
43 43 ${h.render(c.notification.body, renderer=c.visual.default_renderer, mentions=True)}
44 44 %endif
45 45 </div>
46 46 </div>
47 47 </div>
48 48 </div>
49 <script type="text/javascript">
50 var url = "${h.url('notification', notification_id='__NOTIFICATION_ID__')}";
51 var main = "${h.url('notifications')}";
52 $('.delete-notification').on('click',function(e){
53 var notification_id = e.currentTarget.id;
54 deleteNotification(url,notification_id,[function(){window.location=main}])
55 })
56 </script>
49
57 50 </%def>
@@ -1,78 +1,68 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('My Notifications')} ${c.rhodecode_user.username}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${_('My Notifications')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_nav()">
16 ${self.menu_items(active='admin')}
16 ${self.menu_items(active='my_account')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 <!-- box / title -->
22 <div class="title">
23 ${self.breadcrumbs()}
24 ##<ul class="links">
25 ## <li>
26 ## <span ><a href="#">${_('Compose message')}</a></span>
27 ## </li>
28 ##</ul>
29
21 <div class="title">
22 ${self.breadcrumbs()}
30 23 <div class="notifications_buttons">
31 <span id='all' class="action-link first ${'active' if c.current_filter=='all' else ''}"><a href="${h.url.current()}">${_('All')}</a></span>
32 <span id='comment' class="action-link ${'active' if c.current_filter=='comment' else ''}"><a href="${h.url.current(type=c.comment_type)}">${_('Comments')}</a></span>
33 <span id='pull_request' class="action-link last ${'active' if c.current_filter=='pull_request' else ''}"><a href="${h.url.current(type=c.pull_request_type)}">${_('Pull Requests')}</a></span>
34
35 24 %if c.notifications:
36
37 <span id='mark_all_read' class="btn btn-default">${_('Mark all as read')}</span>
38
25 <button id='mark_all_read' class="btn btn-default" type="submit">
26 ${_('Mark all as read')}
27 </button>
28 %else:
29 <button class="btn btn-default" type="submit" disabled="disabled">
30 ${_('Mark all as read')}
31 </button>
39 32 %endif
40 33 </div>
34 </div>
35
36 <div class="sidebar-col-wrapper scw-small">
37 ##main
38 <div class="sidebar">
39 <ul class="nav nav-pills nav-stacked">
40 <li id='unread' class="${'active' if c.current_filter=='unread' else ''}"><a href="${h.route_path('notifications_show_all', _query=dict(type=c.unread_type))}">${_('Unread')} (${c.unread_count})</a></li>
41 <li id='all' class="${'active' if c.current_filter=='all' else ''}"><a href="${h.route_path('notifications_show_all', _query=dict(type=c.all_type))}">${_('All')}</a></li>
42 <li id='comment' class="${'active' if c.current_filter=='comment' else ''}"><a href="${h.route_path('notifications_show_all', _query=dict(type=c.comment_type))}">${_('Comments')}</a></li>
43 <li id='pull_request' class="${'active' if c.current_filter=='pull_request' else ''}"><a href="${h.route_path('notifications_show_all', _query=dict(type=c.pull_request_type))}">${_('Pull Requests')}</a></li>
44 </ul>
41 45 </div>
42 <div id='notification_data' class='main-content-full'>
43 <%include file='notifications_data.mako'/>
46
47 <div class="main-content-full-width">
48 <%include file='notifications_data.mako'/>
49 </div>
44 50 </div>
45 51 </div>
52
46 53 <script type="text/javascript">
47 var url_action = "${h.url('notification', notification_id='__NOTIFICATION_ID__')}";
48 var run = function(){
49 $('#notification_data').on('click','.delete-notification',function(e){
50 var notification_id = e.currentTarget.id;
51 deleteNotification(url_action,notification_id)
52 });
53 $('#notification_data').on('click','.read-notification',function(e){
54 var notification_id = e.currentTarget.id;
55 readNotification(url_action,notification_id)
56 })
57 };
58 run();
59 $('#mark_all_read').on('click',function(e){
60 //set notifications as read
61 var url = "${h.url('notifications_mark_all_read', **request.GET.mixed())}";
62 $.post(url, {'csrf_token': CSRF_TOKEN}).
63 done(function(data){
64 // hide notifications counter
65 $('#quick_login_link > .menu_link_notifications').hide();
66 $('#notification_data').html(data);
67 })
68 .fail(function(data, textStatus, errorThrown){
69 alert("Error while saving notifications.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(this)[0].url));
70 });
71 });
72 54
73 var current_filter = $("${c.current_filter}");
74 if (current_filter.length){
75 current_filter.addClass('active');
76 }
55 $('#mark_all_read').on('click',function(e){
56 //set notifications as read
57 var url = "${h.route_path('notifications_mark_all_read', _query=request.GET.mixed())}";
58 $.post(url, {'csrf_token': CSRF_TOKEN}).
59 done(function(data){
60 window.location = "${request.current_route_path(_query=request.GET.mixed())}";
61 })
62 .fail(function(data, textStatus, errorThrown){
63 alert("Error while saving notifications.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(this)[0].url));
64 });
65 });
66
77 67 </script>
78 68 </%def>
@@ -1,604 +1,600 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <div class="outerwrapper">
5 5 <!-- HEADER -->
6 6 <div class="header">
7 7 <div id="header-inner" class="wrapper">
8 8 <div id="logo">
9 9 <div class="logo-wrapper">
10 10 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
11 11 </div>
12 12 %if c.rhodecode_name:
13 13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
14 14 %endif
15 15 </div>
16 16 <!-- MENU BAR NAV -->
17 17 ${self.menu_bar_nav()}
18 18 <!-- END MENU BAR NAV -->
19 19 </div>
20 20 </div>
21 21 ${self.menu_bar_subnav()}
22 22 <!-- END HEADER -->
23 23
24 24 <!-- CONTENT -->
25 25 <div id="content" class="wrapper">
26 26
27 27 <rhodecode-toast id="notifications"></rhodecode-toast>
28 28
29 29 <div class="main">
30 30 ${next.main()}
31 31 </div>
32 32 </div>
33 33 <!-- END CONTENT -->
34 34
35 35 </div>
36 36 <!-- FOOTER -->
37 37 <div id="footer">
38 38 <div id="footer-inner" class="title wrapper">
39 39 <div>
40 40 <p class="footer-link-right">
41 41 % if c.visual.show_version:
42 42 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
43 43 % endif
44 44 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
45 45 % if c.visual.rhodecode_support_url:
46 46 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
47 47 % endif
48 48 </p>
49 49 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
50 50 <p class="server-instance" style="display:${sid}">
51 51 ## display hidden instance ID if specially defined
52 52 % if c.rhodecode_instanceid:
53 53 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
54 54 % endif
55 55 </p>
56 56 </div>
57 57 </div>
58 58 </div>
59 59
60 60 <!-- END FOOTER -->
61 61
62 62 ### MAKO DEFS ###
63 63
64 64 <%def name="menu_bar_subnav()">
65 65 </%def>
66 66
67 67 <%def name="breadcrumbs(class_='breadcrumbs')">
68 68 <div class="${class_}">
69 69 ${self.breadcrumbs_links()}
70 70 </div>
71 71 </%def>
72 72
73 73 <%def name="admin_menu()">
74 74 <ul class="admin_menu submenu">
75 75 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
76 76 <li><a href="${h.url('repos')}">${_('Repositories')}</a></li>
77 77 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
78 78 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
79 79 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
80 80 <li><a href="${h.url('admin_permissions_application')}">${_('Permissions')}</a></li>
81 81 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
82 82 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
83 83 <li><a href="${h.url('admin_defaults_repositories')}">${_('Defaults')}</a></li>
84 84 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
85 85 </ul>
86 86 </%def>
87 87
88 88
89 89 <%def name="dt_info_panel(elements)">
90 90 <dl class="dl-horizontal">
91 91 %for dt, dd, title, show_items in elements:
92 92 <dt>${dt}:</dt>
93 93 <dd title="${h.tooltip(title)}">
94 94 %if callable(dd):
95 95 ## allow lazy evaluation of elements
96 96 ${dd()}
97 97 %else:
98 98 ${dd}
99 99 %endif
100 100 %if show_items:
101 101 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
102 102 %endif
103 103 </dd>
104 104
105 105 %if show_items:
106 106 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
107 107 %for item in show_items:
108 108 <dt></dt>
109 109 <dd>${item}</dd>
110 110 %endfor
111 111 </div>
112 112 %endif
113 113
114 114 %endfor
115 115 </dl>
116 116 </%def>
117 117
118 118
119 119 <%def name="gravatar(email, size=16)">
120 120 <%
121 121 if (size > 16):
122 122 gravatar_class = 'gravatar gravatar-large'
123 123 else:
124 124 gravatar_class = 'gravatar'
125 125 %>
126 126 <%doc>
127 127 TODO: johbo: For now we serve double size images to make it smooth
128 128 for retina. This is how it worked until now. Should be replaced
129 129 with a better solution at some point.
130 130 </%doc>
131 131 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
132 132 </%def>
133 133
134 134
135 135 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
136 136 <% email = h.email_or_none(contact) %>
137 137 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
138 138 ${self.gravatar(email, size)}
139 139 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
140 140 </div>
141 141 </%def>
142 142
143 143
144 144 ## admin menu used for people that have some admin resources
145 145 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
146 146 <ul class="submenu">
147 147 %if repositories:
148 148 <li class="local-admin-repos"><a href="${h.url('repos')}">${_('Repositories')}</a></li>
149 149 %endif
150 150 %if repository_groups:
151 151 <li class="local-admin-repo-groups"><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
152 152 %endif
153 153 %if user_groups:
154 154 <li class="local-admin-user-groups"><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
155 155 %endif
156 156 </ul>
157 157 </%def>
158 158
159 159 <%def name="repo_page_title(repo_instance)">
160 160 <div class="title-content">
161 161 <div class="title-main">
162 162 ## SVN/HG/GIT icons
163 163 %if h.is_hg(repo_instance):
164 164 <i class="icon-hg"></i>
165 165 %endif
166 166 %if h.is_git(repo_instance):
167 167 <i class="icon-git"></i>
168 168 %endif
169 169 %if h.is_svn(repo_instance):
170 170 <i class="icon-svn"></i>
171 171 %endif
172 172
173 173 ## public/private
174 174 %if repo_instance.private:
175 175 <i class="icon-repo-private"></i>
176 176 %else:
177 177 <i class="icon-repo-public"></i>
178 178 %endif
179 179
180 180 ## repo name with group name
181 181 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
182 182
183 183 </div>
184 184
185 185 ## FORKED
186 186 %if repo_instance.fork:
187 187 <p>
188 188 <i class="icon-code-fork"></i> ${_('Fork of')}
189 189 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
190 190 </p>
191 191 %endif
192 192
193 193 ## IMPORTED FROM REMOTE
194 194 %if repo_instance.clone_uri:
195 195 <p>
196 196 <i class="icon-code-fork"></i> ${_('Clone from')}
197 197 <a href="${h.url(h.safe_str(h.hide_credentials(repo_instance.clone_uri)))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
198 198 </p>
199 199 %endif
200 200
201 201 ## LOCKING STATUS
202 202 %if repo_instance.locked[0]:
203 203 <p class="locking_locked">
204 204 <i class="icon-repo-lock"></i>
205 205 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
206 206 </p>
207 207 %elif repo_instance.enable_locking:
208 208 <p class="locking_unlocked">
209 209 <i class="icon-repo-unlock"></i>
210 210 ${_('Repository not locked. Pull repository to lock it.')}
211 211 </p>
212 212 %endif
213 213
214 214 </div>
215 215 </%def>
216 216
217 217 <%def name="repo_menu(active=None)">
218 218 <%
219 219 def is_active(selected):
220 220 if selected == active:
221 221 return "active"
222 222 %>
223 223
224 224 <!--- CONTEXT BAR -->
225 225 <div id="context-bar">
226 226 <div class="wrapper">
227 227 <ul id="context-pages" class="horizontal-list navigation">
228 228 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
229 229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.url('changelog_home', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
230 230 <li class="${is_active('files')}"><a class="menulink" href="${h.url('files_home', repo_name=c.repo_name, revision=c.rhodecode_db_repo.landing_rev[1])}"><div class="menulabel">${_('Files')}</div></a></li>
231 231 <li class="${is_active('compare')}">
232 232 <a class="menulink" href="${h.url('compare_home',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a>
233 233 </li>
234 234 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
235 235 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
236 236 <li class="${is_active('showpullrequest')}">
237 237 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
238 238 %if c.repository_pull_requests:
239 239 <span class="pr_notifications">${c.repository_pull_requests}</span>
240 240 %endif
241 241 <div class="menulabel">${_('Pull Requests')}</div>
242 242 </a>
243 243 </li>
244 244 %endif
245 245 <li class="${is_active('options')}">
246 246 <a class="menulink dropdown">
247 247 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
248 248 </a>
249 249 <ul class="submenu">
250 250 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
251 251 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
252 252 %endif
253 253 %if c.rhodecode_db_repo.fork:
254 254 <li><a href="${h.url('compare_url',repo_name=c.rhodecode_db_repo.fork.repo_name,source_ref_type=c.rhodecode_db_repo.landing_rev[0],source_ref=c.rhodecode_db_repo.landing_rev[1], target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1], merge=1)}">
255 255 ${_('Compare fork')}</a></li>
256 256 %endif
257 257
258 258 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
259 259
260 260 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
261 261 %if c.rhodecode_db_repo.locked[0]:
262 262 <li><a class="locking_del" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
263 263 %else:
264 264 <li><a class="locking_add" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
265 265 %endif
266 266 %endif
267 267 %if c.rhodecode_user.username != h.DEFAULT_USER:
268 268 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
269 269 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}">${_('Fork')}</a></li>
270 270 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
271 271 %endif
272 272 %endif
273 273 </ul>
274 274 </li>
275 275 </ul>
276 276 </div>
277 277 <div class="clear"></div>
278 278 </div>
279 279 <!--- END CONTEXT BAR -->
280 280
281 281 </%def>
282 282
283 283 <%def name="usermenu(active=False)">
284 284 ## USER MENU
285 285 <li id="quick_login_li" class="${'active' if active else ''}">
286 286 <a id="quick_login_link" class="menulink childs">
287 287 ${gravatar(c.rhodecode_user.email, 20)}
288 288 <span class="user">
289 289 %if c.rhodecode_user.username != h.DEFAULT_USER:
290 290 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
291 291 %else:
292 292 <span>${_('Sign in')}</span>
293 293 %endif
294 294 </span>
295 295 </a>
296 296
297 297 <div class="user-menu submenu">
298 298 <div id="quick_login">
299 299 %if c.rhodecode_user.username == h.DEFAULT_USER:
300 300 <h4>${_('Sign in to your account')}</h4>
301 301 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
302 302 <div class="form form-vertical">
303 303 <div class="fields">
304 304 <div class="field">
305 305 <div class="label">
306 306 <label for="username">${_('Username')}:</label>
307 307 </div>
308 308 <div class="input">
309 309 ${h.text('username',class_='focus',tabindex=1)}
310 310 </div>
311 311
312 312 </div>
313 313 <div class="field">
314 314 <div class="label">
315 315 <label for="password">${_('Password')}:</label>
316 316 %if h.HasPermissionAny('hg.password_reset.enabled')():
317 317 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
318 318 %endif
319 319 </div>
320 320 <div class="input">
321 321 ${h.password('password',class_='focus',tabindex=2)}
322 322 </div>
323 323 </div>
324 324 <div class="buttons">
325 325 <div class="register">
326 326 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
327 327 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
328 328 %endif
329 329 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
330 330 </div>
331 331 <div class="submit">
332 332 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
333 333 </div>
334 334 </div>
335 335 </div>
336 336 </div>
337 337 ${h.end_form()}
338 338 %else:
339 339 <div class="">
340 340 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
341 341 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
342 342 <div class="email">${c.rhodecode_user.email}</div>
343 343 </div>
344 344 <div class="">
345 345 <ol class="links">
346 346 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
347 347 % if c.rhodecode_user.personal_repo_group:
348 348 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
349 349 % endif
350 350 <li class="logout">
351 351 ${h.secure_form(h.route_path('logout'), request=request)}
352 352 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
353 353 ${h.end_form()}
354 354 </li>
355 355 </ol>
356 356 </div>
357 357 %endif
358 358 </div>
359 359 </div>
360 360 %if c.rhodecode_user.username != h.DEFAULT_USER:
361 361 <div class="pill_container">
362 % if c.unread_notifications == 0:
363 <a class="menu_link_notifications empty" href="${h.url('notifications')}">${c.unread_notifications}</a>
364 % else:
365 <a class="menu_link_notifications" href="${h.url('notifications')}">${c.unread_notifications}</a>
366 % endif
362 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
367 363 </div>
368 364 % endif
369 365 </li>
370 366 </%def>
371 367
372 368 <%def name="menu_items(active=None)">
373 369 <%
374 370 def is_active(selected):
375 371 if selected == active:
376 372 return "active"
377 373 return ""
378 374 %>
379 375 <ul id="quick" class="main_nav navigation horizontal-list">
380 376 <!-- repo switcher -->
381 377 <li class="${is_active('repositories')} repo_switcher_li has_select2">
382 378 <input id="repo_switcher" name="repo_switcher" type="hidden">
383 379 </li>
384 380
385 381 ## ROOT MENU
386 382 %if c.rhodecode_user.username != h.DEFAULT_USER:
387 383 <li class="${is_active('journal')}">
388 384 <a class="menulink" title="${_('Show activity journal')}" href="${h.url('journal')}">
389 385 <div class="menulabel">${_('Journal')}</div>
390 386 </a>
391 387 </li>
392 388 %else:
393 389 <li class="${is_active('journal')}">
394 390 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.url('public_journal')}">
395 391 <div class="menulabel">${_('Public journal')}</div>
396 392 </a>
397 393 </li>
398 394 %endif
399 395 <li class="${is_active('gists')}">
400 396 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
401 397 <div class="menulabel">${_('Gists')}</div>
402 398 </a>
403 399 </li>
404 400 <li class="${is_active('search')}">
405 401 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
406 402 <div class="menulabel">${_('Search')}</div>
407 403 </a>
408 404 </li>
409 405 % if h.HasPermissionAll('hg.admin')('access admin main page'):
410 406 <li class="${is_active('admin')}">
411 407 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
412 408 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
413 409 </a>
414 410 ${admin_menu()}
415 411 </li>
416 412 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
417 413 <li class="${is_active('admin')}">
418 414 <a class="menulink childs" title="${_('Delegated Admin settings')}">
419 415 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
420 416 </a>
421 417 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
422 418 c.rhodecode_user.repository_groups_admin,
423 419 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
424 420 </li>
425 421 % endif
426 422 % if c.debug_style:
427 423 <li class="${is_active('debug_style')}">
428 424 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
429 425 <div class="menulabel">${_('Style')}</div>
430 426 </a>
431 427 </li>
432 428 % endif
433 429 ## render extra user menu
434 430 ${usermenu(active=(active=='my_account'))}
435 431 </ul>
436 432
437 433 <script type="text/javascript">
438 434 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
439 435
440 436 /*format the look of items in the list*/
441 437 var format = function(state, escapeMarkup){
442 438 if (!state.id){
443 439 return state.text; // optgroup
444 440 }
445 441 var obj_dict = state.obj;
446 442 var tmpl = '';
447 443
448 444 if(obj_dict && state.type == 'repo'){
449 445 if(obj_dict['repo_type'] === 'hg'){
450 446 tmpl += '<i class="icon-hg"></i> ';
451 447 }
452 448 else if(obj_dict['repo_type'] === 'git'){
453 449 tmpl += '<i class="icon-git"></i> ';
454 450 }
455 451 else if(obj_dict['repo_type'] === 'svn'){
456 452 tmpl += '<i class="icon-svn"></i> ';
457 453 }
458 454 if(obj_dict['private']){
459 455 tmpl += '<i class="icon-lock" ></i> ';
460 456 }
461 457 else if(visual_show_public_icon){
462 458 tmpl += '<i class="icon-unlock-alt"></i> ';
463 459 }
464 460 }
465 461 if(obj_dict && state.type == 'commit') {
466 462 tmpl += '<i class="icon-tag"></i>';
467 463 }
468 464 if(obj_dict && state.type == 'group'){
469 465 tmpl += '<i class="icon-folder-close"></i> ';
470 466 }
471 467 tmpl += escapeMarkup(state.text);
472 468 return tmpl;
473 469 };
474 470
475 471 var formatResult = function(result, container, query, escapeMarkup) {
476 472 return format(result, escapeMarkup);
477 473 };
478 474
479 475 var formatSelection = function(data, container, escapeMarkup) {
480 476 return format(data, escapeMarkup);
481 477 };
482 478
483 479 $("#repo_switcher").select2({
484 480 cachedDataSource: {},
485 481 minimumInputLength: 2,
486 482 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
487 483 dropdownAutoWidth: true,
488 484 formatResult: formatResult,
489 485 formatSelection: formatSelection,
490 486 containerCssClass: "repo-switcher",
491 487 dropdownCssClass: "repo-switcher-dropdown",
492 488 escapeMarkup: function(m){
493 489 // don't escape our custom placeholder
494 490 if(m.substr(0,23) == '<div class="menulabel">'){
495 491 return m;
496 492 }
497 493
498 494 return Select2.util.escapeMarkup(m);
499 495 },
500 496 query: $.debounce(250, function(query){
501 497 self = this;
502 498 var cacheKey = query.term;
503 499 var cachedData = self.cachedDataSource[cacheKey];
504 500
505 501 if (cachedData) {
506 502 query.callback({results: cachedData.results});
507 503 } else {
508 504 $.ajax({
509 505 url: pyroutes.url('goto_switcher_data'),
510 506 data: {'query': query.term},
511 507 dataType: 'json',
512 508 type: 'GET',
513 509 success: function(data) {
514 510 self.cachedDataSource[cacheKey] = data;
515 511 query.callback({results: data.results});
516 512 },
517 513 error: function(data, textStatus, errorThrown) {
518 514 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
519 515 }
520 516 })
521 517 }
522 518 })
523 519 });
524 520
525 521 $("#repo_switcher").on('select2-selecting', function(e){
526 522 e.preventDefault();
527 523 window.location = e.choice.url;
528 524 });
529 525
530 526 </script>
531 527 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
532 528 </%def>
533 529
534 530 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
535 531 <div class="modal-dialog">
536 532 <div class="modal-content">
537 533 <div class="modal-header">
538 534 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
539 535 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
540 536 </div>
541 537 <div class="modal-body">
542 538 <div class="block-left">
543 539 <table class="keyboard-mappings">
544 540 <tbody>
545 541 <tr>
546 542 <th></th>
547 543 <th>${_('Site-wide shortcuts')}</th>
548 544 </tr>
549 545 <%
550 546 elems = [
551 547 ('/', 'Open quick search box'),
552 548 ('g h', 'Goto home page'),
553 549 ('g g', 'Goto my private gists page'),
554 550 ('g G', 'Goto my public gists page'),
555 551 ('n r', 'New repository page'),
556 552 ('n g', 'New gist page'),
557 553 ]
558 554 %>
559 555 %for key, desc in elems:
560 556 <tr>
561 557 <td class="keys">
562 558 <span class="key tag">${key}</span>
563 559 </td>
564 560 <td>${desc}</td>
565 561 </tr>
566 562 %endfor
567 563 </tbody>
568 564 </table>
569 565 </div>
570 566 <div class="block-left">
571 567 <table class="keyboard-mappings">
572 568 <tbody>
573 569 <tr>
574 570 <th></th>
575 571 <th>${_('Repositories')}</th>
576 572 </tr>
577 573 <%
578 574 elems = [
579 575 ('g s', 'Goto summary page'),
580 576 ('g c', 'Goto changelog page'),
581 577 ('g f', 'Goto files page'),
582 578 ('g F', 'Goto files page with file search activated'),
583 579 ('g p', 'Goto pull requests page'),
584 580 ('g o', 'Goto repository settings'),
585 581 ('g O', 'Goto repository permissions settings'),
586 582 ]
587 583 %>
588 584 %for key, desc in elems:
589 585 <tr>
590 586 <td class="keys">
591 587 <span class="key tag">${key}</span>
592 588 </td>
593 589 <td>${desc}</td>
594 590 </tr>
595 591 %endfor
596 592 </tbody>
597 593 </table>
598 594 </div>
599 595 </div>
600 596 <div class="modal-footer">
601 597 </div>
602 598 </div><!-- /.modal-content -->
603 599 </div><!-- /.modal-dialog -->
604 600 </div><!-- /.modal -->
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now