Merge branch 'staging'
This commit is contained in:
commit
645526bb28
232 changed files with 51070 additions and 39574 deletions
|
|
@ -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
46
CODE_OF_CONDUCT.md
Normal 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/
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ frappe.ui.form.on("Contact", {
|
|||
}
|
||||
}
|
||||
});
|
||||
frm.refresh_field("links");
|
||||
},
|
||||
validate: function(frm) {
|
||||
// clear linked customer / supplier / sales partner on saving...
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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'"))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)" \
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
30
frappe/desk/user_progress.py
Normal file
30
frappe/desk/user_progress.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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={}):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
65
frappe/email/doctype/newsletter/templates/newsletter.html
Normal file
65
frappe/email/doctype/newsletter/templates/newsletter.html
Normal 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 %}
|
||||
|
|
@ -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>
|
||||
23
frappe/email/doctype/newsletter/test_newsletter.js
Normal file
23
frappe/email/doctype/newsletter/test_newsletter.js
Normal 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()
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
12
frappe/patches/v8_x/add_bgn_xaf_xof_currencies.py
Normal file
12
frappe/patches/v8_x/add_bgn_xaf_xof_currencies.py
Normal 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()
|
||||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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">×</a></div>')
|
||||
<a class="close" style="position: absolute; right: 15px;">×</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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,\
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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) }}
|
||||
{% } %}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
393
frappe/public/js/frappe/ui/slides.js
Normal file
393
frappe/public/js/frappe/ui/slides.js
Normal 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());
|
||||
}
|
||||
};
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue