core: added user-notice logic to push notice messages.
- will be used for exception tracker info - new version upgrades - dedicated important messages to the users
This commit is contained in:
parent
4de6dd537c
commit
780ec5f73e
13 changed files with 5824 additions and 11 deletions
|
|
@ -45,7 +45,7 @@ PYRAMID_SETTINGS = {}
|
|||
EXTENSIONS = {}
|
||||
|
||||
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
|
||||
__dbversion__ = 104 # defines current db version for migrations
|
||||
__dbversion__ = 105 # defines current db version for migrations
|
||||
__platform__ = platform.system()
|
||||
__license__ = 'AGPLv3, and Commercial License'
|
||||
__author__ = 'RhodeCode GmbH'
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ def admin_routes(config):
|
|||
"""
|
||||
Admin prefixed routes
|
||||
"""
|
||||
|
||||
config.add_route(
|
||||
name='admin_audit_logs',
|
||||
pattern='/audit_logs')
|
||||
|
|
@ -291,6 +290,12 @@ def admin_routes(config):
|
|||
pattern='/users/{user_id:\d+}/create_repo_group',
|
||||
user_route=True)
|
||||
|
||||
# user notice
|
||||
config.add_route(
|
||||
name='user_notice_dismiss',
|
||||
pattern='/users/{user_id:\d+}/notice_dismiss',
|
||||
user_route=True)
|
||||
|
||||
# user auth tokens
|
||||
config.add_route(
|
||||
name='edit_user_auth_tokens',
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
|
|||
from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
|
||||
from rhodecode.authentication.plugins import auth_rhodecode
|
||||
from rhodecode.events import trigger
|
||||
from rhodecode.model.db import true
|
||||
from rhodecode.model.db import true, UserNotice
|
||||
|
||||
from rhodecode.lib import audit_logger, rc_cache
|
||||
from rhodecode.lib.exceptions import (
|
||||
|
|
@ -701,6 +701,32 @@ class UsersView(UserAppView):
|
|||
|
||||
raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
|
||||
|
||||
@LoginRequired()
|
||||
@HasPermissionAllDecorator('hg.admin')
|
||||
@CSRFRequired()
|
||||
@view_config(
|
||||
route_name='user_notice_dismiss', request_method='POST',
|
||||
renderer='json_ext', xhr=True)
|
||||
def user_notice_dismiss(self):
|
||||
_ = self.request.translate
|
||||
c = self.load_default_context()
|
||||
|
||||
user_id = self.db_user_id
|
||||
c.user = self.db_user
|
||||
user_notice_id = safe_int(self.request.POST.get('notice_id'))
|
||||
notice = UserNotice().query()\
|
||||
.filter(UserNotice.user_id == user_id)\
|
||||
.filter(UserNotice.user_notice_id == user_notice_id)\
|
||||
.scalar()
|
||||
read = False
|
||||
if notice:
|
||||
notice.notice_read = True
|
||||
Session().add(notice)
|
||||
Session().commit()
|
||||
read = True
|
||||
|
||||
return {'notice': user_notice_id, 'read': read}
|
||||
|
||||
@LoginRequired()
|
||||
@HasPermissionAllDecorator('hg.admin')
|
||||
@CSRFRequired()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ authentication and permission libraries
|
|||
"""
|
||||
|
||||
import os
|
||||
|
||||
import colander
|
||||
import time
|
||||
import collections
|
||||
import fnmatch
|
||||
|
|
@ -45,15 +47,14 @@ from rhodecode.model import meta
|
|||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.user import UserModel
|
||||
from rhodecode.model.db import (
|
||||
User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
|
||||
UserIpMap, UserApiKeys, RepoGroup, UserGroup)
|
||||
false, User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
|
||||
UserIpMap, UserApiKeys, RepoGroup, UserGroup, UserNotice)
|
||||
from rhodecode.lib import rc_cache
|
||||
from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5, safe_int, sha1
|
||||
from rhodecode.lib.utils import (
|
||||
get_repo_slug, get_repo_group_slug, get_user_group_slug)
|
||||
from rhodecode.lib.caching_query import FromCache
|
||||
|
||||
|
||||
if rhodecode.is_unix:
|
||||
import bcrypt
|
||||
|
||||
|
|
@ -1455,6 +1456,38 @@ class AuthUser(object):
|
|||
|
||||
return rule, default_perm
|
||||
|
||||
def get_notice_messages(self):
|
||||
|
||||
notice_level = 'notice-error'
|
||||
notice_messages = []
|
||||
if self.is_default:
|
||||
return [], notice_level
|
||||
|
||||
notices = UserNotice.query()\
|
||||
.filter(UserNotice.user_id == self.user_id)\
|
||||
.filter(UserNotice.notice_read == false())\
|
||||
.all()
|
||||
|
||||
try:
|
||||
for entry in notices:
|
||||
|
||||
msg = {
|
||||
'msg_id': entry.user_notice_id,
|
||||
'level': entry.notification_level,
|
||||
'subject': entry.notice_subject,
|
||||
'body': entry.notice_body,
|
||||
}
|
||||
notice_messages.append(msg)
|
||||
|
||||
log.debug('Got user %s %s messages', self, len(notice_messages))
|
||||
|
||||
levels = [x['level'] for x in notice_messages]
|
||||
notice_level = 'notice-error' if 'error' in levels else 'notice-warning'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return notice_messages, notice_level
|
||||
|
||||
def __repr__(self):
|
||||
return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
|
||||
% (self.user_id, self.username, self.ip_addr, self.is_authenticated)
|
||||
|
|
|
|||
5547
rhodecode/lib/dbmigrate/schema/db_4_19_0_0.py
Normal file
5547
rhodecode/lib/dbmigrate/schema/db_4_19_0_0.py
Normal file
File diff suppressed because it is too large
Load diff
35
rhodecode/lib/dbmigrate/versions/105_version_4_19_0.py
Normal file
35
rhodecode/lib/dbmigrate/versions/105_version_4_19_0.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from sqlalchemy import *
|
||||
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
from sqlalchemy import BigInteger
|
||||
|
||||
from rhodecode.lib.dbmigrate.versions import _reset_base
|
||||
from rhodecode.model import init_model_encryption
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
"""
|
||||
Upgrade operations go here.
|
||||
Don't create your own engine; bind migrate_engine to your metadata
|
||||
"""
|
||||
_reset_base(migrate_engine)
|
||||
from rhodecode.lib.dbmigrate.schema import db_4_19_0_0 as db
|
||||
|
||||
init_model_encryption(db)
|
||||
db.UserNotice().__table__.create()
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
|
||||
def fixups(models, _SESSION):
|
||||
pass
|
||||
|
|
@ -4517,6 +4517,65 @@ class UserNotification(Base, BaseModel):
|
|||
Session().add(self)
|
||||
|
||||
|
||||
class UserNotice(Base, BaseModel):
|
||||
__tablename__ = 'user_notices'
|
||||
__table_args__ = (
|
||||
base_table_args
|
||||
)
|
||||
|
||||
NOTIFICATION_TYPE_MESSAGE = 'message'
|
||||
NOTIFICATION_TYPE_NOTICE = 'notice'
|
||||
|
||||
NOTIFICATION_LEVEL_INFO = 'info'
|
||||
NOTIFICATION_LEVEL_WARNING = 'warning'
|
||||
NOTIFICATION_LEVEL_ERROR = 'error'
|
||||
|
||||
user_notice_id = Column('gist_id', Integer(), primary_key=True)
|
||||
|
||||
notice_subject = Column('notice_subject', Unicode(512), nullable=True)
|
||||
notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
|
||||
|
||||
notice_read = Column('notice_read', Boolean, default=False)
|
||||
|
||||
notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
|
||||
notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
|
||||
|
||||
notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
|
||||
notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
|
||||
|
||||
user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
|
||||
user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
|
||||
|
||||
@classmethod
|
||||
def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
|
||||
|
||||
if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
|
||||
cls.NOTIFICATION_LEVEL_WARNING,
|
||||
cls.NOTIFICATION_LEVEL_INFO]:
|
||||
return
|
||||
|
||||
from rhodecode.model.user import UserModel
|
||||
user = UserModel().get_user(user)
|
||||
|
||||
new_notice = UserNotice()
|
||||
if not allow_duplicate:
|
||||
existing_msg = UserNotice().query() \
|
||||
.filter(UserNotice.user == user) \
|
||||
.filter(UserNotice.notice_body == body) \
|
||||
.filter(UserNotice.notice_read == false()) \
|
||||
.scalar()
|
||||
if existing_msg:
|
||||
log.warning('Ignoring duplicate notice for user %s', user)
|
||||
return
|
||||
|
||||
new_notice.user = user
|
||||
new_notice.notice_subject = subject
|
||||
new_notice.notice_body = body
|
||||
new_notice.notification_level = notice_level
|
||||
Session().add(new_notice)
|
||||
Session().commit()
|
||||
|
||||
|
||||
class Gist(Base, BaseModel):
|
||||
__tablename__ = 'gists'
|
||||
__table_args__ = (
|
||||
|
|
|
|||
|
|
@ -2101,6 +2101,12 @@ BIN_FILENODE = 7
|
|||
}
|
||||
}
|
||||
|
||||
.notice-messages {
|
||||
.markdown-block,
|
||||
.rst-block {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications_buttons{
|
||||
float: right;
|
||||
|
|
|
|||
|
|
@ -820,7 +820,53 @@ input {
|
|||
}
|
||||
|
||||
.menulabel-notice {
|
||||
border: 1px solid @color5;
|
||||
|
||||
padding:7px 10px;
|
||||
|
||||
&.notice-warning {
|
||||
border: 1px solid @color3;
|
||||
.notice-color-warning
|
||||
}
|
||||
&.notice-error {
|
||||
border: 1px solid @color5;
|
||||
.notice-color-error
|
||||
}
|
||||
&.notice-info {
|
||||
border: 1px solid @color1;
|
||||
.notice-color-info
|
||||
}
|
||||
}
|
||||
|
||||
.notice-messages-container {
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
}
|
||||
|
||||
.notice-messages {
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 300;
|
||||
min-width: 500px;
|
||||
max-width: 500px;
|
||||
min-height: 100px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding: 8px 0;
|
||||
background-color: #fff;
|
||||
border: 1px solid @grey4;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
.notice-color-warning {
|
||||
color: @color3;
|
||||
}
|
||||
|
||||
.notice-color-error {
|
||||
color: @color5;
|
||||
}
|
||||
|
||||
.notice-color-info {
|
||||
color: @color1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@
|
|||
.icon-expand-linked { cursor: pointer; color: @grey3; font-size: 14px }
|
||||
.icon-more-linked { cursor: pointer; color: @grey3 }
|
||||
.icon-flag-filled-red { color: @color5 !important; }
|
||||
.icon-filled-red { color: @color5 !important; }
|
||||
|
||||
.repo-switcher-dropdown .select2-result-label {
|
||||
.icon-git:before {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ function registerRCRoutes() {
|
|||
pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
|
||||
pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
|
||||
pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
|
||||
pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
|
||||
pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
|
||||
pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
|
||||
pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
|
||||
|
|
|
|||
|
|
@ -688,17 +688,50 @@
|
|||
</%def>
|
||||
|
||||
<%def name="menu_items(active=None)">
|
||||
<%
|
||||
notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
|
||||
notice_display = 'none' if len(notice_messages) == 0 else ''
|
||||
%>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
||||
<ul id="quick" class="main_nav navigation horizontal-list">
|
||||
## notice box for important system messages
|
||||
<li style="display: none">
|
||||
<a class="notice-box" href="#openNotice" onclick="return false">
|
||||
<div class="menulabel-notice" >
|
||||
0
|
||||
<li style="display: ${notice_display}">
|
||||
<a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
|
||||
<div class="menulabel-notice ${notice_level}" >
|
||||
${len(notice_messages)}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<div class="notice-messages-container" style="display: none">
|
||||
<div class="notice-messages">
|
||||
<table class="rctable">
|
||||
% for notice in notice_messages:
|
||||
<tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
|
||||
<td style="vertical-align: text-top; width: 20px">
|
||||
<i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
|
||||
</td>
|
||||
<td>
|
||||
<span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
|
||||
${notice['subject']}
|
||||
|
||||
<div id="notice-${notice['msg_id']}" style="display: none">
|
||||
${h.render(notice['body'], renderer='markdown')}
|
||||
</div>
|
||||
</td>
|
||||
<td style="vertical-align: text-top; width: 35px;">
|
||||
<a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
|
||||
<i class="icon-remove icon-filled-red"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
% endfor
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
## Main filter
|
||||
<li>
|
||||
<div class="menulabel main_filter_box">
|
||||
|
|
@ -1058,6 +1091,26 @@
|
|||
}
|
||||
});
|
||||
|
||||
var dismissNotice = function(noticeId) {
|
||||
|
||||
var url = pyroutes.url('user_notice_dismiss',
|
||||
{"user_id": templateContext.rhodecode_user.user_id});
|
||||
|
||||
var postData = {
|
||||
'csrf_token': CSRF_TOKEN,
|
||||
'notice_id': noticeId,
|
||||
};
|
||||
|
||||
var success = function(response) {
|
||||
$('#notice-message-' + noticeId).remove();
|
||||
return false;
|
||||
};
|
||||
var failure = function(data, textStatus, xhr) {
|
||||
alert("error processing request: " + textStatus);
|
||||
return false;
|
||||
};
|
||||
ajaxPOST(url, postData, success, failure);
|
||||
}
|
||||
</script>
|
||||
<script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
|
||||
</%def>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ if getattr(c, 'repo_group', None):
|
|||
c.template_context['repo_group_name'] = c.repo_group.group_name
|
||||
|
||||
if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
|
||||
c.template_context['rhodecode_user']['user_id'] = c.rhodecode_user.user_id
|
||||
c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
|
||||
c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
|
||||
c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue