[IMP] mail: allow to use stand alone activities as reminders

This commit allows to create personal reminders and to manage them using
activities. It is done using activities not necessarily linked to a record
but only to a user. Reminders can be used for example as a personal todo
list.

Technically we choose to model a reminder as an activity related to no
record. This means having a void res_id and model. An activity type is
added to classify reminders in it.

Settings reminder as done is not done like classic activities. Indeed classic
activities are unlinked and a message is logged on the related document.
As there is no document for reminders those are archived instead. It means
an active field is added as well as an override of toggle_archive to update
systray counter.

Activities systray widget is improved to display reminders in activities
list. This entry leads to a kanban view of activities allowing their
management. It is also possible to create them directly from the systray
allowing a quick creation of reminders.

This commit is related to task ID 41745. Closes #22143 .

Co-authored-by: Dharmang Soni <dpr@odoo.com>
Co-authored-by: Xavier Dollé <xdo@odoo.com>
This commit is contained in:
Dharmang Soni
2018-03-27 13:26:41 +02:00
committed by Thibault Delavallée
co-authored by Dharmang Soni Xavier Dollé
parent 4265026ee2
commit f488dc58c3
15 changed files with 478 additions and 47 deletions
+1
View File
@@ -38,6 +38,7 @@
],
'demo': [
'data/mail_demo.xml',
'data/mail_activity_demo.xml',
'data/mail_channel_demo.xml',
],
'installable': True,
+12
View File
@@ -230,3 +230,15 @@ class MailController(http.Controller):
'menu_id': request.env['ir.model.data'].xmlid_to_res_id('mail.mail_channel_menu_root_chat'),
}
return values
@http.route('/mail/activity/new', type='json', auth='user')
def mail_activity_new(self, note, activity_type_id=None, date_deadline=None):
values = {'note': note}
if date_deadline:
values['date_deadline'] = date_deadline
if not activity_type_id:
activity_type_id = request.env['mail.activity.type'].sudo().search([('category', '=', 'reminder')]).id
if activity_type_id:
values['activity_type_id'] = activity_type_id
activity = request.env['mail.activity'].create(values)
return activity.id
+6
View File
@@ -19,5 +19,11 @@
<field name="icon">fa-tasks</field>
<field name="days">5</field>
</record>
<record id="mail_activity_data_reminder" model="mail.activity.type">
<field name="name">Reminder</field>
<field name="category">reminder</field>
<field name="icon">fa-tasks</field>
<field name="days">0</field>
</record>
</data>
</odoo>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<odoo>
<data noupdate="1">
<record id="mail_activity_reminder_0" model="mail.activity">
<field name="note" type="html"><p>Lunch with colleagues this Sunday.</p></field>
<field name="user_id" ref="base.user_root"/>
<field name="active" eval="True"/>
</record>
</data>
</odoo>
-1
View File
@@ -220,6 +220,5 @@ virginie@agrolait.fr</pre></field>
<field name="partner_ids" eval="[(6, 0, [ref('base.partner_root')])]"/>
<field name="date" eval="(DateTime.today() - timedelta(minutes=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
</data>
</odoo>
+66 -9
View File
@@ -5,6 +5,7 @@ from datetime import date, datetime, timedelta
from odoo import api, exceptions, fields, models, _
from odoo.osv import expression
from odoo.tools import html2plaintext
class MailActivityType(models.Model):
@@ -45,7 +46,7 @@ class MailActivityType(models.Model):
'mail.activity.type', 'mail_activity_rel', 'recommended_id', 'activity_id',
string='Preceding Activities')
category = fields.Selection([
('default', 'Other')], default='default',
('default', 'Other'), ('reminder', 'Reminder')], default='default',
string='Category',
help='Categories may trigger specific behavior like opening calendar view')
@@ -69,10 +70,10 @@ class MailActivity(models.Model):
return res
# owner
res_id = fields.Integer('Related Document ID', index=True, required=True)
res_id = fields.Integer('Related Document ID', index=True)
res_model_id = fields.Many2one(
'ir.model', 'Document Model',
index=True, ondelete='cascade', required=True)
index=True, ondelete='cascade')
res_model = fields.Char(
'Related Document Model',
index=True, related='res_model_id.model', store=True, readonly=True)
@@ -89,6 +90,9 @@ class MailActivity(models.Model):
note = fields.Html('Note')
feedback = fields.Html('Feedback')
date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.today)
active = fields.Boolean(
'Open', default=True,
help='Done reminders should be archived instead of marked as done.')
# description
user_id = fields.Many2one(
'res.users', 'Assigned to',
@@ -115,7 +119,8 @@ class MailActivity(models.Model):
@api.depends('res_model', 'res_id')
def _compute_res_name(self):
for activity in self:
activity.res_name = self.env[activity.res_model].browse(activity.res_id).name_get()[0][1]
if activity.res_model and activity.res_id:
activity.res_name = self.env[activity.res_model].browse(activity.res_id).name_get()[0][1]
@api.depends('date_deadline')
def _compute_state(self):
@@ -162,11 +167,17 @@ class MailActivity(models.Model):
pass
else:
return
doc_operation = 'read' if operation == 'read' else 'write'
activity_to_documents = dict()
for activity in self.sudo():
activity_to_documents.setdefault(activity.res_model, list()).append(activity.res_id)
if activity.res_model and activity.res_id:
activity_to_documents.setdefault(activity.res_model, list()).append(activity.res_id)
else:
if (operation != 'create' and self.env.user.id != activity.user_id.id):
raise exceptions.AccessError(_("You can only access your own records."))
elif operation == 'create' and not self.env.user.has_group('base.group_user'):
raise exceptions.AccessError(_("Only employee can create reminder."))
for model, res_ids in activity_to_documents.items():
self.env[model].check_access_rights(doc_operation, raise_exception=True)
try:
@@ -182,11 +193,19 @@ class MailActivity(models.Model):
values_w_defaults = self.default_get(self._fields.keys())
values_w_defaults.update(values)
# Reminder have no summary (for display name) It will add first line of note to summary.
if 'res_model_id' not in values and 'summary' not in values:
values_w_defaults['summary'] = self._compute_summary_from_note(values.get('note', _('Reminder')))
# continue as sudo because activities are somewhat protected
activity = super(MailActivity, self.sudo()).create(values_w_defaults)
activity_user = activity.sudo(self.env.user)
activity_user._check_access('create')
self.env[activity_user.res_model].browse(activity_user.res_id).message_subscribe(partner_ids=[activity_user.user_id.partner_id.id])
# subscribe to document if any
if activity.res_id and activity.res_model:
self.env[activity_user.res_model].browse(activity_user.res_id).message_subscribe(partner_ids=[activity_user.user_id.partner_id.id])
if activity.date_deadline <= fields.Date.today():
self.env['bus.bus'].sendone(
(self._cr.dbname, 'res.partner', activity.user_id.partner_id.id),
@@ -196,13 +215,28 @@ class MailActivity(models.Model):
@api.multi
def write(self, values):
self._check_access('write')
if not self.env.user._is_admin() and any(field in values.keys() for field in ['res_model', 'res_id', 'res_model_id']):
raise exceptions.AccessError(_("You cannot re-attach a reminder to another record."))
if values.get('user_id'):
pre_responsibles = self.mapped('user_id.partner_id')
res = super(MailActivity, self.sudo()).write(values)
reminders = self.filtered(lambda a: not a.res_id and not a.res_model) if 'note' in values and 'summary' not in values else self.env['mail.activity']
activities = self - reminders
res = True
if activities:
res &= super(MailActivity, activities.sudo()).write(values)
if reminders: # if we have reminder, note is set and summary is not set. We need to update summary
upd_values = dict(
values,
summary=self._compute_summary_from_note(values['note'])
)
res &= super(MailActivity, reminders.sudo()).write(upd_values)
if values.get('user_id'):
for activity in self:
self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id])
if activity.res_id and activity.res_model: # subscribe to document if any
self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id])
if activity.date_deadline <= fields.Date.today():
self.env['bus.bus'].sendone(
(self._cr.dbname, 'res.partner', activity.user_id.partner_id.id),
@@ -225,6 +259,29 @@ class MailActivity(models.Model):
{'type': 'activity_updated', 'activity_deleted': True})
return super(MailActivity, self.sudo()).unlink()
@api.multi
def toggle_active(self):
""" Override model-method to send bus notification about (un)archived reminders """
res = super(MailActivity, self).toggle_active()
for activity in self:
if activity.active:
self.env['bus.bus'].sendone(
(self._cr.dbname, 'res.partner', activity.user_id.partner_id.id),
{'type': 'activity_updated', 'activity_created': True})
else:
self.env['bus.bus'].sendone(
(self._cr.dbname, 'res.partner', activity.user_id.partner_id.id),
{'type': 'activity_updated', 'activity_deleted': True})
return res
def _compute_summary_from_note(self, note):
""" Returns the first line of html note """
if note:
summary = html2plaintext(note).strip().replace('*', '').split("\n")[0]
else:
summary = _('Reminder')
return summary or _('Reminder')
@api.multi
def action_done(self):
""" Wrapper without feedback because web button add context as
+17 -5
View File
@@ -70,15 +70,16 @@ class Users(models.Model):
@api.model
def activity_user_count(self):
query = """SELECT m.id, count(*), act.res_model as model,
# Reminders don't having any model so here we assign 'mail.activity' as reminder's model.
query = """SELECT m.id, count(*), act.res_model AS model,
CASE
WHEN now()::date - act.date_deadline::date = 0 Then 'today'
WHEN now()::date - act.date_deadline::date > 0 Then 'overdue'
WHEN now()::date - act.date_deadline::date < 0 Then 'planned'
END AS states
FROM mail_activity AS act
JOIN ir_model AS m ON act.res_model_id = m.id
WHERE user_id = %s
LEFT OUTER JOIN ir_model AS m ON act.res_model_id = m.id
WHERE user_id = %s AND active = TRUE
GROUP BY m.id, states, act.res_model;
"""
self.env.cr.execute(query, [self.env.uid])
@@ -86,7 +87,18 @@ class Users(models.Model):
model_ids = [a['id'] for a in activity_data]
model_names = {n[0]:n[1] for n in self.env['ir.model'].browse(model_ids).name_get()}
user_activities = {}
# always add default entry for reminders
user_activities = {
None: {
'name': _("Reminder"),
'model': None,
'icon': '/mail/static/src/img/reminder.png',
'total_count': 0,
'today_count': 0,
'overdue_count': 0,
'planned_count': 0
}
}
for activity in activity_data:
if not user_activities.get(activity['model']):
user_activities[activity['model']] = {
@@ -96,7 +108,7 @@ class Users(models.Model):
'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0,
}
user_activities[activity['model']]['%s_count' % activity['states']] += activity['count']
if activity['states'] in ('today','overdue'):
if activity['states'] in ('today', 'overdue'):
user_activities[activity['model']]['total_count'] += activity['count']
return list(user_activities.values())
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

+121 -18
View File
@@ -3,11 +3,13 @@ odoo.define('mail.systray', function (require) {
var config = require('web.config');
var core = require('web.core');
var datepicker = require('web.datepicker');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
/**
* Menu item appended in the systray part of the navbar
@@ -157,8 +159,13 @@ var MessagingMenu = Widget.extend({
var ActivityMenu = Widget.extend({
template:'mail.chat.ActivityMenu',
events: {
"click": "_onActivityMenuClick",
"click .o_mail_channel_preview": "_onActivityFilterClick",
'click': '_onActivityMenuClick',
'click .o_mail_channel_preview:not(.o_reminder)': '_onActivityFilterClick',
'click .o_reminder_show': '_onAddReminderClick',
'click .o_reminder_save': '_onReminderSaveClick',
'click .o_reminder_set_datetime': '_onReminderDateTimeSetClick',
'keydown input.o_reminder_input': '_onReminderInputKeyDown',
'click .o_reminder': '_onNewReminderClick',
},
start: function () {
this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels');
@@ -167,9 +174,9 @@ var ActivityMenu = Widget.extend({
this._updateActivityPreview();
return this._super();
},
//--------------------------------------------------
// Private
//--------------------------------------------------
/**
* Make RPC and get current user's activity details
* @private
@@ -184,8 +191,21 @@ var ActivityMenu = Widget.extend({
context: session.user_context,
},
}).then(function (data) {
// We required reminder to be first always. Moving its position from n-th to zero if not at zero
var reminderIndex = _.findIndex(data, function (val) { return val.model == null; });
if (reminderIndex > 0) {
data.splice(0, 0, data.splice(reminderIndex, 1)[0]);
}
self.activities = data;
self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count; }, 0);
//we want to add count in reminders title
var reminders = _.findWhere(data, {'model': null});
if (reminders) {
var totalReminders = reminders.overdue_count + reminders.planned_count + reminders.today_count;
//manualy count total reminders since reminders.total_count only sum today and overdue
reminders.name = _.str.sprintf(_t('Reminders (%s)'), totalReminders);
}
self.activityCounter = _.reduce(self.activities, function (total_count, p_data) { return total_count + p_data.total_count; }, 0);
self.$('.o_notification_counter').text(self.activityCounter);
self.$el.toggleClass('o_no_notification', !self.activityCounter);
});
@@ -241,10 +261,30 @@ var ActivityMenu = Widget.extend({
this.$el.toggleClass('o_no_notification', !this.activityCounter);
}
},
/**
* Save the reminder to database using datepicker date and field as note
* @private
*/
_saveReminder: function () {
var note = this.$('.o_reminder_input').val().trim();
if (! note) {
return;
}
var params = {'note': note};
var reminderDateTime = this.reminderDateTimeWidget.getValue();
if (reminderDateTime) {
params = _.extend(params, {'date_deadline': reminderDateTime});
}
this.$('.o_reminder_show').removeClass('hidden');
this.$('.o_reminder').addClass('hidden');
this._rpc({
route: '/mail/activity/new',
params: params,
}).then(this._updateActivityPreview.bind(this));
},
//------------------------------------------------------------
// Handlers
//-----------------------------------------------------------
/**
* Redirect to particular model view
* @private
@@ -260,15 +300,20 @@ var ActivityMenu = Widget.extend({
} else {
context['search_default_activities_' + data.filter] = 1;
}
this.do_action({
type: 'ir.actions.act_window',
name: data.model_name,
res_model: data.res_model,
views: [[false, 'kanban'], [false, 'form']],
search_view_id: [false],
domain: [['activity_user_id', '=', session.uid]],
context:context,
});
if (data.res_model == null) {
// Opening Reminder custom kanban view with action.
this.do_action('mail.mail_activity_action_reminders', {additional_context: context});
} else {
this.do_action({
type: 'ir.actions.act_window',
name: data.model_name,
res_model: data.res_model,
views: [[false, 'kanban'], [false, 'form']],
search_view_id: [false],
domain: [['activity_user_id', '=', session.uid]],
context:context,
});
}
},
/**
* When menu clicked update activity preview if counter updated
@@ -280,7 +325,65 @@ var ActivityMenu = Widget.extend({
this._updateActivityPreview();
}
},
/**
* When add new reminder button clicked, toggling quick reminder create view inside
* Systray activity view
*
* @private
* @param {MouseEvent} event
*/
_onAddReminderClick: function (event) {
event.stopPropagation();
if (!this.reminderDateTimeWidget){
this.reminderDateTimeWidget = new datepicker.DateWidget(this, {useCurrent: true});
}
this.reminderDateTimeWidget.appendTo(this.$('.o_reminder_datetime'));
this.$('.o_reminder_show, .o_reminder').toggleClass('hidden');
this.$('.o_reminder_input').val('').focus();
},
/**
* When focusing on input for new quick reminder systerm tray must be open.
* Preventing to close
*
* @private
* @param {MouseEvent} event
*/
_onNewReminderClick: function (event) {
event.stopPropagation();
},
/**
* Opens datetime picker for reminder.
* Quick FIX due to no option for set custom icon instead of caret in datepicker.
*
* @private
* @param {MouseEvent} event
*/
_onReminderDateTimeSetClick: function (event) {
event.preventDefault();
event.stopPropagation();
this.reminderDateTimeWidget.$input.click();
},
/**
* Saving reminder (quick create) and updating activity preview
*
* @private
* @param {MouseEvent} event
*/
_onReminderSaveClick: function (event) {
this._saveReminder();
},
/**
* Handling Enter key for quick create reminder.
*
* @private
* @param {KeyboardEvent} event
*/
_onReminderInputKeyDown: function (event) {
if (event.which === $.ui.keyCode.ENTER) {
this._saveReminder();
}
},
});
SystrayMenu.Items.push(MessagingMenu);
+22 -1
View File
@@ -391,7 +391,7 @@
.o_channel_title {
.o-flex-display();
.o_channel_name {
.o-flex(0, 1, auto);
.o-flex(1, 1, auto);
.o-text-overflow();
}
.o_channel_counter {
@@ -428,6 +428,27 @@
}
}
.o_reminder.o_mail_channel_preview {
background-color: white;
.o_reminder_input {
border: none;
}
.o_reminder_datetime {
.o_datepicker {
.o_datepicker_input {
float: right;
text-align: right;
border: none;
font-size: 11px;
width: 50%;
}
.o_datepicker_button {
display: none;
}
}
}
}
@media (max-width: @screen-xs-max) {
@o-mail-chatter-mobile-gap: 2%;
@@ -132,7 +132,15 @@
.o_kanban_inline_block {
display: inline-block;
}
.o_text_line_through {
text-decoration: line-through;
}
.o_reminder_title {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-bottom: 10px;
}
.o_mail_activity {
.o_activity_btn {
span.fa {
+23
View File
@@ -80,6 +80,29 @@
</div>
</div>
</t>
<div class="o_reminder_show">
<a class="btn btn-block text-center">Add new reminder</a>
</div>
<div class="o_reminder o_mail_channel_preview hidden">
<div class="o_mail_channel_image o_mail_channel_app">
<img src="/mail/static/src/img/reminder.png"/>
</div>
<div class="o_channel_info">
<div class="o_channel_title">
<span class="o_channel_name"><strong>Set a reminder</strong></span>
<div class="o_reminder_datetime"/>
<span class="ml4">
<a class="o_reminder_set_datetime text-muted">
<span class="fa fa-clock-o"/>
</a>
</span>
<span class="ml8 mr4">
<a class="o_reminder_save">SAVE</a>
</span>
</div>
<p><input class="o_reminder_input" type="text" placeholder="Remember..." /></p>
</div>
</div>
</t>
<t t-name="mail.chat.ActivityMenu">
+40 -8
View File
@@ -18,15 +18,16 @@ QUnit.module('ActivityMenu', {
'mail.activity.menu': {
fields: {
name: { type: "char" },
res_model: { type: "char" },
model: { type: "char" },
planned_count: { type: "integer"},
today_count: { type: "integer"},
overdue_count: { type: "integer"},
total_count: { type: "integer"},
active: { type: "boolean"}
},
records: [{
name: "Contact",
res_model: "res.partner",
model: "res.partner",
planned_count: 0,
today_count: 1,
overdue_count: 0,
@@ -34,7 +35,7 @@ QUnit.module('ActivityMenu', {
},
{
name: "Task",
res_model: "project.task",
model: "project.task",
planned_count: 1,
today_count: 0,
overdue_count: 0,
@@ -42,11 +43,18 @@ QUnit.module('ActivityMenu', {
},
{
name: "Issue",
res_model: "project.issue",
model: "project.issue",
planned_count: 1,
today_count: 1,
overdue_count: 1,
total_count: 3,
},
{
model: null,
planned_count: 1,
today_count: 1,
overdue_count: 0,
total_count: 2,
}],
},
};
@@ -71,8 +79,8 @@ QUnit.test('activity menu widget: menu with no records', function (assert) {
activityMenu.destroy();
});
QUnit.test('activity menu widget: activity menu with 3 records', function (assert) {
assert.expect(10);
QUnit.test('activity menu widget: activity menu with 4 records', function (assert) {
assert.expect(17);
var self = this;
var activityMenu = new systray.ActivityMenu();
testUtils.addMockEnvironment(activityMenu, {
@@ -81,6 +89,16 @@ QUnit.test('activity menu widget: activity menu with 3 records', function (asser
if (args.method === 'activity_user_count') {
return $.when(self.data['mail.activity.menu']['records']);
}
if (route === '/mail/activity/new') {
assert.deepEqual(args, {'note': 'New Reminder'}, 'Create reminder should get proper value');
_.each(self.data['mail.activity.menu'].records, function (record) {
if (record.model == null) {
record.today_count += 1;
record.total_count += 1;
}
});
return $.when();
}
return this._super(route, args);
},
});
@@ -88,7 +106,7 @@ QUnit.test('activity menu widget: activity menu with 3 records', function (asser
assert.ok(activityMenu.$el.hasClass('o_mail_navbar_item'), 'should be the instance of widget');
assert.ok(activityMenu.$('.o_mail_channel_preview').hasClass('o_mail_channel_preview'), "should instance of widget");
assert.ok(activityMenu.$('.o_notification_counter').hasClass('o_notification_counter'), "widget should have notification counter");
assert.strictEqual(parseInt(activityMenu.el.innerText), 5, "widget should have 5 notification counter");
assert.strictEqual(parseInt(activityMenu.el.innerText), 7, "widget should have 7 notification counter");
var context = {};
testUtils.intercept(activityMenu, 'do_action', function(event) {
@@ -123,7 +141,21 @@ QUnit.test('activity menu widget: activity menu with 3 records', function (asser
activityMenu.$('.dropdown-toggle').click();
activityMenu.$(".o_mail_navbar_dropdown_channels > div[data-model_name='Issue']").click();
// toggle quick create for reminder
activityMenu.$('.dropdown-toggle').click();
assert.strictEqual(activityMenu.$('.o_reminder_show').hasClass("hidden"), false, 'ActivityMenu should have Add new reminder CTA');
activityMenu.$('.o_reminder_show').click();
assert.strictEqual(activityMenu.$('.o_reminder_show').hasClass("hidden"), true, 'ActivityMenu should hide CTA when entering a new reminder');
assert.strictEqual(activityMenu.$('.o_reminder').hasClass("hidden"), false, 'ActivityMenu should display input for new reminder');
// creating quick reminder
activityMenu.$("input.o_reminder_input").val("New Reminder");
activityMenu.$(".o_reminder_save").click();
assert.strictEqual(parseInt(activityMenu.el.innerText), 8, "widget should have 9 notifications (8 base + 1 new reminder)");
assert.strictEqual(activityMenu.$('.o_reminder_show').hasClass("hidden"), false, 'ActivityMenu add reminder button should be displayed');
assert.strictEqual(activityMenu.$('.o_reminder').hasClass("hidden"), true, 'ActivityMenu add reminder input should be hidden');
activityMenu.destroy();
});
});
});
});
+99
View File
@@ -182,4 +182,103 @@
</field>
</record>
<record id="mail_activity_view_form_reminder" model="ir.ui.view">
<field name="name">mail.activity.view.form</field>
<field name="model">mail.activity</field>
<field name="priority">21</field>
<field name="arch" type="xml">
<form string="Reminders">
<sheet>
<field name="active" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" type="object" name="toggle_active" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<group>
<field name="date_deadline" string="Reminder Date"/>
</group>
<field name="note"/>
</sheet>
</form>
</field>
</record>
<record id="mail_activity_view_kanban" model="ir.ui.view">
<field name="name">mail.activity.view.kanban</field>
<field name="model">mail.activity</field>
<field name="arch" type="xml">
<kanban class="o_kanban_small_column">
<field name="summary"/>
<field name="note"/>
<field name="active"/>
<field name="res_id"/>
<field name="res_model"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown">
<a class="dropdown-toggle btn" data-toggle="dropdown" href="#">
<span class="fa fa-ellipsis-v"/>
</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
<li><a type="delete">Delete</a></li>
</ul>
</div>
<div class="oe_kanban_content">
<div>
<div t-attf-class="#{record.active.raw_value ? '' : 'o_text_line_through'} o_reminder_title">
<strong><field name="summary"/></strong>
</div>
<div class="row">
<div class="col-xs-10">
<span class="text-muted"><span class="fa fa-clock-o"/> <field name="date_deadline"/></span>
</div>
<div class="col-xs-2">
<a name="toggle_active" type="object" t-if="record.active.raw_value" title="Mark as done" class="pull-right"><i class="fa fa-check"/></a>
<a name="toggle_active" type="object" t-if="!record.active.raw_value" title="Re-open reminder" class="pull-right"><i class="fa fa-undo"/></a>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="mail_activity_view_search_reminder" model="ir.ui.view">
<field name="name">mail.activity.reminder.view.search</field>
<field name="model">mail.activity</field>
<field name="arch" type="xml">
<search string="Activity">
<field name="active"/>
<field name="note"/>
<separator/>
<filter string="Active" name="filter_open_reminders"
domain="[('active', '=', True)]"/>
<filter string="Archived" name="filter_closed_reminders"
domain="[('active', '=', False)]"/>
<separator/>
<filter string="Late Reminders" name="activities_overdue" domain="[('date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]" help="Show all reminders for which the next action date is before today"/>
<filter string="Today Reminders" name="activities_today" domain="[('date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
<filter string="Future Reminders" name="activities_upcoming_all" domain="[('date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
</search>
</field>
</record>
<record id="mail_activity_action_reminders" model="ir.actions.act_window">
<field name="name">Reminders</field>
<field name="res_model">mail.activity</field>
<field name="view_type">form</field>
<field name="search_view_id" ref="mail_activity_view_search_reminder"/>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('mail_activity_view_kanban')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('mail_activity_view_form_reminder')})]"/>
<field name="view_id" ref="mail.mail_activity_view_kanban"/>
<field name="view_mode">kanban,form</field>
<field name="domain">[('user_id', '=', uid), ('res_model', '=', False), ('res_id', 'in', [0,None])]</field>
</record>
</odoo>
+52 -4
View File
@@ -8,7 +8,7 @@ from odoo import exceptions
from odoo.addons.test_mail.tests.common import BaseFunctionalTest
from odoo.tools import mute_logger
class TestMailActivity(BaseFunctionalTest):
@classmethod
@@ -24,7 +24,7 @@ class TestMailActivity(BaseFunctionalTest):
# employee record an activity and check the deadline
self.env['mail.activity'].create({
'summary': 'Test Activity',
'date_deadline': date.today() + relativedelta(days=1),
'date_deadline': date.today() + relativedelta(days=1),
'activity_type_id': self.env.ref('mail.mail_activity_data_email').id,
'res_model_id': self.env['ir.model']._get(test_record._name).id,
'res_id': test_record.id,
@@ -32,11 +32,11 @@ class TestMailActivity(BaseFunctionalTest):
self.assertEqual(test_record.activity_summary, 'Test Activity')
self.assertEqual(test_record.activity_state, 'planned')
test_record.activity_ids.write({'date_deadline': date.today() - relativedelta(days=1)})
test_record.activity_ids.write({'date_deadline': date.today() - relativedelta(days=1)})
test_record.invalidate_cache() # TDE note: should not have to do it I think
self.assertEqual(test_record.activity_state, 'overdue')
test_record.activity_ids.write({'date_deadline': date.today()})
test_record.activity_ids.write({'date_deadline': date.today()})
test_record.invalidate_cache() # TDE note: should not have to do it I think
self.assertEqual(test_record.activity_state, 'today')
@@ -65,3 +65,51 @@ class TestMailActivity(BaseFunctionalTest):
'res_id': test_record.id,
})
# self.assertEqual(test_record.activity_ids, self.env['mail.activity'])
def test_reminder_security(self):
admin = self.env.ref('base.user_root')
demo = self.env.ref('base.user_demo')
reminder = self.env['mail.activity'].sudo(admin).create({
'note': 'Test Reminder',
'date_deadline': date.today() + relativedelta(days=1),
}) #try to delete record with demo user
with self.assertRaises(exceptions.AccessError):
#try to delete admin record with demo user
reminder.sudo(demo).unlink()
with self.assertRaises(exceptions.AccessError):
#try to update admin record with demo user
reminder.sudo(demo).write({'note': 'Give money to demo user'})
#but demo should be able to edit and delete its own reminder
demo_reminder = self.env['mail.activity'].sudo(demo).create({
'note': 'Test Reminder demo',
'date_deadline': date.today() + relativedelta(days=2),
})
demo_reminder.sudo(demo).write({'note': 'Holidays'})
self.assertEqual(demo_reminder.note, '<p>Holidays</p>')
#but edit or res_id and res_model should be impossible
with self.assertRaises(exceptions.AccessError):
demo_reminder.sudo(demo).write({'res_id': 1})
with self.assertRaises(exceptions.AccessError):
demo_reminder.sudo(demo).write({'res_model_id': 1})
#user can unlink own reminder
demo_reminder.sudo(demo).unlink()
def test_reminder_summary(self):
reminder = self.env['mail.activity'].create({
'note': 'Test Reminder',
'date_deadline': date.today(),
})
self.assertEqual(reminder.summary, 'Test Reminder', "Summary should be first line of note by default")
reminder.write({'note': 'Holidays\nDestination: Pescara'})
self.assertEqual(reminder.summary, 'Holidays', "Summary should be first line of note by default")
reminder.write({'note': ''})
self.assertEqual(reminder.summary, 'Reminder', "If note is empty, summary should be Reminder")
reminder.write({'note': 'Holidays', 'summary': 'Summary'})
self.assertEqual(reminder.summary, 'Summary', "If summary is set, shouldn't be changed")