##// END OF EJS Templates
Notification system improvements...
marcink -
r1712:cac5109a beta
parent child Browse files
Show More
@@ -0,0 +1,84 b''
1 import logging
2
3 from pylons import tmpl_context as c
4
5 from rhodecode.lib.base import BaseController, render
6 from rhodecode.model.db import Notification
7
8 from rhodecode.model.notification import NotificationModel
9 from rhodecode.lib.auth import LoginRequired
10 from rhodecode.lib import helpers as h
11
12 log = logging.getLogger(__name__)
13
14 class NotificationsController(BaseController):
15 """REST Controller styled on the Atom Publishing Protocol"""
16 # To properly map this controller, ensure your config/routing.py
17 # file has a resource setup:
18 # map.resource('notification', 'notifications', controller='_admin/notifications',
19 # path_prefix='/_admin', name_prefix='_admin_')
20
21 @LoginRequired()
22 def __before__(self):
23 super(NotificationsController, self).__before__()
24
25
26 def index(self, format='html'):
27 """GET /_admin/notifications: All items in the collection"""
28 # url('notifications')
29 c.user = self.rhodecode_user
30 c.notifications = NotificationModel()\
31 .get_for_user(self.rhodecode_user.user_id)
32 return render('admin/notifications/notifications.html')
33
34 def create(self):
35 """POST /_admin/notifications: Create a new item"""
36 # url('notifications')
37
38 def new(self, format='html'):
39 """GET /_admin/notifications/new: Form to create a new item"""
40 # url('new_notification')
41
42 def update(self, notification_id):
43 """PUT /_admin/notifications/id: Update an existing item"""
44 # Forms posted to this method should contain a hidden field:
45 # <input type="hidden" name="_method" value="PUT" />
46 # Or using helpers:
47 # h.form(url('notification', notification_id=ID),
48 # method='put')
49 # url('notification', notification_id=ID)
50
51 def delete(self, notification_id):
52 """DELETE /_admin/notifications/id: Delete an existing item"""
53 # Forms posted to this method should contain a hidden field:
54 # <input type="hidden" name="_method" value="DELETE" />
55 # Or using helpers:
56 # h.form(url('notification', notification_id=ID),
57 # method='delete')
58 # url('notification', notification_id=ID)
59
60 no = Notification.get(notification_id)
61 owner = lambda: no.notifications_to_users.user.user_id == c.rhodecode_user.user_id
62 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
63 NotificationModel().delete(notification_id)
64 return 'ok'
65 return 'fail'
66
67 def show(self, notification_id, format='html'):
68 """GET /_admin/notifications/id: Show a specific item"""
69 # url('notification', notification_id=ID)
70 c.user = self.rhodecode_user
71 c.notification = Notification.get(notification_id)
72
73 unotification = NotificationModel()\
74 .get_user_notification(c.user.user_id,
75 c.notification)
76
77 if unotification.read is False:
78 unotification.mark_as_read()
79
80 return render('admin/notifications/show_notification.html')
81
82 def edit(self, notification_id, format='html'):
83 """GET /_admin/notifications/id/edit: Form to edit an existing item"""
84 # url('edit_notification', notification_id=ID)
@@ -0,0 +1,51 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
3
4 <%def name="title()">
5 ${_('Show notification')} ${c.rhodecode_user.username} - ${c.rhodecode_name}
6 </%def>
7
8 <%def name="breadcrumbs_links()">
9 ${h.link_to(_('Notifications'),h.url('notifications'))}
10 &raquo;
11 ${_('Show notification')}
12 </%def>
13
14 <%def name="page_nav()">
15 ${self.menu('admin')}
16 </%def>
17
18 <%def name="main()">
19 <div class="box">
20 <!-- box / title -->
21 <div class="title">
22 ${self.breadcrumbs()}
23 <ul class="links">
24 <li>
25 <span style="text-transform: uppercase;"><a href="#">${_('Compose message')}</a></span>
26 </li>
27 </ul>
28 </div>
29 <div class="table">
30 <div class="notification-header">
31 <div class="gravatar">
32 <img alt="gravatar" src="${h.gravatar_url(h.email(c.notification.created_by_user.email),24)}"/>
33 </div>
34 <div class="desc">
35 ${c.notification.description}
36 </div>
37 <div class="delete-notifications">
38 <span id="${c.notification.notification_id}" class="delete_icon action"></span>
39 </div>
40 </div>
41 <div>${h.rst(c.notification.body)}</div>
42 </div>
43 </div>
44 <script type="text/javascript">
45 var url = "${url('notification', notification_id='__NOTIFICATION_ID__')}";
46 YUE.on(YUQ('.delete-notification'),'click',function(e){
47 var notification_id = e.currentTarget.id;
48 deleteNotification(url,notification_id)
49 })
50 </script>
51 </%def>
@@ -0,0 +1,117 b''
1 from rhodecode.tests import *
2 from rhodecode.model.db import Notification, User, UserNotification
3
4 from rhodecode.model.user import UserModel
5 from rhodecode.model.notification import NotificationModel
6
7 class TestNotificationsController(TestController):
8
9 def test_index(self):
10 self.log_user()
11
12
13 u1 = UserModel().create_or_update(username='u1', password='qweqwe',
14 email='u1@rhodecode.org',
15 name='u1', lastname='u1').user_id
16 u2 = UserModel().create_or_update(username='u2', password='qweqwe',
17 email='u2@rhodecode.org',
18 name='u2', lastname='u2').user_id
19
20 response = self.app.get(url('notifications'))
21 self.assertTrue('''<div class="table">No notifications here yet</div>'''
22 in response.body)
23
24 cur_user = self._get_logged_user()
25
26 NotificationModel().create(created_by=u1, subject=u'test',
27 body=u'notification_1',
28 recipients=[cur_user])
29 response = self.app.get(url('notifications'))
30
31 self.assertTrue(u'notification_1' in response.body)
32
33 User.delete(u1)
34 User.delete(u2)
35
36 # def test_index_as_xml(self):
37 # response = self.app.get(url('formatted_notifications', format='xml'))
38 #
39 # def test_create(self):
40 # response = self.app.post(url('notifications'))
41 #
42 # def test_new(self):
43 # response = self.app.get(url('new_notification'))
44 #
45 # def test_new_as_xml(self):
46 # response = self.app.get(url('formatted_new_notification', format='xml'))
47 #
48 # def test_update(self):
49 # response = self.app.put(url('notification', notification_id=1))
50 #
51 # def test_update_browser_fakeout(self):
52 # response = self.app.post(url('notification', notification_id=1), params=dict(_method='put'))
53
54 def test_delete(self):
55 self.log_user()
56 cur_user = self._get_logged_user()
57
58 u1 = UserModel().create_or_update(username='u1', password='qweqwe',
59 email='u1@rhodecode.org',
60 name='u1', lastname='u1')
61 u2 = UserModel().create_or_update(username='u2', password='qweqwe',
62 email='u2@rhodecode.org',
63 name='u2', lastname='u2')
64
65 # make two notifications
66 notification = NotificationModel().create(created_by=cur_user,
67 subject=u'test',
68 body=u'hi there',
69 recipients=[cur_user, u1, u2])
70
71 u1 = User.get(u1.user_id)
72 u2 = User.get(u2.user_id)
73
74 # check DB
75 self.assertEqual(u1.notifications, [notification])
76 self.assertEqual(u2.notifications, [notification])
77 cur_usr_id = cur_user.user_id
78 response = self.app.delete(url('notification',
79 notification_id=cur_usr_id))
80
81 cur_user = self._get_logged_user()
82 self.assertEqual(cur_user.notifications, [])
83
84 User.delete(u1.user_id)
85 User.delete(u2.user_id)
86
87
88 # def test_delete_browser_fakeout(self):
89 # response = self.app.post(url('notification', notification_id=1), params=dict(_method='delete'))
90
91 def test_show(self):
92 self.log_user()
93 cur_user = self._get_logged_user()
94 u1 = UserModel().create_or_update(username='u1', password='qweqwe',
95 email='u1@rhodecode.org',
96 name='u1', lastname='u1')
97 u2 = UserModel().create_or_update(username='u2', password='qweqwe',
98 email='u2@rhodecode.org',
99 name='u2', lastname='u2')
100
101 notification = NotificationModel().create(created_by=cur_user,
102 subject='test',
103 body='hi there',
104 recipients=[cur_user, u1, u2])
105
106 response = self.app.get(url('notification',
107 notification_id=notification.notification_id))
108
109 # def test_show_as_xml(self):
110 # response = self.app.get(url('formatted_notification', notification_id=1, format='xml'))
111 #
112 # def test_edit(self):
113 # response = self.app.get(url('edit_notification', notification_id=1))
114 #
115 # def test_edit_as_xml(self):
116 # response = self.app.get(url('formatted_edit_notification', notification_id=1, format='xml'))
117
@@ -62,8 +62,8 b' def make_map(config):'
62 rmap.connect('home', '/', controller='home', action='index')
62 rmap.connect('home', '/', controller='home', action='index')
63 rmap.connect('repo_switcher', '/repos', controller='home',
63 rmap.connect('repo_switcher', '/repos', controller='home',
64 action='repo_switcher')
64 action='repo_switcher')
65 rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*}',
65 rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*}',
66 controller='home',action='branch_tag_switcher')
66 controller='home', action='branch_tag_switcher')
67 rmap.connect('bugtracker',
67 rmap.connect('bugtracker',
68 "http://bitbucket.org/marcinkuzminski/rhodecode/issues",
68 "http://bitbucket.org/marcinkuzminski/rhodecode/issues",
69 _static=True)
69 _static=True)
@@ -267,14 +267,41 b' def make_map(config):'
267 action="show", conditions=dict(method=["GET"]))
267 action="show", conditions=dict(method=["GET"]))
268 m.connect("admin_settings_my_account", "/my_account",
268 m.connect("admin_settings_my_account", "/my_account",
269 action="my_account", conditions=dict(method=["GET"]))
269 action="my_account", conditions=dict(method=["GET"]))
270 m.connect("admin_settings_notifications", "/notifications",
271 action="notifications", conditions=dict(method=["GET"]))
272 m.connect("admin_settings_my_account_update", "/my_account_update",
270 m.connect("admin_settings_my_account_update", "/my_account_update",
273 action="my_account_update", conditions=dict(method=["PUT"]))
271 action="my_account_update", conditions=dict(method=["PUT"]))
274 m.connect("admin_settings_create_repository", "/create_repository",
272 m.connect("admin_settings_create_repository", "/create_repository",
275 action="create_repository", conditions=dict(method=["GET"]))
273 action="create_repository", conditions=dict(method=["GET"]))
276
274
277
275
276 #NOTIFICATION REST ROUTES
277 with rmap.submapper(path_prefix=ADMIN_PREFIX,
278 controller='admin/notifications') as m:
279 m.connect("notifications", "/notifications",
280 action="create", conditions=dict(method=["POST"]))
281 m.connect("notifications", "/notifications",
282 action="index", conditions=dict(method=["GET"]))
283 m.connect("formatted_notifications", "/notifications.{format}",
284 action="index", conditions=dict(method=["GET"]))
285 m.connect("new_notification", "/notifications/new",
286 action="new", conditions=dict(method=["GET"]))
287 m.connect("formatted_new_notification", "/notifications/new.{format}",
288 action="new", conditions=dict(method=["GET"]))
289 m.connect("/notification/{notification_id}",
290 action="update", conditions=dict(method=["PUT"]))
291 m.connect("/notification/{notification_id}",
292 action="delete", conditions=dict(method=["DELETE"]))
293 m.connect("edit_notification", "/notification/{notification_id}/edit",
294 action="edit", conditions=dict(method=["GET"]))
295 m.connect("formatted_edit_notification",
296 "/notification/{notification_id}.{format}/edit",
297 action="edit", conditions=dict(method=["GET"]))
298 m.connect("notification", "/notification/{notification_id}",
299 action="show", conditions=dict(method=["GET"]))
300 m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
301 action="show", conditions=dict(method=["GET"]))
302
303
304
278 #ADMIN MAIN PAGES
305 #ADMIN MAIN PAGES
279 with rmap.submapper(path_prefix=ADMIN_PREFIX,
306 with rmap.submapper(path_prefix=ADMIN_PREFIX,
280 controller='admin/admin') as m:
307 controller='admin/admin') as m:
@@ -357,7 +384,7 b' def make_map(config):'
357
384
358 rmap.connect('changeset_comment_delete', '/{repo_name:.*}/changeset/comment/{comment_id}/delete',
385 rmap.connect('changeset_comment_delete', '/{repo_name:.*}/changeset/comment/{comment_id}/delete',
359 controller='changeset', action='delete_comment',
386 controller='changeset', action='delete_comment',
360 conditions = dict(function=check_repo, method=["DELETE"]))
387 conditions=dict(function=check_repo, method=["DELETE"]))
361
388
362 rmap.connect('raw_changeset_home',
389 rmap.connect('raw_changeset_home',
363 '/{repo_name:.*}/raw-changeset/{revision}',
390 '/{repo_name:.*}/raw-changeset/{revision}',
@@ -372,14 +372,6 b' class SettingsController(BaseController)'
372
372
373 return redirect(url('my_account'))
373 return redirect(url('my_account'))
374
374
375
376 @NotAnonymous()
377 def notifications(self):
378 c.user = User.get(self.rhodecode_user.user_id)
379 c.notifications = NotificationModel().get_for_user(c.user.user_id)
380 return render('admin/users/notifications.html'),
381
382
383 @NotAnonymous()
375 @NotAnonymous()
384 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
376 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
385 def create_repository(self):
377 def create_repository(self):
@@ -32,8 +32,7 b' from pylons.controllers.util import redi'
32 from pylons.decorators import jsonify
32 from pylons.decorators import jsonify
33
33
34 import rhodecode.lib.helpers as h
34 import rhodecode.lib.helpers as h
35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 NotAnonymous
37 from rhodecode.lib.base import BaseRepoController, render
36 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.utils import EmptyChangeset
37 from rhodecode.lib.utils import EmptyChangeset
39 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
@@ -274,13 +273,12 b' class ChangesetController(BaseRepoContro'
274 return render('changeset/raw_changeset.html')
273 return render('changeset/raw_changeset.html')
275
274
276 def comment(self, repo_name, revision):
275 def comment(self, repo_name, revision):
277 ccmodel = ChangesetCommentsModel()
276 ChangesetCommentsModel().create(text=request.POST.get('text'),
278
277 repo_id=c.rhodecode_db_repo.repo_id,
279 ccmodel.create(text=request.POST.get('text'),
278 user_id=c.rhodecode_user.user_id,
280 repo_id=c.rhodecode_db_repo.repo_id,
279 revision=revision,
281 user_id=c.rhodecode_user.user_id,
280 f_path=request.POST.get('f_path'),
282 revision=revision, f_path=request.POST.get('f_path'),
281 line_no=request.POST.get('line'))
283 line_no=request.POST.get('line'))
284
282
285 return redirect(h.url('changeset_home', repo_name=repo_name,
283 return redirect(h.url('changeset_home', repo_name=repo_name,
286 revision=revision))
284 revision=revision))
@@ -288,8 +286,8 b' class ChangesetController(BaseRepoContro'
288 @jsonify
286 @jsonify
289 def delete_comment(self, comment_id):
287 def delete_comment(self, comment_id):
290 co = ChangesetComment.get(comment_id)
288 co = ChangesetComment.get(comment_id)
291 if (h.HasPermissionAny('hg.admin', 'repository.admin')() or
289 owner = lambda : co.author.user_id == c.rhodecode_user.user_id
292 co.author.user_id == c.rhodecode_user.user_id):
290 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
293 ccmodel = ChangesetCommentsModel()
291 ccmodel = ChangesetCommentsModel()
294 ccmodel.delete(comment_id=comment_id)
292 ccmodel.delete(comment_id=comment_id)
295 return True
293 return True
@@ -23,13 +23,16 b''
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26
26 import re
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from pylons.i18n.translation import _
31 from sqlalchemy.util.compat import defaultdict
32
33 from rhodecode.lib import helpers as h
30 from rhodecode.model import BaseModel
34 from rhodecode.model import BaseModel
31 from rhodecode.model.db import ChangesetComment, User, Notification
35 from rhodecode.model.db import ChangesetComment, User, Repository, Notification
32 from sqlalchemy.util.compat import defaultdict
33 from rhodecode.model.notification import NotificationModel
36 from rhodecode.model.notification import NotificationModel
34
37
35 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
@@ -38,6 +41,15 b' log = logging.getLogger(__name__)'
38 class ChangesetCommentsModel(BaseModel):
41 class ChangesetCommentsModel(BaseModel):
39
42
40
43
44 def _extract_mentions(self, s):
45 usrs = []
46 for username in re.findall(r'(?:^@|\s@)(\w+)', s):
47 user_obj = User.get_by_username(username, case_insensitive=True)
48 if user_obj:
49 usrs.append(user_obj)
50
51 return usrs
52
41 def create(self, text, repo_id, user_id, revision, f_path=None,
53 def create(self, text, repo_id, user_id, revision, f_path=None,
42 line_no=None):
54 line_no=None):
43 """
55 """
@@ -51,8 +63,10 b' class ChangesetCommentsModel(BaseModel):'
51 :param line_no:
63 :param line_no:
52 """
64 """
53 if text:
65 if text:
66 repo = Repository.get(repo_id)
67 desc = repo.scm_instance.get_changeset(revision).message
54 comment = ChangesetComment()
68 comment = ChangesetComment()
55 comment.repo_id = repo_id
69 comment.repo = repo
56 comment.user_id = user_id
70 comment.user_id = user_id
57 comment.revision = revision
71 comment.revision = revision
58 comment.text = text
72 comment.text = text
@@ -60,18 +74,26 b' class ChangesetCommentsModel(BaseModel):'
60 comment.line_no = line_no
74 comment.line_no = line_no
61
75
62 self.sa.add(comment)
76 self.sa.add(comment)
63 self.sa.commit()
77 self.sa.flush()
64
78
65 # make notification
79 # make notification
66 usr = User.get(user_id)
80 line = ''
67 subj = 'User %s commented on %s' % (usr.username, revision)
81 if line_no:
82 line = _('on line %s') % line_no
83 subj = h.link_to('Re commit: %(commit_desc)s %(line)s' % \
84 {'commit_desc':desc,'line':line},
85 h.url('changeset_home', repo_name=repo.repo_name,
86 revision = revision,
87 anchor = 'comment-%s' % comment.comment_id
88 )
89 )
68 body = text
90 body = text
69 recipients = ChangesetComment.get_users(revision=revision)
91 recipients = ChangesetComment.get_users(revision=revision)
92 recipients += self._extract_mentions(body)
70 NotificationModel().create(created_by=user_id, subject=subj,
93 NotificationModel().create(created_by=user_id, subject=subj,
71 body = body, recipients = recipients,
94 body = body, recipients = recipients,
72 type_ = Notification.TYPE_CHANGESET_COMMENT)
95 type_ = Notification.TYPE_CHANGESET_COMMENT)
73
96
74
75 return comment
97 return comment
76
98
77 def delete(self, comment_id):
99 def delete(self, comment_id):
@@ -49,7 +49,6 b' from rhodecode.lib.caching_query import '
49 from rhodecode.model.meta import Base, Session
49 from rhodecode.model.meta import Base, Session
50
50
51
51
52
53 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
54
53
55 #==============================================================================
54 #==============================================================================
@@ -286,7 +285,9 b' class User(Base, BaseModel):'
286
285
287 group_member = relationship('UsersGroupMember', cascade='all')
286 group_member = relationship('UsersGroupMember', cascade='all')
288
287
289 notifications = relationship('Notification', secondary='user_to_notification')
288 notifications = relationship('Notification',
289 secondary='user_to_notification',
290 order_by=lambda :Notification.created_on.desc())
290
291
291 @property
292 @property
292 def full_contact(self):
293 def full_contact(self):
@@ -301,11 +302,9 b' class User(Base, BaseModel):'
301 return self.admin
302 return self.admin
302
303
303 def __repr__(self):
304 def __repr__(self):
304 try:
305 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
305 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
306 self.user_id, self.username)
306 self.user_id, self.username)
307
307 except:
308 return self.__class__.__name__
309
308
310 @classmethod
309 @classmethod
311 def get_by_username(cls, username, case_insensitive=False, cache=False):
310 def get_by_username(cls, username, case_insensitive=False, cache=False):
@@ -336,6 +335,7 b' class User(Base, BaseModel):'
336 Session.commit()
335 Session.commit()
337 log.debug('updated user %s lastlogin', self.username)
336 log.debug('updated user %s lastlogin', self.username)
338
337
338
339 class UserLog(Base, BaseModel):
339 class UserLog(Base, BaseModel):
340 __tablename__ = 'user_logs'
340 __tablename__ = 'user_logs'
341 __table_args__ = {'extend_existing':True}
341 __table_args__ = {'extend_existing':True}
@@ -1131,9 +1131,9 b' class Notification(Base, BaseModel):'
1131 __tablename__ = 'notifications'
1131 __tablename__ = 'notifications'
1132 __table_args__ = ({'extend_existing':True})
1132 __table_args__ = ({'extend_existing':True})
1133
1133
1134 TYPE_CHANGESET_COMMENT = 'cs_comment'
1134 TYPE_CHANGESET_COMMENT = u'cs_comment'
1135 TYPE_MESSAGE = 'message'
1135 TYPE_MESSAGE = u'message'
1136 TYPE_MENTION = 'mention'
1136 TYPE_MENTION = u'mention'
1137
1137
1138 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1138 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1139 subject = Column('subject', Unicode(512), nullable=True)
1139 subject = Column('subject', Unicode(512), nullable=True)
@@ -1142,9 +1142,10 b' class Notification(Base, BaseModel):'
1142 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1142 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1143 type_ = Column('type', Unicode(256))
1143 type_ = Column('type', Unicode(256))
1144
1144
1145 create_by_user = relationship('User')
1145 created_by_user = relationship('User')
1146 user_notifications = relationship('UserNotification',
1146 notifications_to_users = relationship('UserNotification',
1147 primaryjoin = 'Notification.notification_id==UserNotification.notification_id',
1147 primaryjoin='Notification.notification_id==UserNotification.notification_id',
1148 lazy='joined',
1148 cascade = "all, delete, delete-orphan")
1149 cascade = "all, delete, delete-orphan")
1149
1150
1150 @property
1151 @property
@@ -1158,16 +1159,20 b' class Notification(Base, BaseModel):'
1158 type_ = Notification.TYPE_MESSAGE
1159 type_ = Notification.TYPE_MESSAGE
1159
1160
1160 notification = cls()
1161 notification = cls()
1161 notification.create_by_user = created_by
1162 notification.created_by_user = created_by
1162 notification.subject = subject
1163 notification.subject = subject
1163 notification.body = body
1164 notification.body = body
1164 notification.type_ = type_
1165 notification.type_ = type_
1165 Session.add(notification)
1166 Session.add(notification)
1166 for u in recipients:
1167 for u in recipients:
1167 u.notifications.append(notification)
1168 u.notifications.append(notification)
1168 Session.commit()
1169 return notification
1169 return notification
1170
1170
1171 @property
1172 def description(self):
1173 from rhodecode.model.notification import NotificationModel
1174 return NotificationModel().make_description(self)
1175
1171 class UserNotification(Base, BaseModel):
1176 class UserNotification(Base, BaseModel):
1172 __tablename__ = 'user_to_notification'
1177 __tablename__ = 'user_to_notification'
1173 __table_args__ = (UniqueConstraint('user_id', 'notification_id'),
1178 __table_args__ = (UniqueConstraint('user_id', 'notification_id'),
@@ -1179,9 +1184,12 b' class UserNotification(Base, BaseModel):'
1179 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1184 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1180
1185
1181 user = relationship('User', single_parent=True, lazy="joined")
1186 user = relationship('User', single_parent=True, lazy="joined")
1182 notification = relationship('Notification',single_parent=True,
1187 notification = relationship('Notification', single_parent=True,)
1183 cascade="all, delete, delete-orphan")
1184
1188
1189 def mark_as_read(self):
1190 self.read = True
1191 Session.add(self)
1192 Session.commit()
1185
1193
1186 class DbMigrateVersion(Base, BaseModel):
1194 class DbMigrateVersion(Base, BaseModel):
1187 __tablename__ = 'db_migrate_version'
1195 __tablename__ = 'db_migrate_version'
@@ -15,7 +15,8 b' cache_manager = cache.CacheManager()'
15 #
15 #
16 Session = scoped_session(
16 Session = scoped_session(
17 sessionmaker(
17 sessionmaker(
18 query_cls=caching_query.query_callable(cache_manager)
18 query_cls = caching_query.query_callable(cache_manager),
19 expire_on_commit = True,
19 )
20 )
20 )
21 )
21
22
@@ -29,15 +29,38 b' import traceback'
29
29
30 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
31
31
32 from rhodecode.lib import safe_unicode
32 from rhodecode.lib.helpers import age
33 from rhodecode.lib.caching_query import FromCache
34
33
35 from rhodecode.model import BaseModel
34 from rhodecode.model import BaseModel
36 from rhodecode.model.db import Notification, User, UserNotification
35 from rhodecode.model.db import Notification, User, UserNotification
37
36
37 log = logging.getLogger(__name__)
38
38
39 class NotificationModel(BaseModel):
39 class NotificationModel(BaseModel):
40
40
41
42 def __get_user(self, user):
43 if isinstance(user, User):
44 return user
45 elif isinstance(user, basestring):
46 return User.get_by_username(username=user)
47 elif isinstance(user, int):
48 return User.get(user)
49 else:
50 raise Exception('Unsupported user must be one of int,'
51 'str or User object')
52
53 def __get_notification(self, notification):
54 if isinstance(notification, Notification):
55 return notification
56 elif isinstance(notification, int):
57 return Notification.get(notification)
58 else:
59 if notification:
60 raise Exception('notification must be int or Instance'
61 ' of Notification got %s' % type(notification))
62
63
41 def create(self, created_by, subject, body, recipients,
64 def create(self, created_by, subject, body, recipients,
42 type_=Notification.TYPE_MESSAGE):
65 type_=Notification.TYPE_MESSAGE):
43 """
66 """
@@ -55,37 +78,61 b' class NotificationModel(BaseModel):'
55 if not getattr(recipients, '__iter__', False):
78 if not getattr(recipients, '__iter__', False):
56 raise Exception('recipients must be a list of iterable')
79 raise Exception('recipients must be a list of iterable')
57
80
58 created_by_obj = created_by
81 created_by_obj = self.__get_user(created_by)
59 if not isinstance(created_by, User):
60 created_by_obj = User.get(created_by)
61
62
82
63 recipients_objs = []
83 recipients_objs = []
64 for u in recipients:
84 for u in recipients:
65 if isinstance(u, User):
85 recipients_objs.append(self.__get_user(u))
66 recipients_objs.append(u)
86 recipients_objs = set(recipients_objs)
67 elif isinstance(u, basestring):
87 return Notification.create(created_by=created_by_obj, subject=subject,
68 recipients_objs.append(User.get_by_username(username=u))
88 body=body, recipients=recipients_objs,
69 elif isinstance(u, int):
70 recipients_objs.append(User.get(u))
71 else:
72 raise Exception('Unsupported recipient must be one of int,'
73 'str or User object')
74
75 Notification.create(created_by=created_by_obj, subject=subject,
76 body = body, recipients = recipients_objs,
77 type_=type_)
89 type_=type_)
78
90
91 def delete(self, notification_id):
92 # we don't want to remove actuall notification just the assignment
93 try:
94 notification_id = int(notification_id)
95 no = self.__get_notification(notification_id)
96 if no:
97 UserNotification.delete(no.notifications_to_users.user_to_notification_id)
98 return True
99 except Exception:
100 log.error(traceback.format_exc())
101 raise
79
102
80 def get_for_user(self, user_id):
103 def get_for_user(self, user_id):
81 return User.get(user_id).notifications
104 return User.get(user_id).notifications
82
105
83 def get_unread_cnt_for_user(self, user_id):
106 def get_unread_cnt_for_user(self, user_id):
84 return UserNotification.query()\
107 return UserNotification.query()\
85 .filter(UserNotification.sent_on == None)\
108 .filter(UserNotification.read == False)\
86 .filter(UserNotification.user_id == user_id).count()
109 .filter(UserNotification.user_id == user_id).count()
87
110
88 def get_unread_for_user(self, user_id):
111 def get_unread_for_user(self, user_id):
89 return [x.notification for x in UserNotification.query()\
112 return [x.notification for x in UserNotification.query()\
90 .filter(UserNotification.sent_on == None)\
113 .filter(UserNotification.read == False)\
91 .filter(UserNotification.user_id == user_id).all()]
114 .filter(UserNotification.user_id == user_id).all()]
115
116 def get_user_notification(self, user, notification):
117 user = self.__get_user(user)
118 notification = self.__get_notification(notification)
119
120 return UserNotification.query()\
121 .filter(UserNotification.notification == notification)\
122 .filter(UserNotification.user == user).scalar()
123
124 def make_description(self, notification):
125 """
126 Creates a human readable description based on properties
127 of notification object
128 """
129
130 _map = {notification.TYPE_CHANGESET_COMMENT:_('commented on commit'),
131 notification.TYPE_MESSAGE:_('sent message'),
132 notification.TYPE_MENTION:_('mentioned you')}
133
134 tmpl = "%(user)s %(action)s %(when)s"
135 data = dict(user=notification.created_by_user.username,
136 action=_map[notification.type_],
137 when=age(notification.created_on))
138 return tmpl % data
@@ -2603,7 +2603,8 b' div.gravatar {'
2603 border: 0px solid #D0D0D0;
2603 border: 0px solid #D0D0D0;
2604 float: left;
2604 float: left;
2605 margin-right: 0.7em;
2605 margin-right: 0.7em;
2606 padding: 2px 2px 0;
2606 padding: 2px 2px 2px 2px;
2607 line-height:0;
2607 -webkit-border-radius: 6px;
2608 -webkit-border-radius: 6px;
2608 -khtml-border-radius: 6px;
2609 -khtml-border-radius: 6px;
2609 -moz-border-radius: 6px;
2610 -moz-border-radius: 6px;
@@ -3481,4 +3482,29 b' form.comment-inline-form {'
3481 }
3482 }
3482 .notifications a:hover{
3483 .notifications a:hover{
3483 text-decoration: none !important;
3484 text-decoration: none !important;
3485 }
3486 .notification-header{
3487
3488 }
3489 .notification-header .desc{
3490 font-size: 16px;
3491 height: 24px;
3492 padding-top: 6px;
3493 float: left
3494 }
3495
3496 .notification-header .desc.unread{
3497 font-weight: bold;
3498 font-size: 17px;
3499 }
3500
3501 .notification-header .delete-notifications{
3502 float: right;
3503 padding-top: 8px;
3504 cursor: pointer;
3505 }
3506 .notification-subject{
3507 clear:both;
3508 border-bottom: 1px solid #eee;
3509 padding:5px 0px 5px 38px;
3484 } No newline at end of file
3510 }
@@ -563,3 +563,19 b' var getSelectionLink = function(selecti'
563 }
563 }
564 }
564 }
565 };
565 };
566
567 var deleteNotification = function(url, notification_id){
568 var callback = {
569 success:function(o){
570 var obj = YUD.get(String("notification_"+notification_id));
571 obj.parentNode.removeChild(obj);
572 },
573 failure:function(o){
574 alert("error");
575 },
576 };
577 var postData = '_method=delete';
578 var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
579 var request = YAHOO.util.Connect.asyncRequest('POST', sUrl,
580 callback, postData);
581 };
@@ -25,14 +25,36 b''
25 </ul>
25 </ul>
26 </div>
26 </div>
27 % if c.notifications:
27 % if c.notifications:
28 <%
29 unread = lambda n:{False:'unread'}.get(n)
30 %>
31 <div class="table">
28 %for notification in c.notifications:
32 %for notification in c.notifications:
29 <div class="table">
33 <div id="notification_${notification.notification_id}">
30 <h4>${notification.subject}</h4>
34 <div class="notification-header">
31 <div>${h.rst(notification.body)}</div>
35 <div class="gravatar">
36 <img alt="gravatar" src="${h.gravatar_url(h.email(notification.created_by_user.email),24)}"/>
37 </div>
38 <div class="desc">
39 <a href="${url('notification', notification_id=notification.notification_id)}">${notification.description}</a>
40 </div>
41 <div class="delete-notifications">
42 <span id="${notification.notification_id}" class="delete-notification delete_icon action"></span>
43 </div>
32 </div>
44 </div>
45 <div class="notification-subject">${h.urlify_text(notification.subject)}</div>
46 </div>
33 %endfor
47 %endfor
48 </div>
34 %else:
49 %else:
35 <div class="table">${_('No notifications here yet')}</div>
50 <div class="table">${_('No notifications here yet')}</div>
36 %endif
51 %endif
37 </div>
52 </div>
53 <script type="text/javascript">
54 var url = "${url('notification', notification_id='__NOTIFICATION_ID__')}";
55 YUE.on(YUQ('.delete-notification'),'click',function(e){
56 var notification_id = e.currentTarget.id;
57 deleteNotification(url,notification_id)
58 })
59 </script>
38 </%def>
60 </%def>
@@ -53,7 +53,7 b''
53 ${h.link_to(c.rhodecode_user.username,h.url('admin_settings_my_account'),title='%s %s'%(c.rhodecode_user.name,c.rhodecode_user.lastname))}
53 ${h.link_to(c.rhodecode_user.username,h.url('admin_settings_my_account'),title='%s %s'%(c.rhodecode_user.name,c.rhodecode_user.lastname))}
54 </div>
54 </div>
55 <div class="notifications">
55 <div class="notifications">
56 <a href="${h.url('admin_settings_notifications')}">${c.unread_notifications}</a>
56 <a href="${h.url('notifications')}">${c.unread_notifications}</a>
57 </div>
57 </div>
58 %endif
58 %endif
59 </div>
59 </div>
@@ -9,6 +9,7 b' setup-app`) and provides the base testin'
9 """
9 """
10 import os
10 import os
11 import time
11 import time
12 import logging
12 from os.path import join as jn
13 from os.path import join as jn
13
14
14 from unittest import TestCase
15 from unittest import TestCase
@@ -20,7 +21,8 b' from routes.util import URLGenerator'
20 from webtest import TestApp
21 from webtest import TestApp
21
22
22 from rhodecode.model import meta
23 from rhodecode.model import meta
23 import logging
24 from rhodecode.model.db import User
25
24 import pylons.test
26 import pylons.test
25
27
26 os.environ['TZ'] = 'UTC'
28 os.environ['TZ'] = 'UTC'
@@ -68,10 +70,11 b' class TestController(TestCase):'
68
70
69 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
71 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
70 password=TEST_USER_ADMIN_PASS):
72 password=TEST_USER_ADMIN_PASS):
73 self._logged_username = username
71 response = self.app.post(url(controller='login', action='index'),
74 response = self.app.post(url(controller='login', action='index'),
72 {'username':username,
75 {'username':username,
73 'password':password})
76 'password':password})
74
77
75 if 'invalid user name' in response.body:
78 if 'invalid user name' in response.body:
76 self.fail('could not login using %s %s' % (username, password))
79 self.fail('could not login using %s %s' % (username, password))
77
80
@@ -79,6 +82,10 b' class TestController(TestCase):'
79 self.assertEqual(response.session['rhodecode_user'].username, username)
82 self.assertEqual(response.session['rhodecode_user'].username, username)
80 return response.follow()
83 return response.follow()
81
84
85 def _get_logged_user(self):
86 return User.get_by_username(self._logged_username)
87
88
82 def checkSessionFlash(self, response, msg):
89 def checkSessionFlash(self, response, msg):
83 self.assertTrue('flash' in response.session)
90 self.assertTrue('flash' in response.session)
84 self.assertTrue(msg in response.session['flash'][0][1])
91 self.assertTrue(msg in response.session['flash'][0][1])
@@ -161,28 +161,35 b' class TestNotifications(unittest.TestCas'
161
161
162
162
163 def setUp(self):
163 def setUp(self):
164 self.u1 = UserModel().create_or_update(username='u1', password='qweqwe',
164 self.u1 = UserModel().create_or_update(username=u'u1', password=u'qweqwe',
165 email='u1@rhodecode.org',
165 email=u'u1@rhodecode.org',
166 name='u1', lastname='u1')
166 name=u'u1', lastname=u'u1')
167 self.u2 = UserModel().create_or_update(username='u2', password='qweqwe',
167 self.u2 = UserModel().create_or_update(username=u'u2', password=u'qweqwe',
168 email='u2@rhodecode.org',
168 email=u'u2@rhodecode.org',
169 name='u2', lastname='u3')
169 name=u'u2', lastname=u'u3')
170 self.u3 = UserModel().create_or_update(username='u3', password='qweqwe',
170 self.u3 = UserModel().create_or_update(username=u'u3', password=u'qweqwe',
171 email='u3@rhodecode.org',
171 email=u'u3@rhodecode.org',
172 name='u3', lastname='u3')
172 name=u'u3', lastname=u'u3')
173
173 def tearDown(self):
174 User.delete(self.u1.user_id)
175 User.delete(self.u2.user_id)
176 User.delete(self.u3.user_id)
174
177
175
178
176 def test_create_notification(self):
179 def test_create_notification(self):
177 usrs = [self.u1, self.u2]
180 usrs = [self.u1, self.u2]
178 notification = Notification.create(created_by=self.u1,
181 notification = Notification.create(created_by=self.u1,
179 subject='subj', body='hi there',
182 subject=u'subj', body=u'hi there',
180 recipients=usrs)
183 recipients=usrs)
184 Session.commit()
181
185
182 notifications = Session.query(Notification).all()
186
187 notifications = Notification.query().all()
188 self.assertEqual(len(notifications), 1)
189
183 unotification = UserNotification.query()\
190 unotification = UserNotification.query()\
184 .filter(UserNotification.notification == notification).all()
191 .filter(UserNotification.notification == notification).all()
185 self.assertEqual(len(notifications), 1)
192
186 self.assertEqual(notifications[0].recipients, [self.u1, self.u2])
193 self.assertEqual(notifications[0].recipients, [self.u1, self.u2])
187 self.assertEqual(notification.notification_id,
194 self.assertEqual(notification.notification_id,
188 notifications[0].notification_id)
195 notifications[0].notification_id)
@@ -192,21 +199,23 b' class TestNotifications(unittest.TestCas'
192
199
193 def test_user_notifications(self):
200 def test_user_notifications(self):
194 notification1 = Notification.create(created_by=self.u1,
201 notification1 = Notification.create(created_by=self.u1,
195 subject='subj', body='hi there',
202 subject=u'subj', body=u'hi there',
196 recipients=[self.u3])
203 recipients=[self.u3])
197 notification2 = Notification.create(created_by=self.u1,
204 notification2 = Notification.create(created_by=self.u1,
198 subject='subj', body='hi there',
205 subject=u'subj', body=u'hi there',
199 recipients=[self.u3])
206 recipients=[self.u3])
200 self.assertEqual(self.u3.notifications, [notification1, notification2])
207 self.assertEqual(self.u3.notifications, [notification1, notification2])
201
208
202 def test_delete_notifications(self):
209 def test_delete_notifications(self):
203 notification = Notification.create(created_by=self.u1,
210 notification = Notification.create(created_by=self.u1,
204 subject='title', body='hi there3',
211 subject=u'title', body=u'hi there3',
205 recipients=[self.u3, self.u1, self.u2])
212 recipients=[self.u3, self.u1, self.u2])
213 Session.commit()
206 notifications = Notification.query().all()
214 notifications = Notification.query().all()
207 self.assertTrue(notification in notifications)
215 self.assertTrue(notification in notifications)
208
216
209 Notification.delete(notification.notification_id)
217 Notification.delete(notification.notification_id)
218 Session.commit()
210
219
211 notifications = Notification.query().all()
220 notifications = Notification.query().all()
212 self.assertFalse(notification in notifications)
221 self.assertFalse(notification in notifications)
@@ -214,8 +223,3 b' class TestNotifications(unittest.TestCas'
214 un = UserNotification.query().filter(UserNotification.notification
223 un = UserNotification.query().filter(UserNotification.notification
215 == notification).all()
224 == notification).all()
216 self.assertEqual(un, [])
225 self.assertEqual(un, [])
217
218 def tearDown(self):
219 User.delete(self.u1.user_id)
220 User.delete(self.u2.user_id)
221 User.delete(self.u3.user_id)
General Comments 0
You need to be logged in to leave comments. Login now