[IMP][ADD] mail, various: timeline view.

This commit introduces a new view type, the timeline view. This view is
intended to display messages like the previous chatter. This is now a view
like the form or list view. Like other views it will be possible to have
custom templates for some specific needs, allowing customization.

[IMP] mail: the value tracking is modified. Previously messages were created
containing the modified values. Those values are now stored, using a new
model mail.tracking.value. The message body is dynamically build based on
the values. Tests have been updated.

This version is temporary. Indeed two main modifications will come in a short
future :

 - the new design will improve the display
 - the slack mode will change the way the Inbox and notifications are managed

All glory to the Hypnotoad.

Special thanks to Valerie Pirenne (vpi), Jerome Maes (jem) and Richat Mathot
(rim) that did not code but said a lot of things. Martin Trigaux (mat) did
nothing, a bit like for the slides modules, but he is busy sending emails.
This commit is contained in:
Valerie Pirenne
2015-05-21 12:10:25 +02:00
committed by Thibault Delavallée
parent 946d245c2b
commit c24332e6da
36 changed files with 3900 additions and 3824 deletions
@@ -1,185 +1,146 @@
@charset "UTF-8";
.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_goal {
width: 230px;
min-height: 200px;
}
min-height: 200px; }
.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_badge {
width: 250px;
min-height: 150px; }
.openerp .oe_kanban_badge_avatars {
margin-top: 8px; }
.openerp .oe_kanban_badge_avatars img {
width: 30px;
height: 30px;
padding-left: 0;
margin-top: 3px;
border-radius: 2px;
-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); }
.openerp .oe_margin_top_8 {
margin-top: 8px; }
.openerp .oe_margin_top_16 {
margin-top: 16px; }
.openerp .oe_kanban_goal .oe_goal_state_block {
width: 200px;
height: 130px;
margin: auto;
margin-bottom: -20px;
}
.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state {
font-size: 2.5em;
font-weight: bold;
padding-top: 30px;
}
.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state .fa {
font-size: 3em;
margin-top: -17px;
margin-left: 17px;
}
margin-bottom: -20px; }
.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state {
font-size: 2.5em;
font-weight: bold;
padding-top: 30px; }
.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state .fa {
font-size: 3em;
margin-top: -17px;
margin-left: 17px; }
.openerp .oe_kanban_goal .oe_goal_state_block, .openerp .oe_kanban_goal p {
text-align: center;
}
.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_badge {
width: 250px;
min-height: 150px;
}
.openerp .oe_kanban_badge_avatars {
width: 30px;
}
.openerp .oe_margin_top_8 {
margin-top: 8px;
}
.openerp .oe_margin_top_16 {
margin-top: 16px;
}
text-align: center; }
.openerp .oe_kanban_content .oe_goal_gauge:first-child {
margin: auto; }
.openerp .oe_kanban_content .oe_goal_gauge svg {
margin-top: -20px; }
.openerp .oe_no_overflow {
overflow: hidden;
}
overflow: hidden; }
.openerp .oe_red {
color: red;
}
color: red; }
.openerp .oe_green {
color: green;
}
color: green; }
.openerp .oe_orange {
color: orange;
}
color: orange; }
.openerp .oe_form td .oe_no_padding {
margin-left: -6px;
}
.openerp .oe_mail_wall .oe_mail_wall_aside {
margin-top: 15px;
position: relative;
display: inline-block;
vertical-align: top;
width: 280px;
border-radius: 2px;
}
.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_challenge_list {
background-color: #ededf6;
}
.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_suggestion {
background-color: #d3def1;
}
.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_suggestion ul {
padding-left: 15px;
}
.openerp .oe_mail_wall .oe_mail_wall_aside h4, .openerp .oe_mail_wall .oe_mail_wall_aside .oe_goals_list .oe_thead {
text-align: center;
padding-bottom: 15px;
}
.openerp .oe_mail_wall .oe_mail_wall_aside > div {
border-bottom: solid 5px white;
border-radius: 2px;
}
.openerp .oe_mail_wall .oe_goal {
border-bottom: solid 3px white;
padding: 5px 10px;
}
.openerp .oe_mail_wall .oe_goal .oe_update_challenge.fa, .openerp .oe_mail_wall .oe_goal .oe_goal_action.fa {
visibility: hidden;
font-size: 16px;
float: right;
position: relative;
}
.openerp .oe_mail_wall .oe_goal div:hover > .oe_update_challenge, .openerp .oe_mail_wall .oe_goal div:hover > .oe_goal_action, .openerp .oe_mail_wall .oe_goal th:hover > .oe_goal_action {
visibility: visible;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list {
padding-left: 0;
margin-top: 5px;
margin-bottom: 10px;
width: 100%;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress div {
display: inline-block;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress::before {
content: "•";
padding-right: 4px;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached::before {
content: "✓";
padding-right: 0;
margin-left: -2px;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached .oe_cell {
text-decoration: line-through;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_cell.oe_goal_current {
font-size: 150%;
font-weight: bold;
min-width: 50px;
padding: 0 5px;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_outer_box {
display: inline-block;
position: relative;
z-index: 0;
vertical-align: middle;
width: 100%;
border: solid 1px rgba(0, 0, 0, 0.03);
border-radius: 2px;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_outer_box.oe_no_progress {
border: none;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_progress_background {
background-color: white;
position: absolute;
height: 100%;
width: 100%;
z-index: -2;
top: 0;
left: 0;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_progress {
background-color: #d4e9de;
position: absolute;
height: 100%;
width: 0;
z-index: -1;
top: 0;
left: 0;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_thead {
font-weight: normal;
padding: 5px;
text-align: center;
}
.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_cell {
padding: 3px 0;
}
.openerp .oe_mail_wall .oe_table.oe_goals_list .col0 {
margin-left: -6px; }
.openerp .oe_timeline .oe_tl_inbox_aside .oe_gamification_suggestion {
background-color: #d3def1; }
.openerp .oe_timeline .oe_tl_inbox_aside .oe_gamification_suggestion ul {
padding-left: 15px; }
.openerp .oe_timeline .oe_tl_inbox_aside .oe_gamification_suggestion span {
background-color: #f0eeee;
display: inline-block;
height: 25px;
vertical-align: middle;
font-size: 14px;
font-weight: bold;
margin-top: -20px;
padding-left: 0 5px 0px 5px; }
.openerp .oe_timeline .oe_goal {
background-color: #f0eeee;
border: solid 1px #cacaca;
margin-top: 20px;
padding-left: 10px; }
.openerp .oe_timeline .oe_goal .oe_update_challenge.fa, .openerp .oe_timeline .oe_goal .oe_goal_action.fa {
visibility: hidden;
font-size: 16px;
float: right;
position: relative; }
.openerp .oe_timeline .oe_goal div:hover > .oe_update_challenge, .openerp .oe_timeline .oe_goal div:hover > .oe_goal_action, .openerp .oe_timeline .oe_goal th:hover > .oe_goal_action {
visibility: visible; }
.openerp .oe_timeline .oe_goal .oe_goals_list {
padding-left: 0;
margin-top: 5px;
margin-bottom: 10px;
width: 100%; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress div {
display: inline-block; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress::before {
content: "•";
padding-right: 4px; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached::before {
content: "✓";
padding-right: 0;
margin-left: -2px; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached .oe_cell {
text-decoration: line-through; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_cell.oe_goal_current {
font-size: 150%;
font-weight: bold;
min-width: 50px;
padding: 0 5px; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_outer_box {
display: inline-block;
position: relative;
z-index: 0;
vertical-align: middle;
width: 100%;
border: solid 1px rgba(0, 0, 0, 0.03);
border-radius: 2px; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_outer_box.oe_no_progress {
border: none; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_progress_background {
background-color: white;
position: absolute;
height: 100%;
width: 100%;
z-index: -2;
top: 0;
left: 0; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_progress {
background-color: #d4e9de;
position: absolute;
height: 100%;
width: 0;
z-index: -1;
top: 0;
left: 0; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_thead {
font-weight: normal;
padding: 5px;
text-align: center; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_cell {
padding: 3px 0; }
.openerp .oe_timeline .oe_table.oe_goals_list .col0 {
font-size: 200%;
font-weight: bold;
width: 25px;
text-align: center;
}
.openerp .oe_mail_wall .oe_table.oe_goals_list .col1 {
padding: 0 5px;
}
.openerp .oe_mail_wall .oe_table.oe_goals_list .col2 {
width: auto;
}
.openerp .oe_mail_wall .oe_user_avatar {
text-align: center; }
.openerp .oe_timeline .oe_table.oe_goals_list .col1 {
padding: 0 5px; }
.openerp .oe_timeline .oe_table.oe_goals_list .col2 {
width: auto; }
.openerp .oe_timeline .oe_user_avatar {
width: 24px;
padding-right: 5px;
}
.openerp .oe_mail_wall .oe_mail {
display: inline-block;
}
.openerp .oe_mail_wall .oe_table {
display: table;
}
.openerp .oe_mail_wall .oe_row {
display: table-row;
}
.openerp .oe_mail_wall .oe_cell {
padding-right: 5px; }
.openerp .oe_timeline .oe_table {
display: table; }
.openerp .oe_timeline .oe_row {
display: table-row; }
.openerp .oe_timeline .oe_cell {
display: table-cell;
vertical-align: middle;
}
vertical-align: middle; }
@@ -62,34 +62,29 @@
.oe_form td .oe_no_padding
margin-left: -6px
.oe_mail_wall
.oe_mail_wall_aside
margin-top: 15px
position: relative
display: inline-block
vertical-align: top
width: 280px
border-radius: 2px
.oe_gamification_challenge_list
background-color: #EDEDF6
.oe_timeline
.oe_tl_inbox_aside
.oe_gamification_suggestion
background-color: rgb(211, 222, 241)
ul
background-color: #d3def1
ul
padding-left: 15px
h4, .oe_goals_list .oe_thead
text-align: center
padding-bottom: 15px
.oe_mail_wall_aside > div
border-bottom: solid 5px white
border-radius: 2px
span
background-color: #f0eeee
display: inline-block
height: 25px
vertical-align: middle
font-size: 14px
font-weight: bold
margin-top: -20px
padding-left: 0 5px 0px 5px
.oe_goal
border-bottom: solid 3px white
padding: 5px 10px
background-color: #f0eeee
border: solid 1px #CACACA
margin-top: 20px
padding-left: 10px
.oe_update_challenge.fa, .oe_goal_action.fa
visibility: hidden
font-size: 16px
@@ -97,7 +92,6 @@
position: relative
div:hover > .oe_update_challenge, div:hover > .oe_goal_action, th:hover > .oe_goal_action
visibility: visible
.oe_goals_list
padding-left: 0
margin-top: 5px
@@ -170,18 +164,15 @@
padding: 0 5px
.col2
width: auto
.oe_user_avatar
width: 24px
padding-right: 5px
.oe_mail
display: inline-block
.oe_table
display: table
.oe_row
display: table-row
.oe_cell
display: table-cell
vertical-align: middle
vertical-align: middle
@@ -119,11 +119,11 @@ var Sidebar = Widget.extend({
}
});
mail.Wall.include({
mail.TimelineView.include({
start: function() {
this._super();
var sidebar = new Sidebar(this);
sidebar.appendTo($('.oe_mail_wall_aside'));
sidebar.appendTo($('.o_timeline__inbox_aside'));
},
});
@@ -7,9 +7,9 @@
</t>
<t t-name="gamification.ChallengeSummary">
<div class="oe_goal">
<div>
<div class="oe_gamification_title">
<i class="oe_update_challenge fa fa-refresh" rol="button" t-attf-id="{challenge.id}"/>
<h4><t t-esc="challenge.name" /></h4>
<span><t t-esc="challenge.name" /></span>
</div>
<t t-if="challenge.visibility_mode == 'personal'">
+18 -13
View File
@@ -1,19 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="mail.action_mail_inbox_feeds" model="ir.actions.client">
<field name="params" eval="&quot;{
'domain': [
('to_read', '=', True),
('starred', '=', False),
],
'view_mailbox': True,
'view_inbox': True,
'read_action': 'read',
'show_compose_message': True
}&quot;"/>
<record model="ir.actions.act_window" id="mail.action_mail_inbox_feeds">
<field name="name">Inbox</field>
<field name="res_model">mail.message</field>
<field name="view_type">form</field>
<field name="view_mode">timeline</field>
<field name="context">{
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
'options' : {'view_mailbox': True,
'view_inbox': True,
'show_write_to_followers': True}
}</field>
<field name="domain">[
'|',
('notification_ids.partner_id.user_ids', 'in', [uid]),
('author_id.user_ids', 'in', [uid]),
]</field>
</record>
</data>
</openerp>
+1 -1
View File
@@ -72,7 +72,7 @@ var Employees = Widget.extend({
}
});
mail.WallSidebar.include({
mail.Sidebar.include({
start: function () {
this._super.apply(this, arguments);
var sug_employees = new Employees(this);
+1
View File
@@ -31,6 +31,7 @@ Main Features
'wizard/invite_view.xml',
'wizard/mail_compose_message_view.xml',
'views/mail_message_subtype_views.xml',
'views/mail_tracking_views.xml',
'views/mail_message_views.xml',
'views/mail_mail_views.xml',
'views/mail_followers_views.xml',
+1
View File
@@ -2,6 +2,7 @@
import ir_attachment
import mail_message_subtype
import mail_tracking_value
import mail_alias
import mail_followers
import mail_message
+23 -23
View File
@@ -76,27 +76,28 @@ class MailGroup(models.Model):
).create(vals)
group.alias_id.write({"alias_force_thread_id": group.id, 'alias_parent_thread_id': group.id})
# Create client action for this group and link the menu to it
# Create action window for this group and link the menu to it
inbox_ref = self.env.ref('mail.action_mail_group_feeds')
search_ref = self.env.ref('mail.view_message_search')
params = {
'search_view_id': search_ref.id,
'domain': [
('model', '=', 'mail.group'),
('res_id', '=', group.id),
],
'context': {
'default_model': 'mail.group',
'default_res_id': group.id,
},
'res_model': 'mail.message',
'thread_level': 1,
'header_description': group._get_header(),
'view_mailbox': True,
'compose_placeholder': 'Send a message to the group',
}
new_action = inbox_ref.sudo().copy(default={'params': str(params), 'name': vals['name']})
menu.write({'action': 'ir.actions.client,%d' % new_action.id, 'mail_group_id': group.id})
act_domain = [('model', '=', 'mail.group'), ('res_id', '=', group.id)]
act_context = {'default_model': 'mail.group',
'default_res_id': group.id,
'options': {'view_mailbox': False,
'view_inbox': True,
'read_action': 'read',
'compose_placeholder': 'Send a message to the group'},
'params': {'header_description': group._get_header(),
'name': vals['name'],}
}
act_res_model = 'mail.message'
act_search_view_id = search_ref.id
new_action = inbox_ref.sudo().copy(default={'domain': act_domain,
'context': act_context,
'res_model': act_res_model,
'search_view_id': act_search_view_id,
'name': vals['name']})
menu.write({'action': 'ir.actions.act_window,%d' % new_action.id, 'mail_group_id': group.id})
if vals.get('group_ids'):
group._subscribe_users()
@@ -126,12 +127,11 @@ class MailGroup(models.Model):
result = super(MailGroup, self).write(vals)
if vals.get('group_ids'):
self._subscribe_users()
# if description, name or alias is changed: update client action
# if description, name or alias is changed: update action window
if vals.get('description') or vals.get('name') or vals.get('alias_id') or vals.get('alias_name'):
for group in self:
new_params = group.menu_id.action.params
new_params['header_description'] = group._generate_header_description()
group.menu_id.action.sudo().write({'params': str(new_params)})
if (vals.get('name')):
group.menu_id.action.sudo().write({'name': vals.get('name')})
# if name is changed: update menu
if vals.get('name'):
self.sudo().mapped('menu_id').write({'name': vals.get('name')})
+115 -148
View File
@@ -31,7 +31,7 @@ class Message(models.Model):
_message_read_limit = 30
_message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from',
'message_type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name']
'message_type', 'vote_user_ids', 'attachment_ids', 'tracking_value_ids' ,'author_id', 'partner_ids', 'record_name']
_message_record_name_length = 18
_message_read_more_limit = 1024
@@ -98,6 +98,7 @@ class Message(models.Model):
'res.users', 'mail_vote', 'message_id', 'user_id', string='Votes',
help='Users that voted for this message')
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server', readonly=1)
tracking_value_ids = fields.One2many('mail.tracking.value', 'mail_message_id', string='Tracking values')
@api.depends('notification_ids')
def _get_to_read(self):
@@ -236,32 +237,43 @@ class Message(models.Model):
"""
pid = self.env.user.partner_id.id
# 1. Aggregate partners (author_id and partner_ids) and attachments
partner_ids = set()
attachment_ids = set()
# 1. Aggregate partners (author_id and partner_ids), attachments and tracking values
partners = self.env['res.partner']
attachments = self.env['ir.attachment']
trackings = self.env['mail.tracking.value']
for key, message in message_tree.iteritems():
if message.author_id:
partner_ids |= set([message.author_id.id])
partners |= message.author_id
if message.subtype_id and message.notified_partner_ids: # take notified people of message with a subtype
partner_ids |= set([partner.id for partner in message.notified_partner_ids])
partners |= message.notified_partner_ids
elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log)
partner_ids |= set([partner.id for partner in message.partner_ids])
partners |= message.partner_ids
if message.attachment_ids:
attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
attachments |= message.attachment_ids
if message.tracking_value_ids:
trackings |= message.tracking_value_ids
# Read partners as SUPERUSER -> display the names like classic m2o even if no access
partners = self.env['res.partner'].sudo().browse(partner_ids).name_get()
partner_tree = dict((partner[0], partner) for partner in partners)
partners_names = partners.sudo().name_get()
partner_tree = dict((partner[0], partner) for partner in partners_names)
# 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
attachments = self.env['ir.attachment'].sudo().browse(attachment_ids).read(['id', 'datas_fname', 'name', 'file_type_icon'])
attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'file_type_icon'])
attachments_tree = dict((attachment['id'], {
'id': attachment['id'],
'filename': attachment['datas_fname'],
'name': attachment['name'],
'file_type_icon': attachment['file_type_icon'],
}) for attachment in attachments)
}) for attachment in attachments_data)
# 3. Update message dictionaries
# 3. Tracking values
tracking_tree = dict((tracking.id, {
'id': tracking.id,
'changed_field': tracking.field_desc,
'old_value': tracking.get_old_display_value()[0],
'new_value': tracking.get_new_display_value()[0],
}) for tracking in trackings)
# 4. Update message dictionaries
for message_dict in messages:
message_id = message_dict.get('id')
message = message_tree[message_id]
@@ -280,13 +292,20 @@ class Message(models.Model):
for attachment in message.attachment_ids:
if attachment.id in attachments_tree:
attachment_ids.append(attachments_tree[attachment.id])
tracking_value_ids = []
for tracking_value in message.tracking_value_ids:
if tracking_value.id in tracking_tree:
tracking_value_ids.append(tracking_tree[tracking_value.id])
message_dict.update({
'is_author': pid == author[0],
'author_id': author,
'partner_ids': partner_ids,
'attachment_ids': attachment_ids,
'tracking_value_ids': tracking_value_ids,
'user_pid': pid
})
return True
@api.multi
@@ -306,22 +325,10 @@ class Message(models.Model):
vote_nb = len(self.vote_user_ids)
has_voted = self._uid in [user.id for user in self.vote_user_ids]
try:
if parent_id:
max_length = 300
else:
max_length = 100
body_short = tools.html_email_clean(self.body, remove=False, shorten=True, max_length=max_length)
except Exception:
body_short = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % self.id
_logger.exception(Exception)
return {'id': self.id,
'type': self.message_type,
'message_type': self.message_type,
'subtype': self.subtype_id.name if self.subtype_id else False,
'body': self.body,
'body_short': body_short,
'model': self.model,
'res_id': self.res_id,
'record_name': self.record_name,
@@ -338,108 +345,17 @@ class Message(models.Model):
'has_voted': has_voted,
'is_favorite': self.starred,
'attachment_ids': [],
'tracking_value_ids': [],
}
@api.model
def _message_read_add_expandables(self, messages, message_tree, parent_tree,
message_unload_ids=[], thread_level=0, domain=[], parent_id=False):
""" Create expandables for message_read, to load new messages.
1. get the expandable for new threads
if display is flat (thread_level == 0):
fetch message_ids < min(already displayed ids), because we
want a flat display, ordered by id
else:
fetch message_ids that are not childs of already displayed
messages
2. get the expandables for new messages inside threads if display
is not flat
for each thread header, search for its childs
for each hole in the child list based on message displayed,
create an expandable
:param list messages: list of message structure for the Chatter
widget to which expandables are added
:param dict message_tree: dict [id]: browse record of this message
:param dict parent_tree: dict [parent_id]: [child_ids]
:param list message_unload_ids: list of message_ids we do not want
to load
:return bool: True
"""
def _get_expandable(domain, message_nb, parent_id, max_limit):
return {
'domain': domain,
'nb_messages': message_nb,
'type': 'expandable',
'parent_id': parent_id,
'max_limit': max_limit,
}
if not messages:
return True
message_ids = sorted(message_tree.keys())
# 1. get the expandable for new threads
if thread_level == 0:
exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))]
else:
exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())]
more_count = self.search_count(exp_domain)
if more_count:
# inside a thread: prepend
if parent_id:
messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True))
# new threads: append
else:
messages.append(_get_expandable(exp_domain, -1, parent_id, True))
# 2. get the expandables for new messages inside threads if display is not flat
if thread_level == 0:
return True
for message_id in message_ids:
message = message_tree[message_id]
# generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok)
if message.parent_id:
continue
# check there are message for expandable
child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids)
child_ids = sorted(list(child_ids), reverse=True)
if not child_ids:
continue
# make groups of unread messages
id_min, id_max, nb = max(child_ids), 0, 0
for child_id in child_ids:
if not child_id in message_ids:
nb += 1
if id_min > child_id:
id_min = child_id
if id_max < child_id:
id_max = child_id
elif nb > 0:
exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
idx = [msg.get('id') for msg in messages].index(child_id) + 1
# messages.append(_get_expandable(exp_domain, nb, message_id, False))
messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
id_min, id_max, nb = max(child_ids), 0, 0
else:
id_min, id_max, nb = max(child_ids), 0, 0
if nb > 0:
exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
idx = [msg.get('id') for msg in messages].index(message_id) + 1
# messages.append(_get_expandable(exp_domain, nb, message_id, id_min))
messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
return True
@api.cr_uid_context
def message_read_wrapper(self, cr, uid, ids=None, domain=None, message_unload_ids=None,
thread_level=0, context=None, parent_id=False, limit=None):
return self.message_read(cr, uid, ids, domain=domain, message_unload_ids=message_unload_ids, thread_level=thread_level, parent_id=parent_id, limit=limit)
def message_read_wrapper(self, cr, uid, ids=None, domain=None, context=None,
thread_level=0, parent_id=False, limit=None, child_limit=None):
return self.message_read(cr, uid, ids, domain=domain, thread_level=thread_level, context=context,
parent_id=parent_id, limit=limit, child_limit=child_limit)
@api.multi
def message_read(self, domain=None, message_unload_ids=None, thread_level=0, parent_id=False, limit=None):
def message_read(self, domain=None, thread_level=0, context= None, parent_id=False, limit=None, child_limit=None):
""" Read messages from mail.message, and get back a list of structured
messages to be displayed as discussion threads. If IDs is set,
fetch these records. Otherwise use the domain to fetch messages.
@@ -447,31 +363,33 @@ class Message(models.Model):
well formed threads, if uid has access to them.
After reading the messages, expandable messages are added in the
message list (see ``_message_read_add_expandables``). It consists
in messages holding the 'read more' data: number of messages to
read, domain to apply.
message list. It consists in messages holding the 'read more' data:
number of messages to read, domain to apply.
:param list ids: optional IDs to fetch
:param list domain: optional domain for searching ids if ids not set
:param list message_unload_ids: optional ids we do not want to fetch,
because i.e. they are already displayed somewhere
:param int parent_id: context of parent_id
- if parent_id reached when adding ancestors, stop going further
in the ancestor search
- if set in flat mode, ancestor_id is set to parent_id
:param int limit: number of messages to fetch, before adding the
ancestors and expandables
:return list: list of message structure for the Chatter widget
:param int child_limit: number of child messages to fetch
:return dict:
- int: number of messages read (status 'unread' to 'read')
- list: list of threads [[messages_of_thread1], [messages_of_thread2]]
"""
assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level
domain = domain if domain is not None else []
message_unload_ids = message_unload_ids if message_unload_ids is not None else []
if message_unload_ids:
domain += [('id', 'not in', message_unload_ids)]
limit = limit or self._message_read_limit
child_limit = child_limit or self._message_read_limit
message_tree = {}
message_list = []
parent_tree = {}
child_ids = []
parent_ids = []
exp_domain = []
# no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
if not self.ids and domain:
@@ -497,24 +415,73 @@ class Message(models.Model):
message_tree[parent.id] = parent
# newest messages first
parent_tree.setdefault(tree_parent_id, [])
if tree_parent_id != message_id:
parent_tree[tree_parent_id].append(message_tree[message_id]._message_read_dict(parent_id=tree_parent_id))
if thread_level:
for key, message_id_list in parent_tree.iteritems():
message_id_list.sort(key=lambda item: item['id'])
message_id_list.insert(0, message_tree[key]._message_read_dict())
# build thread structure
# for each parent_id: get child messages, add message expandable and parent message if needed [child1, child2, expandable, parent_message]
# add thread expandable if it remains some uncaught parent_id
if self.ids and len(self.ids) > 0:
for parent in parent_tree:
parent_ids.append(parent)
# create final ordered message_list based on parent_tree
parent_list = parent_tree.items()
parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item[1]]) if item[1] else item[0], reverse=True)
message_list = [message for (key, msg_list) in parent_list for message in msg_list]
if not thread_level:
child_ids = self.ids;
exp_domain = domain + [('id', '<', min(child_ids))]
else:
child_ids = [msg.id for msg in self.browse(parent).child_ids][0:child_limit]
exp_domain = [('parent_id', '=', parent), ('id', '>', parent)]
if len(child_ids):
exp_domain += [('id', '<', min(child_ids))]
# get the child expandable messages for the tree
self._message_read_dict_postprocess(message_list, message_tree)
self._message_read_add_expandables(message_list, message_tree, parent_tree,
thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id)
return message_list
for cid in child_ids:
if cid not in message_tree:
message_tree[cid] = self.browse(cid)
parent_tree[parent].append(message_tree[cid]._message_read_dict(parent_id=parent))
if parent and thread_level:
parent_tree[parent].sort(key=lambda item: item['id'])
parent_tree[parent].reverse();
parent_tree[parent].append(message_tree[parent]._message_read_dict())
self._message_read_dict_postprocess(parent_tree[parent], message_tree)
# add 'message' expandable (inside a thread)
more_count = self.search_count(exp_domain)
if more_count:
exp = {'message_type':'expandable',
'domain': exp_domain,
'nb_messages': more_count,
'parent_id': parent}
if parent and thread_level:
#insert expandable before parent message
parent_tree[parent].insert(len(parent_tree[parent])-1, exp)
else:
#insert expandable at the end of the message list
parent_tree[parent].append(exp)
# create final ordered parent_list based on parent_tree
parent_list = parent_tree.values()
parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item]), reverse=True)
#add 'thread' expandable
if thread_level:
exp_domain = domain + [('id', '<', min(self.ids)), ('id', 'not in', parent_ids), ('parent_id', 'not in', parent_ids)]
more_count = self.search_count(exp_domain)
if more_count:
parent_list.append([{'message_type':'expandable',
'domain': exp_domain,
'nb_messages': more_count,
'parent_id': parent_id}])
nb_read = 0
if context and 'mail_read_set_read' in context and context['mail_read_set_read']:
nb_read = self.set_message_read(True, create_missing=False)
else:
nb_read = 0
parent_list = []
return {'nb_read': nb_read, 'threads': parent_list}
@api.multi
def get_like_names(self, limit=10):
+11 -43
View File
@@ -426,57 +426,27 @@ class MailThread(models.AbstractModel):
@api.multi
def message_track(self, tracked_fields, initial_values):
def convert_for_display(value, col_info):
if not value and col_info['type'] == 'boolean':
return 'False'
if not value:
return ''
if col_info['type'] == 'many2one':
return value.name_get()[0][1]
if col_info['type'] == 'selection':
return dict(col_info['selection'])[value]
return value
def format_message(message_description, tracked_values):
message = ''
if message_description:
message = '<span>%s</span>' % message_description
for name, change in tracked_values.items():
message += '<div> &nbsp; &nbsp; &bull; <b>%s</b>: ' % change.get('col_info')
if change.get('old_value'):
message += '%s &rarr; ' % change.get('old_value')
message += '%s</div>' % change.get('new_value')
return message
if not tracked_fields:
return True
for record in self:
initial = initial_values[record.id]
changes = set()
tracked_values = {}
tracking_value_ids = []
# generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
for col_name, col_info in tracked_fields.items():
field = self._fields[col_name]
initial_value = initial[col_name]
record_value = getattr(record, col_name)
new_value = getattr(record, col_name)
if new_value != initial_value and (new_value or initial_value): # because browse null != False
tracking = self.env['mail.tracking.value'].create_tracking_values(initial_value, new_value, col_name, col_info)
if tracking:
tracking_value_ids.append([0, 0, tracking])
if record_value == initial_value and getattr(field, 'track_visibility', None) == 'always':
tracked_values[col_name] = dict(
col_info=col_info['string'],
new_value=convert_for_display(record_value, col_info),
)
elif record_value != initial_value and (record_value or initial_value): # because browse null != False
if getattr(field, 'track_visibility', None) in ['always', 'onchange']:
tracked_values[col_name] = dict(
col_info=col_info['string'],
old_value=convert_for_display(initial_value, col_info),
new_value=convert_for_display(record_value, col_info),
)
if col_name in tracked_fields:
changes.add(col_name)
if not changes:
continue
@@ -500,10 +470,10 @@ class MailThread(models.AbstractModel):
if not (subtype_rec and subtype_rec.exists()):
_logger.debug('subtype %s not found' % subtype_xmlid)
continue
message = format_message(subtype_rec.description if subtype_rec.description else subtype_rec.name, tracked_values)
record.message_post(subtype=subtype_xmlid, tracking_value_ids=tracking_value_ids)
else:
message = format_message('', tracked_values)
record.message_post(body=message, subtype=subtype_xmlid)
record.message_post(tracking_value_ids=tracking_value_ids)
return True
#------------------------------------------------------
@@ -1509,7 +1479,6 @@ class MailThread(models.AbstractModel):
content_subtype='html', **kwargs):
""" Post a new message in an existing thread, returning the new
mail.message ID.
:param int thread_id: thread ID to post into, or list with one ID;
if False/0, mail.message model will also be set as False
:param str body: body of the message, usually raw HTML that will
@@ -1520,7 +1489,6 @@ class MailThread(models.AbstractModel):
parent partners to the message in case of private discussion
:param tuple(str,str) attachments or list id: list of attachment tuples in the form
``(name,content)``, where content is NOT base64 encoded
Extra keyword arguments will be used as default column values for the
new mail.message record. Special cases:
- attachment_ids: supposed not attached to any document; attach them
+103
View File
@@ -0,0 +1,103 @@
from datetime import datetime
from openerp import api, fields, models, tools
class MailTracking(models.Model):
_name = 'mail.tracking.value'
_description = 'Mail Tracking Value'
field = fields.Char('Changed Field', required=True, readonly=1)
field_desc = fields.Char('Field Description', required=True, readonly=1)
field_type = fields.Char('Field Type')
old_value_integer = fields.Integer('Old Value Integer', readonly=1)
old_value_float = fields.Float('Old Value Float', readonly=1)
old_value_char = fields.Char('Old Value Char', readonly=1)
old_value_text = fields.Text('Old Value Text', readonly=1)
old_value_datetime = fields.Datetime('Old Value DateTime', readonly=1)
new_value_integer = fields.Integer('New Value Integer', readonly=1)
new_value_float = fields.Float('New Value Float', readonly=1)
new_value_char = fields.Char('New Value Char', readonly=1)
new_value_text = fields.Text('New Value Text', readonly=1)
new_value_datetime = fields.Datetime('New Value Datetime', readonly=1)
mail_message_id = fields.Many2one('mail.message', 'Message ID', required=True, select=True, ondelete='cascade')
@api.model
def create_tracking_values(self, initial_value, new_value, col_name, col_info):
tracked = True
values = {'field': col_name, 'field_desc': col_info['string'], 'field_type': col_info['type']}
if col_info['type'] in ['integer', 'float', 'char', 'text', 'datetime']:
values.update({
'old_value_%s' % col_info['type']: initial_value,
'new_value_%s' % col_info['type']: new_value
})
elif col_info['type'] == 'date':
values.update({
'old_value_datetime': initial_value and datetime.strftime(datetime.combine(datetime.strptime(initial_value, tools.DEFAULT_SERVER_DATE_FORMAT), datetime.min.time()), tools.DEFAULT_SERVER_DATETIME_FORMAT) or False,
'new_value_datetime': new_value and datetime.strftime(datetime.combine(datetime.strptime(new_value, tools.DEFAULT_SERVER_DATE_FORMAT), datetime.min.time()), tools.DEFAULT_SERVER_DATETIME_FORMAT) or False,
})
elif col_info['type'] == 'boolean':
values.update({
'old_value_integer': initial_value,
'new_value_integer': new_value
})
elif col_info['type'] == 'selection':
values.update({
'old_value_char': initial_value and dict(col_info['selection'])[initial_value] or '',
'new_value_char': new_value and dict(col_info['selection'])[new_value] or ''
})
elif col_info['type'] == 'many2one':
values.update({
'old_value_integer': initial_value and initial_value.id or 0,
'new_value_integer': new_value and new_value.id or 0,
'old_value_char': initial_value and initial_value.name_get()[0][1] or '',
'new_value_char': new_value and new_value.name_get()[0][1] or ''
})
else:
tracked = False
if tracked:
return values
return {}
@api.multi
def get_old_display_value(self):
result = []
for record in self:
if record.field_type in ['integer', 'float', 'char', 'text', 'datetime']:
result.append(getattr(record, 'old_value_%s' % record.field_type))
elif record.field_type == 'date':
if record.old_value_datetime:
result.append(datetime.strptime(record.old_value_integer, tools.DEFAULT_SERVER_DATETIME_FORMAT).date())
else:
result.append(record.old_value_datetime)
elif record.field_type == 'boolean':
result.append(bool(record.old_value_integer))
elif record.field_type in ['many2one', 'selection']:
result.append(record.old_value_char)
else:
result.append(record.old_value_char)
return result
@api.multi
def get_new_display_value(self):
result = []
for record in self:
if record.field_type in ['integer', 'float', 'char', 'text', 'datetime']:
result.append(getattr(record, 'new_value_%s' % record.field_type))
elif record.field_type == 'date':
if record.new_value_datetime:
result.append(datetime.strptime(record.old_value_integer, tools.DEFAULT_SERVER_DATETIME_FORMAT).date())
else:
result.append(record.new_value_datetime)
elif record.field_type == 'boolean':
result.append(bool(record.new_value_integer))
elif record.field_type in ['many2one', 'selection']:
result.append(record.new_value_char)
else:
result.append(record.new_value_char)
return result
+4
View File
@@ -21,6 +21,10 @@ access_mail_alias_user,mail.alias.user,model_mail_alias,base.group_user,1,1,1,0
access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1,1,1,1
access_mail_message_subtype_all,mail.message.subtype.all,model_mail_message_subtype,,1,0,0,0
access_mail_message_subtype_system,mail.message.subtype.system,model_mail_message_subtype,base.group_system,1,1,1,1
access_mail_tracking_value_all,mail.tracking.value.all,model_mail_tracking_value,,1,0,0,0
access_mail_tracking_value_portal,mail.tracking.value.portal,model_mail_tracking_value,base.group_portal,1,1,1,1
access_mail_tracking_value_user,mail.tracking.value.user,model_mail_tracking_value,base.group_user,1,1,1,1
access_mail_tracking_value_system,mail.tracking.value.system,model_mail_tracking_value,base.group_system,1,1,1,1
access_mail_thread_all,mail.thread.all,model_mail_thread,,1,1,1,1
access_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,,1,1,1,1
access_mail_template,mail.template,model_mail_template,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
21 access_mail_alias_system mail.alias.system model_mail_alias base.group_system 1 1 1 1
22 access_mail_message_subtype_all mail.message.subtype.all model_mail_message_subtype 1 0 0 0
23 access_mail_message_subtype_system mail.message.subtype.system model_mail_message_subtype base.group_system 1 1 1 1
24 access_mail_tracking_value_all mail.tracking.value.all model_mail_tracking_value 1 0 0 0
25 access_mail_tracking_value_portal mail.tracking.value.portal model_mail_tracking_value base.group_portal 1 1 1 1
26 access_mail_tracking_value_user mail.tracking.value.user model_mail_tracking_value base.group_user 1 1 1 1
27 access_mail_tracking_value_system mail.tracking.value.system model_mail_tracking_value base.group_system 1 1 1 1
28 access_mail_thread_all mail.thread.all model_mail_thread 1 1 1 1
29 access_publisher_warranty_contract_all publisher.warranty.contract.all model_publisher_warranty_contract 1 1 1 1
30 access_mail_template mail.template model_mail_template base.group_user 1 1 1 0
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+144 -103
View File
@@ -14,7 +14,7 @@ var qweb = core.qweb;
/**
* ------------------------------------------------------------
* mail_followers Widget
* timeline_followers Widget
* ------------------------------------------------------------
*
* This widget handles the display of a list of records as a vertical
@@ -26,19 +26,19 @@ var qweb = core.qweb;
var Followers = form_common.AbstractField.extend({
template: 'mail.followers',
template: 'mail_followers',
init: function() {
this._super.apply(this, arguments);
this.image = this.node.attrs.image || 'image_small';
this.comment = this.node.attrs.help || false;
this.displayed_limit = this.node.attrs.displayed_nb || 10;
this.displayed_nb = this.displayed_limit;
this.ds_model = new data.DataSetSearch(this, this.view.model);
this.ds_users = new data.DataSetSearch(this, 'res.users');
this.value = [];
this.followers = [];
this.data_subtype = {};
this.view_is_editable = this.__parentedParent.is_action_enabled('edit');
},
@@ -49,7 +49,7 @@ var Followers = form_common.AbstractField.extend({
this.on_check_visibility_mode();
this.reinit();
this.bind_events();
this._super();
return this._super();
},
on_check_visibility_mode: function () {
@@ -62,37 +62,43 @@ var Followers = form_common.AbstractField.extend({
},
reinit: function() {
this.message_is_follower = false;
this.data_subtype = {};
this.message_is_follower == undefined;
this.display_buttons();
},
bind_events: function() {
var self = this;
// event: click on '(Un)Follow' button, that toggles the follow for uid
this.$('.oe_follower').on('click', function (event) {
if($(this).hasClass('oe_notfollow'))
this.$('.o_timeline_follower').on('click', function (event) {
if($(this).hasClass('o_timeline_notfollow')) {
self.do_follow();
else
}
else {
self.do_unfollow();
}
});
// event: click on a subtype, that (un)subscribe for this subtype
this.$el.on('click', '.oe_subtype_list input', function(event) {
this.$el.on('click', '.o_timeline_subtype_list input', function(event) {
self.do_update_subscription(event);
var $list = self.$('.oe_subtype_list');
var $list = self.$('.o_timeline_subtype_list');
if(!$list.hasClass('open')) {
$list.addClass('open');
}
if(self.$('.oe_subtype_list ul')[0].children.length < 1) {
if(self.$('.o_timeline_subtype_list ul')[0].children.length < 1) {
$list.removeClass('open');
}
event.stopPropagation();
});
// event: click on 'invite' button, that opens the invite wizard
this.$('.oe_invite').on('click', self.on_invite_follower);
this.$el.on('click', '.oe_invite', self.on_invite_follower);
// event: click on 'edit_subtype(pencil)' button to edit subscription
this.$el.on('click', '.oe_edit_subtype', self.on_edit_subtype);
this.$el.on('click', '.oe_remove_follower', self.on_remove_follower);
this.$el.on('click', '.oe_show_more', self.on_show_more_followers);
this.$el.on('click', 'a[data-partner]', self.on_follower_clicked);
},
@@ -137,19 +143,16 @@ var Followers = form_common.AbstractField.extend({
});
},
on_show_more_followers: function (event) {
this.displayed_nb += this.displayed_limit;
this.display_followers(false);
},
on_remove_follower: function (event) {
var partner_id = $(event.target).data('id');
var name = $(event.target).parent().find("a").html();
if (confirm(_.str.sprintf(_t("Warning! \n %s won't be notified of any email or discussion on this document. Do you really want to remove him from the followers ?"), name))) {
var context = new data.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context])
.then(this.proxy('read_value'));
}
var name = $(event.target).parent().find("a").html();
if (confirm(_.str.sprintf(_t("Warning! \n %s won't be notified of any email or discussion on this document. Do you really want to remove him from the followers ?"), name))) {
var context = new data.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id],
[partner_id],
context])
.then(this.proxy('read_value'));
}
},
on_follower_clicked: function (event) {
@@ -174,11 +177,11 @@ var Followers = form_common.AbstractField.extend({
read_value: function () {
var self = this;
this.displayed_nb = this.displayed_limit;
return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']).then(function (results) {
self.value = results[0].message_follower_ids;
self.render_value();
});
return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids'])
.then(function (results) {
self.value = results[0].message_follower_ids;
self.render_value();
});
},
render_value: function () {
@@ -200,10 +203,13 @@ var Followers = form_common.AbstractField.extend({
fetch_generic: function (error, event) {
var self = this;
event.preventDefault();
return this.ds_users.call('read', [[session.uid], ['partner_id']]).then(function (results) {
var pid = results[0]['partner_id'][0];
self.message_is_follower = (_.indexOf(self.value, pid) != -1);
}).then(self.proxy('display_generic'));
return this.ds_users.call('read', [[session.uid], ['partner_id']])
.then(function (results) {
var pid = results[0]['partner_id'][0];
self.message_is_follower = (_.indexOf(self.value, pid) != -1);
})
.then(self.proxy('display_generic'));
},
_format_followers: function(count){
@@ -220,8 +226,8 @@ var Followers = form_common.AbstractField.extend({
/* Display generic info about follower, for people not having access to res_partner */
display_generic: function () {
this.$('.oe_follower_list').empty();
this.$('.oe_follower_title').html(this._format_followers(this.value.length));
var node_user_list = this.$('.o_timeline_follower_list').empty();
this.$('.o_timeline_follower_title').html(this._format_followers(this.value.length));
},
/** Display the followers */
@@ -229,140 +235,169 @@ var Followers = form_common.AbstractField.extend({
var self = this;
this.message_is_follower = false;
this.followers = records || this.followers;
// clean and display title
var node_user_list = this.$('.oe_follower_list').empty();
this.$('.oe_follower_title').html(this._format_followers(this.followers.length));
self.message_is_follower = _.indexOf(this.followers.map(function (rec) { return rec[2]['is_uid'];}), true) != -1;
var node_user_list = this.$('.o_timeline_follower_list').empty();
this.$('.o_timeline_follower_title').html(this._format_followers(this.followers.length));
$(qweb.render('mail_followers_add_more', {'widget': self})).appendTo(node_user_list);
self.message_is_follower = _.indexOf(this.followers.map(function(rec) {return rec[2]['is_uid']}), true) != -1;
// truncate number of displayed followers
var truncated = this.followers.slice(0, this.displayed_nb);
_(truncated).each(function (record) {
_(this.followers).each(function (record) {
var partner = {
'id': record[0],
'name': record[1],
'is_uid': record[2]['is_uid'],
'is_editable': record[2]['is_editable'],
'avatar_url': mail_utils.get_image(session, 'res.partner', 'image_small', record[0]),
};
$(qweb.render('mail.followers.partner', {'record': partner, 'widget': self})).appendTo(node_user_list);
}
$(qweb.render('mail_followers_partner', {'record': partner, 'widget': self}))
.appendTo(node_user_list);
// On mouse-enter it will show the edit_subtype pencil.
if (partner.is_editable) {
self.$('.oe_follower_list').on('mouseenter mouseleave', function(e) {
self.$('.oe_edit_subtype').toggleClass('oe_hidden', e.type == 'mouseleave');
self.$('.oe_follower_list').find('.oe_partner').toggleClass('oe_partner_name', e.type == 'mouseenter');
self.$('.o_timeline_follower_list').on('mouseenter mouseleave', function(e) {
self.$('.o_timeline_edit_subtype')
.toggleClass('oe_hidden', e.type == 'mouseleave');
self.$('.o_timeline_follower_list').find('.oe_partner')
.toggleClass('oe_partner_name', e.type == 'mouseenter');
});
}
});
// FVA note: be sure it is correctly translated
if (truncated.length < this.followers.length) {
$(qweb.render('mail.followers.show_more', {'number': (this.followers.length - truncated.length)} )).appendTo(node_user_list);
}
},
display_buttons: function () {
if (this.message_is_follower) {
this.$('button.oe_follower').removeClass('oe_notfollow').addClass('oe_following');
this.$('button.o_timeline_follower').removeClass('o_timeline_notfollow').addClass('o_timeline_following');
}
else {
this.$('button.oe_follower').removeClass('oe_following').addClass('oe_notfollow');
this.$('button.o_timeline_follower').removeClass('o_timeline_following').addClass('o_timeline_notfollow');
}
if (this.view.is_action_enabled('edit'))
this.$('span.oe_mail_invite_wrapper').hide();
else
this.$('span.oe_mail_invite_wrapper').show();
if (this.view.is_action_enabled('edit')) {
this.$('span.oe_timeline_invite_wrapper').hide();
}
else {
this.$('span.oe_timeline_invite_wrapper').show();
}
},
/** Fetch subtypes, only if current user is follower */
fetch_subtypes: function (user_pid) {
var self = this;
var dialog = false;
if (user_pid) {
dialog = true;
} else {
this.$('.oe_subtype_list ul').empty();
}
else {
var subtype_list_ul = this.$('.o_timeline_subtype_list ul').empty();
if (! this.message_is_follower) {
this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', true);
this.$('.o_timeline_subtype_list > .dropdown-toggle').attr('disabled', true);
return;
}
else {
this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', false);
this.$('.o_timeline_subtype_list > .dropdown-toggle').attr('disabled', false);
}
}
var id = this.view.datarecord.id;
this.ds_model.call('message_get_subscription_data', [[id], user_pid, new data.CompoundContext(this.build_context(), {})])
this.ds_model.call('message_get_subscription_data',
[[id], user_pid, new data.CompoundContext(this.build_context(), {})])
.then(function (data) {self.display_subtypes(data, id, dialog);});
},
/** Display subtypes: {'name': default, followed} */
display_subtypes:function (data, id, dialog) {
var self = this;
var $list;
if (dialog) {
$list = self.$dialog.$el;
}
else {
$list = this.$('.oe_subtype_list ul');
}
var records = data[id].message_subtype_data;
this.records_length = $.map(records, function(value, index) { return index; }).length;
if (this.records_length > 1) { self.display_followers(); }
var old_model = '';
_(records).each(function (record, record_name) {
if (old_model != record.parent_model){
if (old_model !== ''){
var index = $($list).find('.oe_subtype').length;
$($($list).find('.oe_subtype')[index-1]).addClass('subtype-border');
}
old_model = record.parent_model;
var $list = this.$('.o_timeline_subtype_list ul');
if (_.isEmpty(this.data_subtype)) {
this.data_subtype = data;
if (dialog) {
$list = this.$dialog.$el;
}
record.name = record_name;
record.followed = record.followed || undefined;
$(qweb.render('mail.followers.subtype', {'record': record, 'dialog': dialog})).appendTo($list);
});
var old_model = '';
var records = data[id].message_subtype_data;
this.records_length = $.map(records, function(value, index) { return index; }).length;
if (this.records_length > 1) {
self.display_followers();
}
_.each(records, function (record, record_name) {
if (old_model != record.parent_model) {
if (old_model != '') {
var index = $($list).find('.oe_subtype').length;
$list.find('.oe_subtype')[index-1].addClass('subtype-border');
}
old_model = record.parent_model;
}
record.name = record_name;
record.followed = record.followed || undefined;
$(qweb.render('mail_followers_subtype', {'record': record,
'dialog': dialog}))
.appendTo($list);
});
}
},
do_follow: function () {
var context = new data.CompoundContext(this.build_context(), {});
this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', false);
this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [session.uid], undefined, context])
this.$('.o_timeline_subtype_list > .dropdown-toggle').attr('disabled', false);
this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id],
[session.uid],
undefined,
context])
.then(this.proxy('read_value'));
_.each(this.$('.oe_subtype_list input'), function (record) {
_.each(this.$('.o_timeline_subtype_list input'), function (record) {
$(record).attr('checked', 'checked');
});
},
do_unfollow: function (user_pid) {
if (confirm(_t("Warning! \nYou won't be notified of any email or discussion on this document. Do you really want to unfollow this document ?"))) {
_(this.$('.oe_msg_subtype_check')).each(function (record) {
_(this.$('.o_timeline_msg_subtype_check')).each(function (record) {
$(record).attr('checked',false);
});
var action_unsubscribe = 'message_unsubscribe_users';
this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', true);
var follower_ids = [session.uid];
if (user_pid) {
action_unsubscribe = 'message_unsubscribe';
follower_ids = [user_pid];
}
var action_unsubscribe = 'message_unsubscribe_users';
this.$('.o_timeline_subtype_list > .dropdown-toggle').attr('disabled', true);
var follower_ids = [session.uid];
if (user_pid) {
action_unsubscribe = 'message_unsubscribe';
follower_ids = [user_pid];
}
var context = new data.CompoundContext(this.build_context(), {});
return this.ds_model.call(action_unsubscribe, [[this.view.datarecord.id], follower_ids, context])
.then(this.proxy('read_value'));
return this.ds_model.call(action_unsubscribe, [[this.view.datarecord.id],
follower_ids, context])
.then(this.proxy('read_value'));
}
return false;
},
do_update_subscription: function (event, user_pid) {
var self = this;
this.data_subtype = {};
var action_subscribe = 'message_subscribe_users';
var follower_ids = [session.uid];
var oe_action = this.$('.oe_actions input[type="checkbox"]');
if (user_pid) {
action_subscribe = 'message_subscribe';
follower_ids = [user_pid];
oe_action = $('.oe_edit_actions input[type="checkbox"]');
}
var checklist = [];
var checklist = new Array();
_(oe_action).each(function (record) {
if ($(record).is(':checked')) {
checklist.push(parseInt($(record).data('id')));
@@ -372,17 +407,23 @@ var Followers = form_common.AbstractField.extend({
if (!checklist.length) {
if (!this.do_unfollow(user_pid)) {
$(event.target).attr("checked", "checked");
} else {
self.$('.oe_subtype_list ul').empty();
}
} else {
else {
self.$('.o_timeline_subtype_list ul').empty();
}
}
else {
var context = new data.CompoundContext(this.build_context(), {});
return this.ds_model.call(action_subscribe, [[this.view.datarecord.id], follower_ids, checklist, context])
.then(this.proxy('read_value'));
return this.ds_model.call(action_subscribe, [[this.view.datarecord.id],
follower_ids,
checklist,
context])
.then(this.proxy('read_value'));
}
},
});
/* Add the widget to registry */
core.form_widget_registry.add('mail_followers', Followers);
});
});
+17
View File
@@ -107,6 +107,22 @@ function breakword(str){
return out;
}
function bindTooltipTo($el, value, position) {
$el.tooltip({
'title': value,
'placement': position,
'html': true,
'trigger': 'manual',
'animation': false,
}).on("mouseleave", function () {
setTimeout(function () {
if (!$(".tooltip:hover").length) {
$el.tooltip("hide");
}
}, 100);
});
}
return {
parse_email: parse_email,
get_image: get_image,
@@ -115,6 +131,7 @@ return {
get_text2html: get_text2html,
expand_domain: expand_domain,
breakword: breakword,
bindTooltipTo: bindTooltipTo,
};
});
+3 -3
View File
@@ -48,7 +48,7 @@ var Groups = Widget.extend({
if (this.groups.length === 0) {
return this.$el.empty();
}
return this.$el.empty().html(QWeb.render('mail.suggestions.groups', {'widget': this}));
return this.$el.empty().html(QWeb.render('TimelineSuggestionGroups', {'widget': this}));
},
join_group: function (event) {
@@ -72,7 +72,7 @@ var Groups = Widget.extend({
}
});
mail.WallSidebar.include({
mail.Sidebar.include({
start: function () {
this._super.apply(this, arguments);
var sug_groups = new Groups(this);
@@ -80,4 +80,4 @@ mail.WallSidebar.include({
},
});
});
});
+236 -335
View File
@@ -1,356 +1,257 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<!--
mail.Widget template used to namespace the css -->
<t t-name="mail.Root">
<div class="oe_mail">
</div>
</t>
<templates>
<!--
mail.compose_message template
This template holds the composition form to write a note or send
an e-mail. It contains by default a textarea, that will be replaced
by another composition form in the main wall composition form, or
for main thread composition form in document form view.
mail.compose_message.compact template
This template holds the composition form to write a message, this box is converted into
mail.compose_message when focus on textarea
-->
<t t-name="mail.compose_message">
<div t-if="widget.show_composer and !widget.options.readonly" t-attf-class="oe_msg oe_msg_composer #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''}">
<div class="oe_msg_left">
<img class="oe_msg_icon" alt="User img" t-attf-src="#{widget.avatar}"/>
</div>
<div class="oe_msg_center">
<div class="oe_msg_content">
<button class="oe_full" title="Open the full mail composer"><i class='fa fa-pencil-square-o'/></button>
<t t-call="mail.thread.list_recipients"/>
<textarea class="field_text"></textarea>
</div>
<div class="oe_msg_footer">
<div class="oe_msg_attachment_list"></div>
<button class="oe_post">
<t t-if="!widget.is_log">Send</t>
<t t-if="widget.is_log">Log an internal note</t>
</button>
<t t-call="mail.compose_message.add_attachment"/>
</div>
</div>
</div>
<div t-if="widget.show_compact_message and !widget.show_composer and !widget.options.readonly" t-attf-class="oe_msg oe_msg_composer_compact #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''}">
<t t-if="widget.options.view_mailbox">
<div class="field_text oe_compact oe_compact_inbox">
<t t-if="widget.options.compose_placeholder" t-raw="widget.options.compose_placeholder"/>
<t t-if="!widget.options.compose_placeholder and widget.options.view_mailbox">Share with my followers...</t>
</div>
</t>
<t t-if="!widget.options.view_mailbox">
<div class="field_text oe_compact oe_compact_record">
<a class="oe_compose_post" t-if="widget.options.compose_placeholder"><t t-raw="widget.options.compose_placeholder"/></a>
<a class="oe_compose_post" t-if="!widget.options.compose_placeholder and !widget.options.view_mailbox"
title="Send a message. Followers will be notified.">Send a message</a>
<t t-if="widget.options.display_log_button">
<span class="oe_grey oe_sep_word">or</span>
<a class="oe_compose_log" title="Log a note. Followers will not be notified.">Log an internal note</a>
<div t-name="TimelineView.buttons" class="o_timeline_buttons">
<t t-if="widget.options.action_buttons !== false">
<button type="button" class="o_timeline_button_new btn btn-primary btn-sm">
New message
</button>
<t t-if="!widget.options.readonly and widget.options.show_write_to_followers">
<span class="oe_alternative">
<span class="oe_fade">or</span>
<div class="oe_bounce_container">
<t t-if="widget.options.compose_placeholder">
<a class="o_timeline_share text-info"><t t-raw="widget.options.compose_placeholder"/></a>
</t>
<t t-if="! widget.options.compose_placeholder">
<a class="o_timeline_share text-info">Write to my followers</a>
</t>
</div>
</t>
</div>
<span t-if="!(widget.show_compact_message and !widget.show_composer) and !widget.show_composer" class="oe_placeholder_compose"></span>
</t>
<!--
mail.compose_message.add_attachment template
Small template to be inserted in the composition for add attachments
-->
<t t-name="mail.compose_message.add_attachment">
<span class="oe_attachment_file">
<span class="oe_add">
<!-- uploader of file -->
<button class="oe_attach"><i class="fa fa-paperclip"/></button>
<span class='oe_attach_label'>Attach a File</span>
<t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
<input type="hidden" name="model" value="mail.compose.message"/>
<input type="hidden" name="id" value="0"/>
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
</t>
</span>
</span>
</t>
<!--
mail.thread.message.attachments template
Template used to display attachments in a mail.message
-->
<t t-name="mail.thread.message.attachments">
<t t-foreach='widget.attachment_ids' t-as='attachment'>
<t t-if="attachment.file_type_icon !== 'webimage'">
<div t-attf-class="oe_attachment #{attachment.upload ? 'oe_uploading' : ''}">
<a t-att-href='attachment.url' target="_blank">
<img t-att-src="'/mail/static/src/img/mimetypes/' + attachment.file_type_icon + '.png'"></img>
<div class='oe_name'><t t-raw='attachment.name' /></div>
</a>
<div class="oe_delete" t-if="!attachment.upload">
<i class='fa fa-times-circle' title="Delete this attachment" t-att-data-id="attachment.id"/>
</div>
<div class='oe_progress_bar'>
uploading
</div>
</div>
</t>
<t t-if="attachment.file_type_icon === 'webimage'">
<div t-attf-class="oe_attachment oe_preview #{attachment.upload ? 'oe_uploading' : ''}">
<a t-att-href='attachment.url' target="_blank">
<img t-att-src="widget.attachments_resize_image(attachment.id, [100,80])"></img>
<div class='oe_name'><t t-raw='attachment.name' /></div>
</a>
<div class="oe_delete" t-if="!attachment.upload">
<i class='fa fa-times-circle' title="Delete this attachment" t-att-data-id="attachment.id"/>
</div>
<div class='oe_progress_bar'>
uploading
</div>
</div>
</t>
</t>
</t>
</div>
<!--
template to the recipients list
-->
<t t-name="mail.thread.list_recipients">
<div class="oe_mail_list_recipients" t-if="!widget.is_log">
To:
<t t-if="!widget.is_private">
<span class="oe_all_follower">
<t t-if="widget.parent_thread.parent_message.record_name">
Followers of <t t-raw="'&quot;' + widget.parent_thread.parent_message.record_name + '&quot;'"/>
</t>
<t t-if="!widget.parent_thread.parent_message.record_name and widget.options.view_inbox">My Followers</t>
<t t-if="!widget.parent_thread.parent_message.record_name and !widget.options.view_inbox">Followers of this document</t>
</span>
</t>
<t t-set="inc" t-value="0"/>
<t t-foreach="widget.partner_ids" t-as="partner">
<t t-if="widget.is_private or (widget.user_pid != partner[0])">
<t t-if="!widget.is_private and inc==0"> and </t>
<span t-attf-class="oe_partner_follower #{inc>=3?'oe_hidden':''}"><t t-if="inc" t-raw="', '"/>
<a t-if="widget.options.show_link" t-attf-href="#model=res.partner&amp;id=#{partner[0]}"><t t-esc="partner[1]"/></a>
<t t-if="!widget.options.show_link" t-esc="partner[1]"/>
</span>
<t t-set="inc" t-value="inc+1"/>
</t>
</t>
<t t-if="widget.partner_ids.length > 3">
<span class="oe_more">, <a><t t-esc="widget.partner_ids.length - 3"/> others...</a></span>
<a class="oe_more_hidden">&lt;&lt;&lt;</a>
</t>
<div t-name="TimelineWall" t-attf-class="o_timeline #{widget.options.view_inbox ? 'o_timeline_inbox' : 'o_timeline_chatter'}">
<div class="o_timeline_buttons"/>
<div class="o_timeline-placeholder"></div>
<t t-if="widget.options.view_inbox">
<aside class="o_timeline_inbox_aside"></aside>
</t>
</div>
<t t-name="ComposeMessage">
<div t-if="widget.show_composer and !widget.options.readonly"
class="o_timeline_msg o_timeline_msg_composer_compact">
<div class="o_timeline_msg o_timeline_msg_composer">
<div class="o_timeline_msg_center">
<div class="o_timeline_msg_content">
<button class="oe_full" title="Open the full mail composer">
<i class='fa fa-pencil-square-o'/>
</button>
<t t-call="ThreadListRecipients"/>
<textarea class="field_text"></textarea>
</div>
<div class="o_timeline_msg_footer">
<div class="o_timeline_msg_attachment_list"></div>
<div class="btn-group btn-group-sm">
<button type="button" class="oe_post btn btn-primary">
<t t-if="!widget.is_log">Send</t>
<t t-if="widget.is_log">Log</t>
</button>
<t t-call="ComposeMessage.AddAttachment"/>
</div>
</div>
</div>
</div>
<div class="oe_recipients" t-if="!widget.is_log">
</div>
<div t-if="widget.show_compact_message and !widget.show_composer and !widget.options.readonly"
class="o_timeline_msg o_timeline_msg_composer_compact">
<t t-if="!widget.options.view_inbox">
<div class="field_text o_timeline_compact o_timeline_compact_record">
<div class="btn-group btn-group-sm">
<button type="button" class="o_timeline_compose_post btn btn-primary"
t-if="widget.options.compose_placeholder">
<t t-raw="widget.options.compose_placeholder"/>
</button>
<button type="button" class="o_timeline_compose_post btn btn-primary"
t-if="!widget.options.compose_placeholder and !widget.options.view_inbox"
title="Send a message. Followers will be notified.">
New message
</button>
</div>
<t t-if="widget.options.display_log_button">
<span class="text-muted o_timeline_sep_word">or</span>
<div class="btn-group btn-group-sm">
<button class="o_timeline_compose_log btn btn-link btn-small"
title="Log a note. Followers will not be notified.">
Log an internal node
</button>
</div>
</t>
</div>
</t>
</div>
<span t-if="!(widget.show_compact_message and !widget.show_composer) and !widget.show_composer"
class="oe_placeholder_compose"></span>
</t>
<!--
ComposeMessage.AddAttachment template
Small template to be inserted in the composition for add attachments
-->
<t t-name="ComposeMessage.AddAttachment">
<span class="oe_attachment_file">
<span class="oe_add">
<!-- uploader of file -->
<button class="oe_attach"><i class="fa fa-paperclip"/></button>
<span class='oe_attach_label'>Attach a file</span>
<t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
<input type="hidden" name="model" value="mail.compose.message"/>
<input type="hidden" name="id" value="0"/>
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
</t>
</span>
</span>
</t>
<!--
template to the recipients list
-->
<t t-name="ThreadListRecipients">
<div class="o_timeline_list_recipients" t-if="!widget.is_log">
To:
<t t-if="!widget.is_private">
<span class="o_timeline_all_follower">
<t t-if="widget.parent_thread.parent_message.record_name">
Followers of
<t t-raw="'&quot;' + widget.parent_thread.parent_message.record_name + '&quot;'"/>
</t>
<t t-if="!widget.parent_thread.parent_message.record_name and widget.options.view_mailbox">
My Followers </t>
<t t-if="!widget.parent_thread.parent_message.record_name and !widget.options.view_mailbox">
Followers of this document </t>
</span>
</t>
<t t-set="inc" t-value="0"/>
<t t-foreach="widget.partner_ids" t-as="partner">
<t t-if="widget.is_private or (widget.user_pid != partner[0])">
<t t-if="!widget.is_private and inc==0"> and </t>
<span t-attf-class="o_timeline_partner_follower #{inc>=3?'oe_hidden':''}">
<t t-if="inc" t-raw="', '"/>
<a t-if="widget.options.show_link"
t-attf-href="#model=res.partner&amp;id=#{partner[0]}">
<t t-esc="partner[1]"/>
</a>
<t t-if="!widget.options.show_link" t-esc="partner[1]"/>
</span>
<t t-set="inc" t-value="inc+1"/>
</t>
</t>
<t t-if="widget.partner_ids.length > 3">
<span class="oe_more">
, <a><t t-esc="widget.partner_ids.length - 3"/> others...</a>
</span>
</t>
<div class="o_timeline_recipients" t-if="!widget.is_log">
<t t-foreach='widget.recipients' t-as='recipient'>
<label t-attf-title="Add as recipient and follower (reason: #{recipient.reason})">
<input type="checkbox" t-att-checked="recipient.checked ? 'checked' : undefined" t-att-data="recipient.full_name"/>
<input type="checkbox"
t-att-checked="recipient.checked ? 'checked' : undefined"
t-att-data="recipient.full_name"/>
<t t-esc="recipient.name"/>
<t t-if="recipient.email_address">(<t t-esc="recipient.email_address"/>)</t>
<t t-if="!recipient.email_address">(no email address)</t>
</label>
</t>
</div>
<div t-if="widget.is_log">
<span>Log an internal note which will not be sent to followers. Note that users reading the document can read internal notes.</span>
</div>
</t>
<!--
wall main template
Template used to display the communication history in the wall.
-->
<div t-name="mail.wall" class="oe-view-manager oe_mail_wall oe_view_manager_current">
<div class="oe-control-panel">
<div class="container-fluid">
<div class="row">
<div class="col-md-6 oe-cp-title">
<ol class="oe-view-title breadcrumb">
<li class="active"><t t-esc="widget.action.name"/></li>
</ol>
</div>
<div class="oe-cp-search-view col-md-6" />
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-6">
<div class="oe-search-options btn-group"/>
</div>
</div>
</div>
</div>
<div class="oe-view-manager-content">
<div class="oe_mail-placeholder"></div>
<aside class="oe_mail_wall_aside"></aside>
</div>
</div>
<!--
Empty template that holds the sidebar of the Wall
-->
<t t-name="mail.wall.sidebar">
<div class="oe_mail_wall_sidebar"></div>
</t>
<!--
display message on the wall when there are no message
-->
<t t-name="mail.wall_no_message">
<div class="oe_view_nocontent">No messages.</div>
</t>
<!--
error message for uploading
-->
<t t-name="mail.error_upload">Uploading error</t>
<t t-name="mail.error_upload_please_wait">Please, wait while the file is uploading.</t>
<!--
record_thread main template
Template used to display the communication history in documents
form view.
-->
<div t-name="mail.record_thread" class="oe_record_thread">
<div class="oe_mail-placeholder">
</div>
</div>
<t t-name="mail.thread">
<div t-attf-class="oe_thread #{widget.root?'oe_root_thread':''}"/>
</t>
<!-- default layout -->
<t t-name="mail.thread.message">
<div t-attf-class="oe_msg #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''} #{widget.subtype ? '' : 'oe_msg_nobody'} oe_msg_#{widget.message_type}">
<div class='oe_msg_left'>
<a t-if="widget.options.show_link" t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}" t-att-title="widget.author_id[1]">
<img class="oe_msg_icon" t-att-src="widget.avatar"/>
</a>
<img t-if="!widget.options.show_link" class="oe_msg_icon" t-att-src="widget.avatar"/>
</div>
<div class="oe_msg_center">
<div class='oe_msg_icons' t-if="!widget.options.readonly">
<span class='oe_read' t-if="widget.options.show_read"><i title="Done" class="fa fa-check"/></span>
<span class='oe_unread' t-if="widget.is_favorite and widget.options.show_unread"><i title="Set back to Todo" class="fa fa-inbox"/></span>
<span class='oe_unread' t-if="!widget.is_favorite and widget.options.show_unread"><i title="Move to Inbox" class="fa fa-inbox"/></span>
<span class='oe_reply' t-if="widget.options.show_reply_button"><i title="Reply" class="fa fa-reply"/></span>
<span t-attf-class="oe_star #{widget.is_favorite?'oe_starred':''}"><i title="Mark as Todo" class="fa fa-star"/></span>
</div>
<!-- message itself -->
<div class="oe_msg_content">
<h1 t-if="(widget.show_record_name or widget.subject) and !widget.thread_level" class="oe_msg_title">
<a t-if="widget.options.show_link and widget.show_record_name" class="oe_mail_action_model"
t-attf-href="#action=mail.action_mail_redirect&amp;model=#{widget.model}&amp;res_id=#{widget.res_id}">
<t t-esc="widget.record_name"/>
</a>
<span t-if="!widget.options.show_link and widget.show_record_name"><t t-esc="widget.record_name"/></span>
<t t-if="widget.show_record_name and widget.subject">: </t>
<t t-if="widget.subject" t-esc="widget.subject"/>
</h1>
<div class="oe_msg_body">
<t t-if="widget.body_short">
<div class="oe_msg_body_short"><t t-raw="widget.body_short"/></div>
<div class="oe_msg_body_long" style="display: none;"><t t-raw="widget.body"/><span class="oe_mail_reduce"><a href="#">read less</a></span></div>
</t>
<t t-if="! widget.body_short">
<t t-raw="widget.body"/>
</t>
</div>
</div>
<div class="oe_msg_footer">
<t t-if="widget.attachment_ids.length > 0">
<div class="oe_msg_attachment_list"></div>
</t>
<a t-if="widget.author_id and widget.options.show_link and widget.author_id[0]" t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}" t-att-data-partner="widget.author_id[0]" class="oe_mail_action_author"><t t-esc="widget.author_id[2]"/></a>
<span t-if="widget.author_id and (!widget.options.show_link or !widget.author_id[0])"><t t-esc="widget.author_id[2]"/></span>
<t t-if="widget.message_type == 'notification'">
updated document
<t t-if="widget.partner_ids.length > 0">
<span class='oe_subtle'>•</span>
</t>
</t>
<t t-if="widget.message_type == 'comment' and ! widget.subtype and widget.partner_ids.length == 0">
logged a note
</t>
<t t-if="(widget.message_type == 'comment' or widget.message_type == 'email') and (widget.subtype or widget.partner_ids.length > 0)">
<!-- Remove nobody if user have no rights on partner that will display 'portal to nobody 5 minutes ago' that will confuse to end user, now it display 'portal 5 minutes ago' if no rights.-->
<t t-if="widget.partner_ids.length > 0">
to
</t>
</t>
<t t-if="widget.message_type == 'notification' or ( (widget.message_type == 'email' or widget.message_type == 'comment') and (widget.subtype or widget.partner_ids.length > 0))"
t-foreach="widget.partner_ids.slice(0, 3)" t-as="partner">
<span t-attf-class="oe_partner_follower">
<a t-if="widget.options.show_link" t-attf-href="#model=res.partner&amp;id=#{partner[0]}" t-att-data-partner="partner[0]" class="oe_mail_action_author"><t t-esc="partner[1]"/></a>
<t t-if="!widget.options.show_link" t-esc="partner[1]"/>
</span>
<t t-if="!partner_last">,</t>
</t>
<t t-if="widget.partner_ids.length > 3">
<span t-att-title="widget.extra_partners_str">and <t t-esc="widget.extra_partners_nbr"/> more</span>
</t>
<t t-if="widget.message_type == 'notification' and widget.partner_ids.length > 0">
notified
</t>
<span class='oe_subtle'>•</span>
<span t-att-title="widget.date">
<t t-if="widget.timerelative" t-esc="widget.timerelative"/>
<t t-if="!widget.timerelative" t-raw="widget.display_date"/>
</span>
<span t-if="!widget.options.readonly" class='oe_subtle'>•</span>
<t t-if="!widget.options.readonly" t-call="mail.thread.message.vote"/>
</div>
</div>
</div>
</t>
<!-- expandable message layout -->
<t t-name="mail.thread.expandable">
<div t-attf-class="oe_msg oe_msg_#{widget.message_type} #{widget.max_limit ? 'oe_max_limit' : ''} #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''}">
<div class="oe_msg_content oe_msg_more_message">
<div class='oe_separator'></div>
<a t-if="widget.nb_messages &lt;= 0" class="oe_msg_fetch_more">show more message</a>
<a t-if="widget.nb_messages === 1" class="oe_msg_fetch_more">show one more message</a>
<a t-if="widget.nb_messages &gt; 1" class="oe_msg_fetch_more">show <t t-esc="widget.nb_messages" /> more messages</a>
</div>
</div>
</t>
<!--
mail.compose_message.button_top_bar
render of the button on the user bar for open wizard compose message
-->
<t t-name="mail.compose_message_top_button">
<li title='Compose new Message' class="oe_topbar_item oe_topbar_compose_full_email">
<a href="#">
<i class="fa fa-envelope-o fa-fw"></i>
</a>
</li>
</t>
<!-- mail.thread.message.vote
Template used to display Like/Unlike in a mail.message
-->
<span t-name="mail.thread.message.vote">
<span class="oe_mail_vote_count" t-if='widget.vote_nb > 0'>
<t t-esc='widget.vote_nb' />
<i class="fa fa-thumbs-o-up"></i>
<div class="o_timeline_is_log" t-if="widget.is_log">
<span class="text-muted">
Log an internal note which will not be sent to followers.
Note that users reading the document can read internal notes.
</span>
<a href='#' class="oe_msg_vote">
<t t-if="!widget.has_voted">like</t>
<t t-if="widget.has_voted">unlike</t>
</a>
</span>
</div>
</t>
</template>
<div t-name="MailThread" class="oe_thread">
<div t-attf-class="#{widget.options.view_inbox ? 'o_timeline_thread_parent' : ''}">
<t t-raw="widget.content"/>
</div>
<t t-if="!widget.messages.length and !widget.root">
<t t-call="NoMessage" />
</t>
</div>
<!--
mail.thread.message.attachments template
Template used to display attachments in a mail.message
-->
<t t-name="ThreadMessageAttachments">
<t t-foreach='widget.format_attachments(record)' t-as='attachment'>
<t t-if="attachment.file_type_icon !== 'webimage'">
<div t-attf-class="oe_attachment #{attachment.upload ? 'oe_uploading' : ''}">
<a t-att-href='attachment.url' target="_blank">
<img t-att-src="'/mail/static/src/img/mimetypes/' + attachment.file_type_icon + '.png'"></img>
<div class='oe_name'><t t-raw='attachment.name' /></div>
</a>
<div class='oe_delete oe_e'>
<i class='fa fa-times-circle' title="Delete this attachment" t-att-data-id="attachment.id"/>
</div>
<div class='oe_progress_bar'>
uploading
</div>
</div>
</t>
<t t-if="attachment.file_type_icon === 'webimage'">
<div t-attf-class="oe_attachment oe_preview #{attachment.upload ? 'oe_uploading' : ''}">
<a t-att-href='attachment.url' target="_blank">
<img t-att-src="widget.attachments_resize_image(attachment.id, [100,80])"></img>
<div class='oe_name'><t t-raw='attachment.name'/></div>
</a>
<div class='oe_delete oe_e'>
<i class='fa fa-times-circle' title="Delete this attachment" t-att-data-id="attachment.id"/>
</div>
<div class='oe_progress_bar'>
uploading
</div>
</div>
</t>
</t>
</t>
<t t-name="MessageReadUnread">
<i t-if="record.options.show_read" title="Done"
class="oe_read fa fa-check" t-att-data-id="record.id"/>
<i t-if="record.is_favorite and record.options.show_unread" title="Set back to Todo"
class="oe_unread fa fa-inbox" t-att-data-id="record.id"/>
<i t-if="!record.is_favorite and record.options.show_unread" title="Move to Inbox"
class="oe_unread fa fa-inbox" t-att-data-id="record.id"/>
</t>
<t t-name="MessageVote">
<span class="o_timeline_vote_count" t-att-data-id="record.id" t-if='record.vote_nb > 0'>
<t t-esc='record.vote_nb'/><i class="fa fa-thumbs-o-up" t-att-data-id="record.id"></i>
</span>
<t t-if="!record.has_voted">Like</t>
<t t-if="record.has_voted">Unlike</t>
</t>
<t t-name="NoMessage">
<div class="oe_view_nocontent">No message.</div>
</t>
<t t-name="TimelineSidebar">
<div class="o_timeline_sidebar"></div>
</t>
<!--
ComposeMessage.ButtonTopBar
render of the button on the user bar for open wizard compose message
-->
<t t-name="ComposeMessageTopButton">
<li title='Compose new Message' class="oe_topbar_item oe_topbar_compose_full_email">
<a href="#">
<i class="fa fa-envelope-o fa-fw"></i>
</a>
</li>
</t>
</templates>
+73 -60
View File
@@ -1,65 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<templates>
<!--
followers main template
Template used to display the followers, the actions and the subtypes in a record.
-->
<div t-name="mail.followers" class="oe_followers">
<div class="oe_actions">
<div t-attf-class="btn-group oe_subtype_list">
<button class="btn oe_follower oe_notfollow">
<span class="oe_follow">Follow</span>
<span class="oe_unfollow">Unfollow</span>
<span class="oe_following">Following</span>
</button>
<button type="button" t-attf-class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu"></ul>
</div>
<t t-if="widget.comment">
<h5 class="oe_comment"><t t-raw="widget.comment"/></h5>
</t>
</div>
<div class='oe_follower_title_box'>
<h4 class='oe_follower_title'>Followers</h4>
<a t-if="widget.view_is_editable" href='#' class="oe_invite">Add others</a>
</div>
<div class="oe_follower_list"></div>
</div>
<!--
followers.partner template
Template used to display a partner following the record
-->
<div t-name="mail.followers.partner" class='oe_partner'>
<img class="oe_mail_thumbnail oe_mail_frame" t-att-src="record.avatar_url"/>
<a t-attf-href="#model=res.partner&amp;id=#{record.id}" t-att-title="record.name" t-att-data-partner="record.id"><t t-esc="record.name"/></a>
<i t-if="record.is_editable and (widget.records_length &gt; 1)" class="oe_edit_subtype fa fa-pencil oe_hidden" title="Edit subscription" t-att-data-id="record.id"/>
<i t-if="widget.view_is_editable" class="oe_remove_follower fa fa-remove" title="Remove this follower" t-att-data-id="record.id"/>
</div>
<!--
followers.subtype template
Template used to display message subtypes of a follower subscription
<!--
followers main template
Template used to display the followers, the actions and the subtypes in a record.
-->
<t t-name="mail.followers.subtype">
<table class='oe_subtype'>
<tr>
<td width="10%">
<input type="checkbox" t-att-checked="record.followed" t-att-id="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')" t-att-data-id="record.id" t-att-name="record.name" class="oe_msg_subtype_check"/>
</td>
<td>
<label t-att-for="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')"><t t-raw="record.name"/></label>
</td>
</tr>
</table>
</t>
<div t-name="mail_followers" class="o_timeline_followers">
<div t-attf-class="btn-group-sm o_timeline_follower_title_box">
<button t-attf-class="btn btn-default dropdown-toggle o_timeline_partner_list"
data-toggle="dropdown" aria-expanded="false">
<span class="o_timeline_follower_title">Followers</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu o_timeline_follower_list" role="menu"></ul>
</div>
<div class="oe_actions">
<div t-attf-class="btn-group-sm o_timeline_subtype_list">
<button class="btn btn-default o_timeline_follower o_timeline_notfollow">
<span class="o_timeline_follow">Follow</span>
<span class="glyphicon glyphicon-remove o_timeline_unfollow" aria-hidden="true"> Unfollow</span>
<span class="glyphicon glyphicon-ok o_timeline_following" aria-hidden="true"> Following</span>
</button>
<button t-attf-class="btn btn-default dropdown-toggle o_timeline_sbtp_list"
data-toggle="dropdown" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu oe_subtype" role="menu"></ul>
</div>
<t t-name="mail.followers.show_more">
<div class="oe_partner oe_show_more">And <t t-esc="number"/> more.</div>
</t>
<!--<t t-if="widget.comment">
<h5 class="oe_comment"><t t-raw="widget.comment"/></h5>
</t>-->
</div>
</div>
</template>
<!--
followers.partner template
Template used to display a partner following the record
-->
<li t-name="mail_followers_partner" class='oe_partner'>
<span> <img class="oe_mail_thumbnail oe_mail_frame" t-att-src="record.avatar_url"/> </span>
<span class="oe_partner_name">
<a t-attf-href="#model=res.partner&amp;id=#{record.id}"
t-att-title="record.name" t-att-data-partner="record.id">
<t t-esc="record.name"/> </a>
</span>
<i t-if="record.is_editable and (widget.records_length &gt; 1)"
class="oe_edit_subtype fa fa-pencil oe_hidden" title="Edit subscription"
t-att-data-id="record.id"/>
<i t-if="widget.view_is_editable" class="oe_remove_follower fa fa-remove"
title="Remove this follower" t-att-data-id="record.id"/>
</li>
<t t-name="mail_followers_add_more">
<li t-if="widget.view_is_editable" class="oe_invite">
<a href="#"> Add followers </a>
</li>
<li t-if="widget.view_is_editable and (widget.followers.length > 0)" class="divider"></li>
</t>
<!--
followers.subtype template
Template used to display message subtypes of a follower subscription
-->
<t t-name="mail_followers_subtype">
<li>
<input type="checkbox" t-att-checked="record.followed"
t-att-id="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')"
t-att-data-id="record.id" t-att-name="record.name" class="o_timeline_msg_subtype_check"/>
<label t-att-for="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')">
<t t-raw="record.name"/>
</label>
</li>
</t>
</templates>
+8 -8
View File
@@ -2,18 +2,17 @@
<template>
<!-- Groups placeholder in sidebar -->
<t t-extend="mail.wall.sidebar">
<t t-jquery=".oe_mail_wall_sidebar" t-operation="append">
<t t-extend="TimelineSidebar">
<t t-jquery=".o_timeline_sidebar" t-operation="append">
<div class="oe_suggestions_groups"></div>
</t>
</t>
<!-- Suggested groups -->
<div t-name="mail.suggestions.groups" class="oe_sidebar_suggestion oe_suggestion_group">
<div t-name="TimelineSuggestionGroups" class="oe_sidebar_suggestion oe_suggestion_group">
<div class="oe_suggest_title">
<i class="oe_suggestion_remove oe_suggestion_group fa fa-remove"/>
<h2>Suggested Groups</h2>
<span>Suggested Groups</span>
</div>
<div class="oe_suggest_items">
<t t-foreach="widget.groups" t-as="result">
@@ -27,12 +26,13 @@
<a class="oe_suggestion_item_name" t-attf-href="#model=mail.group&amp;id=#{result.id}"><t t-esc="result.name"/></a>
<i class="oe_suggestion_remove_item oe_suggestion_group fa fa-remove" t-att-id="result.id"/>
<br/>
<button t-att-id="result.id" class="oe_suggestion_join">Join Group</button>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-default oe_suggestion_join" t-att-id="result.id">Join Group</button>
</div>
</div>
</div>
</t>
</div>
</div>
</template>
</template>
+9 -9
View File
@@ -67,12 +67,12 @@ class TestMailFeatures(TestMail):
inbox_act_id = self.ref('mail.action_mail_inbox_feeds')
action = self.env['mail.thread'].with_context({'params': {}}).sudo(self.user_employee).message_redirect_action()
self.assertEqual(
action.get('type'), 'ir.actions.client',
'URL redirection: action without parameters should redirect to client action Inbox'
action.get('type'), 'ir.actions.act_window',
'URL redirection: action without parameters should redirect to action act window Inbox'
)
self.assertEqual(
action.get('id'), inbox_act_id,
'URL redirection: action without parameters should redirect to client action Inbox'
'URL redirection: action without parameters should redirect to action act window Inbox'
)
def test_inbox_redirection_document(self):
@@ -114,12 +114,12 @@ class TestMailFeatures(TestMail):
'params': {'message_id': message.id}
}).sudo(self.user_public).message_redirect_action()
self.assertEqual(
action.get('type'), 'ir.actions.client',
'URL redirection: action without parameters should redirect to client action Inbox'
action.get('type'), 'ir.actions.act_window',
'URL redirection: action without parameters should redirect to action act window Inbox'
)
self.assertEqual(
action.get('id'), inbox_act_id,
'URL redirection: action without parameters should redirect to client action Inbox'
'URL redirection: action without parameters should redirect to action act window Inbox'
)
@mute_logger('openerp.models')
@@ -130,12 +130,12 @@ class TestMailFeatures(TestMail):
'params': {'model': 'mail.group', 'res_id': self.group_pigs.id}
}).sudo(self.user_public).message_redirect_action()
self.assertEqual(
action.get('type'), 'ir.actions.client',
'URL redirection: action without parameters should redirect to client action Inbox'
action.get('type'), 'ir.actions.act_window',
'URL redirection: action without parameters should redirect to action act window Inbox'
)
self.assertEqual(
action.get('id'), inbox_act_id,
'URL redirection: action without parameters should redirect to client action Inbox'
'URL redirection: action without parameters should redirect to action act window Inbox'
)
@mute_logger('openerp.addons.mail.models.mail_mail')
+73 -63
View File
@@ -17,68 +17,72 @@ class TestMessageRead(TestMail):
self.msg_4 = self.group_pigs.message_post(body='2-1', subtype='mt_comment', parent_id=self.msg_2.id)
self.msg_5 = self.group_pigs.message_post(body='1-2', subtype='mt_comment', parent_id=self.msg_1.id)
self.msg_6 = self.group_pigs.message_post(body='2-2', subtype='mt_comment', parent_id=self.msg_2.id)
self.msg_7 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=self.msg_3.id)
self.msg_8 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=self.msg_4.id)
self.msg_9 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=self.msg_3.id)
self.msg_10 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=self.msg_4.id)
self.msg_7 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=self.msg_1.id)
self.msg_8 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=self.msg_2.id)
self.msg_9 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=self.msg_1.id)
self.msg_10 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=self.msg_2.id)
self.msg_ids = [self.msg_10.id, self.msg_9.id, self.msg_8.id, self.msg_7.id, self.msg_6.id, self.msg_5.id, self.msg_4.id, self.msg_3.id, self.msg_2.id, self.msg_1.id, self.msg_0.id]
self.ordered_msg_ids = [self.msg_2.id, self.msg_4.id, self.msg_6.id, self.msg_8.id, self.msg_10.id, self.msg_1.id, self.msg_3.id, self.msg_5.id, self.msg_7.id, self.msg_9.id, self.msg_0.id]
self.ordered_msg_ids = [self.msg_10.id, self.msg_8.id, self.msg_6.id, self.msg_4.id, self.msg_2.id, self.msg_9.id, self.msg_7.id, self.msg_5.id, self.msg_3.id, self.msg_1.id, self.msg_0.id]
# TODO: with_context({'mail_read_set_read': True})
def test_message_read_ids(self):
""" message_read: test reading specific ids """
messages = self.env['mail.message'].sudo(self.user_employee).browse(self.msg_ids[2:4]).message_read(domain=[('body', 'like', 'dummy')])
read_msg_ids = [msg.get('id') for msg in messages]
self.assertEqual(read_msg_ids, self.msg_ids[2:4], 'message_read with direct ids should read only the requested ids')
read_msg_list = messages['threads'][0]
msg_ids = [msg.get('id') for msg in read_msg_list]
self.assertEqual(msg_ids, self.msg_ids[2:4], 'message_read with direct ids should read only the requested ids')
def test_message_read_domain(self):
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], limit=200)
read_msg_ids = [msg.get('id') for msg in messages]
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], limit=200, child_limit=200)
read_msg_list = [msg for thread in messages['threads'] for msg in thread]
read_msg_ids = [msg.get('id') for msg in read_msg_list]
self.assertEqual(read_msg_ids, self.msg_ids, 'message_read flat with domain on Pigs should equal all messages of Pigs')
def test_message_read_domain_thread(self):
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], limit=200, thread_level=1)
read_msg_ids = [msg.get('id') for msg in messages]
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], thread_level=1, limit=200, child_limit=200)
read_msg_list = [msg for thread in messages['threads'] for msg in thread]
read_msg_ids = [msg.get('id') for msg in read_msg_list]
self.assertEqual(read_msg_ids, self.ordered_msg_ids, 'message_read threaded with domain on Pigs should equal all messages of Pigs, and sort them with newer thread first, last message last in thread')
def test_message_read_expandable(self):
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], limit=1, thread_level=1)
read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable']
# TDE TODO: test expandables order
type_list = map(lambda item: item.get('type'), messages)
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], thread_level=1, limit=1, child_limit=1)
read_msg_list = [msg for thread in messages['threads'] for msg in thread]
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable']
read_ancestor_ids = [msg.get('parent_id') if msg.get('parent_id') != 0 else msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable']
type_list = [msg.get('message_type') for msg in read_msg_list]
# Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
self.assertEqual(len(messages), 4, 'message_read on last Pigs message should return 2 messages and 2 expandables')
self.assertEqual(len(type_list), 4, 'message_read on last Pigs message should return 2 messages and 2 expandables')
self.assertEqual(set([self.msg_2.id, self.msg_10.id]), set(read_msg_ids), 'message_read on the last Pigs message should also get its parent')
self.assertEqual(messages[1].get('parent_id'), messages[0].get('id'), 'message_read should set the ancestor to the thread header')
self.assertEqual(read_ancestor_ids[0], read_ancestor_ids[1], 'message_read should set the ancestor to the thread header')
# Data: get expandables
new_threads_exp, new_msg_exp = None, None
for msg in messages:
if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('max_limit'):
new_threads_exp = msg
elif msg.get('type') == 'expandable':
new_threads_exp = read_msg_list[-1]
for msg in read_msg_list:
if msg.get('parent_id') and msg.get('message_type') == 'expandable':
new_msg_exp = msg
# Do: fetch new messages in first thread, domain from expandable
self.assertIsNotNone(new_msg_exp, 'message_read on last Pigs message should have returned a new messages expandable')
domain = new_msg_exp.get('domain', [])
# Test: expandable, conditions in domain
self.assertIn(('id', 'child_of', self.msg_2.id), domain, 'new messages expandable domain should contain a child_of condition')
self.assertIn(('id', '>=', self.msg_4.id), domain, 'new messages expandable domain should contain an id greater than condition')
self.assertIn(('id', '<=', self.msg_8.id), domain, 'new messages expandable domain should contain an id less than condition')
self.assertIn(('parent_id', '=', self.msg_2.id), domain, 'new messages expandable domain should contain a child_of condition')
self.assertIn(('id', '>', self.msg_2.id), domain, 'new messages expandable domain should contain an id greater than condition')
self.assertIn(('id', '<', self.msg_10.id), domain, 'new messages expandable domain should contain an id less than condition')
self.assertEqual(new_msg_exp.get('parent_id'), self.msg_2.id, 'new messages expandable should have parent_id set to the thread header')
# Do: message_read with domain, thread_level=0, parent_id=msg_id2 (should be imposed by JS), 2 messages
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, limit=2, thread_level=0, parent_id=self.msg_2.id)
read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable']
new_msg_exp = [msg for msg in messages if msg.get('type') == 'expandable'][0]
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, thread_level=0, limit=2)
read_msg_list = [msg for thread in messages['threads'] for msg in thread]
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable']
new_msg_exp = [msg for msg in read_msg_list if msg.get('message_type') == 'expandable'][0]
# Test: structure content, 2 messages and 1 thread expandable
self.assertEqual(len(messages), 3, 'message_read in Pigs thread should return 2 messages and 1 expandables')
self.assertEqual(len(read_msg_list), 3, 'message_read in Pigs thread should return 2 messages and 1 expandables')
self.assertEqual(set([self.msg_6.id, self.msg_8.id]), set(read_msg_ids), 'message_read in Pigs thread should return 2 more previous messages in thread')
# Do: read the last message
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=new_msg_exp.get('domain'), limit=2, thread_level=0, parent_id=self.msg_2.id)
read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable']
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=new_msg_exp.get('domain'), thread_level=0, limit=2)
read_msg_list = [msg for thread in messages['threads'] for msg in thread]
read_msg_ids = [msg.get('id') for msg in read_msg_list]
# Test: structure content, 1 message
self.assertEqual(len(messages), 1, 'message_read in Pigs thread should return 1 message')
self.assertEqual(len(read_msg_list), 1, 'message_read in Pigs thread should return 1 message')
self.assertEqual(set([self.msg_4.id]), set(read_msg_ids), 'message_read in Pigs thread should return the last message in thread')
# Do: fetch a new thread, domain from expandable
self.assertIsNotNone(new_threads_exp, 'message_read on last Pigs message should have returned a new threads expandable')
@@ -88,33 +92,35 @@ class TestMessageRead(TestMail):
self.assertIn(condition, domain, 'new threads expandable domain should contain the message_read domain parameter')
self.assertFalse(new_threads_exp.get('parent_id'), 'new threads expandable should not have an parent_id')
# Do: message_read with domain, thread_level=1 (should be imposed by JS)
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, limit=1, thread_level=1)
read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable']
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, thread_level=1, limit=1, child_limit=1)
read_msg_list = [msg for thread in messages['threads'] for msg in thread]
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable']
read_ancestor_ids = [msg.get('parent_id') if msg.get('parent_id') != 0 else msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable']
type_list = [msg.get('message_type') for msg in read_msg_list]
# Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
self.assertEqual(len(messages), 4, 'message_read on Pigs should return 2 messages and 2 expandables')
self.assertEqual(len(type_list), 4, 'message_read on Pigs should return 2 messages and 2 expandables')
self.assertEqual(set([self.msg_1.id, self.msg_9.id]), set(read_msg_ids), 'message_read on a Pigs message should also get its parent')
self.assertEqual(messages[1].get('parent_id'), messages[0].get('id'), 'message_read should set the ancestor to the thread header')
self.assertEqual(read_ancestor_ids[0], read_ancestor_ids[1], 'message_read should set the ancestor to the thread header')
# Data: get expandables
new_threads_exp, new_msg_exp = None, None
for msg in messages:
if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('max_limit'):
new_threads_exp = msg
elif msg.get('type') == 'expandable':
new_threads_exp = read_msg_list[-1]
for msg in read_msg_list:
if msg.get('parent_id') and msg.get('message_type') == 'expandable':
new_msg_exp = msg
# Do: fetch new messages in second thread, domain from expandable
self.assertIsNotNone(new_msg_exp, 'message_read on Pigs message should have returned a new messages expandable')
domain = new_msg_exp.get('domain', [])
# Test: expandable, conditions in domain
self.assertIn(('id', 'child_of', self.msg_1.id), domain, 'new messages expandable domain should contain a child_of condition')
self.assertIn(('id', '>=', self.msg_3.id), domain, 'new messages expandable domain should contain an id greater than condition')
self.assertIn(('id', '<=', self.msg_7.id), domain, 'new messages expandable domain should contain an id less than condition')
self.assertIn(('parent_id', '=', self.msg_1.id), domain, 'new messages expandable domain should contain a child_of condition')
self.assertIn(('id', '>', self.msg_1.id), domain, 'new messages expandable domain should contain an id greater than condition')
self.assertIn(('id', '<', self.msg_9.id), domain, 'new messages expandable domain should contain an id less than condition')
self.assertEqual(new_msg_exp.get('parent_id'), self.msg_1.id, 'new messages expandable should have ancestor_id set to the thread header')
# Do: message_read with domain, thread_level=0, parent_id=msg_id1 (should be imposed by JS)
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, limit=200, thread_level=0, parent_id=self.msg_1.id)
read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable']
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, thread_level=0, limit=200)
read_msg_list = [msg for thread in messages['threads'] for msg in thread]
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable']
# Test: other message in thread have been fetch
self.assertEqual(set([self.msg_3.id, self.msg_5.id, self.msg_7.id]), set(read_msg_ids), 'message_read on the last Pigs message should also get its parent')
# Test: fetch a new thread, domain from expandable
self.assertIsNotNone(new_threads_exp, 'message_read should have returned a new threads expandable')
domain = new_threads_exp.get('domain', [])
@@ -122,10 +128,12 @@ class TestMessageRead(TestMail):
for condition in [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)]:
self.assertIn(condition, domain, 'general expandable domain should contain the message_read domain parameter')
# Do: message_read with domain, thread_level=1 (should be imposed by JS)
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, limit=1, thread_level=1)
read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable']
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, thread_level=1, limit=1, child_limit=1)
read_msg_list = [msg for thread in messages['threads'] for msg in thread]
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable']
type_list = [msg.get('message_type') for msg in read_msg_list]
# Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
self.assertEqual(len(messages), 1, 'message_read on Pigs should return 1 message because everything else has been fetched')
self.assertEqual(len(type_list), 1, 'message_read on Pigs should return 1 message because everything else has been fetched')
self.assertEqual([self.msg_0.id], read_msg_ids, 'message_read after 2 More should return only 1 last message')
# ----------------------------------------
@@ -133,28 +141,30 @@ class TestMessageRead(TestMail):
# ----------------------------------------
# Do: read 2 lasts message, flat
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], limit=2, thread_level=0)
read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable']
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], thread_level=0, limit=2)
read_msg_list = [msg for thread in messages['threads'] for msg in thread]
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable']
# Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is not set, 1 expandable
self.assertEqual(len(messages), 3, 'message_read on last Pigs message should return 2 messages and 1 expandable')
self.assertEqual(len(read_msg_list), 3, 'message_read on last Pigs message should return 2 messages and 1 expandable')
self.assertEqual(set([self.msg_9.id, self.msg_10.id]), set(read_msg_ids), 'message_read flat on Pigs last messages should only return those messages')
self.assertFalse(messages[0].get('parent_id'), 'message_read flat should set the ancestor as False')
self.assertFalse(messages[1].get('parent_id'), 'message_read flat should set the ancestor as False')
self.assertFalse(read_msg_list[0].get('parent_id'), 'message_read flat should set the ancestor as False')
self.assertFalse(read_msg_list[1].get('parent_id'), 'message_read flat should set the ancestor as False')
# Data: get expandables
new_threads_exp, new_msg_exp = None, None
for msg in messages:
if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('max_limit'):
new_threads_exp = msg
new_msg_exp = None
for msg in read_msg_list:
if msg.get('message_type') == 'expandable':
new_msg_exp = msg
# Do: fetch new messages, domain from expandable
self.assertIsNotNone(new_threads_exp, 'message_read flat on the 2 last Pigs messages should have returns a new threads expandable')
domain = new_threads_exp.get('domain', [])
self.assertIsNotNone(new_msg_exp, 'message_read flat on the 2 last Pigs messages should have returns a new threads expandable')
domain = new_msg_exp.get('domain', [])
# Test: expandable, conditions in domain
for condition in [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)]:
self.assertIn(condition, domain, 'new threads expandable domain should contain the message_read domain parameter')
# Do: message_read with domain, thread_level=0 (should be imposed by JS)
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, limit=20, thread_level=0)
read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable']
messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, thread_level=0, limit=20)
read_msg_list = [msg for thread in messages['threads'] for msg in thread]
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable']
# Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
self.assertEqual(len(messages), 9, 'message_read on Pigs should return 9 messages and 0 expandable')
self.assertEqual(len(read_msg_list), 9, 'message_read on Pigs should return 9 messages and 0 expandable')
self.assertEqual([self.msg_8.id, self.msg_7.id, self.msg_6.id, self.msg_5.id, self.msg_4.id, self.msg_3.id, self.msg_2.id, self.msg_1.id, self.msg_0.id], read_msg_ids,
'message_read, More on flat, should return all remaning messages')
+88 -51
View File
@@ -10,21 +10,60 @@ class TestTracking(TestMail):
should probably be. """
def _strip_string_spaces(body):
return body.replace(' ', '').replace('\n', '')
self.group_pigs.message_subscribe_users(user_ids=[self.user_employee.id])
# Data: res.users.group, to test group_public_id automatic logging
group_system_id = self.env['ir.model.data'].xmlid_to_res_id('base.group_system') or False
# Data: custom subtypes
Subtype = self.env['mail.message.subtype']
Data = self.env['ir.model.data']
mt_private = Subtype.create({'name': 'private', 'description': 'Private public'})
Data.create({'name': 'mt_private', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_private.id})
mt_name_supername = Subtype.create({'name': 'name_supername', 'description': 'Supername name'})
Data.create({'name': 'mt_name_supername', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_name_supername.id})
mt_group_public_set = Subtype.create({'name': 'group_public_set', 'description': 'Group set'})
Data.create({'name': 'mt_group_public_set', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_group_public_set.id})
mt_group_public = Subtype.create({'name': 'group_public', 'description': 'Group changed'})
Data.create({'name': 'mt_group_public', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_group_public.id})
group_system = self.env.ref('base.group_system')
group_user = self.env.ref('base.group_user')
self.group_pigs.message_subscribe_users(user_ids=[self.user_employee.id])
# mt_private: public field (tracked as onchange) set to 'private' (selection)
mt_private = Subtype.create({
'name': 'private',
'description': 'Public field set to private'
})
Data.create({
'name': 'mt_private',
'model': 'mail.message.subtype',
'module': 'mail',
'res_id': mt_private.id
})
# mt_name_supername: name field (tracked as always) set to 'supername' (char)
mt_name_supername = Subtype.create({
'name': 'name_supername',
'description': 'Name field set to supername'
})
Data.create({
'name': 'mt_name_supername',
'model': 'mail.message.subtype',
'module': 'mail',
'res_id': mt_name_supername.id
})
# mt_group_public_set: group_public field (tracked as onchange) set to something (m2o)
mt_group_public_set = Subtype.create({
'name': 'group_public_set',
'description': 'Group_public field set'
})
Data.create({
'name': 'mt_group_public_set',
'model': 'mail.message.subtype',
'module': 'mail',
'res_id': mt_group_public_set.id
})
# mt_group_public_set: group_public field (tracked as onchange) set to nothing (m2o)
mt_group_public_unset = Subtype.create({
'name': 'group_public_unset',
'description': 'Group_public field unset'
})
Data.create({
'name': 'mt_group_public_unset',
'model': 'mail.message.subtype',
'module': 'mail',
'res_id': mt_group_public_unset.id
})
def _track_subtype(self, cr, uid, ids, init_values, context=None):
record = self.browse(cr, uid, ids[0], context=context)
@@ -35,11 +74,15 @@ class TestTracking(TestMail):
elif 'group_public_id' in init_values and record.group_public_id:
return 'mail.mt_group_public_set'
elif 'group_public_id' in init_values and not record.group_public_id:
return 'mail.mt_group_public'
return 'mail.mt_group_public_unset'
return False
self.registry('mail.group')._patch_method('_track_subtype', _track_subtype)
visibility = {'public': 'onchange', 'name': 'always', 'group_public_id': 'onchange'}
visibility = {
'public': 'onchange',
'name': 'always',
'group_public_id': 'onchange'
}
cls = type(self.env['mail.group'])
for key in visibility:
self.assertFalse(hasattr(getattr(cls, key), 'track_visibility'))
@@ -51,44 +94,38 @@ class TestTracking(TestMail):
del getattr(cls, key).track_visibility
# Test: change name -> always tracked, not related to a subtype
self.group_pigs.sudo(self.user_employee).write({'public': 'public'})
self.assertEqual(len(self.group_pigs.message_ids.ids), 1, 'tracked: a message should have been produced')
# Test: first produced message: no subtype, name change tracked
self.group_pigs.sudo(self.user_employee).write({'name': 'my_name'})
self.assertEqual(len(self.group_pigs.message_ids), 1)
last_msg = self.group_pigs.message_ids[-1]
self.assertFalse(last_msg.subtype_id.id, 'tracked: message should not have been linked to a subtype')
self.assertIn(u"Selectedgroupofusers\u2192Everyone", _strip_string_spaces(last_msg.body), 'tracked: message body incorrect')
self.assertIn('Pigs', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field')
# Test: change name as supername, public as private -> 2 subtypes
self.assertFalse(last_msg.subtype_id)
self.assertEqual(len(last_msg.tracking_value_ids), 1)
self.assertEqual(last_msg.tracking_value_ids.field, 'name')
self.assertEqual(last_msg.tracking_value_ids.field_desc, 'Name')
self.assertEqual(last_msg.tracking_value_ids.old_value_char, 'Pigs')
self.assertEqual(last_msg.tracking_value_ids.new_value_char, 'my_name')
# Test: change name as supername, public as private -> 1 subtype, private
self.group_pigs.sudo(self.user_employee).write({'name': 'supername', 'public': 'private'})
self.group_pigs.invalidate_cache()
self.assertEqual(len(self.group_pigs.message_ids.ids), 2, 'tracked: one new message should have been produced')
# Test: first produced message: mt_name_supername
last_msg = self.group_pigs.message_ids[-2]
self.assertEqual(last_msg.subtype_id.id, mt_private.id, 'tracked: message should be linked to mt_private subtype')
self.assertIn('Private public', last_msg.body, 'tracked: message body does not hold the subtype description')
self.assertIn(u'Pigs\u2192supername', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect')
self.assertEqual(len(self.group_pigs.message_ids.ids), 2)
last_msg = self.group_pigs.message_ids[0]
self.assertEqual(last_msg.subtype_id, mt_private)
self.assertEqual(len(last_msg.tracking_value_ids), 2)
self.assertEqual(set(last_msg.tracking_value_ids.mapped('field')), set(['name', 'public']))
self.assertEqual(set(last_msg.tracking_value_ids.mapped('field_desc')), set(['Name', 'Privacy']))
self.assertEqual(set(last_msg.tracking_value_ids.mapped('old_value_char')), set(['my_name', 'Selected group of users']))
self.assertEqual(set(last_msg.tracking_value_ids.mapped('new_value_char')), set(['supername', 'Invited people only']))
# Test: change public as public, group_public_id -> 1 subtype, name always tracked
self.group_pigs.sudo(self.user_employee).write({'public': 'public', 'group_public_id': group_system_id})
# Test: change public as public, group_public_id -> 1 subtype, group public set
self.group_pigs.sudo(self.user_employee).write({'public': 'public', 'group_public_id': group_system.id})
self.group_pigs.invalidate_cache()
self.assertEqual(len(self.group_pigs.message_ids), 3, 'tracked: one message should have been produced')
# Test: first produced message: mt_group_public_set_id, with name always tracked, public tracked on change
last_msg = self.group_pigs.message_ids[-3]
self.assertEqual(last_msg.subtype_id, mt_group_public_set, 'tracked: message should be linked to mt_group_public_set_id')
self.assertIn('Group set', last_msg.body, 'tracked: message body does not hold the subtype description')
self.assertIn(u"Invitedpeopleonly\u2192Everyone", _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field')
self.assertIn(u'HumanResources/Employee\u2192Administration/Settings', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field')
# Test: change group_public_id to False -> 1 subtype, name always tracked
self.group_pigs.sudo(self.user_employee).write({'group_public_id': False})
self.group_pigs.invalidate_cache()
self.assertEqual(len(self.group_pigs.message_ids), 4, 'tracked: one message should have been produced')
# Test: first produced message: mt_group_public_set_id, with name always tracked, public tracked on change
last_msg = self.group_pigs.message_ids[-4]
self.assertEqual(last_msg.subtype_id, mt_group_public, 'tracked: message should be linked to mt_group_public_id')
self.assertIn('Group changed', last_msg.body, 'tracked: message body does not hold the subtype description')
self.assertIn(u'Administration/Settings\u2192', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field')
# Test: change not tracked field, no tracking message
self.group_pigs.sudo(self.user_employee).write({'description': 'Dummy'})
self.group_pigs.invalidate_cache()
self.assertEqual(len(self.group_pigs.message_ids), 4, 'tracked: No message should have been produced')
self.assertEqual(len(self.group_pigs.message_ids), 3)
last_msg = self.group_pigs.message_ids[0]
self.assertEqual(last_msg.subtype_id, mt_group_public_set)
self.assertEqual(len(last_msg.tracking_value_ids), 2)
self.assertEqual(set(last_msg.tracking_value_ids.mapped('field')), set(['group_public_id', 'public']))
self.assertEqual(set(last_msg.tracking_value_ids.mapped('field_desc')), set(['Authorized Group', 'Privacy']))
self.assertEqual(set(last_msg.tracking_value_ids.mapped('old_value_char')), set([group_user.name_get()[0][1], 'Invited people only']))
self.assertEqual(set(last_msg.tracking_value_ids.mapped('new_value_char')), set([group_system.name_get()[0][1], 'Everyone']))
self.assertEqual(set(last_msg.tracking_value_ids.mapped('old_value_integer')), set([0, group_user.id]))
self.assertEqual(set(last_msg.tracking_value_ids.mapped('new_value_integer')), set([0, group_system.id]))
+3 -12
View File
@@ -2,20 +2,11 @@
<openerp>
<data>
<!-- this record will be dynamically adapted to each new menu created -->
<record id="action_mail_group_feeds" model="ir.actions.client">
<record model="ir.actions.act_window" id="action_mail_group_feeds">
<field name="name">Discussion Group</field>
<field name="tag">mail.wall</field>
<field name="res_model">mail.message</field>
<field name="context">{
}</field>
<field name="params">{
'read_action': 'read'
}</field>
<field name="help" type="html">
<p>
No message in this group.
</p>
</field>
<field name="view_type">form</field>
<field name="view_mode">timeline</field>
</record>
<!-- Group Kanban View !-->
+284
View File
@@ -126,7 +126,291 @@
<field name="view_mode">kanban,tree,form</field>
</record>
<record model="ir.ui.view" id="mail_message_view_timeline">
<field name="name">mail.message.view.timeline.</field>
<field name="model">mail.message</field>
<field name="arch" type="xml">
<timeline string="Mail Timeline">
<field name="date"/>
<field name="author_id"/>
<field name="subject"/>
<field name="model"/>
<field name="res_id"/>
<field name="body"/>
<field name="attachment_ids"/>
<field name="tracking_value_ids"/>
<field name="subtype_id"/>
<field name="message_type"/>
<field name="vote_user_ids"/>
<field name="to_read"/>
<field name="starred"/>
<templates>
<t t-name="timeline-message">
<div t-attf-class="row o_timeline_msg o_timeline_msg_#{record.message_type}
#{record.subtype ? '' : 'o_timeline_msg_nobody'}">
<t t-if="record.message_type == 'expandable'">
<div class="col-md-2"> </div>
<span> - - - - - - - - - - - See more messages - - - - - - - - - - - </span>
</t>
<t t-if="record.message_type != 'expandable'">
<div class="col-md-2">
<div class="o_timeline_msg_date">
<span> <t t-raw="record.date"/> </span>
</div>
</div>
<div class="col-md-10 o_timeline_msg_message">
<div class='o_timeline_msg_left'> <!-- *** avatar *** -->
<a t-if="record.options.show_link"
t-attf-href="#model=res.partner&amp;id=#{record.author_id[0]}"
t-att-title="record.author_id[1]">
<img class="o_timeline_msg_icon"
t-att-src="record.author_avatar"/>
</a>
<img t-if="!record.options.show_link" class="o_timeline_msg_icon"
t-att-src="record.author_avatar"/>
</div> <!-- *** avatar *** -->
<div class="o_timeline_msg_center"> <!-- *** msg_center *** -->
<div class="o_timeline_msg_content"> <!-- *** msg_content *** -->
<div class="o_timeline_header"> <!-- *** msg_header *** -->
<!-- *** header when tracking values *** -->
<div t-if="record.tracking_values" class="o_timeline_tracking_value_header">
<span t-if="!record.subtype" class="text-muted o_timeline_no_subtype">
Update </span>
<span t-if="record.subtype" class="o_timeline_subtype">
<t t-raw="record.subtype"/>
</span>
<span t-attf-class="text-muted #{!record.subtype ? 'o_timeline_no_subtype' : ''}">
by </span>
<span t-attf-class="#{!record.subtype ? 'text-muted o_timeline_no_subtype' : ''}">
<a t-if="record.options.show_link"
class="o_timeline_action_author"
t-attf-href="#model=res.partner&amp;id=#{record.author_id[0]}"
t-att-data-partner="record.author_id[0]">
<t t-raw="record.author_id[2]"/>
</a>
<a t-if="!record.options.show_link"
class="o_timeline_action_author">
<t t-raw="record.author_id[2]"/>
</a>
</span>
</div>
<!-- *** header when no tracking value *** -->
<span t-if="!record.tracking_values" class="o_timeline_msg_header">
<span t-if="record.author_id and !(record.message_type == 'notification')"
class="text-muted">
<t t-if="record.subtype and record.message_type == 'comment'"> Message </t>
<t t-if="record.subtype and record.message_type == 'email'"> Email </t>
<t t-if="!record.subtype"> Note </t>
from
<a t-if="record.options.show_link and record.author_id[0]"
t-attf-href="#model=res.partner&amp;id=#{record.author_id[0]}"
t-att-data-partner="record.author_id[0]" t-att-data-record="record.record_name"
class="o_timeline_action_author">
<t t-raw="record.author_id[2]"/>
</a>
<a t-if="record.author_id and (!record.options.show_link or !record.author_id[0])"
class="o_timeline_action_author">
<t t-raw="record.author_id[2]"/>
</a>
</span>
<span t-if="!record.options.readonly and !(record.message_type == 'notification')"
class='oe_subtle oe_sep text-muted'>•</span>
<span t-if="!record.options.readonly and !(record.message_type == 'notification')"
class="o_timeline_vote" t-att-data-id="record.id">
<span class="o_timeline_vote_count" t-att-data-id="record.id" t-if='record.vote_nb > 0'>
<t t-esc='record.vote_nb'/>
<i class="fa fa-thumbs-o-up" t-att-data-id="record.id"></i>
</span>
<t t-if="!record.has_voted">Like</t>
<t t-if="record.has_voted">Unlike</t>
</span>
</span>
<div t-attf-class="o_timeline_msg_icons #{record.tracking_values ?
'o_timeline_msg_icons_track' : ''}"
t-if="!record.options.readonly"> <!-- *** icons *** -->
<span>
<i t-if="record.options.show_read" title="Done"
class="oe_read fa fa-check" t-att-data-id="record.id"/>
<i t-if="record.is_favorite and record.options.show_unread" title="Set back to Todo"
class="oe_unread fa fa-inbox" t-att-data-id="record.id"/>
<i t-if="!record.is_favorite and record.options.show_unread" title="Move to Inbox"
class="oe_unread fa fa-inbox" t-att-data-id="record.id"/>
</span>
<span>
<i title="Mark as Todo" t-attf-class="oe_star #{record.is_favorite ? 'oe_starred':''} fa fa-star"
t-att-data-id="record.id"/>
</span>
</div> <!-- *** icons *** -->
</div> <!-- *** msg_header *** -->
<div t-attf-class="o_timeline_msg_body"> <!-- *** msg_body *** -->
<t t-if="record.tracking_values">
<ul class="o_timeline_tracking_value_list">
<t t-foreach='record.tracking_value_ids' t-as='value'>
<li>
<t t-esc="value.changed_field"/>
<span> : </span>
<t t-if="value.old_value">
<span class="o_timeline_tracking_value"> <t t-esc="value.old_value"/> </span>
<span> <i class="fa fa-long-arrow-right"></i> </span>
</t>
<span class="o_timeline_tracking_value"> <t t-esc="value.new_value"/> </span>
</li>
</t>
</ul>
</t>
<t t-if="!record.tracking_values">
<t t-raw="record.body"/>
<t t-if="!record.body and record.subject">
<t t-raw="record.subject"/>
</t>
</t>
</div> <!-- *** msg_body *** -->
<div class="o_timeline_msg_footer"> <!-- *** msg_footer *** -->
<t t-if="record.attachments">
<div class="o_timeline_msg_attachment_list">
<t t-raw="record.attachments_content"/>
</div>
</t>
<t t-if="(record.message_type == 'comment' or record.message_type == 'email') and
(record.subtype and record.partners)">
<div t-if="record.partners" class="o_timeline_partners_followers">
<span class="text-muted">
To </span>
<span class="o_timeline_partners_list">
<t t-foreach="record.partner_ids.slice(0, 3)" t-as="partner">
<span t-attf-class="o_timeline_partner_follower">
<a t-if="record.options.show_link"
t-attf-href="#model=res.partner&amp;id=#{partner[0]}"
t-att-data-partner="partner[0]"
class="o_timeline_action_author">
<t t-esc="partner[1]"/>
</a>
<t t-if="!record.options.show_link" t-esc="partner[1]"/>
</span>
<t t-if="!partner_last">,</t>
</t>
<t t-if="record.partner_ids.length > 3">
<span t-att-title="record.partners_str">
and <t t-esc="record.partners_nb"/> more
</span>
</t>
</span>
</div>
</t>
</div> <!-- *** msg_footer *** -->
</div> <!-- *** msg_content *** -->
</div> <!-- *** msg_center *** -->
</div>
</t>
</div>
</t>
<t t-name="timeline-thread">
<t t-if="options.view_inbox">
<div t-attf-class="o_timeline_thread_content o_timeline_parent_#{widget.thread_type}">
<t t-if="widget.thread_type == 'expandable'">
<div class="col-md-2"> </div>
<span> - - - - - - - - - - - See more messages - - - - - - - - - - - </span>
</t>
<t t-if="widget.thread_type != 'expandable'">
<span class="col-md-2">
<t t-if="options.is_folded">
<div class="o_timeline_msg_date">
<span> <t t-raw="record.date"/> </span>
</div>
</t>
</span>
<div class="o_timeline_parent_message">
<div class="o_timeline_msg_icons" t-if="!options.readonly"> <!-- *** read *** -->
<i t-if="options.show_read_unread_button" title="Done" class="oe_read fa fa-check"/>
</div> <!-- *** read *** -->
<span t-attf-class="o_timeline_parent_subject #{widget.model and widget.res_id
and options.show_link ? 'o_timeline_small_subject' : ''}"> <!-- *** subject *** -->
<span class="o_timeline_subject">
<t t-if="widget.res_id and widget.subject">
<t t-raw="widget.record_name"/> : <t t-raw="widget.subject"/>
</t>
<t t-if="widget.res_id and !widget.subject">
<t t-raw="widget.record_name"/>
</t>
<t t-if="!widget.res_id and widget.subject">
<t t-raw="widget.subject"/>
</t>
<t t-if="!widget.res_id and !widget.subject">
No subject
</t>
</span>
</span> <!-- *** subject *** -->
<span class="o_timeline_go_to_doc" t-if="record.res_id and record.model and options.show_link"> <!-- *** Go to doc *** -->
<a t-attf-href="#action=mail.action_mail_redirect&amp;model=#{record.model}&amp;res_id=#{record.res_id}">
<i class="fa fa-arrow-circle-right" title="Go to document"></i>
</a>
</span> <!-- *** Go to doc *** -->
<span class="o_timeline_parent_avatar"> <!-- *** avatar *** -->
<a t-if="options.show_link"
t-attf-href="#model=res.partner&amp;id=#{record.author_id[0]}"
t-att-title="record.author_id[1]">
<img class="o_timeline_msg_icon"
t-att-src="record.author_avatar"/>
</a>
<img t-if="!options.show_link" class="o_timeline_msg_icon"
t-att-src="record.author_avatar"/>
</span> <!-- *** avatar *** -->
<span class="o_timeline_parent_author"> <!-- *** author *** -->
<a t-if="options.show_link and record.author_id[0]"
t-attf-href="#model=res.partner&amp;id=#{record.author_id[0]}"
t-att-data-partner="record.author_id[0]" class="o_timeline_action_author"
title="Last sender">
<t t-raw="record.author_id[2]"/>
</a>
<a t-if="record.author_id and (!options.show_link or !record.author_id[0])"
class="o_timeline_action_author"
title="Last sender">
<t t-raw="record.author_id[2]"/>
</a>
</span> <!-- *** author *** -->
<span class="o_timeline_msg_icons" t-if="!options.readonly"> <!-- *** repy *** -->
<i t-if="options.show_reply_button" title="Reply" class="oe_reply fa fa-reply"/>
</span> <!-- *** reply *** -->
<span class="o_timeline_parent_followers" t-if="widget.nb_followers > 0"> <!-- *** followers *** -->
<span title="Number of followers"> <t t-raw="widget.nb_followers"/> </span>
<i class="oe_follwrs fa fa-users"></i>
</span> <!-- *** followers *** -->
</div>
</t>
</div>
</t>
<t t-if="!options.is_folded">
<t t-foreach="widget.messages" t-as="message">
<t t-call="timeline-message">
<t t-set="record" t-value="message"/>
</t>
</t>
</t>
</t>
</templates>
</timeline>
</field>
</record>
</data>
</openerp>
+36 -163
View File
@@ -1,168 +1,41 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="action_mail_inbox_feeds" model="ir.actions.client">
<field name="name">Inbox</field>
<field name="tag">mail.wall</field>
<field name="res_model">mail.message</field>
<field name="context">{
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
'needaction_menu_ref': ['mail.mail_tomefeeds', 'mail.mail_starfeeds']
}</field>
<field name="params" eval="&quot;{
'domain': [
('to_read', '=', True),
('starred', '=', False),
],
'view_mailbox': True,
'view_inbox': True,
'read_action': 'read',
'show_compose_message': False
}&quot;"/>
<field name="help" type="html">
<p>
<b>Good Job!</b> Your inbox is empty.
</p><p>
Your inbox contains private messages or emails sent to you
as well as information related to documents or people you
follow.
</p>
</field>
</record>
<data>
<menuitem id="mail.mail_feeds_main" name="Chat" icon="fa-envelope"
groups="base.group_user" sequence="1"/>
<record id="action_mail_to_me_feeds" model="ir.actions.client">
<field name="name">To: me</field>
<field name="tag">mail.wall</field>
<field name="res_model">mail.message</field>
<field name="context">{
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
'search_default_message_unread': True,
'needaction_menu_ref': ['mail.mail_starfeeds', 'mail.mail_inboxfeeds']
}</field>
<field name="params" eval="&quot;{
'domain': [
('partner_ids.user_ids', 'in', [uid])
],
'view_mailbox': True,
'read_action': 'read',
'show_compose_message': False
}&quot;"/>
<field name="help" type="html">
<p>
<b>No private message.</b>
</p><p>
This list contains messages sent to you.
</p>
</field>
</record>
<record model="ir.actions.act_window" id="action_mail_inbox_feeds">
<field name="name">Inbox</field>
<field name="res_model">mail.message</field>
<field name="view_type">form</field>
<field name="view_mode">timeline</field>
<field name="context">{
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
'options' : {'view_mailbox': True,
'view_inbox': True,
'show_write_to_followers': False}
}</field>
<field name="domain">[
'|',
('notification_ids.partner_id.user_ids', 'in', [uid]),
('author_id.user_ids', 'in', [uid]),
]</field>
</record>
<record id="action_mail_star_feeds" model="ir.actions.client">
<field name="name">To-do</field>
<field name="tag">mail.wall</field>
<field name="res_model">mail.message</field>
<field name="context">{
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
'search_default_message_unread': True
}</field>
<field name="params" eval="&quot;{
'domain': [
('starred', '=', True),
],
'view_mailbox': True,
'read_action': 'read',
'compose_as_todo': True,
'show_compose_message': False
}&quot;"/>
<field name="help" type="html">
<p>
<b>No todo.</b>
</p><p>
When you process messages in your inbox, you can mark some
as <i>todo</i>. From this menu, you can process all your todo.
</p>
</field>
</record>
<menuitem id="mail.mail_inbox" name="Inbox" action="action_mail_inbox_feeds"
parent="mail.mail_feeds_main" groups="base.group_user" sequence="1"/>
<record id="action_mail_redirect" model="ir.actions.server">
<field name="name">Mail Redirection (Document / Inbox)</field>
<field name="condition">True</field>
<field name="model_id" ref="model_mail_thread"/>
<field name="code">action = pool.get('mail.thread').message_redirect_action(cr, uid, context)</field>
<field name="state">code</field>
<field name="type">ir.actions.server</field>
</record>
<record id="action_mail_archives_feeds" model="ir.actions.client">
<field name="name">Archives</field>
<field name="tag">mail.wall</field>
<field name="context">{
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
'needaction_menu_ref': ['mail.mail_tomefeeds', 'mail.mail_starfeeds', 'mail.mail_inboxfeeds']
}</field>
<field name="params" eval="&quot;{
'domain': [
'|',
('notification_ids.partner_id.user_ids', 'in', [uid]),
('author_id.user_ids', 'in', [uid]),
],
'view_mailbox': True,
'show_compose_message': False
}&quot;"/>
<field name="help" type="html">
<p>
No message found and no message sent yet.
</p><p>
Click on the top-right icon to compose a message. This
message will be sent by email if it's an internal contact.
</p>
</field>
</record>
<record id="action_mail_redirect" model="ir.actions.server">
<field name="name">Mail Redirection (Document / Inbox)</field>
<field name="condition">True</field>
<field name="model_id" ref="model_mail_thread"/>
<field name="code">action = pool.get('mail.thread').message_redirect_action(cr, uid, context)</field>
<field name="state">code</field>
<field name="type">ir.actions.server</field>
</record>
<!-- MENU -->
<!-- Top menu item -->
<menuitem name="Chat"
id="mail.mail_feeds_main"
groups="base.group_user"
icon="fa-envelope"
sequence="1"/>
<!-- Left-side menu: Feeds -->
<menuitem id="mail.mail_inbox" name="Inbox" parent="mail.mail_feeds_main" groups="base.group_user" sequence="1" action="action_mail_inbox_feeds"/>
<!-- <record id="mail_inboxfeeds" model="ir.ui.menu">
<field name="name">Inbox</field>
<field name="sequence" eval="10"/>
<field name="action" ref="action_mail_inbox_feeds"/>
<field name="parent_id" ref="mail.mail_inbox"/>
</record>
-->
<!-- <record id="mail_tomefeeds" model="ir.ui.menu">
<field name="name">To: me</field>
<field name="sequence" eval="11"/>
<field name="action" ref="action_mail_to_me_feeds"/>
<field name="parent_id" ref="mail.mail_feeds"/>
</record>
<record id="mail_starfeeds" model="ir.ui.menu">
<field name="name">To-do</field>
<field name="sequence" eval="14"/>
<field name="action" ref="action_mail_star_feeds"/>
<field name="parent_id" ref="mail.mail_feeds"/>
</record>
<record id="mail_archivesfeeds" model="ir.ui.menu">
<field name="name">Archives</field>
<field name="sequence" eval="16"/>
<field name="action" ref="action_mail_archives_feeds"/>
<field name="parent_id" ref="mail.mail_feeds"/>
</record>
-->
</data>
</openerp>
</data>
</openerp>
+67
View File
@@ -0,0 +1,67 @@
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_mail_tracking_value_tree">
<field name="name">mail.tracking.value.tree</field>
<field name="model">mail.tracking.value</field>
<field name="priority">12</field>
<field name="arch" type="xml">
<tree string="Tracking Value">
<field name="field"/>
<field name="old_value_integer"/>
<field name="old_value_float"/>
<field name="old_value_char"/>
<field name="old_value_text"/>
<field name="old_value_datetime"/>
<field name="new_value_integer"/>
<field name="new_value_float"/>
<field name="new_value_char"/>
<field name="new_value_text"/>
<field name="new_value_datetime"/>
<field name="mail_message_id"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_tracking_value_form">
<field name="name">mail.tracking.value.form</field>
<field name="model">mail.tracking.value</field>
<field name="arch" type="xml">
<form string="Tracking Value">
<sheet>
<group>
<group string='Change'>
<field name="field"/>
<field name="field_desc"/>
<field name="field_type"/>
<field name="old_value_integer"/>
<field name="old_value_float"/>
<field name="old_value_char"/>
<field name="old_value_text"/>
<field name="old_value_datetime"/>
<field name="new_value_integer"/>
<field name="new_value_float"/>
<field name="new_value_char"/>
<field name="new_value_text"/>
<field name="new_value_datetime"/>
</group>
<group string='Related Message'>
<field name="mail_message_id"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_mail_tracking_value">
<field name="name">Tracking Values</field>
<field name="res_model">mail.tracking.value</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Tracking Values" id="menu_mail_tracking_value" parent="base.menu_email" action="action_view_mail_tracking_value"/>
</data>
</openerp>
+1 -1
View File
@@ -4,7 +4,7 @@ odoo.define('mail_tip.mail_tip', function (require) {
var mail = require('mail.mail');
var core = require('web.core');
mail.Thread.include({
mail.MailThread.include({
message_fetch: function() {
return this._super.apply(this, arguments).done(function() {
core.bus.trigger('chatter_messages_fetched');
+16 -88
View File
@@ -11,101 +11,29 @@
<menuitem name="Messaging" id="portal_messages" parent="portal_menu" sequence="10" groups="base.group_portal"/>
<record id="action_mail_inbox_feeds_portal" model="ir.actions.client">
<record model="ir.actions.act_window" id="action_mail_inbox_feeds_portal">
<field name="name">Inbox</field>
<field name="tag">mail.wall</field>
<field name="res_model">mail.message</field>
<field name="view_type">form</field>
<field name="view_mode">timeline</field>
<field name="context">{
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
'options' : {'view_mailbox': True,
'view_inbox': True,
'show_link': 'False',
'show_write_to_followers': False}
}</field>
<field name="params" eval="&quot;{
'domain': [
('to_read', '=', True),
],
'show_compose_message': False,
'show_link': False,
'view_mailbox': True,
'view_inbox': True,
'read_action': 'read'
}&quot;"/>
<field name="help" type="html">
<p>
<b>Good Job!</b> Your inbox is empty.
</p><p>
Your inbox contains private messages or emails sent to you
as well as information related to documents or people you
follow.
</p>
</field>
</record>
<!-- TODO: remove me in 8.0 -->
<record id="action_mail_star_feeds_portal" model="ir.actions.client">
<field name="name">To-do</field>
<field name="tag">mail.wall</field>
<field name="res_model">mail.message</field>
<field name="context">{
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
'search_default_message_unread': True
}</field>
<field name="params" eval="&quot;{
'domain': [
('starred', '=', True),
],
'show_compose_message': False,
'show_link': False,
'view_mailbox': True,
'read_action': 'read'
}&quot;"/>
<field name="help" type="html">
<p>
<b>No todo.</b>
</p><p>
When you process messages in your inbox, you can mark some
as <i>todo</i>. From this menu, you can process all your todo.
</p>
</field>
</record>
<menuitem name="To-do" id="portal_mail_starfeeds" parent="portal_messages"
action="action_mail_star_feeds_portal" sequence="20" groups="base.group_no_one"/>
<!-- end -->
<record id="action_mail_archives_feeds_portal" model="ir.actions.client">
<field name="name">Archives</field>
<field name="tag">mail.wall</field>
<field name="context">{
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
}</field>
<field name="params" eval="&quot;{
'domain': [
'|',
('notification_ids.partner_id.user_ids', 'in', [uid]),
('author_id.user_ids', 'in', [uid]),
],
'show_compose_message': False,
'show_link': False,
'view_mailbox': True,
}&quot;"/>
<field name="help" type="html">
<p>
No message found and no message sent yet.
</p><p>
Click on the top-right icon to compose a message. This
message will be sent by email if it's an internal contact.
</p>
</field>
<field name="domain">[
'|',
('notification_ids.partner_id.user_ids', 'in', [uid]),
('author_id.user_ids', 'in', [uid]),
]</field>
</record>
<menuitem name="Inbox" id="portal_inbox" parent="portal_messages"
action="action_mail_inbox_feeds_portal" sequence="10" groups="base.group_portal"/>
<menuitem name="Archives" id="portal_mail_archivesfeeds" parent="portal_messages"
action="action_mail_archives_feeds_portal" sequence="30" groups="base.group_portal"/>
<!--
Create menu items that we'll leave empty for now - they'll be
@@ -142,4 +70,4 @@
</record>
</data>
</openerp>
</openerp>
+3 -3
View File
@@ -91,7 +91,7 @@ class test_portal(TestMail):
msg = group_pigs.message_post(body='My body', partner_ids=[self.user_employee.partner_id.id, self.user_portal.partner_id.id], type='comment', subtype='mail.mt_comment')
# Chell has no read access to pigs -> should redirect to Portal Inbox
action = self.env['mail.thread'].with_context({'params': {'message_id': msg.id}}).sudo(self.user_portal).message_redirect_action()
self.assertEqual(action.get('type'), 'ir.actions.client',
'URL redirection: action without parameters should redirect to client action Inbox')
self.assertEqual(action.get('type'), 'ir.actions.act_window',
'URL redirection: action without parameters should redirect to act_window Inbox')
self.assertEqual(action.get('id'), port_act_id,
'URL redirection: action without parameters should redirect to client action Inbox')
'URL redirection: action without parameters should redirect to act_window Inbox')
+1 -1
View File
@@ -93,7 +93,7 @@ Sidebar.include({
* an action. We do this because 'invite' is based on the share
* mechanism, and it tries to share an action.
*/
mail.RecordThread.include( {
mail.TimelineRecordThread.include( {
start: function() {
var start_res = this._super.apply(this, arguments);
if (has_action_id) {
+1
View File
@@ -1710,6 +1710,7 @@
max-width: 892px;
margin: 0 auto;
padding: 16px 16px 48px;
background-color: #f0eeee;
}
.openerp .oe_form div.oe_form_configuration p, .openerp .oe_form div.oe_form_configuration ul, .openerp .oe_form div.oe_form_configuration ol {
color: #aaa;
+1
View File
@@ -1439,6 +1439,7 @@ $sheet-padding: 16px
max-width: $sheet-max-width + 2* $sheet-padding
margin: 0 auto
padding: 16px 16px 48px
background-color: #f0eeee
div.oe_form_configuration
p, ul, ol
color: #aaa
+1
View File
@@ -192,6 +192,7 @@ class view(osv.osv):
('form','Form'),
('graph', 'Graph'),
('pivot', 'Pivot'),
('timeline', 'Timeline'),
('calendar', 'Calendar'),
('diagram','Diagram'),
('gantt', 'Gantt'),
+15
View File
@@ -696,6 +696,20 @@
</rng:element>
</rng:define>
<rng:define name="timeline">
<rng:element name="timeline">
<rng:optional><rng:attribute name="string" /></rng:optional>
<rng:zeroOrMore>
<rng:ref name="field"/>
</rng:zeroOrMore>
<rng:element name="templates">
<rng:oneOrMore>
<rng:ref name="any"/>
</rng:oneOrMore>
</rng:element>
</rng:element>
</rng:define>
<rng:define name="button">
<rng:element name="button">
<rng:ref name="overload"/>
@@ -796,6 +810,7 @@
<rng:ref name="notebook" />
<rng:ref name="graph" />
<rng:ref name="pivot" />
<rng:ref name="timeline" />
<rng:ref name="calendar" />
<rng:ref name="data" />
<rng:ref name="gantt" />