From dbf1ab157dbc985370a03ded818945cb2ed726dd Mon Sep 17 00:00:00 2001 From: dht-odoo Date: Mon, 3 May 2021 10:00:51 +0000 Subject: [PATCH] [IMP] calendar: improves field type from text to html Replace text fields to html fields as we have our own 'OdooEditor'. Indeed, it gives more options to users in the way they format their content without weighting too much on the UI (tools appear on demand and not by default). Adding a new method convert_online_event_desc_to_text Because online events have fixed format for the description, this method removes some specific html tags, and converts it into readable plaintext (to be used in external calendars). Models -> Fields 1) calendar.event -> description Task Id: 2499504 X-original-commit: 0d5ca6a6fe8821b4ec19dd603bde116518721792 --- addons/calendar/data/mail_template_data.xml | 48 +++-------- addons/calendar/models/calendar_event.py | 85 ++++++------------- addons/calendar/models/mail_activity.py | 5 +- addons/calendar/tests/test_calendar.py | 2 +- addons/google_calendar/models/calendar.py | 4 +- .../tests/test_sync_google2odoo.py | 3 +- addons/microsoft_calendar/models/calendar.py | 5 +- 7 files changed, 50 insertions(+), 102 deletions(-) diff --git a/addons/calendar/data/mail_template_data.xml b/addons/calendar/data/mail_template_data.xml index 4da9984478d..8a1532e49ba 100644 --- a/addons/calendar/data/mail_template_data.xml +++ b/addons/calendar/data/mail_template_data.xml @@ -92,18 +92,6 @@ % if not object.event_id.allday and object.event_id.duration
  • Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}
  • % endif - % if not is_online and object.event_id.description : -
  • Description: ${object.event_id.description}
  • - % elif is_online and object.event_id.description: - % set splitted_description = object.event_id.description_to_html_lines() -
  • Description: - -
  • - % endif
  • Attendees + % if not is_html_empty(object.event_id.description): +

    Description of the event

    + ${object.event_id.description | safe} + % endif
    @@ -222,18 +214,6 @@ % if not object.event_id.allday and object.event_id.duration
  • Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}
  • % endif - % if not is_online and object.event_id.description : -
  • Description: ${object.event_id.description}
  • - % elif is_online and object.event_id.description: - % set splitted_description = object.event_id.description_to_html_lines() -
  • Description: - -
  • - % endif
  • Attendees + % if not is_html_empty(object.event_id.description): +

    Description of the event

    + ${object.event_id.description | safe} + % endif
    @@ -332,18 +316,6 @@ % if not object.event_id.allday and object.event_id.duration
  • Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}
  • % endif - % if not is_online and object.event_id.description : -
  • Description: ${object.event_id.description}
  • - % elif is_online and object.event_id.description: - % set splitted_description = object.event_id.description_to_html_lines() -
  • Description: - -
  • - % endif
  • Attendees + % if not is_html_empty(object.event_id.description): +

    Description of the event

    + ${object.event_id.description | safe} + % endif
    diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py index c8ee1a71b04..6ae4f68e7f8 100644 --- a/addons/calendar/models/calendar_event.py +++ b/addons/calendar/models/calendar_event.py @@ -9,13 +9,12 @@ import pytz from werkzeug.urls import url_join from odoo import api, fields, models -from odoo import tools from odoo.addons.base.models.res_partner import _tz_get from odoo.addons.calendar.models.calendar_attendee import Attendee from odoo.addons.calendar.models.calendar_recurrence import weekday_to_field, RRULE_TYPE_SELECTION, END_TYPE_SELECTION, MONTH_BY_SELECTION, WEEKDAY_SELECTION, BYDAY_SELECTION from odoo.tools.translate import _ from odoo.tools.misc import get_lang -from odoo.tools import pycompat, html_escape +from odoo.tools import pycompat, html2plaintext, is_html_empty from odoo.exceptions import UserError, ValidationError, AccessError _logger = logging.getLogger(__name__) @@ -96,7 +95,7 @@ class Meeting(models.Model): # description name = fields.Char('Meeting Subject', required=True) - description = fields.Text('Description') + description = fields.Html('Description') user_id = fields.Many2one('res.users', 'Organizer', default=lambda self: self.env.user) partner_id = fields.Many2one( 'res.partner', string='Scheduled by', related='user_id.partner_id', readonly=True) @@ -627,7 +626,7 @@ class Meeting(models.Model): if 'name' in fields: activity_values['summary'] = event.name if 'description' in fields: - activity_values['note'] = tools.plaintext2html(event.description) + activity_values['note'] = event.description if 'start' in fields: # self.start is a datetime UTC *only when the event is not allday* # activty.date_deadline is a date (No TZ, but should represent the day in which the user's TZ is) @@ -837,8 +836,12 @@ class Meeting(models.Model): event.add('dtstart').value = ics_datetime(meeting.start, meeting.allday) event.add('dtend').value = ics_datetime(meeting.stop, meeting.allday) event.add('summary').value = meeting.name - if meeting.description: - event.add('description').value = meeting.description + if not is_html_empty(meeting.description): + if 'appointment_type_id' in meeting._fields and self.appointment_type_id: + # convert_online_event_desc_to_text method for correct data formatting in external calendars + event.add('description').value = self.convert_online_event_desc_to_text(meeting.description) + else: + event.add('description').value = html2plaintext(meeting.description) if meeting.location: event.add('location').value = meeting.location if meeting.rrule: @@ -866,6 +869,23 @@ class Meeting(models.Model): return result + def convert_online_event_desc_to_text(self, description): + """ + We can sync the calendar events with google calendar, iCal and Outlook, and we + also pass the event description along with other data. This description needs + to be in plaintext to be displayed properly in above platforms. Because online + events have fixed format for the description, this method removes some specific + html tags, and converts it into readable plaintext (to be used in external + calendars). Note that for regular (offline) events, we simply use the standard + `html2plaintext` method instead. + """ + desc_str = str(description) + tags_to_replace = ["", "
  • "] + for tag in tags_to_replace: + desc_str = desc_str.replace(tag, "") + desc_str = desc_str.replace("
  • ", "
    ") + return html2plaintext(desc_str) + @api.model def _get_display_time(self, start, stop, zduration, zallday): """ Return date and time (from to from) based on duration with timezone in string. Eg : @@ -948,56 +968,3 @@ class Meeting(models.Model): 'id', 'active', 'allday', 'duration', 'user_id', 'interval', 'count', 'rrule', 'recurrence_id', 'show_as', 'privacy'} - - def description_to_html_lines(self): - """ Description could contain some structure content, depending on - its use. Notably appointment could add some structured data in it - in addition to free text. Purpose of this method is to generate a - simple html. - - Input (self.description) example: -Some free text salespeople added -as multi line - * Mobile: +320475000000 - * Email: my.email@test.example.com - * SingleLine: answer - * MultiLine: -Answer1 -Answer2 -Answer3 - * Dropdown: answer - * Radio: answer - * Checkboxes: answer1, answer2 -Some free text salespeople added -as multi line - - Output: a list of items[ -'Some free text salespeople added
    as multi line', -'Mobile: +320475000000', -'Email: my.email@test.example.com', -'SingleLine: answer', -'MultiLine:
    Answer1
    Answer2
    Answer3', -'Dropdown: answer', -'Radio: answer', -'Checkboxes: answer1, answer2', -'Some free text salespeople added
    as multi line'] - - Each item of returned list is escaped so that only our intended
    - are html tags. It should therefore be safe. - """ - - final_lines = [] - parsed_lines = [] - for line in self.description.split('\n'): - if not line.strip(): - continue - # new line - if line.startswith(' *'): - if parsed_lines: - final_lines.append('
    '.join(html_escape(line) for line in parsed_lines)) - parsed_lines = [line.lstrip(' *')] - else: - parsed_lines.append(line) - if parsed_lines: - final_lines.append('
    '.join(html_escape(line) for line in parsed_lines)) - return final_lines diff --git a/addons/calendar/models/mail_activity.py b/addons/calendar/models/mail_activity.py index 58ca5ff75d4..7f483ca27af 100644 --- a/addons/calendar/models/mail_activity.py +++ b/addons/calendar/models/mail_activity.py @@ -34,7 +34,10 @@ class MailActivity(models.Model): if feedback: for event in events: description = event.description - description = '%s\n%s%s' % (description or '', _("Feedback: "), feedback) + description = '%s
    %s' % ( + description if not tools.is_html_empty(description) else '', + _('Feedback: %(feedback)s', tools.plaintext2html(feedback)) if feedback else '', + ) event.write({'description': description}) return messages, activities diff --git a/addons/calendar/tests/test_calendar.py b/addons/calendar/tests/test_calendar.py index ebece3f11de..4083877516c 100644 --- a/addons/calendar/tests/test_calendar.py +++ b/addons/calendar/tests/test_calendar.py @@ -123,7 +123,7 @@ class TestCalendar(SavepointCaseWithUserDemo): self.assertEqual(test_record.activity_ids.date_deadline, (now + timedelta(days=-2)).date()) # update event with a description that have a special character and a new line - test_description3 = 'Test & \n Description' + test_description3 = 'Test &
    Description' test_note3 = '

    Test &
    Description

    ' test_event.write({ 'description': test_description3, diff --git a/addons/google_calendar/models/calendar.py b/addons/google_calendar/models/calendar.py index 24878c93be7..54a76ec40a0 100644 --- a/addons/google_calendar/models/calendar.py +++ b/addons/google_calendar/models/calendar.py @@ -82,7 +82,7 @@ class Meeting(models.Model): name = google_event.summary or related_event and related_event.name or _("(No title)") values = { 'name': name, - 'description': google_event.description, + 'description': tools.plaintext2html(google_event.description), 'location': google_event.location, 'user_id': google_event.owner(self.env).id, 'privacy': google_event.visibility or self.default_get(['privacy'])['privacy'], @@ -214,7 +214,7 @@ class Meeting(models.Model): 'start': start, 'end': end, 'summary': self.name, - 'description': self.description or '', + 'description': tools.html2plaintext(self.description) if not tools.is_html_empty(self.description) else '', 'location': self.location or '', 'guestsCanModify': True, 'organizer': {'email': self.user_id.email, 'self': self.user_id == self.env.user}, diff --git a/addons/google_calendar/tests/test_sync_google2odoo.py b/addons/google_calendar/tests/test_sync_google2odoo.py index 6cdca96f2ca..97cc230bbb9 100644 --- a/addons/google_calendar/tests/test_sync_google2odoo.py +++ b/addons/google_calendar/tests/test_sync_google2odoo.py @@ -8,6 +8,7 @@ from dateutil.relativedelta import relativedelta from odoo.tests.common import new_test_user from odoo.addons.google_calendar.tests.test_sync_common import TestSyncGoogle, patch_api from odoo.addons.google_calendar.utils.google_calendar import GoogleEvent +from odoo.tools import html2plaintext class TestSyncGoogle2Odoo(TestSyncGoogle): @@ -50,7 +51,7 @@ class TestSyncGoogle2Odoo(TestSyncGoogle): self.assertTrue(event, "It should have created an event") self.assertEqual(event.name, values.get('summary')) self.assertFalse(event.allday) - self.assertEqual(event.description, values.get('description')) + self.assertEqual(html2plaintext(event.description), values.get('description')) self.assertEqual(event.start, datetime(2020, 1, 13, 15, 55)) self.assertEqual(event.stop, datetime(2020, 1, 13, 18, 55)) admin_attendee = event.attendee_ids.filtered(lambda e: e.email == 'admin@yourcompany.example.com') diff --git a/addons/microsoft_calendar/models/calendar.py b/addons/microsoft_calendar/models/calendar.py index 1c98788a596..566bd629873 100644 --- a/addons/microsoft_calendar/models/calendar.py +++ b/addons/microsoft_calendar/models/calendar.py @@ -7,6 +7,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError +from odoo.tools import html2plaintext, is_html_empty, plaintext2html ATTENDEE_CONVERTER_O2M = { 'needsAction': 'notresponded', @@ -87,7 +88,7 @@ class Meeting(models.Model): values = { **default_values, 'name': microsoft_event.subject or _("(No title)"), - 'description': microsoft_event.bodyPreview, + 'description': plaintext2html(microsoft_event.bodyPreview), 'location': microsoft_event.location and microsoft_event.location.get('displayName') or False, 'user_id': microsoft_event.owner(self.env).id, 'privacy': sensitivity_o2m.get(microsoft_event.sensitivity, self.default_get(['privacy'])['privacy']), @@ -247,7 +248,7 @@ class Meeting(models.Model): if 'description' in fields_to_sync: values['body'] = { - 'content': self.description or '', + 'content': html2plaintext(self.description) if not is_html_empty(self.description) else '', 'contentType': "text", }