changeset 1712:cac5109ac3b6 beta

Notification system improvements - deleting - tests - ui - moved to separate controller
author Marcin Kuzminski <marcin@python-works.com>
date Wed, 23 Nov 2011 00:55:05 +0200
parents b369bec5d468
children 54687aa00724
files rhodecode/config/routing.py rhodecode/controllers/admin/notifications.py rhodecode/controllers/admin/settings.py rhodecode/controllers/changeset.py rhodecode/model/comment.py rhodecode/model/db.py rhodecode/model/meta.py rhodecode/model/notification.py rhodecode/public/css/style.css rhodecode/public/js/rhodecode.js rhodecode/templates/admin/notifications/notifications.html rhodecode/templates/admin/notifications/show_notification.html rhodecode/templates/admin/users/notifications.html rhodecode/templates/base/base.html rhodecode/tests/__init__.py rhodecode/tests/functional/test_admin_notifications.py rhodecode/tests/test_models.py
diffstat 17 files changed, 555 insertions(+), 133 deletions(-) [+]
line wrap: on
line diff
--- a/rhodecode/config/routing.py	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/config/routing.py	Wed Nov 23 00:55:05 2011 +0200
@@ -62,8 +62,8 @@
     rmap.connect('home', '/', controller='home', action='index')
     rmap.connect('repo_switcher', '/repos', controller='home',
                  action='repo_switcher')
-    rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*}', 
-                 controller='home',action='branch_tag_switcher')    
+    rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*}',
+                 controller='home', action='branch_tag_switcher')
     rmap.connect('bugtracker',
                  "http://bitbucket.org/marcinkuzminski/rhodecode/issues",
                  _static=True)
@@ -267,14 +267,41 @@
                   action="show", conditions=dict(method=["GET"]))
         m.connect("admin_settings_my_account", "/my_account",
                   action="my_account", conditions=dict(method=["GET"]))
-        m.connect("admin_settings_notifications", "/notifications",
-                  action="notifications", conditions=dict(method=["GET"]))
         m.connect("admin_settings_my_account_update", "/my_account_update",
                   action="my_account_update", conditions=dict(method=["PUT"]))
         m.connect("admin_settings_create_repository", "/create_repository",
                   action="create_repository", conditions=dict(method=["GET"]))
 
 
+    #NOTIFICATION REST ROUTES
+    with rmap.submapper(path_prefix=ADMIN_PREFIX,
+                        controller='admin/notifications') as m:
+        m.connect("notifications", "/notifications",
+                  action="create", conditions=dict(method=["POST"]))
+        m.connect("notifications", "/notifications",
+                  action="index", conditions=dict(method=["GET"]))
+        m.connect("formatted_notifications", "/notifications.{format}",
+                  action="index", conditions=dict(method=["GET"]))
+        m.connect("new_notification", "/notifications/new",
+                  action="new", conditions=dict(method=["GET"]))
+        m.connect("formatted_new_notification", "/notifications/new.{format}",
+                  action="new", conditions=dict(method=["GET"]))
+        m.connect("/notification/{notification_id}",
+                  action="update", conditions=dict(method=["PUT"]))
+        m.connect("/notification/{notification_id}",
+                  action="delete", conditions=dict(method=["DELETE"]))
+        m.connect("edit_notification", "/notification/{notification_id}/edit",
+                  action="edit", conditions=dict(method=["GET"]))
+        m.connect("formatted_edit_notification",
+                  "/notification/{notification_id}.{format}/edit",
+                  action="edit", conditions=dict(method=["GET"]))
+        m.connect("notification", "/notification/{notification_id}",
+                  action="show", conditions=dict(method=["GET"]))
+        m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
+                  action="show", conditions=dict(method=["GET"]))
+
+
+
     #ADMIN MAIN PAGES
     with rmap.submapper(path_prefix=ADMIN_PREFIX,
                         controller='admin/admin') as m:
@@ -357,7 +384,7 @@
 
     rmap.connect('changeset_comment_delete', '/{repo_name:.*}/changeset/comment/{comment_id}/delete',
                 controller='changeset', action='delete_comment',
-                conditions = dict(function=check_repo, method=["DELETE"]))
+                conditions=dict(function=check_repo, method=["DELETE"]))
 
     rmap.connect('raw_changeset_home',
                  '/{repo_name:.*}/raw-changeset/{revision}',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/controllers/admin/notifications.py	Wed Nov 23 00:55:05 2011 +0200
@@ -0,0 +1,84 @@
+import logging
+
+from pylons import tmpl_context as c
+
+from rhodecode.lib.base import BaseController, render
+from rhodecode.model.db import Notification
+
+from rhodecode.model.notification import NotificationModel
+from rhodecode.lib.auth import LoginRequired
+from rhodecode.lib import helpers as h
+
+log = logging.getLogger(__name__)
+
+class NotificationsController(BaseController):
+    """REST Controller styled on the Atom Publishing Protocol"""
+    # To properly map this controller, ensure your config/routing.py
+    # file has a resource setup:
+    #     map.resource('notification', 'notifications', controller='_admin/notifications', 
+    #         path_prefix='/_admin', name_prefix='_admin_')
+
+    @LoginRequired()
+    def __before__(self):
+        super(NotificationsController, self).__before__()
+
+
+    def index(self, format='html'):
+        """GET /_admin/notifications: All items in the collection"""
+        # url('notifications')
+        c.user = self.rhodecode_user
+        c.notifications = NotificationModel()\
+                            .get_for_user(self.rhodecode_user.user_id)
+        return render('admin/notifications/notifications.html')
+
+    def create(self):
+        """POST /_admin/notifications: Create a new item"""
+        # url('notifications')
+
+    def new(self, format='html'):
+        """GET /_admin/notifications/new: Form to create a new item"""
+        # url('new_notification')
+
+    def update(self, notification_id):
+        """PUT /_admin/notifications/id: Update an existing item"""
+        # Forms posted to this method should contain a hidden field:
+        #    <input type="hidden" name="_method" value="PUT" />
+        # Or using helpers:
+        #    h.form(url('notification', notification_id=ID),
+        #           method='put')
+        # url('notification', notification_id=ID)
+
+    def delete(self, notification_id):
+        """DELETE /_admin/notifications/id: Delete an existing item"""
+        # Forms posted to this method should contain a hidden field:
+        #    <input type="hidden" name="_method" value="DELETE" />
+        # Or using helpers:
+        #    h.form(url('notification', notification_id=ID),
+        #           method='delete')
+        # url('notification', notification_id=ID)
+
+        no = Notification.get(notification_id)
+        owner = lambda: no.notifications_to_users.user.user_id == c.rhodecode_user.user_id
+        if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
+                NotificationModel().delete(notification_id)
+                return 'ok'
+        return 'fail'
+
+    def show(self, notification_id, format='html'):
+        """GET /_admin/notifications/id: Show a specific item"""
+        # url('notification', notification_id=ID)
+        c.user = self.rhodecode_user
+        c.notification = Notification.get(notification_id)
+
+        unotification = NotificationModel()\
+                            .get_user_notification(c.user.user_id,
+                                                   c.notification)
+
+        if unotification.read is False:
+            unotification.mark_as_read()
+
+        return render('admin/notifications/show_notification.html')
+
+    def edit(self, notification_id, format='html'):
+        """GET /_admin/notifications/id/edit: Form to edit an existing item"""
+        # url('edit_notification', notification_id=ID)
--- a/rhodecode/controllers/admin/settings.py	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/controllers/admin/settings.py	Wed Nov 23 00:55:05 2011 +0200
@@ -372,14 +372,6 @@
 
         return redirect(url('my_account'))
 
-
-    @NotAnonymous()
-    def notifications(self):
-        c.user = User.get(self.rhodecode_user.user_id)
-        c.notifications = NotificationModel().get_for_user(c.user.user_id)
-        return render('admin/users/notifications.html'),
-
-
     @NotAnonymous()
     @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
     def create_repository(self):
--- a/rhodecode/controllers/changeset.py	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/controllers/changeset.py	Wed Nov 23 00:55:05 2011 +0200
@@ -32,8 +32,7 @@
 from pylons.decorators import jsonify
 
 import rhodecode.lib.helpers as h
-from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
-    NotAnonymous
+from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
 from rhodecode.lib.base import BaseRepoController, render
 from rhodecode.lib.utils import EmptyChangeset
 from rhodecode.lib.compat import OrderedDict
@@ -274,13 +273,12 @@
         return render('changeset/raw_changeset.html')
 
     def comment(self, repo_name, revision):
-        ccmodel = ChangesetCommentsModel()
-
-        ccmodel.create(text=request.POST.get('text'),
-                       repo_id=c.rhodecode_db_repo.repo_id,
-                       user_id=c.rhodecode_user.user_id,
-                       revision=revision, f_path=request.POST.get('f_path'),
-                       line_no=request.POST.get('line'))
+        ChangesetCommentsModel().create(text=request.POST.get('text'),
+                                        repo_id=c.rhodecode_db_repo.repo_id,
+                                        user_id=c.rhodecode_user.user_id,
+                                        revision=revision,
+                                        f_path=request.POST.get('f_path'),
+                                        line_no=request.POST.get('line'))
 
         return redirect(h.url('changeset_home', repo_name=repo_name,
                               revision=revision))
@@ -288,8 +286,8 @@
     @jsonify
     def delete_comment(self, comment_id):
         co = ChangesetComment.get(comment_id)
-        if (h.HasPermissionAny('hg.admin', 'repository.admin')() or
-            co.author.user_id == c.rhodecode_user.user_id):
+        owner = lambda : co.author.user_id == c.rhodecode_user.user_id
+        if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
             ccmodel = ChangesetCommentsModel()
             ccmodel.delete(comment_id=comment_id)
             return True
--- a/rhodecode/model/comment.py	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/model/comment.py	Wed Nov 23 00:55:05 2011 +0200
@@ -23,13 +23,16 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-
+import re
 import logging
 import traceback
 
+from pylons.i18n.translation import _
+from sqlalchemy.util.compat import defaultdict
+
+from rhodecode.lib import helpers as h
 from rhodecode.model import BaseModel
-from rhodecode.model.db import ChangesetComment, User, Notification
-from sqlalchemy.util.compat import defaultdict
+from rhodecode.model.db import ChangesetComment, User, Repository, Notification
 from rhodecode.model.notification import NotificationModel
 
 log = logging.getLogger(__name__)
@@ -38,6 +41,15 @@
 class ChangesetCommentsModel(BaseModel):
 
 
+    def _extract_mentions(self, s):
+        usrs = []
+        for username in re.findall(r'(?:^@|\s@)(\w+)', s):
+            user_obj = User.get_by_username(username, case_insensitive=True)
+            if user_obj:
+                usrs.append(user_obj)
+
+        return usrs
+
     def create(self, text, repo_id, user_id, revision, f_path=None,
                line_no=None):
         """
@@ -51,8 +63,10 @@
         :param line_no:
         """
         if text:
+            repo = Repository.get(repo_id)
+            desc = repo.scm_instance.get_changeset(revision).message
             comment = ChangesetComment()
-            comment.repo_id = repo_id
+            comment.repo = repo
             comment.user_id = user_id
             comment.revision = revision
             comment.text = text
@@ -60,18 +74,26 @@
             comment.line_no = line_no
 
             self.sa.add(comment)
-            self.sa.commit()
+            self.sa.flush()
 
             # make notification
-            usr = User.get(user_id)
-            subj = 'User %s commented on %s' % (usr.username, revision)
+            line = ''
+            if line_no:
+                line = _('on line %s') % line_no
+            subj = h.link_to('Re commit: %(commit_desc)s %(line)s' % \
+                                    {'commit_desc':desc,'line':line},
+                             h.url('changeset_home', repo_name=repo.repo_name,
+                                   revision = revision,
+                                   anchor = 'comment-%s' % comment.comment_id
+                                   )
+                             )
             body = text
             recipients = ChangesetComment.get_users(revision=revision)
+            recipients += self._extract_mentions(body)
             NotificationModel().create(created_by=user_id, subject=subj,
                                    body = body, recipients = recipients,
                                    type_ = Notification.TYPE_CHANGESET_COMMENT)
 
-
             return comment
 
     def delete(self, comment_id):
--- a/rhodecode/model/db.py	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/model/db.py	Wed Nov 23 00:55:05 2011 +0200
@@ -49,7 +49,6 @@
 from rhodecode.model.meta import Base, Session
 
 
-
 log = logging.getLogger(__name__)
 
 #==============================================================================
@@ -286,7 +285,9 @@
 
     group_member = relationship('UsersGroupMember', cascade='all')
 
-    notifications = relationship('Notification', secondary='user_to_notification')
+    notifications = relationship('Notification',
+                            secondary='user_to_notification',
+                            order_by=lambda :Notification.created_on.desc())
 
     @property
     def full_contact(self):
@@ -301,11 +302,9 @@
         return self.admin
 
     def __repr__(self):
-        try:
-            return "<%s('id:%s:%s')>" % (self.__class__.__name__,
-                                             self.user_id, self.username)
-        except:
-            return self.__class__.__name__
+        return "<%s('id:%s:%s')>" % (self.__class__.__name__,
+                                     self.user_id, self.username)
+
 
     @classmethod
     def get_by_username(cls, username, case_insensitive=False, cache=False):
@@ -336,6 +335,7 @@
         Session.commit()
         log.debug('updated user %s lastlogin', self.username)
 
+
 class UserLog(Base, BaseModel):
     __tablename__ = 'user_logs'
     __table_args__ = {'extend_existing':True}
@@ -1131,9 +1131,9 @@
     __tablename__ = 'notifications'
     __table_args__ = ({'extend_existing':True})
 
-    TYPE_CHANGESET_COMMENT = 'cs_comment'
-    TYPE_MESSAGE = 'message'
-    TYPE_MENTION = 'mention'
+    TYPE_CHANGESET_COMMENT = u'cs_comment'
+    TYPE_MESSAGE = u'message'
+    TYPE_MENTION = u'mention'
 
     notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
     subject = Column('subject', Unicode(512), nullable=True)
@@ -1142,9 +1142,10 @@
     created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
     type_ = Column('type', Unicode(256))
 
-    create_by_user = relationship('User')
-    user_notifications = relationship('UserNotification',
-        primaryjoin = 'Notification.notification_id==UserNotification.notification_id',
+    created_by_user = relationship('User')
+    notifications_to_users = relationship('UserNotification',
+        primaryjoin='Notification.notification_id==UserNotification.notification_id',
+        lazy='joined',
         cascade = "all, delete, delete-orphan")
 
     @property
@@ -1158,16 +1159,20 @@
             type_ = Notification.TYPE_MESSAGE
 
         notification = cls()
-        notification.create_by_user = created_by
+        notification.created_by_user = created_by
         notification.subject = subject
         notification.body = body
         notification.type_ = type_
         Session.add(notification)
         for u in recipients:
             u.notifications.append(notification)
-        Session.commit()
         return notification
 
+    @property
+    def description(self):
+        from rhodecode.model.notification import NotificationModel
+        return NotificationModel().make_description(self)
+
 class UserNotification(Base, BaseModel):
     __tablename__ = 'user_to_notification'
     __table_args__ = (UniqueConstraint('user_id', 'notification_id'),
@@ -1179,9 +1184,12 @@
     sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
 
     user = relationship('User', single_parent=True, lazy="joined")
-    notification = relationship('Notification',single_parent=True,
-                                cascade="all, delete, delete-orphan")
+    notification = relationship('Notification', single_parent=True,)
 
+    def mark_as_read(self):
+        self.read = True
+        Session.add(self)
+        Session.commit()
 
 class DbMigrateVersion(Base, BaseModel):
     __tablename__ = 'db_migrate_version'
--- a/rhodecode/model/meta.py	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/model/meta.py	Wed Nov 23 00:55:05 2011 +0200
@@ -15,7 +15,8 @@
 #
 Session = scoped_session(
                 sessionmaker(
-                    query_cls=caching_query.query_callable(cache_manager)
+                    query_cls = caching_query.query_callable(cache_manager),
+                    expire_on_commit = True,
                 )
           )
 
--- a/rhodecode/model/notification.py	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/model/notification.py	Wed Nov 23 00:55:05 2011 +0200
@@ -29,15 +29,38 @@
 
 from pylons.i18n.translation import _
 
-from rhodecode.lib import safe_unicode
-from rhodecode.lib.caching_query import FromCache
+from rhodecode.lib.helpers import age
 
 from rhodecode.model import BaseModel
 from rhodecode.model.db import Notification, User, UserNotification
 
+log = logging.getLogger(__name__)
 
 class NotificationModel(BaseModel):
 
+
+    def __get_user(self, user):
+        if isinstance(user, User):
+            return user
+        elif isinstance(user, basestring):
+            return User.get_by_username(username=user)
+        elif isinstance(user, int):
+            return User.get(user)
+        else:
+            raise Exception('Unsupported user must be one of int,'
+                            'str or User object')
+
+    def __get_notification(self, notification):
+        if isinstance(notification, Notification):
+            return notification
+        elif isinstance(notification, int):
+            return Notification.get(notification)
+        else:
+            if notification:
+                raise Exception('notification must be int or Instance'
+                                ' of Notification got %s' % type(notification))
+
+
     def create(self, created_by, subject, body, recipients,
                type_=Notification.TYPE_MESSAGE):
         """
@@ -55,37 +78,61 @@
         if not getattr(recipients, '__iter__', False):
             raise Exception('recipients must be a list of iterable')
 
-        created_by_obj = created_by
-        if not isinstance(created_by, User):
-            created_by_obj = User.get(created_by)
-
+        created_by_obj = self.__get_user(created_by)
 
         recipients_objs = []
         for u in recipients:
-            if isinstance(u, User):
-                recipients_objs.append(u)
-            elif isinstance(u, basestring):
-                recipients_objs.append(User.get_by_username(username=u))
-            elif isinstance(u, int):
-                recipients_objs.append(User.get(u))
-            else:
-                raise Exception('Unsupported recipient must be one of int,'
-                                'str or User object')
-
-        Notification.create(created_by=created_by_obj, subject=subject,
-                            body = body, recipients = recipients_objs,
+            recipients_objs.append(self.__get_user(u))
+        recipients_objs = set(recipients_objs)
+        return Notification.create(created_by=created_by_obj, subject=subject,
+                            body=body, recipients=recipients_objs,
                             type_=type_)
 
+    def delete(self, notification_id):
+        # we don't want to remove actuall notification just the assignment
+        try:
+            notification_id = int(notification_id)
+            no = self.__get_notification(notification_id)
+            if no:
+                UserNotification.delete(no.notifications_to_users.user_to_notification_id)
+                return True
+        except Exception:
+            log.error(traceback.format_exc())
+            raise
 
     def get_for_user(self, user_id):
         return User.get(user_id).notifications
 
     def get_unread_cnt_for_user(self, user_id):
         return UserNotification.query()\
-                .filter(UserNotification.sent_on == None)\
+                .filter(UserNotification.read == False)\
                 .filter(UserNotification.user_id == user_id).count()
 
     def get_unread_for_user(self, user_id):
         return [x.notification for x in UserNotification.query()\
-                .filter(UserNotification.sent_on == None)\
+                .filter(UserNotification.read == False)\
                 .filter(UserNotification.user_id == user_id).all()]
+
+    def get_user_notification(self, user, notification):
+        user = self.__get_user(user)
+        notification = self.__get_notification(notification)
+
+        return UserNotification.query()\
+            .filter(UserNotification.notification == notification)\
+            .filter(UserNotification.user == user).scalar()
+
+    def make_description(self, notification):
+        """
+        Creates a human readable description based on properties
+        of notification object
+        """
+
+        _map = {notification.TYPE_CHANGESET_COMMENT:_('commented on commit'),
+                notification.TYPE_MESSAGE:_('sent message'),
+                notification.TYPE_MENTION:_('mentioned you')}
+
+        tmpl = "%(user)s %(action)s %(when)s"
+        data = dict(user=notification.created_by_user.username,
+                    action=_map[notification.type_],
+                    when=age(notification.created_on))
+        return tmpl % data
--- a/rhodecode/public/css/style.css	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/public/css/style.css	Wed Nov 23 00:55:05 2011 +0200
@@ -2603,7 +2603,8 @@
 	border: 0px solid #D0D0D0;
 	float: left;
 	margin-right: 0.7em;
-	padding: 2px 2px 0;
+	padding: 2px 2px 2px 2px;
+    line-height:0;
 	-webkit-border-radius: 6px;
 	-khtml-border-radius: 6px;
 	-moz-border-radius: 6px;
@@ -3481,4 +3482,29 @@
 }
 .notifications a:hover{
 	text-decoration: none !important;
+}
+.notification-header{
+	
+}
+.notification-header .desc{
+	font-size: 16px;
+    height: 24px;
+    padding-top: 6px;
+    float: left
+}
+
+.notification-header .desc.unread{
+    font-weight: bold;
+    font-size: 17px;
+}
+
+.notification-header .delete-notifications{
+    float: right;
+    padding-top: 8px;
+    cursor: pointer;
+}
+.notification-subject{
+    clear:both;
+    border-bottom: 1px solid #eee;
+    padding:5px 0px 5px 38px;
 }
\ No newline at end of file
--- a/rhodecode/public/js/rhodecode.js	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/public/js/rhodecode.js	Wed Nov 23 00:55:05 2011 +0200
@@ -563,3 +563,19 @@
 	    }
 	}
 };
+
+var deleteNotification = function(url, notification_id){
+    var callback = { 
+		success:function(o){
+		    var obj = YUD.get(String("notification_"+notification_id));
+			obj.parentNode.removeChild(obj);
+		},
+	    failure:function(o){
+	        alert("error");
+	    },
+	};
+    var postData = '_method=delete';
+    var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
+    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, 
+    											  callback, postData);
+};	
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/admin/notifications/notifications.html	Wed Nov 23 00:55:05 2011 +0200
@@ -0,0 +1,60 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${_('My Notifications')} ${c.rhodecode_user.username} - ${c.rhodecode_name}
+</%def>
+
+<%def name="breadcrumbs_links()">
+    ${_('My Notifications')}
+</%def>
+
+<%def name="page_nav()">
+	${self.menu('admin')}
+</%def>
+
+<%def name="main()">
+<div class="box">
+    <!-- box / title -->
+    <div class="title">
+        ${self.breadcrumbs()}       
+        <ul class="links">
+            <li>
+              <span style="text-transform: uppercase;"><a href="#">${_('Compose message')}</a></span>
+            </li>          
+        </ul>            
+    </div>
+    % if c.notifications:
+    <%
+    unread = lambda n:{False:'unread'}.get(n)
+    %>
+    <div class="table">
+      %for notification in c.notifications:
+        <div id="notification_${notification.notification_id}">
+          <div class="notification-header">
+            <div class="gravatar">
+                <img alt="gravatar" src="${h.gravatar_url(h.email(notification.created_by_user.email),24)}"/>
+            </div>
+            <div class="desc">
+            <a href="${url('notification', notification_id=notification.notification_id)}">${notification.description}</a>
+            </div>
+            <div class="delete-notifications">
+              <span id="${notification.notification_id}" class="delete-notification delete_icon action"></span>
+            </div>
+          </div>
+          <div class="notification-subject">${h.urlify_text(notification.subject)}</div>
+        </div>
+      %endfor
+    </div>
+    %else:
+        <div class="table">${_('No notifications here yet')}</div>
+    %endif
+</div>
+<script type="text/javascript">
+var url = "${url('notification', notification_id='__NOTIFICATION_ID__')}";
+   YUE.on(YUQ('.delete-notification'),'click',function(e){
+	   var notification_id = e.currentTarget.id;
+	   deleteNotification(url,notification_id)
+   })
+</script>
+</%def>  
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/admin/notifications/show_notification.html	Wed Nov 23 00:55:05 2011 +0200
@@ -0,0 +1,51 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${_('Show notification')} ${c.rhodecode_user.username} - ${c.rhodecode_name}
+</%def>
+
+<%def name="breadcrumbs_links()">
+    ${h.link_to(_('Notifications'),h.url('notifications'))}
+    &raquo; 
+    ${_('Show notification')}
+</%def>
+
+<%def name="page_nav()">
+    ${self.menu('admin')}
+</%def>
+
+<%def name="main()">
+<div class="box">
+    <!-- box / title -->
+    <div class="title">
+        ${self.breadcrumbs()}       
+        <ul class="links">
+            <li>
+              <span style="text-transform: uppercase;"><a href="#">${_('Compose message')}</a></span>
+            </li>          
+        </ul>            
+    </div>
+    <div class="table">
+      <div class="notification-header">
+        <div class="gravatar">
+            <img alt="gravatar" src="${h.gravatar_url(h.email(c.notification.created_by_user.email),24)}"/>
+        </div>
+        <div class="desc">
+            ${c.notification.description}
+        </div>
+        <div class="delete-notifications">
+          <span id="${c.notification.notification_id}" class="delete_icon action"></span>
+        </div>
+      </div>
+      <div>${h.rst(c.notification.body)}</div>
+    </div>
+</div>
+<script type="text/javascript">
+var url = "${url('notification', notification_id='__NOTIFICATION_ID__')}";
+   YUE.on(YUQ('.delete-notification'),'click',function(e){
+       var notification_id = e.currentTarget.id;
+       deleteNotification(url,notification_id)
+   })
+</script>
+</%def>  
--- a/rhodecode/templates/admin/users/notifications.html	Tue Nov 22 14:10:33 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="/base/base.html"/>
-
-<%def name="title()">
-    ${_('My Notifications')} ${c.rhodecode_user.username} - ${c.rhodecode_name}
-</%def>
-
-<%def name="breadcrumbs_links()">
-    ${_('My Notifications')}
-</%def>
-
-<%def name="page_nav()">
-	${self.menu('admin')}
-</%def>
-
-<%def name="main()">
-<div class="box">
-    <!-- box / title -->
-    <div class="title">
-        ${self.breadcrumbs()}       
-        <ul class="links">
-            <li>
-              <span style="text-transform: uppercase;"><a href="#">${_('Compose message')}</a></span>
-            </li>          
-        </ul>            
-    </div>
-    % if c.notifications:
-      %for notification in c.notifications:
-          <div class="table">
-            <h4>${notification.subject}</h4>
-            <div>${h.rst(notification.body)}</div>
-          </div>
-      %endfor
-    %else:
-        <div class="table">${_('No notifications here yet')}</div>
-    %endif
-</div>    
-</%def>  
--- a/rhodecode/templates/base/base.html	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/templates/base/base.html	Wed Nov 23 00:55:05 2011 +0200
@@ -53,7 +53,7 @@
             ${h.link_to(c.rhodecode_user.username,h.url('admin_settings_my_account'),title='%s %s'%(c.rhodecode_user.name,c.rhodecode_user.lastname))}
             </div>
             <div class="notifications">
-            <a href="${h.url('admin_settings_notifications')}">${c.unread_notifications}</a>
+            <a href="${h.url('notifications')}">${c.unread_notifications}</a>
             </div>
           %endif
           </div>	
--- a/rhodecode/tests/__init__.py	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/tests/__init__.py	Wed Nov 23 00:55:05 2011 +0200
@@ -9,6 +9,7 @@
 """
 import os
 import time
+import logging
 from os.path import join as jn
 
 from unittest import TestCase
@@ -20,7 +21,8 @@
 from webtest import TestApp
 
 from rhodecode.model import meta
-import logging
+from rhodecode.model.db import User
+
 import pylons.test
 
 os.environ['TZ'] = 'UTC'
@@ -68,10 +70,11 @@
 
     def log_user(self, username=TEST_USER_ADMIN_LOGIN,
                  password=TEST_USER_ADMIN_PASS):
+        self._logged_username = username
         response = self.app.post(url(controller='login', action='index'),
                                  {'username':username,
                                   'password':password})
-        
+
         if 'invalid user name' in response.body:
             self.fail('could not login using %s %s' % (username, password))
 
@@ -79,6 +82,10 @@
         self.assertEqual(response.session['rhodecode_user'].username, username)
         return response.follow()
 
+    def _get_logged_user(self):
+        return User.get_by_username(self._logged_username)
+
+
     def checkSessionFlash(self, response, msg):
         self.assertTrue('flash' in response.session)
         self.assertTrue(msg in response.session['flash'][0][1])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/tests/functional/test_admin_notifications.py	Wed Nov 23 00:55:05 2011 +0200
@@ -0,0 +1,117 @@
+from rhodecode.tests import *
+from rhodecode.model.db import Notification, User, UserNotification
+
+from rhodecode.model.user import UserModel
+from rhodecode.model.notification import NotificationModel
+
+class TestNotificationsController(TestController):
+
+    def test_index(self):
+        self.log_user()
+
+
+        u1 = UserModel().create_or_update(username='u1', password='qweqwe',
+                                               email='u1@rhodecode.org',
+                                               name='u1', lastname='u1').user_id
+        u2 = UserModel().create_or_update(username='u2', password='qweqwe',
+                                               email='u2@rhodecode.org',
+                                               name='u2', lastname='u2').user_id
+
+        response = self.app.get(url('notifications'))
+        self.assertTrue('''<div class="table">No notifications here yet</div>'''
+                        in response.body)
+
+        cur_user = self._get_logged_user()
+
+        NotificationModel().create(created_by=u1, subject=u'test',
+                                   body=u'notification_1',
+                                   recipients=[cur_user])
+        response = self.app.get(url('notifications'))
+
+        self.assertTrue(u'notification_1' in response.body)
+
+        User.delete(u1)
+        User.delete(u2)
+
+#    def test_index_as_xml(self):
+#        response = self.app.get(url('formatted_notifications', format='xml'))
+#
+#    def test_create(self):
+#        response = self.app.post(url('notifications'))
+#
+#    def test_new(self):
+#        response = self.app.get(url('new_notification'))
+#
+#    def test_new_as_xml(self):
+#        response = self.app.get(url('formatted_new_notification', format='xml'))
+#
+#    def test_update(self):
+#        response = self.app.put(url('notification', notification_id=1))
+#
+#    def test_update_browser_fakeout(self):
+#        response = self.app.post(url('notification', notification_id=1), params=dict(_method='put'))
+
+    def test_delete(self):
+        self.log_user()
+        cur_user = self._get_logged_user()
+
+        u1 = UserModel().create_or_update(username='u1', password='qweqwe',
+                                               email='u1@rhodecode.org',
+                                               name='u1', lastname='u1')
+        u2 = UserModel().create_or_update(username='u2', password='qweqwe',
+                                               email='u2@rhodecode.org',
+                                               name='u2', lastname='u2')
+
+        # make two notifications 
+        notification = NotificationModel().create(created_by=cur_user,
+                                                  subject=u'test',
+                                                  body=u'hi there',
+                                                  recipients=[cur_user, u1, u2])
+
+        u1 = User.get(u1.user_id)
+        u2 = User.get(u2.user_id)
+
+        # check DB
+        self.assertEqual(u1.notifications, [notification])
+        self.assertEqual(u2.notifications, [notification])
+        cur_usr_id = cur_user.user_id
+        response = self.app.delete(url('notification',
+                                       notification_id=cur_usr_id))
+
+        cur_user = self._get_logged_user()
+        self.assertEqual(cur_user.notifications, [])
+
+        User.delete(u1.user_id)
+        User.delete(u2.user_id)
+
+
+#    def test_delete_browser_fakeout(self):
+#        response = self.app.post(url('notification', notification_id=1), params=dict(_method='delete'))
+
+    def test_show(self):
+        self.log_user()
+        cur_user = self._get_logged_user()
+        u1 = UserModel().create_or_update(username='u1', password='qweqwe',
+                                               email='u1@rhodecode.org',
+                                               name='u1', lastname='u1')
+        u2 = UserModel().create_or_update(username='u2', password='qweqwe',
+                                               email='u2@rhodecode.org',
+                                               name='u2', lastname='u2')
+
+        notification = NotificationModel().create(created_by=cur_user,
+                                                  subject='test',
+                                                  body='hi there',
+                                                  recipients=[cur_user, u1, u2])
+
+        response = self.app.get(url('notification',
+                                    notification_id=notification.notification_id))
+
+#    def test_show_as_xml(self):
+#        response = self.app.get(url('formatted_notification', notification_id=1, format='xml'))
+#
+#    def test_edit(self):
+#        response = self.app.get(url('edit_notification', notification_id=1))
+#
+#    def test_edit_as_xml(self):
+#        response = self.app.get(url('formatted_edit_notification', notification_id=1, format='xml'))
+
--- a/rhodecode/tests/test_models.py	Tue Nov 22 14:10:33 2011 +0200
+++ b/rhodecode/tests/test_models.py	Wed Nov 23 00:55:05 2011 +0200
@@ -161,28 +161,35 @@
 
 
     def setUp(self):
-        self.u1 = UserModel().create_or_update(username='u1', password='qweqwe',
-                                               email='u1@rhodecode.org',
-                                               name='u1', lastname='u1')
-        self.u2 = UserModel().create_or_update(username='u2', password='qweqwe',
-                                               email='u2@rhodecode.org',
-                                               name='u2', lastname='u3')
-        self.u3 = UserModel().create_or_update(username='u3', password='qweqwe',
-                                               email='u3@rhodecode.org',
-                                               name='u3', lastname='u3')
-
+        self.u1 = UserModel().create_or_update(username=u'u1', password=u'qweqwe',
+                                               email=u'u1@rhodecode.org',
+                                               name=u'u1', lastname=u'u1')
+        self.u2 = UserModel().create_or_update(username=u'u2', password=u'qweqwe',
+                                               email=u'u2@rhodecode.org',
+                                               name=u'u2', lastname=u'u3')
+        self.u3 = UserModel().create_or_update(username=u'u3', password=u'qweqwe',
+                                               email=u'u3@rhodecode.org',
+                                               name=u'u3', lastname=u'u3')
+    def tearDown(self):
+        User.delete(self.u1.user_id)
+        User.delete(self.u2.user_id)
+        User.delete(self.u3.user_id)
 
 
     def test_create_notification(self):
         usrs = [self.u1, self.u2]
         notification = Notification.create(created_by=self.u1,
-                                           subject='subj', body='hi there',
+                                           subject=u'subj', body=u'hi there',
                                            recipients=usrs)
+        Session.commit()
 
-        notifications = Session.query(Notification).all()
+
+        notifications = Notification.query().all()
+        self.assertEqual(len(notifications), 1)
+
         unotification = UserNotification.query()\
             .filter(UserNotification.notification == notification).all()
-        self.assertEqual(len(notifications), 1)
+
         self.assertEqual(notifications[0].recipients, [self.u1, self.u2])
         self.assertEqual(notification.notification_id,
                          notifications[0].notification_id)
@@ -192,21 +199,23 @@
 
     def test_user_notifications(self):
         notification1 = Notification.create(created_by=self.u1,
-                                            subject='subj', body='hi there',
+                                            subject=u'subj', body=u'hi there',
                                             recipients=[self.u3])
         notification2 = Notification.create(created_by=self.u1,
-                                            subject='subj', body='hi there',
+                                            subject=u'subj', body=u'hi there',
                                             recipients=[self.u3])
         self.assertEqual(self.u3.notifications, [notification1, notification2])
 
     def test_delete_notifications(self):
         notification = Notification.create(created_by=self.u1,
-                                           subject='title', body='hi there3',
+                                           subject=u'title', body=u'hi there3',
                                     recipients=[self.u3, self.u1, self.u2])
+        Session.commit()
         notifications = Notification.query().all()
         self.assertTrue(notification in notifications)
 
         Notification.delete(notification.notification_id)
+        Session.commit()
 
         notifications = Notification.query().all()
         self.assertFalse(notification in notifications)
@@ -214,8 +223,3 @@
         un = UserNotification.query().filter(UserNotification.notification
                                              == notification).all()
         self.assertEqual(un, [])
-
-    def tearDown(self):
-        User.delete(self.u1.user_id)
-        User.delete(self.u2.user_id)
-        User.delete(self.u3.user_id)