Merge branch 'develop' of https://github.com/frappe/frappe into custom_append_to
This commit is contained in:
commit
595979eb1f
79 changed files with 1228 additions and 1333 deletions
|
|
@ -14,8 +14,8 @@
|
|||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://travis-ci.org/frappe/frappe">
|
||||
<img src="https://img.shields.io/travis/frappe/frappe.svg?style=flat-square">
|
||||
<a href="https://travis-ci.com/frappe/frappe">
|
||||
<img src="https://travis-ci.com/frappe/frappe.svg?branch=develop">
|
||||
</a>
|
||||
<a href='https://frappe.io/docs'>
|
||||
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ if PY2:
|
|||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '12.1.0'
|
||||
__version__ = '12.0.0-dev'
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ def reopen_closed_assignment(doc):
|
|||
return True
|
||||
|
||||
def apply(doc, method=None, doctype=None, name=None):
|
||||
if frappe.flags.in_patch or frappe.flags.in_install:
|
||||
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard:
|
||||
return
|
||||
|
||||
if not doc and doctype and name:
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ class MilestoneTracker(Document):
|
|||
)).insert(ignore_permissions=True)
|
||||
|
||||
def evaluate_milestone(doc, event):
|
||||
if (frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_setup_wizard):
|
||||
return
|
||||
for d in frappe.cache_manager.get_doctype_map('Milestone Tracker', doc.doctype,
|
||||
dict(document_type = doc.doctype, disabled=0)):
|
||||
frappe.get_doc('Milestone Tracker', d.name).apply(doc)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
|||
|
||||
import frappe, json
|
||||
import frappe.defaults
|
||||
from frappe.model.document import Document
|
||||
from frappe.desk.notifications import (delete_notification_count_for,
|
||||
clear_notifications)
|
||||
|
||||
|
|
@ -22,6 +23,10 @@ user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
|
|||
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
|
||||
"linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map')
|
||||
|
||||
count_cache_blacklist = ["Version", "Tag", "ToDo", "List Filter", "Note Seen By", "Notification Log",
|
||||
"Document Follow", "Communication", "Email Queue", "Deleted Document", "File", "Email Queue Recipient"
|
||||
"Comment", "Has Role", "Attendance", "Route History"]
|
||||
|
||||
|
||||
def clear_user_cache(user=None):
|
||||
cache = frappe.cache()
|
||||
|
|
@ -116,9 +121,23 @@ def clear_doctype_map(doctype, name):
|
|||
cache_key = frappe.scrub(doctype) + '_map'
|
||||
frappe.cache().hdel(cache_key, name)
|
||||
|
||||
def build_table_count_cache(*args, **kwargs):
|
||||
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
|
||||
def build_table_count_cache(doc=None, method=None, *args, **kwargs):
|
||||
if (frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_import
|
||||
or frappe.flags.in_setup_wizard):
|
||||
return
|
||||
|
||||
if doc and isinstance(doc, Document):
|
||||
doctype = doc.doctype
|
||||
|
||||
if doc.meta.istable:
|
||||
return
|
||||
|
||||
if doctype in count_cache_blacklist:
|
||||
return
|
||||
|
||||
_cache = frappe.cache()
|
||||
data = frappe.db.multisql({
|
||||
"mariadb": """
|
||||
|
|
@ -138,7 +157,11 @@ def build_table_count_cache(*args, **kwargs):
|
|||
return counts
|
||||
|
||||
def build_domain_restriced_doctype_cache(*args, **kwargs):
|
||||
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
|
||||
if (frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_import
|
||||
or frappe.flags.in_setup_wizard):
|
||||
return
|
||||
_cache = frappe.cache()
|
||||
active_domains = frappe.get_active_domains()
|
||||
|
|
@ -149,7 +172,11 @@ def build_domain_restriced_doctype_cache(*args, **kwargs):
|
|||
return doctypes
|
||||
|
||||
def build_domain_restriced_page_cache(*args, **kwargs):
|
||||
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
|
||||
if (frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_import
|
||||
or frappe.flags.in_setup_wizard):
|
||||
return
|
||||
_cache = frappe.cache()
|
||||
active_domains = frappe.get_active_domains()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import timeit
|
|||
import frappe
|
||||
from datetime import datetime
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, update_progress_bar, cstr
|
||||
from frappe.utils import cint, flt, update_progress_bar, cstr, DATETIME_FORMAT
|
||||
from frappe.utils.csvutils import read_csv_content
|
||||
from frappe.utils.xlsxutils import (
|
||||
read_xlsx_file_from_attached_file,
|
||||
|
|
@ -345,6 +345,9 @@ class Importer:
|
|||
return columns_with_serial_no, data_with_serial_no
|
||||
|
||||
def parse_value(self, value, df):
|
||||
if isinstance(value, datetime) and df.fieldtype in ["Date", "Datetime"]:
|
||||
return value
|
||||
|
||||
value = cstr(value)
|
||||
|
||||
# convert boolean values to 0 or 1
|
||||
|
|
@ -362,14 +365,13 @@ class Importer:
|
|||
return value
|
||||
|
||||
def parse_date_format(self, value, df):
|
||||
date_format = self.get_date_format_for_df(df)
|
||||
if date_format:
|
||||
try:
|
||||
return datetime.strptime(value, date_format)
|
||||
except:
|
||||
# ignore date values that dont match the format
|
||||
# import will break for these values later
|
||||
pass
|
||||
date_format = self.get_date_format_for_df(df) or DATETIME_FORMAT
|
||||
try:
|
||||
return datetime.strptime(value, date_format)
|
||||
except ValueError:
|
||||
# ignore date values that dont match the format
|
||||
# import will break for these values later
|
||||
pass
|
||||
return value
|
||||
|
||||
def get_date_format_for_df(self, df):
|
||||
|
|
@ -396,7 +398,8 @@ class Importer:
|
|||
date_values = [
|
||||
row[column_index] for row in self.data[:PARSE_ROW_COUNT] if row[column_index]
|
||||
]
|
||||
date_formats = [guess_date_format(d) for d in date_values]
|
||||
date_formats = [guess_date_format(d) if isinstance(d, str) else None
|
||||
for d in date_values]
|
||||
if not date_formats:
|
||||
return
|
||||
max_occurred_date_format = max(set(date_formats), key=date_formats.count)
|
||||
|
|
@ -821,7 +824,11 @@ class Importer:
|
|||
id_fieldname = self.get_id_fieldname(self.doctype)
|
||||
id_value = doc[id_fieldname]
|
||||
existing_doc = frappe.get_doc(self.doctype, id_value)
|
||||
existing_doc.flags.via_data_import = self.data_import.name
|
||||
existing_doc.flags.updater_reference = {
|
||||
'doctype': self.data_import.doctype,
|
||||
'docname': self.data_import.name,
|
||||
'label': _('via Data Import')
|
||||
}
|
||||
existing_doc.update(doc)
|
||||
existing_doc.save()
|
||||
return existing_doc
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-02-22 01:27:33",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -116,7 +117,7 @@
|
|||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -450,8 +451,9 @@
|
|||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"modified": "2019-11-15 12:28:24.461628",
|
||||
"modified_by": "umair@erpnext.com",
|
||||
"links": [],
|
||||
"modified": "2020-03-16 14:49:49.672099",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
"owner": "Administrator",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div>
|
||||
<a href={{{{ route }}}}>{{{{ title }}}}</a>
|
||||
<a href="{{{{ doc.route }}}}">{{{{ doc.title or doc.name }}}}</a>
|
||||
</div>
|
||||
<!-- this is a sample default list template -->
|
||||
<!-- this is a sample default list template -->
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from frappe.modules import make_boilerplate, get_doc_path
|
|||
from frappe.database.schema import validate_column_name, validate_column_length
|
||||
from frappe.model.docfield import supports_translation
|
||||
from frappe.modules.import_file import get_file_path
|
||||
from frappe.model.meta import Meta
|
||||
|
||||
|
||||
class InvalidFieldNameError(frappe.ValidationError): pass
|
||||
|
|
@ -277,7 +278,7 @@ class DocType(Document):
|
|||
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
|
||||
self.delete_duplicate_custom_fields()
|
||||
try:
|
||||
frappe.db.updatedb(self.name, self)
|
||||
frappe.db.updatedb(self.name, Meta(self))
|
||||
except Exception as e:
|
||||
print("\n\nThere was an issue while migrating the DocType: {}\n".format(self.name))
|
||||
raise e
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
"allow_login_using_user_name",
|
||||
"allow_error_traceback",
|
||||
"password_settings",
|
||||
"logout_on_password_reset",
|
||||
"force_user_to_reset_password",
|
||||
"column_break_31",
|
||||
"enable_password_policy",
|
||||
|
|
@ -149,7 +150,7 @@
|
|||
"fieldname": "currency_precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Currency Precision",
|
||||
"options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
|
|
@ -407,12 +408,18 @@
|
|||
"fieldname": "dormant_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Run Jobs only Daily if Inactive For (Days)"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "logout_on_password_reset",
|
||||
"fieldtype": "Check",
|
||||
"label": "Logout All Sessions on Password Reset"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-01-31 06:03:35.595384",
|
||||
"modified": "2020-03-16 14:50:40.914532",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -224,3 +224,4 @@ class TestUser(unittest.TestCase):
|
|||
|
||||
def delete_contact(user):
|
||||
frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)
|
||||
frappe.db.sql("DELETE FROM `tabContact Email` WHERE `email_id`= %s", user)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2014-03-11 14:55:00",
|
||||
|
|
@ -178,7 +179,7 @@
|
|||
{
|
||||
"fieldname": "time_zone",
|
||||
"fieldtype": "Select",
|
||||
"label": "Timezone"
|
||||
"label": "Time Zone"
|
||||
},
|
||||
{
|
||||
"description": "Get your globally recognized avatar from Gravatar.com",
|
||||
|
|
@ -302,7 +303,7 @@
|
|||
"default": "0",
|
||||
"fieldname": "logout_all_sessions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Logout from all devices while changing Password"
|
||||
"label": "Logout From All Devices After Changing Password"
|
||||
},
|
||||
{
|
||||
"fieldname": "reset_password_key",
|
||||
|
|
@ -338,7 +339,7 @@
|
|||
"default": "0",
|
||||
"fieldname": "document_follow_notify",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Notifications for documents followed by me"
|
||||
"label": "Send Notifications For Documents Followed By Me"
|
||||
},
|
||||
{
|
||||
"default": "Daily",
|
||||
|
|
@ -359,7 +360,7 @@
|
|||
"default": "1",
|
||||
"fieldname": "thread_notify",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Notifications for Email threads"
|
||||
"label": "Send Notifications For Email Threads"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
|
@ -496,7 +497,7 @@
|
|||
"description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings",
|
||||
"fieldname": "bypass_restrict_ip_check_if_2fa_enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bypass restricted IP Address check If Two Factor Auth Enabled"
|
||||
"label": "Bypass Restricted IP Address Check If Two Factor Auth Enabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break1",
|
||||
|
|
@ -585,8 +586,9 @@
|
|||
"icon": "fa fa-user",
|
||||
"idx": 413,
|
||||
"image_field": "user_image",
|
||||
"links": [],
|
||||
"max_attachments": 5,
|
||||
"modified": "2019-10-22 14:16:34.810223",
|
||||
"modified": "2020-03-23 22:59:26.154985",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User",
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@ class User(Document):
|
|||
frappe.enqueue(
|
||||
'frappe.core.doctype.user.user.create_contact',
|
||||
user=self,
|
||||
ignore_mandatory=True
|
||||
ignore_mandatory=True,
|
||||
now=frappe.flags.in_test
|
||||
)
|
||||
if self.name not in ('Administrator', 'Guest') and not self.user_image:
|
||||
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
|
||||
|
|
@ -554,7 +555,8 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
|
|||
else:
|
||||
user = res['user']
|
||||
|
||||
_update_password(user, new_password, logout_all_sessions=int(logout_all_sessions))
|
||||
logout_all_sessions = cint(logout_all_sessions) or frappe.db.get_single_value("System Settings", "logout_on_password_reset")
|
||||
_update_password(user, new_password, logout_all_sessions=cint(logout_all_sessions))
|
||||
|
||||
user_doc, redirect_url = reset_user_data(user)
|
||||
|
||||
|
|
@ -1038,8 +1040,8 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
|
|||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||
if user.name in ["Administrator", "Guest"]: return
|
||||
|
||||
contact_exists = get_contact_name(user.email)
|
||||
if not contact_exists:
|
||||
contact_name = get_contact_name(user.email)
|
||||
if not contact_name:
|
||||
contact = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": user.first_name,
|
||||
|
|
@ -1058,7 +1060,7 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
|
|||
contact.add_phone(user.mobile_no, is_primary_mobile_no=True)
|
||||
contact.insert(ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory)
|
||||
else:
|
||||
contact = frappe.get_doc("Contact", contact_exists)
|
||||
contact = frappe.get_doc("Contact", contact_name)
|
||||
contact.first_name = user.first_name
|
||||
contact.last_name = user.last_name
|
||||
contact.gender = user.gender
|
||||
|
|
|
|||
|
|
@ -47,7 +47,11 @@ def get_diff(old, new, for_child=False):
|
|||
|
||||
# capture data import if set
|
||||
data_import = new.flags.via_data_import
|
||||
out = frappe._dict(changed = [], added = [], removed = [], row_changed = [], data_import=data_import)
|
||||
updater_reference = new.flags.updater_reference
|
||||
|
||||
out = frappe._dict(changed = [], added = [], removed = [],
|
||||
row_changed = [], data_import=data_import, updater_reference=updater_reference)
|
||||
|
||||
for df in new.meta.fields:
|
||||
if df.fieldtype in no_value_fields and df.fieldtype not in table_fields:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
.chart-wrapper {
|
||||
border: 1px solid #d1d8dd;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.frappe-chart > text.title {
|
||||
margin: 0px;
|
||||
font-size: 14px !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chart-loading-state, .chart-empty-state {
|
||||
height: 100%;
|
||||
margin-top: 160px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-actions {
|
||||
position: relative;
|
||||
right: 0px;
|
||||
top: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.filter-chart {
|
||||
position: relative;
|
||||
right: 5px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.dashboard-date-field {
|
||||
width: 14%;
|
||||
height: 0;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.chart-column-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.last-synced-text {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 50px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dashboard-graph {
|
||||
padding-top: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ class Dashboard {
|
|||
constructor(wrapper) {
|
||||
this.wrapper = $(wrapper);
|
||||
$(`<div class="dashboard">
|
||||
<div class="dashboard-graph row"></div>
|
||||
<div class="dashboard-graph"></div>
|
||||
</div>`).appendTo(this.wrapper.find(".page-content").empty());
|
||||
this.container = this.wrapper.find(".dashboard-graph");
|
||||
this.page = wrapper.page;
|
||||
|
|
@ -78,16 +78,22 @@ class Dashboard {
|
|||
refresh() {
|
||||
this.get_dashboard_doc().then((doc) => {
|
||||
this.dashboard_doc = doc;
|
||||
this.charts = this.dashboard_doc.charts;
|
||||
this.charts = this.dashboard_doc.charts
|
||||
.map(chart => {
|
||||
return {
|
||||
chart_name: chart.chart,
|
||||
label: chart.chart,
|
||||
...chart
|
||||
}
|
||||
});
|
||||
|
||||
this.charts.map((chart) => {
|
||||
let chart_container = $("<div></div>");
|
||||
chart_container.appendTo(this.container);
|
||||
|
||||
frappe.model.with_doc("Dashboard Chart", chart.chart).then( chart_doc => {
|
||||
let dashboard_chart = new frappe.ui.DashboardChart(chart_doc, chart_container);
|
||||
dashboard_chart.show();
|
||||
});
|
||||
this.chart_group = new frappe.widget.WidgetGroup({
|
||||
title: null,
|
||||
container: this.container,
|
||||
type: "chart",
|
||||
columns: 2,
|
||||
allow_sorting: false,
|
||||
widgets: this.charts,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@
|
|||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
|
|
@ -376,7 +376,7 @@
|
|||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-02-01 11:50:09.222967",
|
||||
"modified": "2020-03-16 14:52:43.954709",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ frappe.ui.form.on("Customize Form", {
|
|||
if(frm.doc.doc_type) {
|
||||
frappe.customize_form.set_primary_action(frm);
|
||||
|
||||
frm.add_custom_button(__('Go to {0} List', [frm.doc.doc_type]), function() {
|
||||
frappe.set_route('List', frm.doc.doc_type);
|
||||
});
|
||||
|
||||
frm.add_custom_button(__('Refresh Form'), function() {
|
||||
frm.script_manager.trigger("doc_type");
|
||||
}, "fa fa-refresh", "btn-default");
|
||||
|
|
|
|||
|
|
@ -169,12 +169,11 @@ class CustomizeForm(Document):
|
|||
|
||||
self.flags.update_db = False
|
||||
self.flags.rebuild_doctype_for_global_search = False
|
||||
|
||||
check_email_append_to(self)
|
||||
self.set_property_setters()
|
||||
self.update_custom_fields()
|
||||
self.set_name_translation()
|
||||
validate_fields_for_doctype(self.doc_type)
|
||||
check_email_append_to(self)
|
||||
|
||||
if self.flags.update_db:
|
||||
frappe.db.updatedb(self.doc_type)
|
||||
|
|
@ -366,13 +365,49 @@ class CustomizeForm(Document):
|
|||
|
||||
def validate_fieldtype_change(self, df, old_value, new_value):
|
||||
allowed = False
|
||||
self.check_length_for_fieldtypes = []
|
||||
for allowed_changes in allowed_fieldtype_change:
|
||||
if (old_value in allowed_changes and new_value in allowed_changes):
|
||||
allowed = True
|
||||
if frappe.db.type_map.get(old_value)[1] > frappe.db.type_map.get(new_value)[1]:
|
||||
self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value})
|
||||
self.validate_fieldtype_length()
|
||||
else:
|
||||
self.flags.update_db = True
|
||||
break
|
||||
if not allowed:
|
||||
frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx))
|
||||
|
||||
def validate_fieldtype_length(self):
|
||||
for field in self.check_length_for_fieldtypes:
|
||||
df = field.get('df')
|
||||
max_length = frappe.db.type_map.get(df.fieldtype)[1]
|
||||
fieldname = df.fieldname
|
||||
docs = frappe.db.sql('''
|
||||
SELECT name, {fieldname}, LENGTH({fieldname}) AS len
|
||||
FROM `tab{doctype}`
|
||||
WHERE LENGTH({fieldname}) > {max_length}
|
||||
'''.format(
|
||||
fieldname=fieldname,
|
||||
doctype=self.doc_type,
|
||||
max_length=max_length
|
||||
), as_dict=True)
|
||||
links = []
|
||||
label = df.label
|
||||
for doc in docs:
|
||||
links.append(frappe.utils.get_link_to_form(self.doc_type, doc.name))
|
||||
links_str = ', '.join(links)
|
||||
|
||||
if docs:
|
||||
frappe.throw(_('Value for field {0} is too long in {1}. Length should be lesser than {2} characters')
|
||||
.format(
|
||||
frappe.bold(label),
|
||||
links_str,
|
||||
frappe.bold(max_length)
|
||||
), title=_('Data Too Long'), is_minimizable=len(docs) > 1)
|
||||
|
||||
self.flags.update_db = True
|
||||
|
||||
def reset_to_defaults(self):
|
||||
if not self.doc_type:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@
|
|||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
|
||||
|
|
@ -385,7 +385,7 @@
|
|||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2019-12-27 12:50:51.419763",
|
||||
"modified": "2020-03-16 14:53:40.619043",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class Database(object):
|
|||
|
||||
def __init__(self, host=None, user=None, password=None, ac_name=None, use_default=0, port=None):
|
||||
self.setup_type_map()
|
||||
self.host = host or frappe.conf.db_host or 'localhost'
|
||||
self.host = host or frappe.conf.db_host or '127.0.0.1'
|
||||
self.port = port or frappe.conf.db_port or ''
|
||||
self.user = user or frappe.conf.db_name
|
||||
self.db_name = frappe.conf.db_name
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class PostgresDatabase(Database):
|
|||
|
||||
# pylint: disable=W0221
|
||||
def sql(self, *args, **kwargs):
|
||||
if len(args):
|
||||
if args:
|
||||
# since tuple is immutable
|
||||
args = list(args)
|
||||
args[0] = modify_query(args[0])
|
||||
|
|
@ -276,13 +276,13 @@ class PostgresDatabase(Database):
|
|||
# pylint: disable=W1401
|
||||
return self.sql('''
|
||||
SELECT a.column_name AS name,
|
||||
CASE a.data_type
|
||||
CASE LOWER(a.data_type)
|
||||
WHEN 'character varying' THEN CONCAT('varchar(', a.character_maximum_length ,')')
|
||||
WHEN 'timestamp without TIME zone' THEN 'timestamp'
|
||||
WHEN 'timestamp without time zone' THEN 'timestamp'
|
||||
ELSE a.data_type
|
||||
END AS type,
|
||||
COUNT(b.indexdef) AS Index,
|
||||
COALESCE(a.column_default, NULL) AS default,
|
||||
SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default,
|
||||
BOOL_OR(b.unique) AS unique
|
||||
FROM information_schema.columns a
|
||||
LEFT JOIN
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class DBTable:
|
|||
def __init__(self, doctype, meta=None):
|
||||
self.doctype = doctype
|
||||
self.table_name = 'tab{}'.format(doctype)
|
||||
self.meta = meta or frappe.get_meta(doctype)
|
||||
self.meta = meta or frappe.get_meta(doctype, False)
|
||||
self.columns = {}
|
||||
self.current_columns = {}
|
||||
|
||||
|
|
@ -65,64 +65,35 @@ class DBTable:
|
|||
"""
|
||||
get columns from docfields and custom fields
|
||||
"""
|
||||
fl = frappe.db.sql("SELECT * FROM `tabDocField` WHERE parent = %s", self.doctype, as_dict = 1)
|
||||
lengths = {}
|
||||
precisions = {}
|
||||
uniques = {}
|
||||
fields = self.meta.get_fieldnames_with_value(True)
|
||||
|
||||
# optional fields like _comments
|
||||
if not self.meta.istable:
|
||||
if not self.meta.get('istable'):
|
||||
for fieldname in frappe.db.OPTIONAL_COLUMNS:
|
||||
fl.append({
|
||||
fields.append({
|
||||
"fieldname": fieldname,
|
||||
"fieldtype": "Text"
|
||||
})
|
||||
|
||||
# add _seen column if track_seen
|
||||
if getattr(self.meta, 'track_seen', False):
|
||||
fl.append({
|
||||
if self.meta.get('track_seen'):
|
||||
fields.append({
|
||||
'fieldname': '_seen',
|
||||
'fieldtype': 'Text'
|
||||
})
|
||||
|
||||
if (not frappe.flags.in_install_db
|
||||
and (frappe.flags.in_install != "frappe"
|
||||
or frappe.flags.ignore_in_install)):
|
||||
custom_fl = frappe.db.sql("""
|
||||
SELECT * FROM `tabCustom Field`
|
||||
WHERE dt = %s AND docstatus < 2
|
||||
""", (self.doctype,), as_dict=1)
|
||||
if custom_fl: fl += custom_fl
|
||||
|
||||
# apply length, precision and unique from property setters
|
||||
for ps in frappe.get_all("Property Setter",
|
||||
fields=["field_name", "property", "value"],
|
||||
filters={
|
||||
"doc_type": self.doctype,
|
||||
"doctype_or_field": "DocField",
|
||||
"property": ["in", ["precision", "length", "unique"]]
|
||||
}):
|
||||
|
||||
if ps.property=="length":
|
||||
lengths[ps.field_name] = cint(ps.value)
|
||||
|
||||
elif ps.property=="precision":
|
||||
precisions[ps.field_name] = cint(ps.value)
|
||||
|
||||
elif ps.property=="unique":
|
||||
uniques[ps.field_name] = cint(ps.value)
|
||||
|
||||
for f in fl:
|
||||
self.columns[f['fieldname']] = DbColumn(self,
|
||||
f['fieldname'],
|
||||
f['fieldtype'],
|
||||
lengths.get(f["fieldname"]) or f.get('length'),
|
||||
f.get('default'),
|
||||
f.get('search_index'),
|
||||
f.get('options'),
|
||||
uniques.get(f["fieldname"],
|
||||
f.get('unique')),
|
||||
precisions.get(f['fieldname']) or f.get('precision'))
|
||||
for field in fields:
|
||||
self.columns[field.get('fieldname')] = DbColumn(
|
||||
self,
|
||||
field.get('fieldname'),
|
||||
field.get('fieldtype'),
|
||||
field.get('length'),
|
||||
field.get('default'),
|
||||
field.get('search_index'),
|
||||
field.get('options'),
|
||||
field.get('unique'),
|
||||
field.get('precision')
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
"""Check if change in varchar length isn't truncating the columns"""
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import frappe
|
|||
import json
|
||||
from frappe import _, DoesNotExistError
|
||||
from frappe.boot import get_allowed_pages, get_allowed_reports
|
||||
from six import string_types
|
||||
from frappe.cache_manager import build_domain_restriced_doctype_cache, build_domain_restriced_page_cache, build_table_count_cache
|
||||
|
||||
class Workspace:
|
||||
|
|
@ -24,7 +25,7 @@ class Workspace:
|
|||
self.allowed_pages = get_allowed_pages()
|
||||
self.allowed_reports = get_allowed_reports()
|
||||
|
||||
self.table_counts = build_table_count_cache()
|
||||
self.table_counts = get_table_with_counts()
|
||||
self.restricted_doctypes = build_domain_restriced_doctype_cache()
|
||||
self.restricted_pages = build_domain_restriced_page_cache()
|
||||
|
||||
|
|
@ -109,7 +110,7 @@ class Workspace:
|
|||
new_data = []
|
||||
for section in cards:
|
||||
new_items = []
|
||||
if isinstance(section.links, str):
|
||||
if isinstance(section.links, string_types):
|
||||
links = json.loads(section.links)
|
||||
else:
|
||||
links = section.links
|
||||
|
|
@ -138,12 +139,17 @@ class Workspace:
|
|||
return new_data
|
||||
|
||||
def get_charts(self):
|
||||
all_charts = []
|
||||
if frappe.has_permission("Dashboard Chart", throw=False):
|
||||
charts = self.doc.charts
|
||||
if len(self.extended_charts):
|
||||
charts = charts + self.extended_charts
|
||||
return [chart for chart in charts]
|
||||
return []
|
||||
|
||||
for chart in charts:
|
||||
chart.label = chart.label if chart.label else chart.chart_name
|
||||
all_charts.append(chart)
|
||||
|
||||
return all_charts
|
||||
|
||||
def get_shortcuts(self):
|
||||
|
||||
|
|
|
|||
|
|
@ -28,14 +28,14 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard',
|
||||
{args: values}
|
||||
).then(()=> {
|
||||
let dashboard_route_html =
|
||||
let dashboard_route_html =
|
||||
`<a href = "#dashboard/${values.dashboard}">${values.dashboard}</a>`;
|
||||
let message =
|
||||
let message =
|
||||
__(`Dashboard Chart ${values.chart_name} add to Dashboard ` + dashboard_route_html);
|
||||
|
||||
frappe.msgprint(message);
|
||||
});
|
||||
|
||||
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
|
|
@ -119,15 +119,13 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
frm.trigger('set_chart_field_options');
|
||||
} else {
|
||||
frappe.report_utils.get_report_filters(report_name).then(filters => {
|
||||
frappe.after_ajax(()=> {
|
||||
if (filters) {
|
||||
frm.chart_filters = filters;
|
||||
let filter_values = frappe.report_utils.get_filter_values(filters);
|
||||
frm.set_value('filters_json', JSON.stringify(filter_values));
|
||||
}
|
||||
frm.trigger('show_filters');
|
||||
frm.trigger('set_chart_field_options');
|
||||
});
|
||||
if (filters) {
|
||||
frm.chart_filters = filters;
|
||||
let filter_values = frappe.report_utils.get_filter_values(filters);
|
||||
frm.set_value('filters_json', JSON.stringify(filter_values));
|
||||
}
|
||||
frm.trigger('show_filters');
|
||||
frm.trigger('set_chart_field_options');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +138,8 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
'frappe.desk.query_report.run',
|
||||
{
|
||||
report_name: frm.doc.report_name,
|
||||
filters: filters
|
||||
filters: filters,
|
||||
ignore_prepared_report: 1
|
||||
}
|
||||
).then(data => {
|
||||
frm.report_data = data;
|
||||
|
|
@ -228,13 +227,11 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
show_filters: function(frm) {
|
||||
frm.chart_filters = [];
|
||||
frappe.dashboard_utils.get_filters_for_chart_type(frm.doc).then(filters => {
|
||||
frappe.after_ajax(() => {
|
||||
if (filters) {
|
||||
frm.chart_filters = filters;
|
||||
}
|
||||
|
||||
frm.trigger('render_filters_table');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -269,7 +266,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
|
||||
if (filters.length > 0) {
|
||||
filters.forEach( filter => {
|
||||
const filter_row =
|
||||
const filter_row =
|
||||
$(`<tr>
|
||||
<td>${filter[1]}</td>
|
||||
<td>${filter[2] || ""}</td>
|
||||
|
|
@ -295,7 +292,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
fields.map( f => {
|
||||
if (filters[f.fieldname]) {
|
||||
let condition = '=';
|
||||
const filter_row =
|
||||
const filter_row =
|
||||
$(`<tr>
|
||||
<td>${f.label}</td>
|
||||
<td>${condition}</td>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@
|
|||
"filters_json",
|
||||
"chart_options_section",
|
||||
"type",
|
||||
"width",
|
||||
"column_break_2",
|
||||
"color",
|
||||
"section_break_10",
|
||||
|
|
@ -127,13 +126,6 @@
|
|||
"options": "Line\nBar\nPercentage\nPie",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Select",
|
||||
"label": "Width",
|
||||
"options": "Half\nFull",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
|
|
@ -223,7 +215,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-03-01 22:08:47.135523",
|
||||
"modified": "2020-03-13 19:19:37.162771",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Chart",
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ def get_week_ending(date):
|
|||
if week_of_the_year == 52:
|
||||
date = add_to_date(date, years=1)
|
||||
# first day of next week
|
||||
date = add_to_date('{}-01-01'.format(date.year), weeks = (week_of_the_year + 1)%52)
|
||||
date = add_to_date('{}-01-01'.format(date.year), weeks = (week_of_the_year%52) + 1)
|
||||
# last day of this week
|
||||
return add_to_date(date, days=-1)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,77 +1,41 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"actions": [],
|
||||
"creation": "2019-03-12 15:00:57.052684",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"chart",
|
||||
"width"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"columns": 8,
|
||||
"fieldname": "chart",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Chart",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Dashboard Chart",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "Dashboard Chart"
|
||||
},
|
||||
{
|
||||
"default": "Half",
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Width",
|
||||
"options": "Half\nFull"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-03-12 15:01:31.639414",
|
||||
"links": [],
|
||||
"modified": "2020-03-13 19:23:05.561687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Chart Link",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -6,8 +6,7 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"chart_name",
|
||||
"label",
|
||||
"size"
|
||||
"label"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -23,18 +22,11 @@
|
|||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"fieldname": "size",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Size",
|
||||
"options": "Full\nHalf"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-01-23 16:47:16.265651",
|
||||
"modified": "2020-03-20 10:04:13.992228",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desk Chart",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ def setup_complete(args):
|
|||
stages = get_setup_stages(args)
|
||||
|
||||
try:
|
||||
frappe.flags.in_setup_wizard = True
|
||||
current_task = None
|
||||
for idx, stage in enumerate(stages):
|
||||
frappe.publish_realtime('setup_task', {"progress": [idx, len(stages)],
|
||||
|
|
@ -75,6 +76,8 @@ def setup_complete(args):
|
|||
else:
|
||||
run_setup_success(args)
|
||||
return {'status': 'ok'}
|
||||
finally:
|
||||
frappe.flags.in_setup_wizard = False
|
||||
|
||||
def update_global_settings(args):
|
||||
if args.language and args.language != "English":
|
||||
|
|
@ -349,6 +352,11 @@ def email_setup_wizard_exception(traceback, args):
|
|||
message=message,
|
||||
delayed=False)
|
||||
|
||||
def log_setup_wizard_exception(traceback, args):
|
||||
with open('../logs/setup-wizard.log', 'w+') as setup_log:
|
||||
setup_log.write(traceback)
|
||||
setup_log.write(json.dumps(args))
|
||||
|
||||
def get_language_code(lang):
|
||||
return frappe.db.get_value('Language', {'language_name':lang})
|
||||
|
||||
|
|
|
|||
|
|
@ -77,9 +77,9 @@ def generate_report_result(report, filters=None, user=None):
|
|||
if len(res) > 5:
|
||||
skip_total_row = cint(res[5])
|
||||
|
||||
if report.custom_columns:
|
||||
columns = json.loads(report.custom_columns)
|
||||
result = add_data_to_custom_columns(columns, result)
|
||||
if report.custom_columns:
|
||||
columns = json.loads(report.custom_columns)
|
||||
result = add_data_to_custom_columns(columns, result)
|
||||
|
||||
if result:
|
||||
result = get_filtered_data(report.ref_doctype, columns, result, user)
|
||||
|
|
|
|||
|
|
@ -129,11 +129,19 @@ def get_context(context):
|
|||
allow_update = True
|
||||
if doc.docstatus == 1 and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit:
|
||||
allow_update = False
|
||||
|
||||
if allow_update:
|
||||
frappe.db.set_value(doc.doctype, doc.name, self.set_property_after_alert,
|
||||
self.property_value, update_modified = False)
|
||||
doc.set(self.set_property_after_alert, self.property_value)
|
||||
try:
|
||||
if allow_update and not doc.flags.in_notification_update:
|
||||
doc.set(self.set_property_after_alert, self.property_value)
|
||||
doc.flags.updater_reference = {
|
||||
'doctype': self.doctype,
|
||||
'docname': self.name,
|
||||
'label': _('via Notification')
|
||||
}
|
||||
doc.flags.in_notification_update = True
|
||||
doc.save(ignore_permissions=True)
|
||||
doc.flags.in_notification_update = False
|
||||
except Exception:
|
||||
frappe.log_error(title='Document update failed', message=frappe.get_traceback())
|
||||
|
||||
def send_an_email(self, doc, context):
|
||||
from email.utils import formataddr
|
||||
|
|
@ -301,23 +309,23 @@ def evaluate_alert(doc, alert, event):
|
|||
return
|
||||
|
||||
if event=="Value Change" and not doc.is_new():
|
||||
try:
|
||||
db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed)
|
||||
except Exception as e:
|
||||
if frappe.db.is_missing_column(e):
|
||||
alert.db_set('enabled', 0)
|
||||
frappe.log_error('Notification {0} has been disabled due to missing field'.format(alert.name))
|
||||
return
|
||||
else:
|
||||
raise
|
||||
db_value = parse_val(db_value)
|
||||
if (doc.get(alert.value_changed) == db_value) or (not db_value and not doc.get(alert.value_changed)):
|
||||
return # value not changed
|
||||
if not frappe.db.has_column(doc.doctype, alert.value_changed):
|
||||
alert.db_set('enabled', 0)
|
||||
frappe.log_error('Notification {0} has been disabled due to missing field'.format(alert.name))
|
||||
return
|
||||
|
||||
doc_before_save = doc.get_doc_before_save()
|
||||
field_value_before_save = doc_before_save.get(alert.value_changed) if doc_before_save else None
|
||||
|
||||
field_value_before_save = parse_val(field_value_before_save)
|
||||
if (doc.get(alert.value_changed) == field_value_before_save):
|
||||
# value not changed
|
||||
return
|
||||
|
||||
if event != "Value Change" and not doc.is_new():
|
||||
# reload the doc for the latest values & comments,
|
||||
# except for validate type event.
|
||||
doc = frappe.get_doc(doc.doctype, doc.name)
|
||||
doc.reload()
|
||||
alert.send(doc)
|
||||
except TemplateError:
|
||||
frappe.throw(_("Error while evaluating Notification {0}. Please fix your template.").format(alert))
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ app_include_css = [
|
|||
"assets/css/list.min.css",
|
||||
"assets/css/form.min.css",
|
||||
"assets/css/report.min.css",
|
||||
"assets/css/module.min.css"
|
||||
]
|
||||
|
||||
web_include_js = [
|
||||
|
|
@ -134,12 +133,13 @@ doc_events = {
|
|||
],
|
||||
"on_trash": [
|
||||
"frappe.desk.notifications.clear_doctype_notifications",
|
||||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions"
|
||||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
|
||||
"frappe.cache_manager.build_table_count_cache"
|
||||
],
|
||||
"on_change": [
|
||||
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points"
|
||||
],
|
||||
"after_insert": "frappe.cache_manager.build_table_count_cache",
|
||||
"after_insert": "frappe.cache_manager.build_table_count_cache"
|
||||
},
|
||||
"Event": {
|
||||
"after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar",
|
||||
|
|
@ -257,7 +257,10 @@ bot_parsers = [
|
|||
'frappe.utils.bot.CountBot'
|
||||
]
|
||||
|
||||
setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception"
|
||||
setup_wizard_exception = [
|
||||
"frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception",
|
||||
"frappe.desk.page.setup_wizard.setup_wizard.log_setup_wizard_exception"
|
||||
]
|
||||
|
||||
before_migrate = ['frappe.patches.v11_0.sync_user_permission_doctype_before_migrate.execute']
|
||||
after_migrate = ['frappe.website.doctype.website_theme.website_theme.generate_theme_files_if_not_exist']
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ class Document(BaseDocument):
|
|||
if hasattr(self, "__islocal"):
|
||||
delattr(self, "__islocal")
|
||||
|
||||
if not (frappe.flags.in_migrate or frappe.local.flags.in_install):
|
||||
if not (frappe.flags.in_migrate or frappe.local.flags.in_install or frappe.flags.in_setup_wizard):
|
||||
follow_document(self.doctype, self.name, frappe.session.user)
|
||||
return self
|
||||
|
||||
|
|
@ -846,9 +846,7 @@ class Document(BaseDocument):
|
|||
|
||||
if not self.flags.in_insert:
|
||||
# value change is not applicable in insert
|
||||
event_map['validate'] = 'Value Change'
|
||||
event_map['before_change'] = 'Value Change'
|
||||
event_map['before_update_after_submit'] = 'Value Change'
|
||||
event_map['on_change'] = 'Value Change'
|
||||
|
||||
for alert in self.flags.notifications:
|
||||
event = event_map.get(method, None)
|
||||
|
|
@ -945,7 +943,6 @@ class Document(BaseDocument):
|
|||
elif self._action=="update_after_submit":
|
||||
self.run_method("on_update_after_submit")
|
||||
|
||||
self.run_method('on_change')
|
||||
|
||||
self.clear_cache()
|
||||
self.notify_update()
|
||||
|
|
@ -955,6 +952,8 @@ class Document(BaseDocument):
|
|||
if getattr(self.meta, 'track_changes', False) and self._doc_before_save and not self.flags.ignore_version:
|
||||
self.save_version()
|
||||
|
||||
self.run_method('on_change')
|
||||
|
||||
if (self.doctype, self.name) in frappe.flags.currently_saving:
|
||||
frappe.flags.currently_saving.remove((self.doctype, self.name))
|
||||
|
||||
|
|
@ -979,7 +978,7 @@ class Document(BaseDocument):
|
|||
def reset_seen(self):
|
||||
"""Clear _seen property and set current user as seen"""
|
||||
if getattr(self.meta, 'track_seen', False):
|
||||
self._seen = json.dumps([frappe.session.user])
|
||||
frappe.db.set_value(self.doctype, self.name, "_seen", json.dumps([frappe.session.user]), update_modified=False)
|
||||
|
||||
def notify_update(self):
|
||||
"""Publish realtime that the current document is modified"""
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ class Meta(Document):
|
|||
|
||||
def get_valid_columns(self):
|
||||
if not hasattr(self, "_valid_columns"):
|
||||
if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action', 'DocType Link', "Property Setter"):
|
||||
if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action', 'DocType Link'):
|
||||
self._valid_columns = get_table_columns(self.name)
|
||||
else:
|
||||
self._valid_columns = self.default_fields + \
|
||||
|
|
@ -290,17 +290,20 @@ class Meta(Document):
|
|||
return get_workflow_name(self.name)
|
||||
|
||||
def add_custom_fields(self):
|
||||
try:
|
||||
self.extend("fields", frappe.db.sql("""SELECT * FROM `tabCustom Field`
|
||||
WHERE dt = %s AND docstatus < 2""", (self.name,), as_dict=1,
|
||||
update={"is_custom_field": 1}))
|
||||
except Exception as e:
|
||||
if frappe.db.is_table_missing(e):
|
||||
return
|
||||
else:
|
||||
raise
|
||||
if not frappe.db.table_exists('Custom Field'):
|
||||
return
|
||||
|
||||
custom_fields = frappe.db.sql("""
|
||||
SELECT * FROM `tabCustom Field`
|
||||
WHERE dt = %s AND docstatus < 2
|
||||
""", (self.name,), as_dict=1, update={"is_custom_field": 1})
|
||||
|
||||
self.extend("fields", custom_fields)
|
||||
|
||||
def apply_property_setters(self):
|
||||
if not frappe.db.table_exists('Property Setter'):
|
||||
return
|
||||
|
||||
property_setters = frappe.db.sql("""select * from `tabProperty Setter` where
|
||||
doc_type=%s""", (self.name,), as_dict=1)
|
||||
|
||||
|
|
@ -378,8 +381,9 @@ class Meta(Document):
|
|||
if custom_perms:
|
||||
self.permissions = [Document(d) for d in custom_perms]
|
||||
|
||||
def get_fieldnames_with_value(self):
|
||||
return [df.fieldname for df in self.fields if df.fieldtype not in no_value_fields]
|
||||
def get_fieldnames_with_value(self, with_field_meta=False):
|
||||
return [df if with_field_meta else df.fieldname \
|
||||
for df in self.fields if df.fieldtype not in no_value_fields]
|
||||
|
||||
|
||||
def get_fields_to_check_permissions(self, user_permission_doctypes):
|
||||
|
|
@ -529,7 +533,9 @@ def get_field_currency(df, doc=None):
|
|||
if currency:
|
||||
ref_docname = doc.name
|
||||
else:
|
||||
currency = frappe.db.get_value(doc.parenttype, doc.parent, df.get("options"))
|
||||
if frappe.get_meta(doc.parenttype).has_field(df.get("options")):
|
||||
# only get_value if parent has currency field
|
||||
currency = frappe.db.get_value(doc.parenttype, doc.parent, df.get("options"))
|
||||
|
||||
if currency:
|
||||
frappe.local.field_currency.setdefault((doc.doctype, ref_docname), frappe._dict())\
|
||||
|
|
@ -542,7 +548,7 @@ def get_field_precision(df, doc=None, currency=None):
|
|||
"""get precision based on DocField options and fieldvalue in doc"""
|
||||
from frappe.utils import get_number_format_info
|
||||
|
||||
if cint(df.precision):
|
||||
if df.precision:
|
||||
precision = cint(df.precision)
|
||||
|
||||
elif df.fieldtype == "Currency":
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ def print_workflow_log(messages, title, doctype, indicator):
|
|||
html = "<div>{0}</div>".format(doc)
|
||||
msg += html
|
||||
|
||||
frappe.msgprint(msg, title=_("Workflow Status"), indicator=indicator)
|
||||
frappe.msgprint(msg, title=_("Workflow Status"), indicator=indicator, is_minimizable=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_common_transition_actions(docs, doctype):
|
||||
|
|
|
|||
|
|
@ -268,3 +268,5 @@ execute:frappe.delete_doc_if_exists('DocType', 'Google Maps Settings')
|
|||
execute:frappe.db.set_default('desktop:home_page', 'workspace')
|
||||
execute:frappe.delete_doc_if_exists('DocType', 'GSuite Settings')
|
||||
execute:frappe.delete_doc_if_exists('DocType', 'GSuite Templates')
|
||||
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
|
||||
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
|
||||
|
|
|
|||
|
|
@ -222,6 +222,8 @@
|
|||
"public/js/frappe/views/communication.js",
|
||||
"public/js/frappe/views/translation_manager.js",
|
||||
|
||||
"public/js/frappe/widgets/widget_group.js",
|
||||
|
||||
"public/js/frappe/ui/sort_selector.html",
|
||||
"public/js/frappe/ui/sort_selector.js",
|
||||
|
||||
|
|
@ -237,12 +239,8 @@
|
|||
"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/dashboard_chart.js",
|
||||
"public/js/frappe/barcode_scanner/index.js"
|
||||
],
|
||||
"css/module.min.css": [
|
||||
"public/less/module.less"
|
||||
],
|
||||
"css/form.min.css": [
|
||||
"public/less/form_grid.less"
|
||||
],
|
||||
|
|
|
|||
4
frappe/public/css/bootstrap.css
vendored
4
frappe/public/css/bootstrap.css
vendored
|
|
@ -15,7 +15,6 @@ body {
|
|||
}
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
|
|
@ -24,8 +23,7 @@ hgroup,
|
|||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
audio,
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
.module-head {
|
||||
padding: 15px 30px;
|
||||
border-bottom: 1px solid #EBEFF2;
|
||||
}
|
||||
.module-head h1 {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
.module-body {
|
||||
padding: 0px 15px;
|
||||
}
|
||||
.module-body .section-head {
|
||||
margin-bottom: 15px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
.module-section {
|
||||
border-bottom: 1px solid #EBEFF2;
|
||||
}
|
||||
.module-section .module-section-link {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.module-section-column {
|
||||
padding: 30px;
|
||||
}
|
||||
@media (min-width: 767px) {
|
||||
.module-section:nth-child(even) {
|
||||
background-color: #fafbfc;
|
||||
}
|
||||
.module-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
.module-body {
|
||||
margin-top: 15px;
|
||||
border-top: 1px solid #d1d8dd;
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.module-body {
|
||||
margin-top: 0;
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.module-section {
|
||||
border: none;
|
||||
}
|
||||
.module-section-column {
|
||||
border-bottom: 1px solid #EBEFF2;
|
||||
}
|
||||
.module-section-column:nth-child(even) {
|
||||
background-color: #fafbfc;
|
||||
}
|
||||
.module-section:last-child .module-section-column:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
.module-item {
|
||||
margin: 0px;
|
||||
padding: 7px;
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #d1d8dd;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
-webkit-transition: 0.2s;
|
||||
}
|
||||
.module-item h4 {
|
||||
display: inline-block;
|
||||
}
|
||||
.module-item .module-item-description {
|
||||
margin-top: -5px;
|
||||
}
|
||||
.module-item .badge {
|
||||
margin-top: -2px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.module-item:hover,
|
||||
.module-item:focus {
|
||||
background-color: #F7FAFC;
|
||||
}
|
||||
.module-item:last-child {
|
||||
border: none;
|
||||
}
|
||||
.module-link.active .icon-chevron-right {
|
||||
margin-top: 4px;
|
||||
display: block !important;
|
||||
}
|
||||
.module-item-progress {
|
||||
margin-bottom: 10px;
|
||||
height: 17px;
|
||||
}
|
||||
.module-item-progress-total {
|
||||
height: 7px;
|
||||
background-color: #999999;
|
||||
width: 0px;
|
||||
}
|
||||
.module-item-progress-open {
|
||||
height: 7px;
|
||||
background-color: red;
|
||||
width: 0px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
body[data-route^="Module"] .page-title {
|
||||
width: 100%;
|
||||
}
|
||||
body[data-route^="Module"] .page-actions {
|
||||
display: none !important;
|
||||
}
|
||||
body[data-route^="Module"] .layout-main-section {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
|
@ -560,12 +560,18 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
return;
|
||||
}
|
||||
|
||||
let data_import_link = frappe.utils.get_form_link(
|
||||
'Data Import Beta',
|
||||
data.data_import,
|
||||
true,
|
||||
__('via Data Import')
|
||||
);
|
||||
let updater_reference_link = null;
|
||||
|
||||
if (!$.isEmptyObject(data.updater_reference)) {
|
||||
let label = updater_reference.label || __('via {0}', [updater_reference.doctype]);
|
||||
let updater_reference = data.updater_reference;
|
||||
updater_reference_link = frappe.utils.get_form_link(
|
||||
updater_reference.doctype,
|
||||
updater_reference.docname,
|
||||
true,
|
||||
label
|
||||
);
|
||||
}
|
||||
|
||||
// value changed in parent
|
||||
if (data.changed && data.changed.length) {
|
||||
|
|
@ -573,13 +579,13 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
data.changed.every(function(p) {
|
||||
if (p[0]==='docstatus') {
|
||||
if (p[2]==1) {
|
||||
let message = data.data_import
|
||||
? __('submitted this document {0}', [data_import_link])
|
||||
let message = updater_reference_link
|
||||
? __('submitted this document {0}', [updater_reference_link])
|
||||
: __('submitted this document');
|
||||
out.push(me.get_version_comment(version, message));
|
||||
} else if (p[2]==2) {
|
||||
let message = data.data_import
|
||||
? __('cancelled this document {0}', [data_import_link])
|
||||
let message = updater_reference_link
|
||||
? __('cancelled this document {0}', [updater_reference_link])
|
||||
: __('cancelled this document');
|
||||
out.push(me.get_version_comment(version, message));
|
||||
}
|
||||
|
|
@ -600,10 +606,10 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
}
|
||||
return parts.length < 3;
|
||||
});
|
||||
if(parts.length) {
|
||||
if (parts.length) {
|
||||
let message;
|
||||
if (data.data_import) {
|
||||
message = __("changed value of {0} {1}", [parts.join(', ').bold(), data_import_link]);
|
||||
if (updater_reference_link) {
|
||||
message = __("changed value of {0} {1}", [parts.join(', ').bold(), updater_reference_link]);
|
||||
} else {
|
||||
message = __("changed value of {0}", [parts.join(', ').bold()]);
|
||||
}
|
||||
|
|
@ -638,10 +644,10 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
});
|
||||
return parts.length < 3;
|
||||
});
|
||||
if(parts.length) {
|
||||
if (parts.length) {
|
||||
let message;
|
||||
if (data.data_import) {
|
||||
message = __("changed values for {0} {1}", [parts.join(', '), data_import_link]);
|
||||
if (updater_reference_link) {
|
||||
message = __("changed values for {0} {1}", [parts.join(', '), updater_reference_link]);
|
||||
} else {
|
||||
message = __("changed values for {0}", [parts.join(', ')]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ export default class GridRow {
|
|||
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) {
|
||||
// pass
|
||||
} else {
|
||||
if (!me.grid.is_editable()) {
|
||||
me.docfields.map(df => df.read_only = 1);
|
||||
}
|
||||
me.toggle_view();
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ frappe.ui.form.Sidebar = Class.extend({
|
|||
|
||||
this.make_tags();
|
||||
this.make_like();
|
||||
this.make_follow();
|
||||
if (frappe.boot.user.document_follow_notify) {
|
||||
this.make_follow();
|
||||
}
|
||||
|
||||
this.bind_events();
|
||||
this.setup_keyboard_shortcuts();
|
||||
|
|
@ -74,7 +76,9 @@ frappe.ui.form.Sidebar = Class.extend({
|
|||
this.frm.assign_to.refresh();
|
||||
this.frm.attachments.refresh();
|
||||
this.frm.shared.refresh();
|
||||
this.frm.follow.refresh();
|
||||
if (frappe.boot.user.document_follow_notify) {
|
||||
this.frm.follow.refresh();
|
||||
}
|
||||
this.frm.viewers.refresh();
|
||||
this.frm.tags && this.frm.tags.refresh(this.frm.get_docinfo().tags);
|
||||
this.sidebar.find(".modified-by").html(__("{0} edited this {1}",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
{% } %}
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="timeline-new-email timeline-email-import text-muted small">
|
||||
<div class="timeline-email-import text-muted small">
|
||||
|
||||
</div>
|
||||
<div class="timeline-items">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
</ul>
|
||||
<ul class="list-unstyled sidebar-menu standard-actions">
|
||||
{% if frappe.model.can_get_report(doctype) %}
|
||||
<li class="list-sidebar-label">Views</li>
|
||||
<li class="divider visible-sm visible-xs"></li>
|
||||
<li class="list-link">
|
||||
<div class="btn-group">
|
||||
|
|
@ -64,7 +65,7 @@
|
|||
<li class="list-stats list-link">
|
||||
<div class="btn-group">
|
||||
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="#" onclick="return false;">
|
||||
{{ __("Tags") }}<span class="caret"></span>
|
||||
{{ __("Tags") }} <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu list-stats-dropdown" role="menu">
|
||||
<div class="dropdown-search">
|
||||
|
|
|
|||
|
|
@ -54,19 +54,22 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
render_group_by_items() {
|
||||
let get_item_html = (fieldname) => {
|
||||
let label;
|
||||
let fieldtype;
|
||||
if (fieldname === 'assigned_to') {
|
||||
label = __('Assigned To');
|
||||
} else if (fieldname === 'owner') {
|
||||
label = __('Created By');
|
||||
} else {
|
||||
label = frappe.meta.get_label(this.doctype, fieldname);
|
||||
fieldtype = frappe.meta.get_docfield(this.doctype, fieldname).fieldtype;
|
||||
}
|
||||
|
||||
return `<li class="group-by-field list-link">
|
||||
<div class="btn-group">
|
||||
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
data-label="${label}" data-fieldname="${fieldname}" href="#" onclick="return false;">
|
||||
${__(label)}<span class="caret"></span>
|
||||
data-label="${label}" data-fieldname="${fieldname}" data-fieldtype="${fieldtype}"
|
||||
href="#" onclick="return false;">
|
||||
${__(label)} <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu group-by-dropdown" role="menu">
|
||||
<li><div class="list-loading text-center group-by-loading text-muted">
|
||||
|
|
@ -85,9 +88,10 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
this.$wrapper.on('click', '.group-by-field', (e)=> {
|
||||
let dropdown = $(e.currentTarget).find('.group-by-dropdown');
|
||||
let fieldname = $(e.currentTarget).find('a').attr('data-fieldname');
|
||||
let fieldtype = $(e.currentTarget).find('a').attr('data-fieldtype');
|
||||
this.get_group_by_count(fieldname).then(field_count_list => {
|
||||
if (field_count_list.length) {
|
||||
this.render_dropdown_items(field_count_list, dropdown);
|
||||
this.render_dropdown_items(field_count_list, fieldtype, dropdown);
|
||||
this.sidebar.setup_dropdown_search(dropdown, '.group-by-value');
|
||||
} else {
|
||||
dropdown.find('.group-by-loading').html(`${__("No filters found")}`);
|
||||
|
|
@ -98,7 +102,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
|
||||
get_group_by_dropdown_fields() {
|
||||
let group_by_fields = [];
|
||||
let fields = this.list_view.meta.fields.filter((f)=> ["Select", "Link"].includes(f.fieldtype));
|
||||
let fields = this.list_view.meta.fields.filter((f)=> ["Select", "Link", "Data", "Int", "Check"].includes(f.fieldtype));
|
||||
group_by_fields.push({
|
||||
label: __(this.doctype),
|
||||
fieldname: 'group_by_fields',
|
||||
|
|
@ -118,7 +122,8 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
let current_filters = this.list_view.get_filters_for_args();
|
||||
|
||||
// remove filter of the current field
|
||||
current_filters = current_filters.filter((f_arr) => !f_arr.includes(field === 'assigned_to' ? '_assign': field));
|
||||
current_filters =
|
||||
current_filters.filter((f_arr) => !f_arr.includes(field === 'assigned_to' ? '_assign': field));
|
||||
|
||||
let args = {
|
||||
doctype: this.doctype,
|
||||
|
|
@ -138,11 +143,13 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
});
|
||||
}
|
||||
|
||||
render_dropdown_items(fields, dropdown) {
|
||||
render_dropdown_items(fields, fieldtype, dropdown) {
|
||||
let get_dropdown_html = (field) => {
|
||||
let label = field.name == null ? __('Not Specified') : field.name;
|
||||
if (label === frappe.session.user) {
|
||||
label = __('Me');
|
||||
} else if (fieldtype && fieldtype == 'Check') {
|
||||
label = label == '0'? __('No'): __('Yes');
|
||||
}
|
||||
let value = field.name == null ? '' : encodeURIComponent(field.name);
|
||||
|
||||
|
|
@ -167,7 +174,9 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
this.$wrapper.on('click', '.group-by-item', (e) => {
|
||||
let $target = $(e.currentTarget);
|
||||
let fieldname = $target.parents('.group-by-field').find('a').data('fieldname');
|
||||
let value = decodeURIComponent($target.data('value').trim());
|
||||
let value = typeof $target.data('value') === 'string'
|
||||
? decodeURIComponent($target.data('value').trim())
|
||||
: $target.data('value');
|
||||
fieldname = fieldname === 'assigned_to' ? '_assign': fieldname;
|
||||
|
||||
return this.list_view.filter_area.remove(fieldname)
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ $.extend(frappe.meta, {
|
|||
|
||||
get_field_precision: function(df, doc) {
|
||||
var precision = null;
|
||||
if (df && cint(df.precision)) {
|
||||
if (df && df.precision) {
|
||||
precision = cint(df.precision);
|
||||
} else if(df && df.fieldtype === "Currency") {
|
||||
precision = cint(frappe.defaults.get_default("currency_precision"));
|
||||
|
|
|
|||
|
|
@ -394,11 +394,11 @@ frappe.after_ajax = function(fn) {
|
|||
return new Promise(resolve => {
|
||||
if(frappe.request.ajax_count) {
|
||||
frappe.request.waiting_for_ajax.push(() => {
|
||||
if(fn) fn();
|
||||
if(fn) return resolve(fn());
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
if(fn) fn();
|
||||
if(fn) return resolve(fn());
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,412 +0,0 @@
|
|||
frappe.provide('ui')
|
||||
frappe.provide('frappe.dashboards');
|
||||
frappe.provide('frappe.dashboards.chart_sources');
|
||||
|
||||
frappe.ui.DashboardChart = class DashboardChart {
|
||||
constructor(chart_doc, chart_container, options) {
|
||||
this.chart_doc = chart_doc;
|
||||
this.container = chart_container;
|
||||
this.options = options || {};
|
||||
this.chart_args = {};
|
||||
}
|
||||
|
||||
show() {
|
||||
this.get_settings().then(() => {
|
||||
this.prepare_chart_object();
|
||||
this.prepare_container();
|
||||
|
||||
if (!this.options.hide_actions || this.options.hide_actions == undefined) {
|
||||
this.setup_filter_button();
|
||||
if (this.chart_doc.timeseries && this.chart_doc.chart_type !== 'Custom') {
|
||||
this.render_time_series_filters();
|
||||
}
|
||||
|
||||
this.prepare_chart_actions();
|
||||
}
|
||||
|
||||
this.fetch(this.filters).then( data => {
|
||||
if (this.chart_doc.chart_type == 'Report') {
|
||||
data = this.get_report_chart_data(data);
|
||||
}
|
||||
if (!this.options.hide_last_sync || this.options.hide_last_sync == undefined) {
|
||||
this.update_last_synced();
|
||||
}
|
||||
this.data = data;
|
||||
this.render();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
prepare_container() {
|
||||
const column_width_map = {
|
||||
"Half": "6",
|
||||
"Full": "12",
|
||||
};
|
||||
let columns = column_width_map[this.chart_doc.width];
|
||||
this.chart_container = $(`<div class="col-sm-${columns} chart-column-container">
|
||||
<div class="chart-wrapper">
|
||||
<div class="chart-loading-state text-muted">${__("Loading...")}</div>
|
||||
<div class="chart-empty-state hide text-muted">${__("No Data")}</div>
|
||||
</div>
|
||||
</div>`);
|
||||
this.chart_container.appendTo(this.container);
|
||||
|
||||
if (!this.options.hide_last_sync || this.options.hide_last_sync == undefined) {
|
||||
let last_synced_text = $(`<span class="text-muted last-synced-text"></span>`);
|
||||
last_synced_text.prependTo(this.chart_container);
|
||||
}
|
||||
}
|
||||
|
||||
render_time_series_filters() {
|
||||
let filters = [
|
||||
{
|
||||
label: 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();
|
||||
this.fetch_and_update_chart();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: this.chart_doc.time_interval,
|
||||
options: ['Yearly', 'Quarterly', 'Monthly', 'Weekly', 'Daily'],
|
||||
action: (selected_item) => {
|
||||
this.selected_time_interval = selected_item;
|
||||
this.fetch_and_update_chart();
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
frappe.dashboard_utils.render_chart_filters(filters, 'chart-actions', this.chart_container, 1);
|
||||
}
|
||||
|
||||
fetch_and_update_chart() {
|
||||
this.args = {
|
||||
timespan: this.selected_timespan,
|
||||
time_interval: this.selected_time_interval,
|
||||
from_date: this.selected_from_date,
|
||||
to_date: this.selected_to_date
|
||||
};
|
||||
|
||||
this.fetch(this.filters, true, this.args).then(data => {
|
||||
if (this.chart_doc.chart_type == 'Report') {
|
||||
data = this.get_report_chart_data(data);
|
||||
}
|
||||
|
||||
this.update_chart_object();
|
||||
this.data = data;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
render_date_range_fields() {
|
||||
if (!this.date_field_wrapper || !this.date_field_wrapper.is(':visible')) {
|
||||
this.date_field_wrapper =
|
||||
$(`<div class="dashboard-date-field pull-right"></div>`)
|
||||
.insertBefore(this.chart_container.find('.chart-wrapper'));
|
||||
|
||||
this.date_range_field = frappe.ui.form.make_control({
|
||||
df: {
|
||||
fieldtype: 'DateRange',
|
||||
fieldname: 'from_date',
|
||||
placeholder: 'Date Range',
|
||||
input_class: 'input-xs',
|
||||
reqd: 1,
|
||||
change: () => {
|
||||
let selected_date_range = this.date_range_field.get_value();
|
||||
this.selected_from_date = selected_date_range[0];
|
||||
this.selected_to_date = selected_date_range[1];
|
||||
|
||||
if (selected_date_range && selected_date_range.length == 2) {
|
||||
this.fetch_and_update_chart();
|
||||
}
|
||||
}
|
||||
},
|
||||
parent: this.date_field_wrapper,
|
||||
render_input: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_report_chart_data(result) {
|
||||
if (result.chart && this.chart_doc.is_custom) {
|
||||
return result.chart.data;
|
||||
} else {
|
||||
let y_fields = [];
|
||||
this.chart_doc.y_axis.map( field => {
|
||||
y_fields.push(field.y_field);
|
||||
});
|
||||
|
||||
let chart_fields = {
|
||||
y_fields: y_fields,
|
||||
x_field: this.chart_doc.x_field,
|
||||
chart_type: this.chart_doc.type,
|
||||
color: this.chart_doc.color
|
||||
};
|
||||
let columns = result.columns.map((col)=> {
|
||||
return frappe.report_utils.prepare_field_from_column(col);
|
||||
});
|
||||
|
||||
let data = frappe.report_utils.make_chart_options(columns, result, chart_fields).data;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
prepare_chart_actions() {
|
||||
let actions = [
|
||||
{
|
||||
label: __("Refresh"),
|
||||
action: 'action-refresh',
|
||||
handler: () => {
|
||||
this.fetch_and_update_chart();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Edit..."),
|
||||
action: 'action-edit',
|
||||
handler: () => {
|
||||
frappe.set_route('Form', 'Dashboard Chart', this.chart_doc.name);
|
||||
}
|
||||
}
|
||||
];
|
||||
if (this.chart_doc.document_type) {
|
||||
actions.push({
|
||||
label: __("{0} List", [this.chart_doc.document_type]),
|
||||
action: 'action-list',
|
||||
handler: () => {
|
||||
frappe.set_route('List', this.chart_doc.document_type);
|
||||
}
|
||||
});
|
||||
} else if (this.chart_doc.chart_type === 'Report') {
|
||||
actions.push({
|
||||
label: __("{0} Report", [this.chart_doc.report_name]),
|
||||
action: 'action-list',
|
||||
handler: () => {
|
||||
frappe.set_route('query-report', this.chart_doc.report_name);
|
||||
}
|
||||
})
|
||||
}
|
||||
this.set_chart_actions(actions);
|
||||
}
|
||||
|
||||
setup_filter_button() {
|
||||
|
||||
this.is_document_type = this.chart_doc.chart_type!== 'Report' && this.chart_doc.chart_type!=='Custom';
|
||||
this.filter_button =
|
||||
$(`<div class="filter-chart btn btn-default btn-xs pull-right">${__("Filter")}</div>`);
|
||||
this.filter_button.prependTo(this.chart_container);
|
||||
|
||||
this.filter_button.on('click', () => {
|
||||
let fields;
|
||||
|
||||
frappe.dashboard_utils.get_filters_for_chart_type(this.chart_doc)
|
||||
.then(filters => {
|
||||
if (!this.is_document_type) {
|
||||
if (!filters) {
|
||||
fields = [{
|
||||
fieldtype: "HTML",
|
||||
options: __("No Filters Set")
|
||||
}];
|
||||
} 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;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
fields = [{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'filter_area',
|
||||
}];
|
||||
}
|
||||
|
||||
this.setup_filter_dialog(fields);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setup_filter_dialog(fields) {
|
||||
|
||||
let me = this;
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __(`Set Filters for ${this.chart_doc.chart_name}`),
|
||||
fields: fields,
|
||||
primary_action: function() {
|
||||
let values = this.get_values();
|
||||
if (values) {
|
||||
this.hide();
|
||||
if (me.is_document_type) {
|
||||
me.filters = me.filter_group.get_filters();
|
||||
} else {
|
||||
me.filters = values;
|
||||
}
|
||||
me.fetch_and_update_chart();
|
||||
}
|
||||
},
|
||||
primary_action_label: "Set"
|
||||
});
|
||||
|
||||
if (this.is_document_type) {
|
||||
this.create_filter_group_and_add_filters(dialog.get_field('filter_area').$wrapper);
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
dialog.set_values(this.filters);
|
||||
|
||||
}
|
||||
|
||||
create_filter_group_and_add_filters(parent) {
|
||||
this.filter_group = new frappe.ui.FilterGroup({
|
||||
parent: parent,
|
||||
doctype: this.chart_doc.document_type,
|
||||
on_change: () => {},
|
||||
});
|
||||
|
||||
frappe.model.with_doctype(this.chart_doc.document_type, () => {
|
||||
this.filter_group.add_filters_to_filter_group(this.filters);
|
||||
});
|
||||
}
|
||||
|
||||
set_chart_actions(actions) {
|
||||
this.chart_actions = $(`<div class="chart-actions btn-group dropdown pull-right">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-default btn-xs"><span class="caret"></span></button>
|
||||
</a>
|
||||
<ul class="dropdown-menu" style="max-height: 300px; overflow-y: auto;">
|
||||
${actions.map(action => `<li><a data-action="${action.action}">${action.label}</a></li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
|
||||
this.chart_actions.find("a[data-action]").each((i, o) => {
|
||||
const action = o.dataset.action;
|
||||
$(o).click(actions.find(a => a.action === action));
|
||||
});
|
||||
this.chart_actions.prependTo(this.chart_container);
|
||||
}
|
||||
|
||||
fetch(filters, refresh=false, args) {
|
||||
this.chart_container.find('.chart-loading-state').removeClass('hide');
|
||||
let method = this.settings ? this.settings.method
|
||||
: 'frappe.desk.doctype.dashboard_chart.dashboard_chart.get';
|
||||
|
||||
if (this.chart_doc.chart_type == 'Report') {
|
||||
args = {
|
||||
report_name: this.chart_doc.report_name,
|
||||
filters: filters,
|
||||
};
|
||||
} else {
|
||||
args = {
|
||||
chart_name: this.chart_doc.name,
|
||||
filters: filters,
|
||||
refresh: refresh ? 1 : 0,
|
||||
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,
|
||||
};
|
||||
}
|
||||
return frappe.xcall(
|
||||
method,
|
||||
args
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const chart_type_map = {
|
||||
'Line': 'line',
|
||||
'Bar': 'bar',
|
||||
'Percentage': 'percentage',
|
||||
'Pie': 'pie'
|
||||
};
|
||||
|
||||
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"];
|
||||
}
|
||||
|
||||
this.chart_container.find('.chart-loading-state').addClass('hide');
|
||||
if (!this.data) {
|
||||
this.chart_container.find('.chart-empty-state').removeClass('hide');
|
||||
} else {
|
||||
let title = null;
|
||||
if (!this.options.hide_title || this.options.hide_title == undefined) {
|
||||
title = this.chart_doc.chart_name;
|
||||
}
|
||||
|
||||
let chart_args = {
|
||||
title: title,
|
||||
data: this.data,
|
||||
type: chart_type_map[this.chart_doc.type],
|
||||
colors: colors,
|
||||
axisOptions: {
|
||||
xIsSeries: this.chart_doc.timeseries,
|
||||
shortenYAxisNumbers: 1
|
||||
}
|
||||
};
|
||||
if (!this.chart) {
|
||||
this.chart = new frappe.Chart(this.chart_container.find(".chart-wrapper")[0], chart_args);
|
||||
} else {
|
||||
this.chart.update(this.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_last_synced() {
|
||||
let last_synced_text = __("Last synced {0}", [comment_when(this.chart_doc.last_synced_on)]);
|
||||
this.container.find(".last-synced-text").html(last_synced_text);
|
||||
}
|
||||
|
||||
update_chart_object() {
|
||||
frappe.db.get_doc("Dashboard Chart", this.chart_doc.name).then(doc => {
|
||||
this.chart_doc = doc;
|
||||
this.prepare_chart_object();
|
||||
this.update_last_synced();
|
||||
});
|
||||
}
|
||||
|
||||
prepare_chart_object() {
|
||||
this.filters = this.filters || JSON.parse(this.chart_doc.filters_json || '[]');
|
||||
}
|
||||
|
||||
get_settings() {
|
||||
if (this.chart_doc.chart_type == 'Custom') {
|
||||
// custom source
|
||||
if (frappe.dashboards.chart_sources[this.chart_doc.source]) {
|
||||
this.settings = frappe.dashboards.chart_sources[this.chart_doc.source];
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
const method = 'frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config';
|
||||
return frappe.xcall(method, {name: this.chart_doc.source}).then(config => {
|
||||
frappe.dom.eval(config);
|
||||
this.settings = frappe.dashboards.chart_sources[this.chart_doc.source];
|
||||
});
|
||||
}
|
||||
} else if (this.chart_doc.chart_type == 'Report') {
|
||||
this.settings = {
|
||||
'method': 'frappe.desk.query_report.run'
|
||||
};
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -252,7 +252,7 @@ frappe.ui.GroupBy = class {
|
|||
this.group_by_fields = {};
|
||||
this.all_fields = {};
|
||||
|
||||
let fields = this.report_view.meta.fields.filter(f => ["Select", "Link", "Data", "Int"].includes(f.fieldtype));
|
||||
let fields = this.report_view.meta.fields.filter(f => ["Select", "Link", "Data", "Int", "Check"].includes(f.fieldtype));
|
||||
this.group_by_fields[this.doctype] = fields;
|
||||
this.all_fields[this.doctype] = this.report_view.meta.fields;
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,44 @@ frappe.utils.xss_sanitise = function (string, options) {
|
|||
return sanitised;
|
||||
}
|
||||
|
||||
frappe.utils.sanitise_redirect = (url) => {
|
||||
const is_absolute = ((url) => {
|
||||
// https://github.com/sindresorhus/is-absolute-url
|
||||
// Don't match Windows paths `c:\`
|
||||
if (/^[a-zA-Z]:\\/.test(url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scheme: https://tools.ietf.org/html/rfc3986#section-3.1
|
||||
// Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3
|
||||
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url);
|
||||
});
|
||||
|
||||
const is_external = (() => {
|
||||
return (url) => {
|
||||
function domain(url) {
|
||||
let base_domain = /https?:\/\/((?:[\w\d]+\.)+[\w\d]{2,})/i.exec(url);
|
||||
return base_domain == null ? "" : base_domain[1];
|
||||
}
|
||||
|
||||
return domain(location.href) !== domain(url);
|
||||
}
|
||||
})();
|
||||
|
||||
const sanitise_javascript = ((url) => {
|
||||
// please do not ask how or why
|
||||
const REGEX_SCRIPT = /j[\s]*(&#x.{1,7})?a[\s]*(&#x.{1,7})?v[\s]*(&#x.{1,7})?a[\s]*(&#x.{1,7})?s[\s]*(&#x.{1,7})?c[\s]*(&#x.{1,7})?r[\s]*(&#x.{1,7})?i[\s]*(&#x.{1,7})?p[\s]*(&#x.{1,7})?t/gi;
|
||||
|
||||
return url.replace(REGEX_SCRIPT, "");
|
||||
});
|
||||
|
||||
if (is_absolute(url) && is_external(url)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sanitise_javascript(frappe.utils.xss_sanitise(url, {strategies: ["js"]}));
|
||||
}
|
||||
|
||||
frappe.utils.new_auto_repeat_prompt = function(frm) {
|
||||
const fields = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ frappe.breadcrumbs = {
|
|||
'Workflow': 'Settings',
|
||||
'Printing': 'Settings',
|
||||
'Setup': 'Settings',
|
||||
'Event Streaming': 'Automation'
|
||||
'Event Streaming': 'Tools',
|
||||
'Automation': 'Tools',
|
||||
},
|
||||
|
||||
set_doctype_module: function(doctype, module) {
|
||||
|
|
@ -95,7 +96,7 @@ frappe.breadcrumbs = {
|
|||
|
||||
|
||||
if(module_info && !module_info.blocked && frappe.visible_modules.includes(module_info.module_name)) {
|
||||
$(repl('<li><a href="#modules/%(module)s">%(label)s</a></li>',
|
||||
$(repl('<li><a href="#workspace/%(module)s">%(label)s</a></li>',
|
||||
{ module: breadcrumbs.module, label: __(label) }))
|
||||
.appendTo($breadcrumbs);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
import ChartWidget from "../widgets/chart_widget";
|
||||
import WidgetGroup from "../widgets/widget_group";
|
||||
|
||||
export default class Desktop {
|
||||
constructor({ wrapper }) {
|
||||
this.wrapper = wrapper;
|
||||
|
|
@ -18,12 +15,9 @@ export default class Desktop {
|
|||
|
||||
make() {
|
||||
this.make_container();
|
||||
// this.show_loading_state();
|
||||
this.fetch_desktop_settings().then(() => {
|
||||
this.route();
|
||||
this.make_sidebar();
|
||||
this.setup_events();
|
||||
// this.hide_loading_state();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -43,25 +37,6 @@ export default class Desktop {
|
|||
this.body = this.container.find(".desk-body");
|
||||
}
|
||||
|
||||
show_loading_state() {
|
||||
// Add skeleton
|
||||
let loading_sidebar = $(
|
||||
'<div class="skeleton skeleton-full" style="height: 90vh;"></div>'
|
||||
);
|
||||
let loading_body = $(
|
||||
`<div class="skeleton skeleton-full" style="height: 90vh;"></div>`
|
||||
);
|
||||
|
||||
// Append skeleton to body
|
||||
loading_sidebar.appendTo(this.sidebar);
|
||||
loading_body.appendTo(this.body);
|
||||
}
|
||||
|
||||
hide_loading_state() {
|
||||
// Remove all skeleton
|
||||
this.container.find(".skeleton").remove();
|
||||
}
|
||||
|
||||
fetch_desktop_settings() {
|
||||
return frappe
|
||||
.call("frappe.desk.desktop.get_desk_sidebar_items")
|
||||
|
|
@ -137,7 +112,9 @@ export default class Desktop {
|
|||
}
|
||||
|
||||
get_page_to_show() {
|
||||
const default_page = this.desktop_settings ? this.desktop_settings["Modules"][0].name : "Website";
|
||||
const default_page = this.desktop_settings
|
||||
? this.desktop_settings["Modules"][0].name
|
||||
: "Website";
|
||||
let page =
|
||||
frappe.get_route()[1] ||
|
||||
localStorage.current_desk_page ||
|
||||
|
|
@ -155,13 +132,7 @@ export default class Desktop {
|
|||
return $page;
|
||||
}
|
||||
|
||||
setup_events() {
|
||||
$(document).keydown(e => {
|
||||
if (e.keyCode == 9) {
|
||||
console.log("navigate");
|
||||
}
|
||||
});
|
||||
}
|
||||
setup_events() {}
|
||||
}
|
||||
|
||||
class DesktopPage {
|
||||
|
|
@ -169,7 +140,7 @@ class DesktopPage {
|
|||
this.container = container;
|
||||
this.page_name = page_name;
|
||||
this.sections = {};
|
||||
this.allow_customization = false
|
||||
this.allow_customization = false;
|
||||
this.make();
|
||||
}
|
||||
|
||||
|
|
@ -185,10 +156,10 @@ class DesktopPage {
|
|||
this.make_page();
|
||||
this.get_data().then(res => {
|
||||
this.data = res.message;
|
||||
// this.make_onboarding()
|
||||
// this.make_onboarding();
|
||||
if (!this.data) {
|
||||
delete localStorage.current_desk_page;
|
||||
frappe.set_route('workspace');
|
||||
frappe.set_route("workspace");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +187,7 @@ class DesktopPage {
|
|||
}
|
||||
|
||||
make_onboarding() {
|
||||
this.sections["onboarding"] = new WidgetGroup({
|
||||
this.sections["onboarding"] = new frappe.widget.WidgetGroup({
|
||||
title: `Getting Started`,
|
||||
container: this.page,
|
||||
type: "onboarding",
|
||||
|
|
@ -253,7 +224,7 @@ class DesktopPage {
|
|||
}
|
||||
|
||||
make_charts() {
|
||||
this.sections["charts"] = new WidgetGroup({
|
||||
this.sections["charts"] = new frappe.widget.WidgetGroup({
|
||||
title: this.data.charts.label || `${this.page_name} Dashboard`,
|
||||
container: this.page,
|
||||
type: "chart",
|
||||
|
|
@ -264,7 +235,7 @@ class DesktopPage {
|
|||
}
|
||||
|
||||
make_shortcuts() {
|
||||
this.sections["shortcuts"] = new WidgetGroup({
|
||||
this.sections["shortcuts"] = new frappe.widget.WidgetGroup({
|
||||
title: this.data.shortcuts.label || `Your Shortcuts`,
|
||||
container: this.page,
|
||||
type: "bookmark",
|
||||
|
|
@ -275,7 +246,7 @@ class DesktopPage {
|
|||
}
|
||||
|
||||
make_cards() {
|
||||
let cards = new WidgetGroup({
|
||||
let cards = new frappe.widget.WidgetGroup({
|
||||
title: this.data.cards.label || `Reports & Masters`,
|
||||
container: this.page,
|
||||
type: "links",
|
||||
|
|
@ -310,4 +281,4 @@ class DesktopPage {
|
|||
${legend.join("\n")}
|
||||
</div>`).insertAfter(cards.body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,26 +164,4 @@ frappe.show_message_page = function(opts) {
|
|||
);
|
||||
|
||||
frappe.container.change_to(opts.page_name);
|
||||
};
|
||||
|
||||
frappe.views.ModulesFactory = class ModulesFactory extends frappe.views.Factory {
|
||||
show() {
|
||||
if (frappe.pages.modules) {
|
||||
frappe.container.change_to('modules');
|
||||
} else {
|
||||
this.make('modules');
|
||||
}
|
||||
}
|
||||
|
||||
make(page_name) {
|
||||
const assets = [
|
||||
'/assets/js/modules.min.js'
|
||||
];
|
||||
|
||||
frappe.require(assets, () => {
|
||||
frappe.modules.home = new frappe.modules.Home({
|
||||
parent: this.make_page(true, page_name)
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// MIT License. See license.txt
|
||||
import DataTable from 'frappe-datatable';
|
||||
import { build_summary_item } from "../../widgets/utils";
|
||||
|
||||
frappe.provide('frappe.views');
|
||||
frappe.provide('frappe.query_reports');
|
||||
|
|
@ -196,8 +197,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
let x_field_title = toTitle(chart_args.x_field);
|
||||
let y_field_title = toTitle(chart_args.y_fields[0]);
|
||||
chart_name = chart_name || (`${this.report_name}: ${x_field_title} vs ${y_field_title}`);
|
||||
|
||||
Object.assign(args,
|
||||
|
||||
Object.assign(args,
|
||||
{
|
||||
'chart_name': chart_name,
|
||||
'x_field': chart_args.x_field,
|
||||
|
|
@ -209,7 +210,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
);
|
||||
} else {
|
||||
chart_name = chart_name || this.report_name;
|
||||
Object.assign(args,
|
||||
Object.assign(args,
|
||||
{
|
||||
'chart_name': chart_name,
|
||||
'is_custom': 1
|
||||
|
|
@ -218,7 +219,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
frappe.xcall(
|
||||
'frappe.desk.doctype.dashboard_chart.dashboard_chart.create_report_chart',
|
||||
'frappe.desk.doctype.dashboard_chart.dashboard_chart.create_report_chart',
|
||||
{args: args}
|
||||
).then( () => {
|
||||
let message;
|
||||
|
|
@ -297,6 +298,55 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
}, 1000);
|
||||
}
|
||||
|
||||
refresh_filters_dependency() {
|
||||
this.filters.forEach(filter => {
|
||||
filter.guardian_has_value = true;
|
||||
|
||||
if (filter.df.depends_on) {
|
||||
filter.guardian_has_value =
|
||||
this.evaluate_depends_on_value(filter.df.depends_on, filter.df.label);
|
||||
|
||||
if (filter.guardian_has_value) {
|
||||
if (filter.df.hidden_due_to_dependency) {
|
||||
filter.df.hidden_due_to_dependency = false;
|
||||
this.toggle_filter_display(filter.df.fieldname, false);
|
||||
}
|
||||
} else {
|
||||
if (!filter.df.hidden_due_to_dependency) {
|
||||
filter.df.hidden_due_to_dependency = true;
|
||||
this.toggle_filter_display(filter.df.fieldname, true);
|
||||
filter.set_value(filter.df.default || null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
evaluate_depends_on_value(expression, filter_label) {
|
||||
let out = null;
|
||||
let filters = this.get_filter_values();
|
||||
if (filters) {
|
||||
if (typeof expression === 'boolean') {
|
||||
out = expression;
|
||||
} else if (expression.substr(0, 5) == 'eval:') {
|
||||
try {
|
||||
out = eval(expression.substr(5));
|
||||
} catch (e) {
|
||||
frappe.throw(__(`Invalid "depends_on" expression set in filter ${filter_label}`));
|
||||
}
|
||||
} else {
|
||||
var value = filters[expression];
|
||||
if ($.isArray(value)) {
|
||||
out = !!value.length;
|
||||
} else {
|
||||
out = !!value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
setup_filters() {
|
||||
this.clear_filters();
|
||||
const { filters = [] } = this.report_settings;
|
||||
|
|
@ -314,6 +364,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
if (df.on_change) f.on_change = df.on_change;
|
||||
|
||||
df.onchange = () => {
|
||||
this.refresh_filters_dependency();
|
||||
|
||||
let current_filters = this.get_filter_value();
|
||||
if (this.previous_filters
|
||||
&& (JSON.stringify(this.previous_filters) === JSON.stringify(current_filters))) {
|
||||
|
|
@ -343,6 +395,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
|
||||
}).filter(Boolean);
|
||||
|
||||
this.refresh_filters_dependency();
|
||||
if (this.filters.length === 0) {
|
||||
// hide page form if no filters
|
||||
this.page.hide_form();
|
||||
|
|
@ -451,7 +504,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
else {
|
||||
this.$chart.empty();
|
||||
if (this.chart_fields) {
|
||||
this.chart_options =
|
||||
this.chart_options =
|
||||
frappe.report_utils.make_chart_options(
|
||||
this.columns,
|
||||
this.raw_data,
|
||||
|
|
@ -474,24 +527,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
render_summary(data) {
|
||||
let build_summary_item = (summary) => {
|
||||
let df = {fieldtype: summary.datatype};
|
||||
let doc = null;
|
||||
|
||||
if (summary.datatype == "Currency") {
|
||||
df.options = "currency";
|
||||
doc = {currency: summary.currency};
|
||||
}
|
||||
|
||||
let value = frappe.format(summary.value, df, null, doc);
|
||||
let indicator = summary.indicator ? `indicator ${ summary.indicator.toLowerCase() }` : '';
|
||||
|
||||
return $(`<div class="summary-item">
|
||||
<span class="summary-label small text-muted ${indicator}">${summary.label}</span>
|
||||
<h1 class="summary-value">${ value }</h1>
|
||||
</div>`);
|
||||
};
|
||||
|
||||
data.forEach((summary) => {
|
||||
build_summary_item(summary).appendTo(this.$summary);
|
||||
})
|
||||
|
|
@ -673,7 +708,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
});
|
||||
}
|
||||
|
||||
values.y_fields =
|
||||
values.y_fields =
|
||||
values.y_fields
|
||||
.map(d => d.trim())
|
||||
.filter(Boolean);
|
||||
|
|
@ -698,7 +733,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
dialog.refresh();
|
||||
}
|
||||
else {
|
||||
wrapper[0].innerHTML =
|
||||
wrapper[0].innerHTML =
|
||||
`<div class="flex justify-center align-center text-muted" style="height: 120px; display: flex;">
|
||||
<div>Please select X and Y fields</div>
|
||||
</div>`;
|
||||
|
|
@ -712,7 +747,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
fieldname: 'x_field',
|
||||
label: 'X Field',
|
||||
fieldtype: 'Select',
|
||||
default: me.chart_fields? me.chart_fields.x_field: null,
|
||||
default: me.chart_fields? me.chart_fields.x_field: null,
|
||||
options: field_options.non_numeric_fields,
|
||||
},
|
||||
{
|
||||
|
|
@ -782,7 +817,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
primary_action: (values) => {
|
||||
values = set_chart_values(values);
|
||||
|
||||
let options =
|
||||
let options =
|
||||
frappe.report_utils.make_chart_options(
|
||||
this.columns,
|
||||
this.raw_data,
|
||||
|
|
@ -791,11 +826,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
me.chart_fields = values
|
||||
|
||||
let x_field_label =
|
||||
field_options.numeric_fields.filter(field =>
|
||||
field_options.numeric_fields.filter(field =>
|
||||
field.value == values.y_fields[0]
|
||||
)[0].label;
|
||||
let y_field_label =
|
||||
field_options.non_numeric_fields.filter(field =>
|
||||
field_options.non_numeric_fields.filter(field =>
|
||||
field.value == values.x_field
|
||||
)[0].label;
|
||||
|
||||
|
|
@ -1489,6 +1524,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
}
|
||||
}
|
||||
|
||||
toggle_filter_display(fieldname, flag) {
|
||||
this.$page.find(`div[data-fieldname=${fieldname}]`).toggleClass('hide-control', flag);
|
||||
}
|
||||
|
||||
toggle_report(flag) {
|
||||
this.$report.toggle(flag);
|
||||
this.$chart.toggle(flag);
|
||||
|
|
|
|||
|
|
@ -112,13 +112,14 @@ frappe.report_utils = {
|
|||
|
||||
return frappe.xcall(
|
||||
'frappe.desk.query_report.get_script',
|
||||
{
|
||||
{
|
||||
report_name: report_name
|
||||
}
|
||||
).then(r => {
|
||||
frappe.dom.eval(r.script || '');
|
||||
let filters = frappe.query_reports[report_name].filters;
|
||||
return Promise.resolve(filters);
|
||||
return frappe.after_ajax(() => {
|
||||
return frappe.query_reports[report_name].filters;
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
import Widget from "./base_widget.js";
|
||||
|
||||
export default class ChartWidget extends Widget {
|
||||
constructor(opts) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
//
|
||||
}
|
||||
|
||||
customize() {
|
||||
this.setup_customize_actions();
|
||||
}
|
||||
|
||||
make_chart() {
|
||||
this.body.empty()
|
||||
frappe.model.with_doc("Dashboard Chart", this.chart_name).then(chart_doc => {
|
||||
chart_doc.width = 'Full'
|
||||
this.dashboard = new frappe.ui.DashboardChart(chart_doc, this.body, { hide_title: true, hide_last_sync: true, hide_actions: true });
|
||||
this.dashboard.show();
|
||||
});
|
||||
|
||||
this.summary && this.set_summary();
|
||||
}
|
||||
|
||||
set_body() {
|
||||
this.widget.addClass('dashboard-widget-box')
|
||||
this.make_chart();
|
||||
}
|
||||
|
||||
set_summary() {
|
||||
let summary = $(`<span class="dashboard-summary">$ 54,231</span>`);
|
||||
this.title_field.addClass('text-muted')
|
||||
summary.appendTo(this.body);
|
||||
}
|
||||
|
||||
setup_events() {
|
||||
//
|
||||
}
|
||||
|
||||
setup_customize_actions() {
|
||||
this.action_area.empty()
|
||||
const buttons = $(`<button type="button" class="btn btn-xs btn-secondary btn-default selected">Resize</button>
|
||||
<button class="btn btn-secondary btn-light btn-danger btn-xs"><i class="fa fa-trash" aria-hidden="true"></i></button>`);
|
||||
buttons.appendTo(this.action_area);
|
||||
}
|
||||
|
||||
set_actions() {
|
||||
return
|
||||
this.action_area.empty()
|
||||
const buttons = $(`<div class="btn-group btn-group-xs" role="group" aria-label="Basic example">
|
||||
<button type="button" class="btn btn-secondary btn-default selected">Monthly</button>
|
||||
<button type="button" class="btn btn-secondary btn-default">Quaterly</button>
|
||||
<button type="button" class="btn btn-secondary btn-default">Yearly</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-light btn-default btn-xs"><i class="fa fa-refresh" aria-hidden="true"></i></button>`);
|
||||
buttons.appendTo(this.action_area);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@ export default class Widget {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
//
|
||||
this.set_title();
|
||||
this.set_actions();
|
||||
this.set_body();
|
||||
}
|
||||
|
||||
customize() {
|
||||
|
|
@ -26,12 +28,15 @@ export default class Widget {
|
|||
</div>
|
||||
<div class="widget-body">
|
||||
</div>
|
||||
<div class="widget-footer">
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
this.title_field = this.widget.find(".widget-title");
|
||||
this.body = this.widget.find(".widget-body");
|
||||
this.action_area = this.widget.find(".widget-control");
|
||||
this.head = this.widget.find(".widget-head");
|
||||
this.footer = this.widget.find(".widget-footer");
|
||||
this.set_title();
|
||||
this.set_actions();
|
||||
this.set_body();
|
||||
526
frappe/public/js/frappe/widgets/chart_widget.js
Normal file
526
frappe/public/js/frappe/widgets/chart_widget.js
Normal file
|
|
@ -0,0 +1,526 @@
|
|||
import Widget from "./base_widget.js";
|
||||
import { build_summary_item } from "./utils";
|
||||
frappe.provide("frappe.dashboards");
|
||||
frappe.provide("frappe.dashboards.chart_sources");
|
||||
|
||||
export default class ChartWidget extends Widget {
|
||||
constructor(opts) {
|
||||
super(opts);
|
||||
this.height = 240;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.make_chart();
|
||||
}
|
||||
|
||||
customize() {
|
||||
this.setup_customize_actions();
|
||||
}
|
||||
|
||||
set_body() {
|
||||
this.widget.addClass("dashboard-widget-box");
|
||||
if (this.width == "Full") {
|
||||
this.widget.addClass("full-width");
|
||||
}
|
||||
this.make_chart();
|
||||
}
|
||||
|
||||
setup_container() {
|
||||
this.body.empty();
|
||||
|
||||
this.loading = $(
|
||||
`<div class="chart-loading-state text-muted" style="height: ${this.height}px;">${__(
|
||||
"Loading..."
|
||||
)}</div>`
|
||||
);
|
||||
this.loading.hide().appendTo(this.body);
|
||||
|
||||
this.empty = $(
|
||||
`<div class="chart-loading-state text-muted" style="height: ${this.height}px;">${__(
|
||||
"No Data..."
|
||||
)}</div>`
|
||||
);
|
||||
this.empty.hide().appendTo(this.body);
|
||||
|
||||
this.chart_wrapper = $(`<div></div>`);
|
||||
this.chart_wrapper.appendTo(this.body);
|
||||
}
|
||||
|
||||
set_summary() {
|
||||
if (!this.$summary) {
|
||||
this.$summary = $(`<div class="report-summary"></div>`).hide();
|
||||
this.head.after(this.$summary);
|
||||
} else {
|
||||
this.$summary.empty();
|
||||
}
|
||||
|
||||
this.summary.forEach(summary => {
|
||||
build_summary_item(summary).appendTo(this.$summary);
|
||||
});
|
||||
this.summary.length && this.$summary.show();
|
||||
}
|
||||
|
||||
make_chart() {
|
||||
this.get_settings().then(() => {
|
||||
this.setup_container();
|
||||
this.prepare_chart_object();
|
||||
this.action_area.empty();
|
||||
this.prepare_chart_actions();
|
||||
this.setup_filter_button();
|
||||
|
||||
if (
|
||||
this.chart_doc.timeseries &&
|
||||
this.chart_doc.chart_type !== "Custom"
|
||||
) {
|
||||
this.render_time_series_filters();
|
||||
}
|
||||
|
||||
this.fetch_and_update_chart();
|
||||
});
|
||||
}
|
||||
|
||||
setup_customize_actions() {
|
||||
this.action_area.empty();
|
||||
const buttons = $(`<button type="button" class="btn btn-xs btn-secondary btn-default selected">Resize</button>
|
||||
<button class="btn btn-secondary btn-light btn-danger btn-xs"><i class="fa fa-trash" aria-hidden="true"></i></button>`);
|
||||
buttons.appendTo(this.action_area);
|
||||
}
|
||||
|
||||
render_time_series_filters() {
|
||||
let filters = [
|
||||
{
|
||||
label: 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.fetch_and_update_chart();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: this.chart_doc.time_interval,
|
||||
options: ["Yearly", "Quarterly", "Monthly", "Weekly", "Daily"],
|
||||
action: selected_item => {
|
||||
this.selected_time_interval = selected_item;
|
||||
this.fetch_and_update_chart();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
frappe.dashboard_utils.render_chart_filters(
|
||||
filters,
|
||||
"chart-actions",
|
||||
this.action_area,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
fetch_and_update_chart() {
|
||||
this.args = {
|
||||
timespan: this.selected_timespan,
|
||||
time_interval: this.selected_time_interval,
|
||||
from_date: this.selected_from_date,
|
||||
to_date: this.selected_to_date
|
||||
};
|
||||
|
||||
this.fetch(this.filters, true, this.args).then(data => {
|
||||
if (this.chart_doc.chart_type == "Report") {
|
||||
this.summary = data.report_summary;
|
||||
data = this.get_report_chart_data(data);
|
||||
}
|
||||
|
||||
this.update_chart_object();
|
||||
this.data = data;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
render_date_range_fields() {
|
||||
if (
|
||||
!this.date_field_wrapper ||
|
||||
!this.date_field_wrapper.is(":visible")
|
||||
) {
|
||||
this.date_field_wrapper = $(
|
||||
`<div class="dashboard-date-field pull-right"></div>`
|
||||
).appendTo(this.action_area);
|
||||
|
||||
if (this.width != "Full" && this.widget.width() < 700) {
|
||||
this.title_field.hide();
|
||||
this.head.css('flex-direction', "row-reverse");
|
||||
}
|
||||
|
||||
this.date_range_field = frappe.ui.form.make_control({
|
||||
df: {
|
||||
fieldtype: "DateRange",
|
||||
fieldname: "from_date",
|
||||
placeholder: "Date Range",
|
||||
input_class: "input-xs",
|
||||
reqd: 1,
|
||||
change: () => {
|
||||
let selected_date_range = this.date_range_field.get_value();
|
||||
this.selected_from_date = selected_date_range[0];
|
||||
this.selected_to_date = selected_date_range[1];
|
||||
|
||||
if (
|
||||
selected_date_range &&
|
||||
selected_date_range.length == 2
|
||||
) {
|
||||
this.fetch_and_update_chart();
|
||||
}
|
||||
}
|
||||
},
|
||||
parent: this.date_field_wrapper,
|
||||
render_input: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_report_chart_data(result) {
|
||||
if (result.chart && this.chart_doc.is_custom) {
|
||||
return result.chart.data;
|
||||
} else {
|
||||
let y_fields = [];
|
||||
this.chart_doc.y_axis.map(field => {
|
||||
y_fields.push(field.y_field);
|
||||
});
|
||||
|
||||
let chart_fields = {
|
||||
y_fields: y_fields,
|
||||
x_field: this.chart_doc.x_field,
|
||||
chart_type: this.chart_doc.type,
|
||||
color: this.chart_doc.color
|
||||
};
|
||||
let columns = result.columns.map(col => {
|
||||
return frappe.report_utils.prepare_field_from_column(col);
|
||||
});
|
||||
|
||||
let data = frappe.report_utils.make_chart_options(
|
||||
columns,
|
||||
result,
|
||||
chart_fields
|
||||
).data;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
prepare_chart_actions() {
|
||||
let actions = [
|
||||
{
|
||||
label: __("Refresh"),
|
||||
action: "action-refresh",
|
||||
handler: () => {
|
||||
delete this.dashboard_chart;
|
||||
this.make_chart();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Edit..."),
|
||||
action: "action-edit",
|
||||
handler: () => {
|
||||
frappe.set_route(
|
||||
"Form",
|
||||
"Dashboard Chart",
|
||||
this.chart_doc.name
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (this.chart_doc.document_type) {
|
||||
actions.push({
|
||||
label: __("{0} List", [this.chart_doc.document_type]),
|
||||
action: "action-list",
|
||||
handler: () => {
|
||||
frappe.set_route("List", this.chart_doc.document_type);
|
||||
}
|
||||
});
|
||||
} else if (this.chart_doc.chart_type === "Report") {
|
||||
actions.push({
|
||||
label: __("{0} Report", [this.chart_doc.report_name]),
|
||||
action: "action-list",
|
||||
handler: () => {
|
||||
frappe.set_route(
|
||||
"query-report",
|
||||
this.chart_doc.report_name
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.set_chart_actions(actions);
|
||||
}
|
||||
|
||||
setup_filter_button() {
|
||||
this.is_document_type =
|
||||
this.chart_doc.chart_type !== "Report" &&
|
||||
this.chart_doc.chart_type !== "Custom";
|
||||
this.filter_button = $(
|
||||
`<div class="filter-chart btn btn-default btn-xs pull-right">${__(
|
||||
"Filter"
|
||||
)}</div>`
|
||||
);
|
||||
this.filter_button.appendTo(this.action_area);
|
||||
|
||||
this.filter_button.on("click", () => {
|
||||
let fields;
|
||||
|
||||
frappe.dashboard_utils
|
||||
.get_filters_for_chart_type(this.chart_doc)
|
||||
.then(filters => {
|
||||
if (!this.is_document_type) {
|
||||
if (!filters) {
|
||||
fields = [
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
options: __("No Filters Set")
|
||||
}
|
||||
];
|
||||
} 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;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
fields = [
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
fieldname: "filter_area"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
this.setup_filter_dialog(fields);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setup_filter_dialog(fields) {
|
||||
let me = this;
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __(`Set Filters for ${this.chart_doc.chart_name}`),
|
||||
fields: fields,
|
||||
primary_action: function() {
|
||||
let values = this.get_values();
|
||||
if (values) {
|
||||
this.hide();
|
||||
if (me.is_document_type) {
|
||||
me.filters = me.filter_group.get_filters();
|
||||
} else {
|
||||
me.filters = values;
|
||||
}
|
||||
me.fetch_and_update_chart();
|
||||
}
|
||||
},
|
||||
primary_action_label: "Set"
|
||||
});
|
||||
|
||||
if (this.is_document_type) {
|
||||
this.create_filter_group_and_add_filters(
|
||||
dialog.get_field("filter_area").$wrapper
|
||||
);
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
dialog.set_values(this.filters);
|
||||
}
|
||||
|
||||
create_filter_group_and_add_filters(parent) {
|
||||
this.filter_group = new frappe.ui.FilterGroup({
|
||||
parent: parent,
|
||||
doctype: this.chart_doc.document_type,
|
||||
on_change: () => {}
|
||||
});
|
||||
|
||||
frappe.model.with_doctype(this.chart_doc.document_type, () => {
|
||||
this.filter_group.add_filters_to_filter_group(this.filters);
|
||||
});
|
||||
}
|
||||
|
||||
set_chart_actions(actions) {
|
||||
/* eslint-disable indent */
|
||||
this.chart_actions = $(`<div class="chart-actions dropdown pull-right">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-default btn-xs"><span class="caret"></span></button>
|
||||
</a>
|
||||
<ul class="dropdown-menu" style="max-height: 300px; overflow-y: auto;">
|
||||
${actions
|
||||
.map(
|
||||
action =>
|
||||
`<li><a data-action="${action.action}">${
|
||||
action.label
|
||||
}</a></li>`
|
||||
)
|
||||
.join("")}
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
/* eslint-enable indent */
|
||||
|
||||
this.chart_actions.find("a[data-action]").each((i, o) => {
|
||||
const action = o.dataset.action;
|
||||
$(o).click(actions.find(a => a.action === action));
|
||||
});
|
||||
this.chart_actions.appendTo(this.action_area);
|
||||
}
|
||||
|
||||
fetch(filters, refresh = false, args) {
|
||||
let method = this.settings
|
||||
? this.settings.method
|
||||
: "frappe.desk.doctype.dashboard_chart.dashboard_chart.get";
|
||||
|
||||
if (this.chart_doc.chart_type == "Report") {
|
||||
args = {
|
||||
report_name: this.chart_doc.report_name,
|
||||
filters: filters,
|
||||
ignore_prepared_report: 1
|
||||
};
|
||||
} else {
|
||||
args = {
|
||||
chart_name: this.chart_doc.name,
|
||||
filters: filters,
|
||||
refresh: refresh ? 1 : 0,
|
||||
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
|
||||
};
|
||||
}
|
||||
return frappe.xcall(method, args);
|
||||
}
|
||||
|
||||
render() {
|
||||
const chart_type_map = {
|
||||
Line: "line",
|
||||
Bar: "bar",
|
||||
Percentage: "percentage",
|
||||
Pie: "pie"
|
||||
};
|
||||
|
||||
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"];
|
||||
}
|
||||
|
||||
if (!this.data || !this.data.labels.length || !Object.keys(this.data).length) {
|
||||
this.chart_wrapper.hide();
|
||||
this.loading.hide();
|
||||
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.dashboard_chart) {
|
||||
this.dashboard_chart = new frappe.Chart(
|
||||
this.chart_wrapper[0],
|
||||
chart_args
|
||||
);
|
||||
} else {
|
||||
this.dashboard_chart.update(this.data);
|
||||
}
|
||||
this.width == "Full" && this.summary && this.set_summary();
|
||||
}
|
||||
}
|
||||
|
||||
update_last_synced() {
|
||||
let last_synced_text = __("Last synced {0}", [
|
||||
comment_when(this.chart_doc.last_synced_on)
|
||||
]);
|
||||
this.footer.html(last_synced_text);
|
||||
}
|
||||
|
||||
update_chart_object() {
|
||||
frappe.db.get_doc("Dashboard Chart", this.chart_doc.name).then(doc => {
|
||||
this.chart_doc = doc;
|
||||
this.prepare_chart_object();
|
||||
this.update_last_synced();
|
||||
});
|
||||
}
|
||||
|
||||
prepare_chart_object() {
|
||||
this.filters =
|
||||
this.filters || JSON.parse(this.chart_doc.filters_json || "[]");
|
||||
}
|
||||
|
||||
get_settings() {
|
||||
return frappe.model
|
||||
.with_doc("Dashboard Chart", this.chart_name)
|
||||
.then(chart_doc => {
|
||||
this.chart_doc = chart_doc;
|
||||
if (this.chart_doc.chart_type == "Custom") {
|
||||
// custom source
|
||||
if (
|
||||
frappe.dashboards.chart_sources[this.chart_doc.source]
|
||||
) {
|
||||
this.settings =
|
||||
frappe.dashboards.chart_sources[
|
||||
this.chart_doc.source
|
||||
];
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
const method =
|
||||
"frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config";
|
||||
return frappe
|
||||
.xcall(method, { name: this.chart_doc.source })
|
||||
.then(config => {
|
||||
frappe.dom.eval(config);
|
||||
this.settings =
|
||||
frappe.dashboards.chart_sources[
|
||||
this.chart_doc.source
|
||||
];
|
||||
});
|
||||
}
|
||||
} else if (this.chart_doc.chart_type == "Report") {
|
||||
this.settings = {
|
||||
method: "frappe.desk.query_report.run"
|
||||
};
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -95,36 +95,22 @@ function generate_grid(data) {
|
|||
return grid_template_area
|
||||
}
|
||||
|
||||
// function get_luminosity(color) {
|
||||
// let c = color.substring(1); // strip #
|
||||
// let rgb = parseInt(c, 16); // convert rrggbb to decimal
|
||||
// let r = (rgb >> 16) & 0xff; // extract red
|
||||
// let g = (rgb >> 8) & 0xff; // extract green
|
||||
// let b = (rgb >> 0) & 0xff; // extract blue
|
||||
const build_summary_item = (summary) => {
|
||||
let df = {fieldtype: summary.datatype};
|
||||
let doc = null;
|
||||
|
||||
// let luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
|
||||
if (summary.datatype == "Currency") {
|
||||
df.options = "currency";
|
||||
doc = {currency: summary.currency};
|
||||
}
|
||||
|
||||
// return luma
|
||||
// }
|
||||
let value = frappe.format(summary.value, df, null, doc);
|
||||
let indicator = summary.indicator ? `indicator ${ summary.indicator.toLowerCase() }` : '';
|
||||
|
||||
// function shadeColor(color, percent) {
|
||||
// var R = parseInt(color.substring(1,3),16);
|
||||
// var G = parseInt(color.substring(3,5),16);
|
||||
// var B = parseInt(color.substring(5,7),16);
|
||||
return $(`<div class="summary-item">
|
||||
<span class="summary-label small text-muted ${indicator}">${summary.label}</span>
|
||||
<h1 class="summary-value">${ value }</h1>
|
||||
</div>`);
|
||||
};
|
||||
|
||||
// R = parseInt(R * (100 + percent) / 100);
|
||||
// G = parseInt(G * (100 + percent) / 100);
|
||||
// B = parseInt(B * (100 + percent) / 100);
|
||||
|
||||
// R = (R<255)?R:255;
|
||||
// G = (G<255)?G:255;
|
||||
// B = (B<255)?B:255;
|
||||
|
||||
// var RR = ((R.toString(16).length==1)?"0"+R.toString(16):R.toString(16));
|
||||
// var GG = ((G.toString(16).length==1)?"0"+G.toString(16):G.toString(16));
|
||||
// var BB = ((B.toString(16).length==1)?"0"+B.toString(16):B.toString(16));
|
||||
|
||||
// return "#"+RR+GG+BB;
|
||||
// }
|
||||
|
||||
export { generate_route, generate_grid };
|
||||
export { generate_route, generate_grid, build_summary_item };
|
||||
|
|
@ -4,6 +4,8 @@ import ShortcutWidget from "../widgets/shortcut_widget";
|
|||
import LinksWidget from "../widgets/links_widget";
|
||||
import OnboardingWidget from "../widgets/onboarding_widget";
|
||||
|
||||
frappe.provide('frappe.widget')
|
||||
|
||||
const widget_factory = {
|
||||
chart: ChartWidget,
|
||||
base: BaseWidget,
|
||||
|
|
@ -86,4 +88,6 @@ export default class WidgetGroup {
|
|||
// onStart: (evt) => this.sortable_config.on_start(evt, container)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frappe.widget.WidgetGroup = WidgetGroup;
|
||||
|
|
@ -76,31 +76,6 @@
|
|||
position: relative;
|
||||
min-height: 1px;
|
||||
padding-right: 15px;
|
||||
|
||||
.dashboard-summary {
|
||||
font-size: 32px !important;
|
||||
margin-top: 0px !important;
|
||||
margin-bottom: 8px !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.grid-col-3 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
// grid-auto-rows: minmax(62px, 1fr);
|
||||
column-gap: 15px;
|
||||
row-gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.grid-col-1 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1fr);
|
||||
// grid-auto-rows: minmax(62px, 1fr);
|
||||
column-gap: 15px;
|
||||
row-gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
|
@ -138,6 +113,38 @@
|
|||
}
|
||||
}
|
||||
|
||||
.grid-col-3 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
// grid-auto-rows: minmax(62px, 1fr);
|
||||
column-gap: 15px;
|
||||
row-gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.grid-col-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
// grid-auto-rows: minmax(62px, 1fr);
|
||||
column-gap: 15px;
|
||||
row-gap: 15px;
|
||||
align-items: center;
|
||||
|
||||
.full-width {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-col-1 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(550px, 1fr));
|
||||
// grid-auto-rows: minmax(62px, 1fr);
|
||||
column-gap: 15px;
|
||||
row-gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.legend {
|
||||
display: flex;
|
||||
|
|
@ -147,6 +154,18 @@
|
|||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-col-2 {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
.full-width {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-col-1 {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +192,26 @@
|
|||
}
|
||||
.widget-control {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
// Any immidiate child
|
||||
>* {
|
||||
align-self: center;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.dashboard-date-field {
|
||||
.clearfix,
|
||||
.help-box {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.frappe-control,
|
||||
.form-group {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,15 +222,32 @@
|
|||
|
||||
// Overrides for each widgets
|
||||
&.dashboard-widget-box {
|
||||
padding-bottom: 10px !important;
|
||||
padding: 10px 15px !important;
|
||||
min-height: 260px;
|
||||
|
||||
.chart-column-container {
|
||||
padding-right: 0px !important;
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
border-bottom: none !important;
|
||||
.widget-footer {
|
||||
font-size: 85%;
|
||||
color: @text-muted;
|
||||
}
|
||||
|
||||
.chart-loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.report-summary {
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
border: none;
|
||||
|
||||
.summary-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -320,81 +376,4 @@
|
|||
|
||||
.pill-orange {
|
||||
background: @orange;
|
||||
}
|
||||
|
||||
// .pill-green {
|
||||
// background: @green-light;
|
||||
// color: @green-dark;
|
||||
// }
|
||||
|
||||
// .pill-red {
|
||||
// background: @red-light;
|
||||
// color: @red-dark;
|
||||
// }
|
||||
|
||||
// .pill-blue {
|
||||
// background: @blue-light;
|
||||
// color: @blue-dark;
|
||||
// }
|
||||
|
||||
// .pill-yellow {
|
||||
// background: @yellow-light;
|
||||
// color: @yellow-dark;
|
||||
// }
|
||||
|
||||
// .pill-orange {
|
||||
// background: @orange-light;
|
||||
// color: @orange-dark;
|
||||
// }
|
||||
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
background-image: linear-gradient(90deg, @btn-bg, @panel-bg, @btn-bg);
|
||||
// background-image: linear-gradient(90deg, black, white, black);
|
||||
animation: shine-lines 0.8s infinite cubic-bezier(.65,.05,.36,1)
|
||||
}
|
||||
|
||||
.skeleton.skeleton-full {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
// .skeleton.skeleton-100 {
|
||||
// height: 90%;
|
||||
// }
|
||||
|
||||
// .skeleton.skeleton-50 {
|
||||
// height: 50%;
|
||||
// }
|
||||
|
||||
// .skeleton.skeleton-40 {
|
||||
// height: 40%;
|
||||
// }
|
||||
|
||||
// .skeleton.skeleton-30 {
|
||||
// height: 30%;
|
||||
// }
|
||||
|
||||
// .skeleton.skeleton-20 {
|
||||
// height: 20%;
|
||||
// }
|
||||
|
||||
// .skeleton.skeleton-10 {
|
||||
// height: 10%;
|
||||
// }
|
||||
|
||||
// .skeleton.skeleton-8 {
|
||||
// height: 8%;
|
||||
// }
|
||||
|
||||
// .skeleton.skeleton-3 {
|
||||
// height: 3%;
|
||||
// }
|
||||
|
||||
@keyframes shine-lines {
|
||||
0% {
|
||||
background-position: -100px;
|
||||
}
|
||||
100% {
|
||||
background-position: 100px;
|
||||
}
|
||||
}
|
||||
|
|
@ -719,13 +719,13 @@ h6.uppercase, .h6.uppercase {
|
|||
}
|
||||
|
||||
|
||||
.timeline-new-email {
|
||||
.timeline-new-email, .timeline-email-import {
|
||||
margin: 30px 0px;
|
||||
padding-left: 70px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-new-email::before {
|
||||
.timeline-new-email::before, .timeline-email-import::before {
|
||||
.timeline-indicator();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
@import "variables.less";
|
||||
|
||||
.module-head {
|
||||
padding: 15px 30px;
|
||||
border-bottom: 1px solid @light-border-color;
|
||||
}
|
||||
|
||||
.module-head h1 {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.module-body {
|
||||
padding: 0px 15px;
|
||||
|
||||
.section-head {
|
||||
margin-bottom: 15px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-section {
|
||||
border-bottom: 1px solid @light-border-color;
|
||||
|
||||
.module-section-link {
|
||||
line-height: 1.5em;
|
||||
// font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-section-column {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
@media(min-width: @screen-xs) {
|
||||
.module-section:nth-child(even) {
|
||||
background-color: @light-bg;
|
||||
}
|
||||
|
||||
.module-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: @screen-sm) {
|
||||
.module-body {
|
||||
margin-top: 15px;
|
||||
border-top: 1px solid @border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: @screen-xs) {
|
||||
.module-body {
|
||||
margin-top: 0;
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: @screen-xs) {
|
||||
.module-section {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.module-section-column {
|
||||
border-bottom: 1px solid @light-border-color;
|
||||
}
|
||||
|
||||
.module-section-column:nth-child(even) {
|
||||
background-color: @light-bg;
|
||||
}
|
||||
|
||||
.module-section:last-child .module-section-column:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.module-item {
|
||||
margin: 0px;
|
||||
padding: 7px;
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid @border-color;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
-webkit-transition: 0.2s;
|
||||
}
|
||||
|
||||
.module-item h4 {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.module-item .module-item-description {
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.module-item .badge {
|
||||
margin-top: -2px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.module-item:hover, .module-item:focus {
|
||||
background-color: @panel-bg;
|
||||
}
|
||||
|
||||
.module-item:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.module-link.active .icon-chevron-right {
|
||||
margin-top: 4px;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.module-item-progress {
|
||||
margin-bottom: 10px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.module-item-progress-total {
|
||||
height: 7px;
|
||||
background-color: #999999;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.module-item-progress-open {
|
||||
height: 7px;
|
||||
background-color: red;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
@media(max-width: @screen-xs) {
|
||||
|
||||
body[data-route^="Module"] {
|
||||
.page-title {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.page-actions {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.layout-main-section {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +84,8 @@ def process_energy_points(doc, state):
|
|||
if (frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_import):
|
||||
or frappe.flags.in_import
|
||||
or frappe.flags.in_setup_wizard):
|
||||
return
|
||||
|
||||
if not is_energy_point_enabled():
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ login.bind_events = function() {
|
|||
var args = {};
|
||||
args.cmd = "frappe.core.doctype.user.user.sign_up";
|
||||
args.email = ($("#signup_email").val() || "").trim();
|
||||
args.redirect_to = frappe.utils.get_url_arg("redirect-to") || '';
|
||||
args.redirect_to = frappe.utils.sanitise_redirect(frappe.utils.get_url_arg("redirect-to"));
|
||||
args.full_name = ($("#signup_fullname").val() || "").trim();
|
||||
if(!args.email || !validate_email(args.email) || !args.full_name) {
|
||||
login.set_indicator('{{ _("Valid email and name required") }}', 'red');
|
||||
|
|
@ -173,7 +173,7 @@ login.login_handlers = (function() {
|
|||
200: function(data) {
|
||||
if(data.message == 'Logged In'){
|
||||
login.set_indicator('{{ _("Success") }}', 'green');
|
||||
window.location.href = frappe.utils.get_url_arg("redirect-to") || data.home_page;
|
||||
window.location.href = frappe.utils.sanitise_redirect(frappe.utils.get_url_arg("redirect-to")) || data.home_page;
|
||||
} else if(data.message == 'Password Reset'){
|
||||
window.location.href = data.redirect_to;
|
||||
} else if(data.message=="No App") {
|
||||
|
|
@ -181,7 +181,7 @@ login.login_handlers = (function() {
|
|||
if(localStorage) {
|
||||
var last_visited =
|
||||
localStorage.getItem("last_visited")
|
||||
|| frappe.utils.get_url_arg("redirect-to");
|
||||
|| frappe.utils.sanitise_redirect(frappe.utils.get_url_arg("redirect-to"));
|
||||
localStorage.removeItem("last_visited");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,3 +17,9 @@
|
|||
<div class="no-image bg-light {{ class }} " {% if size %}style="width: {{size}}; height: {{size}};"{% endif %}></div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{%- macro inspect(var, render=True) -%}
|
||||
{%- if render -%}
|
||||
<pre>{{ var | pprint | e }}</pre>
|
||||
{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
|
|
|||
70
frappe/tests/test_db_update.py
Normal file
70
frappe/tests/test_db_update.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import unittest
|
||||
import frappe
|
||||
|
||||
from frappe.core.utils import find
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
||||
|
||||
class TestDBUpdate(unittest.TestCase):
|
||||
def test_db_update(self):
|
||||
doctype = 'User'
|
||||
frappe.reload_doctype('User', force=True)
|
||||
frappe.model.meta.trim_tables('User')
|
||||
make_property_setter(doctype, 'bio', 'fieldtype', 'Text', 'Data')
|
||||
make_property_setter(doctype, 'enabled', 'default', '1', 'Int')
|
||||
|
||||
frappe.db.updatedb(doctype)
|
||||
|
||||
field_defs = get_field_defs(doctype)
|
||||
table_columns = frappe.db.get_table_columns_description('tab{}'.format(doctype))
|
||||
|
||||
self.assertEqual(len(field_defs), len(table_columns))
|
||||
|
||||
for field_def in field_defs:
|
||||
fieldname = field_def.get('fieldname')
|
||||
table_column = find(table_columns, lambda d: d.get('name') == fieldname)
|
||||
|
||||
fieldtype = get_fieldtype_from_def(field_def)
|
||||
|
||||
fallback_default = '0' if field_def.get('fieldtype') in frappe.model.numeric_fieldtypes else 'NULL'
|
||||
default = field_def.default if field_def.default is not None else fallback_default
|
||||
|
||||
self.assertEqual(fieldtype, table_column.type)
|
||||
self.assertIn(table_column.default or 'NULL', [default, "'{}'".format(default)])
|
||||
|
||||
def get_fieldtype_from_def(field_def):
|
||||
fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ('', 0))
|
||||
fieldtype = fieldtuple[0]
|
||||
if fieldtype in ('varchar', 'datetime', 'int'):
|
||||
fieldtype += '({})'.format(field_def.length or fieldtuple[1])
|
||||
return fieldtype
|
||||
|
||||
def get_field_defs(doctype):
|
||||
meta = frappe.get_meta(doctype, cached=False)
|
||||
field_defs = meta.get_fieldnames_with_value(True)
|
||||
field_defs += get_other_fields_meta(meta)
|
||||
return field_defs
|
||||
|
||||
def get_other_fields_meta(meta):
|
||||
default_fields_map = {
|
||||
'name': ('Data', 0),
|
||||
'owner': ('Data', 0),
|
||||
'parent': ('Data', 0),
|
||||
'parentfield': ('Data', 0),
|
||||
'modified_by': ('Data', 0),
|
||||
'parenttype': ('Data', 0),
|
||||
'creation': ('Datetime', 0),
|
||||
'modified': ('Datetime', 0),
|
||||
'idx': ('Int', 8),
|
||||
'docstatus': ('Check', 0)
|
||||
}
|
||||
|
||||
optional_fields = frappe.db.OPTIONAL_COLUMNS
|
||||
if meta.track_seen:
|
||||
optional_fields.append('_seen')
|
||||
|
||||
optional_fields_map = {field: ('Text', 0) for field in optional_fields}
|
||||
fields = dict(default_fields_map, **optional_fields_map)
|
||||
field_map = [frappe._dict({'fieldname': field, 'fieldtype': _type, 'length': _length}) for field, (_type, _length) in fields.items()]
|
||||
|
||||
return field_map
|
||||
|
|
@ -314,6 +314,8 @@ class TestPermissions(unittest.TestCase):
|
|||
|
||||
frappe.set_user('Administrator')
|
||||
frappe.db.sql('DELETE FROM `tabContact`')
|
||||
frappe.db.sql('DELETE FROM `tabContact Email`')
|
||||
frappe.db.sql('DELETE FROM `tabContact Phone`')
|
||||
|
||||
reset('Salutation')
|
||||
reset('Contact')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json, re
|
||||
import bleach, bleach_whitelist.bleach_whitelist as bleach_whitelist
|
||||
import json
|
||||
import re
|
||||
import bleach
|
||||
import bleach_whitelist.bleach_whitelist as bleach_whitelist
|
||||
from six import string_types
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
|
@ -47,7 +49,7 @@ def clean_script_and_style(html):
|
|||
def sanitize_html(html, linkify=False):
|
||||
"""
|
||||
Sanitize HTML tags, attributes and style to prevent XSS attacks
|
||||
Based on bleach clean, bleach whitelist and HTML5lib's Sanitizer defaults
|
||||
Based on bleach clean, bleach whitelist and html5lib's Sanitizer defaults
|
||||
|
||||
Does not sanitize JSON, as it could lead to future problems
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.utils import get_wkhtmltopdf_version, scrub_urls
|
||||
|
||||
|
||||
PDF_CONTENT_ERRORS = ["ContentNotFoundError", "ContentOperationNotPermittedError",
|
||||
"UnknownContentError", "RemoteHostClosedError"]
|
||||
|
||||
|
|
@ -127,7 +128,7 @@ def read_options_from_html(html):
|
|||
toggle_visible_pdf(soup)
|
||||
|
||||
# use regex instead of soup-parser
|
||||
for attr in ("margin-top", "margin-bottom", "margin-left", "margin-right", "page-size", "header-spacing"):
|
||||
for attr in ("margin-top", "margin-bottom", "margin-left", "margin-right", "page-size", "header-spacing", "orientation"):
|
||||
try:
|
||||
pattern = re.compile(r"(\.print-format)([\S|\s][^}]*?)(" + str(attr) + r":)(.+)(mm;)")
|
||||
match = pattern.findall(html)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ def get_context(path, args=None):
|
|||
if hasattr(frappe.local, 'response') and frappe.local.response.get('context'):
|
||||
context.update(frappe.local.response.context)
|
||||
|
||||
# to be able to inspect the context in development
|
||||
# Use the macro "inspect" from macros.html
|
||||
if frappe.conf.developer_mode:
|
||||
context._context_dict = context
|
||||
|
||||
return context
|
||||
|
||||
def update_controller_context(context, controller):
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ def setup_source(page_info):
|
|||
source = jenv.loader.get_source(jenv, page_info.template)[0]
|
||||
html = ''
|
||||
|
||||
if page_info.template.endswith('.md'):
|
||||
if page_info.template.endswith(('.md', '.html')):
|
||||
# extract frontmatter block if exists
|
||||
try:
|
||||
# values will be used to update page_info
|
||||
|
|
@ -248,10 +248,11 @@ def setup_source(page_info):
|
|||
except Exception as e:
|
||||
pass
|
||||
|
||||
source = frappe.utils.md_to_html(source)
|
||||
if page_info.template.endswith('.md'):
|
||||
source = frappe.utils.md_to_html(source)
|
||||
|
||||
if not page_info.show_sidebar:
|
||||
source = '<div class="from-markdown">' + source + '</div>'
|
||||
if not page_info.show_sidebar:
|
||||
source = '<div class="from-markdown">' + source + '</div>'
|
||||
|
||||
# if only content
|
||||
if page_info.template.endswith('.html') or page_info.template.endswith('.md'):
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ def is_signup_enabled():
|
|||
def cleanup_page_name(title):
|
||||
"""make page name from title"""
|
||||
if not title:
|
||||
return title
|
||||
return ''
|
||||
|
||||
name = title.lower()
|
||||
name = re.sub('[~!@#$%^&*+()<>,."\'\?]', '', name)
|
||||
|
|
@ -287,7 +287,9 @@ def extract_title(source, path):
|
|||
if not title and "<h1>" in source:
|
||||
# extract title from h1
|
||||
match = re.findall('<h1>([^<]*)', source)
|
||||
title = match[0].strip()[:300]
|
||||
title_content = match[0].strip()[:300]
|
||||
if '{{' not in title_content:
|
||||
title = title_content
|
||||
|
||||
if not title:
|
||||
# make title from name
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
Babel==2.6.0
|
||||
beautifulsoup4==4.8.2
|
||||
bleach-whitelist==0.0.10
|
||||
bleach==2.1.4
|
||||
bleach==3.1.2
|
||||
boto3==1.10.18
|
||||
braintree==3.57.1
|
||||
chardet==3.0.4
|
||||
|
|
@ -23,8 +23,10 @@ google-auth==1.7.1
|
|||
googlemaps==3.1.1
|
||||
gunicorn==19.10.0
|
||||
html2text==2016.9.19
|
||||
html5lib==1.0.1
|
||||
ipython==5.9.0
|
||||
Jinja2==2.10.3
|
||||
ldap3==2.7
|
||||
markdown2==2.3.8
|
||||
maxminddb-geolite2==2018.703
|
||||
ndg-httpsclient==0.5.1
|
||||
|
|
|
|||
|
|
@ -392,9 +392,9 @@ ace-builds@^1.4.8:
|
|||
integrity sha512-8ZVAxwyCGAxQX8mOp9imSXH0hoSPkGfy8igJy+WO/7axL30saRhKgg1XPACSmxxPA7nfHVwM+ShWXT+vKsNuFg==
|
||||
|
||||
acorn@^5.2.1:
|
||||
version "5.7.3"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
|
||||
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
|
||||
version "5.7.4"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
|
||||
integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
|
||||
|
||||
acorn@^6.1.1:
|
||||
version "6.1.1"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue