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:
Marcin Lulek 2020-04-09 12:30:13 +02:00
parent 4de6dd537c
commit 780ec5f73e
13 changed files with 5824 additions and 11 deletions

View file

@ -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'

View file

@ -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',

View file

@ -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()

View file

@ -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)

File diff suppressed because it is too large Load diff

View 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

View file

@ -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__ = (

View file

@ -2101,6 +2101,12 @@ BIN_FILENODE = 7
}
}
.notice-messages {
.markdown-block,
.rst-block {
padding: 0;
}
}
.notifications_buttons{
float: right;

View file

@ -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;
}

View file

@ -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 {

View file

@ -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']);

View file

@ -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>

View file

@ -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)