Merge branch 'develop' of https://github.com/frappe/frappe into custom_append_to

This commit is contained in:
Himanshu Warekar 2020-03-27 11:16:41 +05:30
commit 595979eb1f
79 changed files with 1228 additions and 1333 deletions

View file

@ -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'/>

View file

@ -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()

View file

@ -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:

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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",

View file

@ -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 -->

View file

@ -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

View file

@ -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",

View file

@ -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)

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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,
});
});
}

View file

@ -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",

View file

@ -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");

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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"""

View file

@ -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):

View file

@ -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>

View file

@ -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",

View file

@ -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)

View file

@ -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
}

View file

@ -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",

View file

@ -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})

View file

@ -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)

View file

@ -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))

View file

@ -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']

View file

@ -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"""

View file

@ -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":

View file

@ -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):

View file

@ -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')

View file

@ -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"
],

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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(', ')]);
}

View file

@ -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;
}

View file

@ -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}",

View file

@ -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">

View file

@ -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">

View file

@ -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)

View file

@ -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"));

View file

@ -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();
}
});

View file

@ -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();
}
}
}

View file

@ -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;

View file

@ -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 = [
{

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -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)
});
});
}
};

View file

@ -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);

View file

@ -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;
})
});
},

View file

@ -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);
}
}

View file

@ -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();

View 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();
}
});
}
}

View file

@ -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 };

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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;
}
}
}

View file

@ -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():

View file

@ -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");
}

View file

@ -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 %}

View 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

View file

@ -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')

View file

@ -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
"""

View file

@ -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)

View file

@ -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):

View file

@ -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'):

View file

@ -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

View file

@ -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

View file

@ -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"