@@ -258,24 +258,26 @@ def make_map(config):
action="delete", conditions=dict(method=["DELETE"]))
m.connect("admin_edit_setting", "/settings/{setting_id}/edit",
action="edit", conditions=dict(method=["GET"]))
m.connect("formatted_admin_edit_setting",
"/settings/{setting_id}.{format}/edit",
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"]))
#ADMIN MAIN PAGES
with rmap.submapper(path_prefix=ADMIN_PREFIX,
controller='admin/admin') as m:
m.connect('admin_home', '', action='index')
m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
action='add_repo')
@@ -38,24 +38,25 @@ from rhodecode.lib.auth import LoginRequ
HasPermissionAnyDecorator, NotAnonymous
from rhodecode.lib.base import BaseController, render
from rhodecode.lib.celerylib import tasks, run_task
from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
set_rhodecode_config, repo_name_slug
from rhodecode.model.db import RhodeCodeUi, Repository, RepoGroup, \
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"""
# To properly map this controller, ensure your config/routing.py
# file has a resource setup:
# map.resource('setting', 'settings', controller='admin/settings',
# path_prefix='/admin', name_prefix='admin_')
@LoginRequired()
@@ -362,24 +363,32 @@ class SettingsController(BaseController)
render('admin/users/user_edit_my_account.html'),
defaults=errors.value,
errors=errors.error_dict or {},
prefix_error=False,
encoding="UTF-8")
except Exception:
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()
c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
new_repo = request.GET.get('repo', '')
c.new_repo = repo_name_slug(new_repo)
return render('admin/repos/repo_add_create_repository.html')
@@ -8,36 +8,39 @@ from pylons import config, tmpl_context
from pylons.controllers import WSGIController
from pylons.controllers.util import redirect
from pylons.templating import render_mako as render
from rhodecode import __version__
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):
"""Invoke the Controller"""
# WSGIController.__call__ dispatches to the Controller method
# the request is routed to. This routing information is
# available in environ['pylons.routes_dict']
start = time.time()
try:
@@ -277,24 +277,26 @@ class User(Base, BaseModel):
ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
user_log = relationship('UserLog', cascade='all')
user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
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):
return '%s %s' % (self.name, self.lastname)
def is_admin(self):
return self.admin
@@ -1102,19 +1104,60 @@ class ChangesetComment(Base, BaseModel):
repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
revision = Column('revision', String(40), nullable=False)
line_no = Column('line_no', Unicode(10), nullable=True)
f_path = Column('f_path', Unicode(1000), nullable=True)
user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
text = Column('text', Unicode(25000), nullable=False)
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()]
@@ -2591,25 +2591,25 @@ table#permissions_manage span.private_re
}
table#permissions_manage td.private_repo_msg {
font-size: 0.8em;
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;
border-radius: 6px;
div.gravatar img {
-webkit-border-radius: 4px;
-khtml-border-radius: 4px;
@@ -3447,12 +3447,31 @@ form.comment-inline-form {
.inline-comments .comment .text {
padding: 8px 6px 6px 14px;
background-color: #FAFAFA;
.inline-comments .comments-number{
padding:0px 0px 10px 0px;
font-weight: bold;
color: #666;
font-size: 16px;
.notifications{
width:22px;
padding:2px;
float:right;
-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
@@ -41,24 +41,27 @@
${h.end_form()}
<div class="gravatar">
<img alt="gravatar" src="${h.gravatar_url(c.rhodecode_user.email,20)}" />
<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>
%if c.rhodecode_user.username != 'default':
<a href="${h.url('journal')}">${_('Journal')}</a>
##(${c.unread_journal}
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)
self.g3 = self.__make_group('test3', skip_if_exists=True)
def tearDown(self):
print 'out'
def __check_path(self, *path):
@@ -142,12 +147,70 @@ class TestReposGroups(unittest.TestCase)
form_data['perms_updates'] = []
RepoModel().update(r.repo_name, form_data)
self.assertEqual(r.repo_name, 'g1/john')
self.__update_group(g1.group_id, 'g1', parent_id=g2.group_id)
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, [])
User.delete(self.u1.user_id)
User.delete(self.u2.user_id)
User.delete(self.u3.user_id)
Status change: