##// 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
@@ -267,14 +267,41 b' def make_map(config):'
267 267 action="show", conditions=dict(method=["GET"]))
268 268 m.connect("admin_settings_my_account", "/my_account",
269 269 action="my_account", conditions=dict(method=["GET"]))
270 m.connect("admin_settings_notifications", "/notifications",
271 action="notifications", conditions=dict(method=["GET"]))
272 270 m.connect("admin_settings_my_account_update", "/my_account_update",
273 271 action="my_account_update", conditions=dict(method=["PUT"]))
274 272 m.connect("admin_settings_create_repository", "/create_repository",
275 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 305 #ADMIN MAIN PAGES
279 306 with rmap.submapper(path_prefix=ADMIN_PREFIX,
280 307 controller='admin/admin') as m:
@@ -372,14 +372,6 b' class SettingsController(BaseController)'
372 372
373 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 375 @NotAnonymous()
384 376 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
385 377 def create_repository(self):
@@ -32,8 +32,7 b' from pylons.controllers.util import redi'
32 32 from pylons.decorators import jsonify
33 33
34 34 import rhodecode.lib.helpers as h
35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
36 NotAnonymous
35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 36 from rhodecode.lib.base import BaseRepoController, render
38 37 from rhodecode.lib.utils import EmptyChangeset
39 38 from rhodecode.lib.compat import OrderedDict
@@ -274,12 +273,11 b' class ChangesetController(BaseRepoContro'
274 273 return render('changeset/raw_changeset.html')
275 274
276 275 def comment(self, repo_name, revision):
277 ccmodel = ChangesetCommentsModel()
278
279 ccmodel.create(text=request.POST.get('text'),
276 ChangesetCommentsModel().create(text=request.POST.get('text'),
280 277 repo_id=c.rhodecode_db_repo.repo_id,
281 278 user_id=c.rhodecode_user.user_id,
282 revision=revision, f_path=request.POST.get('f_path'),
279 revision=revision,
280 f_path=request.POST.get('f_path'),
283 281 line_no=request.POST.get('line'))
284 282
285 283 return redirect(h.url('changeset_home', repo_name=repo_name,
@@ -288,8 +286,8 b' class ChangesetController(BaseRepoContro'
288 286 @jsonify
289 287 def delete_comment(self, comment_id):
290 288 co = ChangesetComment.get(comment_id)
291 if (h.HasPermissionAny('hg.admin', 'repository.admin')() or
292 co.author.user_id == c.rhodecode_user.user_id):
289 owner = lambda : co.author.user_id == c.rhodecode_user.user_id
290 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
293 291 ccmodel = ChangesetCommentsModel()
294 292 ccmodel.delete(comment_id=comment_id)
295 293 return True
@@ -23,13 +23,16 b''
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26
26 import re
27 27 import logging
28 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 34 from rhodecode.model import BaseModel
31 from rhodecode.model.db import ChangesetComment, User, Notification
32 from sqlalchemy.util.compat import defaultdict
35 from rhodecode.model.db import ChangesetComment, User, Repository, Notification
33 36 from rhodecode.model.notification import NotificationModel
34 37
35 38 log = logging.getLogger(__name__)
@@ -38,6 +41,15 b' log = logging.getLogger(__name__)'
38 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 53 def create(self, text, repo_id, user_id, revision, f_path=None,
42 54 line_no=None):
43 55 """
@@ -51,8 +63,10 b' class ChangesetCommentsModel(BaseModel):'
51 63 :param line_no:
52 64 """
53 65 if text:
66 repo = Repository.get(repo_id)
67 desc = repo.scm_instance.get_changeset(revision).message
54 68 comment = ChangesetComment()
55 comment.repo_id = repo_id
69 comment.repo = repo
56 70 comment.user_id = user_id
57 71 comment.revision = revision
58 72 comment.text = text
@@ -60,18 +74,26 b' class ChangesetCommentsModel(BaseModel):'
60 74 comment.line_no = line_no
61 75
62 76 self.sa.add(comment)
63 self.sa.commit()
77 self.sa.flush()
64 78
65 79 # make notification
66 usr = User.get(user_id)
67 subj = 'User %s commented on %s' % (usr.username, revision)
80 line = ''
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 90 body = text
69 91 recipients = ChangesetComment.get_users(revision=revision)
92 recipients += self._extract_mentions(body)
70 93 NotificationModel().create(created_by=user_id, subject=subj,
71 94 body = body, recipients = recipients,
72 95 type_ = Notification.TYPE_CHANGESET_COMMENT)
73 96
74
75 97 return comment
76 98
77 99 def delete(self, comment_id):
@@ -49,7 +49,6 b' from rhodecode.lib.caching_query import '
49 49 from rhodecode.model.meta import Base, Session
50 50
51 51
52
53 52 log = logging.getLogger(__name__)
54 53
55 54 #==============================================================================
@@ -286,7 +285,9 b' class User(Base, BaseModel):'
286 285
287 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 292 @property
292 293 def full_contact(self):
@@ -301,11 +302,9 b' class User(Base, BaseModel):'
301 302 return self.admin
302 303
303 304 def __repr__(self):
304 try:
305 305 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
306 306 self.user_id, self.username)
307 except:
308 return self.__class__.__name__
307
309 308
310 309 @classmethod
311 310 def get_by_username(cls, username, case_insensitive=False, cache=False):
@@ -336,6 +335,7 b' class User(Base, BaseModel):'
336 335 Session.commit()
337 336 log.debug('updated user %s lastlogin', self.username)
338 337
338
339 339 class UserLog(Base, BaseModel):
340 340 __tablename__ = 'user_logs'
341 341 __table_args__ = {'extend_existing':True}
@@ -1131,9 +1131,9 b' class Notification(Base, BaseModel):'
1131 1131 __tablename__ = 'notifications'
1132 1132 __table_args__ = ({'extend_existing':True})
1133 1133
1134 TYPE_CHANGESET_COMMENT = 'cs_comment'
1135 TYPE_MESSAGE = 'message'
1136 TYPE_MENTION = 'mention'
1134 TYPE_CHANGESET_COMMENT = u'cs_comment'
1135 TYPE_MESSAGE = u'message'
1136 TYPE_MENTION = u'mention'
1137 1137
1138 1138 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1139 1139 subject = Column('subject', Unicode(512), nullable=True)
@@ -1142,9 +1142,10 b' class Notification(Base, BaseModel):'
1142 1142 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1143 1143 type_ = Column('type', Unicode(256))
1144 1144
1145 create_by_user = relationship('User')
1146 user_notifications = relationship('UserNotification',
1145 created_by_user = relationship('User')
1146 notifications_to_users = relationship('UserNotification',
1147 1147 primaryjoin = 'Notification.notification_id==UserNotification.notification_id',
1148 lazy='joined',
1148 1149 cascade = "all, delete, delete-orphan")
1149 1150
1150 1151 @property
@@ -1158,16 +1159,20 b' class Notification(Base, BaseModel):'
1158 1159 type_ = Notification.TYPE_MESSAGE
1159 1160
1160 1161 notification = cls()
1161 notification.create_by_user = created_by
1162 notification.created_by_user = created_by
1162 1163 notification.subject = subject
1163 1164 notification.body = body
1164 1165 notification.type_ = type_
1165 1166 Session.add(notification)
1166 1167 for u in recipients:
1167 1168 u.notifications.append(notification)
1168 Session.commit()
1169 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 1176 class UserNotification(Base, BaseModel):
1172 1177 __tablename__ = 'user_to_notification'
1173 1178 __table_args__ = (UniqueConstraint('user_id', 'notification_id'),
@@ -1179,9 +1184,12 b' class UserNotification(Base, BaseModel):'
1179 1184 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1180 1185
1181 1186 user = relationship('User', single_parent=True, lazy="joined")
1182 notification = relationship('Notification',single_parent=True,
1183 cascade="all, delete, delete-orphan")
1187 notification = relationship('Notification', single_parent=True,)
1184 1188
1189 def mark_as_read(self):
1190 self.read = True
1191 Session.add(self)
1192 Session.commit()
1185 1193
1186 1194 class DbMigrateVersion(Base, BaseModel):
1187 1195 __tablename__ = 'db_migrate_version'
@@ -15,7 +15,8 b' cache_manager = cache.CacheManager()'
15 15 #
16 16 Session = scoped_session(
17 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 30 from pylons.i18n.translation import _
31 31
32 from rhodecode.lib import safe_unicode
33 from rhodecode.lib.caching_query import FromCache
32 from rhodecode.lib.helpers import age
34 33
35 34 from rhodecode.model import BaseModel
36 35 from rhodecode.model.db import Notification, User, UserNotification
37 36
37 log = logging.getLogger(__name__)
38 38
39 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 64 def create(self, created_by, subject, body, recipients,
42 65 type_=Notification.TYPE_MESSAGE):
43 66 """
@@ -55,37 +78,61 b' class NotificationModel(BaseModel):'
55 78 if not getattr(recipients, '__iter__', False):
56 79 raise Exception('recipients must be a list of iterable')
57 80
58 created_by_obj = created_by
59 if not isinstance(created_by, User):
60 created_by_obj = User.get(created_by)
61
81 created_by_obj = self.__get_user(created_by)
62 82
63 83 recipients_objs = []
64 84 for u in recipients:
65 if isinstance(u, User):
66 recipients_objs.append(u)
67 elif isinstance(u, basestring):
68 recipients_objs.append(User.get_by_username(username=u))
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,
85 recipients_objs.append(self.__get_user(u))
86 recipients_objs = set(recipients_objs)
87 return Notification.create(created_by=created_by_obj, subject=subject,
76 88 body = body, recipients = recipients_objs,
77 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 103 def get_for_user(self, user_id):
81 104 return User.get(user_id).notifications
82 105
83 106 def get_unread_cnt_for_user(self, user_id):
84 107 return UserNotification.query()\
85 .filter(UserNotification.sent_on == None)\
108 .filter(UserNotification.read == False)\
86 109 .filter(UserNotification.user_id == user_id).count()
87 110
88 111 def get_unread_for_user(self, user_id):
89 112 return [x.notification for x in UserNotification.query()\
90 .filter(UserNotification.sent_on == None)\
113 .filter(UserNotification.read == False)\
91 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 2603 border: 0px solid #D0D0D0;
2604 2604 float: left;
2605 2605 margin-right: 0.7em;
2606 padding: 2px 2px 0;
2606 padding: 2px 2px 2px 2px;
2607 line-height:0;
2607 2608 -webkit-border-radius: 6px;
2608 2609 -khtml-border-radius: 6px;
2609 2610 -moz-border-radius: 6px;
@@ -3481,4 +3482,29 b' form.comment-inline-form {'
3481 3482 }
3482 3483 .notifications a:hover{
3483 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 3510 } No newline at end of file
@@ -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 25 </ul>
26 26 </div>
27 27 % if c.notifications:
28 %for notification in c.notifications:
28 <%
29 unread = lambda n:{False:'unread'}.get(n)
30 %>
29 31 <div class="table">
30 <h4>${notification.subject}</h4>
31 <div>${h.rst(notification.body)}</div>
32 %for notification in c.notifications:
33 <div id="notification_${notification.notification_id}">
34 <div class="notification-header">
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>
44 </div>
45 <div class="notification-subject">${h.urlify_text(notification.subject)}</div>
32 46 </div>
33 47 %endfor
48 </div>
34 49 %else:
35 50 <div class="table">${_('No notifications here yet')}</div>
36 51 %endif
37 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 60 </%def>
@@ -53,7 +53,7 b''
53 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 54 </div>
55 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 57 </div>
58 58 %endif
59 59 </div>
@@ -9,6 +9,7 b' setup-app`) and provides the base testin'
9 9 """
10 10 import os
11 11 import time
12 import logging
12 13 from os.path import join as jn
13 14
14 15 from unittest import TestCase
@@ -20,7 +21,8 b' from routes.util import URLGenerator'
20 21 from webtest import TestApp
21 22
22 23 from rhodecode.model import meta
23 import logging
24 from rhodecode.model.db import User
25
24 26 import pylons.test
25 27
26 28 os.environ['TZ'] = 'UTC'
@@ -68,6 +70,7 b' class TestController(TestCase):'
68 70
69 71 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
70 72 password=TEST_USER_ADMIN_PASS):
73 self._logged_username = username
71 74 response = self.app.post(url(controller='login', action='index'),
72 75 {'username':username,
73 76 'password':password})
@@ -79,6 +82,10 b' class TestController(TestCase):'
79 82 self.assertEqual(response.session['rhodecode_user'].username, username)
80 83 return response.follow()
81 84
85 def _get_logged_user(self):
86 return User.get_by_username(self._logged_username)
87
88
82 89 def checkSessionFlash(self, response, msg):
83 90 self.assertTrue('flash' in response.session)
84 91 self.assertTrue(msg in response.session['flash'][0][1])
@@ -161,28 +161,35 b' class TestNotifications(unittest.TestCas'
161 161
162 162
163 163 def setUp(self):
164 self.u1 = UserModel().create_or_update(username='u1', password='qweqwe',
165 email='u1@rhodecode.org',
166 name='u1', lastname='u1')
167 self.u2 = UserModel().create_or_update(username='u2', password='qweqwe',
168 email='u2@rhodecode.org',
169 name='u2', lastname='u3')
170 self.u3 = UserModel().create_or_update(username='u3', password='qweqwe',
171 email='u3@rhodecode.org',
172 name='u3', lastname='u3')
173
164 self.u1 = UserModel().create_or_update(username=u'u1', password=u'qweqwe',
165 email=u'u1@rhodecode.org',
166 name=u'u1', lastname=u'u1')
167 self.u2 = UserModel().create_or_update(username=u'u2', password=u'qweqwe',
168 email=u'u2@rhodecode.org',
169 name=u'u2', lastname=u'u3')
170 self.u3 = UserModel().create_or_update(username=u'u3', password=u'qweqwe',
171 email=u'u3@rhodecode.org',
172 name=u'u3', lastname=u'u3')
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 179 def test_create_notification(self):
177 180 usrs = [self.u1, self.u2]
178 181 notification = Notification.create(created_by=self.u1,
179 subject='subj', body='hi there',
182 subject=u'subj', body=u'hi there',
180 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 190 unotification = UserNotification.query()\
184 191 .filter(UserNotification.notification == notification).all()
185 self.assertEqual(len(notifications), 1)
192
186 193 self.assertEqual(notifications[0].recipients, [self.u1, self.u2])
187 194 self.assertEqual(notification.notification_id,
188 195 notifications[0].notification_id)
@@ -192,21 +199,23 b' class TestNotifications(unittest.TestCas'
192 199
193 200 def test_user_notifications(self):
194 201 notification1 = Notification.create(created_by=self.u1,
195 subject='subj', body='hi there',
202 subject=u'subj', body=u'hi there',
196 203 recipients=[self.u3])
197 204 notification2 = Notification.create(created_by=self.u1,
198 subject='subj', body='hi there',
205 subject=u'subj', body=u'hi there',
199 206 recipients=[self.u3])
200 207 self.assertEqual(self.u3.notifications, [notification1, notification2])
201 208
202 209 def test_delete_notifications(self):
203 210 notification = Notification.create(created_by=self.u1,
204 subject='title', body='hi there3',
211 subject=u'title', body=u'hi there3',
205 212 recipients=[self.u3, self.u1, self.u2])
213 Session.commit()
206 214 notifications = Notification.query().all()
207 215 self.assertTrue(notification in notifications)
208 216
209 217 Notification.delete(notification.notification_id)
218 Session.commit()
210 219
211 220 notifications = Notification.query().all()
212 221 self.assertFalse(notification in notifications)
@@ -214,8 +223,3 b' class TestNotifications(unittest.TestCas'
214 223 un = UserNotification.query().filter(UserNotification.notification
215 224 == notification).all()
216 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