[IMP] mail, calendar: add today meetings in systray

Purpose of this commit is to display today meetings in the systray. This will
help people in their daily job to have quickly access to their meetings.
It is displayed along with activities and reminders to have all current work
to do in the same place.

Displayed meetings are ongoing or not yet started ones of the current day.
All-day meetings of the current day are also shown. Done meetings are not
displayed to lessen noise.

Clicking on the systray will open the calendar view of meetings in a day
mode. This is done by a small modification in the calendar view allowing
to choose the display mode from context in addition to the mode attribute
of the action definition.

This commit is linked to task ID 60593. Closes #24438. Thanks to @ged-odoo
for quick reviewing.
This commit is contained in:
Barad Mahendra
2018-05-31 16:49:56 +02:00
committed by Thibault Delavallée
parent 642970494e
commit 4a59f93d15
15 changed files with 288 additions and 6 deletions
+1
View File
@@ -7,3 +7,4 @@ from . import res_partner
from . import mail_message
from . import calendar
from . import mail_activity
from . import res_users
+43
View File
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from odoo import api, fields, models, modules, _
class Users(models.Model):
_inherit = 'res.users'
def _systray_get_calendar_event_domain(self):
start_dt = datetime.datetime.utcnow()
start_date = datetime.date.today()
end_dt = datetime.datetime.combine(start_date, datetime.time.max)
return ['&', '|',
'&',
'|', ['start', '>=', fields.Datetime.to_string(start_dt)], ['stop', '>=', fields.Datetime.to_string(start_dt)],
['start', '<=', fields.Datetime.to_string(end_dt)],
'&', ['allday', '=', True], ['start_date', '=', fields.Date.to_string(start_date)],
'&', ('attendee_ids.partner_id', '=', self.env.user.partner_id.id), ('attendee_ids.state', '!=', 'declined')]
@api.model
def systray_get_activities(self):
res = super(Users, self).systray_get_activities()
meetings_lines = self.env['calendar.event'].search_read(
self._systray_get_calendar_event_domain(),
['id', 'start', 'name', 'allday'],
order='start')
if meetings_lines:
meeting_label = _("Today's Meetings")
meetings_systray = {
'type': 'meeting',
'name': meeting_label,
'model': 'calendar.event',
'icon': modules.module.get_module_icon(self.env['calendar.event']._original_module),
'meetings': meetings_lines,
}
res.insert(0, meetings_systray)
return res
+79
View File
@@ -0,0 +1,79 @@
odoo.define('calendar.systray', function (require) {
"use strict";
var ActivityMenu = require('mail.systray').ActivityMenu;
var fieldUtils = require('web.field_utils');
ActivityMenu.include({
events: _.extend({}, ActivityMenu.prototype.events, {
'click .o_meeting_filter': '_onMeetingFilterClick'
}),
//-----------------------------------------
// Private
//-----------------------------------------
/**
* parse date to server value
*
* @private
* @override
*/
_getActivityData: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
var meeting = _.find(self.activities, {type: 'meeting'});
if (meeting && meeting.meetings) {
_.each(meeting.meetings, function (res) {
res.start = fieldUtils.parse.datetime(res.start, false, {isUTC: true});
});
}
});
},
//-----------------------------------------
// Handlers
//-----------------------------------------
/**
* @private
* @override
*/
_onActivityFilterClick: function (ev) {
var $el = $(ev.currentTarget);
var data = _.extend({}, $el.data());
if (data.res_model === "calendar.event" && data.filter === "my") {
this.do_action('calendar.action_calendar_event', {
additional_context: {
default_mode: 'day',
search_default_mymeetings: 1,
}
});
} else {
this._super.apply(this, arguments);
}
},
/**
* When particular meeting clicked, open particular meeting in form view
*
* @private
* @param {MouseEvent} ev
*/
_onMeetingFilterClick: function (ev) {
ev.stopPropagation();
var $el = $(ev.currentTarget);
var data = _.extend({}, $el.data());
if (data.res_model === "calendar.event") {
this.do_action({
type: 'ir.actions.act_window',
name: data.model_name,
res_model: data.res_model,
res_id: data.res_id,
views: [[false, 'form'], [false, 'calendar'], [false, 'list']],
});
}
},
});
});
@@ -77,3 +77,16 @@
color: $brand-danger;
}
}
.o_meeting_filter {
@include text-overflow();
width: 64%;
color: grey;
display: inline-block;
vertical-align: top;
&:hover {
color: $o-brand-optional;
}
&.o_meeting_bold {
font-weight: bold;
}
}
@@ -20,4 +20,29 @@
</t>
</t>
<t t-extend="mail.chat.ActivityMenuPreview">
<t t-jquery="div.o_channel_title" t-operation="after">
<div t-if="activity and activity.type == 'meeting'">
<t t-set="is_next_meeting" t-value="true"/>
<t t-foreach="activity.meetings" t-as="meeting">
<div>
<span t-att-class="!meeting.allday and is_next_meeting ? 'o_meeting_filter o_meeting_bold' : 'o_meeting_filter'" t-att-data-res_model="activity.model" t-att-data-res_id="meeting.id" t-att-data-model_name="activity.name" t-att-title="meeting.name">
<span t-if="!meeting.allday">
<t t-esc="moment(meeting.start).local().format('hh:mm A')"/>
</span>
<span><t t-esc="meeting.name"/></span>
</span>
<span t-if="meeting.start" class="pull-right">
<t t-if="meeting.allday">All Day</t>
<t t-else=''>
<t t-set="is_next_meeting" t-value="false"/>
<t t-esc="moment(meeting.start).fromNow()"/>
</t>
</span>
</div>
</t>
</div>
</t>
</t>
</template>
@@ -0,0 +1,77 @@
odoo.define('calendar.systray_tests', function (require) {
"use strict";
var ChatManager = require('mail.ChatManager');
var systray = require('mail.systray');
var testUtils = require('web.test_utils');
var createBusService = require('mail.testUtils').createBusService;
QUnit.module('calendar', {}, function () {
QUnit.module('ActivityMenu', {
beforeEach: function () {
this.services = [ChatManager, createBusService()];
this.data = {
'calendar.event': {
records: [{
name: "Today's meeting (3)",
model: "calendar.event",
type: 'meeting',
meetings: [{
id: 1,
res_model: "calendar.event",
name: "meeting1",
start: "2018-04-20 06:30:00",
allday: false,
},{
id: 2,
res_model: "calendar.event",
name: "meeting2",
start: "2018-04-20 09:30:00",
allday: false,
}]
}],
},
};
}
});
QUnit.test('activity menu widget:today meetings', function (assert) {
assert.expect(8);
var self = this;
var activityMenu = new systray.ActivityMenu();
testUtils.addMockEnvironment(activityMenu, {
services: this.services,
mockRPC: function (route, args) {
if (args.method === 'systray_get_activities') {
return $.when(self.data['calendar.event']['records']);
}
return this._super(route, args);
},
});
activityMenu.appendTo($('#qunit-fixture'));
assert.ok(activityMenu.$el.hasClass('o_mail_navbar_item'), 'should be the instance of widget');
testUtils.intercept(activityMenu, 'do_action', function (event) {
assert.ok(true, "should have triggered a do_action");
assert.strictEqual(event.data.action.res_id, activityMenu.$('.o_meeting_filter').eq(0).data('res_id'), 'should open particular meeting form view')
});
activityMenu.$('.dropdown-toggle').click();
activityMenu.$('.o_meeting_filter').eq(0).click();
testUtils.intercept(activityMenu, 'do_action', function (event) {
assert.strictEqual(event.data.action, "calendar.action_calendar_event", 'should open meeting calendar view in day mode')
});
activityMenu.$('.o_mail_channel_preview').click();
assert.ok(activityMenu.$('.o_meeting_filter'), "should be a meeting");
assert.strictEqual(activityMenu.$('.o_meeting_filter').length, 2, 'there should be 2 meetings');
assert.strictEqual(activityMenu.$('.o_meeting_filter').eq(0).hasClass('o_meeting_bold'), true, 'this meeting is yet to start');
assert.strictEqual(activityMenu.$('.o_meeting_filter').eq(1).hasClass('o_meeting_bold'), false, 'this meeting has been started');
activityMenu.destroy();
});
});
});
@@ -5,12 +5,14 @@
<link rel="stylesheet" type="text/scss" href="/calendar/static/src/scss/calendar.scss"/>
<script type="text/javascript" src="/calendar/static/src/js/base_calendar.js"></script>
<script type="text/javascript" src="/calendar/static/src/js/mail_activity.js"></script>
<script type="text/javascript" src="/calendar/static/src/js/systray.js"></script>
</xpath>
</template>
<template id="qunit_suite" inherit_id="web.qunit_suite">
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/calendar/static/tests/calendar_tests.js"/>
<script type="text/javascript" src="/calendar/static/tests/systray_tests.js"/>
</xpath>
</template>
+36
View File
@@ -321,6 +321,42 @@
</field>
</record>
<!-- <record id="view_calendar_today_event_calendar" model="ir.ui.view">
<field name="name">calendar.event.calendar</field>
<field name="model">calendar.event</field>
<field name="priority" eval="2"/>
<field name="arch" type="xml">
<calendar string="Meetings" date_start="start" date_stop="stop" date_delay="duration" all_day="allday"
readonly_form_view_id="%(calendar.view_calendar_event_form_popup)s"
event_open_popup="true"
event_limit="3"
color="partner_id"
mode="day">
<field name="name"/>
<field name="partner_ids" write_model="calendar.contacts" write_field="partner_id" avatar_field="image_small"/>
<field name="is_highlighted" invisible="1"/>
</calendar>
</field>
</record> -->
<!-- <record id="action_today_calendar_event" model="ir.actions.act_window">
<field name="name">Today's Meetings</field>
<field name="res_model">calendar.event</field>
<field name="view_mode">calendar,tree,form</field>
<field name="view_id" ref="view_calendar_today_event_calendar"/>
<field name="search_view_id" ref="view_calendar_event_search"/>
<field name="context">{'search_default_mymeetings': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Schedule a new meeting
</p><p>
The calendar is shared between employees and fully integrated with
other applications such as the employee leaves or the business
opportunities.
</p>
</field>
</record> -->
<record id="action_view_calendar_event_calendar" model="ir.actions.act_window.view">
<field name="act_window_id" ref="action_calendar_event"/>
<field name="sequence" eval="1"/>
+1
View File
@@ -98,6 +98,7 @@ class Users(models.Model):
user_activities[activity['model']] = {
'name': model_names[activity['id']],
'model': activity['model'],
'type': 'activity',
'icon': modules.module.get_module_icon(self.env[activity['model']]._original_module),
'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0,
}
+2 -3
View File
@@ -180,9 +180,8 @@ var ActivityMenu = Widget.extend({
return self._rpc({
model: 'res.users',
method: 'systray_get_activities',
kwargs: {
context: session.user_context,
},
args: [],
kwargs: {context: session.user_context},
}).then(function (data) {
self.activities = data;
self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count; }, 0);
+2 -1
View File
@@ -67,10 +67,11 @@
@media (min-width: $screen-sm-min) {
.o_mail_channel_preview {
height: 50px;
min-height: 50px;
padding: 5px;
.o_mail_channel_image {
width: 40px;
height: 40px;
}
.o_channel_info {
margin-left: 10px;
+1 -1
View File
@@ -69,7 +69,7 @@
<t t-esc="activity.name"/>
</span>
</div>
<div>
<div t-if="activity and activity.type == 'activity'">
<button t-if="activity.overdue_count" type="button" class="btn btn-link o_activity_filter_button mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='overdue'><t t-esc="activity.overdue_count"/> Late </button>
<span t-if="!activity.overdue_count" class="o_no_activity mr16">0 Late </span>
<button t-if="activity.today_count" type="button" class="btn btn-link o_activity_filter_button mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='today'> <t t-esc="activity.today_count"/> Today </button>
@@ -20,6 +20,7 @@ QUnit.module('ActivityMenu', {
fields: {
name: { type: "char" },
model: { type: "char" },
type: { type: "char" },
planned_count: { type: "integer"},
today_count: { type: "integer"},
overdue_count: { type: "integer"},
@@ -28,6 +29,7 @@ QUnit.module('ActivityMenu', {
records: [{
name: "Contact",
model: "res.partner",
type: "activity",
planned_count: 0,
today_count: 1,
overdue_count: 0,
@@ -35,6 +37,7 @@ QUnit.module('ActivityMenu', {
},
{
name: "Task",
type: "activity",
model: "project.task",
planned_count: 1,
today_count: 0,
@@ -43,6 +46,7 @@ QUnit.module('ActivityMenu', {
},
{
name: "Issue",
type: "activity",
model: "project.issue",
planned_count: 1,
today_count: 1,
+1
View File
@@ -63,6 +63,7 @@ GROUP BY id"""
activities[note_index]['name'] = note_label
else:
activities.append({
'type': 'activity',
'name': note_label,
'model': 'note.note',
'icon': modules.module.get_module_icon(self.env['note.note']._original_module),
@@ -152,7 +152,7 @@ var CalendarView = AbstractView.extend({
this.loadParams.fieldColor = attrs.color;
this.loadParams.filters = filters;
this.loadParams.mode = attrs.mode;
this.loadParams.mode = (params.context && params.context.default_mode) || attrs.mode;
this.loadParams.initialDate = moment(params.initialDate || new Date());
},
});