diff --git a/.snyk b/.snyk
index b39169dcee..0dfecc6136 100644
--- a/.snyk
+++ b/.snyk
@@ -1,5 +1,5 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
-version: v1.13.5
+version: v1.14.1
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
SNYK-JS-AWESOMPLETE-174474:
@@ -22,3 +22,44 @@ patch:
SNYK-JS-LODASH-450202:
- frappe-datatable > lodash:
patched: '2020-01-31T01:33:09.889Z'
+ SNYK-JS-LODASH-567746:
+ - frappe-datatable > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - quagga > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - tailwindcss > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - '@tailwindcss/ui > @tailwindcss/custom-forms > lodash':
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/dep-graph > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > inquirer > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-config > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-mvn-plugin > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-nodejs-lockfile-parser > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-nuget-plugin > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/dep-graph > graphlib > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-go-plugin > graphlib > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-nuget-plugin > dotnet-deps-parser > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > snyk-php-plugin > @snyk/composer-lockfile-parser > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/ruby-semver > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
+ - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash:
+ patched: '2020-04-30T23:02:32.330Z'
diff --git a/.travis.yml b/.travis.yml
index 174f92ea11..30eb882256 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -102,7 +102,13 @@ install:
- if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi
- if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi
- - bench setup requirements --node
+ - if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
+
+ # install node-sass which is required for website theme test
+ - cd ./apps/frappe
+ - yarn add node-sass@4.13.1
+ - cd ../..
+
- bench start &
- bench --site test_site reinstall --yes
- bench --site test_site_producer reinstall --yes
diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js
index 51cba94a70..47f8efe94b 100644
--- a/cypress/integration/list_view_settings.js
+++ b/cypress/integration/list_view_settings.js
@@ -9,7 +9,9 @@ context('List View Settings', () => {
cy.get('.sidebar-stat').should('contain', "Tags");
});
it('disable count and sidebar stats then verify', () => {
+ cy.wait(300);
cy.visit('/desk#List/DocType/List');
+ cy.wait(300);
cy.get('.list-count').should('contain', "20 of");
cy.get('button').contains('Menu').click();
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
diff --git a/cypress/integration/login.js b/cypress/integration/login.js
index 3f13130b58..861377444c 100644
--- a/cypress/integration/login.js
+++ b/cypress/integration/login.js
@@ -21,6 +21,15 @@ context('Login', () => {
cy.location('pathname').should('eq', '/login');
});
+ it('shows invalid login if incorrect credentials', () => {
+ cy.get('#login_email').type('Administrator');
+ cy.get('#login_password').type('qwer');
+
+ cy.get('.btn-login').click();
+ cy.get('.page-card-head').contains('Invalid Login. Try again.');
+ cy.location('pathname').should('eq', '/login');
+ });
+
it('logs in using correct credentials', () => {
cy.get('#login_email').type('Administrator');
cy.get('#login_password').type(Cypress.config('adminPassword'));
@@ -30,12 +39,30 @@ context('Login', () => {
cy.window().its('frappe.session.user').should('eq', 'Administrator');
});
- it('shows invalid login if incorrect credentials', () => {
+ it('check redirect after login', () => {
+
+ // mock for OAuth 2.0 client_id, redirect_uri, scope and state
+ const payload = new URLSearchParams({
+ uuid: '6fed1519-cfd8-4a2d-84a6-9a1799c7c741',
+ encoded_string: 'hello all',
+ encoded_url: 'http://test.localhost/callback',
+ base64_string: 'aGVsbG8gYWxs'
+ });
+
+ cy.request('/api/method/logout');
+
+ // redirect-to /me page with params to mock OAuth 2.0 like request
+ cy.visit(
+ '/login?redirect-to=/me?' +
+ encodeURIComponent(payload.toString().replace("+", " "))
+ );
+
cy.get('#login_email').type('Administrator');
- cy.get('#login_password').type('qwer');
+ cy.get('#login_password').type(Cypress.config('adminPassword'));
cy.get('.btn-login').click();
- cy.get('.page-card-head').contains('Invalid Login. Try again.');
- cy.location('pathname').should('eq', '/login');
+
+ // verify redirected location and url params after login
+ cy.url().should('include', '/me?' + payload.toString().replace('+', '%20'));
});
});
diff --git a/frappe/__init__.py b/frappe/__init__.py
index eae8b0d76f..f0b6bfe41b 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -345,7 +345,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
style="margin: 0;">{}'''.format(table_rows)
if flags.print_messages and out.message:
- print("Message: " + repr(out.message).encode("utf-8"))
+ print(f"Message: {repr(out.message).encode('utf-8')}")
if title:
out.title = title
diff --git a/frappe/app.py b/frappe/app.py
index 41798b0bc4..3bb764149b 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -26,6 +26,7 @@ from frappe.core.doctype.comment.comment import update_comments_in_parent_after_
from frappe import _
import frappe.recorder
import frappe.monitor
+import frappe.rate_limiter
local_manager = LocalManager([frappe.local])
@@ -54,6 +55,7 @@ def application(request):
frappe.recorder.record()
frappe.monitor.start()
+ frappe.rate_limiter.apply()
if frappe.local.form_dict.cmd:
response = frappe.handler.handle()
@@ -93,9 +95,13 @@ def application(request):
if response and hasattr(frappe.local, 'cookie_manager'):
frappe.local.cookie_manager.flush_cookies(response=response)
+ frappe.rate_limiter.update()
frappe.monitor.stop(response)
frappe.recorder.dump()
+ if response and hasattr(frappe.local, 'rate_limiter'):
+ response.headers.extend(frappe.local.rate_limiter.headers())
+
frappe.destroy()
return response
@@ -171,6 +177,9 @@ def handle_exception(e):
http_status_code=http_status_code, indicator_color='red')
return_as_message = True
+ elif http_status_code == 429:
+ response = frappe.rate_limiter.respond()
+
else:
traceback = "
+ $(`
`).appendTo(this.wrapper.find(".page-content").empty());
this.container = this.wrapper.find(".dashboard-graph");
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 394f38b56c..122e6c7070 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -48,6 +48,7 @@
"allow_in_quick_entry",
"ignore_xss_filter",
"translatable",
+ "hide_border",
"description",
"permlevel",
"width",
@@ -378,12 +379,19 @@
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
}
],
"icon": "fa fa-glass",
"idx": 1,
"links": [],
- "modified": "2020-04-10 11:57:10.392218",
+ "modified": "2020-04-27 11:40:48.325481",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
diff --git a/frappe/website/web_template/navbar_with_links_on_right/__init__.py b/frappe/custom/doctype/custom_link/__init__.py
similarity index 100%
rename from frappe/website/web_template/navbar_with_links_on_right/__init__.py
rename to frappe/custom/doctype/custom_link/__init__.py
diff --git a/frappe/custom/doctype/custom_link/custom_link.js b/frappe/custom/doctype/custom_link/custom_link.js
new file mode 100644
index 0000000000..8662724b1a
--- /dev/null
+++ b/frappe/custom/doctype/custom_link/custom_link.js
@@ -0,0 +1,20 @@
+// Copyright (c) 2020, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Custom Link', {
+ refresh: function(frm) {
+ frm.set_query("document_type", function () {
+ return {
+ filters: {
+ custom: 0,
+ istable: 0,
+ module: ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
+ }
+ };
+ });
+
+ frm.add_custom_button(__('Go to {0} List', [frm.doc.document_type]), function() {
+ frappe.set_route('List', frm.doc.document_type);
+ });
+ }
+});
diff --git a/frappe/custom/doctype/custom_link/custom_link.json b/frappe/custom/doctype/custom_link/custom_link.json
new file mode 100644
index 0000000000..350e6b1c2d
--- /dev/null
+++ b/frappe/custom/doctype/custom_link/custom_link.json
@@ -0,0 +1,52 @@
+{
+ "actions": [],
+ "autoname": "field:document_type",
+ "creation": "2020-04-08 15:16:44.342509",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "links"
+ ],
+ "fields": [
+ {
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Document Type",
+ "options": "DocType",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "links",
+ "fieldtype": "Table",
+ "label": "Links",
+ "options": "DocType Link"
+ }
+ ],
+ "links": [],
+ "modified": "2020-04-08 16:42:59.402671",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Link",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/doctype/custom_link/custom_link.py b/frappe/custom/doctype/custom_link/custom_link.py
new file mode 100644
index 0000000000..11316d5751
--- /dev/null
+++ b/frappe/custom/doctype/custom_link/custom_link.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class CustomLink(Document):
+ pass
diff --git a/frappe/desk/doctype/onboarding/test_onboarding.py b/frappe/custom/doctype/custom_link/test_custom_link.py
similarity index 81%
rename from frappe/desk/doctype/onboarding/test_onboarding.py
rename to frappe/custom/doctype/custom_link/test_custom_link.py
index 8a9e346fd9..a292f73ad0 100644
--- a/frappe/desk/doctype/onboarding/test_onboarding.py
+++ b/frappe/custom/doctype/custom_link/test_custom_link.py
@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
-class TestOnboarding(unittest.TestCase):
+class TestCustomLink(unittest.TestCase):
pass
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index ebf01d11b3..6a54d9c7e6 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -76,7 +76,8 @@ docfield_properties = {
'remember_last_selected_value': 'Check',
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link',
- 'allow_in_quick_entry': 'Check'
+ 'allow_in_quick_entry': 'Check',
+ 'hide_border': 'Check'
}
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json
index d7887cf8bd..2c5fb874f7 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.json
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json
@@ -39,6 +39,7 @@
"allow_on_submit",
"report_hide",
"remember_last_selected_value",
+ "hide_border",
"property_depends_on_section",
"mandatory_depends_on",
"column_break_33",
@@ -388,12 +389,19 @@
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-10 11:58:44.573537",
+ "modified": "2020-04-27 11:39:26.389300",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 46940cc846..bd93069a3f 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -63,6 +63,7 @@ CREATE TABLE `tabDocField` (
`precision` varchar(255) DEFAULT NULL,
`length` int(11) NOT NULL DEFAULT 0,
`translatable` int(1) NOT NULL DEFAULT 0,
+ `hide_border` int(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `label` (`label`),
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 26760dbcc9..76309e7347 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -63,6 +63,7 @@ CREATE TABLE "tabDocField" (
"precision" varchar(255) DEFAULT NULL,
"length" bigint NOT NULL DEFAULT 0,
"translatable" smallint NOT NULL DEFAULT 0,
+ "hide_border" smallint NOT NULL DEFAULT 0,
PRIMARY KEY ("name")
) ;
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index f2047003fa..512b3f2890 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -21,19 +21,17 @@ class Workspace:
self.extended_charts = []
self.extended_shortcuts = []
- user = frappe.get_user()
- user.build_permissions()
-
- user_doc = frappe.get_doc('User', frappe.session.user)
- self.blocked_modules = user_doc.get_blocked_modules()
+ self.user = frappe.get_user()
+ self.allowed_modules = self.get_cached_value('user_allowed_modules', self.get_allowed_modules)
self.doc = self.get_page_for_user()
- if self.doc.module in self.blocked_modules:
+ if self.doc.module not in self.allowed_modules:
raise frappe.PermissionError
- self.user = user
- self.allowed_pages = get_allowed_pages()
- self.allowed_reports = get_allowed_reports()
+ self.can_read = self.get_cached_value('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
@@ -41,6 +39,31 @@ class Workspace:
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):
+ _cache = frappe.cache()
+
+ value = _cache.get_value(cache_key, user=frappe.session.user)
+ if value:
+ return value
+
+ value = fallback_fn()
+
+ # Expire every six hour
+ _cache.set_value(cache_key, value, frappe.session.user, 21600)
+ return value
+
+ def get_can_read_items(self):
+ if not self.user.can_read:
+ self.user.build_permissions()
+
+ return self.user.can_read
+
+ def get_allowed_modules(self):
+ if not self.user.allow_modules:
+ self.user.build_permissions()
+
+ return self.user.allow_modules
+
def get_page_for_user(self):
filters = {
'extends': self.page_name,
@@ -61,14 +84,14 @@ class Workspace:
if not self.doc.onboarding:
return None
- if frappe.db.get_value("Onboarding", self.doc.onboarding, "is_complete"):
+ if frappe.db.get_value("Module Onboarding", self.doc.onboarding, "is_complete"):
return None
- doc = frappe.get_doc("Onboarding", self.doc.onboarding)
+ doc = frappe.get_doc("Module Onboarding", self.doc.onboarding)
# Check if user is allowed
allowed_roles = set(doc.get_allowed_roles())
- user_roles = set(self.user.get_roles())
+ user_roles = set(frappe.get_roles())
if not allowed_roles & user_roles:
return None
@@ -83,7 +106,7 @@ class Workspace:
"extends": self.page_name,
'restrict_to_domain': ['in', frappe.get_active_domains()],
'for_user': '',
- 'module': ['not in', self.blocked_modules]
+ 'module': ['in', self.allowed_modules]
})
pages = [frappe.get_doc("Desk Page", page['name']) for page in pages]
@@ -97,13 +120,15 @@ class Workspace:
item_type = item_type.lower()
if item_type == "doctype":
- return (name in self.user.can_read and name in self.restricted_doctypes)
+ return (name in self.can_read and name in self.restricted_doctypes)
if item_type == "page":
return (name in self.allowed_pages and name in self.restricted_pages)
if item_type == "report":
return name in self.allowed_reports
if item_type == "help":
return True
+ if item_type == "dashboard":
+ return True
return False
@@ -140,9 +165,9 @@ class Workspace:
default_country = frappe.db.get_default("country")
def _doctype_contains_a_record(name):
- exists = self.table_counts.get(name)
- if not exists:
- if not frappe.db.get_value('DocType', name, 'issingle'):
+ exists = self.table_counts.get(name, None)
+ if exists is None:
+ if not frappe.db.get_value('DocType', name, 'issingle', cache=True):
exists = frappe.db.count(name)
else:
exists = True
@@ -249,6 +274,8 @@ class Workspace:
for doc in self.onboarding_doc.get_steps():
step = doc.as_dict().copy()
step.label = _(doc.title)
+ if step.action == "Create Entry":
+ step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True)
steps.append(step)
return steps
@@ -292,7 +319,6 @@ def get_desk_sidebar_items(flatten=False):
filters = {
'restrict_to_domain': ['in', frappe.get_active_domains()],
'extends_another_page': 0,
- 'is_standard': 1,
'for_user': '',
'module': ['not in', blocked_modules]
}
diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json
index c17bc3235c..c0e2bddcf8 100644
--- a/frappe/desk/doctype/dashboard/dashboard.json
+++ b/frappe/desk/doctype/dashboard/dashboard.json
@@ -9,6 +9,7 @@
"dashboard_name",
"is_default",
"charts",
+ "chart_options",
"cards"
],
"fields": [
@@ -33,6 +34,13 @@
"options": "Dashboard Chart Link",
"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"
+ },
{
"fieldname": "cards",
"fieldtype": "Table",
@@ -41,7 +49,7 @@
}
],
"links": [],
- "modified": "2020-04-19 17:44:36.237163",
+ "modified": "2020-04-29 13:26:37.362482",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index b85e135071..af0c48d9c6 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -5,6 +5,8 @@
from __future__ import unicode_literals
from frappe.model.document import Document
import frappe
+from frappe import _
+import json
class Dashboard(Document):
def on_update(self):
@@ -13,13 +15,29 @@ class Dashboard(Document):
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
+ def validate(self):
+ self.validate_custom_options()
+
+ def validate_custom_options(self):
+ if self.chart_options:
+ try:
+ json.loads(self.chart_options)
+ except ValueError as error:
+ frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
+
@frappe.whitelist()
def get_permitted_charts(dashboard_name):
permitted_charts = []
dashboard = frappe.get_doc('Dashboard', dashboard_name)
for chart in dashboard.charts:
if frappe.has_permission('Dashboard Chart', doc=chart.chart):
- permitted_charts.append(chart)
+ chart_dict = frappe._dict()
+ chart_dict.update(chart.as_dict())
+
+ if dashboard.get('chart_options'):
+ chart_dict.custom_options = dashboard.get('chart_options')
+ permitted_charts.append(chart_dict)
+
return permitted_charts
@frappe.whitelist()
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index f8d5886b26..2ec73cff42 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -49,6 +49,7 @@ frappe.ui.form.on('Dashboard Chart', {
});
frm.set_df_property("filters_section", "hidden", 1);
+ frm.trigger('set_time_series');
frm.set_query('document_type', function() {
return {
filters: {
@@ -57,6 +58,7 @@ frappe.ui.form.on('Dashboard Chart', {
}
});
frm.trigger('update_options');
+ frm.trigger('set_heatmap_year_options');
if (frm.doc.report_name) {
frm.trigger('set_chart_report_filters');
}
@@ -70,7 +72,17 @@ frappe.ui.form.on('Dashboard Chart', {
frm.trigger("show_filters");
},
+ set_heatmap_year_options: function(frm) {
+ if (frm.doc.type == 'Heatmap') {
+ frappe.db.get_doc('System Settings').then(doc => {
+ const creation_date = doc.creation;
+ frm.set_df_property('heatmap_year', 'options', frappe.dashboard_utils.get_years_since_creation(creation_date));
+ });
+ }
+ },
+
chart_type: function(frm) {
+ frm.trigger('set_time_series');
if (frm.doc.chart_type == 'Report') {
frm.set_query('report_name', () => {
return {
@@ -80,23 +92,25 @@ frappe.ui.form.on('Dashboard Chart', {
}
});
} else {
- // set timeseries based on chart type
- if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) {
- frm.set_value('timeseries', 1);
- } else {
- frm.set_value('timeseries', 0);
- }
-
if (frm.doc.chart_type == 'Group By') {
- frm.set_df_property('type', 'options', ['Line', 'Bar', 'Percentage', 'Pie']);
+ frm.set_df_property('type', 'options', ['Line', 'Bar', 'Percentage', 'Pie', 'Donut']);
} else {
- frm.set_df_property('type', 'options', ['Line', 'Bar']);
+ frm.set_df_property('type', 'options', ['Line', 'Bar', 'Heatmap']);
}
frm.set_value('document_type', '');
}
},
+ set_time_series: function(frm) {
+ // set timeseries based on chart type
+ if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) {
+ frm.set_value('timeseries', 1);
+ } else {
+ frm.set_value('timeseries', 0);
+ }
+ },
+
document_type: function(frm) {
// update `based_on` options based on date / datetime fields
frm.set_value('source', '');
@@ -283,17 +297,7 @@ frappe.ui.form.on('Dashboard Chart', {
});
}
} else if (frm.chart_filters.length) {
- fields = frm.chart_filters.filter(f => {
- if (f.on_change && !f.reqd) {
- return false;
- }
- if (f.get_query || f.get_data) {
- f.read_only = 1;
- }
-
- return f.fieldname;
- });
-
+ fields = frm.chart_filters.filter(f => f.fieldname);
fields.map( f => {
if (filters[f.fieldname]) {
let condition = '=';
@@ -353,10 +357,10 @@ frappe.ui.form.on('Dashboard Chart', {
}
dialog.show();
+ //Set query report object so that it can be used while fetching filter values in the report
+ frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
dialog.set_values(filters);
});
},
});
-
-
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index b5201a8b1f..72f5c43316 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -23,17 +23,18 @@
"number_of_groups",
"column_break_6",
"is_public",
+ "heatmap_year",
"timespan",
"from_date",
"to_date",
"time_interval",
"timeseries",
+ "type",
"filters_section",
"filters_json",
"chart_options_section",
- "type",
- "column_break_2",
"color",
+ "column_break_2",
"custom_options",
"section_break_10",
"last_synced_on"
@@ -85,14 +86,14 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "timeseries",
+ "depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'",
"fieldname": "timespan",
"fieldtype": "Select",
"label": "Timespan",
"options": "Last Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range"
},
{
- "depends_on": "timeseries",
+ "depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'",
"fieldname": "time_interval",
"fieldtype": "Select",
"label": "Time Interval",
@@ -100,7 +101,7 @@
},
{
"default": "0",
- "depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
+ "depends_on": "eval: !['Group By', 'Report'].includes(doc.chart_type)\n",
"fieldname": "timeseries",
"fieldtype": "Check",
"label": "Time Series"
@@ -123,10 +124,11 @@
"label": "Chart Options"
},
{
+ "default": "Line",
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
- "options": "Line\nBar\nPercentage\nPie\nDonut",
+ "options": "Line\nBar\nHeatmap",
"reqd": 1
},
{
@@ -134,7 +136,7 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.chart_type !== 'Report'",
+ "depends_on": "eval: doc.chart_type !== 'Report' && doc.type !== 'Heatmap'",
"fieldname": "color",
"fieldtype": "Color",
"label": "Color"
@@ -217,7 +219,7 @@
"options": "Dashboard Chart Field"
},
{
- "description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"]",
+ "description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"] (the options set here will override the chart options set in the Dashboard)",
"fieldname": "custom_options",
"fieldtype": "Code",
"label": "Custom Options"
@@ -228,10 +230,16 @@
"fieldname": "is_public",
"fieldtype": "Check",
"label": "Is Public"
+ },
+ {
+ "depends_on": "eval: doc.type == 'Heatmap'",
+ "fieldname": "heatmap_year",
+ "fieldtype": "Select",
+ "label": "Year"
}
],
"links": [],
- "modified": "2020-05-01 15:22:59.119341",
+ "modified": "2020-05-01 19:45:01.669384",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
@@ -275,4 +283,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 417ef2ba82..7e375e835f 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -8,7 +8,7 @@ from frappe import _
import datetime
import json
from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
-from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime
+from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime, cint
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
from frappe.model.document import Document
@@ -58,13 +58,13 @@ def has_permission(doc, ptype, user):
@frappe.whitelist()
@cache_source
def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
- to_date = None, timespan = None, time_interval = None, refresh = None):
+ to_date = None, timespan = None, time_interval = None, heatmap_year=None, refresh = None):
if chart_name:
chart = frappe.get_doc('Dashboard Chart', chart_name)
else:
chart = frappe._dict(frappe.parse_json(chart))
-
+ heatmap_year = heatmap_year or chart.heatmap_year
timespan = timespan or chart.timespan
if timespan == 'Select Date Range':
@@ -87,7 +87,10 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
if chart.chart_type == 'Group By':
chart_config = get_group_by_chart_config(chart, filters)
else:
- chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date)
+ if chart.type == 'Heatmap':
+ chart_config = get_heatmap_chart_config(chart, filters, heatmap_year)
+ else:
+ chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date)
return chart_config
@@ -174,6 +177,41 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
return chart_config
+def get_heatmap_chart_config(chart, filters, heatmap_year):
+ aggregate_function = get_aggregate_function(chart.chart_type)
+ value_field = chart.value_based_on or '1'
+ doctype = chart.document_type
+ datefield = chart.based_on
+ year = cint(heatmap_year) if heatmap_year else getdate(nowdate()).year
+ year_start_date = datetime.date(year, 1, 1).strftime('%Y-%m-%d')
+ next_year_start_date = datetime.date(year + 1, 1, 1).strftime('%Y-%m-%d')
+
+ filters.append([doctype, datefield, '>', "{date}".format(date=year_start_date), False])
+ filters.append([doctype, datefield, '<', "{date}".format(date=next_year_start_date), False])
+
+ if frappe.db.db_type == 'mariadb':
+ timestamp_field = 'unix_timestamp({datefield})'.format(datefield=datefield)
+ else:
+ timestamp_field = 'extract(epoch from timestamp {datefield})'.format(datefield=datefield)
+
+ data = dict(frappe.db.get_all(
+ doctype,
+ fields = [
+ timestamp_field,
+ '{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field),
+ ],
+ filters = filters,
+ group_by = 'date({datefield})'.format(datefield=datefield),
+ as_list = 1,
+ order_by = '{datefield} asc'.format(datefield=datefield),
+ ignore_ifnull = True
+ ))
+
+ chart_config = {
+ 'labels': [],
+ 'dataPoints': data,
+ }
+ return chart_config
def get_group_by_chart_config(chart, filters):
@@ -397,11 +435,11 @@ class DashboardChart(Document):
def check_document_type(self):
if frappe.get_meta(self.document_type).issingle:
- frappe.throw("You cannot create a dashboard chart from single DocTypes")
+ frappe.throw(_("You cannot create a dashboard chart from single DocTypes"))
def validate_custom_options(self):
if self.custom_options:
try:
json.loads(self.custom_options)
except ValueError as error:
- frappe.throw("Invalid json added in the custom options: %s" % error)
\ No newline at end of file
+ frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
diff --git a/frappe/desk/doctype/desk_page/desk_page.js b/frappe/desk/doctype/desk_page/desk_page.js
index 3087a5f5b8..503859eb61 100644
--- a/frappe/desk/doctype/desk_page/desk_page.js
+++ b/frappe/desk/doctype/desk_page/desk_page.js
@@ -2,16 +2,22 @@
// For license information, please see license.txt
frappe.ui.form.on('Desk Page', {
- setup: function(frm) {
+ refresh: function(frm) {
+ frm.enable_save();
frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
frm.get_field("extends_another_page").toggle(frappe.boot.developer_mode);
- if (!frappe.boot.developer_mode || frm.doc.for_user) {
+ frm.get_field("developer_mode_only").toggle(frappe.boot.developer_mode);
+
+ if (frm.doc.for_user) {
+ frm.set_df_property("extends", "read_only", true);
+ }
+
+ if (frm.doc.for_user || (frm.doc.is_standard && !frappe.boot.developer_mode)) {
frm.trigger('disable_form');
}
},
disable_form: function(frm) {
- frm.set_read_only();
frm.fields
.filter(field => field.has_input)
.forEach(field => {
diff --git a/frappe/desk/doctype/desk_page/desk_page.json b/frappe/desk/doctype/desk_page/desk_page.json
index cb106c5dd4..851eb43b23 100644
--- a/frappe/desk/doctype/desk_page/desk_page.json
+++ b/frappe/desk/doctype/desk_page/desk_page.json
@@ -8,8 +8,8 @@
"engine": "InnoDB",
"field_order": [
"label",
- "extends",
"for_user",
+ "extends",
"module",
"category",
"restrict_to_domain",
@@ -170,7 +170,7 @@
"search_index": 1
},
{
- "depends_on": "eval:doc.extends_another_page == 1",
+ "depends_on": "eval:doc.extends_another_page == 1 || doc.for_user",
"fieldname": "extends",
"fieldtype": "Link",
"in_standard_filter": 1,
@@ -188,11 +188,11 @@
"fieldname": "onboarding",
"fieldtype": "Link",
"label": "Onboarding",
- "options": "Onboarding"
+ "options": "Module Onboarding"
}
],
"links": [],
- "modified": "2020-04-26 12:21:46.205079",
+ "modified": "2020-05-13 19:01:42.041524",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Page",
diff --git a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json b/frappe/desk/doctype/desk_shortcut/desk_shortcut.json
index 9f8990732a..f3fd546a77 100644
--- a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json
+++ b/frappe/desk/doctype/desk_shortcut/desk_shortcut.json
@@ -6,9 +6,9 @@
"engine": "InnoDB",
"field_order": [
"type",
- "label",
- "column_break_4",
"link_to",
+ "column_break_4",
+ "label",
"icon",
"restrict_to_domain",
"section_break_5",
@@ -23,7 +23,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
- "options": "DocType\nReport\nPage",
+ "options": "DocType\nReport\nPage\nDashboard",
"reqd": 1
},
{
@@ -81,13 +81,14 @@
{
"fieldname": "label",
"fieldtype": "Data",
+ "in_list_view": 1,
"label": "Label",
"reqd": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-04-07 19:04:23.645198",
+ "modified": "2020-05-14 16:02:15.420993",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Shortcut",
diff --git a/frappe/desk/doctype/module_onboarding/__init__.py b/frappe/desk/doctype/module_onboarding/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/onboarding/onboarding.js b/frappe/desk/doctype/module_onboarding/module_onboarding.js
similarity index 93%
rename from frappe/desk/doctype/onboarding/onboarding.js
rename to frappe/desk/doctype/module_onboarding/module_onboarding.js
index bed7dbd5de..d95920e2ca 100644
--- a/frappe/desk/doctype/onboarding/onboarding.js
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.js
@@ -1,7 +1,7 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on("Onboarding", {
+frappe.ui.form.on("Module Onboarding", {
refresh: function(frm) {
frappe.boot.developer_mode &&
frm.set_intro(
diff --git a/frappe/desk/doctype/onboarding/onboarding.json b/frappe/desk/doctype/module_onboarding/module_onboarding.json
similarity index 98%
rename from frappe/desk/doctype/onboarding/onboarding.json
rename to frappe/desk/doctype/module_onboarding/module_onboarding.json
index b1d563a9dc..9810e7a15f 100644
--- a/frappe/desk/doctype/onboarding/onboarding.json
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.json
@@ -93,7 +93,7 @@
"modified": "2020-05-01 19:37:21.492405",
"modified_by": "Administrator",
"module": "Desk",
- "name": "Onboarding",
+ "name": "Module Onboarding",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/desk/doctype/onboarding/onboarding.py b/frappe/desk/doctype/module_onboarding/module_onboarding.py
similarity index 89%
rename from frappe/desk/doctype/onboarding/onboarding.py
rename to frappe/desk/doctype/module_onboarding/module_onboarding.py
index c8527d22b6..89160a60f0 100644
--- a/frappe/desk/doctype/onboarding/onboarding.py
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.py
@@ -8,10 +8,10 @@ from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
-class Onboarding(Document):
+class ModuleOnboarding(Document):
def on_update(self):
if frappe.conf.developer_mode:
- export_to_files(record_list=[['Onboarding', self.name]], record_module=self.module)
+ export_to_files(record_list=[['Module Onboarding', self.name]], record_module=self.module)
for step in self.steps:
export_to_files(record_list=[['Onboarding Step', step.step]], record_module=self.module)
diff --git a/frappe/desk/doctype/module_onboarding/test_module_onboarding.py b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
new file mode 100644
index 0000000000..ef305667b1
--- /dev/null
+++ b/frappe/desk/doctype/module_onboarding/test_module_onboarding.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestModuleOnboarding(unittest.TestCase):
+ pass
diff --git a/frappe/desk/doctype/note/note.js b/frappe/desk/doctype/note/note.js
index c237998ccf..5718180b70 100644
--- a/frappe/desk/doctype/note/note.js
+++ b/frappe/desk/doctype/note/note.js
@@ -1,9 +1,9 @@
frappe.ui.form.on("Note", {
refresh: function(frm) {
- if(frm.doc.__islocal) {
+ if (frm.doc.__islocal) {
frm.events.set_editable(frm, true);
} else {
- if(!frm.doc.content) {
+ if (!frm.doc.content) {
frm.doc.content = "
";
}
@@ -18,16 +18,15 @@ frappe.ui.form.on("Note", {
// hide all fields other than content
// no permission
- if(editable && !frm.perm[0].write) return;
+ if (editable && !frm.perm[0].write) return;
// content read_only
- frm.set_df_property("content", "read_only", editable ? 0: 1);
+ frm.set_df_property("content", "read_only", editable ? 0 : 1);
// hide all other fields
$.each(frm.fields_dict, function(fieldname) {
-
- if(fieldname !== "content") {
- frm.set_df_property(fieldname, "hidden", editable ? 0: 1);
+ if (fieldname !== "content") {
+ frm.set_df_property(fieldname, "hidden", editable ? 0 : 1);
}
});
@@ -39,3 +38,16 @@ frappe.ui.form.on("Note", {
frm.is_note_editable = editable;
}
});
+
+frappe.tour['Note'] = [
+ {
+ fieldname: "title",
+ title: "Title of the Note",
+ description: "This is the name by which the note will be saved, you can change this later",
+ },
+ {
+ fieldname: "public",
+ title: "Sets the Note to Public",
+ description: "You can change the visibility of the note with this, setting it to public will allow other users to view it.",
+ },
+];
\ No newline at end of file
diff --git a/frappe/desk/doctype/note/test_note.py b/frappe/desk/doctype/note/test_note.py
index 8d46eaf336..38894a9c3d 100644
--- a/frappe/desk/doctype/note/test_note.py
+++ b/frappe/desk/doctype/note/test_note.py
@@ -20,7 +20,7 @@ class TestNote(unittest.TestCase):
note = self.insert_note()
note.title = 'test note 1'
note.content = '1'
- note.save()
+ note.save(ignore_version=False)
version = frappe.get_doc('Version', dict(docname=note.name))
data = version.get_data()
@@ -33,7 +33,7 @@ class TestNote(unittest.TestCase):
# test add
note.append('seen_by', {'user': 'Administrator'})
- note.save()
+ note.save(ignore_version=False)
version = frappe.get_doc('Version', dict(docname=note.name))
data = version.get_data()
@@ -48,7 +48,7 @@ class TestNote(unittest.TestCase):
# test row change
note.seen_by[0].user = 'Guest'
- note.save()
+ note.save(ignore_version=False)
version = frappe.get_doc('Version', dict(docname=note.name))
data = version.get_data()
@@ -62,7 +62,7 @@ class TestNote(unittest.TestCase):
# test remove
note.seen_by = []
- note.save()
+ note.save(ignore_version=False)
version = frappe.get_doc('Version', dict(docname=note.name))
data = version.get_data()
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.js b/frappe/desk/doctype/onboarding_step/onboarding_step.js
index 3e5d4d4260..793e044d98 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.js
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.js
@@ -25,6 +25,24 @@ frappe.ui.form.on("Onboarding Step", {
}
},
+ action: function(frm) {
+ if (frm.doc.action == "Show Form Tour") {
+ frm.fields_dict.reference_document.set_description(`You need to add the steps in the contoller JS file. For example:
note.js
+
+frappe.tour['Note'] = [
+ {
+ fieldname: "title",
+ title: "Title of the Note",
+ description: "...",
+ }
+];
+
+ `);
+ } else {
+ frm.fields_dict.reference_document.set_description(null);
+ }
+ },
+
disable_form: function(frm) {
frm.set_read_only();
frm.fields
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.json b/frappe/desk/doctype/onboarding_step/onboarding_step.json
index e1035a4343..37d1d63dbe 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.json
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.json
@@ -15,10 +15,16 @@
"action",
"column_break_7",
"reference_document",
+ "show_full_form",
+ "is_single",
"reference_report",
"report_reference_doctype",
"report_type",
"report_description",
+ "path",
+ "callback_title",
+ "callback_message",
+ "validate_action",
"field",
"value_to_validate",
"video_url"
@@ -57,7 +63,7 @@
"fieldname": "action",
"fieldtype": "Select",
"label": "Action",
- "options": "Create Entry\nUpdate Settings\nView Report\nWatch Video",
+ "options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nGo to Page\nWatch Video",
"reqd": 1
},
{
@@ -65,10 +71,11 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\"",
+ "depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"",
"fieldname": "reference_document",
"fieldtype": "Link",
"label": "Reference Document",
+ "mandatory_depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"",
"options": "DocType"
},
{
@@ -83,7 +90,8 @@
"depends_on": "eval:doc.action == \"Watch Video\"",
"fieldname": "video_url",
"fieldtype": "Data",
- "label": "Video URL"
+ "label": "Video URL",
+ "mandatory_depends_on": "eval:doc.action == \"Watch Video\""
},
{
"depends_on": "eval:doc.action == \"View Report\"",
@@ -101,17 +109,19 @@
"label": "Is Skipped"
},
{
- "depends_on": "eval:doc.action == \"Update Settings\"",
+ "depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action",
"fieldname": "field",
"fieldtype": "Select",
- "label": "Field"
+ "label": "Field",
+ "mandatory_depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action"
},
{
- "depends_on": "eval:doc.action == \"Update Settings\"",
+ "depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action",
"description": "Use % for any non empty value.",
"fieldname": "value_to_validate",
"fieldtype": "Data",
- "label": "Value to Validate"
+ "label": "Value to Validate",
+ "mandatory_depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action"
},
{
"depends_on": "eval:doc.action == \"View Report\"",
@@ -127,10 +137,54 @@
"fieldtype": "Data",
"label": "Report Reference Doctype",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"",
+ "fetch_from": "reference_document.issingle",
+ "fieldname": "is_single",
+ "fieldtype": "Check",
+ "label": "Is Single"
+ },
+ {
+ "depends_on": "eval:doc.action == \"Go to Page\"",
+ "description": "Example: #Tree/Account",
+ "fieldname": "path",
+ "fieldtype": "Data",
+ "label": "Path",
+ "mandatory_depends_on": "eval:doc.action == \"Go to Page\""
+ },
+ {
+ "depends_on": "eval:doc.action == \"Go to Page\"",
+ "fieldname": "callback_title",
+ "fieldtype": "Data",
+ "label": "Callback Title"
+ },
+ {
+ "depends_on": "eval:doc.action == \"Go to Page\"",
+ "description": "This will be shown in a modal after routing",
+ "fieldname": "callback_message",
+ "fieldtype": "Small Text",
+ "label": "Callback Message"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:doc.action == \"Update Settings\"",
+ "fieldname": "validate_action",
+ "fieldtype": "Check",
+ "label": "Validate Field"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.action == \"Create Entry\"",
+ "description": "Show full form instead of a quick entry modal",
+ "fieldname": "show_full_form",
+ "fieldtype": "Check",
+ "label": "Show Full Form?"
}
],
"links": [],
- "modified": "2020-05-04 12:53:19.276952",
+ "modified": "2020-05-14 15:10:05.627706",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Step",
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py
index e1cc5dfba4..8086acbb2a 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.py
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py
@@ -10,3 +10,7 @@ class OnboardingStep(Document):
def before_export(self, doc):
doc.is_complete = 0
doc.is_skipped = 0
+
+ def validate(self):
+ if self.action == "Go to Page":
+ self.is_mandatory = 0
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index 109dd25f4f..4a1302788b 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -212,7 +212,10 @@ def get_notification_config():
def get_filters_for(doctype):
'''get open filters for doctype'''
config = get_notification_config()
- return config.get("for_doctype").get(doctype, {})
+ doctype_config = config.get("for_doctype").get(doctype, {})
+ filters = doctype_config if not isinstance(doctype_config, string_types) else None
+
+ return filters
@frappe.whitelist()
@frappe.read_only()
diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py
index 6917ef0426..60e1f3242a 100644
--- a/frappe/desk/page/setup_wizard/install_fixtures.py
+++ b/frappe/desk/page/setup_wizard/install_fixtures.py
@@ -14,6 +14,7 @@ def install():
update_global_search_doctypes()
setup_email_linking()
sync_dashboards()
+ add_unsubscribe()
@frappe.whitelist()
def update_genders():
@@ -37,3 +38,15 @@ def setup_email_linking():
"email_id": "email_linking@example.com",
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
+
+def add_unsubscribe():
+ email_unsubscribe = [
+ {"email": "admin@example.com", "global_unsubscribe": 1},
+ {"email": "guest@example.com", "global_unsubscribe": 1}
+ ]
+
+ for unsubscribe in email_unsubscribe:
+ if not frappe.get_all("Email Unsubscribe", filters=unsubscribe):
+ doc = frappe.new_doc("Email Unsubscribe")
+ doc.update(unsubscribe)
+ doc.insert(ignore_permissions=True)
diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js
index ff1e906cff..c43ff27ba3 100644
--- a/frappe/desk/page/user_profile/user_profile.js
+++ b/frappe/desk/page/user_profile/user_profile.js
@@ -108,21 +108,6 @@ class UserProfile {
});
}
- get_years_since_creation() {
- //Get years since user account created
- this.user_creation = frappe.boot.user.creation;
- let creation_year = this.get_year(this.user_creation);
- let current_year = this.get_year(frappe.datetime.now_date());
- let years_list = [];
- for (var year = current_year; year >= creation_year; year--) {
- years_list.push(year);
- }
- return years_list;
- }
-
- get_year(date_str) {
- return date_str.substring(0, date_str.indexOf('-'));
- }
render_line_chart() {
this.line_chart_filters = [['Energy Point Log', 'user', '=', this.user_id, false]];
@@ -246,8 +231,8 @@ class UserProfile {
create_heatmap_chart_filters() {
let filters = [
{
- label: this.get_year(frappe.datetime.now_date()),
- options: this.get_years_since_creation(),
+ label: frappe.dashboard_utils.get_year(frappe.datetime.now_date()),
+ options: frappe.dashboard_utils.get_years_since_creation(frappe.boot.user.creation),
action: (selected_item) => {
this.update_heatmap_data(frappe.datetime.obj_to_str(selected_item));
}
diff --git a/frappe/email/doctype/document_follow/test_document_follow.py b/frappe/email/doctype/document_follow/test_document_follow.py
index 4f1a8733cc..1208a6c5c1 100644
--- a/frappe/email/doctype/document_follow/test_document_follow.py
+++ b/frappe/email/doctype/document_follow/test_document_follow.py
@@ -14,7 +14,7 @@ class TestDocumentFollow(unittest.TestCase):
event_doc = get_event()
event_doc.description = "This is a test description for sending mail"
- event_doc.save()
+ event_doc.save(ignore_version=False)
doc = document_follow.follow_document("Event", event_doc.name , user.name, force=True)
self.assertEquals(doc.user, user.name)
@@ -45,12 +45,12 @@ def get_event():
return doc
def get_user():
- doc = frappe.new_doc("User")
- doc.email = "test@docsub.com"
- doc.first_name = "Test"
- doc.last_name = "User"
- doc.send_welcome_email = 0
- doc.document_follow_notify = 1
- doc.document_follow_frequency = "Hourly"
- doc.insert()
- return doc
\ No newline at end of file
+ doc = frappe.new_doc("User")
+ doc.email = "test@docsub.com"
+ doc.first_name = "Test"
+ doc.last_name = "User"
+ doc.send_welcome_email = 0
+ doc.document_follow_notify = 1
+ doc.document_follow_frequency = "Hourly"
+ doc.insert()
+ return doc
\ No newline at end of file
diff --git a/frappe/email/doctype/newsletter/newsletter..json b/frappe/email/doctype/newsletter/newsletter..json
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index 719d51c176..01f75be954 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -17,7 +17,7 @@
"subject",
"message",
"send_unsubscribe_link",
- "send_attachements",
+ "send_attachments",
"published",
"route",
"test_the_newsletter",
@@ -73,12 +73,6 @@
"fieldtype": "Check",
"label": "Send Unsubscribe Link"
},
- {
- "default": "0",
- "fieldname": "send_attachements",
- "fieldtype": "Check",
- "label": "Send Attachements"
- },
{
"default": "0",
"fieldname": "published",
@@ -127,6 +121,12 @@
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "send_attachments",
+ "fieldtype": "Check",
+ "label": "Send Attachments"
}
],
"has_web_view": 1,
@@ -135,7 +135,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
- "modified": "2020-03-02 06:26:51.622521",
+ "modified": "2020-05-12 18:09:40.137138",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 2469569892..2dccfbead4 100755
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -67,7 +67,7 @@ class Newsletter(WebsiteGenerator):
frappe.db.auto_commit_on_many_writes = True
attachments = []
- if self.send_attachements:
+ if self.send_attachments:
files = frappe.get_all("File", fields=["name"], filters={"attached_to_doctype": "Newsletter",
"attached_to_name": self.name}, order_by="creation desc")
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 9a1c1fb0b0..5a1181f31e 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -49,6 +49,11 @@ class Redirect(Exception):
class CSRFTokenError(Exception):
http_status_code = 400
+
+class TooManyRequestsError(Exception):
+ http_status_code = 429
+
+
class ImproperDBConfigurationError(Exception):
"""
Used when frappe detects that database or tables are not properly
diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py
index 8611c21720..919c334e51 100644
--- a/frappe/frappeclient.py
+++ b/frappe/frappeclient.py
@@ -15,6 +15,9 @@ class AuthError(Exception):
class SiteExpiredError(Exception):
pass
+class SiteUnreachableError(Exception):
+ pass
+
class FrappeException(Exception):
pass
@@ -53,9 +56,16 @@ class FrappeClient(object):
if r.status_code==200 and r.json().get('message') in ("Logged In", "No App"):
return r.json()
+ elif r.status_code == 502:
+ raise SiteUnreachableError
else:
- if json.loads(r.text).get('exc_type') == "SiteExpiredError":
- raise SiteExpiredError
+ try:
+ error = json.loads(r.text)
+ if error.get('exc_type') == "SiteExpiredError":
+ raise SiteExpiredError
+ except json.decoder.JSONDecodeError:
+ error = r.text
+ print(error)
raise AuthError
def setup_key_authentication_headers(self):
diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py
index 0e28c1306c..5874c79108 100644
--- a/frappe/integrations/doctype/google_contacts/google_contacts.py
+++ b/frappe/integrations/doctype/google_contacts/google_contacts.py
@@ -218,7 +218,7 @@ def insert_contacts_to_google_contacts(doc, method=None):
emailAddresses = [{"value": email_id.email_id} for email_id in doc.email_ids]
try:
- contact = google_contacts.people().createContact(parent='people/me', body={"names": [names],"phoneNumbers": phoneNumbers,
+ contact = google_contacts.people().createContact(body={"names": [names],"phoneNumbers": phoneNumbers,
"emailAddresses": emailAddresses}).execute()
frappe.db.set_value("Contact", doc.name, "google_contacts_id", contact.get("resourceName"))
except HttpError as err:
diff --git a/frappe/integrations/frappe_providers/__init__.py b/frappe/integrations/frappe_providers/__init__.py
new file mode 100644
index 0000000000..0b689478d2
--- /dev/null
+++ b/frappe/integrations/frappe_providers/__init__.py
@@ -0,0 +1,14 @@
+# imports - standard imports
+import sys
+
+# imports - module imports
+from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrator
+
+
+def migrate_to(local_site, frappe_provider):
+ if frappe_provider in ("frappe.cloud", "frappecloud.com"):
+ frappe_provider = "frappecloud.com"
+ return frappecloud_migrator(local_site, frappe_provider)
+ 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
new file mode 100644
index 0000000000..4f33c990f9
--- /dev/null
+++ b/frappe/integrations/frappe_providers/frappecloud.py
@@ -0,0 +1,268 @@
+# imports - standard imports
+import getpass
+import json
+import re
+import sys
+
+# imports - third party imports
+import click
+from html2text import html2text
+import requests
+
+# 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
+
+
+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")
+
+
+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
+
+
+def render_plan_table(plans_list):
+ plans_table = []
+
+ # title row
+ visible_headers = ["name", "cpu_time_per_day"]
+ plans_table.append(["Plan", "CPU Time"])
+
+ # all rows
+ 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)
+
+
+@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
+
+
+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)
+
+
+@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 create_session():
+ # 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! ✅")
+ session.headers.update({"X-Press-Team": username})
+ return session
+ else:
+ print("Authorization Failed with Error Code {}".format(login_sc.status_code))
+
+
+@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
+
+
+@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_upload_response = 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
+ })
+ print("Uploading files ({}/3)".format(x+1), end="\r")
+ 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 frappecloud_migrator(local_site, remote_site):
+ global login_url, upload_url, files_url, options_url, site_exists_url, session
+
+ 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)
+
+ print("Frappe Cloud credentials @ {}".format(remote_site))
+
+ # get credentials + auth user + start session
+ session = create_session()
+
+ if session:
+ # connect to site db
+ frappe.init(site=local_site)
+ frappe.connect()
+
+ # 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)
+ frappe.destroy()
+
+ 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:
+ print("Request failed with error code {}".format(site_creation_request.status_code))
+ reason = html2text(site_creation_request.text)
+ print(reason)
+ sys.exit(1)
+
+ else:
+ sys.exit(1)
diff --git a/frappe/migrate.py b/frappe/migrate.py
index 094abbe099..9ec23d8ae7 100644
--- a/frappe/migrate.py
+++ b/frappe/migrate.py
@@ -5,11 +5,13 @@ from __future__ import unicode_literals
import json
import os
+import sys
import frappe
import frappe.translate
import frappe.modules.patch_handler
import frappe.model.sync
from frappe.utils.fixtures import sync_fixtures
+from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
from frappe.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
@@ -19,6 +21,7 @@ from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.utils import global_search
+
def migrate(verbose=True, rebuild_website=False, skip_failing=False):
'''Migrate all apps to the latest version, will:
- run before migrate hooks
@@ -32,6 +35,19 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
- run after migrate hooks
'''
+ service_status = check_connection(redis_services=["redis_cache"])
+ if False in service_status.values():
+ for service in service_status:
+ if not service_status.get(service, True):
+ print("{} service is not running.".format(service))
+ print("""Cannot run bench migrate without the services running.
+If you are running bench in development mode, make sure that bench is running:
+
+$ bench start
+
+Otherwise, check the server logs and ensure that all the required services are running.""")
+ sys.exit(1)
+
touched_tables_file = frappe.get_site_path('touched_tables.json')
if os.path.exists(touched_tables_file):
os.remove(touched_tables_file)
@@ -67,6 +83,9 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
# add static pages to global search
global_search.update_global_search_for_all_web_pages()
+ # updating installed applications data
+ frappe.get_single('Installed Applications').update_versions()
+
#run after_migrate hooks
for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('after_migrate', app_name=app):
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 65cb6073b7..843cb421fe 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -297,8 +297,7 @@ class Document(BaseDocument):
if ignore_permissions!=None:
self.flags.ignore_permissions = ignore_permissions
- if ignore_version!=None:
- self.flags.ignore_version = ignore_version
+ self.flags.ignore_version = frappe.flags.in_test if ignore_version is None else ignore_version
if self.get("__islocal") or not self.get("name"):
self.insert()
@@ -1339,4 +1338,4 @@ def check_doctype_has_consumers(doctype):
if len(event_consumers) and event_consumers[0]:
return True
- return False
\ No newline at end of file
+ return False
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 7bf93d1968..c8fd1a2ac2 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -443,34 +443,41 @@ class Meta(Document):
def add_doctype_links(self, data):
'''add `links` child table in standard link dashboard format'''
+ dashboard_links = []
+
if hasattr(self, 'links') and self.links:
- if not data.transactions:
- # init groups
- data.transactions = []
- data.non_standard_fieldnames = {}
+ dashboard_links.extend(self.links)
- for link in self.links:
- link.added = False
- for group in data.transactions:
- group = frappe._dict(group)
- # group found
- if link.group and group.label == link.group:
- if link.link_doctype not in group.get('items'):
- group.get('items').append(link.link_doctype)
- link.added = True
+ if frappe.get_all("Custom Link", {"document_type": self.name}):
+ dashboard_links.extend(frappe.get_doc("Custom Link", self.name).links)
- if not link.added:
- # group not found, make a new group
- data.transactions.append(dict(
- label = link.group,
- items = [link.link_doctype]
- ))
+ if not data.transactions:
+ # init groups
+ data.transactions = []
+ data.non_standard_fieldnames = {}
- if link.link_fieldname != data.fieldname:
- if data.fieldname:
- data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
- else:
- data.fieldname = link.link_fieldname
+ for link in dashboard_links:
+ link.added = False
+ for group in data.transactions:
+ group = frappe._dict(group)
+ # group found
+ if link.group and group.label == link.group:
+ if link.link_doctype not in group.get('items'):
+ group.get('items').append(link.link_doctype)
+ link.added = True
+
+ if not link.added:
+ # group not found, make a new group
+ data.transactions.append(dict(
+ label = link.group,
+ items = [link.link_doctype]
+ ))
+
+ if link.link_fieldname != data.fieldname:
+ if data.fieldname:
+ data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
+ else:
+ data.fieldname = link.link_fieldname
def get_row_template(self):
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index 320cc24677..b7d9d4d548 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -51,7 +51,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
("desk", "onboarding_permission"),
("desk", "onboarding_step"),
("desk", "onboarding_step_map"),
- ("desk", "onboarding"),
+ ("desk", "module_onboarding"),
("desk", "desk_card"),
("desk", "desk_chart"),
("desk", "desk_shortcut"),
@@ -85,7 +85,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
'data_migration_mapping', 'data_migration_plan', 'desk_page',
- 'onboarding_step', 'onboarding']
+ 'onboarding_step', 'module_onboarding']
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)
diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py
index cddef4f910..27649b8da9 100644
--- a/frappe/modules/import_file.py
+++ b/frappe/modules/import_file.py
@@ -13,7 +13,7 @@ ignore_values = {
"Print Format": ["disabled"],
"Notification": ["enabled"],
"Print Style": ["disabled"],
- "Onboarding": ['is_complete'],
+ "Module Onboarding": ['is_complete'],
"Onboarding Step": ['is_complete', 'is_skipped']
}
diff --git a/frappe/monitor.py b/frappe/monitor.py
index b056286cf9..6802a59584 100644
--- a/frappe/monitor.py
+++ b/frappe/monitor.py
@@ -81,6 +81,12 @@ class Monitor:
self.data.request.status_code = response.status_code
self.data.request.response_length = int(response.headers.get("Content-Length", 0))
+ if hasattr(frappe.local, "rate_limiter"):
+ limiter = frappe.local.rate_limiter
+ self.data.request.counter = limiter.counter
+ if limiter.rejected:
+ self.data.request.reset = limiter.reset
+
self.store()
except Exception:
traceback.print_exc()
diff --git a/frappe/patches.txt b/frappe/patches.txt
index a086fa6f4a..8ab9418e6c 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -278,3 +278,6 @@ frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
frappe.patches.v13_0.migrate_translation_column_data
frappe.patches.v13_0.set_read_times
frappe.patches.v13_0.remove_web_view
+frappe.patches.v13_0.remove_tailwind_from_page_builder
+frappe.patches.v13_0.rename_onboarding
+frappe.patches.v13_0.email_unsubscribe
diff --git a/frappe/patches/v13_0/email_unsubscribe.py b/frappe/patches/v13_0/email_unsubscribe.py
new file mode 100644
index 0000000000..69ed1be948
--- /dev/null
+++ b/frappe/patches/v13_0/email_unsubscribe.py
@@ -0,0 +1,13 @@
+import frappe
+
+def execute():
+ email_unsubscribe = [
+ {"email": "admin@example.com", "global_unsubscribe": 1},
+ {"email": "guest@example.com", "global_unsubscribe": 1}
+ ]
+
+ for unsubscribe in email_unsubscribe:
+ if not frappe.get_all("Email Unsubscribe", filters=unsubscribe):
+ doc = frappe.new_doc("Email Unsubscribe")
+ doc.update(unsubscribe)
+ doc.insert(ignore_permissions=True)
\ No newline at end of file
diff --git a/frappe/patches/v13_0/remove_tailwind_from_page_builder.py b/frappe/patches/v13_0/remove_tailwind_from_page_builder.py
new file mode 100644
index 0000000000..6e7bf67bac
--- /dev/null
+++ b/frappe/patches/v13_0/remove_tailwind_from_page_builder.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ frappe.reload_doc("website", "doctype", "web_page_block")
+ # remove unused templates
+ frappe.delete_doc("Web Template", "Navbar with Links on Right", force=1)
+ frappe.delete_doc("Web Template", "Footer Horizontal", force=1)
+
diff --git a/frappe/patches/v13_0/rename_onboarding.py b/frappe/patches/v13_0/rename_onboarding.py
new file mode 100644
index 0000000000..c506c6076e
--- /dev/null
+++ b/frappe/patches/v13_0/rename_onboarding.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ if frappe.db.exists("DocType", "Onboarding"):
+ frappe.rename_doc("DocType", "Onboarding", "Module Onboarding", ignore_if_exists=True)
+
diff --git a/frappe/public/build.json b/frappe/public/build.json
index d56907b558..30cb2adf87 100755
--- a/frappe/public/build.json
+++ b/frappe/public/build.json
@@ -1,7 +1,4 @@
{
- "css/tailwind.css": [
- "public/tailwind.css"
- ],
"css/frappe-web-b4.css": [
"public/scss/website.scss",
"public/less/indicator.less"
@@ -112,7 +109,9 @@
"public/less/chat.less",
"public/less/filters.less",
"public/less/social.less",
- "node_modules/frappe-charts/dist/frappe-charts.min.css"
+ "node_modules/frappe-charts/dist/frappe-charts.min.css",
+ "node_modules/driver.js/dist/driver.min.css",
+ "public/less/driver.less"
],
"css/frappe-rtl.css": [
"public/css/bootstrap-rtl.css",
@@ -244,6 +243,7 @@
"public/js/frappe/utils/energy_point_utils.js",
"public/js/frappe/utils/dashboard_utils.js",
"public/js/frappe/ui/chart.js",
+ "public/js/frappe/ui/driver.js",
"public/js/frappe/barcode_scanner/index.js"
],
"css/form.min.css": [
diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js
index f54b9e5cbe..6b723d508c 100644
--- a/frappe/public/js/frappe/chat.js
+++ b/frappe/public/js/frappe/chat.js
@@ -2259,14 +2259,19 @@ class extends Component {
) : null,
h("div","",
h("div", { class: "panel-title" },
- h("div", { class: "cursor-pointer", onclick: () => { frappe.set_route(item.route) }},
+ h("div", { class: "cursor-pointer", onclick: () => {
+ frappe.session.user !== "Guest" ?
+ frappe.set_route(item.route) : null;
+ }},
h(frappe.Chat.Widget.MediaProfile, { ...item })
)
)
),
- h("div", { class: popper ? "col-xs-1" : "col-xs-3" },
+ h("div", { class: popper ? "col-xs-2" : "col-xs-3" },
h("div", { class: "text-right" },
-
+ frappe._.is_mobile() && h(frappe.components.Button, { class: "frappe-chat-close", onclick: props.toggle },
+ h(frappe.components.Octicon, { type: "x" })
+ )
)
)
)
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index a5853d96f5..bad7c877fc 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -464,9 +464,9 @@ frappe.ui.form.Form = class FrappeForm {
}
run_after_load_hook() {
- if (frappe.route_options.after_load) {
- let route_callback = frappe.route_options.after_load;
- delete frappe.route_options.after_load;
+ if (frappe.route_hooks.after_load) {
+ let route_callback = frappe.route_hooks.after_load;
+ delete frappe.route_hooks.after_load;
route_callback(this);
}
@@ -580,9 +580,9 @@ frappe.ui.form.Form = class FrappeForm {
me.script_manager.trigger("after_save");
- if (frappe.route_options.after_save) {
- let route_callback = frappe.route_options.after_save;
- delete frappe.route_options.after_save;
+ if (frappe.route_hooks.after_save) {
+ let route_callback = frappe.route_hooks.after_save;
+ delete frappe.route_hooks.after_save;
route_callback(me);
}
@@ -651,6 +651,12 @@ frappe.ui.form.Form = class FrappeForm {
callback && callback();
me.script_manager.trigger("on_submit")
.then(() => resolve(me));
+ if (frappe.route_hooks.after_submit) {
+ let route_callback = frappe.route_hooks.after_submit;
+ delete frappe.route_hooks.after_submit;
+
+ route_callback(me);
+ }
}
}, btn, () => me.handle_save_fail(btn, on_error), resolve);
});
@@ -1556,6 +1562,41 @@ frappe.ui.form.Form = class FrappeForm {
$el.find('input, select, textarea').focus();
}, 1000);
}
+
+ show_tour(on_finish) {
+ if (!Array.isArray(frappe.tour[this.doctype])) {
+ return;
+ }
+
+ const driver = new frappe.Driver({
+ overlayClickNext: true,
+ keyboardControl: true,
+ nextBtnText: 'Next',
+ prevBtnText: 'Previous',
+ opacity: 0.25,
+ onNext: () => {
+ if (!driver.hasNextStep()) {
+ on_finish && on_finish();
+ }
+ }
+ });
+
+ this.layout.sections.forEach(section => section.collapse(false));
+
+ let steps = frappe.tour[this.doctype].map(step => {
+ let field = this.get_docfield(step.fieldname);
+ return {
+ element: `.frappe-control[title='${step.fieldname}']`,
+ popover: {
+ title: step.title || field.label,
+ description: step.description
+ }
+ };
+ });
+
+ driver.defineSteps(steps);
+ driver.start();
+ }
};
frappe.validated = 0;
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index 5aeb29b1ed..d6106255a0 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -599,12 +599,15 @@ frappe.ui.form.Section = Class.extend({
if(this.df.cssClass) {
this.wrapper.addClass(this.df.cssClass);
}
+ if (this.df.hide_border) {
+ this.wrapper.toggleClass("hide-border", true);
+ }
}
-
// for bc
this.body = $('
').appendTo(this.wrapper);
},
+
make_head: function() {
var me = this;
if(!this.df.collapsible) {
@@ -663,9 +666,11 @@ frappe.ui.form.Section = Class.extend({
}
});
},
+
is_collapsed() {
return this.body.hasClass('hide');
},
+
has_missing_mandatory: function() {
var missing_mandatory = false;
for (var j=0, l=this.fields_list.length; j < l; j++) {
diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js
index 9996389a4e..68444c8a3b 100644
--- a/frappe/public/js/frappe/form/quick_entry.js
+++ b/frappe/public/js/frappe/form/quick_entry.js
@@ -107,7 +107,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
});
this.register_primary_action();
- this.render_edit_in_full_page_link();
+ !this.force && this.render_edit_in_full_page_link();
// ctrl+enter to save
this.dialog.wrapper.keydown(function(e) {
if((e.ctrlKey || e.metaKey) && e.which==13) {
@@ -213,8 +213,15 @@ frappe.ui.form.QuickEntryForm = Class.extend({
me.dialog.doc = r.message;
if (frappe._from_link) {
frappe.ui.form.update_calling_link(me.dialog.doc);
+ } else {
+ if (me.after_insert) {
+ me.after_insert(me.dialog.doc);
+ } else {
+ me.open_form_if_not_list();
+ }
}
- cur_frm.reload_doc();
+
+ cur_frm && cur_frm.reload_doc();
}
});
},
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index b87dad1d36..663850d08c 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -268,6 +268,11 @@ $.extend(frappe.model, {
return frappe.boot.single_types.indexOf(doctype) != -1;
},
+ is_tree: function(doctype) {
+ if (!doctype) return false;
+ return frappe.boot.treeviews.indexOf(doctype) != -1;
+ },
+
can_import: function(doctype, frm) {
// system manager can always import
if(frappe.user_roles.includes("System Manager")) return true;
diff --git a/frappe/public/js/frappe/provide.js b/frappe/public/js/frappe/provide.js
index 1dacc4dd47..d4d0fdffb8 100644
--- a/frappe/public/js/frappe/provide.js
+++ b/frappe/public/js/frappe/provide.js
@@ -35,6 +35,7 @@ frappe.provide('locals.DocType');
// for listviews
frappe.provide("frappe.listview_settings");
+frappe.provide("frappe.tour");
frappe.provide("frappe.listview_parent_route");
// constants
diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js
index 06bd6a3bd9..f3f3285245 100644
--- a/frappe/public/js/frappe/router.js
+++ b/frappe/public/js/frappe/router.js
@@ -12,6 +12,7 @@ frappe.route_history = [];
frappe.view_factory = {};
frappe.view_factories = [];
frappe.route_options = null;
+frappe.route_hooks = {};
frappe.route = function() {
diff --git a/frappe/public/js/frappe/ui/driver.js b/frappe/public/js/frappe/ui/driver.js
new file mode 100644
index 0000000000..98ed49ec05
--- /dev/null
+++ b/frappe/public/js/frappe/ui/driver.js
@@ -0,0 +1,3 @@
+import Driver from 'driver.js';
+
+frappe.Driver = Driver;
\ No newline at end of file
diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js
index 1cdabf23e0..9ff4ade761 100644
--- a/frappe/public/js/frappe/utils/common.js
+++ b/frappe/public/js/frappe/utils/common.js
@@ -276,7 +276,7 @@ frappe.utils.sanitise_redirect = (url) => {
// check for base domain only if the url is absolute
// return true for relative url (except protocol-relative urls)
- return is_absolute(url) ? domain(location.href) !== domain(url) : true;
+ return is_absolute(url) ? domain(location.href) !== domain(url) : false;
}
})();
diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js
index a1628be34a..d1621a3e15 100644
--- a/frappe/public/js/frappe/utils/dashboard_utils.js
+++ b/frappe/public/js/frappe/utils/dashboard_utils.js
@@ -82,5 +82,21 @@ frappe.dashboard_utils = {
).then(settings => {
return settings;
});
+ },
+
+ get_years_since_creation(creation) {
+ //Get years since user account created
+ let creation_year = this.get_year(creation);
+ let current_year = this.get_year(frappe.datetime.now_date());
+ let years_list = [];
+ for (var year = current_year; year >= creation_year; year--) {
+ years_list.push(year);
+ }
+ return years_list;
+ },
+
+ get_year(date_str) {
+ return date_str.substring(0, date_str.indexOf('-'));
}
+
};
\ No newline at end of file
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index 7eff0b8e24..7d2c20c693 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -250,7 +250,8 @@ Object.assign(frappe.utils, {
regExp = /^\w+$/;
break;
case "email":
- regExp = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
+ // from https://emailregex.com/
+ regExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
break;
case "url":
regExp = /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js
index 5956a6310d..51add61f07 100644
--- a/frappe/public/js/frappe/views/desktop/desktop.js
+++ b/frappe/public/js/frappe/views/desktop/desktop.js
@@ -294,7 +294,7 @@ class DesktopPage {
make_charts() {
return frappe.dashboard_utils.get_dashboard_settings().then(settings => {
- let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {};
+ let chart_config = settings.chart_config ? JSON.parse(settings.chart_config): {};
if (this.data.charts.items) {
this.data.charts.items.map(chart => {
chart.chart_settings = chart_config[chart.chart_name] || {};
@@ -306,6 +306,7 @@ class DesktopPage {
container: this.page,
type: "chart",
columns: 1,
+ hidden: Boolean(this.onboarding_widget),
options: {
allow_sorting: this.allow_customization,
allow_create: this.allow_customization,
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index 5105494862..e79e43ae02 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -330,8 +330,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
evaluate_depends_on_value(expression, filter_label) {
let out = null;
- let filters = this.get_filter_values();
- if (filters) {
+ let doc = this.get_filter_values();
+ if (doc) {
if (typeof expression === 'boolean') {
out = expression;
} else if (expression.substr(0, 5) == 'eval:') {
@@ -341,7 +341,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
frappe.throw(__(`Invalid "depends_on" expression set in filter ${filter_label}`));
}
} else {
- var value = filters[expression];
+ var value = doc[expression];
if ($.isArray(value)) {
out = !!value.length;
} else {
diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js
index a8149b9134..7b1205482f 100644
--- a/frappe/public/js/frappe/views/reports/report_utils.js
+++ b/frappe/public/js/frappe/views/reports/report_utils.js
@@ -20,7 +20,7 @@ frappe.report_utils = {
return {
data: {
- labels: labels,
+ labels: labels.length? labels: null,
datasets: datasets
},
truncateLegends: 1,
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index 856061f1f0..f7513611d1 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -1020,7 +1020,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
name: __('Totals Row'),
content: totals[col.id],
format: value => {
- return frappe.format(value, col.docfield, { always_show_decimals: true });
+ return frappe.format(value, col.docfield, { always_show_decimals: true }, data[0]);
}
}
})
diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js
index a50acfcd9d..e5378cf2ab 100644
--- a/frappe/public/js/frappe/widgets/chart_widget.js
+++ b/frappe/public/js/frappe/widgets/chart_widget.js
@@ -40,6 +40,10 @@ export default class ChartWidget extends Widget {
setup_container() {
this.body.empty();
+ if (this.chart_doc.type == 'Heatmap') {
+ this.setup_heatmap_container();
+ }
+
this.loading = $(
`
${__(
"Loading..."
@@ -57,9 +61,16 @@ export default class ChartWidget extends Widget {
this.chart_wrapper = $(`
`);
this.chart_wrapper.appendTo(this.body);
+ this.$heatmap_legend = null;
this.set_chart_title();
}
+ setup_heatmap_container() {
+ this.widget.addClass('heatmap-chart');
+ this.widget.removeClass('full-width').addClass('full-width');
+ this.width = 'Full';
+ }
+
set_summary() {
if (!this.$summary) {
this.$summary = $(`
`).hide();
@@ -104,54 +115,7 @@ export default class ChartWidget extends Widget {
}
render_time_series_filters() {
- let filters = [
- {
- label: this.chart_settings.timespan || this.chart_doc.timespan,
- options: [
- "Select Date Range",
- "Last Year",
- "Last Quarter",
- "Last Month",
- "Last Week"
- ],
- action: selected_item => {
- this.selected_timespan = selected_item;
-
- if (this.selected_timespan === "Select Date Range") {
- this.render_date_range_fields();
- } else {
- this.selected_from_date = null;
- this.selected_to_date = null;
- if (this.date_field_wrapper) {
- this.date_field_wrapper.hide();
-
- // Title maybe hidden becuase of date range fields
- // in half width chart
- this.title_field.show();
- this.head.css('flex-direction', "row");
- }
-
- this.save_chart_config_for_user({
- 'timespan': this.selected_timespan,
- 'from_date': null,
- 'to_date': null
-
- });
- this.fetch_and_update_chart();
- }
- }
- },
- {
- label: this.chart_settings.time_interval || this.chart_doc.time_interval,
- options: ["Yearly", "Quarterly", "Monthly", "Weekly", "Daily"],
- action: selected_item => {
- this.selected_time_interval = selected_item;
- this.save_chart_config_for_user({'time_interval': this.selected_time_interval});
- this.fetch_and_update_chart();
- }
- }
- ];
-
+ let filters = this.get_time_series_filters();
frappe.dashboard_utils.render_chart_filters(
filters,
"chart-actions",
@@ -160,12 +124,77 @@ export default class ChartWidget extends Widget {
);
}
+ get_time_series_filters() {
+ let filters;
+ if (this.chart_doc.type == 'Heatmap') {
+ filters = [{
+ label: this.chart_settings.heatmap_year || this.chart_doc.heatmap_year,
+ options: frappe.dashboard_utils.get_years_since_creation(frappe.boot.user.creation),
+ action: selected_item => {
+ this.selected_heatmap_year = selected_item;
+ this.save_chart_config_for_user({'heatmap_year': this.selected_heatmap_year});
+ this.fetch_and_update_chart();
+ }
+ }];
+ } else {
+ filters = [
+ {
+ label: this.chart_settings.timespan || this.chart_doc.timespan,
+ options: [
+ "Select Date Range",
+ "Last Year",
+ "Last Quarter",
+ "Last Month",
+ "Last Week"
+ ],
+ action: selected_item => {
+ this.selected_timespan = selected_item;
+
+ if (this.selected_timespan === "Select Date Range") {
+ this.render_date_range_fields();
+ } else {
+ this.selected_from_date = null;
+ this.selected_to_date = null;
+ if (this.date_field_wrapper) {
+ this.date_field_wrapper.hide();
+
+ // Title maybe hidden becuase of date range fields
+ // in half width chart
+ this.title_field.show();
+ this.head.css('flex-direction', "row");
+ }
+
+ this.save_chart_config_for_user({
+ 'timespan': this.selected_timespan,
+ 'from_date': null,
+ 'to_date': null
+
+ });
+ this.fetch_and_update_chart();
+ }
+ }
+ },
+ {
+ label: this.chart_settings.time_interval || this.chart_doc.time_interval,
+ options: ["Yearly", "Quarterly", "Monthly", "Weekly", "Daily"],
+ action: selected_item => {
+ this.selected_time_interval = selected_item;
+ this.save_chart_config_for_user({'time_interval': this.selected_time_interval});
+ this.fetch_and_update_chart();
+ }
+ }
+ ];
+ }
+ return filters;
+ }
+
fetch_and_update_chart() {
this.args = {
timespan: this.selected_timespan || this.chart_settings.timespan,
time_interval: this.selected_time_interval || this.chart_settings.time_interval,
from_date: this.selected_from_date || this.chart_settings.from_date,
- to_date: this.selected_to_date || this.chart_settings.to_date
+ to_date: this.selected_to_date || this.chart_settings.to_date,
+ heatmap_year: this.selected_heatmap_year || this.chart_settings.heatmap_year,
};
this.fetch(this.filters, true, this.args).then(data => {
@@ -274,7 +303,7 @@ export default class ChartWidget extends Widget {
},
{
label: __("Reset Chart"),
- action: "action-list",
+ action: "action-reset",
handler: () => {
this.reset_chart();
delete this.dashboard_chart;
@@ -332,15 +361,12 @@ export default class ChartWidget extends Widget {
}
];
} else {
- fields = filters.filter(f => {
- if (f.on_change && !f.reqd) {
- return false;
- }
- if (f.get_query || f.get_data) {
- f.read_only = 1;
- }
- return f.fieldname;
- });
+ fields = filters
+ .filter(df => df.fieldname)
+ .map(df => {
+ Object.assign(df, df.dashboard_config || {});
+ return df;
+ });
}
} else {
fields = [
@@ -384,6 +410,8 @@ export default class ChartWidget extends Widget {
}
dialog.show();
+ //Set query report object so that it can be used while fetching filter values in the report
+ frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
dialog.set_values(this.filters);
}
@@ -391,6 +419,9 @@ export default class ChartWidget extends Widget {
this.save_chart_config_for_user(null, 1);
this.chart_settings = {};
this.filters = null;
+ this.selected_time_interval = null;
+ this.selected_timespan = null;
+ this.selected_heatmap_year = null;
}
save_chart_config_for_user(config, reset=0) {
@@ -458,58 +489,25 @@ export default class ChartWidget extends Widget {
time_interval: args && args.time_interval ? args.time_interval : null,
timespan: args && args.timespan ? args.timespan : null,
from_date: args && args.from_date ? args.from_date : null,
- to_date: args && args.to_date ? args.to_date : null
+ to_date: args && args.to_date ? args.to_date : null,
+ heatmap_year: args && args.heatmap_year ? args.heatmap_year : null,
};
}
return frappe.xcall(method, args);
}
render() {
- const chart_type_map = {
- Line: "line",
- Bar: "bar",
- Percentage: "percentage",
- Pie: "pie",
- Donut: "donut"
- };
-
- let colors = [];
-
- if (this.chart_doc.y_axis.length) {
- this.chart_doc.y_axis.map(field => {
- colors.push(field.color);
- });
- } else if (["Line", "Bar"].includes(this.chart_doc.type)) {
- colors = [this.chart_doc.color || []];
- }
-
- if (!this.data || !this.data.labels.length || !Object.keys(this.data).length) {
+ if (!this.data || !this.data.labels || !Object.keys(this.data).length) {
this.chart_wrapper.hide();
this.loading.hide();
- this.$summary.hide();
+ this.$summary && this.$summary.hide();
this.empty.show();
} else {
this.loading.hide();
this.empty.hide();
this.chart_wrapper.show();
- let chart_args = {
- data: this.data,
- type: chart_type_map[this.chart_doc.type],
- colors: colors,
- height: this.height,
- axisOptions: {
- xIsSeries: this.chart_doc.timeseries,
- shortenYAxisNumbers: 1
- }
- };
-
- if (this.chart_doc.custom_options) {
- let custom_options = JSON.parse(this.chart_doc.custom_options);
- for (let key in custom_options) {
- chart_args[key] = custom_options[key];
- }
- }
+ const chart_args = this.get_chart_args();
if (!this.dashboard_chart) {
this.dashboard_chart = new frappe.Chart(
@@ -519,7 +517,93 @@ export default class ChartWidget extends Widget {
} else {
this.dashboard_chart.update(this.data);
}
+
this.width == "Full" && this.summary && this.set_summary();
+ this.chart_doc.type == 'Heatmap' && this.render_heatmap_legend();
+ }
+ }
+
+ get_chart_args() {
+ let colors = this.get_chart_colors();
+
+ const chart_type_map = {
+ Line: "line",
+ Bar: "bar",
+ Percentage: "percentage",
+ Pie: "pie",
+ Donut: "donut",
+ Heatmap: "heatmap"
+ };
+
+ let chart_args = {
+ data: this.data,
+ type: chart_type_map[this.chart_doc.type],
+ colors: colors,
+ height: this.height,
+ axisOptions: {
+ xIsSeries: this.chart_doc.timeseries,
+ shortenYAxisNumbers: 1
+ }
+ };
+
+ if (this.chart_doc.type == "Heatmap") {
+ const heatmap_year = parseInt(this.selected_heatmap_year || this.chart_settings.heatmap_year || this.chart_doc.heatmap_year);
+ chart_args.data.start = new Date(`${heatmap_year}-01-01`);
+ chart_args.data.end = new Date(`${heatmap_year+1}-01-01`);
+ }
+
+ let set_options = (options) => {
+ let custom_options = JSON.parse(options);
+ for (let key in custom_options) {
+ chart_args[key] = custom_options[key];
+ }
+ };
+
+ if (this.custom_options) {
+ set_options(this.custom_options);
+ }
+
+ if (this.chart_doc.custom_options) {
+ set_options(this.chart_doc.custom_options);
+ }
+
+ return chart_args;
+ }
+
+ get_chart_colors() {
+ let colors = [];
+ if (this.chart_doc.y_axis.length) {
+ this.chart_doc.y_axis.map(field => {
+ colors.push(field.color);
+ });
+ } else if (["Line", "Bar"].includes(this.chart_doc.type)) {
+ colors = [this.chart_doc.color || "light-blue"];
+ } else if (this.chart_doc.type == "Heatmap") {
+ colors = [];
+ }
+
+ return colors;
+ }
+
+ render_heatmap_legend() {
+ if (!this.$heatmap_legend && this.widget.width() > 991) {
+ this.$heatmap_legend =
+ $(`
+
+
+
+
${__("Less")}
+
${__("More")}
+
+
+ `);
+ this.body.append(this.$heatmap_legend);
}
}
@@ -542,6 +626,10 @@ export default class ChartWidget extends Widget {
let saved_filters = this.chart_settings.filters || null;
this.filters =
saved_filters || this.filters || JSON.parse(this.chart_doc.filters_json || "[]");
+
+ if (this.chart_doc.type == 'Heatmap' && !this.chart_doc.heatmap_year) {
+ this.chart_doc.heatmap_year = frappe.dashboard_utils.get_year(frappe.datetime.now_date());
+ }
}
get_settings() {
diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js
index cda17e08bc..77cb8a59c2 100644
--- a/frappe/public/js/frappe/widgets/number_card_widget.js
+++ b/frappe/public/js/frappe/widgets/number_card_widget.js
@@ -119,7 +119,8 @@ export default class NumberCardWidget extends Widget {
get_formatted_number() {
const based_on_df =
frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on);
- const shortened_number = shorten_number(this.number);
+ const default_country = frappe.sys_defaults.country;
+ const shortened_number = shorten_number(this.number, default_country);
let number_parts = shortened_number.split(' ');
const symbol = number_parts[1] || '';
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index 78305edd5d..821824a2d2 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -25,7 +25,7 @@ export default class OnboardingWidget extends Widget {
if (step.is_skipped) {
status = "skipped";
- icon_class = "fa-times-circle-o";
+ icon_class = "fa-check-circle-o";
}
if (step.is_complete) {
@@ -56,9 +56,17 @@ export default class OnboardingWidget extends Widget {
// Setup actions
let actions = {
"Watch Video": () => this.show_video(step),
- "Create Entry": () => this.show_quick_entry(step),
+ "Create Entry": () => {
+ if (step.show_full_form) {
+ this.create_entry(step);
+ } else {
+ this.show_quick_entry(step);
+ }
+ },
+ "Show Form Tour": () => this.show_form_tour(step),
"Update Settings": () => this.update_settings(step),
"View Report": () => this.open_report(step),
+ "Go to Page": () => this.go_to_page(step),
};
$step.find("#title").on("click", actions[step.action]);
@@ -67,6 +75,24 @@ export default class OnboardingWidget extends Widget {
return $step;
}
+ go_to_page(step) {
+ frappe.set_route(step.path).then(() => {
+ if (step.callback_message) {
+ let msg_dialog = frappe.msgprint({
+ message: __(step.callback_message),
+ title: __(step.callback_title),
+ primary_action: {
+ action: () => {
+ msg_dialog.hide();
+ },
+ label: () => __("Continue"),
+ },
+ wide: true,
+ });
+ }
+ });
+ }
+
open_report(step) {
let route = generate_route({
name: step.reference_report,
@@ -74,7 +100,7 @@ export default class OnboardingWidget extends Widget {
is_query_report: ["Query Report", "Script Report"].includes(
step.report_type
),
- doctype: step.report_reference_doctype
+ doctype: step.report_reference_doctype,
});
let current_route = frappe.get_route();
@@ -85,8 +111,10 @@ export default class OnboardingWidget extends Widget {
title: __(step.reference_report),
primary_action: {
action: () => {
+ frappe.set_route(current_route).then(() => {
+ this.mark_complete(step);
+ });
msg_dialog.hide();
- this.mark_complete(step);
},
label: () => __("Continue"),
},
@@ -105,15 +133,48 @@ export default class OnboardingWidget extends Widget {
});
}
+ show_form_tour(step) {
+ let route;
+ if (step.is_single) {
+ route = `Form/${step.reference_document}`;
+ } else {
+ route = `Form/${step.reference_document}/New ${step.reference_document}`;
+ }
+
+ let current_route = frappe.get_route();
+
+ frappe.route_hooks = {};
+ frappe.route_hooks.after_load = (frm) => {
+ frm.show_tour(() => {
+ let msg_dialog = frappe.msgprint({
+ message: __("Let's take you back to onboarding"),
+ title: __("Great Job"),
+ primary_action: {
+ action: () => {
+ frappe.set_route(current_route).then(() => {
+ this.mark_complete(step);
+ });
+ msg_dialog.hide();
+ },
+ label: () => __("Continue"),
+ },
+ });
+ });
+ };
+
+ frappe.set_route(route);
+ }
+
update_settings(step) {
let current_route = frappe.get_route();
- frappe.route_options = {};
- frappe.route_options.after_load = (frm) => {
+ frappe.route_hooks = {};
+ frappe.route_hooks.after_load = (frm) => {
frm.scroll_to_field(step.field);
+ frm.doc.__unsaved = true;
};
- frappe.route_options.after_save = (frm) => {
+ frappe.route_hooks.after_save = (frm) => {
let success = false;
let args = {};
@@ -168,6 +229,44 @@ export default class OnboardingWidget extends Widget {
frappe.set_route("Form", step.reference_document);
}
+ create_entry(step) {
+ let current_route = frappe.get_route();
+
+ frappe.route_hooks = {};
+ let callback = () => {
+ frappe.msgprint({
+ message: __("You're doing great, let's take you back to the onboarding page."),
+ title: __("Good Work 🎉"),
+ primary_action: {
+ action: () => {
+ frappe.set_route(current_route).then(() => {
+ this.mark_complete(step);
+ });
+ },
+ label: __("Continue"),
+ },
+ });
+
+ frappe.msg_dialog.custom_onhide = () => {
+ this.mark_complete(step);
+ };
+ };
+
+ if (step.is_submittable) {
+ frappe.route_hooks.after_save = () => {
+ frappe.msgprint({
+ message: __("Submit this document to complete this step."),
+ title: __("Great")
+ });
+ };
+ frappe.route_hooks.after_submit = callback;
+ } else {
+ frappe.route_hooks.after_save = callback;
+ }
+
+ frappe.set_route(`Form/${step.reference_document}/New ${step.reference_document} 1`);
+ }
+
show_quick_entry(step) {
let current_route = frappe.get_route_str();
frappe.ui.form.make_quick_entry(
@@ -185,7 +284,7 @@ export default class OnboardingWidget extends Widget {
});
},
label: __("Continue"),
- }
+ },
});
frappe.msg_dialog.custom_onhide = () => {
@@ -235,8 +334,10 @@ export default class OnboardingWidget extends Widget {
update_step_status(step, status, value, callback) {
let icon_class = {
is_complete: "fa-check-circle-o",
- is_skipped: "fa-times-circle-o",
+ is_skipped: "fa-check-circle-o",
};
+ // Clear any hooks
+ frappe.route_hooks = {};
frappe
.call("frappe.desk.desktop.update_onboarding_step", {
@@ -358,4 +459,4 @@ export default class OnboardingWidget extends Widget {
});
dismiss.appendTo(this.action_area);
}
-}
\ No newline at end of file
+}
diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js
index 59067bd9a0..c92bdc1b5f 100644
--- a/frappe/public/js/frappe/widgets/utils.js
+++ b/frappe/public/js/frappe/widgets/utils.js
@@ -8,7 +8,9 @@ function generate_route(item) {
if (item.link) {
route = strip(item.link, "#");
} else if (type === "doctype") {
- if (frappe.model.is_single(item.doctype)) {
+ if (frappe.model.is_tree(item.doctype)) {
+ route = "Tree/" + item.doctype;
+ } else if (frappe.model.is_single(item.doctype)) {
route = "Form/" + item.doctype;
} else {
if (item.filters) {
@@ -22,6 +24,8 @@ function generate_route(item) {
route = "List/" + item.doctype + "/Report/" + item.name;
} else if (type === "page") {
route = item.name;
+ } else if (type === "dashboard") {
+ route = "dashboard/" + item.name;
}
route = "#" + route;
@@ -123,19 +127,44 @@ function go_to_list_with_filters(doctype, filters) {
});
}
-function shorten_number(number) {
+function shorten_number(number, country) {
+ country = country || '';
+ const number_system = get_number_system(country);
let x = Math.abs(Math.round(number));
-
- switch (true) {
- case x >= 1.0e+12:
- return Math.round(number/1.0e+12) + " T";
- case x >= 1.0e+9:
- return Math.round(number/1.0e+9) + " B";
- case x >= 1.0e+6:
- return Math.round(number/1.0e+6) + " M";
- default:
- return number.toFixed();
+ for (const map of number_system) {
+ if (x >= map.divisor) {
+ return Math.round(number/map.divisor) + ' ' + map.symbol;
+ }
}
+ return number.toFixed();
+}
+
+function get_number_system(country) {
+ let number_system_map = {
+ 'India':
+ [{
+ divisor: 1.0e+7,
+ symbol: 'Cr'
+ },
+ {
+ divisor: 1.0e+5,
+ symbol: 'Lakh'
+ }],
+ '':
+ [{
+ divisor: 1.0e+12,
+ symbol: 'T'
+ },
+ {
+ divisor: 1.0e+9,
+ symbol: 'B'
+ },
+ {
+ divisor: 1.0e+6,
+ symbol: 'M'
+ }]
+ };
+ return number_system_map[country];
}
export { generate_route, generate_grid, build_summary_item, go_to_list_with_filters, shorten_number };
\ No newline at end of file
diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js
index 31215a40c3..5c44533b37 100644
--- a/frappe/public/js/frappe/widgets/widget_dialog.js
+++ b/frappe/public/js/frappe/widgets/widget_dialog.js
@@ -145,7 +145,7 @@ class ShortcutDialog extends WidgetDialog {
fieldname: "type",
label: "Type",
reqd: 1,
- options: "DocType\nReport\nPage",
+ options: "DocType\nReport\nPage\nDashboard",
onchange: () => {
if (this.dialog.get_value("type") == "DocType") {
this.dialog.fields_dict.link_to.get_query = () => {
diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js
index 8c8dd02968..e82cbc6edf 100644
--- a/frappe/public/js/frappe/widgets/widget_group.js
+++ b/frappe/public/js/frappe/widgets/widget_group.js
@@ -52,6 +52,7 @@ export default class WidgetGroup {
`);
this.widget_area = widget_area;
+ if (this.hidden) this.widget_area.hide();
this.title_area = widget_area.find(".widget-group-title");
this.control_area = widget_area.find(".widget-group-control");
this.body = widget_area.find(".widget-group-body");
@@ -96,7 +97,7 @@ export default class WidgetGroup {
}
customize() {
- this.widget_area.show();
+ if (!this.hidden) this.widget_area.show();
this.widgets_list.forEach((wid) => {
wid.customize(this.options);
});
diff --git a/frappe/public/less/desktop.less b/frappe/public/less/desktop.less
index 1e64533079..eef0b29875 100644
--- a/frappe/public/less/desktop.less
+++ b/frappe/public/less/desktop.less
@@ -293,6 +293,75 @@
}
}
+ &.dashboard-widget-box.heatmap-chart {
+ min-height: 0px;
+ height: 180px;
+
+ .widget-footer {
+ display: none;
+ }
+
+ .widget-control {
+ z-index: 1;
+ }
+
+ .frappe-chart .chart-legend {
+ display: none;
+ }
+
+ .chart-loading-state {
+ height: 160px !important;
+ }
+
+ .widget-body {
+ display: flex;
+ max-height: 100%;
+ margin: auto;
+ margin-top: -15px;
+
+ .chart-container {
+ height: 100%;
+ .frappe-chart {
+ height: 100%;
+ }
+ }
+
+ .heatmap-legend {
+ display: flex;
+ margin: 45px 20px 0 20px;
+
+ .legend-colors {
+ padding-left: 1;
+ padding-left: 15px;
+ list-style: none;
+ }
+
+ li {
+ width: 10px;
+ height: 10px;
+ margin: 5px;
+ }
+
+ .legend-label {
+ color: #555b51;
+ font-size: 11px;
+ margin-left: 15px;
+ line-height: 1.6em;
+ }
+
+ @media (max-width: 991px) {
+ display: none;
+ }
+ }
+ }
+ }
+
+ @media (max-width: 768px) {
+ &.dashboard-widget-box.heatmap-chart {
+ display: none;
+ }
+ }
+
&.onboarding-widget-box {
margin-bottom: 50px;
margin-top: 10px;
diff --git a/frappe/public/less/driver.less b/frappe/public/less/driver.less
new file mode 100644
index 0000000000..d331b92e24
--- /dev/null
+++ b/frappe/public/less/driver.less
@@ -0,0 +1,76 @@
+@import "frappe/public/less/variables.less";
+
+div#driver-popover-item {
+ .driver-popover-footer {
+ display: block;
+ margin-top: 12px;
+
+ button {
+ // Edited
+ padding: 1px 5px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 3px;
+ display: inline-block;
+ margin-bottom: 0;
+ font-weight: normal;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ text-shadow: none !important;
+ -ms-touch-action: manipulation;
+ touch-action: manipulation;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ background-image: none;
+ border: 1px solid transparent;
+ }
+
+ .driver-close-btn {
+ // Edited
+ float: left;
+ color: inherit;
+ background-color: #f0f4f7;
+ border-color: transparent;
+ }
+
+ .driver-navigation-btns {
+ // Edited
+ .driver-prev-btn {
+ color: inherit;
+ background-color: #f0f4f7;
+ border-color: transparent;
+ }
+
+ .driver-next-btn {
+ color: #fff;
+ background-color: #5e64ff;
+ border-color: #444bff;
+ }
+ }
+ }
+ .driver-popover-title {
+ // Edited
+ font: 18px/normal sans-serif;
+ margin: 0 0 5px;
+ font-weight: 500;
+ display: block;
+ position: relative;
+ line-height: 1.5;
+ zoom: 1;
+ }
+ .driver-popover-description {
+ // Edited
+ margin-bottom: 0;
+ font: 12px/normal sans-serif;
+ line-height: 1.5;
+ color: @text-muted;
+ font-weight: 400;
+ zoom: 1;
+ }
+}
+
+
diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less
index 8e43b05122..df0334c14f 100644
--- a/frappe/public/less/form.less
+++ b/frappe/public/less/form.less
@@ -314,11 +314,20 @@ h6.uppercase, .h6.uppercase {
}
}
-.form-section:not(:last-child),
+.hide-border {
+ border-top: none !important;
+ padding-top: 0px;
+}
+
+.form-section:not(:first-child) {
+ border-top: 1px solid @border-color;
+}
+
.form-inner-toolbar {
border-bottom: 1px solid @border-color;
}
+
.empty-section {
display: none !important;
}
diff --git a/frappe/public/scss/base.scss b/frappe/public/scss/base.scss
new file mode 100644
index 0000000000..36a1df55ac
--- /dev/null
+++ b/frappe/public/scss/base.scss
@@ -0,0 +1,43 @@
+html {
+ height: 100%;
+}
+
+body {
+ -webkit-font-smoothing: antialiased;
+ font-size: 16px;
+ color: $body-color;
+}
+
+img {
+ max-width: 100%;
+ height: auto;
+}
+
+h1 {
+ font-size: $font-size-3xl;
+ font-weight: 800;
+ line-height: 1.25;
+ letter-spacing: -0.025em;
+
+ @include media-breakpoint-up(sm) {
+ line-height: 2.5rem;
+ font-size: $font-size-4xl;
+ }
+ @include media-breakpoint-up(xl) {
+ line-height: 1;
+ font-size: $font-size-5xl;
+ }
+}
+
+h2 {
+ font-size: $font-size-xl;
+ font-weight: bold;
+
+ @include media-breakpoint-up(sm) {
+ font-size: $font-size-2xl;
+ }
+ @include media-breakpoint-up(md) {
+ font-size: $font-size-3xl;
+ }
+}
+
diff --git a/frappe/public/scss/markdown.scss b/frappe/public/scss/markdown.scss
new file mode 100644
index 0000000000..440a4cfe88
--- /dev/null
+++ b/frappe/public/scss/markdown.scss
@@ -0,0 +1,117 @@
+.from-markdown {
+ line-height: 1.625;
+
+ > * + * {
+ margin-top: 1rem;
+ }
+
+ > :first-child {
+ margin-top: 0;
+ }
+
+ ul,
+ ol {
+ padding-left: 2.5rem;
+ }
+
+ ul {
+ list-style-type: disc;
+ }
+
+ ol {
+ list-style: decimal;
+ }
+
+ li > * + * {
+ margin-top: 1rem;
+ }
+
+ > ul > * + *,
+ > ol > * + * {
+ margin-top: 1rem;
+ }
+
+ > blockquote {
+ padding: 0.75rem 1rem;
+ font-size: $font-size-sm;
+ font-weight: 500;
+ color: $gray-900;
+ border-left: 4px solid $yellow;
+ background-color: lighten($yellow, 42%);
+ border-top-left-radius: 0.1rem;
+ border-bottom-left-radius: 0.1rem;
+ border-top-right-radius: 0.375rem;
+ border-bottom-right-radius: 0.375rem;
+ margin: 1.5rem 0;
+ }
+
+ blockquote p:last-child {
+ margin-bottom: 0;
+ }
+
+ h1 + p {
+ max-width: 42rem;
+ margin-top: 0.75rem;
+ font-size: $font-size-base;
+ color: $gray-900;
+
+ @include media-breakpoint-up(sm) {
+ margin-top: 1.25rem;
+ font-size: 1.125rem;
+ }
+ @include media-breakpoint-up(md) {
+ font-size: 1.25rem;
+ }
+ }
+
+ h2 {
+ margin-bottom: 1rem;
+ margin-top: 3.5rem;
+ }
+
+ h3 {
+ margin-top: 3rem;
+ margin-bottom: 1rem;
+ font-weight: 600;
+ line-height: 1.25;
+ font-size: $font-size-xl;
+ }
+
+ h4 {
+ margin-top: 2.5rem;
+ margin-bottom: 1rem;
+ font-size: 1.125rem;
+ font-weight: 600;
+ line-height: 1.25;
+ }
+
+ h5 {
+ margin-top: 2rem;
+ margin-bottom: 1rem;
+ font-size: $font-size-base;
+ font-weight: 600;
+ line-height: 1.25;
+ }
+
+ h6 {
+ margin-top: 1.5rem;
+ margin-bottom: 1rem;
+ font-size: $font-size-sm;
+ font-weight: 600;
+ line-height: 1.25;
+ }
+
+ tr > td,
+ tr > th {
+ font-size: $font-size-sm;
+ }
+
+ th:empty {
+ display: none;
+ }
+
+ .screenshot {
+ border: 1px solid $gray-400;
+ border-radius: 0.375rem;
+ }
+}
diff --git a/frappe/public/scss/page-builder.scss b/frappe/public/scss/page-builder.scss
new file mode 100644
index 0000000000..f792209c24
--- /dev/null
+++ b/frappe/public/scss/page-builder.scss
@@ -0,0 +1,252 @@
+.hero-subtitle {
+ @extend .lead;
+ max-width: 42rem;
+}
+
+.section-description {
+ max-width: 56rem;
+ margin-top: 0.5rem;
+ font-size: $font-size-base;
+ color: $gray-900;
+
+ @include media-breakpoint-up(lg) {
+ font-size: $font-size-lg;
+ }
+}
+
+.section-image {
+ margin-top: 2rem;
+ border-radius: 0.75rem;
+ width: 100%;
+}
+
+.section-padding {
+ padding-top: 3rem;
+ padding-bottom: 3rem;
+
+ @include media-breakpoint-up(sm) {
+ padding-top: 5rem;
+ padding-bottom: 5rem;
+ }
+ @include media-breakpoint-up(xl) {
+ padding-top: 8rem;
+ padding-bottom: 8rem;
+ }
+}
+
+.section-padding-top {
+ padding-top: 3rem;
+
+ @include media-breakpoint-up(sm) {
+ padding-top: 5rem;
+ }
+ @include media-breakpoint-up(xl) {
+ padding-top: 8rem;
+ }
+}
+
+.section-padding-bottom {
+ padding-bottom: 3rem;
+
+ @include media-breakpoint-up(sm) {
+ padding-bottom: 5rem;
+ }
+ @include media-breakpoint-up(xl) {
+ padding-bottom: 8rem;
+ }
+}
+
+.hero-with-right-image {
+ position: relative;
+
+ .hero-content {
+ display: flex;
+ align-items: center;
+ }
+
+ .hero-image {
+ width: auto;
+ display: none;
+ object-fit: contain;
+ max-height: 36rem;
+
+ &.contain-image {
+ right: 0;
+ }
+
+ @include media-breakpoint-up(md) {
+ display: block;
+ max-width: 28rem;
+ }
+ @include media-breakpoint-up(lg) {
+ max-width: 32rem;
+ }
+ @include media-breakpoint-up(xl) {
+ max-width: 42rem;
+ }
+ }
+}
+
+.card {
+ .card-title {
+ color: $black;
+ }
+
+ .card-body {
+ color: $gray-900;
+ }
+
+ &:hover {
+ border-color: $gray-600;
+ }
+
+ &.card-sm {
+ .card-body {
+ padding: 1.5rem;
+ }
+
+ .card-title {
+ font-size: $font-size-base;
+ font-weight: 600;
+ }
+
+ .card-text {
+ font-size: $font-size-sm;
+ }
+ }
+ &.card-md {
+ .card-body {
+ padding: 1.75rem;
+ }
+
+ .card-title {
+ font-size: $font-size-lg;
+ font-weight: 600;
+
+ @include media-breakpoint-up(md) {
+ font-size: $font-size-xl;
+ }
+ }
+ .card-text {
+ font-size: $font-size-base;
+ }
+ }
+ &.card-lg {
+ .card-body {
+ padding: 2rem;
+ }
+
+ .card-title {
+ font-size: $font-size-xl;
+ font-weight: bold;
+
+ @include media-breakpoint-up(md) {
+ font-size: $font-size-2xl;
+ }
+ }
+
+ .card-text {
+ font-size: $font-size-base;
+
+ @include media-breakpoint-up(xl) {
+ font-size: $font-size-lg;
+ }
+ }
+ }
+}
+
+.nav-tabs {
+ .nav-link {
+ color: $gray-700;
+ font-weight: 500;
+ border: none;
+ padding: 1rem 0.5rem;
+ margin-right: 2rem;
+
+ &:hover {
+ color: $primary;
+ }
+ }
+
+ .nav-link.active,
+ .nav-item.show .nav-link {
+ color: darken($primary, 5%);
+ background-color: #fff;
+ border-bottom: 2px solid $primary;
+ }
+}
+
+.section-markdown > .from-markdown {
+ max-width: 42rem;
+}
+
+.section-cta {
+ padding: 3rem 2rem;
+ text-align: center;
+ background-color: lighten($primary, 42%);
+ border-radius: 0.75rem;
+
+ @include media-breakpoint-up(sm) {
+ padding-left: 3rem;
+ padding-right: 3rem;
+ }
+ @include media-breakpoint-up(md) {
+ padding-top: 5rem;
+ padding-bottom: 5rem;
+ }
+
+ .title {
+ margin: 0 auto;
+ max-width: 36rem;
+ font-size: $font-size-2xl;
+ font-weight: 800;
+ line-height: 1.25;
+ @include media-breakpoint-up(md) {
+ font-size: $font-size-4xl;
+ }
+ }
+ .subtitle {
+ max-width: 36rem;
+ margin: 0 auto;
+ margin-top: 0.5rem;
+ font-size: $font-size-base;
+ color: $gray-900;
+ @include media-breakpoint-up(md) {
+ font-size: $font-size-lg;
+ }
+ }
+ .description {
+ max-width: 36rem;
+ margin: 0 auto;
+ margin-top: 0.5rem;
+ font-size: $font-size-xs;
+ color: $gray-900;
+ }
+}
+
+.section-cta-container {
+ position: relative;
+ .confetti {
+ position: absolute;
+ width: 1rem;
+ height: 1rem;
+ border-radius: 99999px;
+ }
+ .confetti-1 {
+ top: 0;
+ margin-top: -0.5rem;
+ background-color: #84e1bc;
+ left: 25%;
+ }
+ .confetti-2 {
+ background-color: #fdba8c;
+ top: 66.67%;
+ right: 16.67%;
+ }
+ .confetti-3 {
+ bottom: 0;
+ margin-bottom: -0.5rem;
+ background-color: #f8b4b4;
+ left: 16.67%;
+ }
+}
diff --git a/frappe/public/scss/variables.scss b/frappe/public/scss/variables.scss
index 6ee7cda884..e5f3a47f6f 100644
--- a/frappe/public/scss/variables.scss
+++ b/frappe/public/scss/variables.scss
@@ -8,14 +8,45 @@ $gray-600: #8d99a6 !default;
$gray-700: #495057 !default;
$gray-800: #36414c !default;
$gray-900: #2e3338 !default;
-$primary: #5e64ff !default;
+$primary: #2490ef !default;
$black: #000 !default;
$body-color: $gray-800 !default;
$text-muted: $gray-600 !default;
-$border-color: $gray-200 !default;
+$border-color: $gray-300 !default;
-@import "~bootstrap/scss/functions";
-@import "~bootstrap/scss/variables";
+$font-size-xs: 0.75rem !default;
+$font-size-sm: 0.875rem !default;
+$font-size-base: 1rem !default;
+$font-size-lg: 1.125rem !default;
+$font-size-xl: 1.25rem !default;
+$font-size-2xl: 1.5rem !default;
+$font-size-3xl: 1.875rem !default;
+$font-size-4xl: 2.25rem !default;
+$font-size-5xl: 3rem !default;
+$font-size-6xl: 4rem !default;
+$btn-padding-y-lg: 1rem !default;
+$btn-padding-x-lg: 2.5rem !default;
+$btn-font-size-lg: 1.125rem !default;
+$btn-line-height-lg: 1 !default;
+$btn-border-radius-lg: 0.5rem !default;
+$btn-border-radius: 0.375rem !default;
+$btn-font-size: $font-size-sm;
+$btn-padding-x: 1rem !default;
+$btn-padding-y: 0.5rem !default;
+$btn-font-weight: 500 !default;
+
+$navbar-nav-link-padding-x: 1rem !default;
+$navbar-padding-y: 1rem;
+$card-border-radius: 0.75rem !default;
+$card-spacer-y: 1rem !default;
+
+$dropdown-font-size: $font-size-sm !default;
+$dropdown-border-radius: 0.375rem !default;
+$dropdown-item-padding-y: 0.5rem !default;
+$dropdown-item-padding-x: 0.5rem !default;
+
+@import '~bootstrap/scss/functions';
+@import '~bootstrap/scss/variables';
diff --git a/frappe/public/scss/website-image.scss b/frappe/public/scss/website-image.scss
index 18adae4acc..8c32e821fe 100644
--- a/frappe/public/scss/website-image.scss
+++ b/frappe/public/scss/website-image.scss
@@ -34,7 +34,7 @@ img:after {
display: flex;
justify-content: center;
align-items: center;
- font-size: 3rem;
+ font-size: $font-size-5xl;
color: $gray-300;
background: $light;
}
@@ -85,4 +85,4 @@ img:after {
.object-fit-cover {
object-fit: cover;
-}
\ No newline at end of file
+}
diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss
index 546110bd5c..30781c52c1 100644
--- a/frappe/public/scss/website.scss
+++ b/frappe/public/scss/website.scss
@@ -1,42 +1,71 @@
-@import "variables";
-@import "frappe/public/css/font-awesome";
-@import "~bootstrap/scss/bootstrap";
-@import "multilevel-dropdown";
-@import "website-image";
+@import 'variables';
+@import 'frappe/public/css/font-awesome';
+@import '~bootstrap/scss/bootstrap';
+@import 'base';
+@import 'multilevel-dropdown';
+@import 'website-image';
+@import 'page-builder';
+@import 'markdown';
-html {
- height: 100%;
+.container {
+ padding-left: 1.25rem;
+ padding-right: 1.25rem;
}
-body {
- min-height: 100%;
- display: flex;
- flex-direction: column;
- font-size: 16px;
-
- > div {
- flex: 1 0 auto;
- }
-}
-
-footer {
- flex-shrink: 0;
-}
-
-// make navbar padding consistent with the page
-.navbar {
- padding-left: 0;
- padding-right: 0;
-
+@include media-breakpoint-up(sm) {
.container {
- padding-left: 15px;
- padding-right: 15px;
+ padding-left: 1rem;
+ padding-right: 1rem;
}
}
+@include media-breakpoint-up(md) {
+ .container {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+}
+
+@include media-breakpoint-up(lg) {
+ .container {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+}
+
+@include media-breakpoint-up(xl) {
+ .container {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ }
+}
+
+.navbar-light {
+ border-bottom: 1px solid $border-color;
+}
+
+.navbar-light .navbar-nav .nav-link {
+ color: $gray-900;
+ font-size: $font-size-sm;
+ font-weight: 500;
+
+ &:hover,
+ &:focus, &.active {
+ color: $primary;
+ }
+}
+
+.dropdown-menu {
+ padding: 0.25rem;
+}
+
+.dropdown-item {
+ border-radius: $dropdown-border-radius;
+}
+
.navbar.bg-dark {
.dropdown-menu {
- font-size: .75rem;
+ font-size: 0.75rem;
background-color: $dark;
border-radius: 0;
}
@@ -64,7 +93,6 @@ footer {
}
}
-
.input-dark {
background-color: $dark;
border-color: darken($primary, 40%);
@@ -72,25 +100,21 @@ footer {
}
.breadcrumb {
- padding-left: 0;
- padding-right: 0;
- background-color: white;
+ padding-left: 0;
+ padding-right: 0;
+ background-color: white;
}
a.card {
text-decoration: none;
}
-img {
- max-width: 100%;
-}
-
.hidden {
@extend .d-none;
}
.hide-control {
- @extend .d-none;
+ @extend .d-none;
}
.text-underline {
@@ -101,10 +125,49 @@ img {
color: #d1d8dd !important;
}
+// footer
+
.web-footer {
padding: 5rem 0;
min-height: 140px;
- border-top: 1px solid $border-color;
+}
+
+.footer-logo {
+ width: 5rem;
+ height: 2rem;
+}
+
+.footer-link, .footer-child-item a {
+ font-weight: 500;
+ color: $gray-900;
+
+ &:hover {
+ color: $primary;
+ text-decoration: none;
+ }
+}
+
+.footer-col-left, .footer-col-right {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
+
+.footer-col-right {
+ @include media-breakpoint-up(sm) {
+ text-align: right;
+ }
+}
+
+.footer-col-left .footer-link {
+ margin-right: 1rem;
+}
+
+.footer-col-right .footer-link {
+ margin-right: 1rem;
+ @include media-breakpoint-up(sm) {
+ margin-right: 0;
+ margin-left: 1rem;
+ }
}
.footer-group-label {
@@ -112,41 +175,75 @@ img {
}
.footer-parent-item {
- margin-bottom: 1rem;
+ margin-bottom: 0.5rem;
+}
+
+.footer-info {
+ border-top: 1px solid $border-color;
+ color: $text-muted;
+ font-size: $font-size-sm;
}
.no-underline {
- text-decoration: none !important;
+ text-decoration: none !important;
}
.indicator {
- font-size: inherit;
+ font-size: inherit;
}
h4.modal-title {
- font-size: 1em;
+ font-size: 1em;
}
h5.modal-title {
- margin: 0px !important;
+ margin: 0px !important;
}
-.col-xs-1 { @extend .col-1; }
-.col-xs-2 { @extend .col-2; }
-.col-xs-3 { @extend .col-3; }
-.col-xs-4 { @extend .col-4; }
-.col-xs-5 { @extend .col-5; }
-.col-xs-6 { @extend .col-6; }
-.col-xs-7 { @extend .col-7; }
-.col-xs-8 { @extend .col-8; }
-.col-xs-9 { @extend .col-9; }
-.col-xs-10 { @extend .col-10; }
-.col-xs-11 { @extend .col-11; }
-.col-xs-12 { @extend .col-12; }
+.col-xs-1 {
+ @extend .col-1;
+}
+.col-xs-2 {
+ @extend .col-2;
+}
+.col-xs-3 {
+ @extend .col-3;
+}
+.col-xs-4 {
+ @extend .col-4;
+}
+.col-xs-5 {
+ @extend .col-5;
+}
+.col-xs-6 {
+ @extend .col-6;
+}
+.col-xs-7 {
+ @extend .col-7;
+}
+.col-xs-8 {
+ @extend .col-8;
+}
+.col-xs-9 {
+ @extend .col-9;
+}
+.col-xs-10 {
+ @extend .col-10;
+}
+.col-xs-11 {
+ @extend .col-11;
+}
+.col-xs-12 {
+ @extend .col-12;
+}
-.btn-default { @extend .btn-light; }
+.btn-default {
+ @extend .btn-light;
+}
-.btn-xs { @extend .btn-sm; }
+.btn-xs {
+ @extend .btn-sm;
+}
.hidden-xs {
@extend .d-none;
@@ -171,3 +268,29 @@ h5.modal-title {
.pull-right {
float: right;
}
+
+.btn-primary-light {
+ $primary-light: lighten($primary, 42%);
+ @include button-variant(
+ $background: $primary-light,
+ $border: $primary-light,
+ $hover-background: lighten($primary-light, 1%),
+ $hover-border: $primary-light,
+ $active-background: lighten($primary-light, 1%),
+ $active-border: darken($primary-light, 12.5%)
+ );
+
+ color: darken($primary, 5%);
+ &:hover {
+ color: darken($primary, 5%);
+ }
+}
+
+.image-with-blur {
+ transition: filter 300ms ease-in-out;
+ filter: blur(1.5rem);
+}
+
+.image-loaded {
+ filter: blur(0rem);
+}
diff --git a/frappe/public/tailwind.css b/frappe/public/tailwind.css
deleted file mode 100644
index 89595f95ba..0000000000
--- a/frappe/public/tailwind.css
+++ /dev/null
@@ -1,141 +0,0 @@
-@tailwind base;
-
-html,
-body {
- @apply antialiased;
- @apply text-black;
-}
-
-@tailwind components;
-
-details.hide-summary-arrow summary::-webkit-details-marker {
- display: none;
-}
-
-.from-markdown {
- @apply leading-relaxed;
-
- > * + * {
- @apply mt-4;
- }
-
- > :first-child {
- margin-top: 0;
- }
-
- ul,
- ol {
- @apply pl-10;
- }
-
- ul {
- @apply list-disc;
- }
-
- ol {
- @apply list-decimal;
- }
-
- li > * + * {
- @apply mt-4;
- }
-
- > ul > * + *,
- > ol > * + * {
- @apply mt-4;
- }
-
- > blockquote {
- @apply px-4 py-3 text-sm font-medium text-gray-900 border border-gray-400 rounded-md bg-gray-50;
- }
-
- h1 {
- @apply mt-16 mb-4 text-3xl font-extrabold leading-tight tracking-tight;
- @screen sm {
- @apply text-4xl leading-10;
- }
- @screen xl {
- @apply text-5xl leading-none;
- }
- }
-
- h1 + p {
- @apply max-w-2xl mt-3 text-base text-gray-900;
-
- @screen sm {
- @apply mt-5 text-lg;
- }
- @screen md {
- @apply mt-5 text-xl;
- }
- }
-
- h2 {
- @apply mb-4 text-2xl font-bold leading-tight mt-14;
- }
-
- h3 {
- @apply mt-12 mb-4 text-xl font-semibold leading-tight;
- }
-
- h4 {
- @apply mt-10 mb-4 text-lg font-semibold leading-tight;
- }
-
- h5 {
- @apply mt-8 mb-4 text-base font-semibold leading-tight;
- }
-
- h6 {
- @apply mt-6 mb-4 text-sm font-semibold leading-tight;
- }
-
- > a,
- > p a,
- > ul li a,
- > ol li a {
- @apply border-b border-gray-800;
- &:hover {
- @apply text-gray-700;
- }
- }
-
- table {
- @apply w-full my-8 border-t;
- }
-
- tbody {
- @apply border-t;
- }
-
- tr > td,
- tr > th {
- @apply py-4 pr-6 text-sm leading-6 text-left border-b;
- }
-
- th:empty {
- display: none;
- }
-
- .screenshot {
- @apply border border-gray-400 rounded-md;
- }
-}
-
-@tailwind utilities;
-
-.blur-none {
- filter: blur(0rem);
-}
-
-.blur-sm {
- filter: blur(1rem);
-}
-
-.blur-md {
- filter: blur(1.5rem);
-}
-
-.blur-lg {
- filter: blur(2rem);
-}
diff --git a/frappe/rate_limiter.py b/frappe/rate_limiter.py
new file mode 100644
index 0000000000..e29b2b3061
--- /dev/null
+++ b/frappe/rate_limiter.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+
+from datetime import datetime
+import frappe
+from frappe import _
+from frappe.utils import cint
+from werkzeug.wrappers import Response
+
+
+def apply():
+ rate_limit = frappe.conf.rate_limit
+ if rate_limit:
+ frappe.local.rate_limiter = RateLimiter(rate_limit["limit"], rate_limit["window"])
+ frappe.local.rate_limiter.apply()
+
+
+def update():
+ if hasattr(frappe.local, "rate_limiter"):
+ frappe.local.rate_limiter.update()
+
+
+def respond():
+ if hasattr(frappe.local, "rate_limiter"):
+ return frappe.local.rate_limiter.respond()
+
+
+class RateLimiter:
+ def __init__(self, limit, window):
+ self.limit = int(limit * 1000000)
+ self.window = window
+
+ self.start = datetime.utcnow()
+ timestamp = int(frappe.utils.now_datetime().timestamp())
+
+ self.window_number, self.spent = divmod(timestamp, self.window)
+ self.key = frappe.cache().make_key(f"rate-limit-counter-{self.window_number}")
+ self.counter = cint(frappe.cache().get(self.key))
+ self.remaining = max(self.limit - self.counter, 0)
+ self.reset = self.window - self.spent
+
+ self.end = None
+ self.duration = None
+ self.rejected = False
+
+ def apply(self):
+ if self.counter > self.limit:
+ self.rejected = True
+ self.reject()
+
+ def reject(self):
+ raise frappe.TooManyRequestsError
+
+ def update(self):
+ self.end = datetime.utcnow()
+ self.duration = int((self.end - self.start).total_seconds() * 1000000)
+
+ pipeline = frappe.cache().pipeline()
+ pipeline.incrby(self.key, self.duration)
+ pipeline.expire(self.key, self.window)
+ pipeline.execute()
+
+ def headers(self):
+ headers = {
+ "X-RateLimit-Reset": self.reset,
+ "X-RateLimit-Limit": self.limit,
+ "X-RateLimit-Remaining": self.remaining,
+ }
+ if self.rejected:
+ headers["Retry-After"] = self.reset
+ else:
+ headers["X-RateLimit-Used"] = self.duration
+
+ return headers
+
+ def respond(self):
+ if self.rejected:
+ return Response(_("Too Many Requests"), status=429)
diff --git a/frappe/social/doctype/energy_point_settings/energy_point_settings.py b/frappe/social/doctype/energy_point_settings/energy_point_settings.py
index 737aab587c..7299eef916 100644
--- a/frappe/social/doctype/energy_point_settings/energy_point_settings.py
+++ b/frappe/social/doctype/energy_point_settings/energy_point_settings.py
@@ -12,7 +12,7 @@ class EnergyPointSettings(Document):
pass
def is_energy_point_enabled():
- return frappe.get_cached_value('Energy Point Settings', None, 'enabled')
+ return frappe.db.get_single_value('Energy Point Settings', 'enabled', True)
def allocate_review_points():
settings = frappe.get_single('Energy Point Settings')
diff --git a/frappe/templates/base.html b/frappe/templates/base.html
index dd5dd63a1f..5688ce4fc3 100644
--- a/frappe/templates/base.html
+++ b/frappe/templates/base.html
@@ -25,26 +25,10 @@
{{ head_html or "" }}
{%- endif %}
- {%- if theme.based_on == 'Bootstrap 4' or doctype != 'Web Page' -%}
- {%- if theme.theme_url -%}
-
- {%- else -%}
-
- {%- endif -%}
- {% else %}
- {%- if developer_mode -%}
-
-
- {%- else -%}
-
-
-
- {% endif %}
- {%- if theme.theme_css -%}
-
- {%- endif -%}
+ {%- if theme.name != 'Standard' -%}
+
+ {%- else -%}
+
{%- endif -%}
{%- for link in web_include_css %}
@@ -78,11 +62,7 @@
{%- endblock -%}
{%- block navbar -%}
- {%- if navbar_content -%}
- {{ navbar_content }}
- {%- else -%}
- {% include "templates/includes/navbar/navbar.html" %}
- {%- endif -%}
+ {% include "templates/includes/navbar/navbar.html" %}
{%- endblock -%}
{% block content %}
@@ -90,11 +70,7 @@
{% endblock %}
{%- block footer -%}
- {%- if footer_content -%}
- {{ footer_content }}
- {%- else -%}
- {% include "templates/includes/footer/footer.html" %}
- {%- endif -%}
+ {% include "templates/includes/footer/footer.html" %}
{%- endblock -%}
{% block base_scripts %}
diff --git a/frappe/templates/components/button.html b/frappe/templates/components/button.html
deleted file mode 100644
index d2655b4371..0000000000
--- a/frappe/templates/components/button.html
+++ /dev/null
@@ -1,29 +0,0 @@
----
-name: "button"
-variant: "secondary"
-size: "small"
-disabled: 0
-url: null
----
-
-{%- set static_classes = "border inline-flex justify-center items-center focus:outline-none font-medium transition duration-150 ease-in-out" -%}
-{%- set dynamic_classes = {
- "px-4 py-2 text-sm leading-5 rounded-md": size == "small",
- "px-8 py-3 sm:px-10 sm:py-4 text-base sm:text-lg leading-6 rounded-lg": size == "large",
- "opacity-50 cursor-not-allowed pointer-events-none": disabled,
- "bg-primary-500 border-transparent hover:bg-primary-400 text-white focus:shadow-outline-primary focus:border-primary-600":
- variant == "primary",
- "bg-primary-100 border-transparent text-primary-700 hover:text-primary-600 hover:bg-primary-50 focus:shadow-outline-primary focus:border-primary-300":
- variant == "secondary",
- "bg-red-500 border-transparent hover:bg-red-400 text-white focus:shadow-outline-red focus:border-red-700":
- variant == "danger"
- }
--%}
-{%- set html_tag = "a" if url else "button" -%}
-
-<{{html_tag}}
- class="{{ resolve_class([static_classes, dynamic_classes, class]) }}"
- {{ 'disabled' if disabled else '' }}
- {{ ('href="' + url + '"') if url else '' }}>
- {{ label }}
-{{html_tag}}>
diff --git a/frappe/templates/components/dropdown.html b/frappe/templates/components/dropdown.html
deleted file mode 100644
index f73bdf9304..0000000000
--- a/frappe/templates/components/dropdown.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
- {{ label }}
-
-
-
-
-
diff --git a/frappe/templates/components/navbar_link.html b/frappe/templates/components/navbar_link.html
deleted file mode 100644
index 170247dca5..0000000000
--- a/frappe/templates/components/navbar_link.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
- {{ label }}
-
diff --git a/frappe/templates/components/web_blocks.html b/frappe/templates/components/web_blocks.html
deleted file mode 100644
index 3a9e3c5944..0000000000
--- a/frappe/templates/components/web_blocks.html
+++ /dev/null
@@ -1,3 +0,0 @@
-{%- for web_block in web_blocks -%}
-{{ c('web_block', web_block=web_block, htmltag=htmltag) }}
-{%- endfor -%}
diff --git a/frappe/templates/includes/footer/footer.html b/frappe/templates/includes/footer/footer.html
index 7fe6a955f7..671e928d32 100644
--- a/frappe/templates/includes/footer/footer.html
+++ b/frappe/templates/includes/footer/footer.html
@@ -1,45 +1,46 @@
-{%- if theme.based_on == 'Bootstrap 4' or doctype != 'Web Page' -%}