Merge branch 'develop' of https://github.com/frappe/frappe into change-delete-button

This commit is contained in:
Suraj Shetty 2020-01-14 10:08:21 +05:30
commit 45d6951d92
6 changed files with 292 additions and 30 deletions

View file

@ -26,7 +26,7 @@ class TestDocType(unittest.TestCase):
}],
"permissions": [{
"role": "System Manager",
"read": 1
"read": 1,
}],
"name": name
})
@ -295,3 +295,58 @@ class TestDocType(unittest.TestCase):
field_1.search_index = 1
self.assertRaises(CannotIndexedError, doc.insert)
def test_cancel_link_doctype(self):
import json
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
#create doctype
link_doc = self.new_doctype('Test Linked Doctype')
link_doc.is_submittable = 1
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
link_doc.insert()
doc = self.new_doctype('Test Doctype')
doc.is_submittable = 1
field_2 = doc.append('fields', {})
field_2.label = 'Test Linked Doctype'
field_2.fieldname = 'test_linked_doctype'
field_2.fieldtype = 'Link'
field_2.options = 'Test Linked Doctype'
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
doc.insert()
# create doctype data
data_link_doc = frappe.new_doc('Test Linked Doctype')
data_link_doc.some_fieldname = 'Data1'
data_link_doc.insert()
data_link_doc.save()
data_link_doc.submit()
data_doc = frappe.new_doc('Test Doctype')
data_doc.some_fieldname = 'Data1'
data_doc.test_linked_doctype = data_link_doc.name
data_doc.insert()
data_doc.save()
data_doc.submit()
docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name)
dump_docs = json.dumps(docs.get('docs'))
cancel_all_linked_docs(dump_docs)
data_link_doc.cancel()
data_doc.load_from_db()
self.assertEqual(data_link_doc.docstatus, 2)
self.assertEqual(data_doc.docstatus, 2)
# delete doctype record
data_doc.delete()
data_link_doc.delete()
# delete doctype
link_doc.delete()
doc.delete()
frappe.db.commit()

View file

@ -1,14 +1,119 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, json
import json
from collections import defaultdict
from six import string_types
import frappe
import frappe.desk.form.load
import frappe.desk.form.meta
from frappe import _
from frappe.model.meta import is_single
from frappe.modules import load_doctype_module
import frappe.desk.form.meta
import frappe.desk.form.load
from six import string_types
from collections import defaultdict
@frappe.whitelist()
def get_submitted_linked_docs(doctype, name, docs=None):
"""
Get all nested submitted linked doctype linkinfo
Arguments:
doctype (str) - The doctype for which get all linked doctypes
name (str) - The docname for which get all linked doctypes
Keyword Arguments:
docs (list of dict) - (Optional) Get list of dictionary for linked doctype.
Returns:
dict - Return list of documents and link count
"""
if not docs:
docs = []
linkinfo = get_linked_doctypes(doctype)
linked_docs = get_linked_docs(doctype, name, linkinfo)
link_count = 0
for link_doctype, link_names in linked_docs.items():
for link in link_names:
docinfo = link.update({"doctype": link_doctype})
validated_doc = validate_linked_doc(docinfo)
if not validated_doc:
continue
link_count += 1
if link.name in [doc.get("name") for doc in docs]:
continue
links = get_submitted_linked_docs(link_doctype, link.name, docs)
docs.append({
"doctype": link_doctype,
"name": link.name,
"docstatus": link.docstatus,
"link_count": links.get("count")
})
# sort linked documents by ascending number of links
docs.sort(key=lambda doc: doc.get("link_count"))
return {
"docs": docs,
"count": link_count
}
@frappe.whitelist()
def cancel_all_linked_docs(docs):
"""
Cancel all linked doctype
Arguments:
docs (str) - It contains all list of dictionaries of a linked documents.
"""
docs = json.loads(docs)
for i, doc in enumerate(docs, 1):
if validate_linked_doc(doc) is True:
frappe.publish_progress(percent=i * 100 / len(docs), title=_("Cancelling documents"))
linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name"))
linked_doc.cancel()
def validate_linked_doc(docinfo):
"""
Validate a document to be submitted and non-exempted from auto-cancel.
Args:
docs (dict): The document to check for submitted and non-exempt from auto-cancel
Returns:
bool: True if linked document passes all validations, else False
"""
# skip non-submittable doctypes since they don't need to be cancelled
if not frappe.get_meta(docinfo.get('doctype')).is_submittable:
return False
# skip draft or cancelled documents
if docinfo.get('docstatus') != 1:
return False
# skip other doctypes since they don't need to be cancelled
auto_cancel_exempt_doctypes = get_exempted_doctypes()
if docinfo.get('doctype') in auto_cancel_exempt_doctypes:
return False
return True
def get_exempted_doctypes():
""" Get list of doctypes exempted from being auto-cancelled """
auto_cancel_exempt_doctypes = []
for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
auto_cancel_exempt_doctypes.append(doctypes)
return auto_cancel_exempt_doctypes
@frappe.whitelist()
@ -184,8 +289,8 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
if is_single(df.doctype): continue
# optimized to get both link exists and parenttype
possible_link = frappe.db.sql("""select distinct `{doctype_fieldname}`, parenttype
from `tab{doctype}` where `{doctype_fieldname}`=%s""".format(**df), doctype, as_dict=True)
possible_link = frappe.get_all(df.doctype, filters={df.doctype_fieldname: doctype},
fields=['parenttype'], distinct=True)
if not possible_link: continue
@ -203,4 +308,4 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
"doctype_fieldname": df.doctype_fieldname
}
return ret
return ret

View file

@ -31,6 +31,10 @@ def login_via_office365(code, state):
def login_via_salesforce(code, state):
login_via_oauth2("salesforce", code, state, decoder=decoder_compat)
@frappe.whitelist(allow_guest=True)
def login_via_fairlogin(code, state):
login_via_oauth2("fairlogin", code, state, decoder=decoder_compat)
@frappe.whitelist(allow_guest=True)
def custom(code, state):
"""

View file

@ -648,18 +648,96 @@ frappe.ui.form.Form = class FrappeForm {
}
savecancel(btn, callback, on_error) {
var me = this;
const me = this;
this.validate_form_action('Cancel');
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), function() {
frappe.call({
method: "frappe.desk.form.linked_with.get_submitted_linked_docs",
args: {
doctype: me.doc.doctype,
name: me.doc.name
},
freeze: true,
callback: (r) => {
if (!r.exc && r.message.count > 0) {
me._cancel_all(r, btn, callback, on_error);
} else {
me._cancel(btn, callback, on_error, false);
}
}
});
}
_cancel_all(r, btn, callback, on_error) {
const me = this;
// add confirmation message for cancelling all linked docs
let links_text = "";
let links = r.message.docs;
const doctypes = Array.from(new Set(links.map(link => link.doctype)));
for (let doctype of doctypes) {
let docnames = links
.filter((link) => link.doctype == doctype)
.map((link) => frappe.utils.get_form_link(link.doctype, link.name, true))
.join(", ");
links_text += `<li><strong>${doctype}</strong>: ${docnames}</li>`;
}
links_text = `<ul>${links_text}</ul>`;
let confirm_message = __('{0} {1} is linked with the following submitted documents: {2}',
[(me.doc.doctype).bold(), me.doc.name, links_text]);
let can_cancel = links.every((link) => frappe.model.can_cancel(link.doctype));
if (can_cancel) {
confirm_message += __('Do you want to cancel all linked documents?');
} else {
confirm_message += __('You do not have permissions to cancel all linked documents.');
}
// generate dialog box to cancel all linked docs
let d = new frappe.ui.Dialog({
title: __("Cancel All Documents"),
fields: [{
fieldtype: "HTML",
options: `<p class="frappe-confirm-message">${confirm_message}</p>`
}]
}, () => me.handle_save_fail(btn, on_error));
// if user can cancel all linked docs, add action to the dialog
if (can_cancel) {
d.set_primary_action("Cancel All", () => {
d.hide();
frappe.call({
method: "frappe.desk.form.linked_with.cancel_all_linked_docs",
args: {
docs: links
},
freeze: true,
callback: (resp) => {
if (!resp.exc) {
me.reload_doc();
me._cancel(btn, callback, on_error, true);
}
}
});
});
}
d.show();
};
_cancel(btn, callback, on_error, skip_confirm) {
const me = this;
const cancel_doc = () => {
frappe.validated = true;
me.script_manager.trigger("before_cancel").then(function() {
if(!frappe.validated) {
me.script_manager.trigger("before_cancel").then(() => {
if (!frappe.validated) {
return me.handle_save_fail(btn, on_error);
}
var after_cancel = function(r) {
if(r.exc) {
if (r.exc) {
me.handle_save_fail(btn, on_error);
} else {
frappe.utils.play_sound("cancel");
@ -670,8 +748,14 @@ frappe.ui.form.Form = class FrappeForm {
};
frappe.ui.form.save(me, "cancel", after_cancel, btn);
});
}, () => me.handle_save_fail(btn, on_error));
}
}
if (skip_confirm) {
cancel_doc();
} else {
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, me.handle_save_fail(btn, on_error));
}
};
savetrash() {
this.validate_form_action("Delete");

View file

@ -41,7 +41,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
xmloutput_fh = None
if junit_xml_output:
xmloutput_fh = open(junit_xml_output, 'w')
xmloutput_fh = open(junit_xml_output, 'wb')
unittest_runner = xmlrunner_wrapper(xmloutput_fh)
else:
unittest_runner = unittest.TextTestRunner
@ -68,11 +68,11 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
frappe.get_attr(fn)()
if doctype:
ret = run_tests_for_doctype(doctype, verbose, tests, force, profile)
ret = run_tests_for_doctype(doctype, verbose, tests, force, profile, junit_xml_output=junit_xml_output)
elif module:
ret = run_tests_for_module(module, verbose, tests, profile)
ret = run_tests_for_module(module, verbose, tests, profile, junit_xml_output=junit_xml_output)
else:
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast)
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast, junit_xml_output=junit_xml_output)
if frappe.db: frappe.db.commit()
@ -109,7 +109,7 @@ class TimeLoggingTestResult(unittest.TextTestResult):
super(TimeLoggingTestResult, self).addSuccess(test)
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False):
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False, junit_xml_output=False):
import os
apps = [app] if app else frappe.get_installed_apps()
@ -130,11 +130,16 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa
_add_test(app, path, filename, verbose,
test_suite, ui_tests)
if junit_xml_output:
runner = unittest_runner(verbosity=1+(verbose and 1 or 0), failfast=failfast)
else:
runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast)
if profile:
pr = cProfile.Profile()
pr.enable()
out = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast).run(test_suite)
out = runner.run(test_suite)
if profile:
pr.disable()
@ -145,7 +150,7 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa
return out
def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False):
def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False, junit_xml_output=False):
modules = []
if not isinstance(doctypes, (list, tuple)):
doctypes = [doctypes]
@ -163,17 +168,17 @@ def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profil
make_test_records(doctype, verbose=verbose, force=force)
modules.append(importlib.import_module(test_module))
return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile)
return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
def run_tests_for_module(module, verbose=False, tests=(), profile=False):
def run_tests_for_module(module, verbose=False, tests=(), profile=False, junit_xml_output=False):
module = importlib.import_module(module)
if hasattr(module, "test_dependencies"):
for doctype in module.test_dependencies:
make_test_records(doctype, verbose=verbose)
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile)
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
def _run_unittest(modules, verbose=False, tests=(), profile=False):
def _run_unittest(modules, verbose=False, tests=(), profile=False, junit_xml_output=False):
test_suite = unittest.TestSuite()
if not isinstance(modules, (list, tuple)):
@ -189,13 +194,18 @@ def _run_unittest(modules, verbose=False, tests=(), profile=False):
else:
test_suite.addTest(module_test_cases)
if junit_xml_output:
runner = unittest_runner(verbosity=1+(verbose and 1 or 0))
else:
runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0))
if profile:
pr = cProfile.Profile()
pr.enable()
frappe.flags.tests_verbose = verbose
out = unittest_runner(verbosity=1+(verbose and 1 or 0)).run(test_suite)
out = runner.run(test_suite)
if profile:

View file

@ -256,6 +256,10 @@ app_license = "{app_license}"
# "Task": "{app_name}.task.get_dashboard_data"
# }}
# exempt linked doctypes from being automatically cancelled
#
# auto_cancel_exempted_doctypes = ["Auto Repeat"]
"""
desktop_template = """# -*- coding: utf-8 -*-