diff --git a/.travis.yml b/.travis.yml index 36379390b8..0b9f8293df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: python dist: trusty +group: deprecated-2017Q2 python: - "2.7" diff --git a/frappe/__init__.py b/frappe/__init__.py index d147be36be..ee8f544f18 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 -__version__ = '8.1.1' +__version__ = '8.1.2' __title__ = "Frappe Framework" local = Local() @@ -379,7 +379,8 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, 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): + send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False, + inline_images=None): """Send email using user's default **Email Account** or global default **Email Account**. @@ -401,6 +402,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message :param send_after: Send after the given datetime. :param expose_recipients: Display all recipients in the footer message - "This email was sent to" :param communication: Communication link to be set in Email Queue record + :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id """ message = content or message @@ -418,7 +420,8 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message, attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, in_reply_to=in_reply_to, send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, - communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification) + communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification, + inline_images=inline_images) whitelisted = [] guest_methods = [] diff --git a/frappe/contacts/doctype/contact/contact.js b/frappe/contacts/doctype/contact/contact.js index cfecddffb6..d44904d9d5 100644 --- a/frappe/contacts/doctype/contact/contact.js +++ b/frappe/contacts/doctype/contact/contact.js @@ -19,7 +19,7 @@ frappe.ui.form.on("Contact", { if(!frm.doc.user && !frm.is_new() && frm.perm[0].write) { frm.add_custom_button(__("Invite as User"), function() { frappe.call({ - method: "frappe.email.doctype.contact.contact.invite_user", + method: "frappe.contacts.doctype.contact.contact.invite_user", args: { contact: frm.doc.name }, diff --git a/frappe/core/doctype/file/file_list.js b/frappe/core/doctype/file/file_list.js index c2ae9af747..61b4a58a0c 100644 --- a/frappe/core/doctype/file/file_list.js +++ b/frappe/core/doctype/file/file_list.js @@ -17,20 +17,20 @@ frappe.listview_settings['File'] = { }, prepare_data: function(data) { // set image icons - var icon = "" + var icon = ""; if(data.is_folder) { icon += ' '; } else if(frappe.utils.is_image_file(data.file_name)) { icon += ' '; } else { - icon += ' ' + icon += ' '; } - data._title = icon + (data.file_name ? data.file_name : data.file_url) + data._title = icon + (data.file_name ? data.file_name : data.file_url); if (data.is_private) { - data._title += ' ' + data._title += ' '; } }, onload: function(doclist) { @@ -43,7 +43,7 @@ frappe.listview_settings['File'] = { doclist.$page.on("click", ".list-row-checkbox", function(event) { doclist.list_renderer.settings.add_menu_item_copy(doclist); - }) + }); }, list_view_doc:function(doclist){ $(doclist.wrapper).on("click", 'button[list_view_doc="'+doclist.doctype+'"]', function() { @@ -73,7 +73,7 @@ frappe.listview_settings['File'] = { method: "frappe.core.doctype.file.file.create_new_folder", args: data, callback: function(r) { } - }) + }); }, __('Enter folder name'), __("Create")); }); @@ -134,7 +134,7 @@ frappe.listview_settings['File'] = { else{ frappe.throw(__("Please select file to copy")); } - }) + }); doclist.copy = true; } }, @@ -235,5 +235,5 @@ frappe.listview_settings['File'] = { .appendTo(doclist.breadcrumb); } }); - }; + } }; diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 13a8659d4a..cecd1bdbd9 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -120,6 +120,8 @@ def export_query(): data = frappe._dict(frappe.local.form_dict) del data["cmd"] + if "csrf_token" in data: + del data["csrf_token"] if isinstance(data.get("filters"), basestring): filters = json.loads(data["filters"]) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index d8929ef055..920fb7f36b 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -27,6 +27,8 @@ def get_form_params(): data = frappe._dict(frappe.local.form_dict) del data["cmd"] + if "csrf_token" in data: + del data["csrf_token"] if isinstance(data.get("filters"), basestring): data["filters"] = json.loads(data["filters"]) diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 6d7dee38aa..1c450d522f 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -2,19 +2,20 @@ # MIT License. See license.txt from __future__ import unicode_literals -import frappe +import frappe, re from frappe.utils.pdf import get_pdf from frappe.email.smtp import get_outgoing_email_account from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint, - split_emails, to_markdown, markdown, encode, random_string) + split_emails, to_markdown, markdown, encode, random_string, parse_addr) import email.utils -from frappe.utils import parse_addr from six import iteritems +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): + content=None, reply_to=None, cc=[], email_account=None, expose_recipients=None, + inline_images=[]): """send an html email as multipart with attachments and all""" content = content or msg emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, email_account=email_account, expose_recipients=expose_recipients) @@ -22,7 +23,8 @@ def get_email(recipients, sender='', msg='', subject='[No Subject]', if not content.strip().startswith("<"): content = markdown(content) - emailobj.set_html(content, text_content, footer=footer, print_html=print_html, formatted=formatted) + emailobj.set_html(content, text_content, footer=footer, + print_html=print_html, formatted=formatted, inline_images=inline_images) if isinstance(attachments, dict): attachments = [attachments] @@ -39,7 +41,6 @@ class EMail: Also sets all messages as multipart/alternative for cleaner reading in text-only clients """ def __init__(self, sender='', recipients=(), subject='', alternative=0, reply_to=None, cc=(), email_account=None, expose_recipients=None): - from email.mime.multipart import MIMEMultipart from email import Charset Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8') @@ -64,7 +65,8 @@ 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): + def set_html(self, message, text_content = None, footer=None, print_html=None, + formatted=None, inline_images=None): """Attach message in the html portion of multipart/alternative""" if not formatted: formatted = get_formatted_html(self.subject, message, footer, print_html, email_account=self.email_account) @@ -77,7 +79,7 @@ class EMail: else: self.set_html_as_text(expand_relative_urls(formatted)) - self.set_part_html(formatted) + self.set_part_html(formatted, inline_images) self.html_set = True def set_text(self, message): @@ -88,10 +90,28 @@ class EMail: part = MIMEText(message, 'plain', 'utf-8') self.msg_multipart.attach(part) - def set_part_html(self, message): + def set_part_html(self, message, inline_images): from email.mime.text import MIMEText - part = MIMEText(message, 'html', 'utf-8') - self.msg_multipart.attach(part) + if inline_images: + related = MIMEMultipart('related') + + for image in inline_images: + # images in dict like {filename:'', filecontent:'raw'} + content_id = random_string(10) + + # replace filename in message with CID + message = re.sub('''src=['"]{0}['"]'''.format(image.get('filename')), + 'src="cid:{0}"'.format(content_id), message) + + self.add_attachment(image.get('filename'), image.get('filecontent'), + None, content_id=content_id, parent=related) + + html_part = MIMEText(message, 'html', 'utf-8') + related.attach(html_part) + + self.msg_multipart.attach(related) + else: + self.msg_multipart.attach(MIMEText(message, 'html', 'utf-8')) def set_html_as_text(self, html): """return html2text""" @@ -118,7 +138,8 @@ class EMail: self.add_attachment(res[0], res[1]) - def add_attachment(self, fname, fcontent, content_type=None): + def add_attachment(self, fname, fcontent, content_type=None, + parent=None, content_id=None): """add attachment""" from email.mime.audio import MIMEAudio from email.mime.base import MIMEBase @@ -155,8 +176,13 @@ class EMail: if fname: part.add_header(b'Content-Disposition', ("attachment; filename=\"%s\"" % fname).encode('utf-8')) + if content_id: + part.add_header(b'Content-ID', '<{0}>'.format(content_id)) - self.msg_root.attach(part) + if not parent: + parent = self.msg_root + + parent.attach(part) def add_pdf_attachment(self, name, html, options=None): self.add_attachment(name, get_pdf(html, options), 'application/octet-stream') diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 2fe1e34486..cbcaffe738 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -21,7 +21,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, 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): + queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None): """Add email to sending queue (Email Queue) :param recipients: List of recipients. @@ -42,6 +42,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc :param queue_separately: Queue each email separately :param is_notification: Marks email as notification so will not trigger notifications from system :param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1. + :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id """ if not unsubscribe_method: unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe" @@ -112,6 +113,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc read_receipt=read_receipt, queue_separately=queue_separately, is_notification = is_notification, + inline_images = inline_images, now=now) @@ -152,7 +154,8 @@ def get_email_queue(recipients, sender, subject, **kwargs): reply_to=kwargs.get('reply_to'), cc=kwargs.get('cc'), email_account=kwargs.get('email_account'), - expose_recipients=kwargs.get('expose_recipients')) + expose_recipients=kwargs.get('expose_recipients'), + inline_images=kwargs.get('inline_images')) mail.set_message_id(kwargs.get('message_id'),kwargs.get('is_notification')) if kwargs.get('read_receipt'): @@ -431,7 +434,7 @@ def prepare_message(email, recipient, recipients_list): message = email.message if not message: return "" - + if email.add_unsubscribe_link and email.reference_doctype: # is missing the check for unsubscribe message but will not add as there will be no unsubscribe url unsubscribe_url = get_unsubcribed_url(email.reference_doctype, email.reference_name, recipient, email.unsubscribe_method, email.unsubscribe_params) @@ -461,19 +464,19 @@ def clear_outbox(): Called daily via scheduler. Note: Used separate query to avoid deadlock """ - - email_queues = frappe.db.sql_list("""select name from `tabEmail Queue` + + email_queues = frappe.db.sql_list("""select name from `tabEmail Queue` where priority=0 and datediff(now(), modified) > 31""") - + if email_queues: - frappe.db.sql("""delete from `tabEmail Queue` where name in (%s)""" + frappe.db.sql("""delete from `tabEmail Queue` where name in (%s)""" % ','.join(['%s']*len(email_queues)), tuple(email_queues)) - - frappe.db.sql("""delete from `tabEmail Queue Recipient` where parent in (%s)""" + + frappe.db.sql("""delete from `tabEmail Queue Recipient` where parent in (%s)""" % ','.join(['%s']*len(email_queues)), tuple(email_queues)) - + for dt in ("Email Queue", "Email Queue Recipient"): frappe.db.sql(""" - update `tab{0}` + update `tab{0}` set status='Expired' where datediff(curdate(), modified) > 7 and status='Not Sent'""".format(dt)) \ No newline at end of file diff --git a/frappe/public/css/avatar.css b/frappe/public/css/avatar.css index 725bef2f9a..b4dad4a52b 100644 --- a/frappe/public/css/avatar.css +++ b/frappe/public/css/avatar.css @@ -50,6 +50,14 @@ .avatar-large .standard-image { font-size: 36px; } +.avatar-xl { + margin-right: 10px; + width: 108px; + height: 108px; +} +.avatar-xl .standard-image { + font-size: 72px; +} .avatar-xs { margin-right: 3px; margin-top: -2px; diff --git a/frappe/public/js/frappe/form/control.js b/frappe/public/js/frappe/form/control.js index be94b5b1a7..d25b4616ca 100755 --- a/frappe/public/js/frappe/form/control.js +++ b/frappe/public/js/frappe/form/control.js @@ -100,7 +100,7 @@ frappe.ui.form.Control = Class.extend({ set_value: function(value) { this.parse_validate_and_set_in_model(value); }, - parse_validate_and_set_in_model: function(value) { + parse_validate_and_set_in_model: function(value, e) { var me = this; if(this.inside_change_event) return; this.inside_change_event = true; @@ -110,6 +110,11 @@ frappe.ui.form.Control = Class.extend({ me.set_model_value(value); me.inside_change_event = false; me.set_mandatory && me.set_mandatory(value); + + if(me.df.change || me.df.onchange) { + // onchange event specified in df + (me.df.change || me.df.onchange).apply(me, [e]); + } } this.validate ? this.validate(value, set) : set(value); @@ -333,39 +338,8 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ bind_change_event: function() { var me = this; - this.$input && this.$input.on("change", this.change || function(e) { - if(me.df.change || me.df.onchange) { - // onchange event specified in df - (me.df.change || me.df.onchange).apply(this, [e]); - return; - } - if(me.doctype && me.docname && me.get_value) { - me.parse_validate_and_set_in_model(me.get_value()); - } else { - // inline - var value = me.get_value(); - var parsed = me.parse ? me.parse(value) : value; - var set_input = function(before, after) { - if(before !== after) { - me.set_input(after); - } - if(me.doc) { - me.doc[me.df.fieldname] = value; - } - me.set_mandatory && me.set_mandatory(after); - if(me.after_validate) { - me.after_validate(after, me.$input); - } - } - if(me.validate) { - me.validate(parsed, function(validated) { - set_input(value, validated); - }); - } else { - set_input(value, parsed); - } - } + me.parse_validate_and_set_in_model(me.get_value(), e); }); }, bind_focusout: function() { @@ -1430,22 +1404,8 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ return; } var value = me.get_value(); - if(me.doctype && me.docname) { - if(value!==me.last_value) { - me.parse_validate_and_set_in_model(value); - } - } else { - var cache_list = me.$input.cache[me.get_options()]; - if (cache_list && cache_list[""]) { - var docs = cache_list[""].map(item => item.label); - if(docs.includes(value)) { - me.set_mandatory(value); - } else { - me.$input.val(""); - } - } else { - me.$input.val(value); - } + if(value!==me.last_value) { + me.parse_validate_and_set_in_model(value); } }); @@ -1487,17 +1447,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ frappe.boot.user.last_selected_values[me.df.options] = item.value; } - if(me.frm && me.frm.doc) { - me.selected = true; - me.parse_validate_and_set_in_model(item.value); - setTimeout(function() { - me.selected = false; - }, 100); - } else { - me.$input.val(item.value); - me.$input.trigger("change"); - me.set_mandatory(item.value); - } + me.parse_validate_and_set_in_model(item.value); }); this.$input.on("awesomplete-selectcomplete", function(e) { @@ -1585,11 +1535,49 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ return; } - if(this.frm) { - this.frm.script_manager.validate_link_and_fetch(this.df, this.get_options(), - this.docname, value, callback); + this.validate_link_and_fetch(this.df, this.get_options(), + this.docname, value, callback); + }, + validate_link_and_fetch: function(df, doctype, docname, value, callback) { + var me = this; + + if(value) { + var fetch = ''; + + if(this.frm && this.frm.fetch_dict[df.fieldname]) { + fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); + } + + return frappe.call({ + method:'frappe.desk.form.utils.validate_link', + type: "GET", + args: { + 'value': value, + 'options': doctype, + 'fetch': fetch + }, + no_spinner: true, + callback: function(r) { + if(r.message=='Ok') { + if(r.fetch_values && docname) { + me.set_fetch_values(df, docname, r.fetch_values); + } + if(callback) callback(r.valid_value); + } else { + if(callback) callback(""); + } + } + }); + } else if(callback) { + callback(value); } }, + set_fetch_values: function(df, docname, fetch_values) { + var fl = this.frm.fetch_dict[df.fieldname].fields; + for(var i=0; i < fl.length; i++) { + frappe.model.set_value(df.parent, docname, fl[i], fetch_values[i], df.fieldtype); + } + } }); if(Awesomplete) { diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index cc9de2587c..8aa7abfef1 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -169,7 +169,7 @@ frappe.ui.form.Grid = Class.extend({ this.truncate_rows(data); this.grid_rows_by_docname = {}; - for(var ri=0;ri < data.length; ri++) { + for(var ri=0; ri < data.length; ri++) { var d = data[ri]; if(d.idx===undefined) { @@ -951,7 +951,6 @@ frappe.ui.form.GridRow = Class.extend({ parent = column.field_area, df = column.df; - // no text editor in grid if (df.fieldtype=='Text Editor') { df.fieldtype = 'Text'; @@ -966,6 +965,8 @@ frappe.ui.form.GridRow = Class.extend({ doctype: this.doc.doctype, docname: this.doc.name, frm: this.grid.frm, + grid: this.grid, + grid_row: this, value: this.doc[df.fieldname] }); diff --git a/frappe/public/js/frappe/form/linked_with.js b/frappe/public/js/frappe/form/linked_with.js index 7c9b5a3619..4ca2ca568d 100644 --- a/frappe/public/js/frappe/form/linked_with.js +++ b/frappe/public/js/frappe/form/linked_with.js @@ -103,9 +103,11 @@ frappe.ui.form.LinkedWith = class LinkedWith { var me = this; let links = null; - links = - Object.keys(this.frm.__linked_doctypes) - .filter(frappe.model.can_get_report); + if (this.frm.__linked_doctypes) { + links = + Object.keys(this.frm.__linked_doctypes) + .filter(frappe.model.can_get_report); + } let flag; if(!links) { diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js index ad34dcc6a7..be79a38c88 100644 --- a/frappe/public/js/frappe/form/script_manager.js +++ b/frappe/public/js/frappe/form/script_manager.js @@ -150,44 +150,6 @@ frappe.ui.form.ScriptManager = Class.extend({ console.log("----- end of error message -----"); console.group && console.groupEnd(); }, - validate_link_and_fetch: function(df, doctype, docname, value, callback) { - var me = this; - - if(value) { - var fetch = ''; - - if(this.frm && this.frm.fetch_dict[df.fieldname]) - fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); - - return frappe.call({ - method:'frappe.desk.form.utils.validate_link', - type: "GET", - args: { - 'value': value, - 'options': doctype, - 'fetch': fetch - }, - no_spinner: true, - callback: function(r) { - if(r.message=='Ok') { - if(r.fetch_values) - me.set_fetch_values(df, docname, r.fetch_values); - if(callback) callback(r.valid_value); - } else { - if(callback) callback(""); - } - } - }); - } else if(callback) { - callback(value); - } - }, - set_fetch_values: function(df, docname, fetch_values) { - var fl = this.frm.fetch_dict[df.fieldname].fields; - for(var i=0; i < fl.length; i++) { - frappe.model.set_value(df.parent, docname, fl[i], fetch_values[i], df.fieldtype); - } - }, copy_from_first_row: function(parentfield, current_row, fieldnames) { var data = this.frm.doc[parentfield]; if(data.length===1 || data[0]===current_row) return; diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index 2212870480..b89a5b32ad 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -252,6 +252,8 @@ frappe.views.Calendar = Class.extend({ // see event_calendar.js color = d.color; } + + if(!color) color = "blue"; d.className = "fc-bg-" + color; return d; }); diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 31374cbb09..2d7a1e7a3e 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -315,21 +315,18 @@ frappe.views.QueryReport = Class.extend({ if(df.get_query) f.get_query = df.get_query; if(df.on_change) f.on_change = df.on_change; - - // run report on change - f.$input.on("change", function() { + df.onchange = () => { if(!me.flags.filters_set) { // don't trigger change while setting filters return; } - f.$input.blur(); if (f.on_change) { f.on_change(me); } else { me.trigger_refresh(); } f.set_mandatory && f.set_mandatory(f.$input.val()); - }); + } } }); @@ -416,6 +413,7 @@ frappe.views.QueryReport = Class.extend({ return; } }); + if (!missing) { me.refresh(); } diff --git a/frappe/public/less/avatar.less b/frappe/public/less/avatar.less index 363fd52767..a3e4925bd0 100644 --- a/frappe/public/less/avatar.less +++ b/frappe/public/less/avatar.less @@ -63,6 +63,16 @@ } } +.avatar-xl { + margin-right: 10px; + width: 108px; + height: 108px; + + .standard-image { + font-size: 72px; + } +} + .avatar-xs { margin-right: 3px; margin-top: -2px; diff --git a/requirements.txt b/requirements.txt index dfcb0cb574..2e39e22286 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ httplib2 jinja2 markdown2 markupsafe -mysqlclient==1.3.8 +mysqlclient==1.3.10 python-geoip python-geoip-geolite2 python-dateutil @@ -41,4 +41,4 @@ xlwt oauthlib PyJWT pypdf -openpyxl \ No newline at end of file +openpyxl