[IMP] calendar: rework recurrence in UI

This commit changes the way we define recurrences from the UI so it's simpler.
Instead of showing the user many options for recurrence like we currently do,
we add a selection field for each rrule (daily, weekly, monthly and yearly)
that sets some default values for the recurrence without the user having to
define each field manually. This change makes the definition of recurrence from
the UI closer to what is done in google calendar. In order to allow users to
define more advanced types of recurrence, we also add a custom option in the
new selection field that will show the old recurrence options to the user.

Additionally, the week days widget defined in web takes too much space, so it
was decided to create an overwrite of this widget for calendar only that looks
cleaner and takes less space. This commit introduces this new widget and
applies it to the calendar event form view.

task-3234677

closes odoo/odoo#116649

Signed-off-by: Arnaud Joset (arj) <arj@odoo.com>
This commit is contained in:
Leonardo Pavan Rocha
2023-09-04 15:02:46 +00:00
parent e28e1a079f
commit 064213901b
9 changed files with 178 additions and 56 deletions
+38 -5
View File
@@ -3,7 +3,7 @@
import logging
import math
from datetime import timedelta
from datetime import datetime, timedelta
from itertools import repeat
from werkzeug.urls import url_parse
@@ -40,6 +40,13 @@ SORT_ALIASES = {
'start_date': 'sort_start',
}
RRULE_TYPE_SELECTION_UI = [
('daily', 'Daily'),
('weekly', 'Weekly'),
('monthly', 'Monthly'),
('yearly', 'Yearly'),
('custom', 'Custom')
]
def get_weekday_occurence(date):
"""
@@ -99,6 +106,17 @@ class Meeting(models.Model):
partners |= self.env['res.partner'].browse(active_id)
return partners
@api.model
def _default_start(self):
now = fields.Datetime.now()
return now + (datetime.min - now) % timedelta(minutes=30)
@api.model
def _default_stop(self):
now = fields.Datetime.now()
start = now + (datetime.min - now) % timedelta(minutes=30)
return start + timedelta(hours=1)
# description
name = fields.Char('Meeting Subject', required=True)
description = fields.Html('Description')
@@ -138,10 +156,10 @@ class Meeting(models.Model):
'calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags')
# timing
start = fields.Datetime(
'Start', required=True, tracking=True, default=fields.Date.today,
'Start', required=True, tracking=True, default=_default_start,
help="Start date of an event, without time for full days events")
stop = fields.Datetime(
'Stop', required=True, tracking=True, default=lambda self: fields.Datetime.today() + timedelta(hours=1),
'Stop', required=True, tracking=True, default=_default_stop,
compute='_compute_stop', readonly=False, store=True,
help="Stop date of an event, without time for full days events")
display_time = fields.Char('Event Time', compute='_compute_display_time')
@@ -189,6 +207,10 @@ class Meeting(models.Model):
# If some of these fields are set and recurrence_id does not exists,
# a `calendar.recurrence.rule` will be dynamically created.
rrule = fields.Char('Recurrent Rule', compute='_compute_recurrence', readonly=False)
rrule_type_ui = fields.Selection(RRULE_TYPE_SELECTION_UI, string='Repeat',
compute="_compute_rrule_type_ui",
readonly=False,
help="Let the event automatically repeat at that interval")
rrule_type = fields.Selection(RRULE_TYPE_SELECTION, string='Recurrence',
help="Let the event automatically repeat at that interval",
compute='_compute_recurrence', readonly=False)
@@ -198,10 +220,10 @@ class Meeting(models.Model):
END_TYPE_SELECTION, string='Recurrence Termination',
compute='_compute_recurrence', readonly=False)
interval = fields.Integer(
string='Repeat Every', compute='_compute_recurrence', readonly=False,
string='Repeat On', compute='_compute_recurrence', readonly=False,
help="Repeat every (Days/Week/Month/Year)")
count = fields.Integer(
string='Repeat', help="Repeat x times", compute='_compute_recurrence', readonly=False)
string='Number of Repetitions', help="Repeat x times", compute='_compute_recurrence', readonly=False)
mon = fields.Boolean(compute='_compute_recurrence', readonly=False)
tue = fields.Boolean(compute='_compute_recurrence', readonly=False)
wed = fields.Boolean(compute='_compute_recurrence', readonly=False)
@@ -373,6 +395,16 @@ class Meeting(models.Model):
)
@api.depends('recurrence_id', 'recurrency')
def _compute_rrule_type_ui(self):
defaults = self.env["calendar.recurrence"].default_get(["interval", "rrule_type"])
for event in self:
if event.recurrency:
if event.recurrence_id:
event.rrule_type_ui = 'custom' if event.recurrence_id.interval != 1 else (event.recurrence_id.rrule_type)
else:
event.rrule_type_ui = defaults["rrule_type"]
@api.depends('recurrence_id', 'recurrency', 'rrule_type_ui')
def _compute_recurrence(self):
recurrence_fields = self._get_recurrent_fields()
false_values = {field: False for field in recurrence_fields} # computes need to set a value
@@ -388,6 +420,7 @@ class Meeting(models.Model):
if event.recurrence_id[field]
}
rrule_values = rrule_values or default_rrule_values
rrule_values['rrule_type'] = (event.rrule_type if event.rrule_type_ui == "custom" else event.rrule_type_ui) or defaults.pop('rrule_type')
event.update({**false_values, **defaults, **event_values, **rrule_values})
else:
event.update(false_values)
@@ -0,0 +1,18 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { WeekDays, weekDays } from "@web/views/widgets/week_days/week_days";
export class CalendarWeekDays extends WeekDays {
onChange(day) {
this.props.record.update({ [day]: !this.data[day] });
}
};
CalendarWeekDays.template = 'calendar.WeekDays';
export const calendarWeekDays = {
component: CalendarWeekDays,
fieldDependencies: weekDays.fieldDependencies,
};
registry.category("view_widgets").add("calendar_week_days", calendarWeekDays);
@@ -0,0 +1,10 @@
.o_calendar_week_days_rounded {
border-radius: 50%;
margin-right: 8px;
width: 24px;
height: 24px;
justify-content: center;
display: flex;
cursor: pointer;
align-items: center;
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="calendar.WeekDays" owl="1">
<div class="o_recurrent_weekdays mt-2 mb-2 d-flex">
<t t-foreach="weekdays" t-as="day" t-key="day">
<div t-attf-class="o_calendar_week_days_rounded {{data[day] ? 'btn-primary' : 'btn-secondary'}}" t-on-click="() => this.onChange(day)">
<t t-esc="props.record.fields[day].string[0]"/>
</div>
</t>
</div>
</t>
</templates>
@@ -21,7 +21,7 @@ class TestRecurrentEvent(common.TransactionCase):
'duration': 1.0,
'name': 'Test Meeting',
'recurrency': True,
'rrule_type': 'daily'
'rrule_type': 'daily',
})
# I search for all the recurrent meetings
meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([
@@ -45,7 +45,7 @@ class TestRecurrentEvent(common.TransactionCase):
'wed': True,
'name': 'Review code with programmer',
'recurrency': True,
'rrule_type': 'weekly'
'rrule_type': 'weekly',
})
# I search for all the recurrent weekly meetings.
+48 -5
View File
@@ -497,7 +497,7 @@ class TestUpdateRecurrentEvents(TestRecurrentEvents):
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
recurrence = self.env['calendar.recurrence'].search([])
recurrence = self.env['calendar.recurrence'].search([], limit=1)
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 26, 1, 0), datetime(2019, 10, 29, 18, 0)),
(datetime(2019, 11, 2, 1, 0), datetime(2019, 11, 5, 18, 0)),
@@ -587,7 +587,7 @@ class TestUpdateRecurrentEvents(TestRecurrentEvents):
'recurrence_update': 'all_events',
'mon': True, # recurrence is now Tuesday AND Monday
})
recurrence = self.env['calendar.recurrence'].search([])
recurrence = self.env['calendar.recurrence'].search([], limit=1)
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 22, 1, 0), datetime(2019, 10, 24, 18, 0)),
(datetime(2019, 10, 28, 1, 0), datetime(2019, 10, 30, 18, 0)),
@@ -748,7 +748,7 @@ class TestUpdateMultiDayWeeklyRecurrentEvents(TestRecurrentEvents):
'start': event.start + relativedelta(days=2),
'stop': event.stop + relativedelta(days=2),
})
recurrence = self.env['calendar.recurrence'].search([])
recurrence = self.env['calendar.recurrence'].search([], limit=1)
# We don't try to do magic tricks. First event is moved, other remain
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 24, 1, 0), datetime(2019, 10, 26, 18, 0)),
@@ -766,7 +766,7 @@ class TestUpdateMultiDayWeeklyRecurrentEvents(TestRecurrentEvents):
'start': event.start + relativedelta(days=2),
'stop': event.stop + relativedelta(days=3),
})
recurrence = self.env['calendar.recurrence'].search([])
recurrence = self.env['calendar.recurrence'].search([], limit=1)
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 24, 1, 0), datetime(2019, 10, 27, 18, 0)),
(datetime(2019, 10, 31, 1, 0), datetime(2019, 11, 3, 18, 0)),
@@ -819,7 +819,7 @@ class TestUpdateMonthlyByDay(TestRecurrentEvents):
'start': event.start + relativedelta(hours=5),
'stop': event.stop + relativedelta(hours=5),
})
recurrence = self.env['calendar.recurrence'].search([])
recurrence = self.env['calendar.recurrence'].search([], limit=1)
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 15, 6, 0), datetime(2019, 10, 16, 23, 0)),
(datetime(2019, 11, 19, 6, 0), datetime(2019, 11, 20, 23, 0)),
@@ -877,3 +877,46 @@ class TestUpdateMonthlyByDate(TestRecurrentEvents):
(datetime(2019, 11, 25, 1, 0), datetime(2019, 11, 27, 18, 0)),
(datetime(2019, 12, 25, 1, 0), datetime(2019, 12, 27, 18, 0)),
])
def test_recurring_ui_options_daily(self):
with Form(self.env['calendar.event']) as calendar_form:
calendar_form.name = 'test recurrence daily'
calendar_form.recurrency = True
calendar_form.rrule_type_ui = 'daily'
calendar_form.count = 2
calendar_form.start = datetime(2019, 6, 23, 16)
calendar_form.stop = datetime(2019, 6, 23, 17)
event = calendar_form.save()
self.assertEventDates(event.recurrence_id.calendar_event_ids, [
(datetime(2019, 6, 23, 16), datetime(2019, 6, 23, 17)),
(datetime(2019, 6, 24, 16, 0), datetime(2019, 6, 24, 17)),
])
def test_recurring_ui_options_monthly(self):
with Form(self.env['calendar.event']) as calendar_form:
calendar_form.name = 'test recurrence monthly'
calendar_form.recurrency = True
calendar_form.rrule_type_ui = 'monthly'
calendar_form.count = 2
calendar_form.start = datetime(2019, 6, 11, 16)
calendar_form.stop = datetime(2019, 6, 11, 17)
calendar_form.day = 11
event = calendar_form.save()
self.assertEventDates(event.recurrence_id.calendar_event_ids, [
(datetime(2019, 6, 11, 16), datetime(2019, 6, 11, 17)),
(datetime(2019, 7, 11, 16), datetime(2019, 7, 11, 17)),
])
def test_recurring_ui_options_yearly(self):
with Form(self.env['calendar.event']) as calendar_form:
calendar_form.name = 'test recurrence yearly'
calendar_form.recurrency = True
calendar_form.rrule_type_ui = 'yearly'
calendar_form.count = 2
calendar_form.start = datetime(2019, 6, 11, 16)
calendar_form.stop = datetime(2019, 6, 11, 17)
event = calendar_form.save()
self.assertEventDates(event.recurrence_id.calendar_event_ids, [
(datetime(2019, 6, 11, 16), datetime(2019, 6, 11, 17)),
(datetime(2020, 6, 11, 16), datetime(2020, 6, 11, 17)),
])
+48 -44
View File
@@ -171,14 +171,14 @@
<field name="privacy" readonly="not user_can_edit" nolabel="1"/>
</div>
<field name="user_id" widget="many2one_avatar_user" readonly="not user_can_edit"/>
<field name="description"/>
<field name="description" options="{'height': 100}"/>
</group>
<group>
<div class="o_row" colspan="2">
<div colspan="1">
<field name="attendees_count" class="w-auto oe_inline" nolabel="1"/><span> Attendees</span>
</div>
<div name="send_buttons" class="d-flex gap-1" colspan="1">
<div name="send_buttons" class="sm-2" colspan="1">
<button name="action_open_composer" help="Send Email to attendees" type="object" string=" EMAIL" icon="fa-envelope"/>
</div>
</div>
@@ -192,8 +192,10 @@
<field name="partner_ids" widget="many2manyattendee"
placeholder="Select attendees..."
context="{'force_email':True}"
domain="[('type','!=','private')]"
class="oe_inline"
readonly="not user_can_edit"/>
readonly="not user_can_edit"
/>
</div>
<div class="alert alert-warning o_form_header mt-2" invisible="not invalid_email_partner_ids" role="status">
<p><strong>The following attendees have invalid email addresses and won't receive any email notifications:</strong></p>
@@ -202,53 +204,55 @@
</group>
</group>
</page>
<page name="page_options" string="Options">
<group>
<div>
<group>
<field name="recurrency" readonly="not user_can_edit"/>
<field name="recurrence_id" invisible="1" />
<field name="rrule_type" invisible="1" />
<group class="mb-0">
<field name="recurrency" />
<field name="rrule_type_ui" class="w-auto" invisible="not recurrency" required="recurrency"/>
</group>
<newline />
<group invisible="not recurrency">
<group class="o_row" invisible="rrule_type_ui != 'custom'">
<field name="interval" string="Repeat every" class="oe_inline w-auto" />
<field name="rrule_type" nolabel="1" class="oe_inline" required="rrule_type_ui == 'custom'"/>
</group>
<div invisible="not recurrency">
<group>
<label for="interval"/>
<div class="o_col">
<div class="o_row">
<field name="interval" class="oe_inline" readonly="not user_can_edit" required="recurrency"/>
<field name="rrule_type" readonly="not user_can_edit" required="recurrency"/>
</div>
<widget name="week_days" invisible="rrule_type != 'weekly'" readonly="not user_can_edit"/>
</div>
<label string="Until" for="end_type"/>
<div class="o_row">
<field name="end_type" readonly="not user_can_edit" required="recurrency"/>
<field name="count" invisible="end_type != 'count'" readonly="not user_can_edit" required="recurrency"/>
<field name="until" invisible="end_type != 'end_date'" readonly="not user_can_edit" required="end_type == 'end_date' and recurrency"/>
</div>
</group>
<group invisible="rrule_type != 'monthly'">
<label string="Day of Month" for="month_by"/>
<div class="o_row">
<field name="month_by"/>
<field name="day"
invisible="month_by != 'date'"
readonly="not user_can_edit"
required="month_by == 'date' and rrule_type == 'monthly'"/>
<field name="byday" string="The"
invisible="month_by != 'day'"
readonly="not user_can_edit"
required="recurrency and month_by == 'day' and rrule_type == 'monthly'"/>
<field name="weekday" nolabel="1"
invisible="month_by != 'day'"
readonly="not user_can_edit"
required="recurrency and month_by == 'day' and rrule_type == 'monthly'"/>
</div>
</group>
</div>
</div>
</group>
<newline />
<group class="gap-0" invisible="rrule_type_ui not in ['weekly', 'custom'] or (rrule_type_ui == 'custom' and rrule_type != 'weekly')">
<span class="fw-bold pe-3">Repeat on</span>
<widget name="calendar_week_days" />
</group>
<group>
<field name="recurrence_id" invisible="1" />
</group>
</group>
<div class="d-flex gap-2" invisible="rrule_type_ui not in ['monthly', 'custom'] or (rrule_type_ui == 'custom' and rrule_type != 'monthly')">
<label string="Day of Month" for="month_by" />
<field name="month_by" nolabel="1" required="rrule_type_ui == 'monthly' or rrule_type == 'monthly'"/>
<field name="day" nolabel="1"
required="(rrule_type_ui == 'monthly' or rrule_type == 'monthly') and month_by == 'date'"
invisible="month_by != 'date'"
/>
<field name="byday" string="The" nolabel="1"
required="(rrule_type_ui == 'monthly' or rrule_type == 'monthly') and month_by == 'day'"
invisible="month_by != 'day'"
/>
<field name="weekday" nolabel="1"
required="(rrule_type_ui == 'monthly' or rrule_type == 'monthly') and month_by == 'day'"
invisible="month_by != 'day'"
/>
</div>
<div class="d-flex gap-2" invisible="not recurrency">
<label string="Until" for="end_type" />
<field name="end_type" class="oe_inline w-auto" nolabel="1" required="recurrency"/>
<field name="count" class="oe_inline w-auto" nolabel="1" invisible="end_type != 'count'" required="recurrency"/>
<field name="until" class="oe_inline w-auto" nolabel="1" invisible="end_type != 'end_date'"
required="recurrency and end_type == 'end_date'"
/>
</div>
</page>
<page name="page_invitations" string="Invitations" groups="base.group_no_one" invisible="not user_can_edit">
@@ -320,7 +324,7 @@
</group>
<group>
<field name="partner_ids" widget="many2manyattendee"
placeholder="Select attendees..."
placeholder="Add attendees..."
context="{'force_email':True}"
class="oe_inline"
/>
@@ -4,6 +4,7 @@ $o-cw-popup-avatar-size: 16px;
.o_cw_popover {
min-width: 256px;
max-width: 328px;
z-index: $zindex-modal - 1;
font-size: $font-size-base;
.card-header,
@@ -160,6 +160,7 @@ export class CalendarController extends Component {
return {
resModel: this.model.resModel,
viewId: this.model.quickCreateFormViewId,
title: _t("New Event"),
context,
};
}