From 064213901bd817b37f039a4c95be253dcf7a35df Mon Sep 17 00:00:00 2001 From: Leonardo Pavan Rocha Date: Tue, 7 Mar 2023 19:05:01 +0100 Subject: [PATCH] [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) --- addons/calendar/models/calendar_event.py | 43 ++++++++- .../src/views/widgets/calendar_week_days.js | 18 ++++ .../src/views/widgets/calendar_week_days.scss | 10 ++ .../src/views/widgets/calendar_week_days.xml | 12 +++ .../test_calendar_recurrent_event_case2.py | 4 +- .../calendar/tests/test_event_recurrence.py | 53 ++++++++++- addons/calendar/views/calendar_views.xml | 92 ++++++++++--------- .../calendar_common_popover.scss | 1 + .../src/views/calendar/calendar_controller.js | 1 + 9 files changed, 178 insertions(+), 56 deletions(-) create mode 100644 addons/calendar/static/src/views/widgets/calendar_week_days.js create mode 100644 addons/calendar/static/src/views/widgets/calendar_week_days.scss create mode 100644 addons/calendar/static/src/views/widgets/calendar_week_days.xml diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py index 5ba62bf7744..d0bab630603 100644 --- a/addons/calendar/models/calendar_event.py +++ b/addons/calendar/models/calendar_event.py @@ -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) diff --git a/addons/calendar/static/src/views/widgets/calendar_week_days.js b/addons/calendar/static/src/views/widgets/calendar_week_days.js new file mode 100644 index 00000000000..84a4d66eb5a --- /dev/null +++ b/addons/calendar/static/src/views/widgets/calendar_week_days.js @@ -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); diff --git a/addons/calendar/static/src/views/widgets/calendar_week_days.scss b/addons/calendar/static/src/views/widgets/calendar_week_days.scss new file mode 100644 index 00000000000..1b57e10aff8 --- /dev/null +++ b/addons/calendar/static/src/views/widgets/calendar_week_days.scss @@ -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; +} diff --git a/addons/calendar/static/src/views/widgets/calendar_week_days.xml b/addons/calendar/static/src/views/widgets/calendar_week_days.xml new file mode 100644 index 00000000000..964cdd1d74a --- /dev/null +++ b/addons/calendar/static/src/views/widgets/calendar_week_days.xml @@ -0,0 +1,12 @@ + + + +
+ +
+ +
+
+
+
+
diff --git a/addons/calendar/tests/test_calendar_recurrent_event_case2.py b/addons/calendar/tests/test_calendar_recurrent_event_case2.py index 917548baf54..2701f28881d 100644 --- a/addons/calendar/tests/test_calendar_recurrent_event_case2.py +++ b/addons/calendar/tests/test_calendar_recurrent_event_case2.py @@ -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. diff --git a/addons/calendar/tests/test_event_recurrence.py b/addons/calendar/tests/test_event_recurrence.py index 48e36af6fb3..5d13e03cc7d 100644 --- a/addons/calendar/tests/test_event_recurrence.py +++ b/addons/calendar/tests/test_event_recurrence.py @@ -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)), + ]) diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml index 18dc1756eb3..718c39eb703 100644 --- a/addons/calendar/views/calendar_views.xml +++ b/addons/calendar/views/calendar_views.xml @@ -171,14 +171,14 @@ - +
Attendees
-
+
@@ -192,8 +192,10 @@ + readonly="not user_can_edit" + />

The following attendees have invalid email addresses and won't receive any email notifications:

@@ -202,53 +204,55 @@ + -
- - + + + + + + + + + + + -
- - - - -
-
+
+ + + Repeat on + + +
+
+
+
@@ -320,7 +324,7 @@ diff --git a/addons/web/static/src/views/calendar/calendar_common/calendar_common_popover.scss b/addons/web/static/src/views/calendar/calendar_common/calendar_common_popover.scss index 47fec579845..ce51c4dcc87 100644 --- a/addons/web/static/src/views/calendar/calendar_common/calendar_common_popover.scss +++ b/addons/web/static/src/views/calendar/calendar_common/calendar_common_popover.scss @@ -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, diff --git a/addons/web/static/src/views/calendar/calendar_controller.js b/addons/web/static/src/views/calendar/calendar_controller.js index 6be9de28262..f1ccfb56a41 100644 --- a/addons/web/static/src/views/calendar/calendar_controller.js +++ b/addons/web/static/src/views/calendar/calendar_controller.js @@ -160,6 +160,7 @@ export class CalendarController extends Component { return { resModel: this.model.resModel, viewId: this.model.quickCreateFormViewId, + title: _t("New Event"), context, }; }