diff --git a/frappe/boot.py b/frappe/boot.py index 5d042f82ce..9f0c05a6ce 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -30,6 +30,7 @@ def get_bootinfo(): get_user(bootinfo) # system info + bootinfo.sitename = frappe.local.site bootinfo.sysdefaults = frappe.defaults.get_defaults() bootinfo.user_permissions = get_user_permissions() bootinfo.server_date = frappe.utils.nowdate() diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index aa9a7c3a4e..a1ff57907a 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -7,7 +7,7 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import validate_email_add, get_fullname, strip_html, cstr from frappe.core.doctype.communication.comment import (notify_mentions, - update_comment_in_doc) + update_comment_in_doc, on_trash) from frappe.core.doctype.communication.email import (validate_email, notify, _notify, update_parent_status) from frappe.utils.bot import BotReply @@ -111,6 +111,8 @@ class Communication(Document): frappe.publish_realtime('delete_communication', self.as_dict(), doctype= self.reference_doctype, docname = self.reference_name, after_commit=True) + # delete the comments from _comment + on_trash(self) def set_status(self): if not self.is_new(): diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 4341d557aa..1976f4c862 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -76,6 +76,7 @@ class File(NestedSet): if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}): if not self.is_folder and (self.is_private != self.db_get('is_private')): + old_file_url = self.file_url private_files = frappe.get_site_path('private', 'files') public_files = frappe.get_site_path('public', 'files') @@ -91,6 +92,11 @@ class File(NestedSet): self.file_url = "/private/files/{0}".format(self.file_name) + # update documents image url with new file url + if self.attached_to_doctype and self.attached_to_name and \ + frappe.db.get_value(self.attached_to_doctype, self.attached_to_name, "image") == old_file_url: + frappe.db.set_value(self.attached_to_doctype, self.attached_to_name, "image", self.file_url) + def set_folder_size(self): """Set folder size if folder""" if self.is_folder and not self.is_new(): diff --git a/frappe/core/page/data_import_tool/importer.py b/frappe/core/page/data_import_tool/importer.py index aa16802c5d..d26dcfd2b1 100644 --- a/frappe/core/page/data_import_tool/importer.py +++ b/frappe/core/page/data_import_tool/importer.py @@ -217,8 +217,8 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, # header if not rows: - from frappe.utils.file_manager import save_uploaded - file_doc = save_uploaded(dt=None, dn="Data Import", folder='Home', is_private=1) + from frappe.utils.file_manager import get_file_doc + file_doc = get_file_doc(dt='', dn="Data Import", folder='Home', is_private=1) filename, file_extension = os.path.splitext(file_doc.file_name) if file_extension == '.xlsx' and from_data_import == 'Yes': diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json index 4fd21e8b5e..dce8a9e44f 100644 --- a/frappe/email/doctype/newsletter/newsletter.json +++ b/frappe/email/doctype/newsletter/newsletter.json @@ -15,6 +15,7 @@ "engine": "InnoDB", "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -44,6 +45,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -74,6 +76,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -103,6 +106,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -131,6 +135,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -159,6 +164,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -170,7 +176,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Subject", "length": 0, @@ -187,6 +193,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -198,7 +205,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Message", "length": 0, @@ -215,6 +222,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -245,6 +253,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -275,6 +284,69 @@ "unique": 0 }, { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "description": "", + "fieldname": "published", + "fieldtype": "Check", + "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": "Published", + "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, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "route", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Route", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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, "bold": 0, "collapsible": 0, @@ -304,6 +376,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -333,6 +406,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -362,6 +436,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -391,19 +466,20 @@ "unique": 0 } ], - "has_web_view": 0, + "has_web_view": 1, "hide_heading": 0, "hide_toolbar": 0, "icon": "fa fa-envelope", "idx": 1, "image_view": 0, "in_create": 0, + "is_published_field": "published", "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 3, "menu_index": 0, - "modified": "2017-03-07 12:59:18.173824", + "modified": "2017-09-14 15:38:01.891251", "modified_by": "Administrator", "module": "Email", "name": "Newsletter", @@ -433,6 +509,7 @@ "quick_entry": 0, "read_only": 0, "read_only_onload": 0, + "route": "newsletters", "show_name_in_global_search": 0, "sort_order": "ASC", "title_field": "subject", diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 1813b01c53..d69fae1f1d 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import frappe.utils from frappe import throw, _ -from frappe.model.document import Document +from frappe.website.website_generator import WebsiteGenerator from frappe.email.queue import check_email_limit from frappe.utils.verified_command import get_signed_params, verify_request from frappe.utils.background_jobs import enqueue @@ -17,7 +17,7 @@ from frappe.utils import parse_addr from frappe.utils import validate_email_add -class Newsletter(Document): +class Newsletter(WebsiteGenerator): def onload(self): if self.email_sent: self.get("__onload").status_count = dict(frappe.db.sql("""select status, count(name) @@ -25,6 +25,7 @@ class Newsletter(Document): group by status""", (self.doctype, self.name))) or None def validate(self): + self.route = "newsletters/" + self.name if self.send_from: validate_email_add(self.send_from, True) @@ -105,6 +106,26 @@ class Newsletter(Document): throw(_("Please save the Newsletter before sending")) check_email_limit(self.recipients) + def get_context(self, context): + newsletters = get_newsletter_list("Newsletter", None, None, 0) + if newsletters: + newsletter_list = [d.name for d in newsletters] + if self.name not in newsletter_list: + frappe.redirect_to_message(_('Permission Error'), + _("You are not permitted to view the newsletter.")) + frappe.local.flags.redirect_location = frappe.local.response.location + raise frappe.Redirect + else: + context.attachments = get_attachments(self.name) + context.no_cache = 1 + context.show_sidebar = True + + +def get_attachments(name): + return frappe.get_all("File", + fields=["name", "file_name", "file_url", "is_private"], + filters = {"attached_to_name": name, "attached_to_doctype": "Newsletter", "is_private":0}) + def get_email_groups(name): return frappe.db.get_all("Newsletter Email Group", ["email_group"],{"parent":name, "parenttype":"Newsletter"}) @@ -219,4 +240,24 @@ def send_newsletter(newsletter): frappe.db.commit() +def get_list_context(context=None): + context.update({ + "show_sidebar": True, + "show_search": True, + 'no_breadcrumbs': True, + "title": _("Newsletter"), + "get_list": get_newsletter_list, + "row_template": "email/doctype/newsletter/templates/newsletter_row.html", + }) + + +def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"): + email_group_list = frappe.db.sql('''select eg.name from `tabEmail Group` eg, `tabEmail Group Member` egm + where egm.unsubscribed=0 and eg.name=egm.email_group and egm.email = %s''', frappe.session.user) + if email_group_list: + return frappe.db.sql('''select n.name, n.subject, n.message, n.modified + from `tabNewsletter` n, `tabNewsletter Email Group` neg + where n.name = neg.parent and n.email_sent=1 and n.published=1 and neg.email_group in %s + order by n.modified desc limit {0}, {1} + '''.format(limit_start, limit_page_length), [email_group_list], as_dict=1) diff --git a/frappe/email/doctype/newsletter/templates/newsletter.html b/frappe/email/doctype/newsletter/templates/newsletter.html new file mode 100644 index 0000000000..733c7df6af --- /dev/null +++ b/frappe/email/doctype/newsletter/templates/newsletter.html @@ -0,0 +1,65 @@ +{% extends "templates/web.html" %} + +{% block title %} {{ _("Newsletter") }} {% endblock %} + +{% block page_content %} + + +
+
+
+

{{ doc.subject }}

+

+ {{ frappe.format_date(doc.modified) }} +

+
+
+ {{ doc.message }} +
+
+ + {% if attachments %} +
+
+
+ {{ _("Attachments") }} +
+
+
+
+ {% for attachment in attachments %} +

+ + {{ attachment.file_name }} + +

+ {% endfor %} +
+
+
+ {% endif %} + +
+{% endblock %} \ No newline at end of file diff --git a/frappe/email/doctype/newsletter/templates/newsletter_row.html b/frappe/email/doctype/newsletter/templates/newsletter_row.html new file mode 100644 index 0000000000..503fadb1ea --- /dev/null +++ b/frappe/email/doctype/newsletter/templates/newsletter_row.html @@ -0,0 +1,15 @@ +
+ +
+
+ {{ doc.subject }} +
+
+
+ {{ frappe.utils.pretty_date(doc.modified) }} +
+
+
+
+
\ No newline at end of file diff --git a/frappe/email/doctype/newsletter/test_newsletter.js b/frappe/email/doctype/newsletter/test_newsletter.js new file mode 100644 index 0000000000..40664a439a --- /dev/null +++ b/frappe/email/doctype/newsletter/test_newsletter.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: Newsletter", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Newsletter + () => frappe.tests.make('Newsletter', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index 2b13799775..9b2f275260 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -7,24 +7,26 @@ import frappe, unittest from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe from six.moves.urllib.parse import unquote + emails = ["test_subscriber1@example.com", "test_subscriber2@example.com", - "test_subscriber3@example.com"] + "test_subscriber3@example.com", "test1@example.com"] class TestNewsletter(unittest.TestCase): def setUp(self): + frappe.set_user("Administrator") frappe.db.sql('delete from `tabEmail Group Member`') for email in emails: frappe.get_doc({ "doctype": "Email Group Member", "email": email, "email_group": "_Test Email Group" - }).insert() + }).insert() def test_send(self): name = self.send_newsletter() email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] - self.assertEquals(len(email_queue_list), 3) + self.assertEquals(len(email_queue_list), 4) recipients = [e.recipients[0].recipient for e in email_queue_list] for email in emails: self.assertTrue(email in recipients) @@ -41,13 +43,14 @@ class TestNewsletter(unittest.TestCase): name = self.send_newsletter() email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] - self.assertEquals(len(email_queue_list), 2) + self.assertEquals(len(email_queue_list), 3) recipients = [e.recipients[0].recipient for e in email_queue_list] for email in emails: if email != to_unsubscribe: self.assertTrue(email in recipients) - def send_newsletter(self): + @staticmethod + def send_newsletter(published=0): frappe.db.sql("delete from `tabEmail Queue`") frappe.db.sql("delete from `tabEmail Queue Recipient`") frappe.db.sql("delete from `tabNewsletter`") @@ -55,7 +58,8 @@ class TestNewsletter(unittest.TestCase): "doctype": "Newsletter", "subject": "_Test Newsletter", "send_from": "Test Sender ", - "message": "Testing my news." + "message": "Testing my news.", + "published": published }).insert(ignore_permissions=True) newsletter.append("email_group", {"email_group": "_Test Email Group"}) @@ -63,4 +67,21 @@ class TestNewsletter(unittest.TestCase): newsletter.send_emails() return newsletter.name + def test_portal(self): + self.send_newsletter(1) + frappe.set_user("test1@example.com") + from frappe.email.doctype.newsletter.newsletter import get_newsletter_list + newsletters = get_newsletter_list("Newsletter", None, None, 0) + self.assertEquals(len(newsletters), 1) + + def test_newsletter_context(self): + context = frappe._dict() + newsletter_name = self.send_newsletter(1) + frappe.set_user("test2@example.com") + doc = frappe.get_doc("Newsletter", newsletter_name) + doc.get_context(context) + self.assertEquals(context.no_cache, 1) + self.assertTrue("attachments" not in context.keys()) + + test_dependencies = ["Email Group"] diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index 376a9f3c10..53a14ffc5c 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -269,9 +269,11 @@ }, "Benin": { "code": "bj", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Porto-Novo" @@ -386,6 +388,8 @@ }, "Bulgaria": { "code": "bg", + "currency": "BGN", + "currency_name": "Bulgarian Lev", "currency_fraction": "Stotinka", "currency_fraction_units": 100, "currency_symbol": "\u043b\u0432", @@ -396,9 +400,11 @@ }, "Burkina Faso": { "code": "bf", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Ouagadougou" @@ -430,9 +436,11 @@ }, "Cameroon": { "code": "cm", + "currency": "XAF", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Douala" @@ -504,9 +512,11 @@ }, "Central African Republic": { "code": "cf", + "currency": "XAF", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", "number_format": "#,###.##", "timezones": [ "Africa/Bangui" @@ -514,9 +524,11 @@ }, "Chad": { "code": "td", + "currency": "XAF", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Ndjamena" @@ -592,7 +604,12 @@ }, "Congo": { "code": "cg", - "number_format": "#,###.##" + "number_format": "#,###.##", + "currency": "XAF", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", + "currency_fraction": "Centime", + "currency_fraction_units": 100 }, "Congo, The Democratic Republic of the": { "code": "cd", @@ -758,9 +775,11 @@ }, "Equatorial Guinea": { "code": "gq", + "currency": "XAF", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Malabo" @@ -877,9 +896,11 @@ }, "Gabon": { "code": "ga", + "currency": "XAF", + "currency_name": "Central African CFA Franc", + "currency_symbol": "FCFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Libreville" @@ -1019,9 +1040,11 @@ }, "Guinea-Bissau": { "code": "gw", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Bissau" @@ -1205,10 +1228,15 @@ }, "Ivory Coast": { "code": "ci", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", - "number_format": "#,###.##" + "number_format": "#,###.##", + "timeszones": [ + "Africa/Abidjan" + ] }, "Jamaica": { "code": "jm", @@ -1496,9 +1524,11 @@ }, "Mali": { "code": "ml", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Bamako" @@ -1758,9 +1788,11 @@ }, "Niger": { "code": "ne", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Niamey" @@ -2087,9 +2119,11 @@ }, "Senegal": { "code": "sn", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Dakar" @@ -2355,9 +2389,11 @@ }, "Togo": { "code": "tg", + "currency": "XOF", + "currency_name": "West African CFA Franc", + "currency_symbol": "CFA", "currency_fraction": "Centime", "currency_fraction_units": 100, - "currency_symbol": "Fr", "number_format": "#,###.##", "timezones": [ "Africa/Lome" diff --git a/frappe/hooks.py b/frappe/hooks.py index f5d98f0421..9bdd8352a6 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from . import __version__ as app_version + app_name = "frappe" app_title = "Frappe Framework" app_publisher = "Frappe Technologies" @@ -48,9 +49,11 @@ bootstrap = "assets/frappe/css/bootstrap.css" web_include_css = [ "assets/css/frappe-web.css" ] + website_route_rules = [ {"from_route": "/blog/", "to_route": "Blog Post"}, - {"from_route": "/kb/", "to_route": "Help Article"} + {"from_route": "/kb/", "to_route": "Help Article"}, + {"from_route": "/newsletters", "to_route": "Newsletter"} ] write_file_keys = ["file_url", "file_name"] diff --git a/frappe/patches.txt b/frappe/patches.txt index 28bc5cb574..86015c001c 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -193,3 +193,4 @@ frappe.patches.v8_x.update_user_permission frappe.patches.v8_5.patch_event_colors frappe.patches.v8_7.update_email_queue_status frappe.patches.v8_10.delete_static_web_page_from_global_search +frappe.patches.v8_x.add_bgn_xaf_xof_currencies diff --git a/frappe/patches/v8_x/add_bgn_xaf_xof_currencies.py b/frappe/patches/v8_x/add_bgn_xaf_xof_currencies.py new file mode 100644 index 0000000000..298f385f40 --- /dev/null +++ b/frappe/patches/v8_x/add_bgn_xaf_xof_currencies.py @@ -0,0 +1,12 @@ +""" +This will add the following currencies: +1. BGN (Bulgarian Lev) to Bulgaria. +2. XAF (Central African CFA Franc) to Cameroon, Republic of Congo, Chad, Gabon, Equitorial Guinea and + Central African Republic. +3. XOF (West African CFA Franc) to Benin, Niger, Burkina Faso, Mali, Senegal, Togo, Ivory Coast and Guinea Bissau. +""" +from frappe.utils.install import import_country_and_currency + + +def execute(): + import_country_and_currency() diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index f03f8d987d..f2f758aac5 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -601,11 +601,9 @@ select.form-control { .control-code.bold { height: 400px; font-family: Monaco, "Courier New", monospace; - background-color: black; - color: #fffce7; + color: #36414C; font-size: 12px; line-height: 1.7em; - border: none; } .delivery-status-indicator { display: inline-block; diff --git a/frappe/public/css/sidebar.css b/frappe/public/css/sidebar.css index 1bea165047..d5b07cc422 100644 --- a/frappe/public/css/sidebar.css +++ b/frappe/public/css/sidebar.css @@ -94,9 +94,6 @@ body[data-route^="Module"] .main-menu .form-sidebar { position: absolute; right: 5px; } -.form-sidebar .attachment-row a.close { - margin-top: -5px; -} .form-sidebar .form-shared .share-doc-btn, .form-sidebar .form-viewers .share-doc-btn { cursor: pointer; diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 68c63dae10..0610e6b551 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -29,7 +29,7 @@ frappe.Application = Class.extend({ this.startup(); }, startup: function() { - frappe.socket.init(); + frappe.socketio.init(); frappe.model.init(); if(frappe.boot.status==='failed') { diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js index 0efd76dd13..99bef4a113 100644 --- a/frappe/public/js/frappe/form/controls/attach.js +++ b/frappe/public/js/frappe/form/controls/attach.js @@ -9,10 +9,10 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({ }); this.$value = $('
\
\ - \ + \ \
\ - ×
') + ×') .prependTo(me.input_area) .toggle(false); this.input = this.$input.get(0); diff --git a/frappe/public/js/frappe/form/controls/html.js b/frappe/public/js/frappe/form/controls/html.js index 5c4f736071..53f1f04c08 100644 --- a/frappe/public/js/frappe/form/controls/html.js +++ b/frappe/public/js/frappe/form/controls/html.js @@ -2,13 +2,21 @@ frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({ make: function() { this._super(); this.disp_area = this.wrapper; + $(document).on('change', () => { + setTimeout(() => this.refresh_input(), 500); + }); }, refresh_input: function() { var content = this.get_content(); if(content) this.$wrapper.html(content); }, get_content: function() { - return this.df.options || ""; + var content = this.df.options || ""; + try { + return frappe.render(content, this); + } catch (e) { + return content; + } }, html: function(html) { this.$wrapper.html(html || this.get_content()); diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index 1110fee7fc..00af468754 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -137,17 +137,17 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ }); }, get_image: function (fileobj, callback) { - var freader = new FileReader(); + var reader = new FileReader(); - freader.onload = function() { - var dataurl = freader.result; + reader.onload = function() { + var dataurl = reader.result; // add filename to dataurl var parts = dataurl.split(","); parts[0] += ";filename=" + fileobj.name; dataurl = parts[0] + ',' + parts[1]; callback(dataurl); }; - freader.readAsDataURL(fileobj); + reader.readAsDataURL(fileobj); }, hide_elements_on_mobile: function() { this.note_editor.find('.note-btn-underline,\ diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 025f0bba7b..015cc27ded 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -48,7 +48,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({ return false; } - if (this.too_many_mandatory_fields() || this.has_child_table()) { + if (this.too_many_mandatory_fields() || this.has_child_table() + || !this.mandatory.length) { return false; } diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index 19b622e8ee..bde19db1f3 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -34,7 +34,7 @@ frappe.call = function(opts) { var callback = function(data, response_text) { if(data.task_id) { // async call, subscribe - frappe.socket.subscribe(data.task_id, opts); + frappe.socketio.subscribe(data.task_id, opts); if(opts.queued) { opts.queued(data); diff --git a/frappe/public/js/frappe/socketio_client.js b/frappe/public/js/frappe/socketio_client.js index c8af253bed..d058b8a3f4 100644 --- a/frappe/public/js/frappe/socketio_client.js +++ b/frappe/public/js/frappe/socketio_client.js @@ -1,4 +1,4 @@ -frappe.socket = { +frappe.socketio = { open_tasks: {}, open_docs: [], emit_queue: [], @@ -7,40 +7,40 @@ frappe.socket = { return; } - if (frappe.socket.socket) { + if (frappe.socketio.socket) { return; } if (frappe.boot.developer_mode) { // File watchers for development - frappe.socket.setup_file_watchers(); + frappe.socketio.setup_file_watchers(); } //Enable secure option when using HTTPS if (window.location.protocol == "https:") { - frappe.socket.socket = io.connect(frappe.socket.get_host(), {secure: true}); + frappe.socketio.socket = io.connect(frappe.socketio.get_host(), {secure: true}); } else if (window.location.protocol == "http:") { - frappe.socket.socket = io.connect(frappe.socket.get_host()); + frappe.socketio.socket = io.connect(frappe.socketio.get_host()); } else if (window.location.protocol == "file:") { - frappe.socket.socket = io.connect(window.localStorage.server); + frappe.socketio.socket = io.connect(window.localStorage.server); } - if (!frappe.socket.socket) { - console.log("Unable to connect to " + frappe.socket.get_host()); + if (!frappe.socketio.socket) { + console.log("Unable to connect to " + frappe.socketio.get_host()); return; } - frappe.socket.socket.on('msgprint', function(message) { + frappe.socketio.socket.on('msgprint', function(message) { frappe.msgprint(message); }); - frappe.socket.socket.on('eval_js', function(message) { + frappe.socketio.socket.on('eval_js', function(message) { eval(message); }); - frappe.socket.socket.on('progress', function(data) { + frappe.socketio.socket.on('progress', function(data) { if(data.progress) { data.percent = flt(data.progress[0]) / data.progress[1] * 100; } @@ -53,23 +53,24 @@ frappe.socket = { } }); - frappe.socket.setup_listeners(); - frappe.socket.setup_reconnect(); + frappe.socketio.setup_listeners(); + frappe.socketio.setup_reconnect(); + frappe.socketio.uploader = new frappe.socketio.SocketIOUploader(); $(document).on('form-load form-rename', function(e, frm) { if (frm.is_new()) { return; } - for (var i=0, l=frappe.socket.open_docs.length; i${filename} changed Click to Reload @@ -239,7 +240,7 @@ frappe.socket = { } // success - var opts = frappe.socket.open_tasks[data.task_id]; + var opts = frappe.socketio.open_tasks[data.task_id]; if(opts[method]) { opts[method](data); } @@ -264,15 +265,108 @@ frappe.socket = { frappe.provide("frappe.realtime"); frappe.realtime.on = function(event, callback) { - frappe.socket.socket && frappe.socket.socket.on(event, callback); + frappe.socketio.socket && frappe.socketio.socket.on(event, callback); }; frappe.realtime.off = function(event, callback) { - frappe.socket.socket && frappe.socket.socket.off(event, callback); + frappe.socketio.socket && frappe.socketio.socket.off(event, callback); } frappe.realtime.publish = function(event, message) { - if(frappe.socket.socket) { - frappe.socket.socket.emit(event, message); + if(frappe.socketio.socket) { + frappe.socketio.socket.emit(event, message); } } + +frappe.socketio.SocketIOUploader = class SocketIOUploader { + constructor() { + frappe.socketio.socket.on('upload-request-slice', (data) => { + var place = data.currentSlice * this.chunk_size, + slice = this.file.slice(place, + place + Math.min(this.chunk_size, this.file.size - place)); + + if (this.on_progress) { + // update progress + this.on_progress(place / this.file.size * 100); + } + + this.reader.readAsArrayBuffer(slice); + this.keep_alive(); + }); + + frappe.socketio.socket.on('upload-end', (data) => { + if (data.file_url.substr(0, 7)==='/public') { + data.file_url = data.file_url.substr(7); + } + this.callback(data); + this.reader = null; + this.file = null; + }); + + frappe.socketio.socket.on('upload-error', (data) => { + this.disconnect(false); + frappe.msgprint({ + title: __('Upload Failed'), + message: data.error, + indicator: 'red' + }); + }); + + frappe.socketio.socket.on('disconnect', () => { + this.disconnect(); + }); + } + + start({file=null, is_private=0, filename='', callback=null, on_progress=null, + chunk_size=100000} = {}) { + + if (this.reader) { + frappe.throw(__('File Upload in Progress. Please try again in a few moments.')); + } + + this.reader = new FileReader(); + this.file = file; + this.chunk_size = chunk_size; + this.callback = callback; + this.on_progress = on_progress; + + this.reader.onload = () => { + frappe.socketio.socket.emit('upload-accept-slice', { + is_private: is_private, + name: filename, + type: this.file.type, + size: this.file.size, + data: this.reader.result + }); + this.keep_alive(); + }; + + var slice = file.slice(0, this.chunk_size); + this.reader.readAsArrayBuffer(slice); + } + + keep_alive() { + if (this.next_check) { + clearTimeout (this.next_check); + } + this.next_check = setTimeout (() => { + this.disconnect(); + }, 3000); + } + + disconnect(with_message = true) { + if (this.reader) { + this.reader = null; + this.file = null; + frappe.hide_progress(); + if (with_message) { + frappe.msgprint({ + title: __('File Upload'), + message: __('File Upload Disconnected. Please try again.'), + indicator: 'red' + }); + } + } + } + +} \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index 79c1345cfa..b4d4244bbf 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -234,7 +234,7 @@ frappe.verify_password = function(callback) { }, __("Verify Password"), __("Verify")) } -frappe.show_progress = function(title, count, total) { +frappe.show_progress = function(title, count, total=100, description) { if(frappe.cur_progress && frappe.cur_progress.title === title && frappe.cur_progress.$wrapper.is(":visible")) { var dialog = frappe.cur_progress; @@ -242,7 +242,10 @@ frappe.show_progress = function(title, count, total) { var dialog = new frappe.ui.Dialog({ title: title, }); - dialog.progress = $('
') + dialog.progress = $(`
+
+

+
`) .appendTo(dialog.body); dialog.progress_bar = dialog.progress.css({"margin-top": "10px"}) .find(".progress-bar"); @@ -250,7 +253,12 @@ frappe.show_progress = function(title, count, total) { dialog.show(); frappe.cur_progress = dialog; } - dialog.progress_bar.css({"width": cint(flt(count) * 100 / total) + "%" }); + if (description) { + dialog.progress.find('.description').text(description); + } + dialog.percent = cint(flt(count) * 100 / total); + dialog.progress_bar.css({"width": dialog.percent + "%" }); + return dialog; } frappe.hide_progress = function() { diff --git a/frappe/public/js/frappe/upload.js b/frappe/public/js/frappe/upload.js index 319fc61971..5914cb9c79 100644 --- a/frappe/public/js/frappe/upload.js +++ b/frappe/public/js/frappe/upload.js @@ -187,7 +187,7 @@ frappe.upload = { }, upload_multiple_files: function(files /*FileData array*/, args, opts) { var i = -1; - + frappe.upload.total_files = files ? files.length : 0; // upload the first file upload_next(); // subsequent files will be uploaded after @@ -200,7 +200,7 @@ frappe.upload = { var file = files[i]; args.is_private = file.is_private; if(!opts.progress) { - frappe.show_progress(__('Uploading'), i+1, files.length); + frappe.show_progress(__('Uploading'), i, files.length); } } frappe.upload.upload_file(file, args, opts); @@ -225,20 +225,21 @@ frappe.upload = { return; } - if(args.file_url) { - frappe.upload._upload_file(fileobj, args, opts); - } else { + if(fileobj) { frappe.upload.read_file(fileobj, args, opts); + } else { + // with file_url + frappe.upload._upload_file(fileobj, args, opts); } }, - _upload_file: function(fileobj, args, opts, dataurl) { + _upload_file: function(fileobj, args, opts) { if (args.file_size) { frappe.upload.validate_max_file_size(args.file_size); } if(opts.on_attach) { - opts.on_attach(args, dataurl) + opts.on_attach(args) } else { if (opts.confirm_is_private) { frappe.prompt({ @@ -248,55 +249,53 @@ frappe.upload = { "default": 1 }, function(values) { args["is_private"] = values.is_private; - frappe.upload.upload_to_server(fileobj, args, opts, dataurl); + frappe.upload.upload_to_server(fileobj, args, opts); }, __("Private or Public?")); } else { if ("is_private" in opts) { args["is_private"] = opts.is_private; } - frappe.upload.upload_to_server(fileobj, args, opts, dataurl); + frappe.upload.upload_to_server(fileobj, args, opts); } } }, read_file: function(fileobj, args, opts) { - var freader = new FileReader(); + args.filename = fileobj.name.split(' ').join('_'); + args.file_url = null; - freader.onload = function() { - args.filename = fileobj.name.split(' ').join('_'); - if(opts.options && opts.options.toLowerCase()=="image") { - if(!frappe.utils.is_image_file(args.filename)) { - frappe.msgprint(__("Only image extensions (.gif, .jpg, .jpeg, .tiff, .png, .svg) allowed")); - return; - } + if(opts.options && opts.options.toLowerCase()=="image") { + if(!frappe.utils.is_image_file(args.filename)) { + frappe.msgprint(__("Only image extensions (.gif, .jpg, .jpeg, .tiff, .png, .svg) allowed")); + return; } + } - if((opts.max_width || opts.max_height) && frappe.utils.is_image_file(args.filename)) { - frappe.utils.resize_image(freader, function(_dataurl) { - var dataurl = _dataurl; - args.filedata = _dataurl.split(",")[1]; - args.file_size = Math.round(args.filedata.length * 3 / 4); - console.log("resized!") - frappe.upload._upload_file(fileobj, args, opts, dataurl); - }) - } else { - var dataurl = freader.result; - args.filedata = freader.result.split(",")[1]; - args.file_size = fileobj.size; - frappe.upload._upload_file(fileobj, args, opts, dataurl); + let start_complete = frappe.cur_progress ? frappe.cur_progress.percent : 0; + + frappe.socketio.uploader.start({ + file: fileobj, + filename: args.filename, + is_private: args.is_private, + callback: (data) => { + args.file_url = data.file_url; + frappe.upload._upload_file(fileobj, args, opts); + }, + on_progress: (percent_complete) => { + let increment = (flt(percent_complete) / frappe.upload.total_files); + frappe.show_progress(__('Uploading'), + start_complete + increment); } - }; - - freader.readAsDataURL(fileobj); + }); }, - upload_to_server: function(fileobj, args, opts, dataurl) { - // var msgbox = frappe.msgprint(__("Uploading...")); + upload_to_server: function(file, args, opts) { if(opts.start) { opts.start(); } + var ajax_args = { "method": "uploadfile", args: args, @@ -368,7 +367,7 @@ frappe.upload = { d.hide(); opts.loopcallback = function (){ if (i < j) { - args.is_private = d.fields_dict[fileobjs[i].name + "_is_private"].get_value() + args.is_private = d.fields_dict[fileobjs[i].name + "_is_private"].get_value(); frappe.upload.upload_file(fileobjs[i], args, opts); i++; } diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index afec19cd44..196d225027 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -749,11 +749,9 @@ select.form-control { .control-code, .control-code.bold { height: 400px; font-family: Monaco, "Courier New", monospace; - background-color: black; - color: @light-yellow; + color: @text-color; font-size: 12px; line-height: 1.7em; - border: none; } .delivery-status-indicator { diff --git a/frappe/public/less/sidebar.less b/frappe/public/less/sidebar.less index aacb83b17e..e45cc91bd7 100644 --- a/frappe/public/less/sidebar.less +++ b/frappe/public/less/sidebar.less @@ -130,15 +130,6 @@ body[data-route^="Module"] .main-menu { right: 5px; } - // .attachment-row .icon-lock { - // color: @text-warning; - // display: inline-block; - // margin-top: 1px; - // } - - .attachment-row a.close { - margin-top: -5px; - } .form-shared, .form-viewers { .share-doc-btn { diff --git a/frappe/tests/ui/test_control_html.js b/frappe/tests/ui/test_control_html.js new file mode 100644 index 0000000000..8c0276508c --- /dev/null +++ b/frappe/tests/ui/test_control_html.js @@ -0,0 +1,51 @@ +QUnit.module('controls'); + +QUnit.test("Test ControlHTML", function(assert) { + assert.expect(3); + const random_name = frappe.utils.get_random(3).toLowerCase(); + + let done = assert.async(); + + frappe.run_serially([ + () => { + return frappe.tests.make('Custom Field', [ + {dt: 'ToDo'}, + {fieldtype: 'HTML'}, + {label: random_name}, + {options: '

Test

'} + ]); + }, + () => { + return frappe.tests.make('Custom Field', [ + {dt: 'ToDo'}, + {fieldtype: 'HTML'}, + {label: random_name + "_template"}, + {options: '

Test {%= doc.status %}

'} + ]); + }, + () => frappe.set_route('List', 'ToDo'), + () => frappe.new_doc('ToDo'), + () => { + if (frappe.quick_entry) + { + frappe.quick_entry.dialog.$wrapper.find('.edit-full').click(); + return frappe.timeout(1); + } + }, + () => { + const control = $(`.frappe-control[data-fieldname="${random_name}"]`)[0]; + return assert.ok(control.innerHTML === '

Test

'); + }, + () => { + const control = $(`.frappe-control[data-fieldname="${random_name}_template"]`)[0]; + return assert.ok(control.innerHTML === '

Test Open

'); + }, + () => frappe.tests.set_control("status", "Closed"), + () => frappe.timeout(1), + () => { + const control = $(`.frappe-control[data-fieldname="${random_name}_template"]`)[0]; + return assert.ok(control.innerHTML === '

Test Closed

'); + }, + () => done() + ]); +}); diff --git a/frappe/tests/ui/tests.txt b/frappe/tests/ui/tests.txt index bcdd1c6cee..3c0bb98ebb 100644 --- a/frappe/tests/ui/tests.txt +++ b/frappe/tests/ui/tests.txt @@ -13,3 +13,4 @@ frappe/custom/doctype/customize_form/test_customize_form.js frappe/desk/doctype/event/test_event.js frappe/workflow/doctype/workflow/tests/test_workflow_create.js frappe/workflow/doctype/workflow/tests/test_workflow_test.js +frappe/tests/ui/test_control_html.js diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index d3a7b74ac4..845f4a623c 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -179,11 +179,13 @@ def delete_temp_backups(older_than=24): """ Cleans up the backup_link_path directory by deleting files older than 24 hours """ - file_list = os.listdir(get_backup_path()) - for this_file in file_list: - this_file_path = os.path.join(get_backup_path(), this_file) - if is_file_old(this_file_path, older_than): - os.remove(this_file_path) + backup_path = get_backup_path() + if os.path.exists(backup_path): + file_list = os.listdir(get_backup_path()) + for this_file in file_list: + this_file_path = os.path.join(get_backup_path(), this_file) + if is_file_old(this_file_path, older_than): + os.remove(this_file_path) def is_file_old(db_file_name, older_than=24): """ diff --git a/frappe/utils/file_manager.py b/frappe/utils/file_manager.py index 2d32215d6a..de0ebdf12f 100644 --- a/frappe/utils/file_manager.py +++ b/frappe/utils/file_manager.py @@ -15,6 +15,7 @@ from six import text_type class MaxFileSizeReachedError(frappe.ValidationError): pass + def get_file_url(file_data_name): data = frappe.db.get_value("File", file_data_name, ["file_name", "file_url"], as_dict=True) return data.file_url or data.file_name @@ -23,38 +24,51 @@ def upload(): # get record details dt = frappe.form_dict.doctype dn = frappe.form_dict.docname - folder = frappe.form_dict.folder file_url = frappe.form_dict.file_url filename = frappe.form_dict.filename - is_private = cint(frappe.form_dict.is_private) + frappe.form_dict.is_private = cint(frappe.form_dict.is_private) if not filename and not file_url: frappe.msgprint(_("Please select a file or url"), raise_exception=True) - # save - if frappe.form_dict.filedata: - filedata = save_uploaded(dt, dn, folder, is_private) - elif file_url: - filedata = save_url(file_url, filename, dt, dn, folder, is_private) + file_doc = get_file_doc() comment = {} if dt and dn: comment = frappe.get_doc(dt, dn).add_comment("Attachment", _("added {0}").format("{file_name}{icon}".format(**{ - "icon": ' ' if filedata.is_private else "", - "file_url": filedata.file_url.replace("#", "%23") if filedata.file_name else filedata.file_url, - "file_name": filedata.file_name or filedata.file_url + "icon": ' ' \ + if file_doc.is_private else "", + "file_url": file_doc.file_url.replace("#", "%23") \ + if file_doc.file_name else file_doc.file_url, + "file_name": file_doc.file_name or file_doc.file_url }))) return { - "name": filedata.name, - "file_name": filedata.file_name, - "file_url": filedata.file_url, - "is_private": filedata.is_private, + "name": file_doc.name, + "file_name": file_doc.file_name, + "file_url": file_doc.file_url, + "is_private": file_doc.is_private, "comment": comment.as_dict() if comment else {} } +def get_file_doc(dt=None, dn=None, folder=None, is_private=None): + '''returns File object (Document) from given parameters or form_dict''' + r = frappe.form_dict + + if dt is None: dt = r.doctype + if dn is None: dn = r.docname + if folder is None: folder = r.folder + if is_private is None: is_private = r.is_private + + if r.filedata: + file_doc = save_uploaded(dt, dn, folder, is_private) + elif r.file_url: + file_doc = save_url(r.file_url, r.filename, dt, dn, folder, is_private) + + return file_doc + def save_uploaded(dt, dn, folder, is_private): fname, content = get_uploaded_content() if content: @@ -97,55 +111,6 @@ def get_uploaded_content(): frappe.msgprint(_('No file attached')) return None, None -def extract_images_from_doc(doc, fieldname): - content = doc.get(fieldname) - content = extract_images_from_html(doc, content) - if frappe.flags.has_dataurl: - doc.set(fieldname, content) - -def extract_images_from_html(doc, content): - frappe.flags.has_dataurl = False - - def _save_file(match): - data = match.group(1) - data = data.split("data:")[1] - headers, content = data.split(",") - - if "filename=" in headers: - filename = headers.split("filename=")[-1] - - # decode filename - if not isinstance(filename, text_type): - filename = text_type(filename, 'utf-8') - else: - mtype = headers.split(";")[0] - filename = get_random_filename(content_type=mtype) - - doctype = doc.parenttype if doc.parent else doc.doctype - name = doc.parent or doc.name - - # TODO fix this - file_url = save_file(filename, content, doctype, name, decode=True).get("file_url") - if not frappe.flags.has_dataurl: - frappe.flags.has_dataurl = True - - return ']*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content) - - return content - -def get_random_filename(extn=None, content_type=None): - if extn: - if not extn.startswith("."): - extn = "." + extn - - elif content_type: - extn = mimetypes.guess_extension(content_type) - - return random_string(7) + (extn or "") - def save_file(fname, content, dt, dn, folder=None, decode=False, is_private=0): if decode: if isinstance(content, text_type): @@ -370,3 +335,52 @@ def download_file(file_url): frappe.local.response.filename = file_url[file_url.rfind("/")+1:] frappe.local.response.filecontent = filedata frappe.local.response.type = "download" + +def extract_images_from_doc(doc, fieldname): + content = doc.get(fieldname) + content = extract_images_from_html(doc, content) + if frappe.flags.has_dataurl: + doc.set(fieldname, content) + +def extract_images_from_html(doc, content): + frappe.flags.has_dataurl = False + + def _save_file(match): + data = match.group(1) + data = data.split("data:")[1] + headers, content = data.split(",") + + if "filename=" in headers: + filename = headers.split("filename=")[-1] + + # decode filename + if not isinstance(filename, text_type): + filename = text_type(filename, 'utf-8') + else: + mtype = headers.split(";")[0] + filename = get_random_filename(content_type=mtype) + + doctype = doc.parenttype if doc.parent else doc.doctype + name = doc.parent or doc.name + + # TODO fix this + file_url = save_file(filename, content, doctype, name, decode=True).get("file_url") + if not frappe.flags.has_dataurl: + frappe.flags.has_dataurl = True + + return ']*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content) + + return content + +def get_random_filename(extn=None, content_type=None): + if extn: + if not extn.startswith("."): + extn = "." + extn + + elif content_type: + extn = mimetypes.guess_extension(content_type) + + return random_string(7) + (extn or "") diff --git a/frappe/website/doctype/website_slideshow/website_slideshow.js b/frappe/website/doctype/website_slideshow/website_slideshow.js index acb49d6045..471729bda5 100644 --- a/frappe/website/doctype/website_slideshow/website_slideshow.js +++ b/frappe/website/doctype/website_slideshow/website_slideshow.js @@ -1,12 +1,10 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // MIT License. See license.txt -cur_frm.cscript.refresh = function(doc) { - cur_frm.set_intro(""); - if(doc.__islocal) { - cur_frm.set_intro("First set the name and save the record."); +frappe.ui.form.on("Website Slideshow", { + refresh: (frm) => { + let intro = frm.doc.__islocal? + "First set the name and save the record.": "Attach files / urls and add in table."; + frm.set_intro(intro); } - else { - cur_frm.set_intro("Attach files / urls and add in table."); - } -} \ No newline at end of file +}) \ No newline at end of file diff --git a/frappe/website/doctype/website_slideshow/website_slideshow.json b/frappe/website/doctype/website_slideshow/website_slideshow.json index ca8daf59b5..ab6f928a2e 100644 --- a/frappe/website/doctype/website_slideshow/website_slideshow.json +++ b/frappe/website/doctype/website_slideshow/website_slideshow.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, "autoname": "field:slideshow_name", @@ -13,33 +14,7 @@ "editable_grid": 0, "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "slideshow_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Slideshow Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -52,6 +27,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "", @@ -69,6 +45,36 @@ "unique": 0 }, { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "slideshow_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Slideshow Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -80,6 +86,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "Slideshow Items", @@ -98,6 +105,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -110,6 +118,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "Header", @@ -127,18 +136,18 @@ "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 0, "icon": "fa fa-play", "idx": 1, "image_view": 0, "in_create": 0, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 10, - "modified": "2016-12-29 14:40:39.690960", + "modified": "2017-09-18 11:19:31.627585", "modified_by": "Administrator", "module": "Website", "name": "Website Slideshow", @@ -154,7 +163,6 @@ "export": 0, "if_owner": 0, "import": 0, - "is_custom": 0, "permlevel": 0, "print": 1, "read": 1, @@ -169,6 +177,7 @@ "quick_entry": 0, "read_only": 0, "read_only_onload": 0, + "show_name_in_global_search": 0, "track_changes": 1, "track_seen": 0 } \ No newline at end of file diff --git a/frappe/website/doctype/website_slideshow/website_slideshow.py b/frappe/website/doctype/website_slideshow/website_slideshow.py index ee19b85df2..467fd80e2e 100644 --- a/frappe/website/doctype/website_slideshow/website_slideshow.py +++ b/frappe/website/doctype/website_slideshow/website_slideshow.py @@ -5,15 +5,26 @@ from __future__ import unicode_literals import frappe +from frappe import _ from frappe.model.document import Document class WebsiteSlideshow(Document): + def validate(self): + self.validate_images() + def on_update(self): # a slide show can be in use and any change in it should get reflected from frappe.website.render import clear_cache clear_cache() + def validate_images(self): + ''' atleast one image file should be public for slideshow ''' + files = map(lambda row: row.image, self.slideshow_items) + result = frappe.get_all("File", filters={ "file_url":("in", files) }, fields="is_private") + if any([file.is_private for file in result]): + frappe.throw(_("All Images attached to Website Slideshow should be public")) + def get_slideshow(doc): if not doc.slideshow: return {} diff --git a/frappe/website/render.py b/frappe/website/render.py index 33e82efd18..0291185bf4 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -168,6 +168,9 @@ def build_page(path): frappe.local.path = path context = get_context(path) + if "{{" in context.title: + title_template = context.pop('title') + context.title = frappe.render_template(title_template, context) if context.source: html = frappe.render_template(context.source, context) diff --git a/socketio.js b/socketio.js index cd3eb64aa8..15e9263398 100644 --- a/socketio.js +++ b/socketio.js @@ -3,11 +3,22 @@ var http = require('http').Server(app); var io = require('socket.io')(http); var cookie = require('cookie') var fs = require('fs'); +var path = require('path'); var redis = require("redis"); var request = require('superagent'); var conf = get_conf(); var flags = {}; +var files_struct = { + name: null, + type: null, + size: 0, + data: [], + slice: 0, + site_name: null, + is_private: 0 +}; + var subscriber = redis.createClient(conf.redis_socketio || conf.redis_async_broker_port); // serve socketio @@ -21,7 +32,7 @@ app.get('/', function(req, res) { }); // on socket connection -io.on('connection', function(socket){ +io.on('connection', function(socket) { if (get_hostname(socket.request.headers.host) != get_hostname(socket.request.headers.origin)) { return; } @@ -41,6 +52,7 @@ io.on('connection', function(socket){ setTimeout(function() { flags[sid] = null; }, 10000); socket.user = cookie.parse(socket.request.headers.cookie).user_id; + socket.files = {}; // console.log("firing get_user_info"); request.get(get_url(socket, '/api/method/frappe.async.get_user_info')) @@ -61,6 +73,10 @@ io.on('connection', function(socket){ } }); + socket.on('disconnect', function() { + delete socket.files; + }) + socket.on('task_subscribe', function(task_id) { var room = get_task_room(socket, task_id); socket.join(room); @@ -134,9 +150,46 @@ io.on('connection', function(socket){ }); }); - // socket.on('disconnect', function (arguments) { - // console.log("user disconnected", arguments); - // }); + socket.on('upload-accept-slice', (data) => { + try { + if (!socket.files[data.name]) { + socket.files[data.name] = Object.assign({}, files_struct, data); + socket.files[data.name].data = []; + } + + //convert the ArrayBuffer to Buffer + data.data = new Buffer(new Uint8Array(data.data)); + //save the data + socket.files[data.name].data.push(data.data); + socket.files[data.name].slice++; + + if (socket.files[data.name].slice * 100000 >= socket.files[data.name].size) { + // do something with the data + var fileBuffer = Buffer.concat(socket.files[data.name].data); + + const file_url = path.join((socket.files[data.name].is_private ? 'private' : 'public'), + 'files', data.name); + const file_path = path.join('sites', get_site_name(socket), file_url); + + fs.writeFile(file_path, fileBuffer, (err) => { + delete socket.files[data.name]; + if (err) return socket.emit('upload error'); + socket.emit('upload-end', { + file_url: '/' + file_url + }); + }); + } else { + socket.emit('upload-request-slice', { + currentSlice: socket.files[data.name].slice + }); + } + } catch (e) { + console.log(e); + socket.emit('upload-error', { + error: e.message + }); + } + }); }); subscriber.on("message", function(channel, message) {