aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcin Kuzminski <marcin@python-works.com>2011-11-23 00:55:05 +0200
committerMarcin Kuzminski <marcin@python-works.com>2011-11-23 00:55:05 +0200
commit316164a9dde9a32b4d66fc537e98a9d2339cd95f (patch)
treefdb0df88d80244236b8105fd2fed21e583c5865a
parentfc5c80f0d429d09d266e123507cc0afbaa3ff7a3 (diff)
Notification system improvements
- deleting - tests - ui - moved to separate controller --HG-- branch : beta rename : rhodecode/templates/admin/users/notifications.html => rhodecode/templates/admin/notifications/notifications.html
-rw-r--r--rhodecode/config/routing.py37
-rw-r--r--rhodecode/controllers/admin/notifications.py84
-rw-r--r--rhodecode/controllers/admin/settings.py8
-rw-r--r--rhodecode/controllers/changeset.py20
-rw-r--r--rhodecode/model/comment.py38
-rwxr-xr-xrhodecode/model/db.py42
-rw-r--r--rhodecode/model/meta.py3
-rw-r--r--rhodecode/model/notification.py87
-rw-r--r--rhodecode/public/css/style.css28
-rw-r--r--rhodecode/public/js/rhodecode.js16
-rw-r--r--rhodecode/templates/admin/notifications/notifications.html60
-rw-r--r--rhodecode/templates/admin/notifications/show_notification.html51
-rw-r--r--rhodecode/templates/admin/users/notifications.html38
-rw-r--r--rhodecode/templates/base/base.html2
-rw-r--r--rhodecode/tests/__init__.py11
-rw-r--r--rhodecode/tests/functional/test_admin_notifications.py117
-rw-r--r--rhodecode/tests/test_models.py46
17 files changed, 555 insertions, 133 deletions
diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py
index ee4e793f..fb5ff875 100644
--- a/rhodecode/config/routing.py
+++ b/rhodecode/config/routing.py
@@ -62,8 +62,8 @@ def make_map(config):
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 @@ def make_map(config):
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 @@ def make_map(config):
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}',
diff --git a/rhodecode/controllers/admin/notifications.py b/rhodecode/controllers/admin/notifications.py
new file mode 100644
index 00000000..36a66898
--- /dev/null
+++ b/rhodecode/controllers/admin/notifications.py
@@ -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)
diff --git a/rhodecode/controllers/admin/settings.py b/rhodecode/controllers/admin/settings.py
index f97d7e72..2dbe7e9c 100644
--- a/rhodecode/controllers/admin/settings.py
+++ b/rhodecode/controllers/admin/settings.py
@@ -372,14 +372,6 @@ class SettingsController(BaseController):
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):
diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py
index b4ab4e30..fbc62a2a 100644
--- a/rhodecode/controllers/changeset.py
+++ b/rhodecode/controllers/changeset.py
@@ -32,8 +32,7 @@ from pylons.controllers.util import redirect
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 @@ class ChangesetController(BaseRepoController):
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 @@ class ChangesetController(BaseRepoController):
@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
diff --git a/rhodecode/model/comment.py b/rhodecode/model/comment.py
index 4fe5fde1..efba4693 100644
--- a/rhodecode/model/comment.py
+++ b/rhodecode/model/comment.py
@@ -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 rhodecode.model import BaseModel
-from rhodecode.model.db import ChangesetComment, User, Notification
+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, Repository, Notification
from rhodecode.model.notification import NotificationModel
log = logging.getLogger(__name__)
@@ -38,6 +41,15 @@ log = logging.getLogger(__name__)
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 @@ class ChangesetCommentsModel(BaseModel):
: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 @@ class ChangesetCommentsModel(BaseModel):
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):
diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py
index 7667a136..53f9f5a7 100755
--- a/rhodecode/model/db.py
+++ b/rhodecode/model/db.py
@@ -49,7 +49,6 @@ from rhodecode.lib.caching_query import FromCache
from rhodecode.model.meta import Base, Session
-
log = logging.getLogger(__name__)
#==============================================================================
@@ -286,7 +285,9 @@ class User(Base, BaseModel):
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 @@ class User(Base, BaseModel):
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 @@ class User(Base, BaseModel):
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 @@ class Notification(Base, BaseModel):
__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 @@ class Notification(Base, BaseModel):
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 @@ class Notification(Base, BaseModel):
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 @@ class UserNotification(Base, BaseModel):
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'
diff --git a/rhodecode/model/meta.py b/rhodecode/model/meta.py
index 8f6eb6dc..ab44ba04 100644
--- a/rhodecode/model/meta.py
+++ b/rhodecode/model/meta.py
@@ -15,7 +15,8 @@ __all__ = ['Base', 'Session']
#
Session = scoped_session(
sessionmaker(
- query_cls=caching_query.query_callable(cache_manager)
+ query_cls = caching_query.query_callable(cache_manager),
+ expire_on_commit = True,
)
)
diff --git a/rhodecode/model/notification.py b/rhodecode/model/notification.py
index 3f11e149..c9d38960 100644
--- a/rhodecode/model/notification.py
+++ b/rhodecode/model/notification.py
@@ -29,15 +29,38 @@ import traceback
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 @@ class NotificationModel(BaseModel):
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
diff --git a/rhodecode/public/css/style.css b/rhodecode/public/css/style.css
index e180e515..58bf9e22 100644
--- a/rhodecode/public/css/style.css
+++ b/rhodecode/public/css/style.css
@@ -2603,7 +2603,8 @@ div.gravatar {
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 @@ form.comment-inline-form {
}
.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
diff --git a/rhodecode/public/js/rhodecode.js b/rhodecode/public/js/rhodecode.js
index 32412379..c23acd4f 100644
--- a/rhodecode/public/js/rhodecode.js
+++ b/rhodecode/public/js/rhodecode.js
@@ -563,3 +563,19 @@ var getSelectionLink = function(selection_link_label) {
}
}
};
+
+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);
+};
diff --git a/rhodecode/templates/admin/notifications/notifications.html b/rhodecode/templates/admin/notifications/notifications.html
new file mode 100644
index 00000000..6a1e8812
--- /dev/null
+++ b/rhodecode/templates/admin/notifications/notifications.html
@@ -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>
diff --git a/rhodecode/templates/admin/notifications/show_notification.html b/rhodecode/templates/admin/notifications/show_notification.html
new file mode 100644
index 00000000..368f0745
--- /dev/null
+++ b/rhodecode/templates/admin/notifications/show_notification.html
@@ -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>
diff --git a/rhodecode/templates/admin/users/notifications.html b/rhodecode/templates/admin/users/notifications.html
deleted file mode 100644
index 0de979b3..00000000
--- a/rhodecode/templates/admin/users/notifications.html
+++ /dev/null
@@ -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>
diff --git a/rhodecode/templates/base/base.html b/rhodecode/templates/base/base.html
index 244035b0..22717885 100644
--- a/rhodecode/templates/base/base.html
+++ b/rhodecode/templates/base/base.html
@@ -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>
diff --git a/rhodecode/tests/__init__.py b/rhodecode/tests/__init__.py
index cb892fc1..dcea5c7d 100644
--- a/rhodecode/tests/__init__.py
+++ b/rhodecode/tests/__init__.py
@@ -9,6 +9,7 @@ setup-app`) and provides the base testing objects.
"""
import os
import time
+import logging
from os.path import join as jn
from unittest import TestCase
@@ -20,7 +21,8 @@ from routes.util import URLGenerator
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 @@ class TestController(TestCase):
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 @@ class TestController(TestCase):
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])
diff --git a/rhodecode/tests/functional/test_admin_notifications.py b/rhodecode/tests/functional/test_admin_notifications.py
new file mode 100644
index 00000000..32b78650
--- /dev/null
+++ b/rhodecode/tests/functional/test_admin_notifications.py
@@ -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'))
+
diff --git a/rhodecode/tests/test_models.py b/rhodecode/tests/test_models.py
index e7945ba4..3d71b077 100644
--- a/rhodecode/tests/test_models.py
+++ b/rhodecode/tests/test_models.py
@@ -161,28 +161,35 @@ 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')
-
+ 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 = Notification.query().all()
+ self.assertEqual(len(notifications), 1)
- 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.notification_id,
notifications[0].notification_id)
@@ -192,21 +199,23 @@ class TestNotifications(unittest.TestCase):
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 @@ class TestNotifications(unittest.TestCase):
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)