Merge branch 'staging'

This commit is contained in:
Nabin Hait 2017-09-26 16:02:54 +05:30
commit 645526bb28
232 changed files with 51070 additions and 39574 deletions

View file

@ -49,7 +49,6 @@
"moment": true,
"hljs": true,
"Awesomplete": true,
"CalHeatMap": true,
"Sortable": true,
"Showdown": true,
"Taggle": true,

46
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@frappe.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View file

@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json
from .exceptions import *
from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template
__version__ = '8.10.9'
__version__ = '9.0.0'
__title__ = "Frappe Framework"
local = Local()
@ -1022,7 +1022,8 @@ def compare(val1, condition, val2):
return frappe.utils.compare(val1, condition, val2)
def respond_as_web_page(title, html, success=None, http_status_code=None,
context=None, indicator_color=None, primary_action='/', primary_label = None, fullpage=False):
context=None, indicator_color=None, primary_action='/', primary_label = None, fullpage=False,
width=None):
"""Send response as a web page with a message rather than JSON. Used to show permission errors etc.
:param title: Page title and heading.
@ -1033,7 +1034,9 @@ def respond_as_web_page(title, html, success=None, http_status_code=None,
:param indicator_color: color of indicator in title
:param primary_action: route on primary button (default is `/`)
:param primary_label: label on primary button (defaut is "Home")
:param fullpage: hide header / footer"""
:param fullpage: hide header / footer
:param width: Width of message in pixels
"""
local.message_title = title
local.message = html
local.response['type'] = 'page'
@ -1057,6 +1060,8 @@ def respond_as_web_page(title, html, success=None, http_status_code=None,
context['primary_action'] = primary_action
context['error_code'] = http_status_code
context['fullpage'] = fullpage
if width:
context['card_width'] = width
local.response['context'] = context
@ -1174,7 +1179,7 @@ def as_json(obj, indent=1):
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler)
def are_emails_muted():
from utils import cint
from frappe.utils import cint
return flags.mute_emails or cint(conf.get("mute_emails") or 0) or False
def get_test_records(doctype):
@ -1344,7 +1349,7 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
whitelisted_globals = {
"int": int,
"float": float,
"long": long,
"long": int,
"round": round
}
@ -1360,7 +1365,7 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
return eval(code, eval_globals, eval_locals)
def get_system_settings(key):
if not local.system_settings.has_key(key):
if key not in local.system_settings:
local.system_settings.update({key: db.get_single_value('System Settings', key)})
return local.system_settings.get(key)

View file

@ -166,7 +166,7 @@ def handle_exception(e):
frappe.respond_as_web_page("Server Error",
traceback, http_status_code=http_status_code,
indicator_color='red')
indicator_color='red', width=640)
return_as_message = True
if e.__class__ == frappe.AuthenticationError:
@ -214,11 +214,11 @@ def serve(port=8000, profile=False, site=None, sites_path='.'):
if not os.environ.get('NO_STATICS'):
application = SharedDataMiddleware(application, {
b'/assets': os.path.join(sites_path, 'assets'),
'/assets': os.path.join(sites_path, 'assets'),
})
application = StaticDataMiddleware(application, {
b'/files': os.path.abspath(sites_path)
'/files': os.path.abspath(sites_path)
})
application.debug = True

View file

@ -199,6 +199,9 @@ class LoginManager:
if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")):
user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user
if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_user_name")):
user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user
self.check_if_enabled(user)
self.user = self.check_password(user, pwd)
@ -331,4 +334,4 @@ def get_website_user_home_page(user):
home_page = frappe.get_attr(home_page_method[-1])(user)
return '/' + home_page.strip('/')
else:
return '/me'
return '/me'

View file

@ -30,6 +30,7 @@ def get_bootinfo():
get_user(bootinfo)
# system info
bootinfo.sitename = frappe.local.site
bootinfo.sysdefaults = frappe.defaults.get_defaults()
bootinfo.user_permissions = get_user_permissions()
bootinfo.server_date = frappe.utils.nowdate()

View file

@ -344,6 +344,26 @@ def run_ui_tests(context, app=None, test=False, profile=False):
if os.environ.get('CI'):
sys.exit(ret)
@click.command('run-setup-wizard-ui-test')
@click.option('--app', help="App to run tests on, leave blank for all apps")
@click.option('--profile', is_flag=True, default=False)
@pass_context
def run_setup_wizard_ui_test(context, app=None, profile=False):
"Run setup wizard UI test"
import frappe.test_runner
site = get_site(context)
frappe.init(site=site)
frappe.connect()
ret = frappe.test_runner.run_setup_wizard_ui_test(app=app, verbose=context.verbose,
profile=profile)
if len(ret.failures) == 0 and len(ret.errors) == 0:
ret = 0
if os.environ.get('CI'):
sys.exit(ret)
@click.command('serve')
@click.option('--port', default=8000)
@click.option('--profile', is_flag=True, default=False)
@ -485,6 +505,7 @@ commands = [
reset_perms,
run_tests,
run_ui_tests,
run_setup_wizard_ui_test,
serve,
set_config,
watch,

View file

@ -75,6 +75,7 @@ class Address(Document):
return False
@frappe.whitelist()
def get_default_address(doctype, name, sort_key='is_primary_address'):
'''Returns default Address name for the given doctype, name'''
out = frappe.db.sql('''select

View file

@ -38,6 +38,7 @@ frappe.ui.form.on("Contact", {
}
}
});
frm.refresh_field("links");
},
validate: function(frm) {
// clear linked customer / supplier / sales partner on saving...

View file

@ -7,7 +7,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_email_add, get_fullname, strip_html, cstr
from frappe.core.doctype.communication.comment import (notify_mentions,
update_comment_in_doc)
update_comment_in_doc, on_trash)
from frappe.core.doctype.communication.email import (validate_email,
notify, _notify, update_parent_status)
from frappe.utils.bot import BotReply
@ -110,6 +110,8 @@ class Communication(Document):
frappe.publish_realtime('delete_communication', self.as_dict(),
doctype= self.reference_doctype, docname = self.reference_name,
after_commit=True)
# delete the comments from _comment
on_trash(self)
def set_status(self):
if not self.is_new():

View file

@ -332,7 +332,7 @@ class DocType(Document):
"""Make boilerplate controller template."""
make_boilerplate("controller._py", self)
if not (self.istable or self.issingle):
if not self.istable:
make_boilerplate("test_controller._py", self.as_dict())
if not self.istable:
@ -451,7 +451,7 @@ def validate_fields(meta):
def check_dynamic_link_options(d):
if d.fieldtype=="Dynamic Link":
doctype_pointer = filter(lambda df: df.fieldname==d.options, fields)
doctype_pointer = list(filter(lambda df: df.fieldname==d.options, fields))
if not doctype_pointer or (doctype_pointer[0].fieldtype not in ("Link", "Select")) \
or (doctype_pointer[0].fieldtype=="Link" and doctype_pointer[0].options!="DocType"):
frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'"))

View file

@ -49,12 +49,12 @@
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-26 21:29:00.353105",
"modified": "2017-09-15 12:26:21.827149",
"modified_by": "Administrator",
"module": "Core",
"name": "Domain",
@ -65,7 +65,7 @@
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"create": 1,
"delete": 0,
"email": 1,
"export": 1,
@ -75,15 +75,15 @@
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"role": "Administrator",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
"write": 1
}
],
"quick_entry": 1,
"read_only": 1,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "domain",
"show_name_in_global_search": 0,

View file

@ -76,6 +76,7 @@ class File(NestedSet):
if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}):
if not self.is_folder and (self.is_private != self.db_get('is_private')):
old_file_url = self.file_url
private_files = frappe.get_site_path('private', 'files')
public_files = frappe.get_site_path('public', 'files')
@ -91,6 +92,11 @@ class File(NestedSet):
self.file_url = "/private/files/{0}".format(self.file_name)
# update documents image url with new file url
if self.attached_to_doctype and self.attached_to_name and \
frappe.db.get_value(self.attached_to_doctype, self.attached_to_name, "image") == old_file_url:
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name, "image", self.file_url)
def set_folder_size(self):
"""Set folder size if folder"""
if self.is_folder and not self.is_new():
@ -170,7 +176,7 @@ class File(NestedSet):
super(File, self).on_trash()
self.delete_file()
def make_thumbnail(self, set_as_thumbnail=True, width=300, height=300, suffix="small"):
def make_thumbnail(self, set_as_thumbnail=True, width=300, height=300, suffix="small", crop=False):
if self.file_url:
if self.file_url.startswith("/files"):
try:
@ -185,7 +191,10 @@ class File(NestedSet):
return
size = width, height
image.thumbnail(size)
if crop:
image = ImageOps.fit(image, size, Image.ANTIALIAS)
else:
image.thumbnail(size, Image.ANTIALIAS)
thumbnail_url = filename + "_" + suffix + "." + extn

View file

@ -30,7 +30,7 @@ class ModuleDef(Document):
with open(frappe.get_app_path(self.app_name, "modules.txt"), "r") as f:
content = f.read()
if not self.name in content.splitlines():
modules = filter(None, content.splitlines())
modules = list(filter(None, content.splitlines()))
modules.append(self.name)
if modules:

View file

@ -147,10 +147,12 @@ class Report(Document):
limit=limit,
user=user)
meta = frappe.get_meta(self.ref_doctype)
columns = [meta.get_field(c[0]) or frappe._dict(label=meta.get_label(c[0]), fieldname=c[0])
for c in columns]
_columns = []
for column in columns:
meta = frappe.get_meta(column[1])
field = [meta.get_field(column[0]) or frappe._dict(label=meta.get_label(column[0]), fieldname=column[0])]
_columns.extend(field)
columns = _columns
out = out + [list(d) for d in result]

View file

@ -19,4 +19,10 @@ frappe.ui.form.on("System Settings", "enable_password_policy", function(frm) {
} else {
frm.set_value("minimum_password_score", "2");
}
});
frappe.ui.form.on("System Settings", "enable_two_factor_auth", function(frm) {
if(frm.doc.enable_two_factor_auth == 0){
frm.set_value("bypass_2fa_for_retricted_ip_users", 0);
}
});

View file

@ -160,7 +160,37 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_first_startup",
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is First Startup",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -838,6 +868,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"description": "User can login using Email id or Mobile number",
"fieldname": "allow_login_using_mobile_number",
"fieldtype": "Check",
@ -863,6 +894,38 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"description": "User can login using Email id or User Name",
"fieldname": "allow_login_using_user_name",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow Login using User Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -956,8 +1019,40 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"depends_on": "enable_two_factor_auth",
"fieldname": "bypass_2fa_for_retricted_ip_users",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Bypass Two Factor Auth for users who login from restricted IP Address",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
@ -1186,8 +1281,8 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-08-07 23:29:18.858797",
"modified_by": "Administrator",
"modified": "2017-09-13 13:26:11.045262",
"modified_by": "shri@zerodha.com",
"module": "Core",
"name": "System Settings",
"name_case": "",

View file

@ -31,6 +31,8 @@ class SystemSettings(Document):
if not frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'):
frappe.throw(_('Please setup SMS before setting it as an authentication method, via SMS Settings'))
toggle_two_factor_auth(True, roles=['All'])
else:
self.bypass_2fa_for_retricted_ip_users = 0
def on_update(self):
for df in self.meta.get("fields"):
@ -59,4 +61,4 @@ def load():
return {
"timezones": get_all_timezones(),
"defaults": defaults
}
}

View file

@ -34,6 +34,13 @@ frappe.ui.form.on('Test Runner', {
frappe.dom.eval(f.script);
});
QUnit.config.notrycatch = true;
window.onerror = function(msg, url, lineNo, columnNo, error) {
console.log(error.stack); // eslint-disable-line
$('<div id="frappe-qunit-done"></div>').appendTo($('body'));
};
QUnit.testDone(function(details) {
// var result = {
// "Module name": details.module,

View file

@ -600,7 +600,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "gender",
"fieldtype": "Select",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@ -613,7 +613,7 @@
"no_copy": 0,
"oldfieldname": "gender",
"oldfieldtype": "Select",
"options": "\nMale\nFemale\nOther",
"options": "Gender",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
@ -2001,7 +2001,7 @@
"istable": 0,
"max_attachments": 5,
"menu_index": 0,
"modified": "2017-08-23 10:34:11.944298",
"modified": "2017-09-14 14:55:26.044665",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

View file

@ -112,7 +112,9 @@ class User(Document):
self.get("roles")]):
return
if self.name not in STANDARD_USERS and self.user_type == "System User" and not self.get_other_system_managers():
if (self.name not in STANDARD_USERS and self.user_type == "System User" and not self.get_other_system_managers()
and cint(frappe.db.get_single_value('System Settings', 'setup_complete'))):
msgprint(_("Adding System Manager to this User as there must be atleast one System Manager"))
self.append("roles", {
"doctype": "Has Role",

View file

@ -72,7 +72,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
return [], -1
def filter_empty_columns(columns):
empty_cols = filter(lambda x: x in ("", None), columns)
empty_cols = list(filter(lambda x: x in ("", None), columns))
if empty_cols:
if columns[-1*len(empty_cols):] == empty_cols:
@ -217,8 +217,8 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
# header
if not rows:
from frappe.utils.file_manager import save_uploaded
file_doc = save_uploaded(dt=None, dn="Data Import", folder='Home', is_private=1)
from frappe.utils.file_manager import get_file_doc
file_doc = get_file_doc(dt='', dn="Data Import", folder='Home', is_private=1)
filename, file_extension = os.path.splitext(file_doc.file_name)
if file_extension == '.xlsx' and from_data_import == 'Yes':

View file

@ -106,6 +106,21 @@ def create_custom_field(doctype, df):
"hidden": df.hidden or 0
}).insert()
def create_custom_fields(custom_fields):
'''Add / update multiple custom fields
:param custom_fields: example `{'Sales Invoice': [dict(fieldname='test')]}`'''
for doctype, fields in custom_fields.items():
for df in fields:
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": df["fieldname"]})
if not field:
create_custom_field(doctype, df)
else:
custom_field = frappe.get_doc("Custom Field", field)
custom_field.update(df)
custom_field.save()
@frappe.whitelist()
def add_custom_field(doctype, df):
df = json.loads(df)

View file

@ -62,10 +62,10 @@ class Database:
'key':frappe.conf.db_ssl_key
}
if usessl:
self._conn = MySQLdb.connect(user=self.user, host=self.host, passwd=self.password,
self._conn = MySQLdb.connect(self.host, self.user or '', self.password or '',
use_unicode=True, charset='utf8mb4', ssl=self.ssl)
else:
self._conn = MySQLdb.connect(user=self.user, host=self.host, passwd=self.password,
self._conn = MySQLdb.connect(self.host, self.user or '', self.password or '',
use_unicode=True, charset='utf8mb4')
self._conn.converter[246]=float
self._conn.converter[12]=get_datetime
@ -607,7 +607,7 @@ class Database:
return r
def _get_value_for_many_names(self, doctype, names, field, debug=False):
names = filter(None, names)
names = list(filter(None, names))
if names:
return dict(self.sql("select name, `%s` from `tab%s` where name in (%s)" \

View file

@ -42,7 +42,7 @@ def get_user_default_as_list(key, user=None):
else:
d = user_defaults.get(frappe.scrub(key), None)
return filter(None, (not isinstance(d, (list, tuple))) and [d] or d)
return list(filter(None, (not isinstance(d, (list, tuple))) and [d] or d))
def is_a_user_permission_key(key):
return ":" not in key and key != frappe.scrub(key)

View file

@ -106,6 +106,36 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "color",
"fieldtype": "Color",
"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": "Color",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -514,7 +544,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-13 17:44:54.369254",
"modified": "2017-09-05 12:54:58.044162",
"modified_by": "Administrator",
"module": "Desk",
"name": "ToDo",

View file

@ -14,7 +14,7 @@ def get_notifications():
config = get_notification_config()
groups = config.get("for_doctype").keys() + config.get("for_module").keys()
groups = list(config.get("for_doctype").keys()) + list(config.get("for_module").keys())
cache = frappe.cache()
notification_count = {}
@ -156,13 +156,12 @@ def get_notifications_for_targets(config, notification_percent):
return doc_target_percents
def clear_notifications(user=None):
if frappe.flags.in_install:
return
config = get_notification_config()
groups = config.get("for_doctype").keys() + config.get("for_module").keys()
groups = list(config.get("for_doctype").keys()) + list(config.get("for_module").keys())
cache = frappe.cache()
for name in groups:

View file

@ -180,30 +180,12 @@ frappe.activity.render_heatmap = function(page) {
method: "frappe.desk.page.activity.activity.get_heatmap_data",
callback: function(r) {
if(r.message) {
var legend = [];
var max = Math.max.apply(this, $.map(r.message, function(v) { return v }));
var legend = [cint(max/5), cint(max*2/5), cint(max*3/5), cint(max*4/5)];
var heatmap = new CalHeatMap();
heatmap.init({
itemSelector: ".heatmap",
domain: "month",
subDomain: "day",
start: moment().subtract(1, 'year').add(1, 'month').toDate(),
cellSize: 9,
cellPadding: 2,
domainGutter: 2,
range: 12,
domainLabelFormat: function(date) {
return moment(date).format("MMM").toUpperCase();
},
displayLegend: false,
legend: legend,
tooltip: true,
subDomainTitleFormat: {
empty: "{date}",
filled: "{count} actions on {date}"
},
subDomainDateFormat: "%d-%b"
var heatmap = new frappe.ui.HeatMap({
parent: $(".heatmap"),
height: 100,
start: new Date(moment().subtract(1, 'year').toDate()),
count_label: "actions",
discrete_domains: 0
});
heatmap.update(r.message);

View file

@ -12,6 +12,7 @@ import json
from frappe import _
from distutils.spawn import find_executable
from frappe.utils.background_jobs import enqueue
from six.moves import reload_module
@frappe.whitelist()
def get_app_list():
@ -65,7 +66,7 @@ def install_app(name):
frappe.cache().delete_value(["app_hooks"])
# reload sys.path
import site
reload(site)
reload_module(site)
else:
# will only come via direct API
frappe.throw(_("Listing app not allowed"))

View file

@ -1,146 +0,0 @@
#page-setup-wizard {
margin-top: 30px;
}
.setup-wizard-slide {
padding-left: 0px;
padding-right: 0px;
}
@media (min-width: 768px) {
.setup-wizard-slide {
max-width: 500px;
}
}
.setup-wizard-slide .lead {
margin: 30px;
color: #777777;
text-align: center;
font-size: 24px;
}
.setup-wizard-slide .col-sm-12 {
padding: 0px;
}
.setup-wizard-slide .section-body .col-sm-6:first-child {
padding-left: 0px;
}
.setup-wizard-slide .section-body .col-sm-6:last-child {
padding-right: 0px;
}
.setup-wizard-slide .form-control {
font-weight: 500;
}
.setup-wizard-slide .form-control.bold {
background-color: #fff;
}
.setup-wizard-slide.with-form {
margin: 60px auto;
padding: 10px 50px;
border: 1px solid #d1d8dd;
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.1);
}
.setup-wizard-slide .footer {
padding: 30px 0px;
}
.setup-wizard-slide a.next-btn.disabled,
.setup-wizard-slide a.complete-btn.disabled {
background-color: #b1bdca;
color: #fff;
border-color: #b1bdca;
}
.setup-wizard-progress {
padding: 15px;
}
.setup-wizard-slide .fa-fw {
vertical-align: middle;
font-size: 10px;
}
.setup-wizard-slide .fa-fw.active {
color: #5e64ff;
}
.setup-wizard-slide .icon-circle-blank {
font-size: 7px;
}
.setup-wizard-slide .icon-circle {
font-size: 10px;
}
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] {
width: 140px;
height: 180px; /*depends on presence of heading*/
margin-top: 20px;
}
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] .form-group,
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] .clearfix {
display: none;
}
.setup-wizard-slide .missing-image,
.setup-wizard-slide .attach-image-display {
display: block;
position: relative;
border-radius: 4px;
}
.setup-wizard-slide .missing-image {
border: 1px solid #d1d8dd;
border-radius: 6px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
}
.setup-wizard-slide .missing-image .octicon {
position: relative;
top: 50%;
transform: translate(0px, -50%);
-webkit-transform: translate(0px, -50%);
}
.setup-wizard-slide .img-container {
height: 100%;
width: 100%;
padding: 2px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border: 1px solid #d1d8dd;
border-radius: 6px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
}
.setup-wizard-slide .img-overlay {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
color: #777777;
background-color: rgba(255, 255, 255, 0.7);
opacity: 0;
}
.setup-wizard-slide .img-overlay:hover {
opacity: 1;
cursor: pointer;
}
.setup-wizard-message-image {
margin: 15px auto;
}

View file

@ -1,5 +1,6 @@
frappe.provide("frappe.wiz");
frappe.provide("frappe.setup");
frappe.provide("frappe.setup.events");
frappe.provide("frappe.ui");
frappe.setup = {
slides: [],
@ -7,7 +8,6 @@ frappe.setup = {
data: {},
utils: {},
remove_app_slides: [],
on: function(event, fn) {
if(!frappe.setup.events[event]) {
frappe.setup.events[event] = [];
@ -29,277 +29,252 @@ frappe.pages['setup-wizard'].on_page_load = function(wrapper) {
// setup page ui
$(".navbar:first").toggle(false);
var requires = ["/assets/frappe/css/animate.min.css"].concat(frappe.boot.setup_wizard_requires || []);
var requires = ["/assets/frappe/css/animate.min.css"].concat(
frappe.boot.setup_wizard_requires || []);
frappe.require(requires, function() {
frappe.setup.run_event("before_load");
frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.load_languages",
freeze: true,
callback: function(r) {
frappe.setup.data.lang = r.message;
var wizard_settings = {
page_name: "setup-wizard",
parent: wrapper,
slides: frappe.setup.slides,
title: __("Welcome")
}
frappe.setup.run_event("before_load");
var wizard_settings = {
parent: wrapper,
slides: frappe.setup.slides,
slide_class: frappe.setup.SetupWizardSlide,
unidirectional: 1,
before_load: ($footer) => {
$footer.find('.next-btn').removeClass('btn-default')
.addClass('btn-primary');
$footer.find('.text-right').prepend(
$(`<a class="complete-btn btn btn-sm primary">
${__("Complete Setup")}</a>`));
frappe.wizard = new frappe.setup.Wizard(wizard_settings);
frappe.setup.run_event("after_load");
// frappe.wizard.values = test_values_edu;
var route = frappe.get_route();
if(route) {
frappe.wizard.show(route[1]);
}
}
}
frappe.wizard = new frappe.setup.SetupWizard(wizard_settings);
frappe.setup.run_event("after_load");
// frappe.wizard.values = test_values_edu;
let route = frappe.get_route();
if(route) {
frappe.wizard.show_slide(route[1]);
}
}
});
});
}
};
frappe.pages['setup-wizard'].on_page_show = function(wrapper) {
if(frappe.get_route()[1]) {
frappe.wizard && frappe.wizard.show(frappe.get_route()[1]);
frappe.wizard && frappe.wizard.show_slide(frappe.get_route()[1]);
}
}
};
frappe.setup.on("before_load", function() {
// load slides
frappe.setup.slides_settings.map(frappe.setup.add_slide);
});
frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
constructor(args = {}) {
super(args);
$.extend(this, args);
frappe.setup.Wizard = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.make();
this.slides;
this.slide_dict = {};
this.values = {};
this.welcomed = true;
this.page_name = "setup-wizard";
frappe.set_route("setup-wizard/0");
},
make: function() {
this.parent = $('<div class="setup-wizard-wrapper">').appendTo(this.parent);
},
get_message: function(html) {
return $(repl('<div data-state="setup-complete">\
<div style="padding: 40px;" class="text-center">%(html)s</div>\
</div>', {html:html}))
},
show_working: function() {
this.hide_current_slide();
frappe.set_route(this.page_name);
this.current_slide = {"$wrapper": this.get_message(this.working_html()).appendTo(this.parent)};
},
show_complete: function() {
this.hide_current_slide();
this.current_slide = {"$wrapper": this.get_message(this.complete_html()).appendTo(this.parent)};
},
show: function(id) {
}
make() {
super.make();
this.container.addClass("container setup-wizard-slide with-form");
this.$next_btn.addClass('action');
this.$complete_btn = this.$footer.find('.complete-btn').addClass('action');
this.setup_keyboard_nav();
}
setup_keyboard_nav() {
this.container.on('keydown', (e) => {
if(e.which === 13) {
var $target = $(e.target);
if($target.hasClass('prev-btn')) {
$target.trigger('click');
} else {
this.container.find('.next-btn').trigger('click');
e.preventDefault();
}
}
});
}
before_show_slide() {
if(!this.welcomed) {
frappe.set_route(this.page_name);
return;
return false;
}
id = cint(id);
if(this.current_slide && this.current_slide.id===id) {
return;
return true;
}
show_slide(id) {
super.show_slide(id);
frappe.set_route(this.page_name, id + "");
}
show_hide_prev_next(id) {
super.show_hide_prev_next(id);
if (id + 1 === this.slides.length){
this.$next_btn.removeClass("btn-primary").hide();
this.$complete_btn.addClass("btn-primary").show()
.on('click', this.action_on_complete.bind(this));
} else {
this.$next_btn.addClass("btn-primary").show();
this.$complete_btn.removeClass("btn-primary").hide();
}
}
this.update_values();
if(!this.slide_dict[id]) {
this.slide_dict[id] = new frappe.setup.WizardSlide($.extend(this.slides[id], {wiz:this, id:id}));
this.slide_dict[id].make();
}
this.hide_current_slide();
this.current_slide = this.slide_dict[id];
this.current_slide.$wrapper.removeClass("hidden");
},
hide_current_slide: function() {
if(this.current_slide) {
this.current_slide.$wrapper.addClass("hidden");
this.current_slide = null;
}
},
get_values: function() {
var values = {};
$.each(this.slide_dict, function(id, slide) {
if(slide.values) {
$.extend(values, slide.values);
}
});
return values;
},
working_html: function() {
var msg = $(frappe.render_template("setup_wizard_message", {
image: "/assets/frappe/images/ui/bubble-tea-smile.svg",
title: __("Setting Up"),
message: __('Sit tight while your system is being setup. This may take a few moments.')
}));
msg.find(".setup-wizard-message-image").addClass("animated infinite bounce");
return msg.html();
},
complete_html: function() {
return frappe.render_template("setup_wizard_message", {
image: "/assets/frappe/images/ui/bubble-tea-happy.svg",
title: __('Setup Complete'),
message: ""
});
},
on_complete: function() {
var me = this;
this.update_values();
this.show_working();
return frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.setup_complete",
args: {args: this.values},
callback: function(r) {
me.show_complete();
if(frappe.setup.welcome_page) {
localStorage.setItem("session_last_route", frappe.setup.welcome_page);
}
setTimeout(function() {
window.location = "/desk";
}, 2000);
},
error: function(r) {
var d = frappe.msgprint(__("There were errors."));
d.custom_onhide = function() {
frappe.set_route(me.page_name, me.slides.length - 1);
};
}
});
},
update_values: function() {
this.values = $.extend(this.values, this.get_values());
},
refresh_slides: function() {
// reset all slides so that labels are translated
var me = this;
if(this.in_refresh_slides) {
refresh_slides() {
// For Translations, etc.
if(this.in_refresh_slides || !this.current_slide.set_values()) {
return;
}
this.in_refresh_slides = true;
if(!this.current_slide.set_values()) {
return;
}
this.update_values();
frappe.setup.slides = [];
frappe.setup.run_event("before_load");
// remove slides listed in remove_app_slides
var new_slides = [];
frappe.setup.slides = this.get_setup_slides_filtered_by_domain();
this.slides = frappe.setup.slides;
frappe.setup.run_event("after_load");
// re-render all slide, only remake made slides
$.each(this.slide_dict, (id, slide) => {
if(slide.made) {
this.made_slide_ids.push(id);
}
});
this.made_slide_ids.push(this.current_id);
this.setup();
this.show_slide(this.current_id);
setTimeout(() => {
this.container.find('.form-control').first().focus();
}, 200);
this.in_refresh_slides = false;
}
action_on_complete() {
var me = this;
if (!this.current_slide.set_values()) return;
this.update_values();
this.show_working_state();
return frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.setup_complete",
args: {args: this.values},
callback: function() {
me.show_setup_complete_state();
if(frappe.setup.welcome_page) {
localStorage.setItem("session_last_route", frappe.setup.welcome_page);
}
setTimeout(function() {
// frappe.ui.toolbar.clear_cache();
window.location = "/desk";
}, 2000);
setTimeout(()=> {
$('body').removeClass('setup-state');
}, 20000);
},
error: function() {
var d = frappe.msgprint(__("There were errors."));
d.custom_onhide = function() {
$(me.parent).find('.page-card-container').remove();
$('body').removeClass('setup-state');
me.container.show();
frappe.set_route(me.page_name, me.slides.length - 1);
};
}
});
}
get_setup_slides_filtered_by_domain() {
var filtered_slides = [];
frappe.setup.slides.forEach(function(slide) {
if(frappe.setup.domain) {
var domains = slide.domains;
if (domains.indexOf('all') !== -1 ||
domains.indexOf(frappe.setup.domain.toLowerCase()) !== -1) {
new_slides.push(slide);
filtered_slides.push(slide);
}
} else {
new_slides.push(slide);
filtered_slides.push(slide);
}
})
frappe.setup.slides = new_slides;
this.slides = frappe.setup.slides;
frappe.setup.run_event("after_load");
// re-render all slides
this.slide_dict = {};
var current_id = this.current_slide.id;
this.current_slide.destroy();
this.show(current_id);
this.in_refresh_slides = false;
return filtered_slides;
}
});
frappe.setup.WizardSlide = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.$wrapper = $('<div class="slide-wrapper hidden"></div>')
.appendTo(this.wiz.parent)
.attr("data-slide-id", this.id);
},
make: function() {
var me = this;
if(this.$body) this.$body.remove();
show_working_state() {
this.container.hide();
$('body').addClass('setup-state');
frappe.set_route(this.page_name);
var fields = JSON.parse(JSON.stringify(this.fields));
this.working_state_message = this.get_message(
__("Setting Up"),
__("Sit tight while your system is being setup. This may take a few moments."),
true
).appendTo(this.parent);
if(this.add_more) {
this.count = 1;
fields = fields.map((field, i) => {
if(field.fieldname) {
field.fieldname += '_1';
this.current_id = this.slides.length;
this.current_slide = null;
this.completed_state_message = this.get_message(
__("Setup Complete"),
__("You're all set!")
);
}
show_setup_complete_state() {
this.working_state_message.hide();
this.completed_state_message.appendTo(this.parent);
}
get_message(title, message="", loading=false) {
return $(`<div class="page-card-container" data-state="setup">
<div class="page-card">
<div class="page-card-head">
${loading
? `<span class="indicator orange">${title}</span>`
: `<span class="indicator green">${title}</span>`
}
</div>
<p>${message}</p>
<div class="state-icon-container">
${loading
? '<div style="width:100%;height:100%" class="lds-rolling state-icon"><div></div></div>'
: `<div style="width:100%;height:100%" class="state-icon"><i class="fa fa-check-circle text-success"
style="font-size: 64px; margin-top: -8px;">
</i></div>`
}
if(i === 1 && this.mandatory_entry) {
field.reqd = 1;
}
if(!field.static) {
if(field.label) field.label += ' 1';
}
return field;
});
}
</div>
</div>
</div>`);
}
};
if(this.before_load) {
this.before_load(this);
}
frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide {
constructor(slide = null) {
super(slide);
}
this.$body = $(frappe.render_template("setup_wizard_page", {
help: __(this.help),
title:__(this.title),
main_title:__(this.wiz.title),
step: this.id + 1,
name: this.name,
slides_count: this.wiz.slides.length
})).appendTo(this.$wrapper);
this.body = this.$body.find(".form")[0];
if(this.fields) {
this.form = new frappe.ui.FieldGroup({
fields: fields,
body: this.body,
no_submit_on_enter: true
});
this.form.make();
} else {
$(this.body).html(this.html);
}
this.set_reqd_fields();
make() {
super.make();
this.set_init_values();
this.make_prev_next_buttons();
if(this.add_more) this.bind_more_button();
this.reset_action_button_state();
}
var $primary_btn = this.$next ? this.$next : this.$complete;
this.bind_fields_to_next($primary_btn);
if(this.onload) {
this.onload(this);
}
this.set_reqd_fields();
this.bind_fields_to_next($primary_btn);
this.reset_next($primary_btn);
this.focus_first_input();
},
set_reqd_fields: function() {
var dict = this.form.fields_dict;
this.reqd_fields = [];
Object.keys(dict).map(key => {
if(dict[key].df.reqd) {
this.reqd_fields.push(dict[key]);
}
});
},
set_init_values: function() {
set_init_values () {
var me = this;
// set values from frappe.setup.values
if(frappe.wizard.values && this.fields) {
@ -310,141 +285,21 @@ frappe.setup.WizardSlide = Class.extend({
}
});
}
},
}
set_values: function() {
this.values = this.form.get_values();
if(this.values===null) {
return false;
}
if(this.validate && !this.validate()) {
return false;
}
return true;
},
};
bind_more_button: function() {
this.$more = this.$body.find('.more-btn');
this.$more.removeClass('hide')
.on('click', () => {
this.count++;
var fields = JSON.parse(JSON.stringify(this.fields));
this.form.add_fields(fields.map(field => {
if(field.fieldname) field.fieldname += '_' + this.count;
if(!field.static) {
if(field.label) field.label += ' ' + this.count;
}
return field;
}));
if(this.count === this.max_count) {
this.$more.addClass('hide');
}
});
},
// Frappe slides settings
// ======================================================
make_prev_next_buttons: function() {
var me = this;
// prev
if(this.id > 0) {
this.$prev = this.$body.find('.prev-btn')
.removeClass("hide")
.attr('tabIndex', 0)
.click(function() {
me.prev();
})
.css({"margin-right": "10px"});
}
// next or complete
if(this.id+1 < this.wiz.slides.length) {
this.$next = this.$body.find('.next-btn')
.removeClass("hide")
.attr('tabIndex', 0)
.click(this.next_or_complete.bind(this));
} else {
this.$complete = this.$body.find('.complete-btn')
.removeClass("hide")
.attr('tabIndex', 0)
.click(this.next_or_complete.bind(this));
}
// setup mousefree navigation
this.$body.on('keypress', function(e) {
if(e.which === 13) {
var $target = $(e.target);
if($target.hasClass('prev-btn')) {
me.prev();
} else if($target.hasClass('btn-attach')) {
//do nothing
} else {
me.next_or_complete();
e.preventDefault();
}
}
});
},
bind_fields_to_next: function($primary_btn) {
var me = this;
this.reqd_fields.map((field) => {
field.$wrapper.on('change input', () => {
me.reset_next($primary_btn);
});
});
},
next_or_complete: function() {
if(this.set_values()) {
if(this.id+1 < this.wiz.slides.length) {
this.next();
} else {
this.wiz.on_complete(this.wiz);
}
}
},
reset_next: function($primary_btn) {
var empty_fields = this.reqd_fields.filter((field) => {
return !field.get_value();
})
if(empty_fields.length) {
$primary_btn.addClass('disabled');
} else {
$primary_btn.removeClass('disabled');
}
},
focus_first_input: function() {
setTimeout(function() {
this.$body.find('.form-control').first().focus();
}.bind(this), 0);
},
next: function() {
frappe.set_route(this.wiz.page_name, this.id+1 + "");
},
prev: function() {
frappe.set_route(this.wiz.page_name, this.id-1 + "");
},
get_input: function(fn) {
return this.form.get_input(fn);
},
get_field: function(fn) {
return this.form.get_field(fn);
},
destroy: function() {
this.$body.remove();
if(frappe.wizard.current_slide===this) {
frappe.wizard.current_slide = null;
}
},
});
var frappe_slides = [
frappe.setup.slides_settings = [
{
// Welcome (language) slide
name: "welcome",
domains: ["all"],
title: __("Hello!"),
icon: "fa fa-world",
help: __("Let's prepare the system for first use."),
// help: __("Let's prepare the system for first use."),
fields: [
{ fieldname: "language", label: __("Your Language"),
@ -452,16 +307,22 @@ var frappe_slides = [
],
onload: function(slide) {
if (frappe.setup.data.lang) {
this.setup_fields(slide);
} else {
utils.load_languages(slide, this.setup_fields);
this.setup_fields(slide);
var language_field = slide.get_field("language");
language_field.set_input(frappe.setup.data.default_language || "English");
if (!frappe.setup._from_load_messages) {
language_field.$input.trigger("change");
}
delete frappe.setup._from_load_messages;
moment.locale("en");
},
setup_fields: function(slide) {
utils.setup_language_field(slide);
utils.bind_language_events(slide);
frappe.setup.utils.setup_language_field(slide);
frappe.setup.utils.bind_language_events(slide);
},
},
@ -471,7 +332,7 @@ var frappe_slides = [
domains: ["all"],
title: __("Select Your Region"),
icon: "fa fa-flag",
help: __("Select your Country, Time Zone and Currency"),
// help: __("Select your Country, Time Zone and Currency"),
fields: [
{ fieldname: "country", label: __("Your Country"), reqd:1,
fieldtype: "Select" },
@ -487,13 +348,13 @@ var frappe_slides = [
if(frappe.setup.data.regional_data) {
this.setup_fields(slide);
} else {
utils.load_regional_data(slide, this.setup_fields);
frappe.setup.utils.load_regional_data(slide, this.setup_fields);
}
},
setup_fields: function(slide) {
utils.setup_region_fields(slide);
utils.bind_region_events(slide);
frappe.setup.utils.setup_region_fields(slide);
frappe.setup.utils.bind_region_events(slide);
}
},
@ -512,7 +373,7 @@ var frappe_slides = [
"fieldtype": "Data", "options":"Email"},
{ "fieldname": "password", "label": __("Password"), "fieldtype": "Password" }
],
help: __('The first user will become the System Manager (you can change this later).'),
// help: __('The first user will become the System Manager (you can change this later).'),
onload: function(slide) {
if(frappe.session.user!=="Administrator") {
slide.form.fields_dict.email.$wrapper.toggle(false);
@ -542,7 +403,7 @@ var frappe_slides = [
slide.form.fields_dict.password.df.reqd = 1;
slide.form.fields_dict.password.refresh();
utils.load_user_details(slide, this.setup_fields);
frappe.setup.utils.load_user_details(slide, this.setup_fields);
}
},
@ -564,28 +425,7 @@ var frappe_slides = [
}
];
var utils = {
load_languages: function(slide, callback) {
frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.load_languages",
freeze: true,
callback: function(r) {
frappe.setup.data.lang = r.message;
callback(slide);
var language_field = slide.get_field("language");
language_field.set_input(frappe.setup.data.default_language || "English");
if (!frappe.setup._from_load_messages) {
language_field.$input.trigger("change");
}
delete frappe.setup._from_load_messages;
moment.locale("en");
}
});
},
frappe.setup.utils = {
load_regional_data: function(slide, callback) {
frappe.call({
method:"frappe.geo.country_info.get_country_timezone_info",
@ -714,10 +554,4 @@ var utils = {
});
});
},
}
frappe.setup.on("before_load", function() {
// load slides
frappe_slides.map(frappe.setup.add_slide);
});
};

View file

@ -151,6 +151,7 @@ def add_all_roles_to(name):
def disable_future_access():
frappe.db.set_default('desktop:home_page', 'desktop')
frappe.db.set_value('System Settings', 'System Settings', 'setup_complete', 1)
frappe.db.set_value('System Settings', 'System Settings', 'is_first_startup', 1)
if not frappe.flags.in_test:
# remove all roles and add 'Administrator' to prevent future access
@ -202,6 +203,10 @@ def load_user_details():
"email": frappe.cache().hget("email", "signup")
}
@frappe.whitelist()
def reset_is_first_startup():
frappe.db.set_value('System Settings', 'System Settings', 'is_first_startup', 0)
def prettify_args(args):
# remove attachments
for key, val in args.items():

View file

@ -1,7 +0,0 @@
<div class="container setup-wizard-slide">
<img class="img-responsive setup-wizard-message-image" src="{%= image %}">
<p class="text-center lead">{%= title %}</p>
<p class="text-center">{%= message %}</p>
</div>

View file

@ -1,25 +0,0 @@
<div class="container setup-wizard-slide single-column with-form" data-slide-name="{%= name %}">
<div class="text-center setup-wizard-progress text-extra-muted">
{% for (var i=0; i < slides_count; i++) { %}
<!--dev_mode: link progress bubbles-->
<!--<a href="http://erpnext.domainify:8000/desk#setup-wizard/{%= i %}">-->
<i class="fa fa-fw fa-circle{% if (i+1<=step) { %} active {% } %}"></i>
<!--</a>-->
{% } %}
</div>
<p class="lead">{%= title %}</p>
<div class="row">
<div class="setup-wizard-body col-sm-12">
<!-- {% if (help) { %} <p class="text-center">{%= help %}</p> {% } %} -->
<div class="form"></div>
<a class="more-btn hide btn btn-default btn-sm" style="margin-left: 41%;">{%= __("Add More") %}</a>
</div>
</div>
<div class="footer text-right">
<div>
<a class="prev-btn hide grey small">{%= __("Previous") %}</a>
<a class="next-btn hide btn btn-primary btn-sm">{%= __("Next") %}</a>
<a class="complete-btn hide btn btn-primary btn-sm"><b>{%= __("Complete Setup") %}</b></a>
</div>
</div>
</div>

View file

@ -128,7 +128,7 @@ def build_description_standard(meta, tl):
coloptions.append(desc[2] or '')
colwidths.append(desc[3] or '100')
elif meta.get(dt,{}).has_key(fn):
elif fn in meta.get(dt,{}):
# type specified for a multi-table join
# usually from Report Builder

View file

@ -0,0 +1,30 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import cint
@frappe.whitelist()
def get_user_progress_slides():
'''
Return user progress slides for the desktop (called via `get_user_progress_slides` hook)
'''
slides = []
if cint(frappe.db.get_single_value('System Settings', 'setup_complete')):
for fn in frappe.get_hooks('get_user_progress_slides'):
slides += frappe.get_attr(fn)()
return slides
@frappe.whitelist()
def update_and_get_user_progress():
'''
Return setup progress action states (called via `update_and_get_user_progress` hook)
'''
states = {}
for fn in frappe.get_hooks('update_and_get_user_progress'):
states.update(frappe.get_attr(fn)())
return states

View file

@ -1,14 +1,17 @@
# Dialogs Types
Frappé provide a group of standard dialogs that are very usefull while coding.
Frappé provides a group of standard dialogs that are very useful while coding.
## Alert Dialog
<img class="screenshot" src="/docs/assets/img/app-development/show_alert.png">
<img class="screenshot" src="/docs/assets/img/app-development/show-alert.png">
Is helpfull for show a non-obstructive message.
Alert Dialog is used for showing non-obstructive messages.
This dialog have 2 parameters `txt`that is the message and `seconds` that is the time that the message will be showed for the user, the standard is `3 seconds`.
It has 2 parameters:
- **txt:** The message to be shown in the `Alert Dialog`
- **seconds:** The duration that the message will be displayed. The default is `3 seconds`.
### Example
@ -20,12 +23,12 @@ This dialog have 2 parameters `txt`that is the message and `seconds` that is the
<img class="screenshot" src="/docs/assets/img/app-development/prompt.png">
Is helpful for ask a value for the user
Prompt Dialog is used for collecting data from users.
This dialog have 4 parameters, they are:
It has 4 parameters:
- **fields:** a list with the fields objects
- **callback:** the function that manage the received values
- **callback:** a function to process the data in the dialog
- **title:** the title of the dialog
- **primary_label:** the label of the primary button
@ -46,11 +49,11 @@ This dialog have 4 parameters, they are:
<img class="screenshot" src="/docs/assets/img/app-development/confirm-dialog.png">
Usefull to get a confirmation from the user before do an action
Confirm Dialog is used to get a confirmation from the user before executing an action.
This dialog have 3 arguments, they are:
It has 3 arguments:
- **mesage:** The message content
- **mesage:** The message to display in the dialog
- **onyes:** The callback on positive confirmation
- **oncancel:** The callback on negative confirmation
@ -72,11 +75,11 @@ This dialog have 3 arguments, they are:
<img class="screenshot" src="/docs/assets/img/app-development/msgprint.png">
Is helpfull for show a informational dialog for the user;
Message Print is used for showing information to users.
This dialog have 2 arguments, they are:
It has 2 arguments:
- **message:** The message content, can be a HTML string too
- **message:** The message to display. It can be a HTML string
- **title:** The title of the dialog
### Example
@ -95,9 +98,7 @@ This dialog have 2 arguments, they are:
<img class="screenshot" src="/docs/assets/img/app-development/dialog.png">
Frappé provide too a `Class` that you can extend and build your own custom dialogs
`frappe.ui.Dialog`
You can extend and build your own custom dialogs using `frappe.ui.Dialog`
### Example

View file

@ -103,7 +103,7 @@ class AutoEmailReport(Document):
@staticmethod
def get_spreadsheet_data(columns, data):
out = [[df.label for df in columns], ]
out = [[_(df.label) for df in columns], ]
for row in data:
new_row = []
out.append(new_row)

View file

@ -265,8 +265,8 @@ class EmailAccount(Document):
uid_reindexed = emails.get("uid_reindexed", False)
for idx, msg in enumerate(incoming_mails):
uid = None if not uid_list else uid_list[idx]
try:
uid = None if not uid_list else uid_list[idx]
args = {
"uid": uid,
"seen": None if not seen_status else get_seen(seen_status.get(uid, None)),
@ -282,7 +282,7 @@ class EmailAccount(Document):
frappe.db.rollback()
log('email_account.receive')
if self.use_imap:
self.handle_bad_emails(email_server, msg[1], msg[0], frappe.get_traceback())
self.handle_bad_emails(email_server, uid, msg, frappe.get_traceback())
exceptions.append(frappe.get_traceback())
else:
@ -309,13 +309,14 @@ class EmailAccount(Document):
message_id = "can't be parsed"
unhandled_email = frappe.get_doc({
"doctype": "Unhandled Email",
"email_account": email_server.settings.email_account,
"raw": raw,
"uid": uid,
"reason":reason,
"message_id": message_id,
"reason":reason
"doctype": "Unhandled Email",
"email_account": email_server.settings.email_account
})
unhandled_email.save()
unhandled_email.insert(ignore_permissions=True)
frappe.db.commit()
def insert_communication(self, msg, args={}):

View file

@ -15,6 +15,7 @@
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -44,6 +45,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -74,6 +76,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -103,6 +106,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -131,6 +135,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -159,6 +164,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -170,7 +176,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Subject",
"length": 0,
@ -187,6 +193,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -198,7 +205,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
@ -215,6 +222,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -245,6 +253,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -275,6 +284,69 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"description": "",
"fieldname": "published",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Published",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "route",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Route",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -304,6 +376,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -333,6 +406,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -362,6 +436,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -391,19 +466,20 @@
"unique": 0
}
],
"has_web_view": 0,
"has_web_view": 1,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-envelope",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_published_field": "published",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 3,
"menu_index": 0,
"modified": "2017-03-07 12:59:18.173824",
"modified": "2017-09-14 15:38:01.891251",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
@ -433,6 +509,7 @@
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"route": "newsletters",
"show_name_in_global_search": 0,
"sort_order": "ASC",
"title_field": "subject",

View file

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
import frappe.utils
from frappe import throw, _
from frappe.model.document import Document
from frappe.website.website_generator import WebsiteGenerator
from frappe.email.queue import check_email_limit
from frappe.utils.verified_command import get_signed_params, verify_request
from frappe.utils.background_jobs import enqueue
@ -17,7 +17,7 @@ from frappe.utils import parse_addr
from frappe.utils import validate_email_add
class Newsletter(Document):
class Newsletter(WebsiteGenerator):
def onload(self):
if self.email_sent:
self.get("__onload").status_count = dict(frappe.db.sql("""select status, count(name)
@ -25,6 +25,7 @@ class Newsletter(Document):
group by status""", (self.doctype, self.name))) or None
def validate(self):
self.route = "newsletters/" + self.name
if self.send_from:
validate_email_add(self.send_from, True)
@ -105,6 +106,26 @@ class Newsletter(Document):
throw(_("Please save the Newsletter before sending"))
check_email_limit(self.recipients)
def get_context(self, context):
newsletters = get_newsletter_list("Newsletter", None, None, 0)
if newsletters:
newsletter_list = [d.name for d in newsletters]
if self.name not in newsletter_list:
frappe.redirect_to_message(_('Permission Error'),
_("You are not permitted to view the newsletter."))
frappe.local.flags.redirect_location = frappe.local.response.location
raise frappe.Redirect
else:
context.attachments = get_attachments(self.name)
context.no_cache = 1
context.show_sidebar = True
def get_attachments(name):
return frappe.get_all("File",
fields=["name", "file_name", "file_url", "is_private"],
filters = {"attached_to_name": name, "attached_to_doctype": "Newsletter", "is_private":0})
def get_email_groups(name):
return frappe.db.get_all("Newsletter Email Group", ["email_group"],{"parent":name, "parenttype":"Newsletter"})
@ -219,4 +240,24 @@ def send_newsletter(newsletter):
frappe.db.commit()
def get_list_context(context=None):
context.update({
"show_sidebar": True,
"show_search": True,
'no_breadcrumbs': True,
"title": _("Newsletter"),
"get_list": get_newsletter_list,
"row_template": "email/doctype/newsletter/templates/newsletter_row.html",
})
def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"):
email_group_list = frappe.db.sql('''select eg.name from `tabEmail Group` eg, `tabEmail Group Member` egm
where egm.unsubscribed=0 and eg.name=egm.email_group and egm.email = %s''', frappe.session.user)
if email_group_list:
return frappe.db.sql('''select n.name, n.subject, n.message, n.modified
from `tabNewsletter` n, `tabNewsletter Email Group` neg
where n.name = neg.parent and n.email_sent=1 and n.published=1 and neg.email_group in %s
order by n.modified desc limit {0}, {1}
'''.format(limit_start, limit_page_length), [email_group_list], as_dict=1)

View file

@ -0,0 +1,65 @@
{% extends "templates/web.html" %}
{% block title %} {{ _("Newsletter") }} {% endblock %}
{% block page_content %}
<style>
.blog-container {
max-width: 720px;
margin: auto;
}
.blog-header {
font-weight: 700;
font-size: 1.5em;
}
.blog-info {
text-align:center;
margin-top: 30px;
}
.blog-text {
padding-top: 50px;
padding-bottom: 50px;
font-size: 15px;
line-height: 1.5;
}
.blog-text p {
margin-bottom: 30px;
}
</style>
<div class="blog-container">
<article class="blog-content" itemscope>
<div class="blog-info">
<h1 itemprop="headline" class="blog-header">{{ doc.subject }}</h1>
<p class="post-by text-muted">
{{ frappe.format_date(doc.modified) }}
</p>
</div>
<div itemprop="articleBody" class="longform blog-text">
{{ doc.message }}
</div>
</article>
{% if attachments %}
<div>
<div class="row text-muted">
<div class="col-sm-12 h6 text-uppercase">
{{ _("Attachments") }}
</div>
</div>
<div class="row">
<div class="col-sm-12">
{% for attachment in attachments %}
<p class="small">
<a href="{{ attachment.file_url }}" target="blank">
{{ attachment.file_name }}
</a>
</p>
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,15 @@
<div class="web-list-item transaction-list-item">
<a href = "{{ route }}/">
<div class="row">
<div class="col-sm-8 text-left bold">
{{ doc.subject }}
</div>
<div class="col-sm-4">
<div class="text-muted text-right"
title="{{ frappe.utils.format_datetime(doc.modified, "medium") }}">
{{ frappe.utils.pretty_date(doc.modified) }}
</div>
</div>
</div>
</a>
</div>

View file

@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Newsletter", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Newsletter
() => frappe.tests.make('Newsletter', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View file

@ -7,24 +7,26 @@ import frappe, unittest
from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe
from six.moves.urllib.parse import unquote
emails = ["test_subscriber1@example.com", "test_subscriber2@example.com",
"test_subscriber3@example.com"]
"test_subscriber3@example.com", "test1@example.com"]
class TestNewsletter(unittest.TestCase):
def setUp(self):
frappe.set_user("Administrator")
frappe.db.sql('delete from `tabEmail Group Member`')
for email in emails:
frappe.get_doc({
"doctype": "Email Group Member",
"email": email,
"email_group": "_Test Email Group"
}).insert()
}).insert()
def test_send(self):
name = self.send_newsletter()
email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")]
self.assertEquals(len(email_queue_list), 3)
self.assertEquals(len(email_queue_list), 4)
recipients = [e.recipients[0].recipient for e in email_queue_list]
for email in emails:
self.assertTrue(email in recipients)
@ -41,13 +43,14 @@ class TestNewsletter(unittest.TestCase):
name = self.send_newsletter()
email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")]
self.assertEquals(len(email_queue_list), 2)
self.assertEquals(len(email_queue_list), 3)
recipients = [e.recipients[0].recipient for e in email_queue_list]
for email in emails:
if email != to_unsubscribe:
self.assertTrue(email in recipients)
def send_newsletter(self):
@staticmethod
def send_newsletter(published=0):
frappe.db.sql("delete from `tabEmail Queue`")
frappe.db.sql("delete from `tabEmail Queue Recipient`")
frappe.db.sql("delete from `tabNewsletter`")
@ -55,7 +58,8 @@ class TestNewsletter(unittest.TestCase):
"doctype": "Newsletter",
"subject": "_Test Newsletter",
"send_from": "Test Sender <test_sender@example.com>",
"message": "Testing my news."
"message": "Testing my news.",
"published": published
}).insert(ignore_permissions=True)
newsletter.append("email_group", {"email_group": "_Test Email Group"})
@ -63,4 +67,21 @@ class TestNewsletter(unittest.TestCase):
newsletter.send_emails()
return newsletter.name
def test_portal(self):
self.send_newsletter(1)
frappe.set_user("test1@example.com")
from frappe.email.doctype.newsletter.newsletter import get_newsletter_list
newsletters = get_newsletter_list("Newsletter", None, None, 0)
self.assertEquals(len(newsletters), 1)
def test_newsletter_context(self):
context = frappe._dict()
newsletter_name = self.send_newsletter(1)
frappe.set_user("test2@example.com")
doc = frappe.get_doc("Newsletter", newsletter_name)
doc.get_context(context)
self.assertEquals(context.no_cache, 1)
self.assertTrue("attachments" not in context.keys())
test_dependencies = ["Email Group"]

View file

@ -1,5 +1,6 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
@ -11,6 +12,7 @@
"editable_grid": 0,
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -21,6 +23,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Email Account",
@ -40,6 +43,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -50,9 +54,10 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "uid",
"label": "UID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -68,6 +73,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -78,6 +84,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Reason",
@ -96,6 +103,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -106,6 +114,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message-id",
@ -124,6 +133,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -134,6 +144,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Raw Email",
@ -152,17 +163,17 @@
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-01-20 05:15:57.216825",
"modified": "2017-09-19 16:28:00.042256",
"modified_by": "Administrator",
"module": "Email",
"name": "Unhandled Email",
@ -173,8 +184,8 @@
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
@ -187,12 +198,13 @@
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 1
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,

View file

@ -51,7 +51,7 @@ class EMail:
Also sets all messages as multipart/alternative for cleaner reading in text-only clients
"""
def __init__(self, sender='', recipients=(), subject='', alternative=0, reply_to=None, cc=(), email_account=None, expose_recipients=None):
from email import Charset
from email import charset as Charset
Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
if isinstance(recipients, string_types):
@ -219,10 +219,7 @@ class EMail:
frappe.get_attr(hook)(self)
def set_header(self, key, value):
key = encode(key)
value = encode(value)
if self.msg_root.has_key(key):
if key in self.msg_root:
del self.msg_root[key]
self.msg_root[key] = value

View file

@ -53,7 +53,7 @@ def get_outgoing_email_account(raise_exception_not_set=True, append_to=None):
if not email_account:
email_account = get_default_outgoing_email_account(raise_exception_not_set=raise_exception_not_set)
if not email_account and raise_exception_not_set:
if not email_account and raise_exception_not_set and cint(frappe.db.get_single_value('System Settings', 'setup_complete')):
frappe.throw(_("Please setup default Email Account from Setup > Email > Email Account"),
frappe.OutgoingEmailError)

View file

@ -292,3 +292,37 @@ class FrappeClient(object):
return rjson['data']
else:
return None
class FrappeOAuth2Client(FrappeClient):
def __init__(self, url, access_token, verify=True):
self.access_token = access_token
self.headers = {
"Authorization": "Bearer " + access_token,
"content-type": "application/x-www-form-urlencoded"
}
self.verify = verify
self.session = OAuth2Session(self.headers)
self.url = url
def get_request(self, params):
res = requests.get(self.url, params=self.preprocess(params), headers=self.headers, verify=self.verify)
res = self.post_process(res)
return res
def post_request(self, data):
res = requests.post(self.url, data=self.preprocess(data), headers=self.headers, verify=self.verify)
res = self.post_process(res)
return res
class OAuth2Session():
def __init__(self, headers):
self.headers = headers
def get(self, url, params, verify):
res = requests.get(url, params=params, headers=self.headers, verify=verify)
return res
def post(self, url, data, verify):
res = requests.post(url, data=data, headers=self.headers, verify=verify)
return res
def put(self, url, data, verify):
res = requests.put(url, data=data, headers=self.headers, verify=verify)
return res

View file

@ -269,9 +269,11 @@
},
"Benin": {
"code": "bj",
"currency": "XOF",
"currency_name": "West African CFA Franc",
"currency_symbol": "CFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Porto-Novo"
@ -386,6 +388,8 @@
},
"Bulgaria": {
"code": "bg",
"currency": "BGN",
"currency_name": "Bulgarian Lev",
"currency_fraction": "Stotinka",
"currency_fraction_units": 100,
"currency_symbol": "\u043b\u0432",
@ -396,9 +400,11 @@
},
"Burkina Faso": {
"code": "bf",
"currency": "XOF",
"currency_name": "West African CFA Franc",
"currency_symbol": "CFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Ouagadougou"
@ -430,9 +436,11 @@
},
"Cameroon": {
"code": "cm",
"currency": "XAF",
"currency_name": "Central African CFA Franc",
"currency_symbol": "FCFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Douala"
@ -504,9 +512,11 @@
},
"Central African Republic": {
"code": "cf",
"currency": "XAF",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"currency_name": "Central African CFA Franc",
"currency_symbol": "FCFA",
"number_format": "#,###.##",
"timezones": [
"Africa/Bangui"
@ -514,9 +524,11 @@
},
"Chad": {
"code": "td",
"currency": "XAF",
"currency_name": "Central African CFA Franc",
"currency_symbol": "FCFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Ndjamena"
@ -592,7 +604,12 @@
},
"Congo": {
"code": "cg",
"number_format": "#,###.##"
"number_format": "#,###.##",
"currency": "XAF",
"currency_name": "Central African CFA Franc",
"currency_symbol": "FCFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100
},
"Congo, The Democratic Republic of the": {
"code": "cd",
@ -758,9 +775,11 @@
},
"Equatorial Guinea": {
"code": "gq",
"currency": "XAF",
"currency_name": "Central African CFA Franc",
"currency_symbol": "FCFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Malabo"
@ -877,9 +896,11 @@
},
"Gabon": {
"code": "ga",
"currency": "XAF",
"currency_name": "Central African CFA Franc",
"currency_symbol": "FCFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Libreville"
@ -1019,9 +1040,11 @@
},
"Guinea-Bissau": {
"code": "gw",
"currency": "XOF",
"currency_name": "West African CFA Franc",
"currency_symbol": "CFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Bissau"
@ -1205,10 +1228,15 @@
},
"Ivory Coast": {
"code": "ci",
"currency": "XOF",
"currency_name": "West African CFA Franc",
"currency_symbol": "CFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##"
"number_format": "#,###.##",
"timeszones": [
"Africa/Abidjan"
]
},
"Jamaica": {
"code": "jm",
@ -1496,9 +1524,11 @@
},
"Mali": {
"code": "ml",
"currency": "XOF",
"currency_name": "West African CFA Franc",
"currency_symbol": "CFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Bamako"
@ -1758,9 +1788,11 @@
},
"Niger": {
"code": "ne",
"currency": "XOF",
"currency_name": "West African CFA Franc",
"currency_symbol": "CFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Niamey"
@ -2087,9 +2119,11 @@
},
"Senegal": {
"code": "sn",
"currency": "XOF",
"currency_name": "West African CFA Franc",
"currency_symbol": "CFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Dakar"
@ -2355,9 +2389,11 @@
},
"Togo": {
"code": "tg",
"currency": "XOF",
"currency_name": "West African CFA Franc",
"currency_symbol": "CFA",
"currency_fraction": "Centime",
"currency_fraction_units": 100,
"currency_symbol": "Fr",
"number_format": "#,###.##",
"timezones": [
"Africa/Lome"

View file

@ -1,294 +1,322 @@
[
{
"code": "am",
"code": "af",
"name": "Afrikaans"
},
{
"code": "am",
"name": "\u12a0\u121b\u122d\u129b"
},
},
{
"code": "ar",
"code": "ar",
"name": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629"
},
},
{
"code": "bg",
"code": "bg",
"name": "B\u01celgarski"
},
},
{
"code": "bn",
"code": "bn",
"name": "\u09ac\u09be\u0999\u09be\u09b2\u09bf"
},
},
{
"code": "bo",
"code": "bo",
"name": "\u0f63\u0fb7\u0f0b\u0f66\u0f60\u0f72\u0f0b\u0f66\u0f90\u0f51\u0f0b"
},
},
{
"code": "bs",
"code": "bs",
"name": "Bosanski"
},
},
{
"code": "ca",
"code": "ca",
"name": "Catal\u00e0"
},
},
{
"code": "cs",
"code": "cs",
"name": "\u010desky"
},
},
{
"code": "da",
"code": "da",
"name": "Dansk"
},
},
{
"code": "da-DK",
"code": "da-DK",
"name": "Dansk (Danmark)"
},
},
{
"code": "de",
"code": "de",
"name": "Deutsch"
},
},
{
"code": "el",
"code": "el",
"name": "\u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"
},
},
{
"code": "en",
"code": "en",
"name": "English"
},
},
{
"code": "en-GB",
"code": "en-GB",
"name": "English (United Kingdom)"
},
},
{
"code": "en-US",
"code": "en-US",
"name": "English (United States)"
},
},
{
"code": "es",
"code": "es",
"name": "Espa\u00f1ol"
},
},
{
"code": "es-AR",
"code": "es-AR",
"name": "Espa\u00f1ol (Argentina)"
},
},
{
"code": "es-CL",
"code": "es-BO",
"name": "Espa\u00f1ol (Bolivia)"
},
{
"code": "es-CL",
"name": "Espa\u00f1ol (Chile)"
},
},
{
"code": "es-GT",
"code": "es-CO",
"name": "Espa\u00f1ol (Colombia)"
},
{
"code": "es-DO",
"name": "Espa\u00f1ol (Rep\u00fablica Dominicana)"
},
{
"code": "es-EC",
"name": "Espa\u00f1ol (Ecuador)"
},
{
"code": "es-GT",
"name": "Espa\u00f1ol (Guatemala)"
},
},
{
"code": "es-MX",
"code": "es-MX",
"name": "Espa\u00f1ol (M\u00e9xico)"
},
},
{
"code": "es-NI",
"code": "es-NI",
"name": "Espa\u00f1ol (Nicaragua)"
},
},
{
"code": "es-PE",
"code": "es-PE",
"name": "Espa\u00f1ol (Per\u00fa)"
},
},
{
"code": "et",
"code": "et",
"name": "Eesti"
},
},
{
"code": "fa",
"code": "fa",
"name": "\u067e\u0627\u0631\u0633\u06cc"
},
},
{
"code": "fi",
"name": "Suomalainen"
},
"code": "fi",
"name": "Suomi"
},
{
"code": "fr",
"code": "fr",
"name": "Fran\u00e7ais"
},
},
{
"code": "fr-CA",
"code": "fr-CA",
"name": "Fran\u00e7ais Canadien"
},
},
{
"code": "gu",
"code": "gu",
"name": "\u0a97\u0ac1\u0a9c\u0ab0\u0abe\u0aa4\u0ac0"
},
},
{
"code": "he",
"code": "he",
"name": "\u05e2\u05d1\u05e8\u05d9\u05ea"
},
},
{
"code": "hi",
"code": "hi",
"name": "\u0939\u093f\u0902\u0926\u0940"
},
},
{
"code": "hr",
"code": "hr",
"name": "Hrvatski"
},
},
{
"code": "hu",
"code": "hu",
"name": "Magyar"
},
},
{
"code": "id",
"code": "id",
"name": "Indonesia"
},
},
{
"code": "is",
"code": "is",
"name": "\u00edslenska"
},
},
{
"code": "it",
"code": "it",
"name": "Italiano"
},
},
{
"code": "ja",
"code": "ja",
"name": "\u65e5\u672c\u8a9e"
},
},
{
"code": "km",
"code": "km",
"name": "\u1797\u17b6\u179f\u17b6\u1781\u17d2\u1798\u17c2\u179a"
},
},
{
"code": "kn",
"code": "kn",
"name": "\u0c95\u0ca8\u0ccd\u0ca8\u0ca1"
},
},
{
"code": "ko",
"code": "ko",
"name": "\ud55c\uad6d\uc758"
},
},
{
"code": "ku",
"code": "ku",
"name": "\u06a9\u0648\u0631\u062f\u06cc"
},
},
{
"code": "lo",
"code": "lo",
"name": "\u0ea5\u0eb2\u0ea7"
},
},
{
"code": "lt",
"code": "lt",
"name": "Lietuvi\u0173 kalba"
},
},
{
"code": "lv",
"code": "lv",
"name": "Latvie\u0161u valoda"
},
},
{
"code": "mk",
"code": "mk",
"name": "\u043c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438"
},
},
{
"code": "ml",
"code": "ml",
"name": "\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02"
},
},
{
"code": "mr",
"code": "mr",
"name": "\u092e\u0930\u093e\u0920\u0940"
},
},
{
"code": "ms",
"code": "ms",
"name": "Melayu"
},
},
{
"code": "my",
"code": "my",
"name": "\u1019\u103c\u1014\u103a\u1019\u102c"
},
},
{
"code": "nl",
"code": "nl",
"name": "Nederlands"
},
},
{
"code": "no",
"code": "no",
"name": "Norsk"
},
},
{
"code": "pl",
"code": "pl",
"name": "Polski"
},
},
{
"code": "ps",
"code": "ps",
"name": "\u067e\u069a\u062a\u0648"
},
},
{
"code": "pt",
"code": "pt",
"name": "Portugu\u00eas"
},
},
{
"code": "pt-BR",
"code": "pt-BR",
"name": "Portugu\u00eas Brasileiro"
},
},
{
"code": "ro",
"code": "ro",
"name": "Rom\u00e2n"
},
},
{
"code": "ru",
"code": "ru",
"name": "\u0440\u0443\u0441\u0441\u043a\u0438\u0439"
},
},
{
"code": "rw",
"code": "rw",
"name": "Kinyarwanda"
},
},
{
"code": "si",
"code": "si",
"name": "\u0dc3\u0dd2\u0d82\u0dc4\u0dbd"
},
},
{
"code": "sk",
"code": "sk",
"name": "Sloven\u010dina (Slovak)"
},
},
{
"code": "sl",
"code": "sl",
"name": "Sloven\u0161\u010dina (Slovene)"
},
},
{
"code": "sq",
"code": "sq",
"name": "Shqiptar"
},
},
{
"code": "sr",
"code": "sr",
"name": "\u0441\u0440\u043f\u0441\u043a\u0438"
},
},
{
"code": "sr-SP",
"code": "sr-SP",
"name": "Srpski"
},
},
{
"code": "sv",
"code": "sv",
"name": "Svenska"
},
},
{
"code": "ta",
"code": "sw",
"name": "Swahili"
},
{
"code": "ta",
"name": "\u0ba4\u0bae\u0bbf\u0bb4\u0bcd"
},
},
{
"code": "te",
"code": "te",
"name": "\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41"
},
},
{
"code": "th",
"code": "th",
"name": "\u0e44\u0e17\u0e22"
},
},
{
"code": "tr",
"code": "tr",
"name": "T\u00fcrk"
},
},
{
"code": "uk",
"code": "uk",
"name": "\u0443\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430"
},
},
{
"code": "ur",
"code": "ur",
"name": "\u0627\u0631\u062f\u0648"
},
},
{
"code": "vi",
"code": "uz",
"name": "\u040e\u0437\u0431\u0435\u043a"
},
{
"code": "vi",
"name": "Vi\u1ec7t"
},
},
{
"code": "zh",
"code": "zh",
"name": "\u7b80\u4f53\u4e2d\u6587"
},
},
{
"code": "zh-TW",
"code": "zh-TW",
"name": "\u7e41\u9ad4\u4e2d\u6587"
}
]
]

View file

@ -1,6 +1,7 @@
from __future__ import unicode_literals
from . import __version__ as app_version
app_name = "frappe"
app_title = "Frappe Framework"
app_publisher = "Frappe Technologies"
@ -48,9 +49,11 @@ bootstrap = "assets/frappe/css/bootstrap.css"
web_include_css = [
"assets/css/frappe-web.css"
]
website_route_rules = [
{"from_route": "/blog/<category>", "to_route": "Blog Post"},
{"from_route": "/kb/<category>", "to_route": "Help Article"}
{"from_route": "/kb/<category>", "to_route": "Help Article"},
{"from_route": "/newsletters", "to_route": "Newsletter"}
]
write_file_keys = ["file_url", "file_name"]

View file

@ -47,7 +47,7 @@ def gsuite_callback(code=None):
'grant_type': 'authorization_code'}
r = requests.post('https://www.googleapis.com/oauth2/v4/token', data=data).json()
frappe.db.set_value("Gsuite Settings", None, "authorization_code", code)
if r.has_key('refresh_token'):
if 'refresh_token' in r:
frappe.db.set_value("Gsuite Settings", None, "refresh_token", r['refresh_token'])
frappe.db.commit()
return

View file

@ -328,6 +328,11 @@ class Document(BaseDocument):
and parenttype=%s and parentfield=%s""".format(df.options),
(self.name, self.doctype, fieldname))
def get_doc_before_save(self):
if not getattr(self, '_doc_before_save', None):
self._doc_before_save = frappe.get_doc(self.doctype, self.name)
return self._doc_before_save
def set_new_name(self):
"""Calls `frappe.naming.se_new_name` for parent and child docs."""
set_new_name(self)
@ -763,7 +768,7 @@ class Document(BaseDocument):
self._doc_before_save = None
if not self.is_new() and getattr(self.meta, 'track_changes', False):
self._doc_before_save = frappe.get_doc(self.doctype, self.name)
self.get_doc_before_save()
if self.flags.ignore_validate:
return

View file

@ -148,9 +148,9 @@ class OAuthWebRequestValidator(RequestValidator):
oc = frappe.get_doc("OAuth Client", request.client_id)
else:
#Extract token, instantiate OAuth Bearer Token and use clientid from there.
if frappe.form_dict.has_key("refresh_token"):
if "refresh_token" in frappe.form_dict:
oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", {"refresh_token": frappe.form_dict["refresh_token"]}, 'client'))
elif frappe.form_dict.has_key("token"):
elif "token" in frappe.form_dict:
oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", frappe.form_dict["token"], 'client'))
else:
oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", frappe.get_request_header("Authorization").split(" ")[1], 'client'))

View file

@ -193,3 +193,4 @@ frappe.patches.v8_x.update_user_permission
frappe.patches.v8_5.patch_event_colors
frappe.patches.v8_7.update_email_queue_status
frappe.patches.v8_10.delete_static_web_page_from_global_search
frappe.patches.v8_x.add_bgn_xaf_xof_currencies

View file

@ -6,7 +6,7 @@ def execute():
for ls in list_settings:
if ls and ls.data:
data = json.loads(ls.data)
if not data.has_key("fields"):
if "fields" not in data:
continue
fields = data["fields"]
for field in fields:

View file

@ -0,0 +1,12 @@
"""
This will add the following currencies:
1. BGN (Bulgarian Lev) to Bulgaria.
2. XAF (Central African CFA Franc) to Cameroon, Republic of Congo, Chad, Gabon, Equitorial Guinea and
Central African Republic.
3. XOF (West African CFA Franc) to Benin, Niger, Burkina Faso, Mali, Senegal, Togo, Ivory Coast and Guinea Bissau.
"""
from frappe.utils.install import import_country_and_currency
def execute():
import_country_and_currency()

View file

@ -245,7 +245,7 @@ def get_role_permissions(meta, user=None, verbose=False):
perms["apply_user_permissions"][ptype] = 1
# delete 0 values
for key, value in perms.get("apply_user_permissions").items():
for key, value in list(perms.get("apply_user_permissions").items()):
if not value:
del perms["apply_user_permissions"][key]

View file

@ -61,7 +61,6 @@
"public/js/frappe/form/link_selector.js",
"public/js/frappe/form/multi_select_dialog.js",
"public/js/frappe/ui/dialog.js",
"public/js/frappe/form/controls/base_control.js",
"public/js/frappe/form/controls/base_input.js",
"public/js/frappe/form/controls/data.js",
@ -98,7 +97,6 @@
"public/css/bootstrap.css",
"public/css/font-awesome.css",
"public/css/octicons/octicons.css",
"public/css/cal-heatmap.css",
"public/css/c3.min.css",
"public/css/desk.css",
"public/css/indicator.css",
@ -162,6 +160,7 @@
"public/js/frappe/ui/page.html",
"public/js/frappe/ui/page.js",
"public/js/frappe/ui/slides.js",
"public/js/frappe/ui/find.js",
"public/js/frappe/ui/iconbar.js",
"public/js/frappe/form/layout.js",
@ -231,7 +230,6 @@
],
"js/d3.min.js": [
"public/js/lib/d3.min.js",
"public/js/lib/cal-heatmap.js",
"public/js/lib/c3.min.js"
],
"css/module.min.css": [

View file

@ -1,140 +0,0 @@
/* Cal-HeatMap CSS */
.cal-heatmap-container {
display: block;
}
.cal-heatmap-container .graph-label
{
fill: #999;
font-size: 10px
}
.cal-heatmap-container .graph, .cal-heatmap-container .graph-legend rect {
shape-rendering: crispedges
}
.cal-heatmap-container .graph-rect
{
fill: #ededed
}
.cal-heatmap-container .graph-subdomain-group rect:hover
{
stroke: #000;
stroke-width: 1px
}
.cal-heatmap-container .subdomain-text {
font-size: 8px;
fill: #999;
pointer-events: none
}
.cal-heatmap-container .hover_cursor:hover {
cursor: pointer
}
.cal-heatmap-container .qi {
background-color: #999;
fill: #999
}
/*
Remove comment to apply this style to date with value equal to 0
.q0
{
background-color: #fff;
fill: #fff;
stroke: #ededed
}
*/
.cal-heatmap-container .q1
{
background-color: #dae289;
fill: #dae289
}
.cal-heatmap-container .q2
{
background-color: #cedb9c;
fill: #9cc069
}
.cal-heatmap-container .q3
{
background-color: #b5cf6b;
fill: #669d45
}
.cal-heatmap-container .q4
{
background-color: #637939;
fill: #637939
}
.cal-heatmap-container .q5
{
background-color: #3b6427;
fill: #3b6427
}
.cal-heatmap-container rect.highlight
{
stroke:#444;
stroke-width:1
}
.cal-heatmap-container text.highlight
{
fill: #444
}
.cal-heatmap-container rect.now
{
stroke: red
}
.cal-heatmap-container text.now
{
fill: red;
font-weight: 800
}
.cal-heatmap-container .domain-background {
fill: none;
shape-rendering: crispedges
}
.ch-tooltip {
padding: 10px;
background: #222;
color: #bbb;
font-size: 12px;
line-height: 1.4;
width: 140px;
position: absolute;
z-index: 99999;
text-align: center;
border-radius: 2px;
box-shadow: 2px 2px 2px rgba(0,0,0,0.2);
display: none;
box-sizing: border-box;
}
.ch-tooltip::after{
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
content: "";
padding: 0;
display: block;
bottom: -6px;
left: 50%;
margin-left: -6px;
border-width: 6px 6px 0;
border-top-color: #222;
}

View file

@ -441,7 +441,7 @@ fieldset[disabled] .form-control {
}
}
@media (min-width: 768px) {
.video-modal {
.video-modal .modal-dialog {
width: 700px;
}
}
@ -510,7 +510,7 @@ fieldset[disabled] .form-control {
margin-right: 10px;
}
a.progress-small .progress-chart {
width: 60px;
width: 40px;
margin-top: 4px;
float: right;
}
@ -518,6 +518,20 @@ a.progress-small .progress {
margin-bottom: 0;
}
a.progress-small .progress-bar {
transition: unset;
background-color: #98d85b;
}
li.user-progress .progress-chart {
width: 50px;
margin-top: 8px;
}
li.user-progress .progress {
margin-bottom: 0;
background-color: #fff;
border: 1px solid #e5e7e9;
}
li.user-progress .progress-bar {
transition: unset;
background-color: #98d85b;
}
/* on small screens, show only icons on top */
@ -530,6 +544,9 @@ a.progress-small .progress-bar {
margin-top: 0px;
margin-left: 2px;
}
li.user-progress .progress-chart {
width: 25px;
}
}
.msg-box {
padding: 30px 15px;
@ -1070,3 +1087,75 @@ input[type="checkbox"]:checked:before {
margin: -2px 0 0 3px;
border: 1px solid rgba(0, 0, 0, 0.25);
}
.slides-wrapper:focus {
outline: none;
}
.slides-wrapper .fa-circle {
font-size: 10px;
margin: 0px 2px;
}
.slides-wrapper .fa-circle.active {
color: #5e64ff;
}
.slides-wrapper .fa-circle.link {
cursor: pointer;
}
.slides-wrapper .slide-wrapper:focus {
outline: none;
}
.slides-wrapper .form {
margin-top: 30px;
}
.slides-wrapper .form .form-layout {
margin-top: 0px;
margin-bottom: 0px;
}
.slides-wrapper .form .form-section {
padding: 0px 7px;
border: none;
}
.slides-wrapper .add-more {
margin-bottom: 30px;
}
.slides-wrapper .lead {
margin-top: 20px;
}
.slides-wrapper .success-state {
margin-bottom: 20px;
}
.slides-wrapper .next-steps-links .title {
text-transform: uppercase;
color: #8D99A6;
font-size: 11px;
}
.slides-wrapper .btn-primary {
font-weight: bold;
}
.slides-wrapper .footer {
margin-top: 15px;
padding: 0px 7px;
}
.slides-wrapper .footer .btn:not(:last-child) {
margin-right: 3px;
}
.slides-wrapper .footer a.btn.make-btn {
margin-right: 7px;
}
.slides-wrapper .footer a.make-btn.disabled {
background-color: #b1bdca;
color: #fff;
border-color: #b1bdca;
}
.user-progress-dialog .slides-progress {
margin-top: 15px;
}
.user-progress-dialog .done-state .check-container {
font-size: 64px;
margin: 40px;
}
.user-progress-dialog .done-state .title {
font-weight: normal;
}
.user-progress-dialog .done-state .help-links a {
margin: 0px 10px;
}

View file

@ -98,6 +98,10 @@
.form-dashboard-section:last-child {
border-bottom: none;
}
.form-heatmap .heatmap {
display: flex;
justify-content: center;
}
.form-heatmap .heatmap-message {
margin-top: 10px;
}
@ -597,11 +601,9 @@ select.form-control {
.control-code.bold {
height: 400px;
font-family: Monaco, "Courier New", monospace;
background-color: black;
color: #fffce7;
color: #36414C;
font-size: 12px;
line-height: 1.7em;
border: none;
}
.delivery-status-indicator {
display: inline-block;

View file

@ -2,9 +2,10 @@
.graph-container .graph-focus-margin {
margin: 0px 5%;
}
.graph-container .graph-graphics {
.graph-container .graphics {
margin-top: 10px;
padding: 10px 0px;
padding-top: 10px;
padding-bottom: 10px;
position: relative;
}
.graph-container .graph-stats-group {
@ -34,31 +35,28 @@
.graph-container .graph-stats-container .graph-data .stats-value {
color: #98d85b;
}
.graph-container .bar-graph .axis,
.graph-container .line-graph .axis {
.graph-container .axis,
.graph-container .chart-label {
font-size: 10px;
fill: #6a737d;
fill: #959ba1;
}
.graph-container .bar-graph .axis line,
.graph-container .line-graph .axis line {
.graph-container .axis line,
.graph-container .chart-label line {
stroke: rgba(27, 31, 35, 0.1);
}
.graph-container .percentage-graph {
margin-top: 35px;
}
.graph-container .percentage-graph .progress {
margin-bottom: 0px;
}
.graph-container .graph-data-points circle {
.graph-container .data-points circle {
stroke: #fff;
stroke-width: 2;
}
.graph-container .graph-data-points path {
.graph-container .data-points path {
fill: none;
stroke-opacity: 1;
stroke-width: 2px;
}
.graph-container line.graph-dashed {
.graph-container line.dashed {
stroke-dasharray: 5,3;
}
.graph-container .tick.x-axis-label {
@ -73,7 +71,7 @@
.graph-container .tick .x-value-text {
text-anchor: middle;
}
.graph-container .graph-svg-tip {
.graph-svg-tip {
position: absolute;
z-index: 99999;
padding: 10px;
@ -83,12 +81,12 @@
background: rgba(0, 0, 0, 0.8);
border-radius: 3px;
}
.graph-container .graph-svg-tip.comparison {
.graph-svg-tip.comparison {
padding: 0;
text-align: left;
pointer-events: none;
}
.graph-container .graph-svg-tip.comparison .title {
.graph-svg-tip.comparison .title {
display: block;
padding: 10px;
margin: 0;
@ -96,28 +94,28 @@
line-height: 1;
pointer-events: none;
}
.graph-container .graph-svg-tip.comparison ul {
.graph-svg-tip.comparison ul {
margin: 0;
white-space: nowrap;
list-style: none;
}
.graph-container .graph-svg-tip.comparison li {
.graph-svg-tip.comparison li {
display: inline-block;
padding: 5px 10px;
}
.graph-container .graph-svg-tip ul,
.graph-container .graph-svg-tip ol {
.graph-svg-tip ul,
.graph-svg-tip ol {
padding-left: 0;
display: flex;
}
.graph-container .graph-svg-tip ul.data-point-list li {
.graph-svg-tip ul.data-point-list li {
min-width: 90px;
flex: 1;
}
.graph-container .graph-svg-tip strong {
.graph-svg-tip strong {
color: #dfe2e5;
}
.graph-container .graph-svg-tip::after {
.graph-svg-tip .svg-pointer {
position: absolute;
bottom: -10px;
left: 50%;
@ -128,147 +126,147 @@
border: 5px solid transparent;
border-top-color: rgba(0, 0, 0, 0.8);
}
.graph-container .stroke.grey {
.stroke.grey {
stroke: #F0F4F7;
}
.graph-container .stroke.blue {
.stroke.blue {
stroke: #5e64ff;
}
.graph-container .stroke.red {
.stroke.red {
stroke: #ff5858;
}
.graph-container .stroke.light-green {
.stroke.light-green {
stroke: #98d85b;
}
.graph-container .stroke.green {
.stroke.green {
stroke: #28a745;
}
.graph-container .stroke.orange {
.stroke.orange {
stroke: #ffa00a;
}
.graph-container .stroke.purple {
.stroke.purple {
stroke: #743ee2;
}
.graph-container .stroke.darkgrey {
.stroke.darkgrey {
stroke: #b8c2cc;
}
.graph-container .stroke.black {
.stroke.black {
stroke: #36414C;
}
.graph-container .stroke.yellow {
.stroke.yellow {
stroke: #FEEF72;
}
.graph-container .stroke.light-blue {
.stroke.light-blue {
stroke: #7CD6FD;
}
.graph-container .stroke.lightblue {
.stroke.lightblue {
stroke: #7CD6FD;
}
.graph-container .fill.grey {
.fill.grey {
fill: #F0F4F7;
}
.graph-container .fill.blue {
.fill.blue {
fill: #5e64ff;
}
.graph-container .fill.red {
.fill.red {
fill: #ff5858;
}
.graph-container .fill.light-green {
.fill.light-green {
fill: #98d85b;
}
.graph-container .fill.green {
.fill.green {
fill: #28a745;
}
.graph-container .fill.orange {
.fill.orange {
fill: #ffa00a;
}
.graph-container .fill.purple {
.fill.purple {
fill: #743ee2;
}
.graph-container .fill.darkgrey {
.fill.darkgrey {
fill: #b8c2cc;
}
.graph-container .fill.black {
.fill.black {
fill: #36414C;
}
.graph-container .fill.yellow {
.fill.yellow {
fill: #FEEF72;
}
.graph-container .fill.light-blue {
.fill.light-blue {
fill: #7CD6FD;
}
.graph-container .fill.lightblue {
.fill.lightblue {
fill: #7CD6FD;
}
.graph-container .background.grey {
.background.grey {
background: #F0F4F7;
}
.graph-container .background.blue {
.background.blue {
background: #5e64ff;
}
.graph-container .background.red {
.background.red {
background: #ff5858;
}
.graph-container .background.light-green {
.background.light-green {
background: #98d85b;
}
.graph-container .background.green {
.background.green {
background: #28a745;
}
.graph-container .background.orange {
.background.orange {
background: #ffa00a;
}
.graph-container .background.purple {
.background.purple {
background: #743ee2;
}
.graph-container .background.darkgrey {
.background.darkgrey {
background: #b8c2cc;
}
.graph-container .background.black {
.background.black {
background: #36414C;
}
.graph-container .background.yellow {
.background.yellow {
background: #FEEF72;
}
.graph-container .background.light-blue {
.background.light-blue {
background: #7CD6FD;
}
.graph-container .background.lightblue {
.background.lightblue {
background: #7CD6FD;
}
.graph-container .border-top.grey {
.border-top.grey {
border-top: 3px solid #F0F4F7;
}
.graph-container .border-top.blue {
.border-top.blue {
border-top: 3px solid #5e64ff;
}
.graph-container .border-top.red {
.border-top.red {
border-top: 3px solid #ff5858;
}
.graph-container .border-top.light-green {
.border-top.light-green {
border-top: 3px solid #98d85b;
}
.graph-container .border-top.green {
.border-top.green {
border-top: 3px solid #28a745;
}
.graph-container .border-top.orange {
.border-top.orange {
border-top: 3px solid #ffa00a;
}
.graph-container .border-top.purple {
.border-top.purple {
border-top: 3px solid #743ee2;
}
.graph-container .border-top.darkgrey {
.border-top.darkgrey {
border-top: 3px solid #b8c2cc;
}
.graph-container .border-top.black {
.border-top.black {
border-top: 3px solid #36414C;
}
.graph-container .border-top.yellow {
.border-top.yellow {
border-top: 3px solid #FEEF72;
}
.graph-container .border-top.light-blue {
.border-top.light-blue {
border-top: 3px solid #7CD6FD;
}
.graph-container .border-top.lightblue {
.border-top.lightblue {
border-top: 3px solid #7CD6FD;
}

View file

@ -67,6 +67,10 @@ body {
#navbar-breadcrumbs {
margin: 0px;
display: inline-block;
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#navbar-breadcrumbs > li,
#navbar-breadcrumbs > li > a {
@ -187,11 +191,8 @@ body {
}
}
@media (max-width: 991px) and (max-width: 480px) {
#navbar-breadcrumbs li a {
#navbar-breadcrumbs li > a {
width: 100px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
@media (max-width: 767px) {

View file

@ -133,6 +133,7 @@
background-color: #ff5858;
}
.navbar-form .awesomplete {
margin-left: -15px;
width: 300px;
}
@media (max-width: 1199px) {
@ -195,13 +196,14 @@
}
#navbar-breadcrumbs > li > a {
padding: 6px 15px 10px 0px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 170px;
}
@media (min-width: 991px) and (max-width: 1199px) {
#navbar-breadcrumbs > li > a {
max-width: 143px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px;
}
}
.toolbar-user-fullname {

View file

@ -153,3 +153,210 @@ select.input-sm {
font-size: 18px;
}
}
#page-setup-wizard {
margin-top: 30px;
}
@media (min-width: 768px) {
.setup-wizard-slide {
max-width: 500px;
}
}
.setup-wizard-slide {
margin: 60px auto;
padding: 10px 50px;
border: 1px solid #d1d8dd;
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.1);
}
.setup-wizard-slide .slides-progress {
margin-top: 20px;
}
.setup-wizard-slide .lead {
margin: 30px;
color: #777777;
text-align: center;
font-size: 24px;
}
.setup-wizard-slide .col-sm-12 {
padding: 0px;
}
.setup-wizard-slide .section-body .col-sm-6:first-child {
padding-left: 0px;
}
.setup-wizard-slide .section-body .col-sm-6:last-child {
padding-right: 0px;
}
.setup-wizard-slide .form-control {
font-weight: 500;
}
.setup-wizard-slide .form-control.bold {
background-color: #fff;
}
.setup-wizard-slide .add-more {
margin: 0px;
}
.setup-wizard-slide .footer {
padding: 30px 7px;
}
.setup-wizard-slide a.next-btn.disabled {
background-color: #b1bdca;
color: #fff;
border-color: #b1bdca;
}
.setup-wizard-slide a.complete-btn.disabled {
background-color: #b1bdca;
color: #fff;
border-color: #b1bdca;
}
.setup-wizard-slide .fa-fw {
vertical-align: middle;
font-size: 10px;
}
.setup-wizard-slide .fa-fw.active {
color: #5e64ff;
}
.setup-wizard-slide .icon-circle-blank {
font-size: 7px;
}
.setup-wizard-slide .icon-circle {
font-size: 10px;
}
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] {
width: 140px;
height: 180px;
margin-top: 20px;
}
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] .form-group {
display: none;
}
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] .clearfix {
display: none;
}
.setup-wizard-slide .missing-image {
display: block;
position: relative;
border-radius: 4px;
border: 1px solid #d1d8dd;
border-radius: 6px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.setup-wizard-slide .missing-image .octicon {
position: relative;
top: 50%;
transform: translate(0px, -50%);
-webkit-transform: translate(0px, -50%);
}
.setup-wizard-slide .attach-image-display {
display: block;
position: relative;
border-radius: 4px;
}
.setup-wizard-slide .img-container {
height: 100%;
width: 100%;
padding: 2px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border: 1px solid #d1d8dd;
border-radius: 6px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.setup-wizard-slide .img-overlay {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
color: #777777;
background-color: rgba(255, 255, 255, 0.7);
opacity: 0;
}
.setup-wizard-slide .img-overlay:hover {
opacity: 1;
cursor: pointer;
}
.page-card-container,
.setup-state {
background-color: #f5f7fa;
}
.page-card-container {
padding: 70px;
}
.page-card {
max-width: 360px;
margin: 70px auto;
padding: 15px;
border: 1px solid #d1d8dd;
border-radius: 4px;
background-color: #fff;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
}
.page-card .page-card-head {
padding: 10px 15px;
margin: -15px;
margin-bottom: 15px;
border-bottom: 1px solid #d1d8dd;
}
.page-card .btn {
margin-top: 30px;
}
.state-icon-container {
display: flex;
justify-content: center;
}
.state-icon {
position: relative;
width: 100px !important;
height: 100px !important;
display: flex;
justify-content: center;
align-items: center;
}
@keyframes lds-rolling {
0% {
-webkit-transform: translate(-50%, -50%) rotate(0deg);
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
-webkit-transform: translate(-50%, -50%) rotate(360deg);
transform: translate(-50%, -50%) rotate(360deg);
}
}
@-webkit-keyframes lds-rolling {
0% {
-webkit-transform: translate(-50%, -50%) rotate(0deg);
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
-webkit-transform: translate(-50%, -50%) rotate(360deg);
transform: translate(-50%, -50%) rotate(360deg);
}
}
.lds-rolling {
-webkit-transform: translate(-100px, -100px) scale(1) translate(100px, 100px);
transform: translate(-100px, -100px) scale(1) translate(100px, 100px);
}
.lds-rolling div {
position: absolute;
width: 60px;
height: 60px;
border: 3px solid #d1d8dd;
border-top-color: transparent;
border-radius: 50%;
-webkit-animation: lds-rolling 1s linear infinite;
animation: lds-rolling 1s linear infinite;
top: 50px;
left: 50px;
}
.lds-rolling div:after {
position: absolute;
width: 60px;
height: 60px;
border: 3px solid #d1d8dd;
border-top-color: transparent;
border-radius: 50%;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}

View file

@ -94,9 +94,6 @@ body[data-route^="Module"] .main-menu .form-sidebar {
position: absolute;
right: 5px;
}
.form-sidebar .attachment-row a.close {
margin-top: -5px;
}
.form-sidebar .form-shared .share-doc-btn,
.form-sidebar .form-viewers .share-doc-btn {
cursor: pointer;

View file

@ -29,7 +29,7 @@ frappe.Application = Class.extend({
this.startup();
},
startup: function() {
frappe.socket.init();
frappe.socketio.init();
frappe.model.init();
if(frappe.boot.status==='failed') {
@ -45,7 +45,6 @@ frappe.Application = Class.extend({
this.make_nav_bar();
this.set_favicon();
this.setup_analytics();
this.setup_beforeunload();
frappe.ui.keys.setup();
this.set_rtl();
@ -481,23 +480,6 @@ frappe.Application = Class.extend({
}
},
setup_beforeunload: function() {
if (frappe.defaults.get_default('in_selenium') || frappe.boot.developer_mode) {
return;
}
window.onbeforeunload = function () {
if (frappe.flags.in_test) return null;
var unsaved_docs = [];
for (const doctype in locals) {
for (const name in locals[doctype]) {
var doc = locals[doctype][name];
if(doc.__unsaved) { unsaved_docs.push(doc.name); }
}
}
return unsaved_docs.length ? true : null;
};
},
show_notes: function() {
var me = this;
if(frappe.boot.notes.length) {

View file

@ -9,10 +9,10 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
});
this.$value = $('<div style="margin-top: 5px;">\
<div class="ellipsis" style="display: inline-block; width: 90%;">\
<i class="fa fa-paper-clip"></i> \
<i class="fa fa-paperclip"></i> \
<a class="attached-file" target="_blank"></a>\
</div>\
<a class="close">&times;</a></div>')
<a class="close" style="position: absolute; right: 15px;">&times;</a></div>')
.prependTo(me.input_area)
.toggle(false);
this.input = this.$input.get(0);
@ -169,7 +169,17 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
if(this.frm) {
return this.value;
} else {
return this.fileobj ? (this.fileobj.filename + "," + this.dataurl) : null;
if ( this.fileobj ) {
if ( this.fileobj.file_url ) {
return this.fileobj.file_url;
} else if ( this.fileobj.filename ) {
var dataURI = this.fileobj.filename + ',' + this.dataurl;
return dataURI;
}
}
return null;
}
},
@ -182,6 +192,7 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
} else {
this.value = this.get_value();
this.refresh();
frappe.hide_progress();
}
},
});

View file

@ -39,9 +39,10 @@ frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({
this.datepicker_options = {
language: lang,
autoClose: true,
todayButton: frappe.datetime.now_date(true),
todayButton: true,
dateFormat: (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'),
startDate: frappe.datetime.now_date(true),
keyboardNav: false,
onSelect: () => {
this.$input.trigger('change');
},
@ -70,6 +71,17 @@ frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({
set_datepicker: function() {
this.$input.datepicker(this.datepicker_options);
this.datepicker = this.$input.data('datepicker');
// today button didn't work as expected,
// so explicitly bind the event
this.datepicker.$datepicker
.find('[data-action="today"]')
.click(() => {
this.datepicker.selectDate(this.get_now_date());
});
},
get_now_date: function() {
return frappe.datetime.now_date(true);
},
set_t_for_today: function() {
var me = this;

View file

@ -5,10 +5,12 @@ frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({
this.date_format = moment.defaultDatetimeFormat;
$.extend(this.datepicker_options, {
timepicker: true,
timeFormat: "hh:ii:ss",
todayButton: frappe.datetime.now_datetime(true)
timeFormat: "hh:ii:ss"
});
},
get_now_date: function() {
return frappe.datetime.now_datetime(true);
},
set_description: function() {
const { description } = this.df;
const { time_zone } = frappe.sys_defaults;

View file

@ -2,13 +2,21 @@ frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({
make: function() {
this._super();
this.disp_area = this.wrapper;
$(document).on('change', () => {
setTimeout(() => this.refresh_input(), 500);
});
},
refresh_input: function() {
var content = this.get_content();
if(content) this.$wrapper.html(content);
},
get_content: function() {
return this.df.options || "";
var content = this.df.options || "";
try {
return frappe.render(content, this);
} catch (e) {
return content;
}
},
html: function(html) {
this.$wrapper.html(html || this.get_content());

View file

@ -27,6 +27,7 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
set_options: function(value) {
// reset options, if something new is set
var options = this.df.options || [];
if(typeof this.df.options==="string") {
options = this.df.options.split("\n");
}

View file

@ -137,17 +137,17 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
});
},
get_image: function (fileobj, callback) {
var freader = new FileReader();
var reader = new FileReader();
freader.onload = function() {
var dataurl = freader.result;
reader.onload = function() {
var dataurl = reader.result;
// add filename to dataurl
var parts = dataurl.split(",");
parts[0] += ";filename=" + fileobj.name;
dataurl = parts[0] + ',' + parts[1];
callback(dataurl);
};
freader.readAsDataURL(fileobj);
reader.readAsDataURL(fileobj);
},
hide_elements_on_mobile: function() {
this.note_editor.find('.note-btn-underline,\

View file

@ -14,9 +14,15 @@ frappe.ui.form.ControlTime = frappe.ui.form.ControlData.extend({
onShow: function() {
$('.datepicker--button:visible').text(__('Now'));
},
todayButton: frappe.datetime.now_time(true)
keyboardNav: false,
todayButton: true
});
this.datepicker = this.$input.data('datepicker');
this.datepicker.$datepicker
.find('[data-action="today"]')
.click(() => {
this.datepicker.selectDate(frappe.datetime.now_time(true));
});
this.refresh();
},
set_input: function(value) {

View file

@ -230,7 +230,7 @@ frappe.ui.form.Dashboard = Class.extend({
} else {
return false;
}
} else {
} else if(this.data.fieldname) {
frappe.route_options = this.get_document_filter(doctype);
if(show_open) {
frappe.ui.notifications.show_open_count_list(doctype);
@ -250,7 +250,7 @@ frappe.ui.form.Dashboard = Class.extend({
return filter;
},
set_open_count: function() {
if(!this.data.transactions) {
if(!this.data.transactions || !this.data.fieldname) {
return;
}
@ -334,22 +334,12 @@ frappe.ui.form.Dashboard = Class.extend({
// heatmap
render_heatmap: function() {
if(!this.heatmap) {
this.heatmap = new CalHeatMap();
this.heatmap.init({
itemSelector: "#heatmap-" + frappe.model.scrub(this.frm.doctype),
domain: "month",
subDomain: "day",
start: moment().subtract(1, 'year').add(1, 'month').toDate(),
cellSize: 9,
cellPadding: 2,
domainGutter: 2,
range: 12,
domainLabelFormat: function(date) {
return moment(date).format("MMM").toUpperCase();
},
displayLegend: false,
legend: [5, 10, 15, 20]
// subDomainTextFormat: "%d",
this.heatmap = new frappe.ui.HeatMap({
parent: this.heatmap_area.find("#heatmap-" + frappe.model.scrub(this.frm.doctype)),
height: 100,
start: new Date(moment().subtract(1, 'year').toDate()),
count_label: "items",
discrete_domains: 0
});
// center the heatmap
@ -388,16 +378,14 @@ frappe.ui.form.Dashboard = Class.extend({
return indicator;
},
//graphs
// graphs
setup_graph: function() {
var me = this;
var method = this.data.graph_method;
var args = {
doctype: this.frm.doctype,
docname: this.frm.doc.name,
};
$.extend(args, this.data.graph_method_args);
frappe.call({
@ -421,29 +409,9 @@ frappe.ui.form.Dashboard = Class.extend({
mode: 'line',
height: 140
});
new frappe.ui.Graph(args);
},
setup_chart: function(opts) {
var me = this;
this.graph_area.removeClass('hidden');
$.extend(opts, {
wrapper: me.graph_area,
padding: {
right: 30,
bottom: 30
}
});
this.chart = new frappe.ui.Chart(opts);
if(this.chart) {
this.show();
this.chart.set_chart_size(me.wrapper.width() - 60);
}
},
show: function() {
this.section.removeClass('hidden');
}

View file

@ -657,7 +657,7 @@ frappe.ui.form.Timeline = Class.extend({
var valid_users = Object.keys(frappe.boot.user_info)
.filter(user => !["Administrator", "Guest"].includes(user));
return valid_users.map(user => frappe.boot.user_info[user].username);
return valid_users.map(user => frappe.boot.user_info[user].username || frappe.boot.user_info[user].name);
},
setup_comment_like: function() {

View file

@ -48,7 +48,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({
return false;
}
if (this.too_many_mandatory_fields() || this.has_child_table()) {
if (this.too_many_mandatory_fields() || this.has_child_table()
|| !this.mandatory.length) {
return false;
}

View file

@ -2,7 +2,7 @@
<div class="progress-area hidden form-dashboard-section">
</div>
<div class="form-heatmap hidden form-dashboard-section">
<div id="heatmap-{{ frappe.model.scrub(frm.doctype) }}"></div>
<div id="heatmap-{{ frappe.model.scrub(frm.doctype) }}" class="heatmap"></div>
<div class="text-muted small heatmap-message hidden"></div>
</div>
<div class="form-graph form-dashboard-section hidden"></div>

View file

@ -11,7 +11,7 @@
{%= __("Delete") %}</button>
<button type="reset"
class="grid-add-multiple-rows btn btn-xs btn-default hide"
style="margin-right: 10px;">
style="margin-right: 4px;">
{%= __("Add Multiple") %}</a>
<!-- hack to allow firefox include this in tabs -->
<button type="reset" class="btn btn-xs btn-default grid-add-row">

View file

@ -32,6 +32,8 @@
data-filter="{%= col.fieldname %},=,{%= value %}">
{% if(formatters && formatters[col.fieldname]) { %}
{{ formatters[col.fieldname](value, col.df, data) }}
{% } else if(col.fieldtype == "Code") { %}
{{ value }}
{% } else { %}
{{ frappe.format(value, col.df, null, data) }}
{% } %}

View file

@ -47,6 +47,9 @@ frappe.views.ListRenderer = Class.extend({
},
init_settings: function () {
this.settings = frappe.listview_settings[this.doctype] || {};
if(!("selectable" in this.settings)) {
this.settings.selectable = true;
}
this.init_user_settings();
this.order_by = this.user_settings.order_by || this.settings.order_by;
@ -231,7 +234,10 @@ frappe.views.ListRenderer = Class.extend({
this.columns = this.columns.uniqBy(col => col.title);
// Remove TextEditor field columns
this.columns = this.columns.filter(col => col.fieldtype !== 'Text Editor')
this.columns = this.columns.filter(col => col.fieldtype !== 'Text Editor');
// Remove color field
this.columns = this.columns.filter(col => col.fieldtype !== 'Color');
// Limit number of columns to 4
this.columns = this.columns.slice(0, 4);

View file

@ -620,7 +620,9 @@ frappe.views.ListView = frappe.ui.BaseList.extend({
}
this.make_bulk_assignment();
this.make_bulk_printing();
if(frappe.model.can_print(this.doctype)) {
this.make_bulk_printing();
}
// add to desktop
this.page.add_menu_item(__('Add to Desktop'), function () {
@ -777,7 +779,7 @@ frappe.views.ListView = frappe.ui.BaseList.extend({
setup_delete: function () {
var me = this;
if (!(this.can_delete || this.list_renderer.settings.selectable)) {
if (!this.can_delete) {
return;
}
this.$page.on('change', '.list-row-checkbox, .list-select-all', function() {

View file

@ -24,9 +24,9 @@ frappe.help.show_video = function(youtube_id, title) {
var dialog = frappe.msgprint('<iframe width="'+size[0]+'" height="'+size[1]+'" \
src="https://www.youtube.com/embed/'+ youtube_id +'" \
frameborder="0" allowfullscreen></iframe>' + (frappe.help_feedback_link || ""),
title || __("Help"));
title || __("Help"));
dialog.$wrapper.find(".modal-content").addClass("video-modal");
dialog.$wrapper.addClass("video-modal");
}
$("body").on("click", "a.help-link", function() {

View file

@ -22,6 +22,16 @@ frappe.click_link = function(text, idx) {
return frappe.timeout(0.5);
};
frappe.click_element = function(selector, idx) {
// Selector by class name like $(`.cart-items`)
let element = $(`${selector}`);
if(!element.length) {
throw `did not find any link containing ${selector}`;
}
element.get(idx || 0).click();
return frappe.timeout(0.5);
};
frappe.set_control= function(fieldname, value) {
let control = $(`.form-control[data-fieldname="${fieldname}"]:visible`);
if(!control.length) {

View file

@ -289,6 +289,10 @@ $.extend(frappe.model, {
} else if (!opts.source_name && opts.frm) {
opts.source_name = opts.frm.doc.name;
// Allow opening a mapped doc without a source document name
} else if (!opts.frm) {
opts.source_name = null;
}
return frappe.call({
@ -297,7 +301,7 @@ $.extend(frappe.model, {
args: {
method: opts.method,
source_name: opts.source_name,
selected_children: opts.frm.get_selected()
selected_children: opts.frm ? opts.frm.get_selected() : null
},
freeze: true,
callback: function(r) {

View file

@ -183,7 +183,7 @@ $.extend(frappe.model, {
},
scrub: function(txt) {
return txt.replace(/ /g, "_").toLowerCase();
return txt.replace(/ /g, "_").toLowerCase(); // use to slugify or create a slug, a "code-friendly" string
},
unscrub: function(txt) {

View file

@ -1,3 +1,5 @@
frappe.provide('frappe.utils');
function get_url_arg(name) {
return get_query_params()[name] || "";
}
@ -36,8 +38,29 @@ function get_query_params(query_string) {
return query_params;
}
function make_query_string(obj) {
var query_params = [];
$.each(obj, function(k, v) { query_params.push(encodeURIComponent(k) + "=" + encodeURIComponent(v)); });
return "?" + query_params.join("&");
function make_query_string(obj, encode=true) {
let query_params = [];
for (let key in obj) {
let value = obj[key];
if (value === undefined || value === '' || value === null) {
continue;
}
if (typeof value === 'object') {
value = JSON.stringify(value);
}
if (encode) {
key = encodeURIComponent(key);
value = encodeURIComponent(value);
}
query_params.push(`${key}=${value}`);
}
return '?' + query_params.join('&');
}
Object.assign(frappe.utils, {
get_url_arg,
get_query_params,
make_query_string
});

View file

@ -34,7 +34,7 @@ frappe.call = function(opts) {
var callback = function(data, response_text) {
if(data.task_id) {
// async call, subscribe
frappe.socket.subscribe(data.task_id, opts);
frappe.socketio.subscribe(data.task_id, opts);
if(opts.queued) {
opts.queued(data);

View file

@ -1,4 +1,4 @@
frappe.socket = {
frappe.socketio = {
open_tasks: {},
open_docs: [],
emit_queue: [],
@ -7,40 +7,40 @@ frappe.socket = {
return;
}
if (frappe.socket.socket) {
if (frappe.socketio.socket) {
return;
}
if (frappe.boot.developer_mode) {
// File watchers for development
frappe.socket.setup_file_watchers();
frappe.socketio.setup_file_watchers();
}
//Enable secure option when using HTTPS
if (window.location.protocol == "https:") {
frappe.socket.socket = io.connect(frappe.socket.get_host(), {secure: true});
frappe.socketio.socket = io.connect(frappe.socketio.get_host(), {secure: true});
}
else if (window.location.protocol == "http:") {
frappe.socket.socket = io.connect(frappe.socket.get_host());
frappe.socketio.socket = io.connect(frappe.socketio.get_host());
}
else if (window.location.protocol == "file:") {
frappe.socket.socket = io.connect(window.localStorage.server);
frappe.socketio.socket = io.connect(window.localStorage.server);
}
if (!frappe.socket.socket) {
console.log("Unable to connect to " + frappe.socket.get_host());
if (!frappe.socketio.socket) {
console.log("Unable to connect to " + frappe.socketio.get_host());
return;
}
frappe.socket.socket.on('msgprint', function(message) {
frappe.socketio.socket.on('msgprint', function(message) {
frappe.msgprint(message);
});
frappe.socket.socket.on('eval_js', function(message) {
frappe.socketio.socket.on('eval_js', function(message) {
eval(message);
});
frappe.socket.socket.on('progress', function(data) {
frappe.socketio.socket.on('progress', function(data) {
if(data.progress) {
data.percent = flt(data.progress[0]) / data.progress[1] * 100;
}
@ -53,23 +53,24 @@ frappe.socket = {
}
});
frappe.socket.setup_listeners();
frappe.socket.setup_reconnect();
frappe.socketio.setup_listeners();
frappe.socketio.setup_reconnect();
frappe.socketio.uploader = new frappe.socketio.SocketIOUploader();
$(document).on('form-load form-rename', function(e, frm) {
if (frm.is_new()) {
return;
}
for (var i=0, l=frappe.socket.open_docs.length; i<l; i++) {
var d = frappe.socket.open_docs[i];
for (var i=0, l=frappe.socketio.open_docs.length; i<l; i++) {
var d = frappe.socketio.open_docs[i];
if (frm.doctype==d.doctype && frm.docname==d.name) {
// already subscribed
return false;
}
}
frappe.socket.doc_subscribe(frm.doctype, frm.docname);
frappe.socketio.doc_subscribe(frm.doctype, frm.docname);
});
$(document).on("form_refresh", function(e, frm) {
@ -77,7 +78,7 @@ frappe.socket = {
return;
}
frappe.socket.doc_open(frm.doctype, frm.docname);
frappe.socketio.doc_open(frm.doctype, frm.docname);
});
$(document).on('form-unload', function(e, frm) {
@ -85,8 +86,8 @@ frappe.socket = {
return;
}
// frappe.socket.doc_unsubscribe(frm.doctype, frm.docname);
frappe.socket.doc_close(frm.doctype, frm.docname);
// frappe.socketio.doc_unsubscribe(frm.doctype, frm.docname);
frappe.socketio.doc_close(frm.doctype, frm.docname);
});
window.onbeforeunload = function() {
@ -96,7 +97,7 @@ frappe.socket = {
// if tab/window is closed, notify other users
if (cur_frm.doc) {
frappe.socket.doc_close(cur_frm.doctype, cur_frm.docname);
frappe.socketio.doc_close(cur_frm.doctype, cur_frm.docname);
}
}
},
@ -115,16 +116,16 @@ frappe.socket = {
subscribe: function(task_id, opts) {
// TODO DEPRECATE
frappe.socket.socket.emit('task_subscribe', task_id);
frappe.socket.socket.emit('progress_subscribe', task_id);
frappe.socketio.socket.emit('task_subscribe', task_id);
frappe.socketio.socket.emit('progress_subscribe', task_id);
frappe.socket.open_tasks[task_id] = opts;
frappe.socketio.open_tasks[task_id] = opts;
},
task_subscribe: function(task_id) {
frappe.socket.socket.emit('task_subscribe', task_id);
frappe.socketio.socket.emit('task_subscribe', task_id);
},
task_unsubscribe: function(task_id) {
frappe.socket.socket.emit('task_unsubscribe', task_id);
frappe.socketio.socket.emit('task_unsubscribe', task_id);
},
doc_subscribe: function(doctype, docname) {
if (frappe.flags.doc_subscribe) {
@ -137,12 +138,12 @@ frappe.socket = {
// throttle to 1 per sec
setTimeout(function() { frappe.flags.doc_subscribe = false }, 1000);
frappe.socket.socket.emit('doc_subscribe', doctype, docname);
frappe.socket.open_docs.push({doctype: doctype, docname: docname});
frappe.socketio.socket.emit('doc_subscribe', doctype, docname);
frappe.socketio.open_docs.push({doctype: doctype, docname: docname});
},
doc_unsubscribe: function(doctype, docname) {
frappe.socket.socket.emit('doc_unsubscribe', doctype, docname);
frappe.socket.open_docs = $.filter(frappe.socket.open_docs, function(d) {
frappe.socketio.socket.emit('doc_unsubscribe', doctype, docname);
frappe.socketio.open_docs = $.filter(frappe.socketio.open_docs, function(d) {
if(d.doctype===doctype && d.name===docname) {
return null;
} else {
@ -152,44 +153,44 @@ frappe.socket = {
},
doc_open: function(doctype, docname) {
// notify that the user has opened this doc, if not already notified
if(!frappe.socket.last_doc
|| (frappe.socket.last_doc[0]!=doctype && frappe.socket.last_doc[0]!=docname)) {
frappe.socket.socket.emit('doc_open', doctype, docname);
if(!frappe.socketio.last_doc
|| (frappe.socketio.last_doc[0]!=doctype && frappe.socketio.last_doc[0]!=docname)) {
frappe.socketio.socket.emit('doc_open', doctype, docname);
}
frappe.socket.last_doc = [doctype, docname];
frappe.socketio.last_doc = [doctype, docname];
},
doc_close: function(doctype, docname) {
// notify that the user has closed this doc
frappe.socket.socket.emit('doc_close', doctype, docname);
frappe.socketio.socket.emit('doc_close', doctype, docname);
},
setup_listeners: function() {
frappe.socket.socket.on('task_status_change', function(data) {
frappe.socket.process_response(data, data.status.toLowerCase());
frappe.socketio.socket.on('task_status_change', function(data) {
frappe.socketio.process_response(data, data.status.toLowerCase());
});
frappe.socket.socket.on('task_progress', function(data) {
frappe.socket.process_response(data, "progress");
frappe.socketio.socket.on('task_progress', function(data) {
frappe.socketio.process_response(data, "progress");
});
},
setup_reconnect: function() {
// subscribe again to open_tasks
frappe.socket.socket.on("connect", function() {
frappe.socketio.socket.on("connect", function() {
// wait for 5 seconds before subscribing again
// because it takes more time to start python server than nodejs server
// and we use validation requests to python server for subscribing
setTimeout(function() {
$.each(frappe.socket.open_tasks, function(task_id, opts) {
frappe.socket.subscribe(task_id, opts);
$.each(frappe.socketio.open_tasks, function(task_id, opts) {
frappe.socketio.subscribe(task_id, opts);
});
// re-connect open docs
$.each(frappe.socket.open_docs, function(d) {
$.each(frappe.socketio.open_docs, function(d) {
if(locals[d.doctype] && locals[d.doctype][d.name]) {
frappe.socket.doc_subscribe(d.doctype, d.name);
frappe.socketio.doc_subscribe(d.doctype, d.name);
}
});
if (cur_frm && cur_frm.doc) {
frappe.socket.doc_open(cur_frm.doc.doctype, cur_frm.doc.name);
frappe.socketio.doc_open(cur_frm.doc.doctype, cur_frm.doc.name);
}
}, 5000);
});
@ -208,9 +209,9 @@ frappe.socket = {
}
host = host + ':' + port;
frappe.socket.file_watcher = io.connect(host);
frappe.socketio.file_watcher = io.connect(host);
// css files auto reload
frappe.socket.file_watcher.on('reload_css', function(filename) {
frappe.socketio.file_watcher.on('reload_css', function(filename) {
let abs_file_path = "assets/" + filename;
const link = $(`link[href*="${abs_file_path}"]`);
abs_file_path = abs_file_path.split('?')[0] + '?v='+ moment();
@ -221,7 +222,7 @@ frappe.socket = {
}, 5);
});
// js files show alert
frappe.socket.file_watcher.on('reload_js', function(filename) {
frappe.socketio.file_watcher.on('reload_js', function(filename) {
filename = "assets/" + filename;
var msg = $(`
<span>${filename} changed <a data-action="reload">Click to Reload</a></span>
@ -239,7 +240,7 @@ frappe.socket = {
}
// success
var opts = frappe.socket.open_tasks[data.task_id];
var opts = frappe.socketio.open_tasks[data.task_id];
if(opts[method]) {
opts[method](data);
}
@ -264,15 +265,117 @@ frappe.socket = {
frappe.provide("frappe.realtime");
frappe.realtime.on = function(event, callback) {
frappe.socket.socket && frappe.socket.socket.on(event, callback);
frappe.socketio.socket && frappe.socketio.socket.on(event, callback);
};
frappe.realtime.off = function(event, callback) {
frappe.socket.socket && frappe.socket.socket.off(event, callback);
frappe.socketio.socket && frappe.socketio.socket.off(event, callback);
}
frappe.realtime.publish = function(event, message) {
if(frappe.socket.socket) {
frappe.socket.socket.emit(event, message);
if(frappe.socketio.socket) {
frappe.socketio.socket.emit(event, message);
}
}
frappe.socketio.SocketIOUploader = class SocketIOUploader {
constructor() {
frappe.socketio.socket.on('upload-request-slice', (data) => {
var place = data.currentSlice * this.chunk_size,
slice = this.file.slice(place,
place + Math.min(this.chunk_size, this.file.size - place));
if (this.on_progress) {
// update progress
this.on_progress(place / this.file.size * 100);
}
this.reader.readAsArrayBuffer(slice);
this.keep_alive();
});
frappe.socketio.socket.on('upload-end', (data) => {
if (data.file_url.substr(0, 7)==='/public') {
data.file_url = data.file_url.substr(7);
}
this.callback(data);
this.reader = null;
this.file = null;
});
frappe.socketio.socket.on('upload-error', (data) => {
this.disconnect(false);
frappe.msgprint({
title: __('Upload Failed'),
message: data.error,
indicator: 'red'
});
});
frappe.socketio.socket.on('disconnect', () => {
this.disconnect();
});
}
start({file=null, is_private=0, filename='', callback=null, on_progress=null,
chunk_size=100000, fallback=null} = {}) {
if (this.reader) {
frappe.throw(__('File Upload in Progress. Please try again in a few moments.'));
}
if (!frappe.socketio.socket.connected) {
if (fallback) {
fallback();
return;
} else {
frappe.throw(__('Socketio is not connected. Cannot upload'));
}
}
this.reader = new FileReader();
this.file = file;
this.chunk_size = chunk_size;
this.callback = callback;
this.on_progress = on_progress;
this.reader.onload = () => {
frappe.socketio.socket.emit('upload-accept-slice', {
is_private: is_private,
name: filename,
type: this.file.type,
size: this.file.size,
data: this.reader.result
});
this.keep_alive();
};
var slice = file.slice(0, this.chunk_size);
this.reader.readAsArrayBuffer(slice);
}
keep_alive() {
if (this.next_check) {
clearTimeout (this.next_check);
}
this.next_check = setTimeout (() => {
this.disconnect();
}, 3000);
}
disconnect(with_message = true) {
if (this.reader) {
this.reader = null;
this.file = null;
frappe.hide_progress();
if (with_message) {
frappe.msgprint({
title: __('File Upload'),
message: __('File Upload Disconnected. Please try again.'),
indicator: 'red'
});
}
}
}
}

View file

@ -2,11 +2,15 @@
// MIT License. See license.txt
frappe.provide("frappe.ui")
frappe.provide("frappe.ui");
frappe.ui.app_icon = {
get_html: function(module, small) {
var icon = module.icon;
var color = module.color;
if (icon
&& icon.match(/([\uE000-\uF8FF]|\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDDFF])/g)) {
module.emoji = module.icon;
}
var icon_style = "";
if(module.reverse) {
icon_style = "color: #36414C;";
@ -17,10 +21,10 @@ frappe.ui.app_icon = {
}
// first letter
if(!icon) {
if(!icon || module.emoji) {
icon = '<span class="inner" ' +
(module.reverse ? ('style="' + icon_style + '"') : '')
+ '>' + module._label[0].toUpperCase() + '</span>';
+ '>' + (module.emoji || module._label[0].toUpperCase()) + '</span>';
} else if(icon.split(".").slice(-1)[0]==="svg") {
$.ajax({
url: frappe.urllib.get_full_url(icon),
@ -29,13 +33,13 @@ frappe.ui.app_icon = {
success: function(data) {
icon = data;
}
})
});
icon = '<object>'+ icon+'</object>';
} else {
icon = '<i class="'+ icon+'" title="' + module._label + '" style="'+ icon_style + '"></i>';
}
return '<div class="app-icon'+ (small ? " app-icon-small" : "")
+'" style="background-color: '+ color +'" title="'+ module._label +'">'+icon+'</div>'
+'" style="background-color: '+ color +'" title="'+ module._label +'">'+icon+'</div>';
}
}
};

View file

@ -67,15 +67,6 @@ frappe.ui.Dialog = frappe.ui.FieldGroup.extend({
});
},
focus_on_first_input: function() {
if(this.no_focus) return;
$.each(this.fields_list, function(i, f) {
if(!in_list(['Date', 'Datetime', 'Time'], f.df.fieldtype) && f.set_focus) {
f.set_focus();
return false;
}
});
},
get_primary_btn: function() {
return this.$wrapper.find(".modal-header .btn-primary");
},

View file

@ -60,6 +60,15 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({
});
},
first_button: false,
focus_on_first_input: function() {
if(this.no_focus) return;
$.each(this.fields_list, function(i, f) {
if(!in_list(['Date', 'Datetime', 'Time'], f.df.fieldtype) && f.set_focus) {
f.set_focus();
return false;
}
});
},
catch_enter_as_submit: function() {
var me = this;
$(this.body).find('input[type="text"], input[type="password"]').keypress(function(e) {

View file

@ -133,7 +133,9 @@ frappe.ui.FilterList = Class.extend({
for(var i in this.filters) {
if(this.filters[i].field) {
var f = this.filters[i].get_value();
if(f[0]==doctype && f[1]==fieldname && f[2]==condition && f[3]==value) {
var val = this.get_formatted_value(this.filters[i].field, f[3]);
if(f[0]==doctype && f[1]==fieldname && f[2]==condition && val==value) {
flag = true;
} else if($.isArray(value) && frappe.utils.arrays_equal(value, f[3])) {
flag = true;
@ -174,6 +176,19 @@ frappe.ui.FilterList = Class.extend({
if(this.filters[i].field && this.filters[i].field.df.fieldname==fieldname)
return this.filters[i];
}
},
get_formatted_value: function(field, val){
var value = val;
if(field.df.fieldname==="docstatus") {
value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value;
} else if(field.df.original_type==="Check") {
value = {0:"No", 1:"Yes"}[cint(value)];
}
value = frappe.format(value, field.df, {only_value: 1});
return value;
}
});
@ -214,6 +229,7 @@ frappe.ui.Filter = Class.extend({
this.wrapper.find(".set-filter-and-run").on("click", function() {
me.wrapper.removeClass("is-new-filter");
me.flist.base_list.run();
me.apply();
});
// add help for "in" codition
@ -244,6 +260,14 @@ frappe.ui.Filter = Class.extend({
}
},
apply: function() {
var f = this.get_value();
this.flist.filters.pop();
var val = this.flist.get_formatted_value(this.field, f[3]);
this.flist.push_new_filter(f[0], f[1], f[2], val);
this.wrapper.remove();
},
remove: function(dont_run) {
this.wrapper.remove();
this.$btn_group && this.$btn_group.remove();
@ -467,14 +491,7 @@ frappe.ui.Filter = Class.extend({
set_filter_button_text: function() {
var value = this.get_selected_value();
if(this.field.df.fieldname==="docstatus") {
value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value;
} else if(this.field.df.original_type==="Check") {
value = {0:"No", 1:"Yes"}[cint(value)];
}
value = frappe.format(value, this.field.df, {only_value: 1});
value = this.flist.get_formatted_value(this.field, value);
// for translations
// __("like"), __("not like"), __("in")

View file

@ -28,8 +28,7 @@ frappe.ui.Graph = class Graph {
specific_values = [],
summary = [],
color = 'blue',
mode = '',
mode = ''
}) {
if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) {
@ -43,34 +42,36 @@ frappe.ui.Graph = class Graph {
}
this.parent = parent;
this.base_height = height;
this.height = height - 40;
this.translate_x = 60;
this.translate_y = 10;
this.set_margins(height);
this.title = title;
this.subtitle = subtitle;
// Begin axis graph-related args
this.y = y;
this.x = x;
this.specific_values = specific_values;
this.summary = summary;
this.color = color;
this.mode = mode;
// this.current_hover_index = 0;
// this.current_selected_index = 0;
this.$graph = null;
// Validate all arguments
// Validate all arguments, check passed data format, set defaults
frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this));
}
setup() {
this.bind_window_event();
this.refresh();
frappe.require("assets/frappe/js/lib/snap.svg-min.js", () => {
this.bind_window_event();
this.refresh();
});
}
bind_window_event() {
@ -81,18 +82,16 @@ frappe.ui.Graph = class Graph {
refresh() {
this.base_width = this.parent.width() - 20;
this.width = this.base_width - 100;
this.setup_base_values();
this.set_width();
this.width = this.base_width - this.translate_x * 2;
this.setup_container();
this.setup_components();
this.setup_values();
this.setup_utils();
this.setup_components();
this.make_graph_components();
this.make_tooltip();
if(this.summary.length > 0) {
@ -102,6 +101,20 @@ frappe.ui.Graph = class Graph {
}
}
set_margins(height) {
this.base_height = height;
this.height = height - 40;
this.translate_x = 60;
this.translate_y = 10;
}
set_width() {
this.base_width = this.parent.width();
}
setup_base_values() {}
setup_container() {
// Graph needs a dedicated parent element
this.parent.empty();
@ -110,11 +123,11 @@ frappe.ui.Graph = class Graph {
.addClass('graph-container')
.append($(`<h6 class="title" style="margin-top: 15px;">${this.title}</h6>`))
.append($(`<h6 class="sub-title uppercase">${this.subtitle}</h6>`))
.append($(`<div class="graph-graphics"></div>`))
.append($(`<div class="graphics"></div>`))
.append($(`<div class="graph-stats-container"></div>`))
.appendTo(this.parent);
this.$graphics = this.container.find('.graph-graphics');
this.$graphics = this.container.find('.graphics');
this.$stats_container = this.container.find('.graph-stats-container');
this.$graph = $('<div>')
@ -130,6 +143,13 @@ frappe.ui.Graph = class Graph {
return this.$svg;
}
setup_components() {
this.y_axis_group = this.snap.g().attr({ class: "y axis" });
this.x_axis_group = this.snap.g().attr({ class: "x axis" });
this.data_units = this.snap.g().attr({ class: "data-points" });
this.specific_y_lines = this.snap.g().attr({ class: "specific axis" });
}
setup_values() {
// Multiplier
let all_values = this.specific_values.map(d => d.value);
@ -170,20 +190,15 @@ frappe.ui.Graph = class Graph {
});
}
setup_components() {
this.y_axis_group = this.snap.g().attr({ class: "y axis" });
this.x_axis_group = this.snap.g().attr({ class: "x axis" });
this.data_units = this.snap.g().attr({ class: "graph-data-points" });
this.specific_y_lines = this.snap.g().attr({ class: "specific axis" });
}
make_graph_components() {
this.make_y_axis();
this.make_x_axis();
this.y_colors = ['lightblue', 'purple', 'blue', 'green', 'lightgreen',
'yellow', 'orange', 'red']
this.y.map((d, i) => {
this.make_units(d.y_tops, d.color, i);
this.make_path(d);
this.make_units(d.y_tops, d.color || this.y_colors[i], i);
this.make_path(d, d.color || this.y_colors[i]);
});
if(this.specific_values.length > 0) {
@ -246,6 +261,11 @@ frappe.ui.Graph = class Graph {
transform: `translate(0,${start_at})`
});
this.x.values.map((point, i) => {
let allowed_space = this.avg_unit_width * 1.5;
if(this.get_strwidth(point) > allowed_space) {
let allowed_letters = allowed_space / 8;
point = point.slice(0, allowed_letters-3) + " ...";
}
this.x_axis_group.add(this.snap.g(
this.snap.line(0, 0, 0, height),
this.snap.text(0, text_start_at, point).attr({
@ -262,8 +282,13 @@ frappe.ui.Graph = class Graph {
make_units(y_values, color, dataset_index) {
let d = this.unit_args;
y_values.map((y, i) => {
let data_unit = this.draw[d.type](this.x_axis_values[i],
y, d.args, color, dataset_index);
let data_unit = this.draw[d.type](
this.x_axis_values[i],
y,
d.args,
color,
dataset_index
);
this.data_units.add(data_unit);
this.y[dataset_index].data_units.push(data_unit);
});
@ -272,75 +297,58 @@ frappe.ui.Graph = class Graph {
make_path() { }
make_tooltip() {
this.tip = $(`<div class="graph-svg-tip comparison">
<span class="title"></span>
<ul class="data-point-list">
</ul>
</div>`).attr({
style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;`
}).appendTo(this.$graphics);
this.tip_title = this.tip.find('.title');
this.tip_data_point_list = this.tip.find('.data-point-list');
// should be w.r.t. this.parent
this.tip = new frappe.ui.SvgTip({
parent: this.$graphics,
});
this.bind_tooltip();
}
bind_tooltip() {
// should be w.r.t. this.parent, but will have to take care of
// all the elements and padding, margins on top
this.$graphics.on('mousemove', (e) => {
let offset = $(this.$graphics).offset();
let offset = this.$graphics.offset();
var relX = e.pageX - offset.left - this.translate_x;
var relY = e.pageY - offset.top - this.translate_y;
if(relY < this.height) {
for(var i=this.x_axis_values.length - 1; i >= 0 ; i--) {
let x_val = this.x_axis_values[i];
if(relX > x_val - this.avg_unit_width/2) {
let x = x_val - this.tip.width()/2 + this.translate_x;
let y = this.y_min_tops[i] - this.tip.height() + this.translate_y;
this.fill_tooltip(i);
this.tip.attr({
style: `top: ${y}px; left: ${x-0.5}px; opacity: 1; pointer-events: none;`
});
break;
}
}
if(relY < this.height + this.translate_y * 2) {
this.map_tooltip_x_position_and_show(relX);
} else {
this.tip.attr({
style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;`
});
this.tip.hide_tip()
}
});
this.$graphics.on('mouseleave', () => {
this.tip.attr({
style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;`
});
});
}
fill_tooltip(i) {
this.tip_title.html(this.x.formatted && this.x.formatted.length>0
? this.x.formatted[i] : this.x.values[i]);
this.tip_data_point_list.empty();
this.y.map(y_set => {
let $li = $(`<li>
<strong style="display: block;">
${y_set.formatted ? y_set.formatted[i] : y_set.values[i]}
</strong>
${y_set.title ? y_set.title : '' }
</li>`).addClass(`border-top ${y_set.color}`);
this.tip_data_point_list.append($li);
});
map_tooltip_x_position_and_show(relX) {
for(var i=this.x_axis_values.length - 1; i >= 0 ; i--) {
let x_val = this.x_axis_values[i];
// let delta = i === 0 ? this.avg_unit_width : x_val - this.x_axis_values[i-1];
if(relX > x_val - this.avg_unit_width/2) {
let x = x_val + this.translate_x - 0.5;
let y = this.y_min_tops[i] + this.translate_y;
let title = this.x.formatted && this.x.formatted.length>0
? this.x.formatted[i] : this.x.values[i];
let values = this.y.map((set, j) => {
return {
title: set.title,
value: set.formatted ? set.formatted[i] : set.values[i],
color: set.color || this.y_colors[j],
}
});
this.tip.set_values(x, y, title, '', values);
this.tip.show_tip();
break;
}
}
}
show_specific_values() {
this.specific_values.map(d => {
this.specific_y_lines.add(this.snap.g(
this.snap.line(0, 0, this.width, 0).attr({
class: d.line_type === "dashed" ? "graph-dashed": ""
class: d.line_type === "dashed" ? "dashed": ""
}),
this.snap.text(this.width + 5, 0, d.name.toUpperCase()).attr({
dy: ".32em",
@ -434,6 +442,9 @@ frappe.ui.Graph = class Graph {
let width = total_width / args.no_of_datasets;
let current_x = start_x + width * index;
if(y == this.height) {
y = this.height * 0.98;
}
return this.snap.rect(current_x, y, width, this.height - y).attr({
class: `bar mini fill ${color}`
});
@ -442,6 +453,11 @@ frappe.ui.Graph = class Graph {
return this.snap.circle(x, y, args.radius).attr({
class: `fill ${color}`
});
},
'rect': (x, y, args, color) => {
return this.snap.rect(x, y, args.width, args.height).attr({
class: `fill ${color}`
});
}
};
@ -459,6 +475,7 @@ frappe.ui.Graph = class Graph {
frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph {
constructor(args = {}) {
super(args);
this.setup();
}
setup_values() {
@ -470,8 +487,8 @@ frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph {
this.unit_args = {
type: 'bar',
args: {
space_width: this.y.length > 1 ?
me.avg_unit_width/2 : me.avg_unit_width/8,
// More intelligent width setting
space_width:me.avg_unit_width/2,
no_of_datasets: this.y.length
}
};
@ -486,6 +503,7 @@ frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph {
frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph {
constructor(args = {}) {
super(args);
this.setup();
}
setup_values() {
@ -498,10 +516,10 @@ frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph {
};
}
make_path(d) {
make_path(d, color) {
let points_list = d.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y));
let path_str = "M"+points_list.join("L");
d.path = this.snap.path(path_str).attr({class: `stroke ${d.color}`});
d.path = this.snap.path(path_str).attr({class: `stroke ${color}`});
this.data_units.prepend(d.path);
}
};
@ -509,10 +527,13 @@ frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph {
frappe.ui.PercentageGraph = class PercentageGraph extends frappe.ui.Graph {
constructor(args = {}) {
super(args);
this.setup();
}
make_graph_area() {
this.$graphics.addClass('graph-focus-margin');
this.$graphics.addClass('graph-focus-margin').attr({
style: `margin-top: 45px;`
});
this.$stats_container.addClass('graph-focus-margin').attr({
style: `padding-top: 0px; margin-bottom: 30px;`
});
@ -533,37 +554,536 @@ frappe.ui.PercentageGraph = class PercentageGraph extends frappe.ui.Graph {
return total;
});
// Calculate x unit distances for tooltips
if(!this.x.colors) {
this.x.colors = ['green', 'blue', 'purple', 'red', 'orange',
'yellow', 'lightblue', 'lightgreen'];
}
}
setup_utils() { }
setup_components() {
this.$percentage_bar = $(`<div class="progress">
</div>`).appendTo(this.$chart);
</div>`).appendTo(this.$chart); // get this.height, width and avg from this if needed
}
make_graph_components() {
let grand_total = this.x.totals.reduce((a, b) => a + b, 0);
this.grand_total = this.x.totals.reduce((a, b) => a + b, 0);
this.x.units = [];
this.x.totals.map((total, i) => {
let $part = $(`<div class="progress-bar background ${this.x.colors[i]}"
style="width: ${total*100/grand_total}%"></div>`);
style="width: ${total*100/this.grand_total}%"></div>`);
this.x.units.push($part);
this.$percentage_bar.append($part);
});
}
make_tooltip() { }
bind_tooltip() {
this.x.units.map(($part, i) => {
$part.on('mouseenter', () => {
let g_off = this.$graphics.offset(), p_off = $part.offset();
let x = p_off.left - g_off.left + $part.width()/2;
let y = p_off.top - g_off.top - 6;
let title = (this.x.formatted && this.x.formatted.length>0
? this.x.formatted[i] : this.x.values[i]) + ': ';
let percent = (this.x.totals[i]*100/this.grand_total).toFixed(1);
this.tip.set_values(x, y, title, percent);
this.tip.show_tip();
});
});
}
show_summary() {
let values = this.x.formatted.length > 0 ? this.x.formatted : this.x.values;
let x_values = this.x.formatted && this.x.formatted.length > 0
? this.x.formatted : this.x.values;
this.x.totals.map((d, i) => {
this.$stats_container.append($(`<div class="stats">
<span class="indicator ${this.x.colors[i]}">
<span class="text-muted">${values[i]}:</span>
${d}
</span>
</div>`));
if(d) {
this.$stats_container.append($(`<div class="stats">
<span class="indicator ${this.x.colors[i]}">
<span class="text-muted">${x_values[i]}:</span>
${d}
</span>
</div>`));
}
});
}
};
frappe.ui.HeatMap = class HeatMap extends frappe.ui.Graph {
constructor({
parent = null,
height = 240,
title = '', subtitle = '',
start = new Date(moment().subtract(1, 'year').toDate()),
domain = '',
subdomain = '',
data = {},
discrete_domains = 0,
count_label = '',
// TODO: remove these graph related args
y = [],
x = [],
specific_values = [],
summary = [],
mode = 'heatmap'
} = {}) {
super(arguments[0]);
this.start = start;
this.data = data;
this.discrete_domains = discrete_domains;
this.count_label = count_label;
this.legend_colors = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
this.setup();
}
setup_base_values() {
this.today = new Date();
if(!this.start) {
this.start = new Date();
this.start.setFullYear( this.start.getFullYear() - 1 );
}
this.first_week_start = new Date(this.start.toDateString());
this.last_week_start = new Date(this.today.toDateString());
if(this.first_week_start.getDay() !== 7) {
this.add_days(this.first_week_start, (-1) * this.first_week_start.getDay());
}
if(this.last_week_start.getDay() !== 7) {
this.add_days(this.last_week_start, (-1) * this.last_week_start.getDay());
}
this.no_of_cols = this.get_weeks_between(this.first_week_start + '', this.last_week_start + '') + 1;
}
set_width() {
this.base_width = (this.no_of_cols) * 12;
}
setup_components() {
this.domain_label_group = this.snap.g().attr({ class: "domain-label-group chart-label" });
this.data_groups = this.snap.g().attr({ class: "data-groups", transform: `translate(0, 20)` });
}
setup_values() {
this.distribution = this.get_distribution(this.data, this.legend_colors);
this.month_names = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
this.render_all_weeks_and_store_x_values(this.no_of_cols);
}
render_all_weeks_and_store_x_values(no_of_weeks) {
let current_week_sunday = new Date(this.first_week_start);
this.week_col = 0;
this.current_month = current_week_sunday.getMonth();
this.months = [this.current_month + ''];
this.month_weeks = {}, this.month_start_points = [];
this.month_weeks[this.current_month] = 0;
this.month_start_points.push(13);
this.date_values = {};
Object.keys(this.data).map(key => {
let date = new Date(key * 1000);
let date_str = this.get_dd_mm_yyyy(date);
this.date_values[date_str] = this.data[key];
});
for(var i = 0; i < no_of_weeks; i++) {
let data_group, month_change = 0;
let day = new Date(current_week_sunday);
[data_group, month_change] = this.get_week_squares_group(day, this.week_col);
this.data_groups.add(data_group);
this.week_col += 1 + parseInt(this.discrete_domains && month_change);
this.month_weeks[this.current_month]++;
if(month_change) {
this.current_month = (this.current_month + 1) % 12;
this.months.push(this.current_month + '');
this.month_weeks[this.current_month] = 1;
}
this.add_days(current_week_sunday, 7);
}
this.render_month_labels();
}
get_week_squares_group(current_date, index) {
const no_of_weekdays = 7;
const square_side = 10;
const cell_padding = 2;
const step = 1;
let month_change = 0;
let week_col_change = 0;
let data_group = this.snap.g().attr({ class: "data-group" });
for(var y = 0, i = 0; i < no_of_weekdays; i += step, y += (square_side + cell_padding)) {
let data_value = 0;
let color_index = 0;
let timestamp = this.get_dd_mm_yyyy(current_date);
if(this.date_values[timestamp]) {
data_value = this.date_values[timestamp];
color_index = this.get_max_checkpoint(data_value, this.distribution);
}
if(this.date_values[Math.round(timestamp)]) {
data_value = this.date_values[Math.round(timestamp)];
color_index = this.get_max_checkpoint(data_value, this.distribution);
}
let x = 13 + (index + week_col_change) * 12;
data_group.add(this.snap.rect(x, y, square_side, square_side).attr({
'class': `day`,
'fill': this.legend_colors[color_index],
'data-date': this.get_dd_mm_yyyy(current_date),
'data-value': data_value,
'data-day': current_date.getDay()
}));
let next_date = new Date(current_date);
this.add_days(next_date, 1);
if(next_date.getMonth() - current_date.getMonth()) {
month_change = 1;
if(this.discrete_domains) {
week_col_change = 1;
}
this.month_start_points.push(13 + (index + week_col_change) * 12);
}
current_date = next_date;
}
return [data_group, month_change];
}
render_month_labels() {
this.first_month_label = 1;
// if (this.first_week_start.getDate() > 8) {
// this.first_month_label = 0;
// }
this.last_month_label = 1;
let first_month = this.months.shift();
let first_month_start = this.month_start_points.shift();
// render first month if
let last_month = this.months.pop();
let last_month_start = this.month_start_points.pop();
// render last month if
this.month_start_points.map((start, i) => {
let month_name = this.month_names[this.months[i]].substring(0, 3);
this.domain_label_group.add(this.snap.text(start + 12, 10, month_name).attr({
dy: ".32em",
class: "y-value-text"
}));
});
}
make_graph_components() {
this.container.find('.graph-stats-container, .sub-title, .title').hide();
this.container.find('.graphics').css({'margin-top': '0px', 'padding-top': '0px'});
}
bind_tooltip() {
this.container.on('mouseenter', '.day', (e) => {
let subdomain = $(e.target);
let count = subdomain.attr('data-value');
let date_parts = subdomain.attr('data-date').split('-');
let month = this.month_names[parseInt(date_parts[1])-1].substring(0, 3);
let g_off = this.$graphics.offset(), p_off = subdomain.offset();
let width = parseInt(subdomain.attr('width'));
let x = p_off.left - g_off.left + (width+2)/2;
let y = p_off.top - g_off.top - (width+2)/2;
let value = count + ' ' + this.count_label;
let name = ' on ' + month + ' ' + date_parts[0] + ', ' + date_parts[2];
this.tip.set_values(x, y, name, value, [], 1);
this.tip.show_tip();
});
}
update(data) {
this.data = data;
this.setup_values();
}
get_distribution(data={}, mapper_array) {
let data_values = Object.keys(data).map(key => data[key]);
let data_max_value = Math.max(...data_values);
let distribution_step = 1 / (mapper_array.length - 1);
let distribution = [];
mapper_array.map((color, i) => {
let checkpoint = data_max_value * (distribution_step * i);
distribution.push(checkpoint);
});
return distribution;
}
get_max_checkpoint(value, distribution) {
return distribution.filter((d, i) => {
return value > d;
}).length;
}
// TODO: date utils, move these out
// https://stackoverflow.com/a/11252167/6495043
treat_as_utc(date_str) {
let result = new Date(date_str);
result.setMinutes(result.getMinutes() - result.getTimezoneOffset());
return result;
}
get_dd_mm_yyyy(date) {
let dd = date.getDate();
let mm = date.getMonth() + 1; // getMonth() is zero-based
return [
(dd>9 ? '' : '0') + dd,
(mm>9 ? '' : '0') + mm,
date.getFullYear()
].join('-');
}
get_weeks_between(start_date_str, end_date_str) {
return Math.ceil(this.get_days_between(start_date_str, end_date_str) / 7);
}
get_days_between(start_date_str, end_date_str) {
let milliseconds_per_day = 24 * 60 * 60 * 1000;
return (this.treat_as_utc(end_date_str) - this.treat_as_utc(start_date_str)) / milliseconds_per_day;
}
// mutates
add_days(date, number_of_days) {
date.setDate(date.getDate() + number_of_days);
}
get_month_name() {}
}
frappe.ui.SvgTip = class {
constructor({
parent = null
}) {
this.parent = parent;
this.title_name = '';
this.title_value = '';
this.list_values = [];
this.title_value_first = 0;
this.x = 0;
this.y = 0;
this.top = 0;
this.left = 0;
this.setup();
}
setup() {
this.make_tooltip();
}
refresh() {
this.fill();
this.calc_position();
// this.show_tip();
}
make_tooltip() {
this.container = $(`<div class="graph-svg-tip comparison">
<span class="title"></span>
<ul class="data-point-list"></ul>
<div class="svg-pointer"></div>
</div>`).appendTo(this.parent);
this.hide_tip();
this.title = this.container.find('.title');
this.data_point_list = this.container.find('.data-point-list');
this.parent.on('mouseleave', () => {
this.hide_tip();
});
}
fill() {
let title;
if(this.title_value_first) {
title = `<strong>${this.title_value}</strong>${this.title_name}`;
} else {
title = `${this.title_name}<strong>${this.title_value}</strong>`;
}
this.title.html(title);
this.data_point_list.empty();
this.list_values.map((set, i) => {
let $li = $(`<li>
<strong style="display: block;">${set.value ? set.value : '' }</strong>
${set.title ? set.title : '' }
</li>`).addClass(`border-top ${set.color || 'black'}`);
this.data_point_list.append($li);
});
}
calc_position() {
this.top = this.y - this.container.height();
this.left = this.x - this.container.width()/2;
let max_left = this.parent.width() - this.container.width();
let $pointer = this.container.find('.svg-pointer');
if(this.left < 0) {
$pointer.css({ 'left': `calc(50% - ${-1 * this.left}px)` });
this.left = 0;
} else if(this.left > max_left) {
let delta = this.left - max_left;
$pointer.css({ 'left': `calc(50% + ${delta}px)` });
this.left = max_left;
} else {
$pointer.css({ 'left': `50%` });
}
}
set_values(x, y, title_name = '', title_value = '', list_values = [], title_value_first = 0) {
this.title_name = title_name;
this.title_value = title_value;
this.list_values = list_values;
this.x = x;
this.y = y;
this.title_value_first = title_value_first;
this.refresh();
}
hide_tip() {
this.container.css({
'top': '0px',
'left': '0px',
'opacity': '0'
});
}
show_tip() {
this.container.css({
'top': this.top + 'px',
'left': this.left + 'px',
'opacity': '1'
});
}
};
frappe.provide("frappe.ui.graphs");
frappe.ui.graphs.get_timeseries = function(start, frequency, length) {
}
frappe.ui.graphs.map_c3 = function(chart) {
if (chart.data) {
let data = chart.data;
let mode = chart.chart_type || 'line';
if(mode === 'pie') {
mode = 'percentage';
}
let x = {}, y = [];
if(data.columns) {
let columns = data.columns;
x.values = columns.filter(col => {
return col[0] === data.x;
})[0];
if(x.values && x.values.length) {
let dataset_length = x.values.length;
let dirty = false;
columns.map(col => {
if(col[0] !== data.x) {
if(col.length === dataset_length) {
let title = col[0];
col.splice(0, 1);
y.push({
title: title,
values: col,
});
} else {
dirty = true;
}
}
})
if(dirty) {
return;
}
x.values.splice(0, 1);
return {
mode: mode,
y: y,
x: x
}
}
} else if(data.rows) {
let rows = data.rows;
x.values = rows[0];
rows.map((row, i) => {
if(i === 0) {
x.values = row;
} else {
y.push({
title: 'data' + i,
values: row,
})
}
});
return {
mode: mode,
y: y,
x: x
}
}
}
}
// frappe.ui.CompositeGraph = class {
// constructor({
// parent = null
// }) {
// this.parent = parent;
// this.title_name = '';
// this.title_value = '';
// this.list_values = [];
// this.x = 0;
// this.y = 0;
// this.top = 0;
// this.left = 0;
// this.setup();
// }
// }

View file

@ -234,7 +234,7 @@ frappe.verify_password = function(callback) {
}, __("Verify Password"), __("Verify"))
}
frappe.show_progress = function(title, count, total) {
frappe.show_progress = function(title, count, total=100, description) {
if(frappe.cur_progress && frappe.cur_progress.title === title
&& frappe.cur_progress.$wrapper.is(":visible")) {
var dialog = frappe.cur_progress;
@ -242,7 +242,10 @@ frappe.show_progress = function(title, count, total) {
var dialog = new frappe.ui.Dialog({
title: title,
});
dialog.progress = $('<div class="progress"><div class="progress-bar"></div></div>')
dialog.progress = $(`<div class="progress">
<div class="progress-bar"></div>
<p class="description text-muted small"></p>
</div>`)
.appendTo(dialog.body);
dialog.progress_bar = dialog.progress.css({"margin-top": "10px"})
.find(".progress-bar");
@ -250,7 +253,12 @@ frappe.show_progress = function(title, count, total) {
dialog.show();
frappe.cur_progress = dialog;
}
dialog.progress_bar.css({"width": cint(flt(count) * 100 / total) + "%" });
if (description) {
dialog.progress.find('.description').text(description);
}
dialog.percent = cint(flt(count) * 100 / total);
dialog.progress_bar.css({"width": dialog.percent + "%" });
return dialog;
}
frappe.hide_progress = function() {

View file

@ -1,20 +1,26 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
// __("Form")
/**
* Make a standard page layout with a toolbar and title
*
* @param {Object} opts
*
* @param {string} opts.parent [HTMLElement] Parent element
* @param {boolean} opts.single_column Whether to include sidebar
* @param {string} [opts.title] Page title
* @param {Object} [opts.required_libs] resources to load
* @param {Object} [opts.make_page]
*
* @returns {frappe.ui.Page}
*/
/**
* @typedef {Object} frappe.ui.Page
*/
// parent, title, single_column
// standard page with page
frappe.ui.make_app_page = function(opts) {
/* help: make a standard page layout with a toolbar and title */
/* options: [
"parent: [HTMLElement] parent element",
"single_column: [Boolean] false/true",
"title: [optional] set this title"
]
*/
opts.parent.page = new frappe.ui.Page(opts);
return opts.parent.page;
}
@ -36,9 +42,47 @@ frappe.ui.Page = Class.extend({
make: function() {
this.wrapper = $(this.parent);
this.setup_render();
},
get_empty_state: function({title, message, primary_action}) {
let $empty_state = $(`<div class="page-card-container">
<div class="page-card">
<div class="page-card-head">
<span class="indicator blue">
${title}</span>
</div>
<p>${message}</p>
<div><a href="/login" class="btn btn-primary btn-sm">${primary_action.label}</a></div>
</div>
</div>`);
$empty_state.find('.btn-primary').on('click', () => {
primary_action.on_click();
});
return $empty_state;
},
setup_render: function() {
var lib_exists = (typeof this.required_libs === 'string' && this.required_libs)
|| ($.isArray(this.required_libs) && this.required_libs.length);
if (lib_exists) {
this.load_lib(() => {
this.add_main_section();
});
} else {
this.add_main_section();
}
},
load_lib: function (callback) {
frappe.require(this.required_libs, callback);
},
add_main_section: function() {
$(frappe.render_template("page", {})).appendTo(this.wrapper);
if(this.single_column) {
// nesting under col-sm-12 for consistency
this.add_view("main", '<div class="row layout-main">\
@ -48,18 +92,19 @@ frappe.ui.Page = Class.extend({
</div>\
</div>');
} else {
var main = this.add_view("main", '<div class="row layout-main">\
this.add_view("main", '<div class="row layout-main">\
<div class="col-md-2 layout-side-section"></div>\
<div class="col-md-10 layout-main-section-wrapper">\
<div class="layout-main-section"></div>\
<div class="layout-footer hide"></div>\
</div>\
</div>');
// this.wrapper.find('.page-title')
// .removeClass('col-md-7').addClass('col-md-offset-2 col-md-5')
// .css({'padding-left': '45px'});
}
this.setup_page();
},
setup_page: function() {
this.$title_area = this.wrapper.find("h1");
this.$sub_title_area = this.wrapper.find("h6");
@ -92,6 +137,10 @@ frappe.ui.Page = Class.extend({
this.page_form = $('<div class="page-form row hide"></div>').prependTo(this.main);
this.inner_toolbar = $('<div class="form-inner-toolbar hide"></div>').prependTo(this.main);
this.icon_group = this.page_actions.find(".page-icon-group");
if(this.make_page) {
this.make_page();
}
},
set_indicator: function(label, color) {
@ -437,7 +486,11 @@ frappe.ui.Page = Class.extend({
return values;
},
add_view: function(name, html) {
this.views[name] = $(html).appendTo($(this.wrapper).find(".page-content"));
let element = html;
if(typeof(html) === "string") {
element = $(html);
}
this.views[name] = element.appendTo($(this.wrapper).find(".page-content"));
if(!this.current_view) {
this.current_view = this.views[name];
} else {

View file

@ -0,0 +1,393 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.provide("frappe.ui");
frappe.ui.Slide = class Slide {
constructor(slide = null) {
$.extend(this, slide);
this.setup();
}
setup() {
this.$wrapper = $('<div class="slide-wrapper hidden"></div>')
.attr({"data-slide-id": this.id, "data-slide-name": this.name})
.appendTo(this.parent);
}
// Make has to be called manually, to account for on-demand use cases
make() {
if(this.before_load) { this.before_load(this); }
this.$body = $(`<div class="slide-body">
<div class="content text-center">
<p class="title lead">${this.title}</p>
</div>
<div class="form-wrapper">
<div class="form"></div>
<div class="add-more text-center" style="margin-top: 5px;">
<a class="form-more-btn hide btn btn-default btn-xs">${__("Add More")}</a>
</div>
</div>
</div>`).appendTo(this.$wrapper);
this.$content = this.$body.find(".content");
this.$form = this.$body.find(".form");
this.$primary_btn = this.slides_footer.find('.primary');
if(this.help) this.$content.append($(`<p class="slide-help">${this.help}</p>`));
if(this.image_src) this.$content.append(
$(`<img src="${this.image_src}" style="margin: 20px;">`));
this.reqd_fields = [];
this.refresh();
this.made = true;
}
refresh() {
this.render_parent_dots();
if(!this.done) {
this.setup_form();
} else {
this.setup_done_state();
}
}
setup_form() {
this.form = new frappe.ui.FieldGroup({
fields: this.get_atomic_fields(),
body: this.$form[0],
no_submit_on_enter: true
});
this.form.make();
if(this.add_more) this.bind_more_button();
this.set_reqd_fields();
if(this.onload) { this.onload(this); }
this.set_reqd_fields();
}
// Form methods
get_atomic_fields() {
var fields = JSON.parse(JSON.stringify(this.fields));
if(this.add_more) {
this.count = 1;
fields = fields.map((field, i) => {
if(field.fieldname) {
field.fieldname += '_1';
}
if(i === 1 && this.mandatory_entry) {
field.reqd = 1;
}
if(!field.static) {
if(field.label) field.label += ' 1';
}
return field;
});
}
return fields;
}
set_reqd_fields() {
var dict = this.form.fields_dict;
this.reqd_fields = [];
Object.keys(dict).map(key => {
if(dict[key].df.reqd) {
this.reqd_fields.push(dict[key]);
}
});
}
set_values() {
this.values = this.form.get_values();
if(this.values===null) {
return false;
}
if(this.validate && !this.validate()) {
return false;
}
return true;
}
bind_more_button() {
this.$more = this.$body.find('.form-more-btn');
this.$more.removeClass('hide')
.on('click', () => {
this.count++;
var fields = JSON.parse(JSON.stringify(this.fields));
this.form.add_fields(fields.map(field => {
if(field.fieldname) field.fieldname += '_' + this.count;
if(!field.static) {
if(field.label) field.label += ' ' + this.count;
}
return field;
}));
if(this.count === this.max_count) {
this.$more.addClass('hide');
}
});
}
// Primary button (outside of slide)
resetup_primary_button() {
this.unbind_primary_action();
this.bind_fields_to_action_btn();
this.reset_action_button_state();
this.bind_primary_action();
}
bind_fields_to_action_btn() {
var me = this;
this.reqd_fields.map((field) => {
field.$wrapper.on('change input', () => {
me.reset_action_button_state();
});
});
}
reset_action_button_state() {
var empty_fields = this.reqd_fields.filter((field) => {
return !field.get_value();
});
if(empty_fields.length) {
this.slides_footer.find('.action').addClass('disabled');
} else {
this.slides_footer.find('.action').removeClass('disabled');
}
}
unbind_primary_action() {
this.slides_footer.find(".primary").off();
}
bind_primary_action() {
this.slides_footer.find(".primary").on('click', () => {
this.primary_action();
});
}
before_show() { }
show_slide() {
this.$wrapper.removeClass("hidden");
this.before_show();
this.resetup_primary_button();
if(!this.done) {
this.$body.find('.form-control').first().focus();
this.$primary_btn.show();
} else {
this.$primary_btn.hide();
}
}
hide_slide() {
this.$wrapper.addClass("hidden");
}
get_input(fieldname) {
return this.form.get_input(fieldname);
}
get_field(fieldname) {
return this.form.get_field(fieldname);
}
destroy() {
this.$body.remove();
}
primary_action() { }
};
frappe.ui.Slides = class Slides {
constructor({
parent = null,
slides = [],
slide_class = null,
unidirectional = 0,
done_state = 0,
before_load = null,
on_update = null
}) {
this.parent = parent;
this.slides = slides;
this.slide_class = slide_class;
this.unidirectional = unidirectional;
this.done_state = done_state;
this.before_load = before_load;
this.on_update = on_update;
this.slide_dict = {};
//In case of refreshing
this.made_slide_ids = [];
this.values = {};
this.make();
}
make() {
this.container = $('<div>').addClass("slides-wrapper").attr({"tabindex": -1})
.appendTo(this.parent);
this.$slide_progress = $(`<div>`).addClass(`slides-progress text-center text-extra-muted`)
.appendTo(this.container);
this.$body = $(`<div>`).addClass(`slide-container`)
.appendTo(this.container);
this.$footer = $(`<div>`).addClass(`footer`)
.appendTo(this.container);
this.render_progress_dots();
this.make_prev_next_buttons();
if(this.before_load) { this.before_load(this.$footer); }
// can be on demand
this.setup();
// can be on demand
this.show_slide(0);
}
setup() {
this.slides.map((slide, id) => {
if(!this.slide_dict[id]) {
this.slide_dict[id] = new (this.slide_class)(
$.extend(this.slides[id], {
parent: this.$body,
slides_footer: this.$footer,
render_parent_dots: this.render_progress_dots.bind(this),
id: id,
})
);
if(!this.unidirectional) {
this.slide_dict[id].make();
}
} else {
if(this.made_slide_ids.includes(id+"")) {
this.slide_dict[id].destroy();
this.slide_dict[id].make();
}
}
});
}
refresh(id) {
this.render_progress_dots();
this.show_hide_prev_next(id);
this.$body.find('.form-control').first().focus();
}
render_progress_dots() {
// Depends on this.unidirectional and this.done_state
// Can be called by a slide to update states
this.$slide_progress.empty();
this.slides.map((slide, id) => {
let $dot = $(`<i class="fa fa-fw fa-circle"> </i> `)
.attr({'data-step-id': id});
if(this.done_state && (this.slide_dict[id] &&
this.slide_dict[id].done || slide.done)) {
$dot.addClass('text-success');
}
if((this.unidirectional && id <= this.current_id) ||
id === this.current_id) {
$dot.addClass('active');
}
// Add pointer event for non-unidirectional
this.$slide_progress.append($dot);
});
this.completed = 0;
this.slides.map((slide, i) => {
if(this.slide_dict[i]) {
if(this.slide_dict[i].done) this.completed++;
} else {
if(slide.done) this.completed++;
}
});
if(this.on_update) {this.on_update(this.completed, this.slides.length);}
if(!this.unidirectional) this.bind_progress_dots();
}
make_prev_next_buttons() {
$(`<div class="row">
<div class="col-sm-4">
<a class="prev-btn btn btn-default btn-sm" tabindex="0">${__("Previous")}</a>
</div>
<div class="col-sm-8 text-right">
<a class="next-btn btn btn-default btn-sm" tabindex="0">${__("Next")}</a>
</div>
</div>`).appendTo(this.$footer);
this.$prev_btn = this.$footer.find('.prev-btn').attr('tabIndex', 0)
.on('click', () => { this.show_slide(this.current_id - 1); });
this.$next_btn = this.$footer.find('.next-btn').attr('tabIndex', 0)
.on('click', () => {
if (!this.unidirectional || (this.unidirectional && this.current_slide.set_values())) {
this.show_slide(this.current_id + 1);
}
});
}
bind_progress_dots() {
var me = this;
this.$slide_progress.find('.fa-circle').addClass('link').on('click', function() {
let id = $(this).attr('data-step-id');
me.show_slide(id);
});
}
before_show_slide() {
return true;
}
show_slide(id) {
id = cint(id);
if(!this.before_show_slide() ||
(this.current_slide && this.current_id===id)) {
return;
}
this.update_values();
if(this.current_slide) this.current_slide.hide_slide();
if(this.unidirectional && !this.slide_dict[id].made) {
this.slide_dict[id].make();
}
this.current_id = id;
this.current_slide = this.slide_dict[id];
this.current_slide.show_slide();
this.refresh(id);
}
destroy_slide(id) {
if(this.slide_dict[id]) this.slide_dict[id].destroy();
this.slide_dict[id] = null;
}
on_update(completed, total) {}
show_hide_prev_next(id) {
(id === 0) ?
this.$prev_btn.hide() : this.$prev_btn.show();
(id + 1 === this.slides.length) ?
this.$next_btn.hide() : this.$next_btn.show();
}
get_values() {
var values = {};
$.each(this.slide_dict, function(id, slide) {
if(slide.values) {
$.extend(values, slide.values);
}
});
return values;
}
update_values() {
this.values = $.extend(this.values, this.get_values());
}
};

View file

@ -35,10 +35,10 @@ frappe.ui.misc.about = function() {
var v = versions[key];
if(v.branch) {
var text = $.format('<p><b>{0}:</b> v{1} ({2})<br></p>',
[v.title, v.branch_version || v.version, v.branch])
[v.title, v.branch_version || v.version, v.branch])
} else {
var text = $.format('<p><b>{0}:</b> v{1}<br></p>',
[v.title, v.version])
[v.title, v.version])
}
$(text).appendTo($wrap);
});

Some files were not shown because too many files have changed in this diff Show more