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:
-
- % for description_line in splitted_description:
- - ${description_line | safe}
- % endfor
-
-
- % 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:
-
- % for description_line in splitted_description:
- - ${description_line | safe}
- % endfor
-
-
- % 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:
-
- % for description_line in splitted_description:
- - ${description_line | safe}
- % endfor
-
-
- % 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",
}