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", }