diff --git a/.github/frappe-framework-logo.png b/.github/frappe-framework-logo.png
deleted file mode 100644
index 5049078a46..0000000000
Binary files a/.github/frappe-framework-logo.png and /dev/null differ
diff --git a/.github/frappe-framework-logo.svg b/.github/frappe-framework-logo.svg
new file mode 100644
index 0000000000..ba04ebf264
--- /dev/null
+++ b/.github/frappe-framework-logo.svg
@@ -0,0 +1,4 @@
+
diff --git a/README.md b/README.md
index 7545249610..1f59376f48 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,12 @@
-
it's pronounced - fra-pay
diff --git a/frappe/__init__.py b/frappe/__init__.py
index f35409fa48..d644d2a473 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -490,7 +490,8 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
message = content or message
if as_markdown:
- message = frappe.utils.md_to_html(message)
+ from frappe.utils import md_to_html
+ message = md_to_html(message)
if not delayed:
now = True
diff --git a/frappe/auth.py b/frappe/auth.py
index 1353acf10f..ab3624bee8 100644
--- a/frappe/auth.py
+++ b/frappe/auth.py
@@ -333,12 +333,20 @@ class CookieManager:
# sid expires in 3 days
expires = datetime.datetime.now() + datetime.timedelta(days=3)
if frappe.session.sid:
- self.cookies["sid"] = {"value": frappe.session.sid, "expires": expires}
+ self.set_cookie("sid", frappe.session.sid, expires=expires, httponly=True)
if frappe.session.session_country:
- self.cookies["country"] = {"value": frappe.session.get("session_country")}
+ self.set_cookie("country", frappe.session.session_country)
- def set_cookie(self, key, value, expires=None):
- self.cookies[key] = {"value": value, "expires": expires}
+ def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Strict"):
+ if not secure:
+ secure = frappe.local.request.scheme == "https"
+ self.cookies[key] = {
+ "value": value,
+ "expires": expires,
+ "secure": secure,
+ "httponly": httponly,
+ "samesite": samesite
+ }
def delete_cookie(self, to_delete):
if not isinstance(to_delete, (list, tuple)):
@@ -349,7 +357,10 @@ class CookieManager:
def flush_cookies(self, response):
for key, opts in self.cookies.items():
response.set_cookie(key, quote((opts.get("value") or "").encode('utf-8')),
- expires=opts.get("expires"))
+ expires=opts.get("expires"),
+ secure=opts.get("secure"),
+ httponly=opts.get("httponly"),
+ samesite=opts.get("samesite"))
# expires yesterday!
expires = datetime.datetime.now() + datetime.timedelta(days=-1)
diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py
index ca4f86f210..a946fcc81c 100644
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.py
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py
@@ -146,7 +146,7 @@ class AutoRepeat(Document):
def make_new_document(self):
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
- new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
+ new_doc = frappe.copy_doc(reference_doc)
self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)
diff --git a/frappe/boot.py b/frappe/boot.py
index 8862ce3c61..b552d7d703 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -107,7 +107,7 @@ def load_desktop_data(bootinfo):
from frappe.config import get_modules_from_all_apps_for_user
from frappe.desk.desktop import get_desk_sidebar_items
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
- bootinfo.allowed_workspaces = get_desk_sidebar_items(True)
+ bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False)
bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map()
bootinfo.dashboards = frappe.get_all("Dashboard")
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index 92d12289c6..97b6c235b5 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -21,7 +21,7 @@ global_cache_keys = ("app_hooks", "installed_apps",
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",
"desktop_icons", 'portal_menu_items', 'user_perm_can_read',
- "has_role:Page", "has_role:Report")
+ "has_role:Page", "has_role:Report", "desk_sidebar_items")
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
"linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map', 'data_import_column_header_map')
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 0f51f21104..26eb455338 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -201,16 +201,31 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro
def install_app(context, apps):
"Install a new app to site, supports multiple apps"
from frappe.installer import install_app as _install_app
+ exit_code = 0
+
+ if not context.sites:
+ raise SiteNotSpecifiedError
+
for site in context.sites:
frappe.init(site=site)
frappe.connect()
- try:
- for app in apps:
+
+ for app in apps:
+ try:
_install_app(app, verbose=context.verbose)
- finally:
- frappe.destroy()
- if not context.sites:
- raise SiteNotSpecifiedError
+ except frappe.IncompatibleApp as err:
+ err_msg = ":\n{}".format(err) if str(err) else ""
+ print("App {} is Incompatible with Site {}{}".format(app, site, err_msg))
+ exit_code = 1
+ except Exception as err:
+ err_msg = ":\n{}".format(err if str(err) else frappe.get_traceback())
+ print("An error occurred while installing {}{}".format(app, err_msg))
+ exit_code = 1
+
+ frappe.destroy()
+
+ sys.exit(exit_code)
+
@click.command('list-apps')
@pass_context
@@ -422,15 +437,16 @@ def remove_from_installed_apps(context, app):
@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False, multiple=True)
@click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False)
@click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False)
+@click.option('--force', help='Force remove app from site', is_flag=True, default=False)
@pass_context
-def uninstall(context, app, dry_run=False, yes=False, no_backup=False):
+def uninstall(context, app, dry_run, yes, no_backup, force):
"Remove app and linked modules from site"
from frappe.installer import remove_app
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
- remove_app(app, dry_run, yes, no_backup)
+ remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force)
finally:
frappe.destroy()
if not context.sites:
@@ -615,6 +631,29 @@ def stop_recording(context):
if not context.sites:
raise SiteNotSpecifiedError
+@click.command('ngrok')
+@pass_context
+def start_ngrok(context):
+ from pyngrok import ngrok
+
+ site = get_site(context)
+ frappe.init(site=site)
+
+ port = frappe.conf.http_port or frappe.conf.webserver_port
+ public_url = ngrok.connect(port=port, options={
+ 'host_header': site
+ })
+ print(f'Public URL: {public_url}')
+ print('Inspect logs at http://localhost:4040')
+
+ ngrok_process = ngrok.get_ngrok_process()
+ try:
+ # Block until CTRL-C or some other terminating event
+ ngrok_process.proc.wait()
+ except KeyboardInterrupt:
+ print("Shutting down server...")
+ frappe.destroy()
+ ngrok.kill()
commands = [
add_system_manager,
@@ -640,5 +679,6 @@ commands = [
browse,
start_recording,
stop_recording,
- add_to_hosts
+ add_to_hosts,
+ start_ngrok
]
diff --git a/frappe/config/settings.py b/frappe/config/settings.py
index 848ef2e1aa..e43abd9fcb 100644
--- a/frappe/config/settings.py
+++ b/frappe/config/settings.py
@@ -16,6 +16,13 @@ def get_data():
"description": _("Language, Date and Time settings"),
"hide_count": True
},
+ {
+ "type": "doctype",
+ "name": "Global Defaults",
+ "label": _("Global Defaults"),
+ "description": _("Company, Fiscal Year and Currency defaults"),
+ "hide_count": True
+ },
{
"type": "doctype",
"name": "Error Log",
diff --git a/frappe/core/desk_page/settings/settings.json b/frappe/core/desk_page/settings/settings.json
index 6569b2fb20..642a4fdadd 100644
--- a/frappe/core/desk_page/settings/settings.json
+++ b/frappe/core/desk_page/settings/settings.json
@@ -18,7 +18,7 @@
{
"hidden": 0,
"label": "Core",
- "links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]"
+ "links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Company, Fiscal Year and Currency defaults\",\n \"hide_count\": true,\n \"label\": \"Global Defaults\",\n \"name\": \"Global Defaults\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@@ -39,10 +39,11 @@
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
+ "hide_custom": 0,
"idx": 0,
"is_standard": 1,
"label": "Settings",
- "modified": "2020-04-01 11:24:40.636747",
+ "modified": "2020-07-14 10:09:09.520557",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",
diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py
index daf64d4b8b..e9db865ade 100755
--- a/frappe/core/doctype/communication/email.py
+++ b/frappe/core/doctype/communication/email.py
@@ -221,7 +221,7 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
:param print_html: Send given value as HTML attachment.
:param print_format: Attach print format of parent document."""
- view_link = frappe.utils.cint(frappe.db.get_value("Print Settings", "Print Settings", "attach_view_link"))
+ view_link = frappe.utils.cint(frappe.db.get_value("System Settings", "System Settings", "attach_view_link"))
if print_format and view_link:
doc.content += get_attach_link(doc, print_format)
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index ec3cccc1b1..910e42af1a 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -59,6 +59,7 @@ class Importer:
frappe.flags.in_import = True
frappe.flags.mute_emails = self.data_import.mute_emails
+ self.data_import.db_set("status", "Pending")
self.data_import.db_set("template_warnings", "")
def import_data(self):
@@ -440,9 +441,8 @@ class ImportFile:
# if there are child doctypes, find the subsequent rows
if len(doctypes) > 1:
- # subsequent rows either dont have any parent value set
- # or have the same value as the parent row
- # we include a row if either of conditions match
+ # subsequent rows that have blank values in parent columns
+ # are considered as child rows
parent_column_indexes = self.header.get_column_indexes(self.doctype)
parent_row_values = first_row.get_values(parent_column_indexes)
@@ -453,11 +453,8 @@ class ImportFile:
if all([v in INVALID_VALUES for v in row_values]):
rows.append(row)
continue
- # if the row has same values as parent row, it's a child row doc
- if row_values == parent_row_values:
- rows.append(row)
- continue
- # if any of those conditions dont match, it's the next doc
+ # if we encounter a row which has values in parent columns,
+ # then it is the next doc
break
parent_doc = None
@@ -618,7 +615,7 @@ class Row:
def validate_value(self, value, col):
df = col.df
if df.fieldtype == "Select":
- select_options = df.get_select_options()
+ select_options = [d for d in (df.options or '').split('\n') if d]
if select_options and value not in select_options:
options_string = ", ".join([frappe.bold(d) for d in select_options])
msg = _("Value must be one of {0}").format(options_string)
@@ -692,6 +689,9 @@ class Row:
return value
def get_date(self, value, column):
+ if isinstance(value, datetime):
+ return value
+
date_format = column.date_format
if date_format:
try:
@@ -957,7 +957,7 @@ class Column:
if self.df.fieldtype == 'Link':
# find all values that dont exist
- values = list(set([v for v in self.column_values[1:] if v]))
+ values = list(set([cstr(v) for v in self.column_values[1:] if v]))
exists = [d.name for d in frappe.db.get_all(self.df.options, filters={'name': ('in', values)})]
not_exists = list(set(values) - set(exists))
if not_exists:
@@ -970,6 +970,13 @@ class Column:
elif self.df.fieldtype in ("Date", "Time", "Datetime"):
# guess date format
self.date_format = self.guess_date_format_for_column()
+ if not self.date_format:
+ self.date_format = '%Y-%m-%d'
+ self.warnings.append({
+ 'col': self.column_number,
+ 'message': _("Date format could not determined from the values in this column. Defaulting to yyyy-mm-dd."),
+ 'type': 'info'
+ })
def as_dict(self):
d = frappe._dict()
@@ -1060,6 +1067,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
# other fields
fields = get_standard_fields(doctype) + frappe.get_meta(doctype).fields
for df in fields:
+ label = (df.label or '').strip()
fieldtype = df.fieldtype or "Data"
parent = df.parent or parent_doctype
if fieldtype not in no_value_fields:
@@ -1068,12 +1076,12 @@ def build_fields_dict_for_column_matching(parent_doctype):
# Label
# label
# Label (label)
- if not out.get(df.label):
+ if not out.get(label):
# if Label is already set, don't set it again
# in case of duplicate column headers
- out[df.label] = df
+ out[label] = df
out[df.fieldname] = df
- label_with_fieldname = "{0} ({1})".format(df.label, df.fieldname)
+ label_with_fieldname = "{0} ({1})".format(label, df.fieldname)
out[label_with_fieldname] = df
else:
# in case there are multiple table fields with the same doctype
@@ -1084,7 +1092,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
"fields", {"fieldtype": ["in", table_fieldtypes], "options": parent}
)
for table_field in table_fields:
- by_label = "{0} ({1})".format(df.label, table_field.label)
+ by_label = "{0} ({1})".format(label, table_field.label)
by_fieldname = "{0}.{1}".format(table_field.fieldname, df.fieldname)
# create a new df object to avoid mutation problems
diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json
index d9ab504db7..3008e27aa0 100644
--- a/frappe/core/doctype/file/file.json
+++ b/frappe/core/doctype/file/file.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"creation": "2012-12-12 11:19:22",
"doctype": "DocType",
@@ -63,7 +64,8 @@
"fieldname": "is_home_folder",
"fieldtype": "Check",
"hidden": 1,
- "label": "Is Home Folder"
+ "label": "Is Home Folder",
+ "search_index": 1
},
{
"default": "0",
@@ -172,7 +174,8 @@
],
"icon": "fa fa-file",
"idx": 1,
- "modified": "2019-08-30 19:46:20.796453",
+ "links": [],
+ "modified": "2020-06-28 12:21:30.772386",
"modified_by": "Administrator",
"module": "Core",
"name": "File",
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index 831d2ab22d..1748c60020 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -100,26 +100,26 @@ class File(Document):
self.validate_file()
self.generate_content_hash()
- self.validate_url()
-
if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}):
old_file_url = self.file_url
if not self.is_folder and (self.is_private != self.db_get('is_private')):
private_files = frappe.get_site_path('private', 'files')
public_files = frappe.get_site_path('public', 'files')
+ file_name = self.file_url.split('/')[-1]
if not self.is_private:
- shutil.move(os.path.join(private_files, self.file_name),
- os.path.join(public_files, self.file_name))
+ shutil.move(os.path.join(private_files, file_name),
+ os.path.join(public_files, file_name))
- self.file_url = "/files/{0}".format(self.file_name)
+ self.file_url = "/files/{0}".format(file_name)
else:
- shutil.move(os.path.join(public_files, self.file_name),
- os.path.join(private_files, self.file_name))
+ shutil.move(os.path.join(public_files, file_name),
+ os.path.join(private_files, file_name))
- self.file_url = "/private/files/{0}".format(self.file_name)
+ self.file_url = "/private/files/{0}".format(file_name)
+ update_existing_file_docs(self)
# update documents image url with new file url
if self.attached_to_doctype and self.attached_to_name:
@@ -135,6 +135,8 @@ class File(Document):
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
self.attached_to_field, self.file_url)
+ self.validate_url()
+
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
@@ -182,13 +184,7 @@ class File(Document):
if duplicate_file:
duplicate_file_doc = frappe.get_cached_doc('File', duplicate_file.name)
if duplicate_file_doc.exists_on_disk():
- # if it is attached to a document then throw FileAlreadyAttachedException
- if self.attached_to_doctype and self.attached_to_name:
- self.duplicate_entry = duplicate_file.name
- frappe.throw(_("Same file has already been attached to the record"),
- frappe.FileAlreadyAttachedException)
- # else just use the url, to avoid uploading a duplicate
- else:
+ # just use the url, to avoid uploading a duplicate
self.file_url = duplicate_file.file_url
def set_file_name(self):
@@ -909,3 +905,20 @@ def get_files_in_folder(folder):
{ 'folder': folder },
['name', 'file_name', 'file_url', 'is_folder', 'modified']
)
+
+def update_existing_file_docs(doc):
+ # Update is private and file url of all file docs that point to the same file
+ frappe.db.sql("""
+ UPDATE `tabFile`
+ SET
+ file_url = %(file_url)s,
+ is_private = %(is_private)s
+ WHERE
+ content_hash = %(content_hash)s
+ and name != %(file_name)s
+ """, dict(
+ file_url=doc.file_url,
+ is_private=doc.is_private,
+ content_hash=doc.content_hash,
+ file_name=doc.name
+ ))
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index cc9628ed5b..ec4f97bf67 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -294,4 +294,37 @@ class TestFile(unittest.TestCase):
folder = frappe.get_doc("File", "Home/Test Folder 1/Test Folder 3")
self.assertRaises(frappe.ValidationError, folder.delete)
+ def test_same_file_url_update(self):
+ attached_to_doctype1, attached_to_docname1 = make_test_doc()
+ attached_to_doctype2, attached_to_docname2 = make_test_doc()
+
+ file1 = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'file1.txt',
+ "attached_to_doctype": attached_to_doctype1,
+ "attached_to_name": attached_to_docname1,
+ "is_private": 1,
+ "content": test_content1}).insert()
+
+ file2 = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'file2.txt',
+ "attached_to_doctype": attached_to_doctype2,
+ "attached_to_name": attached_to_docname2,
+ "is_private": 1,
+ "content": test_content1}).insert()
+
+ self.assertEqual(file1.is_private, file2.is_private, 1)
+ self.assertEqual(file1.file_url, file2.file_url)
+ self.assertTrue(os.path.exists(file1.get_full_path()))
+
+ file1.is_private = 0
+ file1.save()
+
+ file2 = frappe.get_doc('File', file2.name)
+
+ self.assertEqual(file1.is_private, file2.is_private, 0)
+ self.assertEqual(file1.file_url, file2.file_url)
+ self.assertTrue(os.path.exists(file2.get_full_path()))
+
diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py
index 0c0e7c4f45..755cb86dbe 100644
--- a/frappe/core/doctype/module_def/module_def.py
+++ b/frappe/core/doctype/module_def/module_def.py
@@ -42,6 +42,10 @@ class ModuleDef(Document):
def on_trash(self):
"""Delete module name from modules.txt"""
+
+ if frappe.flags.in_uninstall:
+ return
+
modules = None
if frappe.local.module_app.get(frappe.scrub(self.name)):
with open(frappe.get_app_path(self.app_name, "modules.txt"), "r") as f:
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 1d0cda95a4..b2cb67dbc9 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -59,6 +59,7 @@
"column_break_18",
"disable_standard_email_footer",
"hide_footer_in_auto_email_reports",
+ "attach_view_link",
"chat",
"enable_chat",
"use_socketio_to_upload_file"
@@ -422,12 +423,18 @@
"fieldname": "enable_onboarding",
"fieldtype": "Check",
"label": "Enable Onboarding"
+ },
+ {
+ "default": "1",
+ "fieldname": "attach_view_link",
+ "fieldtype": "Check",
+ "label": "Send document Web View link in email"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2020-05-01 19:21:15.496065",
+ "modified": "2020-07-02 16:13:00.166382",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index fc58f66bfc..64bff32189 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals, print_function
import frappe
from frappe.model.document import Document
-from frappe.utils import cint, flt, has_gravatar, format_datetime, now_datetime, get_formatted_email, today
+from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password
from frappe.desk.notifications import clear_notifications
@@ -770,7 +770,7 @@ def sign_up(email, full_name, redirect_to):
user = frappe.get_doc({
"doctype":"User",
"email": email,
- "first_name": full_name,
+ "first_name": escape_html(full_name),
"enabled": 1,
"new_password": random_string(10),
"user_type": "Website User"
diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py
index 80236b2dc2..3345fce735 100644
--- a/frappe/database/db_manager.py
+++ b/frappe/database/db_manager.py
@@ -49,7 +49,7 @@ class DbManager:
host = self.get_current_host()
if frappe.conf.get('rds_db', 0) == 1:
- self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON `%s`.* TO '%s'@'%s';" % (target, user, host))
+ self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, LOCK TABLES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
else:
self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index e806e8e415..4bbecd2a2e 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -82,5 +82,7 @@ class MariaDBTable(DBTable):
fieldname = str(e).split("'")[-2]
frappe.throw(_("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format(
fieldname, self.table_name))
+ elif e.args[0]==1067:
+ frappe.throw(str(e.args[1]))
else:
raise e
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 142c103c68..68b57a93d4 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -29,31 +29,56 @@ def handle_not_exist(fn):
class Workspace:
- def __init__(self, page_name):
+ def __init__(self, page_name, minimal=False):
self.page_name = page_name
self.extended_cards = []
self.extended_charts = []
self.extended_shortcuts = []
self.user = frappe.get_user()
- self.allowed_modules = self.get_cached_value('user_allowed_modules', self.get_allowed_modules)
+ self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules)
+
self.doc = self.get_page_for_user()
if self.doc.module not in self.allowed_modules:
raise frappe.PermissionError
- self.can_read = self.get_cached_value('user_perm_can_read', self.get_can_read_items)
+ self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items)
self.allowed_pages = get_allowed_pages(cache=True)
self.allowed_reports = get_allowed_reports(cache=True)
- self.onboarding_doc = self.get_onboarding_doc()
- self.onboarding = None
-
- self.table_counts = get_table_with_counts()
+
+ if not minimal:
+ self.onboarding_doc = self.get_onboarding_doc()
+ self.onboarding = None
+
+ self.table_counts = get_table_with_counts()
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
- def get_cached_value(self, cache_key, fallback_fn):
+ def is_page_allowed(self):
+ cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module) + self.extended_cards
+ shortcuts = self.doc.shortcuts + self.extended_shortcuts
+
+ for section in cards:
+ links = loads(section.links) if isinstance(section.links, string_types) else section.links
+ for item in links:
+ if self.is_item_allowed(item.get('name'), item.get('type')):
+ return True
+
+ def _in_active_domains(item):
+ if not item.restrict_to_domain:
+ return True
+ else:
+ return item.restrict_to_domain in frappe.get_active_domains()
+
+ for item in shortcuts:
+ if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
+ return True
+
+ return False
+
+ def get_cached(self, cache_key, fallback_fn):
_cache = frappe.cache()
value = _cache.get_value(cache_key, user=frappe.session.user)
@@ -83,12 +108,12 @@ class Workspace:
'extends': self.page_name,
'for_user': frappe.session.user
}
- pages = frappe.get_list("Desk Page", filters=filters)
+ pages = frappe.get_all("Desk Page", filters=filters, limit=1)
if pages:
- return frappe.get_doc("Desk Page", pages[0])
+ return frappe.get_cached_doc("Desk Page", pages[0])
self.get_pages_to_extend()
- return frappe.get_doc("Desk Page", self.page_name)
+ return frappe.get_cached_doc("Desk Page", self.page_name)
def get_onboarding_doc(self):
# Check if onboarding is enabled
@@ -123,7 +148,7 @@ class Workspace:
'module': ['in', self.allowed_modules]
})
- pages = [frappe.get_doc("Desk Page", page['name']) for page in pages]
+ pages = [frappe.get_cached_doc("Desk Page", page['name']) for page in pages]
for page in pages:
self.extended_cards = self.extended_cards + page.cards
@@ -170,6 +195,7 @@ class Workspace:
'docs_url': self.onboarding_doc.documentation_url,
'items': self.get_onboarding_steps()
}
+
@handle_not_exist
def get_cards(self):
cards = self.doc.cards
@@ -323,25 +349,44 @@ def get_desktop_page(page):
}
@frappe.whitelist()
-def get_desk_sidebar_items(flatten=False):
+def get_desk_sidebar_items(flatten=False, cache=True):
"""Get list of sidebar items for desk
"""
- # don't get domain restricted pages
- blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
+ pages = []
+ _cache = frappe.cache()
+ if cache:
+ pages = _cache.get_value("desk_sidebar_items", user=frappe.session.user)
+
+ if not pages or not cache:
+ # don't get domain restricted pages
+ blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
- filters = {
- 'restrict_to_domain': ['in', frappe.get_active_domains()],
- 'extends_another_page': 0,
- 'for_user': '',
- 'module': ['not in', blocked_modules]
- }
+ filters = {
+ 'restrict_to_domain': ['in', frappe.get_active_domains()],
+ 'extends_another_page': 0,
+ 'for_user': '',
+ 'module': ['not in', blocked_modules]
+ }
- if not frappe.local.conf.developer_mode:
- filters['developer_mode_only'] = '0'
+ if not frappe.local.conf.developer_mode:
+ filters['developer_mode_only'] = '0'
+
+ # pages sorted based on pinned to top and then by name
+ order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
+ all_pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True)
+ pages = []
+
+ # Filter Page based on Permission
+ for page in all_pages:
+ try:
+ wspace = Workspace(page.get('name'), True)
+ if wspace.is_page_allowed():
+ pages.append(page)
+ except frappe.PermissionError:
+ pass
+
+ _cache.set_value("desk_sidebar_items", pages, frappe.session.user)
- # pages sorted based on pinned to top and then by name
- order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
- pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True)
if flatten:
return pages
@@ -375,7 +420,7 @@ def get_custom_reports_and_doctypes(module):
]
def get_custom_doctype_list(module):
- doctypes = frappe.get_list("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name", ignore_permissions=True)
+ doctypes = frappe.get_all("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name")
out = []
for d in doctypes:
@@ -390,9 +435,9 @@ def get_custom_doctype_list(module):
def get_custom_report_list(module):
"""Returns list on new style reports for modules."""
- reports = frappe.get_list("Report", fields=["name", "ref_doctype", "report_type"], filters=
+ reports = frappe.get_all("Report", fields=["name", "ref_doctype", "report_type"], filters=
{"is_standard": "No", "disabled": 0, "module": module},
- order_by="name", ignore_permissions=True)
+ order_by="name")
out = []
for r in reports:
diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js
index 609e943995..9be6b55d53 100644
--- a/frappe/desk/doctype/dashboard/dashboard.js
+++ b/frappe/desk/doctype/dashboard/dashboard.js
@@ -5,10 +5,15 @@ frappe.ui.form.on('Dashboard', {
refresh: function(frm) {
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
+ if (!frappe.boot.developer_mode) {
+ frm.disable_form();
+ }
+
frm.set_query("chart", "charts", function() {
return {
filters: {
- is_public: 1
+ is_public: 1,
+ is_standard: 1,
}
};
});
@@ -16,7 +21,8 @@ frappe.ui.form.on('Dashboard', {
frm.set_query("card", "cards", function() {
return {
filters: {
- is_public: 1
+ is_public: 1,
+ is_standard: 1,
}
};
});
diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json
index c0e2bddcf8..c7128823fe 100644
--- a/frappe/desk/doctype/dashboard/dashboard.json
+++ b/frappe/desk/doctype/dashboard/dashboard.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_rename": 1,
"autoname": "field:dashboard_name",
"creation": "2019-01-10 12:54:40.938705",
"doctype": "DocType",
@@ -8,6 +9,8 @@
"field_order": [
"dashboard_name",
"is_default",
+ "is_standard",
+ "module",
"charts",
"chart_options",
"cards"
@@ -35,21 +38,35 @@
"reqd": 1
},
{
- "description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
- "fieldname": "chart_options",
- "fieldtype": "Code",
- "label": "Chart Options",
- "options": "JSON"
+ "description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
+ "fieldname": "chart_options",
+ "fieldtype": "Code",
+ "label": "Chart Options",
+ "options": "JSON"
},
{
"fieldname": "cards",
"fieldtype": "Table",
"label": "Cards",
"options": "Number Card Link"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard"
+ },
+ {
+ "depends_on": "eval: doc.is_standard",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module",
+ "mandatory_depends_on": "eval: doc.is_standard",
+ "options": "Module Def"
}
],
"links": [],
- "modified": "2020-04-29 13:26:37.362482",
+ "modified": "2020-07-10 17:48:19.468813",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index af0c48d9c6..b12bcfe27d 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
from frappe.model.document import Document
+from frappe.modules.export_file import export_to_files
import frappe
from frappe import _
import json
@@ -15,7 +16,23 @@ class Dashboard(Document):
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
+ if frappe.conf.developer_mode and self.is_standard:
+ export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module)
+
def validate(self):
+ if not frappe.conf.developer_mode and self.is_standard:
+ frappe.throw('Cannot edit Standard Dashboards')
+
+ if self.is_standard:
+ non_standard_docs_map = {
+ 'Dashboard Chart': get_non_standard_charts_in_dashboard(self),
+ 'Number Card': get_non_standard_cards_in_dashboard(self)
+ }
+
+ if non_standard_docs_map['Dashboard Chart'] or non_standard_docs_map['Number Card']:
+ message = get_non_standard_warning_message(non_standard_docs_map)
+ frappe.throw(message, title=_("Standard Not Set"), is_minimizable=True)
+
self.validate_custom_options()
def validate_custom_options(self):
@@ -48,3 +65,29 @@ def get_permitted_cards(dashboard_name):
if frappe.has_permission('Number Card', doc=card.card):
permitted_cards.append(card)
return permitted_cards
+
+def get_non_standard_charts_in_dashboard(dashboard):
+ non_standard_charts = [doc.name for doc in frappe.get_list('Dashboard Chart', {'is_standard': 0})]
+ return [chart_link.chart for chart_link in dashboard.charts if chart_link.chart in non_standard_charts]
+
+def get_non_standard_cards_in_dashboard(dashboard):
+ non_standard_cards = [doc.name for doc in frappe.get_list('Number Card', {'is_standard': 0})]
+ return [card_link.card for card_link in dashboard.cards if card_link.card in non_standard_cards]
+
+def get_non_standard_warning_message(non_standard_docs_map):
+ message = _('''Please set the following documents in this Dashboard as standard first.''')
+
+ def get_html(docs, doctype):
+ html = '
{}
'.format(frappe.bold(doctype))
+ for doc in docs:
+ html += '
'.format(doctype=doctype, doc=doc)
+ html += '
'
+ return html
+
+ html = message + '
'
+
+ for doctype in non_standard_docs_map:
+ if non_standard_docs_map[doctype]:
+ html += get_html(non_standard_docs_map[doctype], doctype)
+
+ return html
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index a10d3d96f2..6f071a6e2b 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -9,9 +9,24 @@ frappe.ui.form.on('Dashboard Chart', {
frm.add_fetch('source', 'timeseries', 'timeseries');
},
+ before_save: function(frm) {
+ let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null');
+ let static_filters = JSON.parse(frm.doc.filters_json || 'null');
+ static_filters =
+ frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters);
+
+ frm.set_value('filters_json', JSON.stringify(static_filters));
+ frm.trigger('show_filters');
+ },
refresh: function(frm) {
frm.chart_filters = null;
+
+ if (!frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.set_df_property('chart_options_section', 'hidden', 1);
+ frm.disable_form();
+ }
+
frm.add_custom_button('Add Chart to Dashboard', () => {
const d = new frappe.ui.Dialog({
title: __('Add to Dashboard'),
@@ -49,6 +64,8 @@ frappe.ui.form.on('Dashboard Chart', {
});
frm.set_df_property("filters_section", "hidden", 1);
+ frm.set_df_property("dynamic_filters_section", "hidden", 1);
+
frm.trigger('set_time_series');
frm.set_query('document_type', function() {
return {
@@ -66,6 +83,15 @@ frappe.ui.form.on('Dashboard Chart', {
if (!frappe.boot.developer_mode) {
frm.set_df_property("custom_options", "hidden", 1);
}
+
+ },
+
+ is_standard: function(frm) {
+ if (frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.trigger('render_dynamic_filters_table');
+ } else {
+ frm.set_df_property("dynamic_filters_section", "hidden", 1);
+ }
},
source: function(frm) {
@@ -111,6 +137,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_value('based_on', '');
frm.set_value('value_based_on', '');
frm.set_value('filters_json', '[]');
+ frm.set_value('dynamic_filters_json', '[]');
frm.trigger('update_options');
},
@@ -119,6 +146,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_value('y_axis', []);
frm.set_df_property('x_field', 'options', []);
frm.set_value('filters_json', '{}');
+ frm.set_value('dynamic_filters_json', '{}');
frm.trigger('set_chart_report_filters');
},
@@ -146,7 +174,7 @@ frappe.ui.form.on('Dashboard Chart', {
},
set_chart_field_options: function(frm) {
- let filters = frm.doc.filters_json.length > 2? JSON.parse(frm.doc.filters_json): null;
+ let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
frappe.xcall(
'frappe.desk.query_report.run',
{
@@ -240,11 +268,14 @@ frappe.ui.form.on('Dashboard Chart', {
show_filters: function(frm) {
frm.chart_filters = [];
frappe.dashboard_utils.get_filters_for_chart_type(frm.doc).then(filters => {
- if (filters) {
- frm.chart_filters = filters;
- }
+ if (filters) {
+ frm.chart_filters = filters;
+ }
+ frm.trigger('render_filters_table');
- frm.trigger('render_filters_table');
+ if (frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.trigger('render_dynamic_filters_table');
+ }
});
},
@@ -257,8 +288,8 @@ frappe.ui.form.on('Dashboard Chart', {
let table = $(`
- | ${__('Filter')} |
- ${__('Condition')} |
+ ${__('Filter')} |
+ ${__('Condition')} |
${__('Value')} |
@@ -378,4 +409,141 @@ frappe.ui.form.on('Dashboard Chart', {
});
},
+ render_dynamic_filters_table(frm) {
+ frm.set_df_property("dynamic_filters_section", "hidden", 0);
+
+ let is_document_type = frm.doc.chart_type !== 'Report'
+ && frm.doc.chart_type !== 'Custom';
+
+ let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty();
+
+ frm.dynamic_filter_table = $(`
+
+
+ | ${__('Filter')} |
+ ${__('Condition')} |
+ ${__('Value')} |
+
+
+
+
`).appendTo(wrapper);
+
+ frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
+
+ frm.trigger('set_dynamic_filters_in_table');
+
+ let filters = JSON.parse(frm.doc.filters_json || '[]');
+ let fields = [
+ {
+ fieldtype: 'HTML',
+ fieldname: 'description',
+ options:
+ `
+
Set dynamic filter values in JavaScript for the required fields here.
+
+
Ex:
+ frappe.defaults.get_user_default("Company")
+
+
`
+ }
+ ];
+
+ if (is_document_type) {
+ if (frm.dynamic_filters) {
+ filters = [...filters, ...frm.dynamic_filters];
+ }
+ filters.forEach(f => {
+ for (let field of fields) {
+ if (field.fieldname == f[0] + ':' + f[1]) {
+ return;
+ }
+ }
+ if (f[2] == '=') {
+ fields.push({
+ label: `${f[1]} (${f[0]})`,
+ fieldname: f[0] + ':' + f[1],
+ fieldtype: 'Data',
+ });
+ }
+ });
+ } else {
+ filters = {...frm.dynamic_filters, ...filters};
+ for (let key of Object.keys(filters)) {
+ fields.push({
+ label: key,
+ fieldname: key,
+ fieldtype: 'Data',
+ });
+ }
+ }
+
+ frm.dynamic_filter_table.on('click', () => {
+ let dialog = new frappe.ui.Dialog({
+ title: __('Set Dynamic Filters'),
+ fields: fields,
+ primary_action: () => {
+ let values = dialog.get_values();
+ dialog.hide();
+ let dynamic_filters = [];
+ for (let key of Object.keys(values)) {
+ if (is_document_type) {
+ let [doctype, fieldname] = key.split(':');
+ dynamic_filters.push([doctype, fieldname, '=', values[key]]);
+ }
+ }
+
+ if (is_document_type) {
+ frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters));
+ } else {
+ frm.set_value('dynamic_filters_json', JSON.stringify(values));
+ }
+ frm.trigger('set_dynamic_filters_in_table');
+ },
+ primary_action_label: "Set"
+ });
+
+ dialog.show();
+ dialog.set_values(frm.dynamic_filters);
+ });
+ },
+
+ set_dynamic_filters_in_table: function(frm) {
+ frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
+
+ if (!frm.dynamic_filters) {
+ const filter_row = $(`|
+ ${__("Click to Set Dynamic Filters")} |
`);
+ frm.dynamic_filter_table.find('tbody').html(filter_row);
+ } else {
+ let filter_rows = '';
+ if ($.isArray(frm.dynamic_filters)) {
+ frm.dynamic_filters.forEach(filter => {
+ filter_rows +=
+ `
+ | ${filter[1]} |
+ ${filter[2] || ""} |
+ ${filter[3]} |
+
`;
+ });
+ } else {
+ let condition = '=';
+ for (let [key, val] of Object.entries(frm.dynamic_filters)) {
+ filter_rows +=
+ `
+ | ${key} |
+ ${condition} |
+ ${val || ""} |
+
`
+ ;
+ }
+ }
+
+ frm.dynamic_filter_table.find('tbody').html(filter_rows);
+ }
+ }
+
});
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index 4bab76337f..d67e725eb9 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -7,6 +7,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "is_standard",
+ "module",
"chart_name",
"chart_type",
"report_name",
@@ -32,10 +34,12 @@
"type",
"filters_section",
"filters_json",
+ "dynamic_filters_section",
+ "dynamic_filters_json",
"chart_options_section",
- "color",
- "column_break_2",
"custom_options",
+ "column_break_2",
+ "color",
"section_break_10",
"last_synced_on"
],
@@ -67,7 +71,8 @@
"fieldname": "document_type",
"fieldtype": "Link",
"label": "Document Type",
- "options": "DocType"
+ "options": "DocType",
+ "set_only_once": 1
},
{
"depends_on": "eval: doc.timeseries && ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
@@ -200,7 +205,8 @@
"fieldname": "report_name",
"fieldtype": "Link",
"label": "Report Name",
- "options": "Report"
+ "options": "Report",
+ "set_only_once": 1
},
{
"default": "0",
@@ -235,10 +241,43 @@
"fieldname": "heatmap_year",
"fieldtype": "Select",
"label": "Year"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "depends_on": "eval: doc.is_standard",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module",
+ "mandatory_depends_on": "eval: doc.is_standard",
+ "options": "Module Def",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "dynamic_filters_json",
+ "fieldtype": "Code",
+ "label": "Dynamic Filters JSON",
+ "options": "JSON",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "dynamic_filters_section",
+ "fieldtype": "Section Break",
+ "label": "Dynamic Filters",
+ "show_days": 1,
+ "show_seconds": 1
}
],
"links": [],
- "modified": "2020-05-16 15:03:02.455395",
+ "modified": "2020-07-10 16:09:47.102062",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 4ad6943e0b..70aece3ee7 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -13,6 +13,7 @@ from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
from frappe.model.document import Document
+from frappe.modules.export_file import export_to_files
def get_permission_query_conditions(user):
@@ -80,7 +81,9 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
to_date = get_datetime(chart.to_date)
timegrain = time_interval or chart.time_interval
- filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
+ filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
+ if not filters:
+ filters = []
# don't include cancelled documents
filters.append([chart.document_type, 'docstatus', '<', 2, False])
@@ -347,8 +350,13 @@ class DashboardChart(Document):
def on_update(self):
frappe.cache().delete_key('chart-data:{}'.format(self.name))
+ if frappe.conf.developer_mode and self.is_standard:
+ export_to_files(record_list=[['Dashboard Chart', self.name]], record_module=self.module)
+
def validate(self):
+ if not frappe.conf.developer_mode and self.is_standard:
+ frappe.throw('Cannot edit Standard charts')
if self.chart_type != 'Custom' and self.chart_type != 'Report':
self.check_required_field()
self.check_document_type()
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 12f2c41274..c4c6077e85 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -69,7 +69,6 @@ def make_notification_logs(doc, users):
_doc = frappe.new_doc('Notification Log')
_doc.update(doc)
_doc.for_user = user
- _doc.subject = _doc.subject.replace('', '').replace('
', '')
if _doc.for_user != _doc.from_user or doc.type == 'Energy Point' or doc.type == 'Alert':
_doc.insert(ignore_permissions=True)
diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js
index 184fe5e6cb..d7abe57e2a 100644
--- a/frappe/desk/doctype/number_card/number_card.js
+++ b/frappe/desk/doctype/number_card/number_card.js
@@ -3,9 +3,32 @@
frappe.ui.form.on('Number Card', {
refresh: function(frm) {
+ if (!frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.disable_form();
+ }
frm.set_df_property("filters_section", "hidden", 1);
+ frm.set_df_property("dynamic_filters_section", "hidden", 1);
frm.trigger('set_options');
frm.trigger('render_filters_table');
+ frm.trigger('render_dynamic_filters_table');
+ },
+
+ before_save: function(frm) {
+ let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null');
+ let static_filters = JSON.parse(frm.doc.filters_json || 'null');
+ static_filters =
+ frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters);
+
+ frm.set_value('filters_json', JSON.stringify(static_filters));
+ frm.trigger('render_filters_table');
+ },
+
+ is_standard: function(frm) {
+ if (frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.trigger('render_dynamic_filters_table');
+ } else {
+ frm.set_df_property("dynamic_filters_section", "hidden", 1);
+ }
},
document_type: function(frm) {
@@ -17,6 +40,7 @@ frappe.ui.form.on('Number Card', {
};
});
frm.set_value('filters_json', '[]');
+ frm.set_value('dynamic_filters_json', '[]');
frm.set_value('aggregate_function_based_on', '');
frm.trigger('set_options');
},
@@ -47,7 +71,7 @@ frappe.ui.form.on('Number Card', {
frm.set_df_property("filters_section", "hidden", 0);
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
- frm.filter_table = $(`
+ let table = $(`
| ${__('Filter')} |
@@ -60,9 +84,9 @@ frappe.ui.form.on('Number Card', {
frm.filters = JSON.parse(frm.doc.filters_json || '[]');
- frm.trigger('set_filters_in_table');
+ set_filters_in_table(frm.filters, table);
- frm.filter_table.on('click', () => {
+ table.on('click', () => {
let dialog = new frappe.ui.Dialog({
title: __('Set Filters'),
fields: [{
@@ -75,7 +99,8 @@ frappe.ui.form.on('Number Card', {
this.hide();
frm.filters = frm.filter_group.get_filters();
frm.set_value('filters_json', JSON.stringify(frm.filters));
- frm.trigger('set_filters_in_table');
+ set_filters_in_table(frm.filters, table);
+ frm.trigger('render_dynamic_filters_table');
}
},
primary_action_label: "Set"
@@ -97,23 +122,110 @@ frappe.ui.form.on('Number Card', {
},
- set_filters_in_table: function(frm) {
- if (!frm.filters.length) {
- const filter_row = $(`
|
- ${__("Click to Set Filters")} |
`);
- frm.filter_table.find('tbody').html(filter_row);
- } else {
- let filter_rows = '';
- frm.filters.forEach(filter => {
- filter_rows +=
- `
- | ${filter[1]} |
- ${filter[2] || ""} |
- ${filter[3]} |
-
`;
-
- });
- frm.filter_table.find('tbody').html(filter_rows);
+ render_dynamic_filters_table: function(frm) {
+ if (!frappe.boot.developer_mode || !frm.doc.is_standard) {
+ return;
}
- }
+ let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty();
+
+ frm.set_df_property("dynamic_filters_section", "hidden", 0);
+
+ let table = $(`
+
+
+ | ${__('Filter')} |
+ ${__('Condition')} |
+ ${__('Value')} |
+
+
+
+
`).appendTo(wrapper);
+
+ frm.dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || '[]');
+
+ set_filters_in_table(frm.dynamic_filters, table);
+
+ let filters = JSON.parse(frm.doc.filters_json || '[]');
+ let fields = [
+ {
+ fieldtype: 'HTML',
+ fieldname: 'description',
+ options:
+ `
+
Set dynamic filter values in JavaScript for the required fields here.
+
+
Ex:
+ frappe.defaults.get_user_default("Company")
+
+
`
+ }
+ ];
+
+ if (frm.dynamic_filters.length) {
+ filters = [...filters, ...frm.dynamic_filters];
+ }
+
+ filters.forEach(f => {
+ for (let field of fields) {
+ if (field.fieldname == f[0] + ':' + f[1]) {
+ return;
+ }
+ }
+ if (f[2] == '=') {
+ fields.push({
+ label: `${f[1]} (${f[0]})`,
+ fieldname: f[0] + ':' + f[1],
+ fieldtype: 'Data',
+ });
+ }
+ });
+
+ table.on('click', () => {
+ let dialog = new frappe.ui.Dialog({
+ title: __('Set Filters'),
+ fields: fields,
+ primary_action: () => {
+ let values = dialog.get_values();
+ if (values) {
+ dialog.hide();
+ let dynamic_filters = [];
+ for (let key of Object.keys(values)) {
+ let [doctype, fieldname] = key.split(':');
+ dynamic_filters.push([doctype, fieldname, '=', values[key]]);
+ }
+
+ frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters));
+ frm.dynamic_filters = dynamic_filters;
+ set_filters_in_table(frm.dynamic_filters, table);
+ }
+ },
+ primary_action_label: "Set"
+ });
+
+ dialog.show();
+ dialog.set_values(filters);
+ });
+
+ },
+
});
+
+function set_filters_in_table(filters, table) {
+ if (!filters.length) {
+ const filter_row = $(`|
+ ${__("Click to Set Filters")} |
`);
+ table.find('tbody').html(filter_row);
+ } else {
+ let filter_rows = '';
+ filters.forEach(filter => {
+ filter_rows +=
+ `
+ | ${filter[1]} |
+ ${filter[2] || ""} |
+ ${filter[3]} |
+
`;
+
+ });
+ table.find('tbody').html(filter_rows);
+ }
+}
diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json
index ec6a1e9190..41362a8982 100644
--- a/frappe/desk/doctype/number_card/number_card.json
+++ b/frappe/desk/doctype/number_card/number_card.json
@@ -1,10 +1,13 @@
{
"actions": [],
+ "allow_rename": 1,
"creation": "2020-04-15 18:06:39.444683",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "is_standard",
+ "module",
"label",
"function",
"aggregate_function_based_on",
@@ -16,6 +19,9 @@
"stats_time_interval",
"filters_section",
"filters_json",
+ "dynamic_filters_section",
+ "dynamic_filters_json",
+ "section_break_16",
"color"
],
"fields": [
@@ -95,10 +101,47 @@
"fieldname": "stats_section",
"fieldtype": "Section Break",
"label": "Stats"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard"
+ },
+ {
+ "depends_on": "eval: doc.is_standard",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module",
+ "mandatory_depends_on": "eval: doc.is_standard",
+ "options": "Module Def",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "dynamic_filters_json",
+ "fieldtype": "Code",
+ "label": "Dynamic Filters JSON",
+ "options": "JSON",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "dynamic_filters_section",
+ "fieldtype": "Section Break",
+ "label": "Dynamic Filters Section",
+ "show_days": 1,
+ "show_seconds": 1
}
],
"links": [],
- "modified": "2020-05-06 19:47:57.753574",
+ "modified": "2020-07-10 17:55:35.873222",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index c4a427c4e0..2f5cfb561c 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils import cint
from frappe.model.naming import append_number_if_name_exists
+from frappe.modules.export_file import export_to_files
class NumberCard(Document):
def autoname(self):
@@ -16,6 +17,10 @@ class NumberCard(Document):
if frappe.db.exists("Number Card", self.name):
self.name = append_number_if_name_exists('Number Card', self.name)
+ def on_update(self):
+ if frappe.conf.developer_mode and self.is_standard:
+ export_to_files(record_list=[['Number Card', self.name]], record_module=self.module)
+
def get_permission_query_conditions(user=None):
if not user:
user = frappe.session.user
@@ -67,6 +72,9 @@ def get_result(doc, to_date=None):
filters = frappe.parse_json(doc.filters_json)
+ if not filters:
+ filters = []
+
if to_date:
filters.append([doc.document_type, 'creation', '<', to_date, False])
diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py
index 694b44b907..cae1bf5c77 100644
--- a/frappe/desk/form/save.py
+++ b/frappe/desk/form/save.py
@@ -18,12 +18,7 @@ def savedocs(doc, action):
if doc.docstatus==1:
doc.submit()
else:
- try:
- doc.save()
- except frappe.NameError as e:
- doctype, name, original_exception = e if isinstance(e, tuple) else (doc.doctype or "", doc.name or "", None)
- frappe.msgprint(frappe._("{0} {1} already exists").format(doctype, name))
- raise
+ doc.save()
# update recent documents
run_onload(doc)
diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py
index 1ebf32febe..e5654c853f 100644
--- a/frappe/desk/leaderboard.py
+++ b/frappe/desk/leaderboard.py
@@ -14,13 +14,16 @@ def get_leaderboards():
return leaderboards
@frappe.whitelist()
-def get_energy_point_leaderboard(from_date, company = None, field = None, limit = None):
+def get_energy_point_leaderboard(date_range, company = None, field = None, limit = None):
+ filters = [
+ ['type', '!=', 'Review'],
+ ]
+ if date_range:
+ date_range = frappe.parse_json(date_range)
+ filters.append(['creation', 'between', [date_range[0], date_range[1]]])
energy_point_users = frappe.db.get_all('Energy Point Log',
fields = ['user as name', 'sum(points) as value'],
- filters = [
- ['type', '!=', 'Review'],
- ['creation', '>', from_date]
- ],
+ filters = filters,
group_by = 'user',
order_by = 'value desc'
)
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index 4472a2978a..189949ac68 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -49,7 +49,7 @@ class Leaderboard {
this.timespans = [
"This Week", "This Month", "This Quarter", "This Year",
"Last Week", "Last Month", "Last Quarter", "Last Year",
- "All Time", "Select From Date"
+ "All Time", "Select Date Range"
];
// for saving current selected filters
@@ -113,7 +113,7 @@ class Leaderboard {
return {"label": __(d), value: d };
})
);
- this.create_from_date_field();
+ this.create_date_range_field();
this.type_select = this.page.add_select(__("Field"),
this.options.selected_filter.map(d => {
@@ -123,12 +123,12 @@ class Leaderboard {
this.timespan_select.on("change", (e) => {
this.options.selected_timespan = e.currentTarget.value;
- if (this.options.selected_timespan === 'Select From Date') {
- this.from_date_field.show();
+ if (this.options.selected_timespan === 'Select Date Range') {
+ this.date_range_field.show();
} else {
- this.from_date_field.hide();
- this.make_request();
+ this.date_range_field.hide();
}
+ this.make_request();
});
this.type_select.on("change", (e) => {
@@ -137,21 +137,21 @@ class Leaderboard {
});
}
- create_from_date_field() {
+ create_date_range_field() {
let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`);
- this.from_date_field = $(``).insertAfter(timespan_field).hide();
+ this.date_range_field = $(``).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
df: {
- fieldtype: 'Date',
- fieldname: 'selected_from_date',
- placeholder: frappe.datetime.month_start(),
- default: frappe.datetime.month_start(),
+ fieldtype: 'DateRange',
+ fieldname: 'selected_date_range',
+ placeholder: "Date Range",
+ default: [frappe.datetime.month_start(), frappe.datetime.now_date()],
input_class: 'input-sm',
reqd: 1,
change: () => {
- this.selected_from_date = date_field.get_value();
- if (this.selected_from_date) this.make_request();
+ this.selected_date_range = date_field.get_value();
+ if (this.selected_date_range) this.make_request();
}
},
parent: $(this.parent).find('.from-date-field'),
@@ -225,7 +225,7 @@ class Leaderboard {
frappe.call(
this.leaderboard_config[this.options.selected_doctype].method,
{
- 'from_date': this.get_from_date(),
+ 'date_range': this.get_date_range(),
'company': this.options.selected_company,
'field': this.options.selected_filter_item,
'limit': this.leaderboard_limit,
@@ -375,23 +375,22 @@ class Leaderboard {
`);
}
- get_from_date() {
+ get_date_range() {
let timespan = this.options.selected_timespan.toLowerCase();
let current_date = frappe.datetime.now_date();
- let get_from_date = {
- "this week": frappe.datetime.week_start(),
- "this month": frappe.datetime.month_start(),
- "this quarter": frappe.datetime.quarter_start(),
- "this year": frappe.datetime.year_start(),
- "last week": frappe.datetime.add_days(current_date, -7),
- "last month": frappe.datetime.add_months(current_date, -1),
- "last quarter": frappe.datetime.add_months(current_date, -3),
- "last year": frappe.datetime.add_months(current_date, -12),
- "all time": "",
- "select from date": this.selected_from_date || frappe.datetime.month_start()
+ let date_range_map = {
+ "this week": [frappe.datetime.week_start(), current_date],
+ "this month": [frappe.datetime.month_start(), current_date],
+ "this quarter": [frappe.datetime.quarter_start(), current_date],
+ "this year": [frappe.datetime.year_start(), current_date],
+ "last week": [frappe.datetime.add_days(current_date, -7), current_date],
+ "last month": [frappe.datetime.add_months(current_date, -1), current_date],
+ "last quarter": [frappe.datetime.add_months(current_date, -3), current_date],
+ "last year": [frappe.datetime.add_months(current_date, -12), current_date],
+ "all time": null,
+ "select date range": this.selected_date_range || [frappe.datetime.month_start(), current_date]
}
-
- return get_from_date[timespan];
+ return date_range_map[timespan];
}
}
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 2065f5558a..cf8c6e80c6 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -478,26 +478,38 @@ class EmailAccount(Document):
if self.append_to and self.sender_field:
if self.subject_field:
- # try and match by subject and sender
- # if sent by same sender with same subject,
- # append it to old coversation
- subject = frappe.as_unicode(strip(re.sub(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*",
- "", email.subject, 0, flags=re.IGNORECASE)))
+ if '#' in email.subject:
+ # try and match if ID is found
+ # document ID is appended to subject
+ # example "Re: Your email (#OPP-2020-2334343)"
+ parent_id = email.subject.rsplit('#', 1)[-1].strip(' ()')
+ if parent_id:
+ parent = frappe.db.get_all(self.append_to, filters = dict(name = parent_id),
+ fields = 'name')
- parent = frappe.db.get_all(self.append_to, filters={
- self.sender_field: email.from_email,
- self.subject_field: ("like", "%{0}%".format(subject)),
- "creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
- }, fields="name")
+ if not parent:
+ # try and match by subject and sender
+ # if sent by same sender with same subject,
+ # append it to old coversation
+ subject = frappe.as_unicode(strip(re.sub(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*",
+ "", email.subject, 0, flags=re.IGNORECASE)))
+
+ parent = frappe.db.get_all(self.append_to, filters={
+ self.sender_field: email.from_email,
+ self.subject_field: ("like", "%{0}%".format(subject)),
+ "creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
+ }, fields = "name", limit = 1)
- # match only subject field
- # when the from_email is of a user in the system
- # and subject is atleast 10 chars long
if not parent and len(subject) > 10 and is_system_user(email.from_email):
+ # match only subject field
+ # when the from_email is of a user in the system
+ # and subject is atleast 10 chars long
parent = frappe.db.get_all(self.append_to, filters={
self.subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
- }, fields="name")
+ }, fields = "name", limit = 1)
+
+
if parent:
parent = frappe._dict(doctype=self.append_to, name=parent[0].name)
diff --git a/frappe/installer.py b/frappe/installer.py
index 40fdc057d6..4baf0929f0 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -9,6 +9,7 @@ from __future__ import unicode_literals, print_function
from six.moves import input
import os, json, subprocess, shutil
+import click
import frappe
import frappe.database
import importlib
@@ -118,12 +119,20 @@ def remove_from_installed_apps(app_name):
if frappe.flags.in_install:
post_install()
-def remove_app(app_name, dry_run=False, yes=False, no_backup=False):
+def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False):
"""Remove app and all linked to the app's module with the app from a site."""
+ # dont allow uninstall app if not installed unless forced
+ if not force:
+ if app_name not in frappe.get_installed_apps():
+ click.secho("App {0} not installed on Site {1}".format(app_name, frappe.local.site), fg="yellow")
+ return
+
+ print("Uninstalling App {0} from Site {1}...".format(app_name, frappe.local.site))
+
if not dry_run and not yes:
- confirm = input("All doctypes (including custom), modules related to this app will be deleted. Are you sure you want to continue (y/n) ? ")
- if confirm!="y":
+ confirm = click.confirm("All doctypes (including custom), modules related to this app will be deleted. Are you sure you want to continue?")
+ if not confirm:
return
if not no_backup:
@@ -146,8 +155,12 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False):
if not doctype.issingle:
drop_doctypes.append(doctype.name)
- # remove reports, pages and web forms
- for doctype in ("Report", "Page", "Web Form"):
+
+ linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent'])
+ ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"]
+ doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes]
+
+ for doctype in doctypes_with_linked_modules:
for record in frappe.get_list(doctype, filters={"module": module_name}):
print("removing {0} {1}...".format(doctype, record.name))
if not dry_run:
@@ -166,6 +179,8 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False):
for doctype in set(drop_doctypes):
frappe.db.sql("drop table `tab{0}`".format(doctype))
+ click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green")
+
frappe.flags.in_uninstall = False
def post_install(rebuild_website=False):
diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py
index 0c28e95a24..c110694dff 100644
--- a/frappe/integrations/doctype/google_drive/google_drive.py
+++ b/frappe/integrations/doctype/google_drive/google_drive.py
@@ -189,10 +189,10 @@ def upload_system_backup_to_google_drive():
if frappe.flags.create_new_backup:
set_progress(1, "Backing up Data.")
backup = new_backup()
- fileurl_backup = os.path.basename(backup.backup_path_db)
- fileurl_site_config = os.path.basename(backup.site_config_backup_path)
- fileurl_public_files = os.path.basename(backup.backup_path_files)
- fileurl_private_files = os.path.basename(backup.backup_path_private_files)
+ fileurl_backup = backup.backup_path_db
+ fileurl_site_config = backup.site_config_backup_path
+ fileurl_public_files = backup.backup_path_files
+ fileurl_private_files = backup.backup_path_private_files
else:
fileurl_backup, fileurl_site_config, fileurl_public_files, fileurl_private_files = get_latest_backup_file(with_files=True)
@@ -208,7 +208,7 @@ def upload_system_backup_to_google_drive():
try:
media = MediaFileUpload(get_absolute_path(filename=fileurl), mimetype="application/gzip", resumable=True)
except IOError as e:
- frappe.throw(_("Google Drive - Could not locate locate - {0}").format(e))
+ frappe.throw(_("Google Drive - Could not locate - {0}").format(e))
try:
set_progress(2, "Uploading backup to Google Drive.")
@@ -232,7 +232,7 @@ def weekly_backup():
upload_system_backup_to_google_drive()
def get_absolute_path(filename):
- file_path = os.path.join(get_backups_path()[2:], filename)
+ file_path = os.path.join(get_backups_path()[2:], os.path.basename(filename))
return "{0}/sites/{1}".format(get_bench_path(), file_path)
def set_progress(progress, message):
diff --git a/frappe/integrations/frappe_providers/__init__.py b/frappe/integrations/frappe_providers/__init__.py
index 887e191e16..161937a936 100644
--- a/frappe/integrations/frappe_providers/__init__.py
+++ b/frappe/integrations/frappe_providers/__init__.py
@@ -7,7 +7,7 @@ from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrato
def migrate_to(local_site, frappe_provider):
if frappe_provider in ("frappe.cloud", "frappecloud.com"):
- return frappecloud_migrator(local_site, frappe_provider)
+ return frappecloud_migrator(local_site)
else:
print("{} is not supported yet".format(frappe_provider))
sys.exit(1)
diff --git a/frappe/integrations/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py
index 16bc09d9bf..e09f09a44b 100644
--- a/frappe/integrations/frappe_providers/frappecloud.py
+++ b/frappe/integrations/frappe_providers/frappecloud.py
@@ -1,412 +1,29 @@
-# imports - standard imports
-import getpass
-import json
-import os
-import re
-import sys
-
-# imports - third party imports
import click
-from html2text import html2text
import requests
-from tenacity import retry, stop_after_attempt, wait_fixed
+from html2text import html2text
-# imports - module imports
import frappe
-import frappe.utils.backups
-from frappe.utils import get_installed_apps_info
-from frappe.utils.commands import render_table, add_line_after, add_line_before
-# TODO: check upgrade compatibility
-
-
-def render_actions_table():
- actions_table = [["#", "Action"]]
- actions = []
-
- for n, action in enumerate(migrator_actions):
- actions_table.append([n+1, action["title"]])
- actions.append(action["fn"])
-
- render_table(actions_table)
- return actions
-
-
-def render_site_table(sites_info):
- sites_table = [["#", "Site Name", "Status"]]
- available_sites = []
-
- for n, site_data in enumerate(sites_info):
- name, status = site_data["name"], site_data["status"]
- if status in ("Active", "Broken"):
- sites_table.append([n + 1, name, status])
- available_sites.append(name)
-
- render_table(sites_table)
- return available_sites
-
-
-def render_teams_table(teams):
- teams_table = [["#", "Team"]]
-
- for n, team in enumerate(teams):
- teams_table.append([n+1, team])
-
- render_table(teams_table)
-
-
-def render_plan_table(plans_list):
- plans_table = [["Plan", "CPU Time"]]
- visible_headers = ["name", "cpu_time_per_day"]
-
- for plan in plans_list:
- plan, cpu_time = [plan[header] for header in visible_headers]
- plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
-
- render_table(plans_table)
-
-
-def render_group_table(app_groups):
- # title row
- app_groups_table = [["#", "App Group", "Apps"]]
-
- # all rows
- for idx, app_group in enumerate(app_groups):
- apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
- row = [idx + 1, app_group["name"], apps_list]
- app_groups_table.append(row)
-
- render_table(app_groups_table)
-
-
-def handle_request_failure(request=None, message=None, traceback=True, exit_code=1):
- message = message or "Request failed with error code {}".format(request.status_code)
- response = html2text(request.text) if traceback else ""
-
- print("{0}{1}".format(message, "\n" + response))
- sys.exit(exit_code)
-
-
-@add_line_after
-def select_primary_action():
- actions = render_actions_table()
- idx = click.prompt("What do you want to do?", type=click.IntRange(1, len(actions))) - 1
-
- return actions[idx]
-
-
-@add_line_after
-def select_site():
- get_all_sites_request = session.post(all_site_url, headers={
- "accept": "application/json",
- "accept-encoding": "gzip, deflate, br",
- "content-type": "application/json; charset=utf-8"
- })
-
- if get_all_sites_request.ok:
- all_sites = get_all_sites_request.json()["message"]
- available_sites = render_site_table(all_sites)
-
- while True:
- selected_site = click.prompt("Name of the site you want to restore to", type=str).strip()
- if selected_site in available_sites:
- return selected_site
- else:
- print("Site {} does not exist. Try again ❌".format(selected_site))
- else:
- print("Couldn't retrive sites list...Try again later")
- sys.exit(1)
-
-
-@add_line_before
-def select_team(session):
- # get team options
- account_details_sc = session.post(account_details_url)
- if account_details_sc.ok:
- account_details = account_details_sc.json()["message"]
- available_teams = account_details["teams"]
-
- # ask if they want to select, go ahead with if only one exists
- if len(available_teams) == 1:
- team = available_teams[0]
- else:
- render_teams_table(available_teams)
- idx = click.prompt("Select Team", type=click.IntRange(1, len(available_teams))) - 1
- team = available_teams[idx]
-
- print("Team '{}' set for current session".format(team))
-
- return team
-
-
-@retry(stop=stop_after_attempt(5))
-def get_new_site_options():
- site_options_sc = session.post(options_url)
-
- if site_options_sc.ok:
- site_options = site_options_sc.json()["message"]
- return site_options
- else:
- print("Couldn't retrive New site information: {}".format(site_options_sc.status_code))
-
-
-def is_valid_subdomain(subdomain):
- if len(subdomain) < 5:
- print("Subdomain too short. Use 5 or more characters")
- return False
- matched = re.match("^[a-z0-9][a-z0-9-]*[a-z0-9]$", subdomain)
- if matched:
- return True
- print("Subdomain contains invalid characters. Use lowercase characters, numbers and hyphens")
-
-
-@retry(stop=stop_after_attempt(5))
-def is_subdomain_available(subdomain):
- res = session.post(site_exists_url, {"subdomain": subdomain})
- if res.ok:
- available = not res.json()["message"]
- if not available:
- print("Subdomain already exists! Try another one")
-
- return available
-
-
-@add_line_after
-def choose_plan(plans_list):
- print("{} plans available".format(len(plans_list)))
- available_plans = [plan["name"] for plan in plans_list]
- render_plan_table(plans_list)
-
- while True:
- input_plan = click.prompt("Select Plan").strip()
- if input_plan in available_plans:
- print("{} Plan selected ✅".format(input_plan))
- return input_plan
- else:
- print("Invalid Selection ❌")
-
-
-@add_line_after
-def check_app_compat(available_group):
- is_compat = True
- incompatible_apps, filtered_apps, branch_msgs = [], [], []
- existing_group = [(app["app_name"], app["branch"]) for app in get_installed_apps_info()]
- print("Checking availability of existing app group")
-
- for (app, branch) in existing_group:
- info = [ (a["name"], a["branch"]) for a in available_group["apps"] if a["scrubbed"] == app ]
- if info:
- app_title, available_branch = info[0]
-
- if branch != available_branch:
- print("⚠️ App {}:{} => {}".format(app, branch, available_branch))
- branch_msgs.append([app, branch, available_branch])
- filtered_apps.append(app_title)
- is_compat = False
-
- else:
- print("✅ App {}:{}".format(app, branch))
- filtered_apps.append(app_title)
-
- else:
- incompatible_apps.append(app)
- print("❌ App {}:{}".format(app, branch))
- is_compat = False
-
- start_msg = "\nSelecting this group will "
- incompatible_apps = ("\n\nDrop the following apps:\n" + "\n".join(incompatible_apps)) if incompatible_apps else ""
- branch_change = ("\n\nUpgrade the following apps:\n" + "\n".join(["{}: {} => {}".format(*x) for x in branch_msgs])) if branch_msgs else ""
- changes = (incompatible_apps + branch_change) or "be perfect for you :)"
- warning_message = start_msg + changes
- print(warning_message)
-
- return is_compat, filtered_apps
-
-
-@add_line_after
-def filter_apps(app_groups):
- render_group_table(app_groups)
-
- while True:
- app_group_index = click.prompt("Select App Group Number", type=int) - 1
- try:
- if app_group_index == -1:
- raise IndexError
- selected_group = app_groups[app_group_index]
- except IndexError:
- print("Invalid Selection ❌")
- continue
-
- is_compat, filtered_apps = check_app_compat(selected_group)
-
- if is_compat or click.confirm("Continue anyway?"):
- print("App Group {} selected! ✅".format(selected_group["name"]))
- break
-
- return selected_group["name"], filtered_apps
-
-
-@add_line_after
-def get_subdomain(domain):
- while True:
- subdomain = click.prompt("Enter subdomain").strip()
- if is_valid_subdomain(subdomain) and is_subdomain_available(subdomain):
- print("Site Domain: {}.{}".format(subdomain, domain))
- return subdomain
-
-
-@retry(stop=stop_after_attempt(2), wait=wait_fixed(5))
-def upload_backup_file(file_type, file_path):
- return session.post(files_url, data={}, files={
- "file": open(file_path, "rb"),
- "is_private": 1,
- "folder": "Home",
- "method": "press.api.site.upload_backup",
- "type": file_type
- })
-
-
-@add_line_after
-def upload_backup(local_site):
- # take backup
- files_session = {}
- print("Taking backup for site {}".format(local_site))
- odb = frappe.utils.backups.new_backup(ignore_files=False, force=True)
-
- # upload files
- for x, (file_type, file_path) in enumerate([
- ("database", odb.backup_path_db),
- ("public", odb.backup_path_files),
- ("private", odb.backup_path_private_files)
- ]):
- file_name = file_path.split(os.sep)[-1]
-
- print("Uploading {} file: {} ({}/3)".format(file_type, file_name, x+1))
- file_upload_response = upload_backup_file(file_type, file_path)
-
- if file_upload_response.ok:
- files_session[file_type] = file_upload_response.json()["message"]
- else:
- print("Upload failed for: {}".format(file_path))
-
- files_uploaded = { k: v["file_url"] for k, v in files_session.items() }
- print("Uploaded backup files! ✅")
-
- return files_uploaded
-
-
-def new_site(local_site):
- # get new site options
- site_options = get_new_site_options()
-
- # set preferences from site options
- subdomain = get_subdomain(site_options["domain"])
- plan = choose_plan(site_options["plans"])
-
- app_groups = site_options["groups"]
- selected_group, filtered_apps = filter_apps(app_groups)
- files_uploaded = upload_backup(local_site)
-
- # push to frappe_cloud
- payload = json.dumps({
- "site": {
- "apps": filtered_apps,
- "files": files_uploaded,
- "group": selected_group,
- "name": subdomain,
- "plan": plan
- }
- })
-
- session.headers.update({"Content-Type": "application/json; charset=utf-8"})
- site_creation_request = session.post(upload_url, payload)
-
- if site_creation_request.ok:
- site_url = site_creation_request.json()["message"]
- print("Your site {} is being migrated ✨".format(local_site))
- print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
- print("Your site URL: {}".format(site_url))
- else:
- handle_request_failure(site_creation_request)
-
-
-def restore_site(local_site):
- # get list of existing sites they can restore
- selected_site = select_site()
-
- # TODO: check if they can restore it
-
- click.confirm("This is an irreversible action. Are you sure you want to continue?", abort=True)
-
- # backup site
- files_uploaded = upload_backup(local_site)
-
- # push to frappe_cloud
- payload = json.dumps({
- "name": selected_site,
- "files": files_uploaded
- })
- headers = {"Content-Type": "application/json; charset=utf-8"}
- site_restore_request = session.post(restore_site_url, payload, headers=headers)
-
- if site_restore_request.ok:
- print("Your site {0} is being restored on {1} ✨".format(local_site, selected_site))
- print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, selected_site))
- print("Your site URL: {}".format(selected_site))
- else:
- handle_request_failure(site_restore_request)
-
-
-@add_line_after
-def create_session():
- print("Frappe Cloud credentials @ {}".format(remote_site))
-
- # take user input from STDIN
- username = click.prompt("Username").strip()
- password = getpass.unix_getpass()
-
- auth_credentials = {"usr": username, "pwd": password}
-
- session = requests.Session()
- login_sc = session.post(login_url, auth_credentials)
-
- if login_sc.ok:
- print("Authorization Successful! ✅")
- team = select_team(session)
- session.headers.update({
- "X-Press-Team": team,
- "Connection": "keep-alive"
- })
- return session
- else:
- handle_request_failure(message="Authorization Failed with Error Code {}".format(login_sc.status_code), traceback=False)
-
-
-def frappecloud_migrator(local_site, frappecloud_site):
- global login_url, upload_url, files_url, options_url, site_exists_url, restore_site_url, account_details_url, all_site_url
- global session, migrator_actions, remote_site
-
+def frappecloud_migrator(local_site):
+ print("Retreiving Site Migrator...")
remote_site = frappe.conf.frappecloud_url or "frappecloud.com"
+ request_url = "https://{}/api/method/press.api.script".format(remote_site)
+ request = requests.get(request_url)
- login_url = "https://{}/api/method/login".format(remote_site)
- upload_url = "https://{}/api/method/press.api.site.new".format(remote_site)
- files_url = "https://{}/api/method/upload_file".format(remote_site)
- options_url = "https://{}/api/method/press.api.site.options_for_new".format(remote_site)
- site_exists_url = "https://{}/api/method/press.api.site.exists".format(remote_site)
- account_details_url = "https://{}/api/method/press.api.account.get".format(remote_site)
- all_site_url = "https://{}/api/method/press.api.site.all".format(remote_site)
- restore_site_url = "https://{}/api/method/press.api.site.restore".format(remote_site)
+ if request.status_code / 100 != 2:
+ print("Request exitted with Status Code: {}\nPayload: {}".format(request.status_code, html2text(request.text)))
+ click.secho("Some errors occurred while recovering the migration script. Please contact us @ Frappe Cloud if this issue persists", fg="yellow")
+ return
- migrator_actions = [
- { "title": "Create a new site", "fn": new_site },
- { "title": "Restore to an existing site", "fn": restore_site }
- ]
+ script_contents = request.json()["message"]
- # get credentials + auth user + start session
- session = create_session()
+ import tempfile
+ import os
+ import sys
- # available actions defined in migrator_actions
- primary_action = select_primary_action()
-
- primary_action(local_site)
+ py = sys.executable
+ script = tempfile.NamedTemporaryFile(mode="w")
+ script.write(script_contents)
+ print("Site Migrator stored at {}".format(script.name))
+ os.execv(py, [py, script.name, local_site])
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index d7028870f4..c7ef7890b4 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -334,7 +334,7 @@ class BaseDocument(object):
self.db_insert()
return
- frappe.msgprint(_("Duplicate name {0} {1}").format(self.doctype, self.name))
+ frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red")
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
elif frappe.db.is_unique_key_violation(e):
diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py
index 2142d544fe..fcf648e718 100644
--- a/frappe/model/create_new.py
+++ b/frappe/model/create_new.py
@@ -45,7 +45,9 @@ def make_new_doc(doctype):
doc = doc.get_valid_dict(sanitize=False)
doc["doctype"] = doctype
doc["__islocal"] = 1
- doc["__unsaved"] = 1
+
+ if not frappe.model.meta.is_single(doctype):
+ doc["__unsaved"] = 1
return doc
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 19517aa4a1..ac87b1d907 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -203,7 +203,7 @@ class DatabaseQuery(object):
def sanitize_fields(self):
'''
regex : ^.*[,();].*
- purpose : The regex will look for malicious patterns like `,`, '(', ')', ';' in each
+ purpose : The regex will look for malicious patterns like `,`, '(', ')', '@', ;' in each
field which may leads to sql injection.
example :
field = "`DocType`.`issingle`, version()"
@@ -211,11 +211,11 @@ class DatabaseQuery(object):
the system will filter out this field.
'''
- sub_query_regex = re.compile("^.*[,();].*")
- blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case']
+ sub_query_regex = re.compile("^.*[,();@].*")
+ blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case', 'show']
blacklisted_functions = ['concat', 'concat_ws', 'if', 'ifnull', 'nullif', 'coalesce',
'connection_id', 'current_user', 'database', 'last_insert_id', 'session_user',
- 'system_user', 'user', 'version']
+ 'system_user', 'user', 'version', 'global']
def _raise_exception():
frappe.throw(_('Use of sub-query or function is restricted'), frappe.DataError)
@@ -238,6 +238,10 @@ class DatabaseQuery(object):
if any("{0}(".format(keyword) in field.lower() for keyword in blacklisted_functions):
_raise_exception()
+ if '@' in field.lower():
+ # prevent access to global variables
+ _raise_exception()
+
if re.compile(r"[0-9a-zA-Z]+\s*'").match(field):
_raise_exception()
@@ -854,4 +858,4 @@ def get_date_range(operator, value):
timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value
- return get_timespan_date_range(timespan)
\ No newline at end of file
+ return get_timespan_date_range(timespan)
diff --git a/frappe/model/document.py b/frappe/model/document.py
index ea693167f8..69a781d6d1 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -403,9 +403,16 @@ class Document(BaseDocument):
def set_new_name(self, force=False, set_name=None, set_child_names=True):
"""Calls `frappe.naming.set_new_name` for parent and child docs."""
+
if self.flags.name_set and not force:
return
+ # If autoname has set as Prompt (name)
+ if self.get("__newname"):
+ self.name = self.get("__newname")
+ self.flags.name_set = True
+ return
+
if set_name:
self.name = set_name
else:
diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py
index b904132530..4b22c82105 100644
--- a/frappe/modules/export_file.py
+++ b/frappe/modules/export_file.py
@@ -12,16 +12,17 @@ def export_doc(doc):
def export_to_files(record_list=None, record_module=None, verbose=0, create_init=None):
"""
- Export record_list to files. record_list is a list of lists ([doctype],[docname] ) ,
+ Export record_list to files. record_list is a list of lists ([doctype, docname, folder name],) ,
"""
if frappe.flags.in_import:
return
if record_list:
for record in record_list:
- write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init)
+ folder_name = record[2] if len(record) == 3 else None
+ write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init, folder_name=folder_name)
-def write_document_file(doc, record_module=None, create_init=True):
+def write_document_file(doc, record_module=None, create_init=True, folder_name=None):
newdoc = doc.as_dict(no_nulls=True)
doc.run_method("before_export", newdoc)
@@ -35,7 +36,10 @@ def write_document_file(doc, record_module=None, create_init=True):
module = record_module or get_module_name(doc)
# create folder
- folder = create_folder(module, doc.doctype, doc.name, create_init)
+ if folder_name:
+ folder = create_folder(module, folder_name, doc.name, create_init)
+ else:
+ folder = create_folder(module, doc.doctype, doc.name, create_init)
# write the data file
fname = scrub(doc.name)
diff --git a/frappe/patches.txt b/frappe/patches.txt
index f883d2a7bb..1108f1fb1b 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -19,6 +19,7 @@ execute:frappe.reload_doc('core', 'doctype', 'module_def') #2017-09-22
execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01
execute:frappe.reload_doc('email', 'doctype', 'document_follow')
execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02
+execute:frappe.reload_doc('core', 'doctype', 'has_role')
execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02
frappe.patches.v11_0.replicate_old_user_permissions
frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03
@@ -263,6 +264,7 @@ 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
frappe.patches.v12_0.change_existing_dashboard_chart_filters
+frappe.patches.v12_0.set_correct_assign_value_in_docs #2020-07-13
execute:frappe.delete_doc("Test Runner")
execute:frappe.delete_doc_if_exists('DocType', 'Google Maps Settings')
execute:frappe.db.set_default('desktop:home_page', 'workspace')
@@ -272,6 +274,7 @@ execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
+frappe.patches.v12_0.set_correct_url_in_files
frappe.patches.v13_0.website_theme_custom_scss
frappe.patches.v13_0.set_existing_dashboard_charts_as_public
frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
@@ -290,3 +293,4 @@ execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link")
frappe.patches.v13_0.update_date_filters_in_user_settings
frappe.patches.v13_0.update_duration_options
frappe.patches.v13_0.replace_old_data_import # 2020-06-24
+frappe.patches.v13_0.create_custom_dashboards_cards_and_charts
\ No newline at end of file
diff --git a/frappe/patches/v11_0/reload_and_rename_view_log.py b/frappe/patches/v11_0/reload_and_rename_view_log.py
index 611de79a3c..12c71b746f 100644
--- a/frappe/patches/v11_0/reload_and_rename_view_log.py
+++ b/frappe/patches/v11_0/reload_and_rename_view_log.py
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import frappe
def execute():
- if frappe.db.exists('DocType', 'View log'):
+ if frappe.db.table_exists('View log'):
# for mac users direct renaming would not work since mysql for mac saves table name in lower case
# so while renaming `tabView log` to `tabView Log` we get "Table 'tabView Log' already exists" error
# more info https://stackoverflow.com/a/44753093/5955589 ,
diff --git a/frappe/patches/v12_0/set_correct_assign_value_in_docs.py b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
new file mode 100644
index 0000000000..65a635c170
--- /dev/null
+++ b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
@@ -0,0 +1,32 @@
+import frappe
+
+def execute():
+ frappe.reload_doc('desk', 'doctype', 'todo')
+
+ query = '''
+ SELECT
+ name, reference_type, reference_name, {} as assignees
+ FROM
+ `tabToDo`
+ WHERE
+ COALESCE(reference_type, '') != '' AND
+ COALESCE(reference_name, '') != '' AND
+ status != 'Cancelled'
+ GROUP BY
+ reference_type, reference_name
+ '''
+
+ assignments = frappe.db.multisql({
+ 'mariadb': query.format('GROUP_CONCAT(DISTINCT `owner`)'),
+ 'postgres': query.format('STRING_AGG(DISTINCT "owner", ",")')
+ }, as_dict=True)
+
+ for doc in assignments:
+ assignments = doc.assignees.split(',')
+ frappe.db.set_value(
+ doc.reference_type,
+ doc.reference_name,
+ '_assign',
+ frappe.as_json(assignments),
+ update_modified=False
+ )
diff --git a/frappe/patches/v12_0/set_correct_url_in_files.py b/frappe/patches/v12_0/set_correct_url_in_files.py
new file mode 100644
index 0000000000..4f820c1b24
--- /dev/null
+++ b/frappe/patches/v12_0/set_correct_url_in_files.py
@@ -0,0 +1,39 @@
+import frappe
+import os
+
+def execute():
+ files = frappe.get_all('File',
+ fields = ['name', 'file_name', 'file_url'],
+ filters = {
+ 'is_folder': 0,
+ 'file_url': ['!=', ''],
+ })
+
+ private_file_path = frappe.get_site_path('private', 'files')
+ public_file_path = frappe.get_site_path('public', 'files')
+
+ for file in files:
+ file_path = file.file_url
+ file_name = file_path.split('/')[-1]
+
+ if not file_path.startswith(('/private/', '/files/')):
+ continue
+
+ file_is_private = file_path.startswith('/private/files/')
+ full_path = frappe.utils.get_files_path(file_name, is_private=file_is_private)
+
+ if not os.path.exists(full_path):
+ if file_is_private:
+ public_file_url = os.path.join(public_file_path, file_name)
+ if os.path.exists(public_file_url):
+ frappe.db.set_value('File', file.name, {
+ 'file_url': '/files/{0}'.format(file_name),
+ 'is_private': 0
+ })
+ else:
+ private_file_url = os.path.join(private_file_path, file_name)
+ if os.path.exists(private_file_url):
+ frappe.db.set_value('File', file.name, {
+ 'file_url': '/private/files/{0}'.format(file_name),
+ 'is_private': 1
+ })
diff --git a/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py b/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py
new file mode 100644
index 0000000000..9a075a22cc
--- /dev/null
+++ b/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py
@@ -0,0 +1,45 @@
+import frappe
+from frappe.model.naming import append_number_if_name_exists
+from frappe.utils.dashboard import get_dashboards_with_link
+
+def execute():
+ if not frappe.db.table_exists('Dashboard Chart')\
+ or not frappe.db.table_exists('Number Card')\
+ or not frappe.db.table_exists('Dashboard'):
+ return
+
+ frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
+ frappe.reload_doc('desk', 'doctype', 'number_card')
+ frappe.reload_doc('desk', 'doctype', 'dashboard')
+
+ modified_charts = get_modified_docs('Dashboard Chart')
+ modified_cards = get_modified_docs('Number Card')
+ modified_dashboards = [doc.name for doc in get_modified_docs('Dashboard')]
+
+ for chart in modified_charts:
+ modified_dashboards += get_dashboards_with_link(chart.name, 'Dashboard Chart')
+ rename_modified_doc(chart.name, 'Dashboard Chart')
+
+ for card in modified_cards:
+ modified_dashboards += get_dashboards_with_link(card.name, 'Number Card')
+ rename_modified_doc(card.name, 'Number Card')
+
+ modified_dashboards = list(set(modified_dashboards))
+
+ for dashboard in modified_dashboards:
+ rename_modified_doc(dashboard, 'Dashboard')
+
+def get_modified_docs(doctype):
+ return frappe.get_all(doctype,
+ filters = {
+ 'owner': 'Administrator',
+ 'modified_by': ['!=', 'Administrator']
+ })
+
+def rename_modified_doc(docname, doctype):
+ new_name = docname + ' Custom'
+ try:
+ frappe.rename_doc(doctype, docname, new_name)
+ except frappe.ValidationError:
+ new_name = append_number_if_name_exists(doctype, new_name)
+ frappe.rename_doc(doctype, docname, new_name)
diff --git a/frappe/patches/v13_0/replace_old_data_import.py b/frappe/patches/v13_0/replace_old_data_import.py
index f3eed6253c..920ee7b553 100644
--- a/frappe/patches/v13_0/replace_old_data_import.py
+++ b/frappe/patches/v13_0/replace_old_data_import.py
@@ -6,11 +6,15 @@ import frappe
def execute():
- if not frappe.db.exists("DocType", "Data Import Beta"):
+ if not frappe.db.table_exists("Data Import"): return
+
+ meta = frappe.get_meta("Data Import")
+ # if Data Import is the new one, return early
+ if meta.fields[1].fieldname == "import_type":
return
frappe.db.sql("DROP TABLE IF EXISTS `tabData Import Legacy`")
- frappe.rename_doc('DocType', 'Data Import', 'Data Import Legacy')
+ frappe.rename_doc("DocType", "Data Import", "Data Import Legacy")
frappe.db.commit()
frappe.db.sql("DROP TABLE IF EXISTS `tabData Import`")
- frappe.rename_doc('DocType', 'Data Import Beta', 'Data Import')
+ frappe.rename_doc("DocType", "Data Import Beta", "Data Import")
diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json
index 397d9dda5d..f93ad0ee5a 100644
--- a/frappe/printing/doctype/print_settings/print_settings.json
+++ b/frappe/printing/doctype/print_settings/print_settings.json
@@ -1,932 +1,203 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
+ "actions": [],
"creation": "2014-07-17 06:54:20.782907",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "System",
- "editable_grid": 0,
+ "engine": "InnoDB",
+ "field_order": [
+ "pdf_settings",
+ "send_print_as_pdf",
+ "repeat_header_footer",
+ "column_break_4",
+ "pdf_page_size",
+ "view_link_in_email",
+ "with_letterhead",
+ "allow_print_for_draft",
+ "add_draft_heading",
+ "column_break_10",
+ "allow_page_break_inside_tables",
+ "allow_print_for_cancelled",
+ "server_printer",
+ "enable_print_server",
+ "server_ip",
+ "printer_name",
+ "port",
+ "raw_printing_section",
+ "enable_raw_printing",
+ "print_style_section",
+ "print_style",
+ "print_style_preview",
+ "section_break_8",
+ "font",
+ "font_size"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "pdf_settings",
"fieldtype": "Section Break",
- "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": "PDF Settings",
- "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": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "PDF Settings"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"description": "Send Email Print Attachments as PDF (Recommended)",
- "fetch_if_empty": 0,
"fieldname": "send_print_as_pdf",
"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": "Send Print as PDF",
- "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": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Send Print as PDF"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
- "fetch_if_empty": 0,
"fieldname": "repeat_header_footer",
"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": "Repeat Header and Footer in PDF",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Repeat Header and Footer in PDF"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "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,
- "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,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "A4",
- "fetch_if_empty": 0,
"fieldname": "pdf_page_size",
"fieldtype": "Select",
- "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": "PDF Page Size",
- "length": 0,
- "no_copy": 0,
- "options": "A4\nLetter",
- "permlevel": 0,
- "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,
- "translatable": 0,
- "unique": 0
+ "options": "A4\nLetter"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "view_link_in_email",
"fieldtype": "Section Break",
- "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": "Page Settings",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Page Settings"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
- "description": "",
- "fetch_if_empty": 0,
"fieldname": "with_letterhead",
"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": "Print with letterhead",
- "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": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Print with letterhead"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
- "description": "",
- "fetch_if_empty": 0,
"fieldname": "allow_print_for_draft",
"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": "Allow Print for Draft",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Allow Print for Draft"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "1",
- "description": "",
- "fetch_if_empty": 0,
- "fieldname": "attach_view_link",
- "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": "Send document web view link in email",
- "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,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_10",
- "fieldtype": "Column Break",
- "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,
- "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,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
- "fetch_if_empty": 0,
"fieldname": "add_draft_heading",
"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": "Always add \"Draft\" Heading for printing draft documents",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Always add \"Draft\" Heading for printing draft documents"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "allow_page_break_inside_tables",
"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": "Allow page break inside tables",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Allow page break inside tables"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "",
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "allow_print_for_cancelled",
"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": "Allow Print for Cancelled",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Allow Print for Cancelled"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_if_empty": 0,
"fieldname": "server_printer",
"fieldtype": "Section Break",
- "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": "Print Server",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Print Server"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "enable_print_server",
"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": "Enable Print Server",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Enable Print Server"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "localhost",
"depends_on": "enable_print_server",
- "fetch_if_empty": 0,
"fieldname": "server_ip",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Server IP",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Server IP"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "enable_print_server",
- "fetch_if_empty": 0,
"fieldname": "printer_name",
"fieldtype": "Select",
- "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": "Printer Name",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Printer Name"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "631",
"depends_on": "enable_print_server",
- "fetch_if_empty": 0,
"fieldname": "port",
"fieldtype": "Int",
- "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": "Port",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Port"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "raw_printing_section",
"fieldtype": "Section Break",
- "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": "Raw Printing",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Raw Printing"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "enable_raw_printing",
"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": "Enable Raw Printing",
- "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,
- "translatable": 0,
- "unique": 0
+ "label": "Enable Raw Printing"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "print_style_section",
"fieldtype": "Section Break",
- "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": "Print Style",
- "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": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Print Style"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Modern",
- "fetch_if_empty": 0,
"fieldname": "print_style",
"fieldtype": "Link",
- "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": "Print Style",
- "length": 0,
- "no_copy": 0,
- "options": "Print Style",
- "permlevel": 0,
- "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,
- "translatable": 0,
- "unique": 0
+ "options": "Print Style"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "print_style_preview",
"fieldtype": "HTML",
- "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": "Print Style Preview",
- "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": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Print Style Preview"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
- "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": "Fonts",
- "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": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Fonts"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Default",
- "fetch_if_empty": 0,
"fieldname": "font",
"fieldtype": "Select",
- "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": "Font",
- "length": 0,
- "no_copy": 0,
- "options": "Default\nArial\nHelvetica\nVerdana\nMonospace",
- "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,
- "translatable": 0,
- "unique": 0
+ "options": "Default\nArial\nHelvetica\nVerdana\nMonospace"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"description": "In points. Default is 9.",
- "fetch_if_empty": 0,
"fieldname": "font_size",
"fieldtype": "Float",
- "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": "Font Size",
- "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": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Font Size"
}
],
- "has_web_view": 0,
- "hide_toolbar": 0,
"icon": "fa fa-cog",
- "idx": 0,
- "in_create": 0,
- "is_submittable": 0,
"issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2019-04-10 14:12:31.081187",
+ "links": [],
+ "modified": "2020-07-02 16:14:47.470668",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Settings",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
"read": 1,
- "report": 0,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
"quick_entry": 1,
- "read_only": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 79a78717cb..2e80dbfd85 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -101,15 +101,6 @@ frappe.Application = Class.extend({
frappe.ui.startup_setup_dialog.show();
}
- // listen to csrf_update
- frappe.realtime.on("csrf_generated", function(data) {
- // handles the case when a user logs in again from another tab
- // and it leads to invalid request in the current tab
- if (data.csrf_token && data.sid===frappe.get_cookie("sid")) {
- frappe.csrf_token = data.csrf_token;
- }
- });
-
frappe.realtime.on("version-update", function() {
var dialog = frappe.msgprint({
message:__("The application has been updated to a new version, please refresh this page"),
diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js
index 3c0f7d5110..e367989b81 100644
--- a/frappe/public/js/frappe/form/controls/text_editor.js
+++ b/frappe/public/js/frappe/form/controls/text_editor.js
@@ -199,6 +199,8 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
get_input_value() {
let value = this.quill ? this.quill.root.innerHTML : '';
+ // hack to retain space sequence.
+ value = value.replace(/(\s)(\s)/g, ' ');
return value;
},
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index ebe94b4cdb..f7d1b3e873 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -842,6 +842,15 @@ frappe.ui.form.Form = class FrappeForm {
this.page.clear_primary_action();
}
+ disable_form() {
+ this.set_read_only();
+ this.fields
+ .forEach((field) => {
+ this.set_df_property(field.df.fieldname, "read_only", "1");
+ });
+ this.disable_save();
+ }
+
handle_save_fail(btn, on_error) {
$(btn).prop('disabled', false);
if (on_error) {
@@ -1604,6 +1613,7 @@ frappe.ui.form.Form = class FrappeForm {
});
driver.defineSteps(steps);
+ frappe.route.on('change', () => driver.reset());
driver.start();
}
};
diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js
index 68444c8a3b..2da7b8f236 100644
--- a/frappe/public/js/frappe/form/quick_entry.js
+++ b/frappe/public/js/frappe/form/quick_entry.js
@@ -240,13 +240,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({
var me = this;
var data = this.dialog.get_values(true);
$.each(data, function(key, value) {
- if(key==='__newname') {
- me.dialog.doc.name = value;
- }
- else {
- if(!is_null(value)) {
- me.dialog.doc[key] = value;
- }
+ if (!is_null(value)) {
+ me.dialog.doc[key] = value;
}
});
return this.dialog.doc;
@@ -282,7 +277,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
field.doctype = me.doc.doctype;
field.docname = me.doc.name;
- if(!is_null(me.doc[fieldname])) {
+ if (!is_null(me.doc[fieldname])) {
field.set_input(me.doc[fieldname]);
}
});
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js
index b94257106e..bbe2fa2f95 100644
--- a/frappe/public/js/frappe/list/base_list.js
+++ b/frappe/public/js/frappe/list/base_list.js
@@ -109,9 +109,10 @@ frappe.views.BaseList = class BaseList {
this.fields = this.fields.uniqBy(f => f[0] + f[1]);
}
- _add_field(fieldname) {
+ _add_field(fieldname, doctype) {
if (!fieldname) return;
- let doctype = this.doctype;
+
+ if (!doctype) doctype = this.doctype;
if (typeof fieldname === 'object') {
// df is passed
@@ -120,6 +121,8 @@ frappe.views.BaseList = class BaseList {
doctype = df.parent;
}
+ if (!this.fields) this.fields = [];
+
const is_valid_field = frappe.model.std_fields_list.includes(fieldname)
|| frappe.meta.has_field(doctype, fieldname)
|| fieldname === '_seen';
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 8da73b0dec..4d8121ebd6 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -528,6 +528,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
get_header_html() {
+ if (!this.columns) {
+ return;
+ }
+
const subject_field = this.columns[0].df;
let subject_html = `
@@ -784,6 +788,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return '#Form/' + this.doctype + '/' + docname;
}
+ get_seen_class(doc) {
+ return JSON.parse(doc._seen || '[]').includes(frappe.session.user)
+ ? ''
+ : 'bold';
+ }
+
get_subject_html(doc) {
let user = frappe.session.user;
let subject_field = this.columns[0].df;
@@ -795,8 +805,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
let heart_class = liked_by.includes(user) ?
'liked-by' : 'text-extra-muted not-liked';
- const seen = JSON.parse(doc._seen || '[]')
- .includes(user) ? '' : 'bold';
+ const seen = this.get_seen_class(doc);
let subject_html = `
@@ -1146,7 +1155,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
});
this.toggle_result_area();
this.render_list();
- if (this.$checks.length) {
+ if (this.$checks && this.$checks.length) {
this.set_rows_as_checked();
}
});
diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js
index 0a5f5e7f6b..5cf50bd0a3 100644
--- a/frappe/public/js/frappe/request.js
+++ b/frappe/public/js/frappe/request.js
@@ -126,7 +126,7 @@ frappe.request.call = function(opts) {
message: __('The resource you are looking for is not available')});
},
403: function(xhr) {
- if (frappe.get_cookie('sid')==='Guest') {
+ if (frappe.session.user === 'Guest') {
// session expired
frappe.app.handle_session_expired();
}
@@ -321,7 +321,7 @@ frappe.request.cleanup = function(opts, r) {
if(r) {
// session expired? - Guest has no business here!
- if(r.session_expired || frappe.get_cookie("sid")==="Guest") {
+ if (r.session_expired || frappe.session.user === "Guest") {
frappe.app.handle_session_expired();
return;
}
diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js
index 3570420c81..03f3662d2a 100644
--- a/frappe/public/js/frappe/ui/notifications/notifications.js
+++ b/frappe/public/js/frappe/ui/notifications/notifications.js
@@ -309,7 +309,7 @@ frappe.ui.Notifications = class Notifications {
let mark_read_action = field.read ? '': 'data-action="mark_as_read"';
let message = field.subject;
let title = message.match(/(.*?)<\/b>/);
- message = title ? message.replace(title[1], frappe.ellipsis(title[1], 100)): message;
+ message = title ? message.replace(title[1], frappe.ellipsis(strip_html(title[1]), 100)) : message;
let message_html = `${message}
`;
let user = field.from_user;
let user_avatar = frappe.avatar(user, 'avatar-small user-avatar');
diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
index 3e59986928..e11adcfb66 100644
--- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
+++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
@@ -228,7 +228,7 @@ frappe.search.AwesomeBar = Class.extend({
}
this.options.push({
- label: __("Search for '{0}'", [txt.bold()]),
+ label: __("Search for '{0}'", [frappe.utils.xss_sanitise(txt).bold()]),
value: __("Search for '{0}'", [txt]),
match: txt,
index: 100,
diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js
index d1621a3e15..f737c6ad12 100644
--- a/frappe/public/js/frappe/utils/dashboard_utils.js
+++ b/frappe/public/js/frappe/utils/dashboard_utils.js
@@ -47,7 +47,7 @@ frappe.dashboard_utils = {
frappe.dom.eval(config);
return frappe.dashboards.chart_sources[chart.source].filters;
});
- } else if (chart.chart_type === 'Report') {
+ } else if (chart.chart_type === 'Report' && chart.report_name) {
return frappe.report_utils.get_report_filters(chart.report_name).then(filters => {
return filters;
});
@@ -97,6 +97,28 @@ frappe.dashboard_utils = {
get_year(date_str) {
return date_str.substring(0, date_str.indexOf('-'));
+ },
+
+ remove_common_static_filter_values(static_filters, dynamic_filters) {
+ if (dynamic_filters) {
+ if ($.isArray(static_filters)) {
+ static_filters = static_filters.filter(static_filter => {
+ for (let dynamic_filter of dynamic_filters) {
+ if (static_filter[0] == dynamic_filter[0]
+ && static_filter[1] == dynamic_filter[1]) {
+ return false;
+ }
+ }
+ return true;
+ });
+ } else {
+ for (let key of Object.keys(dynamic_filters)) {
+ delete static_filters[key];
+ }
+ }
+ }
+
+ return static_filters;
}
};
\ No newline at end of file
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 8dad5d9121..53d946f75d 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -174,17 +174,21 @@ frappe.views.CommunicationComposer = Class.extend({
}
if (!this.subject) {
- if (this.frm.subject_field && this.frm.doc[this.frm.subject_field]) {
- this.subject = __("Re: {0}", [this.frm.doc[this.frm.subject_field]]);
- } else {
- let title = this.frm.doc.name;
- if(this.frm.meta.title_field && this.frm.doc[this.frm.meta.title_field]
- && this.frm.doc[this.frm.meta.title_field] != this.frm.doc.name) {
- title = `${this.frm.doc[this.frm.meta.title_field]} (#${this.frm.doc.name})`;
- }
- this.subject = `${__(this.frm.doctype)}: ${title}`;
+ this.subject = this.frm.doc.name;
+ if (this.frm.meta.subject_field && this.frm.doc[this.frm.meta.subject_field]) {
+ this.subject = this.frm.doc[this.frm.meta.subject_field];
+ } else if (this.frm.meta.title_field && this.frm.doc[this.frm.meta.title_field]) {
+ this.subject = this.frm.doc[this.frm.meta.title_field];
}
}
+
+ // always add an identifier to catch a reply
+ // some email clients (outlook) may not send the message id to identify
+ // the thread. So as a backup we use the name of the document as identifier
+ let identifier = `#${this.frm.doc.name}`;
+ if (!this.subject.includes(identifier)) {
+ this.subject = `${this.subject} (${identifier})`;
+ }
}
if (this.frm && !this.recipients) {
diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js
index acc49c79a4..0974a6c9f5 100644
--- a/frappe/public/js/frappe/views/desktop/desktop.js
+++ b/frappe/public/js/frappe/views/desktop/desktop.js
@@ -53,7 +53,7 @@ export default class Desktop {
.call("frappe.desk.desktop.get_desk_sidebar_items")
.then(response => {
if (response.message) {
- this.desktop_settings = response.message;
+ this.sidebar_configuration = response.message;
} else {
frappe.throw({
title: __("Couldn't Load Desk"),
@@ -71,10 +71,11 @@ export default class Desktop {
make_sidebar() {
const get_sidebar_item = function(item) {
- return $(`
{{ _("Leave a Comment") }}