[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
This commit is contained in:
committed by
Thibault Delavallée
parent
366aa2907e
commit
dbf1ab157d
@@ -92,18 +92,6 @@
|
||||
% if not object.event_id.allday and object.event_id.duration
|
||||
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
|
||||
% endif
|
||||
% if not is_online and object.event_id.description :
|
||||
<li>Description: ${object.event_id.description}</li>
|
||||
% elif is_online and object.event_id.description:
|
||||
% set splitted_description = object.event_id.description_to_html_lines()
|
||||
<li>Description:
|
||||
<ul>
|
||||
% for description_line in splitted_description:
|
||||
<li>${description_line | safe}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</li>
|
||||
% endif
|
||||
<li>Attendees
|
||||
<ul>
|
||||
% for attendee in object.event_id.attendee_ids:
|
||||
@@ -121,6 +109,10 @@
|
||||
<li>Join Video Call: <a href="${object.event_id.videocall_location}" target="_blank">${object.event_id.videocall_location}</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% if not is_html_empty(object.event_id.description):
|
||||
<p><strong>Description of the event</strong></p>
|
||||
${object.event_id.description | safe}
|
||||
% endif
|
||||
</td>
|
||||
</tr></table>
|
||||
<br/>
|
||||
@@ -222,18 +214,6 @@
|
||||
% if not object.event_id.allday and object.event_id.duration
|
||||
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
|
||||
% endif
|
||||
% if not is_online and object.event_id.description :
|
||||
<li>Description: ${object.event_id.description}</li>
|
||||
% elif is_online and object.event_id.description:
|
||||
% set splitted_description = object.event_id.description_to_html_lines()
|
||||
<li>Description:
|
||||
<ul>
|
||||
% for description_line in splitted_description:
|
||||
<li>${description_line | safe}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</li>
|
||||
% endif
|
||||
<li>Attendees
|
||||
<ul>
|
||||
% for attendee in object.event_id.attendee_ids:
|
||||
@@ -251,6 +231,10 @@
|
||||
<li>Join Video Call: <a href="${object.event_id.videocall_location}" target="_blank">${object.event_id.videocall_location}</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% if not is_html_empty(object.event_id.description):
|
||||
<p><strong>Description of the event</strong></p>
|
||||
${object.event_id.description | safe}
|
||||
% endif
|
||||
</td>
|
||||
</tr></table>
|
||||
<br/>
|
||||
@@ -332,18 +316,6 @@
|
||||
% if not object.event_id.allday and object.event_id.duration
|
||||
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
|
||||
% endif
|
||||
% if not is_online and object.event_id.description :
|
||||
<li>Description: ${object.event_id.description}</li>
|
||||
% elif is_online and object.event_id.description:
|
||||
% set splitted_description = object.event_id.description_to_html_lines()
|
||||
<li>Description:
|
||||
<ul>
|
||||
% for description_line in splitted_description:
|
||||
<li>${description_line | safe}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</li>
|
||||
% endif
|
||||
<li>Attendees
|
||||
<ul>
|
||||
% for attendee in object.event_id.attendee_ids:
|
||||
@@ -361,6 +333,10 @@
|
||||
<li>Join Video Call: <a href="${object.event_id.videocall_location}" target="_blank">${object.event_id.videocall_location}</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
% if not is_html_empty(object.event_id.description):
|
||||
<p><strong>Description of the event</strong></p>
|
||||
${object.event_id.description | safe}
|
||||
% endif
|
||||
</td>
|
||||
</tr></table>
|
||||
<br/>
|
||||
|
||||
@@ -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 = ["<ul>", "</ul>", "<li>"]
|
||||
for tag in tags_to_replace:
|
||||
desc_str = desc_str.replace(tag, "")
|
||||
desc_str = desc_str.replace("</li>", "<br/>")
|
||||
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<br />as multi line',
|
||||
'Mobile: +320475000000',
|
||||
'Email: my.email@test.example.com',
|
||||
'SingleLine: answer',
|
||||
'MultiLine:<br />Answer1<br />Answer2<br />Answer3',
|
||||
'Dropdown: answer',
|
||||
'Radio: answer',
|
||||
'Checkboxes: answer1, answer2',
|
||||
'Some free text salespeople added<br .>as multi line']
|
||||
|
||||
Each item of returned list is escaped so that only our intended <br />
|
||||
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('<br />'.join(html_escape(line) for line in parsed_lines))
|
||||
parsed_lines = [line.lstrip(' *')]
|
||||
else:
|
||||
parsed_lines.append(line)
|
||||
if parsed_lines:
|
||||
final_lines.append('<br />'.join(html_escape(line) for line in parsed_lines))
|
||||
return final_lines
|
||||
|
||||
@@ -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<br />%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
|
||||
|
||||
|
||||
@@ -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 & <br> Description'
|
||||
test_note3 = '<p>Test & <br> Description</p>'
|
||||
test_event.write({
|
||||
'description': test_description3,
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user