changeset 1702:8cb7f5c4d494 beta

#302 - basic notification system, models+tests
author Marcin Kuzminski <marcin@python-works.com>
date Sun, 20 Nov 2011 01:53:00 +0200
parents b702d0d4b030
children f23828b00b21
files rhodecode/config/routing.py rhodecode/controllers/admin/settings.py rhodecode/lib/base.py rhodecode/model/db.py rhodecode/model/notification.py rhodecode/public/css/style.css rhodecode/templates/admin/users/notifications.html rhodecode/templates/base/base.html rhodecode/tests/test_models.py
diffstat 9 files changed, 238 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/rhodecode/config/routing.py	Sat Nov 19 21:37:54 2011 +0200
+++ b/rhodecode/config/routing.py	Sun Nov 20 01:53:00 2011 +0200
@@ -267,6 +267,8 @@
                   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",
--- a/rhodecode/controllers/admin/settings.py	Sat Nov 19 21:37:54 2011 +0200
+++ b/rhodecode/controllers/admin/settings.py	Sun Nov 20 01:53:00 2011 +0200
@@ -47,6 +47,7 @@
 from rhodecode.model.scm import ScmModel
 from rhodecode.model.user import UserModel
 from rhodecode.model.db import User
+from rhodecode.model.notification import NotificationModel
 
 log = logging.getLogger(__name__)
 
@@ -371,6 +372,14 @@
 
         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/lib/base.py	Sat Nov 19 21:37:54 2011 +0200
+++ b/rhodecode/lib/base.py	Sun Nov 20 01:53:00 2011 +0200
@@ -17,6 +17,7 @@
 from rhodecode.model.scm import ScmModel
 from rhodecode import BACKENDS
 from rhodecode.model.db import Repository
+from rhodecode.model.notification import NotificationModel
 
 log = logging.getLogger(__name__)
 
@@ -29,6 +30,8 @@
         c.ga_code = config.get('rhodecode_ga_code')
         c.repo_name = get_repo_slug(request)
         c.backends = BACKENDS.keys()
+        c.unread_notifications = NotificationModel()\
+                        .get_unread_cnt_for_user(c.rhodecode_user.user_id)
         self.cut_off_limit = int(config.get('cut_off_limit'))
 
         self.sa = meta.Session()
--- a/rhodecode/model/db.py	Sat Nov 19 21:37:54 2011 +0200
+++ b/rhodecode/model/db.py	Sun Nov 20 01:53:00 2011 +0200
@@ -286,6 +286,8 @@
 
     group_member = relationship('UsersGroupMember', cascade='all')
 
+    notifications = relationship('Notification', secondary='user_to_notification')
+
     @property
     def full_contact(self):
         return '%s %s <%s>' % (self.name, self.lastname, self.email)
@@ -1111,6 +1113,47 @@
     repo = relationship('Repository')
 
 
+class Notification(Base, BaseModel):
+    __tablename__ = 'notifications'
+    __table_args__ = ({'extend_existing':True})
+    notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
+    subject = Column('subject', Unicode(512), nullable=True)
+    body = Column('body', Unicode(50000), nullable=True)
+    created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
+
+    user_notifications = relationship('UserNotification',
+        primaryjoin = 'Notification.notification_id==UserNotification.notification_id',
+        cascade = "all, delete, delete-orphan")
+
+    @property
+    def recipients(self):
+        return [x.user for x in UserNotification.query()\
+                .filter(UserNotification.notification == self).all()]
+
+    @classmethod
+    def create(cls, subject, body, recipients):
+        notification = cls()
+        notification.subject = subject
+        notification.body = body
+        Session.add(notification)
+        for u in recipients:
+            u.notifications.append(notification)
+        Session.commit()
+        return notification
+
+class UserNotification(Base, BaseModel):
+    __tablename__ = 'user_to_notification'
+    __table_args__ = ({'extend_existing':True})
+    user_to_notification_id = Column("user_to_notification_id", Integer(), nullable=False, unique=True, primary_key=True)
+    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
+    notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), nullable=False)
+    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")
+
+
 class DbMigrateVersion(Base, BaseModel):
     __tablename__ = 'db_migrate_version'
     __table_args__ = {'extend_existing':True}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/model/notification.py	Sun Nov 20 01:53:00 2011 +0200
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+"""
+    rhodecode.model.notification
+    ~~~~~~~~~~~~~~
+
+    Model for notifications
+    
+    
+    :created_on: Nov 20, 2011
+    :author: marcink
+    :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# 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 logging
+import traceback
+
+from pylons.i18n.translation import _
+
+from rhodecode.lib import safe_unicode
+from rhodecode.lib.caching_query import FromCache
+
+from rhodecode.model import BaseModel
+from rhodecode.model.db import Notification, User, UserNotification
+
+
+class NotificationModel(BaseModel):
+
+    def create(self, subject, body, recipients):
+
+        if not getattr(recipients, '__iter__', False):
+            raise Exception('recipients must be a list of iterable')
+
+        for x in recipients:
+            if not isinstance(x, User):
+                raise Exception('recipient is not instance of %s got %s' % \
+                                (User, type(x)))
+
+
+        Notification.create(subject, body, recipients)
+
+
+    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.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.user_id == user_id).all()]
--- a/rhodecode/public/css/style.css	Sat Nov 19 21:37:54 2011 +0200
+++ b/rhodecode/public/css/style.css	Sun Nov 20 01:53:00 2011 +0200
@@ -2600,7 +2600,7 @@
 
 div.gravatar {
 	background-color: #FFF;
-	border: 1px solid #D0D0D0;
+	border: 0px solid #D0D0D0;
 	float: left;
 	margin-right: 0.7em;
 	padding: 2px 2px 0;
@@ -3456,3 +3456,22 @@
     color: #666;
     font-size: 16px;
 }
+.notifications{
+	width:22px;
+    padding:2px;
+	float:right;
+    -webkit-border-radius: 4px;
+    -moz-border-radius: 4px;
+    border-radius: 4px;
+    text-align: center;
+    margin: -1px -10px 0px 5px;
+    background-color: #DEDEDE;
+}
+.notifications a{
+	color:#888 !important;
+	display: block;
+	font-size: 10px
+}
+.notifications a:hover{
+	text-decoration: none !important;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/templates/admin/users/notifications.html	Sun Nov 20 01:53:00 2011 +0200
@@ -0,0 +1,28 @@
+## -*- 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()}       
+    </div>
+    % for notification in c.notifications:
+        ${notification.title}
+    %else:
+        <div class="table">${_('No notifications here yet')}</div>
+    %endfor
+</div>    
+</%def>  
--- a/rhodecode/templates/base/base.html	Sat Nov 19 21:37:54 2011 +0200
+++ b/rhodecode/templates/base/base.html	Sun Nov 20 01:53:00 2011 +0200
@@ -50,6 +50,9 @@
               <a href="${h.url('public_journal')}">${_('Public journal')}</a>   
           %else:                        		            
           	${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 class="notifications">
+            <a href="${h.url('admin_settings_notifications')}">${c.unread_notifications}</a>
+            </div>
           %endif
           </div>	
          </li>
--- a/rhodecode/tests/test_models.py	Sat Nov 19 21:37:54 2011 +0200
+++ b/rhodecode/tests/test_models.py	Sun Nov 20 01:53:00 2011 +0200
@@ -4,8 +4,13 @@
 
 from rhodecode.model.repos_group import ReposGroupModel
 from rhodecode.model.repo import RepoModel
-from rhodecode.model.db import RepoGroup, User
+from rhodecode.model.db import RepoGroup, User, Notification, UserNotification
 from sqlalchemy.exc import IntegrityError
+from rhodecode.model.user import UserModel
+
+from rhodecode.model import meta
+
+Session = meta.Session()
 
 class TestReposGroups(unittest.TestCase):
 
@@ -151,3 +156,61 @@
         self.assertEqual(r.repo_name, os.path.join('g2', 'g1', r.just_name))
 
 
+class TestNotifications(unittest.TestCase):
+
+
+
+    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')
+
+
+
+    def test_create_notification(self):
+        usrs = [self.u1, self.u2]
+        notification = Notification.create(subject='subj', body='hi there',
+                            recipients=usrs)
+
+        notifications = Session.query(Notification).all()
+        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, notifications[0])
+        self.assertEqual(len(unotification), len(usrs))
+        self.assertEqual([x.user.user_id for x in unotification],
+                         [x.user_id for x in usrs])
+
+    def test_user_notifications(self):
+        notification1 = Notification.create(subject='subj', body='hi there',
+                            recipients=[self.u3])
+        notification2 = Notification.create(subject='subj', body='hi there',
+                            recipients=[self.u3])
+        self.assertEqual(self.u3.notifications, [notification1, notification2])
+
+    def test_delete_notifications(self):
+        notification = Notification.create(subject='title', body='hi there3',
+                            recipients=[self.u3, self.u1, self.u2])
+        notifications = Notification.query().all()
+        self.assertTrue(notification in notifications)
+
+        Notification.delete(notification.notification_id)
+
+        notifications = Notification.query().all()
+        self.assertFalse(notification in notifications)
+
+        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)