diff --git a/frappe/__init__.py b/frappe/__init__.py index 1a2c09e2cf..083ea3e1f3 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json from .exceptions import * from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template -__version__ = '8.5.8' +__version__ = '8.6.0' __title__ = "Frappe Framework" local = Local() @@ -380,7 +380,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message attachments=None, content=None, doctype=None, name=None, reply_to=None, cc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False, - inline_images=None, template=None, args=None, header=False): + inline_images=None, template=None, args=None, header=None): """Send email using user's default **Email Account** or global default **Email Account**. diff --git a/frappe/async.py b/frappe/async.py index 11d3d1abf6..70dad31636 100644 --- a/frappe/async.py +++ b/frappe/async.py @@ -165,6 +165,8 @@ def get_task_log_file_path(task_id, stream_type): @frappe.whitelist(allow_guest=True) def can_subscribe_doc(doctype, docname, sid): + if os.environ.get('CI'): + return True from frappe.sessions import Session from frappe.exceptions import PermissionError session = Session(None, resume=True).get_session_data() diff --git a/frappe/contacts/doctype/address/address.js b/frappe/contacts/doctype/address/address.js index 7809f426ea..a8d4860117 100644 --- a/frappe/contacts/doctype/address/address.js +++ b/frappe/contacts/doctype/address/address.js @@ -32,10 +32,15 @@ frappe.ui.form.on("Address", { } }, after_save: function() { - var last_route = frappe.route_history.slice(-2, -1)[0]; - if(frappe.dynamic_link && frappe.dynamic_link.doc - && frappe.dynamic_link.doc.name == last_route[2]){ - frappe.set_route(last_route[0], last_route[1], last_route[2]); - } + frappe.run_serially([ + () => frappe.timeout(1), + () => { + var last_route = frappe.route_history.slice(-2, -1)[0]; + if(frappe.dynamic_link && frappe.dynamic_link.doc + && frappe.dynamic_link.doc.name == last_route[2]){ + frappe.set_route(last_route[0], last_route[1], last_route[2]); + } + } + ]); } }); diff --git a/frappe/core/doctype/doctype/boilerplate/controller_list.js b/frappe/core/doctype/doctype/boilerplate/controller_list.js index 9d0a405176..b1f6d12008 100644 --- a/frappe/core/doctype/doctype/boilerplate/controller_list.js +++ b/frappe/core/doctype/doctype/boilerplate/controller_list.js @@ -1,5 +1,5 @@ /* eslint-disable */ frappe.listview_settings['{doctype}'] = {{ - add_fields: ["status"], - filters:[["status","=", "Open"]] + // add_fields: ["status"], + // filters:[["status","=", "Open"]] }}; diff --git a/frappe/core/doctype/doctype/boilerplate/test_controller.js b/frappe/core/doctype/doctype/boilerplate/test_controller.js new file mode 100644 index 0000000000..6749c53bb0 --- /dev/null +++ b/frappe/core/doctype/doctype/boilerplate/test_controller.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: {doctype}", function (assert) {{ + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('{doctype}', [ + // insert a new {doctype} + () => frappe.tests.make([ + // values to be set + {{key: 'value'}} + ]), + () => {{ + assert.equal(cur_frm.doc.key, 'value'); + }}, + () => done() + ]); + +}}); diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index f4563876ed..2e13e64f57 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -337,6 +337,10 @@ class DocType(Document): if not self.istable: make_boilerplate("controller.js", self.as_dict()) + #make_boilerplate("controller_list.js", self.as_dict()) + if not os.path.exists(frappe.get_module_path(frappe.scrub(self.module), + 'doctype', frappe.scrub(self.name), 'tests')): + make_boilerplate("test_controller.js", self.as_dict()) if self.has_web_view: templates_path = frappe.get_module_path(frappe.scrub(self.module), 'doctype', frappe.scrub(self.name), 'templates') diff --git a/frappe/core/doctype/test_runner/_test_test_runner.js b/frappe/core/doctype/test_runner/_test_test_runner.js new file mode 100644 index 0000000000..0b0bd9a98b --- /dev/null +++ b/frappe/core/doctype/test_runner/_test_test_runner.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Test Runner", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('Test Runner', [ + // insert a new Test Runner + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/core/doctype/test_runner/test_runner.js b/frappe/core/doctype/test_runner/test_runner.js index 0c305e7014..d5cac7f8a5 100644 --- a/frappe/core/doctype/test_runner/test_runner.js +++ b/frappe/core/doctype/test_runner/test_runner.js @@ -32,31 +32,28 @@ frappe.ui.form.on('Test Runner', { frappe.dom.eval(f.script); }); - // if(frm.doc.module_name) { - // QUnit.module.only(frm.doc.module_name); - // } - QUnit.testDone(function(details) { - var result = { - "Module name": details.module, - "Test name": details.name, - "Assertions": { - "Total": details.total, - "Passed": details.passed, - "Failed": details.failed - }, - "Skipped": details.skipped, - "Todo": details.todo, - "Runtime": details.runtime - }; + // var result = { + // "Module name": details.module, + // "Test name": details.name, + // "Assertions": { + // "Total": details.total, + // "Passed": details.passed, + // "Failed": details.failed + // }, + // "Skipped": details.skipped, + // "Todo": details.todo, + // "Runtime": details.runtime + // }; + + // eslint-disable-next-line + // console.log(JSON.stringify(result, null, 2)); details.assertions.map(a => { // eslint-disable-next-line console.log(`${a.result ? '✔' : '✗'} ${a.message}`); }); - // eslint-disable-next-line - console.log(JSON.stringify(result, null, 2)); }); QUnit.load(); diff --git a/frappe/core/doctype/test_runner/test_runner.json b/frappe/core/doctype/test_runner/test_runner.json index 8396d5df43..ccc1361dc9 100644 --- a/frappe/core/doctype/test_runner/test_runner.json +++ b/frappe/core/doctype/test_runner/test_runner.json @@ -42,6 +42,36 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "app", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "App", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -83,7 +113,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-12 23:16:15.910891", + "modified": "2017-07-19 03:22:33.221169", "modified_by": "Administrator", "module": "Core", "name": "Test Runner", diff --git a/frappe/core/doctype/test_runner/test_runner.py b/frappe/core/doctype/test_runner/test_runner.py index a59ddc69a5..c09d75ae4f 100644 --- a/frappe/core/doctype/test_runner/test_runner.py +++ b/frappe/core/doctype/test_runner/test_runner.py @@ -13,37 +13,23 @@ class TestRunner(Document): def get_test_js(): '''Get test + data for app, example: app/tests/ui/test_name.js''' test_path = frappe.db.get_single_value('Test Runner', 'module_path') + test_js = [] # split app, test_path = test_path.split(os.path.sep, 1) - test_js = get_test_data(app) - # full path + # now full path test_path = frappe.get_app_path(app, test_path) - with open(test_path, 'r') as fileobj: - test_js.append(dict( - script = fileobj.read() - )) - return test_js - -def get_test_data(app): - '''Get the test fixtures from all js files in app/tests/ui/data''' - test_js = [] - def add_file(path): with open(path, 'r') as fileobj: test_js.append(dict( script = fileobj.read() )) - data_path = frappe.get_app_path(app, 'tests', 'ui', 'data') - if os.path.exists(data_path): - for fname in os.listdir(data_path): - if fname.endswith('.js'): - add_file(os.path.join(data_path, fname)) - - if app != 'frappe': - add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js')) + # add test_lib.js + add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js')) + add_file(test_path) return test_js + diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 487cb3fb11..da08997456 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -279,7 +279,7 @@ class User(Document): sender = frappe.session.user not in STANDARD_USERS and get_formatted_email(frappe.session.user) or None frappe.sendmail(recipients=self.email, sender=sender, subject=subject, - template=template, args=args, + template=template, args=args, header=[subject, "green"], delayed=(not now) if now!=None else self.flags.delay_emails, retry=3) def a_system_manager_should_exist(self): diff --git a/frappe/core/doctype/version/test_version.py b/frappe/core/doctype/version/test_version.py index a10d944316..82f13242ae 100644 --- a/frappe/core/doctype/version/test_version.py +++ b/frappe/core/doctype/version/test_version.py @@ -3,9 +3,37 @@ from __future__ import unicode_literals import frappe -import unittest - -test_records = frappe.get_test_records('Version') +import unittest, copy +from frappe.test_runner import make_test_objects +from frappe.core.doctype.version.version import get_diff class TestVersion(unittest.TestCase): - pass + def test_get_diff(self): + test_records = make_test_objects('Event', reset = True) + old_doc = frappe.get_doc("Event", test_records[0]) + new_doc = copy.deepcopy(old_doc) + + old_doc.color = None + + diff = get_diff(old_doc, new_doc)['changed'] + + self.assertEquals(get_fieldnames(diff)[0], 'color') + self.assertTrue(get_old_values(diff)[0] is None) + self.assertEquals(get_new_values(diff)[0], 'blue') + + new_doc.starts_on = "2017-07-20" + + diff = get_diff(old_doc, new_doc)['changed'] + + self.assertEquals(get_fieldnames(diff)[0], 'starts_on') + self.assertEquals(get_old_values(diff)[0], '01-01-2014 00:00:00') + self.assertEquals(get_new_values(diff)[0], '07-20-2017 00:00:00') + +def get_fieldnames(change_array): + return [d[0] for d in change_array] + +def get_old_values(change_array): + return [d[1] for d in change_array] + +def get_new_values(change_array): + return [d[2] for d in change_array] diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index e270d9987f..671eb8c597 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -69,10 +69,13 @@ def get_diff(old, new, for_child=False): if not d.name in new_row_by_name: out.removed.append([df.fieldname, d.as_dict()]) - elif (old_value != new_value - and old.get_formatted(df.fieldname) != new.get_formatted(df.fieldname)): - out.changed.append((df.fieldname, old.get_formatted(df.fieldname), - new.get_formatted(df.fieldname))) + elif (old_value != new_value): + # Check for None values + old_data = old.get_formatted(df.fieldname) if old_value else old_value + new_data = new.get_formatted(df.fieldname) if new_value else new_value + + if old_data != new_data: + out.changed.append((df.fieldname, old_data, new_data)) # docstatus if not for_child and old.docstatus != new.docstatus: diff --git a/frappe/database.py b/frappe/database.py index 80cdc19d22..9b9e9e1446 100644 --- a/frappe/database.py +++ b/frappe/database.py @@ -53,8 +53,20 @@ class Database: def connect(self): """Connects to a database as set in `site_config.json`.""" warnings.filterwarnings('ignore', category=MySQLdb.Warning) - self._conn = MySQLdb.connect(user=self.user, host=self.host, passwd=self.password, - use_unicode=True, charset='utf8mb4') + usessl = 0 + if frappe.conf.db_ssl_ca and frappe.conf.db_ssl_cert and frappe.conf.db_ssl_key: + usessl = 1 + self.ssl = { + 'ca':frappe.conf.db_ssl_ca, + 'cert':frappe.conf.db_ssl_cert, + 'key':frappe.conf.db_ssl_key + } + if usessl: + self._conn = MySQLdb.connect(user=self.user, host=self.host, passwd=self.password, + use_unicode=True, charset='utf8mb4', ssl=self.ssl) + else: + self._conn = MySQLdb.connect(user=self.user, host=self.host, passwd=self.password, + use_unicode=True, charset='utf8mb4') self._conn.converter[246]=float self._conn.converter[12]=get_datetime self._conn.encoders[UnicodeWithAttrs] = self._conn.encoders[UnicodeType] diff --git a/frappe/docs/user/en/guides/automated-testing/qunit-testing.md b/frappe/docs/user/en/guides/automated-testing/qunit-testing.md index d66dde2782..ac7a1f3ccf 100644 --- a/frappe/docs/user/en/guides/automated-testing/qunit-testing.md +++ b/frappe/docs/user/en/guides/automated-testing/qunit-testing.md @@ -22,11 +22,16 @@ To run a Test Runner based test, use the `run-ui-tests` bench command by passing This will pass the filename to `test_test_runner.py` that will load the required JS in the browser and execute the tests -### Adding Fixtures / Test Data +### Debugging Tests -You can also add data that you require for all tests in the `tests/ui/data` folder of your app. All the files in this folder will be loaded in the browser before running the test. +To debug a test, you can open it in the **Test Runner** from your UI and run it manually to see where it is exactly failing. -The file `frappe/tests/ui/data/test_lib.js`, which contains library functions for testing is always loaded. +### Test Sequence + +In Frappé UI tests are run in a fixed sequence to ensure dependencies. + +The sequence in which the tests will be run will be in `tests/ui/tests.txt` +file. ### Running All UI Tests diff --git a/frappe/docs/user/en/guides/basics/site_config.md b/frappe/docs/user/en/guides/basics/site_config.md index 4ed73239f1..cf56db9e9a 100755 --- a/frappe/docs/user/en/guides/basics/site_config.md +++ b/frappe/docs/user/en/guides/basics/site_config.md @@ -20,13 +20,20 @@ Example: ### Optional Settings -- `db_host`: Database host if not `localhost`. - `admin_password`: Default Password for "Administrator". - `mute_emails`: Stops email sending if true. - `deny_multiple_logins`: Stop users from having more than one active session. - `root_password`: MariaDB root password. -### Defaut Outgoing Email Settings +### Remote Database Host Settings +- `db_host`: Database host if not `localhost`. + +To connect to a remote database server using ssl, you must first configure the database host to accept SSL connections. An example of how to do this is available at https://www.digitalocean.com/community/tutorials/how-to-configure-ssl-tls-for-mysql-on-ubuntu-16-04. After you do the configuration, set the following three options. All options must be set for Frappe to attempt to connect using SSL. +- `db_ssl_ca`: Full path to the ca.pem file used for connecting to a database host using ssl. Example value is `"/etc/mysql/ssl/ca.pem"`. +- `db_ssl_cert`: Full path to the cert.pem file used for connecting to a database host using ssl. Example value is `"/etc/mysql/ssl/client-cert.pem"`. +- `db_ssl_key`: Full path to the key.pem file used for connecting to a database host using ssl. Example value is `"/etc/mysql/ssl/client-key.pem"`. + +### Default Outgoing Email Settings - `mail_server`: SMTP server hostname. - `mail_port`: STMP port. diff --git a/frappe/docs/user/es/bench/__init__.py b/frappe/docs/user/es/bench/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/docs/user/es/guides/__init__.py b/frappe/docs/user/es/guides/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/docs/user/es/tutorial/__init__.py b/frappe/docs/user/es/tutorial/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/docs/user/es/videos/__init__.py b/frappe/docs/user/es/videos/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 79aeff9713..8903b813dc 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.model.document import Document from datetime import timedelta import frappe.utils +from frappe.utils import now, global_date_format, format_time from frappe.utils.xlsxutils import make_xlsx from frappe.utils.csvutils import to_csv @@ -76,16 +77,28 @@ class AutoEmailReport(Document): return xlsx_file.getvalue() elif self.format == 'CSV': - spreadsheet_data = self.get_spreadsheet_data(columns, data) + spreadsheet_data = self.get_spreadsheet_data(columns, data) return to_csv(spreadsheet_data) else: frappe.throw(_('Invalid Output Format')) - def get_html_table(self, columns, data): - return frappe.render_template('frappe/templates/includes/print_table.html', { + def get_html_table(self, columns=None, data=None): + + date_time = global_date_format(now()) + ' ' + format_time(now()) + report_doctype = frappe.db.get_value('Report', self.report, 'ref_doctype') + + return frappe.render_template('frappe/templates/emails/auto_email_report.html', { + 'title': self.name, + 'description': self.description, + 'date_time': date_time, 'columns': columns, - 'data': data + 'data': data, + 'report_url': frappe.utils.get_url_to_report(self.report, + self.report_type, report_doctype), + 'report_name': self.report, + 'edit_report_settings': frappe.utils.get_link_to_form('Auto Email Report', + self.name) }) @staticmethod @@ -111,29 +124,17 @@ class AutoEmailReport(Document): return attachments = None - message = '

{0}

'.format(_('{0} generated on {1}')\ - .format(frappe.bold(self.name), - frappe.utils.format_datetime(frappe.utils.now_datetime()))) - - if self.description: - message += '
' + self.description - - if self.format=='HTML': - message += '
' + data + if self.format == "HTML": + message = data else: + message = self.get_html_table() + + if not self.format=='HTML': attachments = [{ 'fname': self.get_file_name(), 'fcontent': data }] - report_doctype = frappe.db.get_value('Report', self.report, 'ref_doctype') - report_footer = frappe.render_template(self.get_report_footer(), - dict(report_url = frappe.utils.get_url_to_report(self.report, self.report_type, report_doctype), - report_name = self.report, - edit_report_settings = frappe.utils.get_link_to_form('Auto Email Report', self.name))) - - message += report_footer - frappe.sendmail( recipients = self.email_to.split(), subject = self.name, @@ -141,14 +142,6 @@ class AutoEmailReport(Document): attachments = attachments ) - def get_report_footer(self): - return """
-

- View report in your browser: - {{report_name}}

- Edit Auto Email Report Settings: {{edit_report_settings}} -

""" - @frappe.whitelist() def download(name): '''Download report locally''' diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 41753dbaf7..7f97a34b4d 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -15,7 +15,7 @@ from email.mime.multipart import MIMEMultipart def get_email(recipients, sender='', msg='', subject='[No Subject]', text_content = None, footer=None, print_html=None, formatted=None, attachments=None, content=None, reply_to=None, cc=[], email_account=None, expose_recipients=None, - inline_images=[], header=False): + inline_images=[], header=None): """ Prepare an email with the following format: - multipart/mixed - multipart/alternative @@ -76,7 +76,7 @@ class EMail: self.email_account = email_account or get_outgoing_email_account() def set_html(self, message, text_content = None, footer=None, print_html=None, - formatted=None, inline_images=None, header=False): + formatted=None, inline_images=None, header=None): """Attach message in the html portion of multipart/alternative""" if not formatted: formatted = get_formatted_html(self.subject, message, footer, print_html, @@ -233,12 +233,12 @@ class EMail: self.make() return self.msg_root.as_string() -def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=False): +def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=None): if not email_account: email_account = get_outgoing_email_account(False) rendered_email = frappe.get_template("templates/emails/standard.html").render({ - "header": get_header() if header else None, + "header": get_header(header), "content": message, "signature": get_signature(email_account), "footer": get_footer(email_account, footer), @@ -247,7 +247,27 @@ def get_formatted_html(subject, message, footer=None, print_html=None, email_acc "subject": subject }) - return scrub_urls(rendered_email) + sanitized_html = scrub_urls(rendered_email) + transformed_html = inline_style_in_html(sanitized_html) + return transformed_html + +def inline_style_in_html(html): + ''' Convert email.css and html to inline-styled html + ''' + from premailer import Premailer + + apps = frappe.get_installed_apps() + + css_files = [] + for app in apps: + path = 'assets/{0}/css/email.css'.format(app) + if os.path.exists(os.path.abspath(path)): + css_files.append(path) + + p = Premailer(html=html, external_styles=css_files, strip_important=False) + + return p.transform() + def add_attachment(fname, fcontent, content_type=None, parent=None, content_id=None, inline=False): @@ -396,23 +416,27 @@ def get_filecontent_from_path(path): return None -def get_header(): +def get_header(header=None): """ Build header from template """ from frappe.utils.jinja import get_email_from_template - default_brand_image = 'assets/frappe/images/favicon.png' # svg doesn't work in email - email_brand_image = frappe.get_hooks('email_brand_image') - if len(email_brand_image): - email_brand_image = email_brand_image[-1] - else: - email_brand_image = default_brand_image + if not header: return None + + if isinstance(header, basestring): + # header = 'My Title' + header = [header, None] + if len(header) == 1: + # header = ['My Title'] + header.append(None) + # header = ['My Title', 'orange'] + title, indicator = header - email_brand_image = default_brand_image - brand_text = frappe.get_hooks('app_title')[-1] + if not title: + title = frappe.get_hooks('app_title')[-1] email_header, text = get_email_from_template('email_header', { - 'brand_image': email_brand_image, - 'brand_text': brand_text + 'header_title': title, + 'indicator': indicator }) return email_header diff --git a/frappe/email/queue.py b/frappe/email/queue.py index be7f3f9f7d..e5630bc88e 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -23,7 +23,7 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= attachments=None, reply_to=None, cc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None, queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None, - header=False): + header=None): """Add email to sending queue (Email Queue) :param recipients: List of recipients. diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index fda4646b68..f68e2684af 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -3,7 +3,8 @@ from __future__ import unicode_literals import frappe, unittest, os, base64 -from frappe.email.email_body import replace_filename_with_cid, get_email +from frappe.email.email_body import (replace_filename_with_cid, + get_email, inline_style_in_html, get_header) class TestEmailBody(unittest.TestCase): def setUp(self): @@ -95,6 +96,46 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> '''.format(inline_images[0].get('content_id')) self.assertEquals(message, processed_message) + def test_inline_styling(self): + html = ''' +

Hi John

+

This is a test email

+''' + transformed_html = ''' +

Hi John

+

This is a test email

+''' + self.assertTrue(transformed_html in inline_style_in_html(html)) + + def test_email_header(self): + email_html = ''' +

Hey John Doe!

+

This is embedded image you asked for

+''' + email_string = get_email( + recipients=['test@example.com'], + sender='me@example.com', + subject='Test Subject', + content=email_html, + header=['Email Title', 'green'] + ).as_string() + + self.assertTrue('''''' in email_string) + self.assertTrue('Email Title' in email_string) + + def test_get_email_header(self): + html = get_header(['This is test', 'orange']) + self.assertTrue('' in html) + self.assertTrue('This is test' in html) + + html = get_header(['This is another test']) + self.assertTrue('This is another test' in html) + + html = get_header('This is string') + self.assertTrue('This is string' in html) + def fixed_column_width(string, chunk_size): parts = [string[0+i:chunk_size+i] for i in range(0, len(string), chunk_size)] diff --git a/frappe/public/build.json b/frappe/public/build.json index 3b58de727b..3fafb6c5f2 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -125,6 +125,7 @@ "public/js/frappe/misc/common.js", "public/js/frappe/misc/pretty_date.js", "public/js/frappe/misc/utils.js", + "public/js/frappe/misc/test_utils.js", "public/js/frappe/misc/tools.js", "public/js/frappe/misc/datetime.js", "public/js/frappe/misc/number_format.js", diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css new file mode 100644 index 0000000000..3f2df6d15f --- /dev/null +++ b/frappe/public/css/email.css @@ -0,0 +1,151 @@ +/* csslint ignore:start */ +body { + line-height: 1.5; + color: #36414C; +} +p { + margin: 1em 0 !important; +} +hr { + border-top: 1px solid #d1d8dd; +} +.body-table { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; +} +.body-table td { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; +} +.email-header, +.email-body, +.email-footer { + width: 100% !important; + min-width: 100% !important; +} +.email-body { + font-size: 14px; +} +.email-footer { + border-top: 1px solid #d1d8dd; + font-size: 12px; +} +.email-header { + border: 1px solid #d1d8dd; + border-radius: 4px 4px 0 0; +} +.email-header .brand-image { + width: 24px; + height: 24px; + display: block; +} +.email-header-title { + font-weight: bold; +} +.body-table.has-header .email-body { + border: 1px solid #d1d8dd; + border-radius: 0 0 4px 4px; + border-top: none; +} +.body-table.has-header .email-footer { + border-top: none; +} +.btn { + text-decoration: none; + padding: 7px 10px; + font-size: 12px; + border: 1px solid; + border-radius: 3px; +} +.btn.btn-default { + color: #fff; + background-color: #f0f4f7; + border-color: transparent; +} +.btn.btn-primary { + color: #fff; + background-color: #5E64FF; + border-color: #444bff; +} +.table { + width: 100%; + border-collapse: collapse; +} +.table td, +.table th { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #d1d8dd; + text-align: left; +} +.table th { + font-weight: bold; +} +.table > thead > tr > th { + vertical-align: middle; + border-bottom: 2px solid #d1d8dd; +} +.table > thead:first-child > tr:first-child > th { + border-top: none; +} +.table.table-bordered { + border: 1px solid #d1d8dd; +} +.table.table-bordered td, +.table.table-bordered th { + border: 1px solid #d1d8dd; +} +.more-info { + font-size: 80% !important; + color: #8D99A6 !important; + border-top: 1px solid #EBEFF2; + padding-top: 10px; +} +.text-right { + text-align: right !important; +} +.text-center { + text-align: center !important; +} +.text-muted { + color: #8D99A6 !important; +} +.text-extra-muted { + color: #d1d8dd !important; +} +.text-regular { + font-size: 14px; +} +.text-medium { + font-size: 12px; +} +.text-small { + font-size: 10px; +} +.indicator { + width: 8px; + height: 8px; + border-radius: 8px; + background-color: #b8c2cc; + display: inline-block; + margin-right: 5px; +} +.indicator.indicator-blue { + background-color: #5e64ff; +} +.indicator.indicator-green { + background-color: #98d85b; +} +.indicator.indicator-orange { + background-color: #ffa00a; +} +.indicator.indicator-red { + background-color: #ff5858; +} +.indicator.indicator-yellow { + background-color: #FEEF72; +} +/* auto email report */ +.report-title { + margin-bottom: 20px; +} +/* csslint ignore:end */ diff --git a/frappe/public/css/slickgrid.css b/frappe/public/css/slickgrid.css index a2fc898d52..30d7133a2d 100644 --- a/frappe/public/css/slickgrid.css +++ b/frappe/public/css/slickgrid.css @@ -54,3 +54,6 @@ .slick-row.odd .slick-cell { background-color: #fafbfc; } +.frappe-rtl .slick-wrapper { + direction: ltr; +} diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index c66841ceee..f3727ae16f 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -219,13 +219,6 @@ frappe.get_modal = function(title, content) { return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body); }; -frappe._in = function(source, target) { - // returns true if source is in target and both are not empty / falsy - if(!source) return false; - if(!target) return false; - return (target.indexOf(source) !== -1); -}; - // add