Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Rich Text Emails #33779

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2ba80f1
Add RICH_TEXT_EMAILS toggle
proteusvacuum Nov 2, 2023
048f344
Add CKEditor5
proteusvacuum Nov 2, 2023
994d1ae
Add preliminary ckeditor knockout binding
proteusvacuum Nov 2, 2023
95a20d0
Add rich text editor for non-translated usecase
proteusvacuum Nov 2, 2023
ec88d35
Send html email through backend
proteusvacuum Nov 2, 2023
f584736
Allow translations
proteusvacuum Nov 3, 2023
84f7cf6
Add preliminary template
proteusvacuum Nov 3, 2023
4deb434
Add html_body to Email and html_message to EmailContent
proteusvacuum Nov 6, 2023
5d3ee1e
Only show WYSIWYG editor for emails
proteusvacuum Nov 6, 2023
573c1f9
Sanitize HTML data
proteusvacuum Nov 15, 2023
533fff6
Preliminary Image Uploads and Downloads
proteusvacuum Nov 15, 2023
c7a40fd
Remove unused images in a task
proteusvacuum Nov 19, 2023
4050120
Use html_message instead
proteusvacuum Nov 21, 2023
466af4e
HTML editor is read only when viewing broadcasts
proteusvacuum Nov 21, 2023
43e198b
Update CKEditor to include image upload adaptor
proteusvacuum Nov 21, 2023
af7a112
Add image uploads to frontend
proteusvacuum Nov 21, 2023
c3c2aeb
Add csrftoken to image uploads
proteusvacuum Nov 22, 2023
c1863f3
Add css_inline dependency
proteusvacuum Nov 22, 2023
979c4cb
Add ckeditor css and inline it when sending message
proteusvacuum Nov 22, 2023
3b34d70
Add a few missing styles
proteusvacuum Nov 22, 2023
a8cbaf3
Save and send plaintext emails
proteusvacuum Nov 23, 2023
256e525
Set template for translations
proteusvacuum Nov 25, 2023
8cfc245
Lint
proteusvacuum Nov 26, 2023
f4649dc
Only get translation if html_message is present
proteusvacuum Nov 26, 2023
052e187
Update migration
proteusvacuum Nov 26, 2023
47bc300
Move beautifulsoup4 to be a prod requirement
proteusvacuum Nov 26, 2023
52f12d7
Add image deletion test and add delete_after
proteusvacuum Nov 26, 2023
72f5593
Add yarn shims
proteusvacuum Nov 22, 2023
e37bb6c
Use yarn ckeditor build
proteusvacuum Nov 21, 2023
9a3207b
Enable restricted editing mode
proteusvacuum Nov 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* globals requirejs */

Check failure on line 1 in corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap3/requirejs_config.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'requirejs' is already defined as a built-in global variable

Check failure on line 1 in corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap3/requirejs_config.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'requirejs' is already defined as a built-in global variable
requirejs.config({
baseUrl: '/static/',
paths: {
"ckeditor5": "ckeditor5/build/ckeditor5-dll",
"jquery": "jquery/dist/jquery.min",
"underscore": "underscore/underscore",
"bootstrap": "bootstrap/dist/js/bootstrap.min",
Expand All @@ -14,6 +15,22 @@
shim: {
"ace-builds/src-min-noconflict/ace": { exports: "ace" },
"bootstrap": { deps: ['jquery'] },
"ckeditor5": { exports: "CKEditor5" },
"@ckeditor/ckeditor5-editor-classic/build/editor-classic": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-autoformat/build/autoformat": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-basic-styles/build/basic-styles": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-block-quote/build/block-quote": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-essentials/build/essentials": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-font/build/font": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-heading/build/heading": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-horizontal-line/build/horizontal-line": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-html-support/build/html-support": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-restricted-editing/build/restricted-editing": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-image/build/image": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-indent/build/indent": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-link/build/link": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-list/build/list": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-paste-from-office/build/paste-from-office": { deps: ["ckeditor5"] },
"ko.mapping": { deps: ['knockout'] },
"hqwebapp/js/bootstrap3/hq.helpers": { deps: ['jquery', 'bootstrap', 'knockout', 'underscore'] },
"datatables.bootstrap": { deps: ['datatables'] },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* globals requirejs */

Check failure on line 1 in corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap5/requirejs_config.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'requirejs' is already defined as a built-in global variable

Check failure on line 1 in corehq/apps/hqwebapp/static/hqwebapp/js/bootstrap5/requirejs_config.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'requirejs' is already defined as a built-in global variable
requirejs.config({
baseUrl: '/static/',
paths: {
"ckeditor5": "ckeditor5/build/ckeditor5-dll",
"es6": "requirejs-babel7/es6",
"babel": "@babel/standalone/babel.min",
"babel-plugin-transform-modules-requirejs-babel": "babel-plugin-transform-modules-requirejs-babel/index",
Expand All @@ -17,6 +18,21 @@
},
shim: {
"ace-builds/src-min-noconflict/ace": { exports: "ace" },
"ckeditor5": { exports: "CKEditor5" },
"@ckeditor/ckeditor5-editor-classic/build/editor-classic": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-autoformat/build/autoformat": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-basic-styles/build/basic-styles": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-block-quote/build/block-quote": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-essentials/build/essentials": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-font/build/font": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-heading/build/heading": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-horizontal-line/build/horizontal-line": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-html-support/build/html-support": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-restricted-editing/build/restricted-editing": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-image/build/image": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-indent/build/indent": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-link/build/link": { deps: ["ckeditor5"] },
"@ckeditor/ckeditor5-list/build/list": { deps: ["ckeditor5"] },
"ko.mapping": { deps: ['knockout'] },
"hqwebapp/js/bootstrap5/hq.helpers": { deps: ['jquery', 'knockout', 'underscore'] },
"datatables.bootstrap": { deps: ['datatables'] },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
hqDefine('hqwebapp/js/ckeditor_knockout_bindings', [
'jquery',
'underscore',
'knockout',
'hqwebapp/js/initial_page_data',
'ckeditor5',
'@ckeditor/ckeditor5-editor-classic/build/editor-classic',
'@ckeditor/ckeditor5-autoformat/build/autoformat',
'@ckeditor/ckeditor5-basic-styles/build/basic-styles',
'@ckeditor/ckeditor5-block-quote/build/block-quote',
'@ckeditor/ckeditor5-essentials/build/essentials',
'@ckeditor/ckeditor5-font/build/font',
'@ckeditor/ckeditor5-heading/build/heading',
'@ckeditor/ckeditor5-html-support/build/html-support',
'@ckeditor/ckeditor5-horizontal-line/build/horizontal-line',
'@ckeditor/ckeditor5-image/build/image',
'@ckeditor/ckeditor5-indent/build/indent',
'@ckeditor/ckeditor5-link/build/link',
'@ckeditor/ckeditor5-list/build/list',
'@ckeditor/ckeditor5-paste-from-office/build/paste-from-office',
'@ckeditor/ckeditor5-restricted-editing/build/restricted-editing',
], function (
$,
_,
ko,
initialPageData,
CKEditor5
) {
ko.bindingHandlers.ckeditor = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {

Check failure on line 30 in corehq/apps/hqwebapp/static/hqwebapp/js/ckeditor_knockout_bindings.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'allBindingsAccessor' is defined but never used

Check failure on line 30 in corehq/apps/hqwebapp/static/hqwebapp/js/ckeditor_knockout_bindings.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'viewModel' is defined but never used

Check failure on line 30 in corehq/apps/hqwebapp/static/hqwebapp/js/ckeditor_knockout_bindings.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'allBindingsAccessor' is defined but never used

Check failure on line 30 in corehq/apps/hqwebapp/static/hqwebapp/js/ckeditor_knockout_bindings.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'viewModel' is defined but never used
var options = {
plugins: [
CKEditor5.link.AutoLink,
CKEditor5.autoformat.Autoformat,
CKEditor5.basicStyles.Bold,
CKEditor5.basicStyles.Italic,
CKEditor5.essentials.Essentials,
CKEditor5.font.Font,
CKEditor5.font.FontColor,
CKEditor5.heading.Heading,
CKEditor5.horizontalLine.HorizontalLine,
CKEditor5.htmlSupport.GeneralHtmlSupport,
CKEditor5.image.Image,
CKEditor5.image.ImageCaption,
CKEditor5.image.ImageStyle,
CKEditor5.image.ImageResize,
CKEditor5.image.ImageResizeButtons,
CKEditor5.image.ImageToolbar,
CKEditor5.image.ImageUpload,
CKEditor5.indent.Indent,
CKEditor5.link.Link,
CKEditor5.link.LinkImage,
CKEditor5.list.List,
CKEditor5.paragraph.Paragraph,
CKEditor5.pasteFromOffice.PasteFromOffice,
CKEditor5.restrictedEditing.RestrictedEditingMode,
CKEditor5.upload.SimpleUploadAdapter,
],
toolbar: {
items: [
'heading',
'fontFamily',
'fontSize',
'fontColor',
'|',
'bold',
'italic',
'link',
'bulletedList',
'numberedList',
'|',
'outdent',
'indent',
'|',
'uploadImage',
'undo',
'redo',
'restrictedEditing',
],
},
image: {
insert: {
type: 'inline',
},
toolbar: [
'imageStyle:side',
'|',
'toggleImageCaption',
'|',
'linkImage',
],
},
simpleUpload: {
uploadUrl: initialPageData.reverse(element.attributes['data-image-upload-url'].value),
withCredentials: true,
headers: {
'X-CSRFTOKEN': $("#csrfTokenContainer").val(),
},
},
htmlSupport: {
// We allow all HTML here, and filter it out in a sanitizing step
allow: [
{
name: /.*/,
attributes: true,
classes: true,
styles: true,
},
],
},
restrictedEditing: {
allowedCommands: [

Check failure on line 112 in corehq/apps/hqwebapp/static/hqwebapp/js/ckeditor_knockout_bindings.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Unexpected comma in middle of array

Check failure on line 112 in corehq/apps/hqwebapp/static/hqwebapp/js/ckeditor_knockout_bindings.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Unexpected comma in middle of array
"fontColor",
"fontBackgroundColor",
"deleteForward",
"forwardDelete",
"delete",
"bold",
"italic",
"enter",
"selectAll",
"shiftEnter",
"insertText",
"input",
"undo",
"redo",
"fontFamily",
"fontSize",
"paragraph",
"insertParagraph",
"heading",
"horizontalLine",
"insertImage",
"replaceImageSource",
"imageInsert",
"imageTextAlternative",
"imageTypeInline",
"toggleImageCaption",
"imageStyle",
"resizeImage",
"imageResize",
"uploadImage",
"imageUpload",
"indent",
"outdent",
"link",
"unlink",
"numberedList",
"bulletedList",
"indentList",
,"outdentList",
],
},
},
editorInstance = undefined;

CKEditor5.editorClassic.ClassicEditor.create(element, options).then(function (editor) {
var isSubscriberChange = false,
isEditorChange = false,
editorInstance = editor;
if (typeof ko.utils.unwrapObservable(valueAccessor()) !== "undefined") {
editorInstance.setData(ko.utils.unwrapObservable(valueAccessor()));
}

// Update the observable value when the document changes
editorInstance.model.document.on('change:data', function (data) {

Check failure on line 166 in corehq/apps/hqwebapp/static/hqwebapp/js/ckeditor_knockout_bindings.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'data' is defined but never used

Check failure on line 166 in corehq/apps/hqwebapp/static/hqwebapp/js/ckeditor_knockout_bindings.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'data' is defined but never used
if (!isSubscriberChange) {
isEditorChange = true;
valueAccessor()(editorInstance.getData());
isEditorChange = false;
}

});

// Update the document whenever the observable changes
valueAccessor().subscribe(function (value) {
if (!isEditorChange) {
isSubscriberChange = true;
editorInstance.setData(value);
isSubscriberChange = false;
}

});

if (initialPageData.get('read_only_mode')) {
editorInstance.enableReadOnlyMode('');
}
});

// handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
CKEditor5.editorClassic.ClassicEditor.remove(editorInstance);
});

},
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @license Copyright (c) 2014-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Alignment } from '@ckeditor/ckeditor5-alignment';
import { Bold, Italic, Strikethrough, Underline } from '@ckeditor/ckeditor5-basic-styles';
import type { EditorConfig } from '@ckeditor/ckeditor5-core';
import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { FontBackgroundColor, FontColor, FontFamily, FontSize } from '@ckeditor/ckeditor5-font';
import { Heading } from '@ckeditor/ckeditor5-heading';
import { HorizontalLine } from '@ckeditor/ckeditor5-horizontal-line';
import { DataFilter, DataSchema, GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support';
import { AutoImage, Image, ImageCaption, ImageInsert, ImageResize, ImageStyle, ImageToolbar, ImageUpload } from '@ckeditor/ckeditor5-image';
import { AutoLink, Link, LinkImage } from '@ckeditor/ckeditor5-link';
import { List } from '@ckeditor/ckeditor5-list';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
import { RemoveFormat } from '@ckeditor/ckeditor5-remove-format';
import { SelectAll } from '@ckeditor/ckeditor5-select-all';
import { Style } from '@ckeditor/ckeditor5-style';
import { SimpleUploadAdapter } from '@ckeditor/ckeditor5-upload';
declare class Editor extends ClassicEditor {
static builtinPlugins: (typeof Alignment | typeof AutoImage | typeof AutoLink | typeof Bold | typeof DataFilter | typeof DataSchema | typeof Essentials | typeof FontBackgroundColor | typeof FontColor | typeof FontFamily | typeof FontSize | typeof GeneralHtmlSupport | typeof Heading | typeof HorizontalLine | typeof Image | typeof ImageCaption | typeof ImageInsert | typeof ImageResize | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof Italic | typeof Link | typeof LinkImage | typeof List | typeof Paragraph | typeof PasteFromOffice | typeof RemoveFormat | typeof SelectAll | typeof SimpleUploadAdapter | typeof Strikethrough | typeof Style | typeof Underline)[];
static defaultConfig: EditorConfig;
}
export default Editor;
18 changes: 18 additions & 0 deletions corehq/apps/sms/migrations/0058_email_html_body.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2023-11-06 14:27

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('sms', '0057_fcm_content_type_messaging_events'),
]

operations = [
migrations.AddField(
model_name='email',
name='html_body',
field=models.TextField(null=True),
),
]
1 change: 1 addition & 0 deletions corehq/apps/sms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2678,3 +2678,4 @@ class Email(models.Model):
recipient_address = models.CharField(max_length=255, db_index=True)
subject = models.TextField(null=True)
body = models.TextField(null=True)
html_body = models.TextField(null=True)
1 change: 1 addition & 0 deletions corehq/blobs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class CODES:
demo_user_restore = 14 # DemoUserRestore
data_file = 15 # domain data file (see DataFile class)
form_multimedia = 16 # form submission multimedia zip
email_multimedia = 17 # email images and attachments


CODES.name_of = {code: name
Expand Down
Loading
Loading