diff --git a/cypress/fixtures/custom_submittable_doctype.js b/cypress/fixtures/custom_submittable_doctype.js new file mode 100644 index 0000000000..c88d37b373 --- /dev/null +++ b/cypress/fixtures/custom_submittable_doctype.js @@ -0,0 +1,53 @@ +export default { + name: 'Custom Submittable DocType', + custom: 1, + actions: [], + is_submittable: 1, + creation: '2019-12-10 06:29:07.215072', + doctype: 'DocType', + editable_grid: 1, + engine: 'InnoDB', + fields: [ + { + fieldname: 'enabled', + fieldtype: 'Check', + label: 'Enabled', + allow_on_submit: 1, + reqd: 1 + }, + { + fieldname: 'title', + fieldtype: 'Data', + label: 'title', + reqd: 1 + }, + { + fieldname: 'description', + fieldtype: 'Text Editor', + label: 'Description' + } + ], + links: [], + modified: '2019-12-10 14:40:53.127615', + modified_by: 'Administrator', + module: 'Custom', + owner: 'Administrator', + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: 'System Manager', + share: 1, + write: 1, + submit: 1, + cancel: 1 + } + ], + quick_entry: 1, + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; \ No newline at end of file diff --git a/cypress/integration/form.js b/cypress/integration/form.js index 5a8b85d19e..ed5ff21ff1 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -1,8 +1,4 @@ context('Form', () => { - beforeEach(() => { - cy.login(); - cy.visit('/desk'); - }); before(() => { cy.login(); cy.visit('/desk'); @@ -10,6 +6,9 @@ context('Form', () => { frappe.call("frappe.tests.ui_test_helpers.create_contact_records"); }); }); + beforeEach(() => { + cy.visit('/desk'); + }); it('create a new form', () => { cy.visit('/desk#Form/ToDo/New ToDo 1'); cy.fill_field('description', 'this is a test todo', 'Text Editor').blur(); @@ -27,10 +26,11 @@ context('Form', () => { cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true }); cy.get('.filter-box .btn:contains("Apply")').click({ force: true }); cy.visit('/desk#Form/Contact/Test Form Contact 3'); - cy.get('.prev-doc').click(); + cy.get('.prev-doc').click({ force: true }); cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible'); - cy.get('.modal-backdrop').click(); - cy.get('.next-doc').click(); + cy.get('.btn-modal-close:visible').click(); + cy.get('.next-doc').click({ force: true }); + cy.wait(200); cy.contains('Test Form Contact 2').should('not.exist'); cy.get('.page-title .title-text').should('contain', 'Test Form Contact 1'); cy.visit('/desk#List/Contact'); diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js index cf41e31ee6..67fdb8acf0 100644 --- a/cypress/integration/grid_pagination.js +++ b/cypress/integration/grid_pagination.js @@ -14,15 +14,15 @@ context('Grid Pagination', () => { cy.visit('/desk#Form/Contact/Test Contact'); cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('.current-page-number').should('contain', '1'); - cy.get('@table').find('.total-page-number').should('contain', '50'); - cy.get('@table').find('.grid-body .grid-row').should('have.length', 20); + cy.get('@table').find('.total-page-number').should('contain', '20'); + cy.get('@table').find('.grid-body .grid-row').should('have.length', 50); }); it('goes to the next and previous page', () => { cy.visit('/desk#Form/Contact/Test Contact'); cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('.next-page').click(); cy.get('@table').find('.current-page-number').should('contain', '2'); - cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '21'); + cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '51'); cy.get('@table').find('.prev-page').click(); cy.get('@table').find('.current-page-number').should('contain', '1'); cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1'); @@ -32,19 +32,20 @@ context('Grid Pagination', () => { cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('button.grid-add-row').click(); cy.get('@table').find('.grid-body .row-index').should('contain', 1001); - cy.get('@table').find('.current-page-number').should('contain', '51'); - cy.get('@table').find('.total-page-number').should('contain', '51'); + cy.get('@table').find('.current-page-number').should('contain', '21'); + cy.get('@table').find('.total-page-number').should('contain', '21'); cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({force: true}); cy.get('@table').find('button.grid-remove-rows').click(); cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000); - cy.get('@table').find('.current-page-number').should('contain', '50'); - cy.get('@table').find('.total-page-number').should('contain', '50'); + cy.get('@table').find('.current-page-number').should('contain', '20'); + cy.get('@table').find('.total-page-number').should('contain', '20'); }); it('deletes all rows', ()=> { cy.visit('/desk#Form/Contact/Test Contact'); cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true}); cy.get('@table').find('button.grid-remove-all-rows').click(); + cy.get('.modal-dialog .btn-primary').contains('Yes').click(); cy.get('@table').find('.grid-body .grid-row').should('have.length', 0); }); }); \ No newline at end of file diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js new file mode 100644 index 0000000000..c7aeaa92de --- /dev/null +++ b/cypress/integration/report_view.js @@ -0,0 +1,40 @@ +import custom_submittable_doctype from '../fixtures/custom_submittable_doctype'; +const doctype_name = custom_submittable_doctype.name; + +context('Report View', () => { + before(() => { + cy.login(); + cy.visit('/desk'); + cy.insert_doc('DocType', custom_submittable_doctype, true); + cy.clear_cache(); + cy.insert_doc(doctype_name, { + 'title': 'Doc 1', + 'description': 'Random Text', + 'enabled': 0, + // submit document + 'docstatus': 1 + }, true).as('doc'); + }); + it('Field with enabled allow_on_submit should be editable.', () => { + cy.server(); + cy.route('POST', 'api/method/frappe.client.set_value').as('value-update'); + cy.visit(`/desk#List/${doctype_name}/Report`); + let cell = cy.get('.dt-row-0 > .dt-cell--col-3'); + // select the cell + cell.dblclick(); + cell.find('input[data-fieldname="enabled"]').check({force: true}); + cy.get('.dt-row-0 > .dt-cell--col-4').click(); + cy.wait('@value-update'); + cy.get('@doc').then(doc => { + cy.call('frappe.client.get_value', { + doctype: doc.doctype, + filters: { + name: doc.name, + }, + fieldname: 'enabled' + }).then(r => { + expect(r.message.enabled).to.equals(1); + }); + }); + }); +}); \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 464cbbe1d5..41d9c16d7b 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -183,3 +183,31 @@ Cypress.Commands.add('hide_dialog', () => { cy.get_open_dialog().find('.btn-modal-close').click(); cy.get('.modal:visible').should('not.exist'); }); + +Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'POST', + url: `/api/resource/${doctype}`, + body: args, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + failOnStatusCode: !ignore_duplicate + }) + .then(res => { + let status_codes = [200]; + if (ignore_duplicate) { + status_codes.push(409); + } + expect(res.status).to.be.oneOf(status_codes); + return res.body.data; + }); + }); +}); \ No newline at end of file diff --git a/frappe/__init__.py b/frappe/__init__.py index cb1b6e5358..b383ae958e 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -23,7 +23,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '12.0.20' +__version__ = '12.1.0' __title__ = "Frappe Framework" local = Local() diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index b431c7c473..55792b2648 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -31,12 +31,6 @@ class AssignmentRule(Document): return False - def apply_close(self, doc, assignments): - if (self.close_assignments and - self.name in [d.assignment_rule for d in assignments]): - return self.close_assignments(doc) - - return False def apply_assign(self, doc): if self.safe_eval('assign_condition', doc): @@ -157,16 +151,17 @@ def bulk_apply(doctype, docnames): apply(None, doctype=doctype, name=name) def reopen_closed_assignment(doc): - todo = frappe.db.exists('ToDo', dict( + todo_list = frappe.db.get_all('ToDo', filters = dict( reference_type = doc.doctype, reference_name = doc.name, status = 'Closed' )) - if not todo: + if not todo_list: return False - todo = frappe.get_doc("ToDo", todo) - todo.status = 'Open' - todo.save(ignore_permissions=True) + for todo in todo_list: + todo_doc = frappe.get_doc('ToDo', todo.name) + todo_doc.status = 'Open' + todo_doc.save(ignore_permissions=True) return True def apply(doc, method=None, doctype=None, name=None): @@ -225,13 +220,12 @@ def apply(doc, method=None, doctype=None, name=None): continue if not new_apply: - reopen = reopen_closed_assignment(doc) - if reopen: - break - close = assignment_rule.apply_close(doc, assignments) - if close: - break - + # only reopen if close condition is not satisfied + if not assignment_rule.safe_eval('close_condition', doc): + reopen = reopen_closed_assignment(doc) + if reopen: + break + assignment_rule.close_assignments(doc) def get_assignment_rules(): return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))] diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 27f17a1a62..e618c7d63e 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -117,8 +117,8 @@ class AutoRepeat(Document): start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day) if self.end_date: - start_date = start_date = get_next_schedule_date( - start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) + start_date = get_next_schedule_date( + start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True) while (getdate(start_date) < getdate(end_date)): row = { "reference_document" : self.reference_document, @@ -126,10 +126,9 @@ class AutoRepeat(Document): "next_scheduled_date" : start_date } schedule_details.append(row) - start_date = start_date = get_next_schedule_date( + start_date = get_next_schedule_date( start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) - return schedule_details def create_documents(self): diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 82fff31394..36c297cc26 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "Prompt", "creation": "2019-09-30 11:56:57.943241", "doctype": "DocType", @@ -43,7 +44,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete" + "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" }, { "depends_on": "eval:doc.script_type==='API'", @@ -73,7 +74,8 @@ "fieldtype": "Section Break" } ], - "modified": "2019-10-09 15:08:40.085059", + "links": [], + "modified": "2019-12-17 12:55:07.389775", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 878810f459..2e1a5ae8bb 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -14,6 +14,8 @@ EVENT_MAP = { 'on_cancel': 'After Cancel', 'on_trash': 'Before Delete', 'after_delete': 'After Delete', + 'before_update_after_submit': 'Before Save (Submitted Document)', + 'on_update_after_submit': 'After Save (Submitted Document)' } def run_server_script_api(method): diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index b142047059..3a8815ca71 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -182,6 +182,8 @@ def get_notification_info(): return out def get_notification_config(): + user = frappe.session.user or 'Guest' + def _get(): subscribed_documents = get_subscribed_documents() config = frappe._dict() @@ -205,7 +207,7 @@ def get_notification_config(): config[key].update(nc.get(key, {})) return config - return frappe.cache().hget("notification_config", frappe.session.user, _get) + return frappe.cache().hget("notification_config", user, _get) def get_filters_for(doctype): '''get open filters for doctype''' diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py index bb598ab180..e7e147fb7d 100644 --- a/frappe/desk/page/setup_wizard/install_fixtures.py +++ b/frappe/desk/page/setup_wizard/install_fixtures.py @@ -10,6 +10,7 @@ from frappe.desk.doctype.global_search_settings.global_search_settings import up def install(): update_genders_and_salutations() update_global_search_doctypes() + setup_email_linking() @frappe.whitelist() def update_genders_and_salutations(): @@ -20,13 +21,12 @@ def update_genders_and_salutations(): for record in records: doc = frappe.new_doc(record.get("doctype")) doc.update(record) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) - try: - doc.insert(ignore_permissions=True) - except frappe.DuplicateEntryError as e: - # pass DuplicateEntryError and continue - if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name: - # make sure DuplicateEntryError is for the exact same doc and not a related doc - pass - else: - raise \ No newline at end of file +def setup_email_linking(): + doc = frappe.get_doc({ + "doctype": "Email Account", + "email_id": "email_linking@example.com", + }) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + \ No newline at end of file diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 21a69f5111..7dc561193f 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -510,7 +510,7 @@ def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner cell_value = None if isinstance(row, dict): cell_value = row.get(idx) - elif isinstance(row, list): + elif isinstance(row, (list, tuple)): cell_value = row[idx] if dt in match_filters and cell_value not in match_filters.get(dt) and frappe.db.exists(dt, cell_value): diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 5db6ae18bf..9caf72d3bd 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -267,18 +267,14 @@ def get_sidebar_stats(stats, doctype, filters=[]): data = frappe._dict(frappe.local.form_dict) filters = json.loads(data["filters"]) - if not frappe.cache().hget("Tags", doctype): - tags = set([tag.tag for tag in frappe.get_list("Tag Link", filters={"document_type": doctype}, fields=["tag"])]) - frappe.cache().hset("Tags", doctype, tags) - - for tag in list(frappe.cache().hget("Tags", doctype)): + for tag in frappe.get_all("Tag Link", filters={"document_type": doctype}, fields=["tag"]): tag_filters = [] tag_filters.extend(filters) - tag_filters.extend([['Tag Link', 'tag', '=', tag]]) + tag_filters.extend([['Tag Link', 'tag', '=', tag.tag]]) count = frappe.get_all(doctype, filters=tag_filters, fields=["count(*)"]) if count[0].get("count(*)") > 0: - _user_tags.append([tag, count[0].get("count(*)")]) + _user_tags.append([tag.tag, count[0].get("count(*)")]) return {"stats": {"_user_tags": _user_tags}} diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index c05a0f3fe4..495644f652 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -322,16 +322,16 @@ class EmailAccount(Document): unhandled_email.insert(ignore_permissions=True) frappe.db.commit() - def insert_communication(self, msg, args={}): + def insert_communication(self, msg, args=None): if isinstance(msg, list): raw, uid, seen = msg else: raw = msg uid = -1 seen = 0 - - if args.get("uid", -1): uid = args.get("uid", -1) - if args.get("seen", 0): seen = args.get("seen", 0) + if isinstance(args, dict): + if args.get("uid", -1): uid = args.get("uid", -1) + if args.get("seen", 0): seen = args.get("seen", 0) email = Email(raw) @@ -355,7 +355,7 @@ class EmailAccount(Document): name = names[0].get("name") # email is already available update communication uid instead frappe.db.set_value("Communication", name, "uid", uid, update_modified=False) - return + return frappe.get_doc("Communication", name) if email.content_type == 'text/html': email.content = clean_email_html(email.content) diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 792b47296a..1c9a2fd3de 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -3,22 +3,25 @@ from __future__ import unicode_literals import frappe +import sys from six.moves import html_parser as HTMLParser import smtplib, quopri, json -from frappe import msgprint, throw, _, safe_decode +from frappe import msgprint, _, safe_decode from frappe.email.smtp import SMTPServer, get_outgoing_email_account from frappe.email.email_body import get_email, get_formatted_html, add_attachment from frappe.utils.verified_command import get_signed_params, verify_request from html2text import html2text -from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint +from frappe.utils import get_url, nowdate, now_datetime, add_days, split_emails, cstr, cint from rq.timeouts import JobTimeoutException -from six import text_type, string_types +from six import text_type, string_types, PY3 +from email.parser import Parser + class EmailLimitCrossedError(frappe.ValidationError): pass def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None, reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, - attachments=None, reply_to=None, cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, + attachments=None, reply_to=None, cc=None, bcc=None, 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=None, print_letterhead=False): @@ -52,6 +55,11 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= if not recipients and not cc: return + if not cc: + cc = [] + if not bcc: + bcc = [] + if isinstance(recipients, string_types): recipients = split_emails(recipients) @@ -68,7 +76,6 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= if not sender or sender == "Administrator": sender = email_account.default_sender - if not text_content: try: text_content = html2text(message) @@ -404,7 +411,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals message = prepare_message(email, recipient.recipient, recipients_list) if not frappe.flags.in_test: - smtpserver.sess.sendmail(email.sender, recipient.recipient, encode(message)) + smtpserver.sess.sendmail(email.sender, recipient.recipient, message) recipient.status = "Sent" frappe.db.sql("""update `tabEmail Queue Recipient` set status='Sent', modified=%s where name=%s""", @@ -509,37 +516,41 @@ def prepare_message(email, recipient, recipients_list): message = (message and message.encode('utf8')) or '' message = safe_decode(message) - if not email.attachments: - return message - # On-demand attachments - from email.parser import Parser + if PY3: + from email.policy import SMTPUTF8 + message = Parser(policy=SMTPUTF8).parsestr(message) + else: + message = Parser().parsestr(message) - msg_obj = Parser().parsestr(message) - attachments = json.loads(email.attachments) + if email.attachments: + # On-demand attachments - for attachment in attachments: - if attachment.get('fcontent'): continue + attachments = json.loads(email.attachments) - fid = attachment.get("fid") - if fid: - _file = frappe.get_doc("File", fid) - fcontent = _file.get_content() - attachment.update({ - 'fname': _file.file_name, - 'fcontent': fcontent, - 'parent': msg_obj - }) - attachment.pop("fid", None) - add_attachment(**attachment) + for attachment in attachments: + if attachment.get('fcontent'): + continue - elif attachment.get("print_format_attachment") == 1: - attachment.pop("print_format_attachment", None) - print_format_file = frappe.attach_print(**attachment) - print_format_file.update({"parent": msg_obj}) - add_attachment(**print_format_file) + fid = attachment.get("fid") + if fid: + _file = frappe.get_doc("File", fid) + fcontent = _file.get_content() + attachment.update({ + 'fname': _file.file_name, + 'fcontent': fcontent, + 'parent': message + }) + attachment.pop("fid", None) + add_attachment(**attachment) - return msg_obj.as_string() + elif attachment.get("print_format_attachment") == 1: + attachment.pop("print_format_attachment", None) + print_format_file = frappe.attach_print(**attachment) + print_format_file.update({"parent": message}) + add_attachment(**print_format_file) + + return message.as_string() def clear_outbox(): """Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days. diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index feb8e80007..26c4e5ba5d 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -5,7 +5,10 @@ from __future__ import unicode_literals import unittest, os, base64 from frappe.email.receive import Email from frappe.email.email_body import (replace_filename_with_cid, - get_email, inline_style_in_html, get_header) + get_email, inline_style_in_html, get_header) +from frappe.email.queue import prepare_message, get_email_queue +from six import PY3 + class TestEmailBody(unittest.TestCase): def setUp(self): @@ -37,6 +40,53 @@ This is the text version of this email text_content=email_text ).as_string() + def test_prepare_message_returns_already_encoded_string(self): + + if PY3: + uni_chr1 = chr(40960) + uni_chr2 = chr(1972) + else: + uni_chr1 = unichr(40960) + uni_chr2 = unichr(1972) + + email = get_email_queue( + recipients=['test@example.com'], + sender='me@example.com', + subject='Test Subject', + content='

' + uni_chr1 + 'abcd' + uni_chr2 + '

', + formatted='

' + uni_chr1 + 'abcd' + uni_chr2 + '

', + text_content='whatever') + result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + self.assertTrue("

=EA=80=80abcd=DE=B4

" in result) + + def test_prepare_message_returns_cr_lf(self): + email = get_email_queue( + recipients=['test@example.com'], + sender='me@example.com', + subject='Test Subject', + content='

\n this is a test of newlines\n' + '

', + formatted='

\n this is a test of newlines\n' + '

', + text_content='whatever') + result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + if PY3: + self.assertTrue(result.count('\n') == result.count("\r")) + else: + self.assertTrue(True) + + def test_rfc_5322_header_is_wrapped_at_998_chars(self): + # unfortunately the db can only hold 140 chars so this can't be tested properly. test at max chars anyway. + email = get_email_queue( + recipients=['test@example.com'], + sender='me@example.com', + subject='Test Subject', + content='

Whatever

', + text_content='whatever', + message_id= "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" + + ".really.long.message.id.that.should.not.wrap.unti") + result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + self.assertTrue( + "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" + + ".really.long.message.id.that.should.not.wrap.unti" in result) def test_image(self): img_signature = ''' @@ -49,7 +99,6 @@ Content-Disposition: inline; filename="favicon.png" self.assertTrue(img_signature in self.email_string) self.assertTrue(self.img_base64 in self.email_string) - def test_text_content(self): text_content = ''' Content-Type: text/plain; charset="utf-8" @@ -62,7 +111,6 @@ This is the text version of this email ''' self.assertTrue(text_content in self.email_string) - def test_email_content(self): html_head = ''' Content-Type: text/html; charset="utf-8" @@ -79,7 +127,6 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> self.assertTrue(html_head in self.email_string) self.assertTrue(html in self.email_string) - def test_replace_filename_with_cid(self): original_message = '''
@@ -152,6 +199,7 @@ Reply-To: test2_@erpnext.com mail = Email(content_bytes) self.assertEqual(mail.text_content, text_content) + def fixed_column_width(string, chunk_size): - parts = [string[0+i:chunk_size+i] for i in range(0, len(string), chunk_size)] - return '\n'.join(parts) \ No newline at end of file + parts = [string[0 + i:chunk_size + i] for i in range(0, len(string), chunk_size)] + return '\n'.join(parts) diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.json b/frappe/integrations/doctype/social_login_key/social_login_key.json index 290aae0e4e..6c0fbdb26c 100644 --- a/frappe/integrations/doctype/social_login_key/social_login_key.json +++ b/frappe/integrations/doctype/social_login_key/social_login_key.json @@ -157,7 +157,7 @@ "label": "User ID Property" } ], - "modified": "2019-12-03 12:35:55.115260", + "modified": "2019-12-03 13:13:46.989099", "modified_by": "Administrator", "module": "Integrations", "name": "Social Login Key", diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index 6dd0ff3eec..f697d8051a 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -61,7 +61,9 @@ def set_user_and_static_default_values(doc): user_default_value = get_user_default_value(df, defaults, doctype_user_permissions, allowed_records, default_doc) if user_default_value is not None: - doc.set(df.fieldname, user_default_value) + # if fieldtype is link check if doc exists + if not df.fieldtype == "Link" or frappe.db.exists(df.options, user_default_value): + doc.set(df.fieldname, user_default_value) else: if df.fieldname != doc.meta.title_field: diff --git a/frappe/patches.txt b/frappe/patches.txt index 90abc8c31d..1599fd50ac 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -260,4 +260,6 @@ frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable frappe.patches.v12_0.copy_to_parent_for_tags frappe.patches.v12_0.create_notification_settings_for_user frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26 +frappe.patches.v12_0.setup_email_linking +frappe.patches.v12_0.fix_home_settings_for_all_users execute:frappe.delete_doc("Test Runner") \ No newline at end of file diff --git a/frappe/patches/v12_0/fix_home_settings_for_all_users.py b/frappe/patches/v12_0/fix_home_settings_for_all_users.py new file mode 100644 index 0000000000..e26cbd9eef --- /dev/null +++ b/frappe/patches/v12_0/fix_home_settings_for_all_users.py @@ -0,0 +1,41 @@ +import frappe +from frappe.config import get_modules_from_all_apps_for_user +import json +def execute(): + users = frappe.get_all('User', fields=['name', 'home_settings']) + + for user in users: + + if not user.home_settings: + continue + + home_settings = json.loads(user.home_settings) + + modules_by_category = home_settings.get('modules_by_category') + if not modules_by_category: + continue + visible_modules = [] + category_to_check = [] + + for category, modules in modules_by_category.items(): + visible_modules += modules + category_to_check.append(category) + + all_modules = get_modules_from_all_apps_for_user(user.name) + all_modules = set([m.get('name') or m.get('module_name') or m.get('label') \ + for m in all_modules if m.get('category') in category_to_check]) + + hidden_modules = home_settings.get("hidden_modules", []) + + modules_in_home_settings = set(visible_modules + hidden_modules) + + all_modules = all_modules.union(modules_in_home_settings) + + missing_modules = all_modules - modules_in_home_settings + + if missing_modules: + home_settings['hidden_modules'] = hidden_modules + list(missing_modules) + home_settings = json.dumps(home_settings) + frappe.set_value('User', user.name, 'home_settings', home_settings) + + frappe.cache().delete_key('home_settings') diff --git a/frappe/patches/v12_0/setup_email_linking.py b/frappe/patches/v12_0/setup_email_linking.py new file mode 100644 index 0000000000..08f57ca5e4 --- /dev/null +++ b/frappe/patches/v12_0/setup_email_linking.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals + +from frappe.desk.page.setup_wizard.install_fixtures import setup_email_linking + +def execute(): + setup_email_linking() \ No newline at end of file diff --git a/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json b/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json index 2f51d2e18a..e0125bad3c 100644 --- a/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json +++ b/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json @@ -12,9 +12,10 @@ } ], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/letterhead-onboard.png", + "image_src": "", + "is_completed": 1, "max_count": 0, - "modified": "2019-12-03 22:54:57.618989", + "modified": "2019-12-09 15:12:45.588567", "modified_by": "Administrator", "name": "Company Letter Head", "owner": "Administrator", diff --git a/frappe/public/js/frappe/change_log.html b/frappe/public/js/frappe/change_log.html index c05aadfe27..fce6539abc 100644 --- a/frappe/public/js/frappe/change_log.html +++ b/frappe/public/js/frappe/change_log.html @@ -6,11 +6,13 @@ {{ app_info.title }} {{ __("updated to {0}", [app_info.version]) }} +
{% for (var x=0, y=app_info.change_log.length; x < y; x++) { var version_info = app_info.change_log[x]; if(version_info) { %}

{{ frappe.markdown(version_info[1]) }}

{% } } %} +
{% } %} diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 92194acdca..23d7bffec2 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -466,12 +466,27 @@ frappe.Application = Class.extend({ show_change_log: function() { var me = this; - var d = frappe.msgprint( - frappe.render_template("change_log", {"change_log": frappe.boot.change_log}), - __("Updated To New Version") - ); - d.keep_open = true; - d.custom_onhide = function() { + let change_log = frappe.boot.change_log; + + // frappe.boot.change_log = [{ + // "change_log": [ + // [, ], + // [, ], + // ], + // "description": "ERP made simple", + // "title": "ERPNext", + // "version": "12.2.0" + // }]; + + // Iterate over changelog + var change_log_dialog = frappe.msgprint({ + message: frappe.render_template("change_log", {"change_log": change_log}), + title: __("Updated To New Version 🎉"), + wide: true, + scroll: true + }); + change_log_dialog.keep_open = true; + change_log_dialog.custom_onhide = function() { frappe.call({ "method": "frappe.utils.change_log.update_last_known_versions" }); diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index 6411c2fb2b..819ecb526e 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -291,7 +291,7 @@ frappe.get_modal = function(title, content) {