@@ -264,12 +264,14 @@ def make_map(config):
m.connect("admin_setting", "/settings/{setting_id}",
action="show", conditions=dict(method=["GET"]))
m.connect("formatted_admin_setting", "/settings/{setting_id}.{format}",
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"]))
@@ -44,12 +44,13 @@ from rhodecode.model.db import RhodeCode
RhodeCodeSetting
from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
ApplicationUiSettingsForm
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__)
class SettingsController(BaseController):
"""REST Controller styled on the Atom Publishing Protocol"""
@@ -368,12 +369,20 @@ class SettingsController(BaseController)
log.error(traceback.format_exc())
h.flash(_('error occurred during update of user %s') \
% form_result.get('username'), category='error')
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'),
@HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
def create_repository(self):
"""GET /_admin/create_repository: Form to create a new item"""
c.repo_groups = RepoGroup.groups_choices()
@@ -14,24 +14,27 @@ from rhodecode.lib import str2bool
from rhodecode.lib.auth import AuthUser, get_container_username
from rhodecode.lib.utils import get_repo_slug
from rhodecode.model import meta
from rhodecode import BACKENDS
from rhodecode.model.db import Repository
class BaseController(WSGIController):
def __before__(self):
c.rhodecode_version = __version__
c.rhodecode_name = config.get('rhodecode_title')
c.use_gravatar = str2bool(config.get('use_gravatar'))
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()
self.scm_model = ScmModel(self.sa)
def __call__(self, environ, start_response):
@@ -283,12 +283,14 @@ class User(Base, BaseModel):
repositories = relationship('Repository')
user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
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)
def short_contact(self):
@@ -1108,12 +1110,53 @@ class ChangesetComment(Base, BaseModel):
modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
author = relationship('User')
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")
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'
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}
repository_id = Column('repository_id', String(250), primary_key=True)
repository_path = Column('repository_path', Text)
version = Column('version', Integer)
new file 100644
# -*- 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.user_id == user_id).all()]
@@ -2597,13 +2597,13 @@ table#permissions_manage td.private_repo
table#permissions_manage tr#add_perm_input td {
vertical-align: middle;
}
div.gravatar {
background-color: #FFF;
border: 1px solid #D0D0D0;
border: 0px solid #D0D0D0;
float: left;
margin-right: 0.7em;
padding: 2px 2px 0;
-webkit-border-radius: 6px;
-khtml-border-radius: 6px;
-moz-border-radius: 6px;
@@ -3453,6 +3453,25 @@ form.comment-inline-form {
.inline-comments .comments-number{
padding:0px 0px 10px 0px;
font-weight: bold;
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
## -*- 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 name="page_nav()">
${self.menu('admin')}
<%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
@@ -47,12 +47,15 @@
<div class="account">
%if c.rhodecode_user.username == 'default':
<a href="${h.url('public_journal')}">${_('Public journal')}</a>
${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>
%endif
</li>
<li>
<a href="${h.url('home')}">${_('Home')}</a>
import os
import unittest
from rhodecode.tests import *
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
Session = meta.Session()
class TestReposGroups(unittest.TestCase):
def setUp(self):
self.g1 = self.__make_group('test1', skip_if_exists=True)
self.g2 = self.__make_group('test2', skip_if_exists=True)
@@ -148,6 +153,64 @@ class TestReposGroups(unittest.TestCase)
self.assertTrue(self.__check_path('g2', 'g1'))
# test repo
self.assertEqual(r.repo_name, os.path.join('g2', 'g1', r.just_name))
class TestNotifications(unittest.TestCase):
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',
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)
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)
Status change: