Add user notification

This commit is contained in:
Nicolas Évrard
2025-12-02 13:08:54 +01:00
parent 4dce4ec167
commit 48dcd6ffdd
23 changed files with 773 additions and 26 deletions
+1
View File
@@ -1,3 +1,4 @@
* Manage user notification
* Add search on empty relation field to domain parser
* Escape completion content with custom format (issue14363)
* Use sandboxed iframe to display document (issue14290)
+1
View File
@@ -24,6 +24,7 @@ module.exports = function(grunt) {
'src/board.js',
'src/bus.js',
'src/chat.js',
'src/notification.js',
'src/plugins.js',
'src/html_sanitizer.js'
];
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M160-200v-80h80v-280q0-83 50-147.5T420-792v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v28q80 20 130 84.5T720-560v280h80v80H160Zm320-300Zm0 420q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80ZM320-280h320v-280q0-66-47-113t-113-47q-66 0-113 47t-47 113v280Z"/></svg>

After

Width:  |  Height:  |  Size: 387 B

+5 -4
View File
@@ -39,8 +39,8 @@ this repository contains the full copyright notices and license terms. -->
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse" data-target="#main_navbar">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#main_navbar">
<span class="notification-badge"></span>
<span class="caret"></span>
</button>
<a class="navbar-brand" href="javascript:void(0)" data-toggle="menu">
@@ -57,7 +57,7 @@ this repository contains the full copyright notices and license terms. -->
<form class="navbar-form navbar-left flip" role="search" id="global-search" style="border-style: none;">
</form>
<ul class="nav navbar-nav navbar-right flip">
<li id="user-preferences"></li>
<li id="user-preferences" class="dropdown"></li>
<li id="user-logout">
<a href="#">
<span class="icon hidden-xs">
@@ -92,6 +92,7 @@ this repository contains the full copyright notices and license terms. -->
jQuery('[data-toggle="menu"]').click(function() {
jQuery('#menu').toggleClass('hidden');
jQuery('#tabs').toggleClass('hidden-xs');
jQuery('#main_navbar').collapse('hide');
});
jQuery('#tabs').on('ready', function() {
var mq = window.matchMedia('(max-width: 991px)');
@@ -105,7 +106,7 @@ this repository contains the full copyright notices and license terms. -->
}
}
});
jQuery('#main_navbar').on('click', 'a', function() {
jQuery('#main_navbar').on('click', 'a:not(.dropdown-toggle)', function() {
jQuery('#main_navbar').collapse('hide');
});
});
+1
View File
@@ -3154,6 +3154,7 @@
'tryton-log',
'tryton-menu',
'tryton-note',
'tryton-notification',
'tryton-ok',
'tryton-open',
'tryton-print',
+197
View File
@@ -0,0 +1,197 @@
/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
class _NotificationMenu {
constructor() {
this.el = jQuery('<ul/>', {
'class': 'notification-menu dropdown-menu',
'role': 'menu',
});
this.el.on('show.bs.dropdown', () => {
this.fill();
this.indicator.hide();
});
this.indicator = jQuery('<span/>', {
'class': 'notification-badge',
});
let indicator_observer = new MutationObserver(() => {
let indicators = jQuery('.notification-badge').not(this.indicator);
indicators.text(this.indicator.text())
indicators.toggle(this.indicator.css('display') !== 'none');
});
indicator_observer.observe(
this.indicator.get(0), {
characterData: true,
attributes: true,
attributeFilter: ['style'],
});
this.indicator.hide();
}
fill() {
let Notification = new Sao.Model('res.notification');
Notification.execute('get', []).done((notifications) => {
this.el.empty();
for (let notification of notifications) {
let notification_item = jQuery('<div/>', {
}).append(jQuery('<span/>', {
'class': 'notification-label',
'text': notification.label,
})).append(jQuery('<span/>', {
'class': 'notification-description',
'text': notification.description
}));
let link = jQuery('<a/>', {
'role': 'menuitem',
'href': '#',
}).click((evt) => {
evt.preventDefault();
this.open(notification)
}).append(notification_item);
let li = jQuery('<li/>', {
'class': 'notification-item',
'role': 'presentation',
});
let img = jQuery('<img/>', {
'class': 'icon',
});
link.prepend(img);
Sao.common.ICONFACTORY.get_icon_url(
notification.icon || 'tryton-notification')
.then(url => {
img.attr('src', url);
// Append only when the url is known to prevent
// layout shifts
li.append(link);
});
if (notification.unread) {
li.addClass('notification-unread');
}
this.el.append(li)
}
this.el.append(
jQuery('<li/>', {
'role': 'presentation',
'class': 'notification-item notification-action',
}).append(jQuery('<a/>', {
'role': 'menuitem',
'href': '#',
'title': Sao.i18n.gettext("All Notifications..."),
}).append(jQuery('<span/>', {
'class': 'caret',
})).click((evt) => {
evt.preventDefault();
let params = {
context: jQuery.extend({}, Sao.Session.current_session.context),
domain: [['user', '=', Sao.Session.current_session.user_id]],
};
params.model = 'res.notification';
Sao.Tab.create(params).done(() => {
this.indicator.hide();
});
}))
);
if (notifications.length > 0) {
this.el.append(
jQuery('<li/>', {
'role': 'separator',
'class': 'divider',
}));
}
let preferences_img = jQuery('<img/>', {
'class': 'icon',
});
let preferences = jQuery('<li/>', {
'class': 'notification-item',
'role': 'presentation',
}).append(
jQuery('<a/>', {
'role': 'menuitem',
'href': '#',
'text': Sao.i18n.gettext("Preferences..."),
}).prepend(preferences_img
).click((evt) => {
evt.preventDefault();
Sao.preferences();
}));
Sao.common.ICONFACTORY.get_icon_url('tryton-launch')
.then(url => {
preferences_img.attr('src', url);
});
this.el.append(preferences);
});
}
open(notification) {
let prms = [];
if (notification.model && notification.records) {
let params = {
context: jQuery.extend({}, Sao.Session.current_session.context),
domain: [['id', 'in', notification.records]],
};
if (notification.records.length == 1) {
params['res_id'] = notification.records[0];
params['mode'] = ['form', 'tree'];
}
params.model = notification.model;
prms.push(Sao.Tab.create(params));
}
if (notification.action) {
prms.push(Sao.Action.execute(notification.action));
}
jQuery.when.apply(jQuery, prms).done(() => {
if (notification.unread) {
let Notification = new Sao.Model('res.notification');
Notification.execute('mark_read', [[notification.id]]);
}
});
}
_update(count) {
if (count > 0) {
if (count < 10) {
this.indicator.text(count);
} else {
// Let's keep the text short
this.indicator.text('9+');
}
this.indicator.show();
} else {
this.indicator.hide();
}
}
notify(message) {
if (message.type == 'user-notification') {
this._update(message.count);
try {
if (Notification.permission == "granted") {
message.content.forEach((body) => {
new Notification(
Sao.config.title, {
'body': body,
});
});
}
} catch (e) {
Sao.Logger.error(e.message, e.stack);
}
}
}
count() {
let Notification = new Sao.Model('res.notification');
Notification.execute('get_count', [])
.done((count) => {
this._update(count);
});
}
}
Sao.NotificationMenu = new _NotificationMenu();
}());
+25 -6
View File
@@ -611,7 +611,14 @@ var Sao = {
Sao.menu(preferences);
Sao.user_menu(preferences);
Sao.open_url(url);
let user_id = Sao.Session.current_session.user_id;
Sao.Bus.register(
`notification:${user_id}`,
Sao.NotificationMenu.notify
.bind(Sao.NotificationMenu));
Sao.NotificationMenu.count();
Sao.Bus.listen();
});
}
}, function() {
@@ -633,6 +640,9 @@ var Sao = {
jQuery('#user-preferences').empty();
jQuery('#global-search').empty();
jQuery('#menu').empty();
let user_id = Sao.Session.current_session.user_id;
Sao.Bus.unregister(
`notification:${user_id}`, Sao.NotificationMenu.update);
session.do_logout().always(Sao.login);
Sao.set_title();
});
@@ -702,16 +712,24 @@ var Sao = {
};
Sao.user_menu = function(preferences) {
jQuery('#user-preferences').empty();
let user_preferences = jQuery('#user-preferences');
user_preferences.empty();
var user = jQuery('<a/>', {
'href': '#',
'class': 'dropdown-toggle',
'data-toggle': 'dropdown',
'role': 'button',
'aria-expanded': false,
'aria-haspopup': true,
'title': preferences.status_bar,
}).click(function(evt) {
evt.preventDefault();
user.prop('disabled', true);
Sao.preferences().then(() => user.prop('disabled', false));
}).text(preferences.status_bar);
jQuery('#user-preferences').append(user);
user_preferences
.off('show.bs.dropdown')
.on('show.bs.dropdown', () => {
Sao.NotificationMenu.fill();
Sao.NotificationMenu.indicator.hide();
})
.append(user).append(Sao.NotificationMenu.el);
if (preferences.avatar_badge_url) {
user.prepend(jQuery('<img/>', {
'src': preferences.avatar_badge_url + '?s=15',
@@ -724,6 +742,7 @@ var Sao = {
'class': 'img-circle',
}));
}
user.prepend(Sao.NotificationMenu.indicator);
var title = Sao.i18n.gettext("Logout");
jQuery('#user-logout > a')
.attr('title', title)
+88 -13
View File
@@ -414,24 +414,99 @@ html[theme="default"] {
}
}
#user-preferences > a {
max-width: 30em;
overflow: hidden;
text-overflow: ellipsis;
.notification-badge {
z-index: 100;
position: absolute;
inset-block-start: 5px;
inset-inline-start: 5px;
color: @navbar-default-link-hover-color;
background-color: @brand-info;
min-width: 20px;
min-height: 20px;
padding: 0.2em;
border-radius: 10px;
white-space: nowrap;
text-align: center;
font-size: smaller;
> img {
margin: -5px 5px;
width: 30px;
.navbar-toggle > & {
inset-block-start: -5px;
inset-inline-start: -15px;
}
}
&[src=''] {
width: 0;
#user-preferences {
> a {
max-width: 30em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
> img {
margin: -5px 5px;
width: 30px;
&[src=''] {
width: 0;
}
&.img-badge {
margin-left: -15px;
margin-bottom: -25px;
width: 15px;
}
}
}
ul.notification-menu {
@media (min-width: @grid-float-breakpoint) {
width: 320px;
}
&.img-badge {
margin-left: -15px;
margin-bottom: -25px;
width: 15px;
> li.notification-item {
padding: 2px 0;
&.notification-unread {
background-color: @state-selected-bg;
}
> a {
display: flex;
padding: 3px 10px;
img.icon {
padding-inline-end: 3px;
}
> div {
display: flex;
flex-direction: column;
overflow: hidden;
span {
overflow: hidden;
text-overflow: ellipsis;
}
span.notification-label {
font-weight: bold;
}
span.notification-description {
}
}
}
&.notification-action {
margin: 0.3em 0;
> a {
display: block;
text-align: center;
background-color: @dropdown-link-hover-bg;
}
}
}
}
}
+1
View File
@@ -1,3 +1,4 @@
* Manage user notification
* Add search on empty relation field to domain parser
* Add support for multiple button in the tree view
+1 -1
View File
@@ -37,7 +37,7 @@ class Action(object):
callback=callback)
@staticmethod
def execute(action, data, context=None, keyword=False):
def execute(action, data=None, context=None, keyword=False):
if isinstance(action, int):
# Must be executed synchronously to avoid double execution
# on double click.
+8
View File
@@ -48,6 +48,14 @@ def main():
font-size: medium;
font-weight: normal;
}
.unread-notification {
color: @theme_selected_fg_color;
background-color: lighter(@theme_selected_bg_color);
}
.unread-notification:hover {
color: @theme_selected_fg_color;
background-color: @theme_selected_bg_color;
}
"""
screen = Gdk.Screen.get_default()
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M160-200v-80h80v-280q0-83 50-147.5T420-792v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v28q80 20 130 84.5T720-560v280h80v80H160Zm320-300Zm0 420q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80ZM320-280h320v-280q0-66-47-113t-113-47q-66 0-113 47t-47 113v280Z"/></svg>

After

Width:  |  Height:  |  Size: 387 B

+8
View File
@@ -19,11 +19,13 @@ import tryton.plugins
import tryton.rpc as rpc
import tryton.translate as translate
from tryton.action import Action
from tryton.bus import Bus
from tryton.common import RPCContextReload, RPCException, RPCExecute
from tryton.common.cellrendererclickablepixbuf import (
CellRendererClickablePixbuf)
from tryton.config import CONFIG, TRYTON_ICON, get_config_dir
from tryton.exceptions import TrytonError, TrytonServerUnavailable
from tryton.gui.notification import NotificationMenu
from tryton.gui.window import Window
from tryton.jsonrpc import object_hook
from tryton.pyson import PYSONDecoder
@@ -164,6 +166,9 @@ class Main(Gtk.Application):
self.set_global_search()
self.header.pack_start(self.global_search_entry)
self.notification_menu = NotificationMenu(self)
self.header.pack_end(self.notification_menu.button)
self.accel_group = Gtk.AccelGroup()
self.window.add_accel_group(self.accel_group)
@@ -263,6 +268,9 @@ class Main(Gtk.Application):
msg_type=Gtk.MessageType.ERROR)
return self.quit()
self.get_preferences()
Bus.register(
f'notification:{rpc._USER}', self.notification_menu.notify)
self.notification_menu.count()
def do_command_line(self, cmd):
self.do_activate()
+124
View File
@@ -0,0 +1,124 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import gettext
from gi.repository import Gtk, Pango
import tryton.common as common
import tryton.rpc as rpc
from tryton.action import Action
from tryton.config import CONFIG
from tryton.gui.window import Window
_ = gettext.gettext
class NotificationMenu:
def __init__(self, app):
self.app = app
self.button = Gtk.MenuButton()
img = common.IconFactory.get_image(
'tryton-notification', Gtk.IconSize.BUTTON)
self.button.set_image(img)
self.menu = Gtk.Menu()
self.menu.connect('show', self.fill)
self.button.set_popup(self.menu)
def fill(self, menu):
for child in menu.get_children():
menu.remove(child)
notifications = rpc.execute(
'model', 'res.notification', 'get', rpc.CONTEXT)
for notification in notifications:
item = Gtk.MenuItem()
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
hbox.set_margin_top(4)
hbox.set_margin_bottom(4)
hbox.set_margin_start(6)
hbox.set_margin_end(6)
img = common.IconFactory.get_image(
notification['icon'] or 'tryton-notification',
Gtk.IconSize.MENU)
hbox.pack_start(img, False, False, 0)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
label = Gtk.Label(notification['label'])
label.set_xalign(0)
vbox.pack_start(label, False, False, 0)
description = Gtk.Label(notification['description'])
description.set_xalign(0)
description.get_style_context().add_class("dim-label")
description.set_max_width_chars(30)
description.set_line_wrap(True)
description.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
description.set_ellipsize(Pango.EllipsizeMode.END)
description.set_lines(2)
vbox.pack_start(description, False, False, 0)
hbox.pack_start(vbox, True, True, 0)
item.add(hbox)
item.connect('activate', self.open, notification)
if notification['unread']:
style = item.get_style_context()
style.add_class('unread-notification')
menu.append(item)
if notifications:
menu.append(Gtk.SeparatorMenuItem())
def open_all_notifications(item):
params = {
'domain': [['user', '=', rpc._USER]],
}
Window.create('res.notification', **params)
all_notifications = Gtk.MenuItem(label=_("All Notifications..."))
all_notifications.connect('activate', open_all_notifications)
menu.append(all_notifications)
menu.show_all()
self._update(0)
def open(self, menuitem, notification):
if notification.get('model') and notification.get('records'):
params = {
'domain': [['id', 'in', notification['records']]],
}
if len(notification['records']) == 1:
params['res_id'] = notification['records'][0]
params['mode'] = ['form', 'tree']
Window.create(notification['model'], **params)
if notification.get('action'):
Action.execute(notification['action'])
if notification['unread']:
rpc.execute(
'model', 'res.notification', 'mark_read',
[notification['id']], rpc.CONTEXT)
def _update(self, count):
img = common.IconFactory.get_image(
'tryton-notification', Gtk.IconSize.BUTTON,
badge=2 if count else None)
self.button.set_image(img)
def notify(self, message):
if message['type'] == 'user-notification':
self._update(message['count'])
for msg in message['content']:
self.app.show_notification(CONFIG['client.title'], msg)
def count(self):
count = rpc.execute(
'model', 'res.notification', 'get_count', rpc.CONTEXT)
self._update(count)
+1
View File
@@ -1,3 +1,4 @@
* Add user notification
* Add support for materializing ModelSQL based on a table query
* Add an option to trytond-console to run a script file
* Store only immutable structure in MemoryCache
+1
View File
@@ -60,6 +60,7 @@ CLIENT_ICONS = [(x, x) for x in [
'tryton-log',
'tryton-menu',
'tryton-note',
'tryton-notification',
'tryton-ok',
'tryton-open',
'tryton-print',
+10 -2
View File
@@ -11,13 +11,21 @@ class Rule(metaclass=PoolMeta):
@classmethod
def _get_context(cls, model_name):
context = super()._get_context(model_name)
if model_name in {'res.user.warning', 'res.user.application'}:
if model_name in {
'res.user.warning',
'res.user.application',
'res.notification',
}:
context['user_id'] = Transaction().user
return context
@classmethod
def _get_cache_key(cls, model_names):
key = super()._get_cache_key(model_names)
if model_names & {'res.user.warning', 'res.user.application'}:
if model_names & {
'res.user.warning',
'res.user.application',
'res.notification',
}:
key = (*key, Transaction().user)
return key
+161
View File
@@ -0,0 +1,161 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import json
from collections import defaultdict
from sql import Literal
from sql.aggregate import Count
from trytond.bus import Bus
from trytond.ir.ui.menu import CLIENT_ICONS
from trytond.model import Index, ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.pyson import Bool, Eval
from trytond.rpc import RPC
from trytond.transaction import Transaction
class Notification(
fields.fmany2one(
'model_ref', 'model', 'ir.model,name', "Model",
ondelete='CASCADE'),
ModelSQL, ModelView):
__name__ = 'res.notification'
user = fields.Many2One(
'res.user', "User", required=True, ondelete='CASCADE',
states={
'readonly': Eval('id', 0) > 0,
})
label = fields.Char("Label")
description = fields.Char("Description")
icon = fields.Selection('list_icons', 'Icon', translate=False)
unread = fields.Boolean("Unread")
model = fields.Char(
"Model",
states={
'required': Bool(Eval('records')),
})
records = fields.Char(
"Records",
states={
'required': Bool(Eval('model')),
})
action = fields.Many2One(
'ir.action', "Action", ondelete='CASCADE',
states={
'required': Bool(Eval('action_value')),
})
action_value = fields.Char("Action Value")
@classmethod
def __setup__(cls):
super().__setup__()
cls.__rpc__.update({
'get': RPC(),
'get_count': RPC(),
'mark_read': RPC(
readonly=False, instantiate=0, check_access=False),
})
table = cls.__table__()
cls._sql_indexes.update({
Index(
table,
(table.user, Index.Equality()),
where=table.unread),
Index(table, (table.user, Index.Equality())),
})
@classmethod
def default_unread(cls):
return True
@classmethod
def copy(cls, notifications, default=None):
default = default.copy() if default is not None else {}
default.setdefault('unread', True)
return super().copy(notifications, default=default)
@classmethod
def create(cls, vlist):
notifications = super().create(vlist)
notifications_by_user = defaultdict(list)
for notification in notifications:
notifications_by_user[notification.user.id].append(
notification)
notification = cls.__table__()
cursor = Transaction().connection.cursor()
cursor.execute(*notification.select(
notification.user, Count(Literal('*')),
where=((notification.user.in_(list(notifications_by_user)))
& notification.unread),
group_by=[notification.user]))
for user, count in cursor.fetchall():
messages = [
'\n'.join(filter(None, (n.label, n.description)))
for n in notifications_by_user[user]]
Bus.publish(
f'notification:{user}', {
'type': 'user-notification',
'count': count,
'content': messages,
})
return notifications
@classmethod
def list_icons(cls):
pool = Pool()
Icon = pool.get('ir.ui.icon')
return sorted(CLIENT_ICONS
+ [(name, name) for _, name in Icon.list_icons()]
+ [(None, "")],
key=lambda e: e[1])
@property
def _action_value(self):
if self.action_value:
action_value = self.action.get_action_value()
action_value.update(json.loads(self.action_value))
return action_value
elif self.action:
return self.action.id
@classmethod
def get(cls, count=10):
"Get the last count notifications"
notifications = cls.search([
('user', '=', Transaction().user),
],
limit=count, order=[('create_date', 'DESC'), ('id', 'DESC')])
return [{
'id': n.id,
'label': n.label,
'description': n.description,
'icon': n.icon,
'model': n.model,
'records': json.loads(n.records) if n.records else None,
'action': n._action_value,
'unread': bool(n.unread),
} for n in notifications]
@classmethod
def get_count(cls):
notification = cls.__table__()
cursor = Transaction().connection.cursor()
cursor.execute(*notification.select(
Count(Literal('*')),
where=((notification.user == Transaction().user)
& notification.unread)))
return cursor.fetchone()[0]
@classmethod
def mark_read(cls, notifications):
current_user = Transaction().user
notifications = [n for n in notifications if n.user.id == current_user]
cls.write(notifications, {'unread': False})
+85
View File
@@ -0,0 +1,85 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="notification_view_list">
<field name="model">res.notification</field>
<field name="type">tree</field>
<field name="name">notification_list</field>
</record>
<record model="ir.ui.view" id="notification_view_form">
<field name="model">res.notification</field>
<field name="type">form</field>
<field name="priority" eval="10"/>
<field name="name">notification_form</field>
</record>
<record model="ir.ui.view" id="notification_view_form_admin">
<field name="model">res.notification</field>
<field name="type">form</field>
<field name="priority" eval="20"/>
<field name="name">notification_form_admin</field>
</record>
<record model="ir.action.act_window" id="act_notification">
<field name="name">Notifications</field>
<field name="res_model">res.notification</field>
</record>
<record model="ir.action.act_window.view" id="act_notification_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="notification_view_list"/>
<field name="act_window" ref="act_notification"/>
</record>
<record model="ir.action.act_window.view" id="act_notification_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="notification_view_form_admin"/>
<field name="act_window" ref="act_notification"/>
</record>
<menuitem parent="res.menu_res" action="act_notification" id="menu_notification_form"/>
<record model="ir.model.access" id="access_notification">
<field name="model">res.notification</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_notification_admin">
<field name="model">res.notification</field>
<field name="group" ref="group_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.rule.group" id="rule_group_notification">
<field name="name">Own notification</field>
<field name="model">res.notification</field>
<field name="global_p" eval="False"/>
<field name="default_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_user_notification1">
<field name="domain" eval="[('user', '=', Eval('user_id', -1))]" pyson="1"/>
<field name="rule_group" ref="rule_group_notification"/>
</record>
<record model="ir.rule.group" id="rule_group_notification_admin">
<field name="name">Any notification</field>
<field name="model">res.notification</field>
<field name="global_p" eval="False"/>
<field name="default_p" eval="False"/>
</record>
<record model="ir.rule" id="rule_user_notification_admin1">
<field name="domain" eval="[]" pyson="1"/>
<field name="rule_group" ref="rule_group_notification_admin"/>
</record>
<record model="ir.rule.group-res.group" id="rule_group_notification_admin_admin">
<field name="rule_group" ref="rule_group_notification_admin"/>
<field name="group" ref="group_admin"/>
</record>
</data>
</tryton>
+2
View File
@@ -5,6 +5,7 @@ xml:
res.xml
group.xml
user.xml
notification.xml
message.xml
[register]
@@ -19,6 +20,7 @@ model:
user.Warning_
user.UserApplication
user.UserConfigStart
notification.Notification
wizard:
user.UserConfig
report:
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form col="2">
<label name="user"/>
<field name="user"/>
<label name="label"/>
<field name="label"/>
<label name="description"/>
<field name="description"/>
<label name="unread"/>
<field name="unread"/>
</form>
@@ -0,0 +1,29 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="icon"/>
<field name="icon"/>
<label name="user"/>
<field name="user"/>
<label name="label"/>
<field name="label" colspan="3"/>
<label name="description"/>
<field name="description" colspan="3"/>
<label name="unread"/>
<field name="unread"/>
<newline/>
<label name="model_ref"/>
<field name="model_ref"/>
<label name="records"/>
<field name="records" widget="pyson"/>
<label name="action"/>
<field name="action"/>
<label name="action_value"/>
<field name="action_value" widget="pyson"/>
</form>
@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="user" optional="0"/>
<field name="label" icon="icon" expand="1"/>
<field name="description" expand="2"/>
<field name="unread" optional="0"/>
</tree>