@@ -117,6 +117,7 @@ | |||
"set_field_options": true, | |||
"getCookie": true, | |||
"getCookies": true, | |||
"get_url_arg": true | |||
"get_url_arg": true, | |||
"QUnit": true | |||
} | |||
} |
@@ -1,9 +1,5 @@ | |||
language: python | |||
dist: trusty | |||
group: deprecated-2017Q2 | |||
python: | |||
- "2.7" | |||
addons: | |||
apt: | |||
@@ -11,15 +7,18 @@ addons: | |||
- google-chrome | |||
packages: | |||
- google-chrome-stable | |||
# sauce_connect: | |||
# username: "rmehta1" | |||
# access_key: "a80640ec-24c8-44ad-9398-1b6f123ae4a1" | |||
python: | |||
- "2.7" | |||
services: | |||
- mysql | |||
before_install: | |||
- export DISPLAY=:99.0 | |||
- sh -e /etc/init.d/xvfb start | |||
install: | |||
- sudo rm /etc/apt/sources.list.d/docker.list | |||
- sudo apt-get purge -y mysql-common mysql-server mysql-client | |||
- nvm install v7.10.0 | |||
- wget https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py | |||
@@ -31,10 +30,19 @@ install: | |||
- cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ | |||
before_script: | |||
- wget http://chromedriver.storage.googleapis.com/2.27/chromedriver_linux64.zip | |||
- unzip chromedriver_linux64.zip | |||
- sudo apt-get install libnss3 | |||
- sudo apt-get --only-upgrade install google-chrome-stable | |||
- sudo cp chromedriver /usr/local/bin/. | |||
- sudo chmod +x /usr/local/bin/chromedriver | |||
- export DISPLAY=:99.0 | |||
- sh -e /etc/init.d/xvfb start | |||
- sleep 3 | |||
- mysql -u root -ptravis -e 'create database test_frappe' | |||
- echo "USE mysql;\nCREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe';\nFLUSH PRIVILEGES;\n" | mysql -u root -ptravis | |||
- echo "USE mysql;\nGRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost';\n" | mysql -u root -ptravis | |||
- cd ~/frappe-bench | |||
- bench use test_site | |||
- bench reinstall --yes | |||
@@ -44,5 +52,5 @@ before_script: | |||
script: | |||
- set -e | |||
- bench --verbose run-tests | |||
- bench reinstall --yes | |||
- bench run-ui-tests --ci | |||
- sleep 5 | |||
- bench --verbose run-tests --ui-tests |
@@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json | |||
from .exceptions import * | |||
from .utils.jinja import get_jenv, get_template, render_template | |||
__version__ = '8.2.7' | |||
__version__ = '8.3.0' | |||
__title__ = "Frappe Framework" | |||
local = Local() | |||
@@ -138,8 +138,7 @@ def init(site, sites_path=None, new_site=False): | |||
local.module_app = None | |||
local.app_modules = None | |||
local.system_settings = None | |||
local.system_country = None | |||
local.system_settings = _dict() | |||
local.user = None | |||
local.user_perms = None | |||
@@ -1364,7 +1363,7 @@ def get_active_domains(): | |||
return active_domains | |||
def get_system_country(): | |||
if local.system_country is None: | |||
local.system_country = db.get_single_value('System Settings', 'country') or '' | |||
return local.system_country | |||
def get_system_settings(key): | |||
if not local.system_settings.has_key(key): | |||
local.system_settings.update({key: db.get_single_value('System Settings', key)}) | |||
return local.system_settings.get(key) |
@@ -272,6 +272,7 @@ function watch_js(ondirty) { | |||
if (sources.includes(filename)) { | |||
pack(target, sources); | |||
ondirty && ondirty(target); | |||
// break; | |||
} | |||
} | |||
}); | |||
@@ -298,11 +298,13 @@ def console(context): | |||
@click.option('--doctype', help="For DocType") | |||
@click.option('--test', multiple=True, help="Specific test") | |||
@click.option('--driver', help="For Travis") | |||
@click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests") | |||
@click.option('--module', help="Run tests in a module") | |||
@click.option('--profile', is_flag=True, default=False) | |||
@click.option('--junit-xml-output', help="Destination file path for junit xml report") | |||
@pass_context | |||
def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None, profile=False, junit_xml_output=False): | |||
def run_tests(context, app=None, module=None, doctype=None, test=(), | |||
driver=None, profile=False, junit_xml_output=False, ui_tests = False): | |||
"Run tests" | |||
import frappe.test_runner | |||
tests = test | |||
@@ -311,7 +313,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None | |||
frappe.init(site=site) | |||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, | |||
force=context.force, profile=profile, junit_xml_output=junit_xml_output) | |||
force=context.force, profile=profile, junit_xml_output=junit_xml_output, | |||
ui_tests = ui_tests) | |||
if len(ret.failures) == 0 and len(ret.errors) == 0: | |||
ret = 0 | |||
@@ -30,5 +30,12 @@ frappe.ui.form.on("Address", { | |||
frappe.model.remove_from_locals(d.link_doctype, d.link_name); | |||
}); | |||
} | |||
}, | |||
after_save: function() { | |||
var last_route = frappe.route_history.slice(-2, -1)[0]; | |||
if(frappe.dynamic_link && frappe.dynamic_link.doc | |||
&& frappe.dynamic_link.doc.name == last_route[2]){ | |||
frappe.set_route(last_route[0], last_route[1], last_route[2]); | |||
} | |||
} | |||
}); |
@@ -0,0 +1,3 @@ | |||
frappe.listview_settings['Contact'] = { | |||
add_fields: ["image"], | |||
}; |
@@ -1,6 +1,7 @@ | |||
[ | |||
{ | |||
"doctype": "Contact", | |||
"salutation": "Mr", | |||
"email_id": "test_conctact@example.com", | |||
"first_name": "_Test Contact For _Test Customer", | |||
"is_primary_contact": 1, | |||
@@ -0,0 +1,8 @@ | |||
[ | |||
{ | |||
"salutation": "Mr" | |||
}, | |||
{ | |||
"salutation": "Mrs" | |||
} | |||
] |
@@ -13,13 +13,12 @@ class TestAuthenticationLog(unittest.TestCase): | |||
from frappe.auth import LoginManager, CookieManager | |||
# test user login log | |||
frappe.local.form_dict = { 'cmd': 'login' } | |||
frappe.form_dict = { | |||
frappe.local.form_dict = frappe._dict({ | |||
'cmd': 'login', | |||
'sid': 'Guest', | |||
'pwd': 'admin', | |||
'usr': 'Administrator' | |||
} | |||
}) | |||
frappe.local.cookie_manager = CookieManager() | |||
frappe.local.login_manager = LoginManager() | |||
@@ -38,9 +37,11 @@ class TestAuthenticationLog(unittest.TestCase): | |||
auth_log = self.get_auth_log() | |||
self.assertEquals(auth_log.status, 'Failed') | |||
frappe.local.form_dict = frappe._dict() | |||
def get_auth_log(self, operation='Login'): | |||
names = frappe.db.sql_list("""select name from `tabAuthentication Log` | |||
where user='Administrator' and operation='{operation}' order by | |||
names = frappe.db.sql_list("""select name from `tabAuthentication Log` | |||
where user='Administrator' and operation='{operation}' order by | |||
creation desc""".format(operation=operation)) | |||
name = names[0] | |||
@@ -14,7 +14,7 @@ from frappe.model.document import Document | |||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter | |||
from frappe.desk.notifications import delete_notification_count_for | |||
from frappe.modules import make_boilerplate | |||
from frappe.model.db_schema import validate_column_name | |||
from frappe.model.db_schema import validate_column_name, validate_column_length | |||
import frappe.website.render | |||
class InvalidFieldNameError(frappe.ValidationError): pass | |||
@@ -78,7 +78,7 @@ class DocType(Document): | |||
if not [d.fieldname for d in self.fields if d.in_list_view]: | |||
cnt = 0 | |||
for d in self.fields: | |||
if d.reqd and not d.hidden: | |||
if d.reqd and not d.hidden and not d.fieldtype == "Table": | |||
d.in_list_view = 1 | |||
cnt += 1 | |||
if cnt == 4: break | |||
@@ -385,9 +385,10 @@ def validate_fields(meta): | |||
1. There are no illegal characters in fieldnames | |||
2. If fieldnames are unique. | |||
3. Fields that do have database columns are not mandatory. | |||
4. `Link` and `Table` options are valid. | |||
5. **Hidden** and **Mandatory** are not set simultaneously. | |||
3. Validate column length. | |||
4. Fields that do have database columns are not mandatory. | |||
5. `Link` and `Table` options are valid. | |||
6. **Hidden** and **Mandatory** are not set simultaneously. | |||
7. `Check` type field has default as 0 or 1. | |||
8. `Dynamic Links` are correctly defined. | |||
9. Precision is set in numeric fields and is between 1 & 6. | |||
@@ -406,6 +407,9 @@ def validate_fields(meta): | |||
if len(duplicates) > 1: | |||
frappe.throw(_("Fieldname {0} appears multiple times in rows {1}").format(fieldname, ", ".join(duplicates))) | |||
def check_fieldname_length(fieldname): | |||
validate_column_length(fieldname) | |||
def check_illegal_mandatory(d): | |||
if (d.fieldtype in no_value_fields) and d.fieldtype!="Table" and d.reqd: | |||
frappe.throw(_("Field {0} of type {1} cannot be mandatory").format(d.label, d.fieldtype)) | |||
@@ -581,7 +585,6 @@ def validate_fields(meta): | |||
frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname), | |||
InvalidFieldNameError) | |||
fields = meta.get("fields") | |||
fieldname_list = [d.fieldname for d in fields] | |||
@@ -598,6 +601,7 @@ def validate_fields(meta): | |||
d.fieldname = d.fieldname.lower() | |||
check_illegal_characters(d.fieldname) | |||
check_unique_fieldname(d.fieldname) | |||
check_fieldname_length(d.fieldname) | |||
check_illegal_mandatory(d) | |||
check_link_table_options(d) | |||
check_dynamic_link_options(d) | |||
@@ -766,3 +770,11 @@ def init_list(doctype): | |||
doc = frappe.get_meta(doctype) | |||
make_boilerplate("controller_list.js", doc) | |||
make_boilerplate("controller_list.html", doc) | |||
def check_if_fieldname_conflicts_with_methods(doctype, fieldname): | |||
doc = frappe.get_doc({"doctype": doctype}) | |||
method_list = [method for method in dir(doc) if callable(getattr(doc, method))] | |||
if fieldname in method_list: | |||
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname)) | |||
@@ -745,7 +745,7 @@ | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"description": "eg. If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User", | |||
"description": "If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User", | |||
"fieldname": "ignore_user_permissions_if_missing", | |||
"fieldtype": "Check", | |||
"hidden": 0, | |||
@@ -770,6 +770,38 @@ | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"default": "0", | |||
"description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User", | |||
"fieldname": "apply_strict_user_permissions", | |||
"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": "Apply Strict User Permissions", | |||
"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, | |||
@@ -965,7 +997,7 @@ | |||
"issingle": 1, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-06-12 13:05:28.924098", | |||
"modified": "2017-06-23 07:48:10.453011", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "System Settings", | |||
@@ -0,0 +1,63 @@ | |||
// Copyright (c) 2017, Frappe Technologies and contributors | |||
// For license information, please see license.txt | |||
frappe.ui.form.on('Test Runner', { | |||
refresh: (frm) => { | |||
frm.disable_save(); | |||
frm.page.set_primary_action(__("Run Tests"), () => { | |||
return new Promise(resolve => { | |||
let wrapper = $(frm.fields_dict.output.wrapper).empty(); | |||
$("<p>Loading...</p>").appendTo(wrapper); | |||
// all tests | |||
frappe.call({ | |||
method: 'frappe.core.doctype.test_runner.test_runner.get_all_tests' | |||
}).always((data) => { | |||
$("<div id='qunit'></div>").appendTo(wrapper.empty()); | |||
frm.events.run_tests(frm, data.message); | |||
resolve(); | |||
}); | |||
}); | |||
}); | |||
}, | |||
run_tests: function(frm, files) { | |||
let require_list = [ | |||
"assets/frappe/js/lib/jquery/qunit.js", | |||
"assets/frappe/js/lib/jquery/qunit.css" | |||
].concat(); | |||
frappe.require(require_list, () => { | |||
files.forEach((f) => { | |||
frappe.dom.eval(f.script); | |||
}); | |||
// if(frm.doc.module_name) { | |||
// QUnit.module.only(frm.doc.module_name); | |||
// } | |||
QUnit.testDone(function(details) { | |||
var result = { | |||
"Module name": details.module, | |||
"Test name": details.name, | |||
"Assertions": { | |||
"Total": details.total, | |||
"Passed": details.passed, | |||
"Failed": details.failed | |||
}, | |||
"Skipped": details.skipped, | |||
"Todo": details.todo, | |||
"Runtime": details.runtime | |||
}; | |||
// eslint-disable-next-line | |||
console.log(JSON.stringify(result, null, 2)); | |||
}); | |||
QUnit.load(); | |||
QUnit.done(() => { | |||
frappe.set_route('Form', 'Test Runner', 'Test Runner'); | |||
}); | |||
}); | |||
} | |||
}); |
@@ -0,0 +1,122 @@ | |||
{ | |||
"allow_copy": 0, | |||
"allow_guest_to_view": 0, | |||
"allow_import": 0, | |||
"allow_rename": 0, | |||
"beta": 0, | |||
"creation": "2017-06-26 10:57:19.976624", | |||
"custom": 0, | |||
"docstatus": 0, | |||
"doctype": "DocType", | |||
"document_type": "", | |||
"editable_grid": 1, | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "module_path", | |||
"fieldtype": "Data", | |||
"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": "Module Path", | |||
"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": "output", | |||
"fieldtype": "HTML", | |||
"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": "Output", | |||
"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 | |||
} | |||
], | |||
"has_web_view": 0, | |||
"hide_heading": 0, | |||
"hide_toolbar": 0, | |||
"idx": 0, | |||
"image_view": 0, | |||
"in_create": 0, | |||
"is_submittable": 0, | |||
"issingle": 1, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-06-26 10:57:19.976624", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Test Runner", | |||
"name_case": "", | |||
"owner": "Administrator", | |||
"permissions": [ | |||
{ | |||
"amend": 0, | |||
"apply_user_permissions": 0, | |||
"cancel": 0, | |||
"create": 1, | |||
"delete": 1, | |||
"email": 1, | |||
"export": 0, | |||
"if_owner": 0, | |||
"import": 0, | |||
"permlevel": 0, | |||
"print": 1, | |||
"read": 1, | |||
"report": 0, | |||
"role": "System Manager", | |||
"set_user_permissions": 0, | |||
"share": 1, | |||
"submit": 0, | |||
"write": 1 | |||
} | |||
], | |||
"quick_entry": 1, | |||
"read_only": 0, | |||
"read_only_onload": 0, | |||
"show_name_in_global_search": 0, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1, | |||
"track_seen": 0 | |||
} |
@@ -0,0 +1,27 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2017, Frappe Technologies and contributors | |||
# For license information, please see license.txt | |||
from __future__ import unicode_literals | |||
import frappe, os | |||
from frappe.model.document import Document | |||
class TestRunner(Document): | |||
pass | |||
@frappe.whitelist() | |||
def get_all_tests(): | |||
tests = [] | |||
for app in frappe.get_installed_apps(): | |||
tests_path = frappe.get_app_path(app, 'tests', 'ui') | |||
if os.path.exists(tests_path): | |||
for basepath, folders, files in os.walk(tests_path): # pylint: disable=unused-variable | |||
for fname in files: | |||
if fname.startswith('test') and fname.endswith('.js'): | |||
path = os.path.join(basepath, fname) | |||
with open(path, 'r') as fileobj: | |||
tests.append(dict( | |||
path = os.path.relpath(frappe.get_app_path(app), path), | |||
script = fileobj.read() | |||
)) | |||
return tests |
@@ -31,6 +31,13 @@ | |||
"new_password": "Eastern_43A1W", | |||
"enabled": 1 | |||
}, | |||
{ | |||
"doctype": "User", | |||
"email": "test3@example.com", | |||
"first_name": "_Test3", | |||
"new_password": "Eastern_43A1W", | |||
"enabled": 1 | |||
}, | |||
{ | |||
"doctype": "User", | |||
"email": "testperm@example.com", | |||
@@ -36,6 +36,6 @@ | |||
<li>{%= __("Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.") %}</li> | |||
</ol> | |||
<p>{%= __("If these instructions where not helpful, please add in your suggestions on GitHub Issues.") %} | |||
<a href="https://github.com/frappe/frappe/issues" target="_blank">{%= __("Submit an Issue") %}</a> | |||
<a href="https://github.com/frappe/frappe/issues" target="_blank" rel="noopener noreferrer">{%= __("Submit an Issue") %}</a> | |||
</p> | |||
</div> |
@@ -11,7 +11,7 @@ frappe.ui.form.on('Custom Field', { | |||
['DocType', 'issingle', '=', 0], | |||
]; | |||
if(frappe.session.user!=="Administrator") { | |||
filters.push(['DocType', 'module', '!=', 'Core']) | |||
filters.push(['DocType', 'module', 'not in', ['Core', 'Custom']]) | |||
} | |||
return { | |||
"filters": filters | |||
@@ -39,6 +39,10 @@ class CustomField(Document): | |||
if not self.fieldname: | |||
frappe.throw(_("Fieldname not set for Custom Field")) | |||
if not self.flags.ignore_validate: | |||
from frappe.core.doctype.doctype.doctype import check_if_fieldname_conflicts_with_methods | |||
check_if_fieldname_conflicts_with_methods(self.dt, self.fieldname) | |||
def on_update(self): | |||
frappe.clear_cache(doctype=self.dt) | |||
if not self.flags.ignore_validate: | |||
@@ -15,7 +15,7 @@ frappe.ui.form.on("Customize Form", { | |||
['DocType', 'custom', '=', 0], | |||
['DocType', 'name', 'not in', 'DocType, DocField, DocPerm, User, Role, Has Role, \ | |||
Page, Has Role, Module Def, Print Format, Report, Customize Form, \ | |||
Customize Form Field'], | |||
Customize Form Field, Property Setter, Custom Field, Custom Script'], | |||
['DocType', 'restrict_to_domain', 'in', frappe.boot.active_domains] | |||
] | |||
}; | |||
@@ -14,6 +14,7 @@ | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -42,6 +43,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -72,6 +74,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -83,9 +86,9 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 1, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"in_standard_filter": 1, | |||
"label": "Priority", | |||
"length": 0, | |||
"no_copy": 0, | |||
@@ -104,6 +107,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, | |||
@@ -143,7 +148,7 @@ | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"in_standard_filter": 1, | |||
"label": "Due Date", | |||
"length": 0, | |||
"no_copy": 0, | |||
@@ -161,6 +166,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -190,6 +196,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -219,6 +226,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -251,6 +259,7 @@ | |||
"width": "300px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -279,6 +288,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -310,6 +320,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -341,6 +352,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -368,6 +380,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -399,6 +412,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -428,6 +442,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -458,6 +473,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -498,7 +514,7 @@ | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-03-08 14:39:02.027528", | |||
"modified": "2017-07-06 10:23:39.656033", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "ToDo", | |||
@@ -65,7 +65,7 @@ class FormMeta(Meta): | |||
def _get_path(fname): | |||
return os.path.join(path, scrub(fname)) | |||
system_country = frappe.get_system_country() | |||
system_country = frappe.get_system_settings("country") | |||
self._add_code(_get_path(self.name + '.js'), '__js') | |||
if system_country: | |||
@@ -82,6 +82,7 @@ class FormMeta(Meta): | |||
self.add_code_via_hook("doctype_js", "__js") | |||
self.add_code_via_hook("doctype_list_js", "__list_js") | |||
self.add_code_via_hook("doctype_tree_js", "__tree_js") | |||
self.add_code_via_hook("doctype_calendar_js", "__calendar_js") | |||
self.add_custom_script() | |||
self.add_html_templates(path) | |||
@@ -5,7 +5,7 @@ | |||
<div class="media"> | |||
<div class="pull-right app-buttons"> | |||
<a class="btn btn-default btn-xs" | |||
href="{{ app.app_url }}" target="_blank">{{ __("Website") }}</a> | |||
href="{{ app.app_url }}" target="_blank" rel="noopener noreferrer">{{ __("Website") }}</a> | |||
{% if (app.installed) { %} | |||
<button class="btn btn-danger btn-xs btn-remove" | |||
data-title="{{ app.app_title }}" | |||
@@ -21,7 +21,7 @@ | |||
{{ f[1] }} | |||
</td> | |||
<td> | |||
<a href="{{ f[0] }}" target="_blank">{{ f[0] }}</a> | |||
<a href="{{ f[0] }}" target="_blank" rel="noopener noreferrer">{{ f[0] }}</a> | |||
</td> | |||
<td> | |||
{{ f[2] }} | |||
@@ -1,3 +1,22 @@ | |||
.setup-wizard-brand { | |||
margin: 40px; | |||
text-align: center; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center | |||
} | |||
.setup-wizard-brand .brand-icon { | |||
width: 36px; | |||
height: 36px; | |||
} | |||
.setup-wizard-brand .brand-name { | |||
font-size: 20px; | |||
margin-left: 8px; | |||
color: #36414C; | |||
} | |||
.setup-wizard-slide { | |||
padding-left: 0px; | |||
padding-right: 0px; | |||
@@ -14,22 +33,67 @@ | |||
} | |||
.setup-wizard-slide .lead { | |||
margin-bottom: 10px; | |||
margin: 40px; | |||
color: #777777; | |||
text-align: center; | |||
font-size: 30px; | |||
} | |||
.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 { | |||
height: 35px; | |||
font-weight: 500; | |||
} | |||
.setup-wizard-slide .has-error .control-label { | |||
color: #ffa00a; | |||
} | |||
.setup-wizard-slide .has-error .form-control{ | |||
border-color: #ffa00a; | |||
} | |||
.setup-wizard-slide .form-control.bold { | |||
background-color: #fff; | |||
} | |||
.setup-wizard-slide.with-form { | |||
margin: 40px 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; | |||
padding: 30px 0px; | |||
} | |||
.setup-wizard-slide a.next-btn, | |||
.setup-wizard-slide a.complete-btn { | |||
font-size: 14px; | |||
padding: 7px 25px; | |||
} | |||
.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; | |||
padding: 15px; | |||
} | |||
.setup-wizard-slide .fa-fw { | |||
@@ -50,16 +114,28 @@ | |||
} | |||
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] { | |||
width: 140px; | |||
height: 180px; /*depends on presence of heading*/ | |||
text-align: center; | |||
margin-left: 33%; | |||
} | |||
.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; | |||
left: 50%; | |||
transform: translate(-50%, 0); | |||
-webkit-transform: translate(-50%, 0); | |||
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 { | |||
@@ -69,6 +145,38 @@ | |||
-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,22 +1,25 @@ | |||
frappe.provide("frappe.wiz"); | |||
frappe.provide("frappe.wiz.events"); | |||
frappe.provide("frappe.setup.events"); | |||
frappe.wiz = { | |||
frappe.setup = { | |||
slides: [], | |||
events: {}, | |||
data: {}, | |||
utils: {}, | |||
remove_app_slides: [], | |||
on: function(event, fn) { | |||
if(!frappe.wiz.events[event]) { | |||
frappe.wiz.events[event] = []; | |||
if(!frappe.setup.events[event]) { | |||
frappe.setup.events[event] = []; | |||
} | |||
frappe.wiz.events[event].push(fn); | |||
frappe.setup.events[event].push(fn); | |||
}, | |||
add_slide: function(slide) { | |||
frappe.wiz.slides.push(slide); | |||
frappe.setup.slides.push(slide); | |||
}, | |||
run_event: function(event) { | |||
$.each(frappe.wiz.events[event] || [], function(i, fn) { | |||
$.each(frappe.setup.events[event] || [], function(i, fn) { | |||
fn(); | |||
}); | |||
} | |||
@@ -25,21 +28,21 @@ frappe.wiz = { | |||
frappe.pages['setup-wizard'].on_page_load = function(wrapper) { | |||
// setup page ui | |||
$(".navbar:first").toggle(false); | |||
$("body").css({"padding-top":"30px"}); | |||
var requires = ["/assets/frappe/css/animate.min.css"].concat(frappe.boot.setup_wizard_requires || []); | |||
frappe.require(requires, function() { | |||
frappe.wiz.run_event("before_load"); | |||
frappe.setup.run_event("before_load"); | |||
var wizard_settings = { | |||
page_name: "setup-wizard", | |||
parent: wrapper, | |||
slides: frappe.wiz.slides, | |||
slides: frappe.setup.slides, | |||
title: __("Welcome") | |||
} | |||
frappe.wizard = new frappe.wiz.Wizard(wizard_settings); | |||
frappe.wiz.run_event("after_load"); | |||
frappe.wizard = new frappe.setup.Wizard(wizard_settings); | |||
frappe.setup.run_event("after_load"); | |||
// frappe.wizard.values = test_values_edu; | |||
@@ -56,7 +59,7 @@ frappe.pages['setup-wizard'].on_page_show = function(wrapper) { | |||
} | |||
} | |||
frappe.wiz.Wizard = Class.extend({ | |||
frappe.setup.Wizard = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.make(); | |||
@@ -75,6 +78,7 @@ frappe.wiz.Wizard = Class.extend({ | |||
</div>', {html:html})) | |||
}, | |||
show_working: function() { | |||
$('header').find('.setup-wizard-brand').hide(); | |||
this.hide_current_slide(); | |||
frappe.set_route(this.page_name); | |||
this.current_slide = {"$wrapper": this.get_message(this.working_html()).appendTo(this.parent)}; | |||
@@ -96,7 +100,7 @@ frappe.wiz.Wizard = Class.extend({ | |||
this.update_values(); | |||
if(!this.slide_dict[id]) { | |||
this.slide_dict[id] = new frappe.wiz.WizardSlide($.extend(this.slides[id], {wiz:this, id:id})); | |||
this.slide_dict[id] = new frappe.setup.WizardSlide($.extend(this.slides[id], {wiz:this, id:id})); | |||
this.slide_dict[id].make(); | |||
} | |||
@@ -147,8 +151,8 @@ frappe.wiz.Wizard = Class.extend({ | |||
args: {args: this.values}, | |||
callback: function(r) { | |||
me.show_complete(); | |||
if(frappe.wiz.welcome_page) { | |||
localStorage.setItem("session_last_route", frappe.wiz.welcome_page); | |||
if(frappe.setup.welcome_page) { | |||
localStorage.setItem("session_last_route", frappe.setup.welcome_page); | |||
} | |||
setTimeout(function() { | |||
window.location = "/desk"; | |||
@@ -181,26 +185,27 @@ frappe.wiz.Wizard = Class.extend({ | |||
this.update_values(); | |||
frappe.wiz.slides = []; | |||
frappe.wiz.run_event("before_load"); | |||
frappe.setup.slides = []; | |||
frappe.setup.run_event("before_load"); | |||
// remove slides listed in remove_app_slides | |||
var new_slides = []; | |||
frappe.wiz.slides.forEach(function(slide) { | |||
if(frappe.wiz.domain) { | |||
frappe.setup.slides.forEach(function(slide) { | |||
if(frappe.setup.domain) { | |||
var domains = slide.domains; | |||
if (domains.indexOf('all') !== -1 || | |||
domains.indexOf(frappe.wiz.domain.toLowerCase()) !== -1) { | |||
domains.indexOf(frappe.setup.domain.toLowerCase()) !== -1) { | |||
new_slides.push(slide); | |||
} | |||
} else { | |||
new_slides.push(slide); | |||
} | |||
}) | |||
frappe.wiz.slides = new_slides; | |||
this.slides = frappe.wiz.slides; | |||
frappe.wiz.run_event("after_load"); | |||
frappe.setup.slides = new_slides; | |||
this.slides = frappe.setup.slides; | |||
frappe.setup.run_event("after_load"); | |||
// re-render all slides | |||
this.slide_dict = {}; | |||
@@ -213,7 +218,7 @@ frappe.wiz.Wizard = Class.extend({ | |||
} | |||
}); | |||
frappe.wiz.WizardSlide = Class.extend({ | |||
frappe.setup.WizardSlide = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.$wrapper = $('<div class="slide-wrapper hidden"></div>') | |||
@@ -224,6 +229,17 @@ frappe.wiz.WizardSlide = Class.extend({ | |||
var me = this; | |||
if(this.$body) this.$body.remove(); | |||
var fields = JSON.parse(JSON.stringify(this.fields)); | |||
if(this.add_more) { | |||
this.count = 1; | |||
fields = fields.map(field => { | |||
if(field.fieldname) field.fieldname += '_1'; | |||
if(field.label) field.label += ' 1'; | |||
return field; | |||
}); | |||
} | |||
if(this.before_load) { | |||
this.before_load(this); | |||
} | |||
@@ -234,7 +250,6 @@ frappe.wiz.WizardSlide = Class.extend({ | |||
main_title:__(this.wiz.title), | |||
step: this.id + 1, | |||
name: this.name, | |||
css_class: this.css_class || "", | |||
slides_count: this.wiz.slides.length | |||
})).appendTo(this.$wrapper); | |||
@@ -242,7 +257,7 @@ frappe.wiz.WizardSlide = Class.extend({ | |||
if(this.fields) { | |||
this.form = new frappe.ui.FieldGroup({ | |||
fields: this.fields, | |||
fields: fields, | |||
body: this.body, | |||
no_submit_on_enter: true | |||
}); | |||
@@ -251,18 +266,33 @@ frappe.wiz.WizardSlide = Class.extend({ | |||
$(this.body).html(this.html); | |||
} | |||
this.set_reqd_fields(); | |||
this.set_init_values(); | |||
this.make_prev_next_buttons(); | |||
if(this.add_more) this.bind_more_button(); | |||
var $primary_btn = this.$next ? this.$next : this.$complete; | |||
this.bind_fields_to_next($primary_btn); | |||
if(this.onload) { | |||
this.onload(this); | |||
} | |||
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() { | |||
var me = this; | |||
// set values from frappe.wiz.values | |||
// set values from frappe.setup.values | |||
if(frappe.wizard.values && this.fields) { | |||
this.fields.forEach(function(f) { | |||
var value = frappe.wizard.values[f.fieldname]; | |||
@@ -284,6 +314,23 @@ frappe.wiz.WizardSlide = Class.extend({ | |||
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.label) field.label += ' ' + this.count; | |||
return field; | |||
})); | |||
if(this.count === this.max_count) { | |||
this.$more.addClass('hide'); | |||
} | |||
}); | |||
}, | |||
make_prev_next_buttons: function() { | |||
var me = this; | |||
@@ -311,7 +358,7 @@ frappe.wiz.WizardSlide = Class.extend({ | |||
.click(this.next_or_complete.bind(this)); | |||
} | |||
//setup mousefree navigation | |||
// setup mousefree navigation | |||
this.$body.on('keypress', function(e) { | |||
if(e.which === 13) { | |||
var $target = $(e.target); | |||
@@ -326,6 +373,14 @@ frappe.wiz.WizardSlide = Class.extend({ | |||
} | |||
}); | |||
}, | |||
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) { | |||
@@ -335,6 +390,17 @@ frappe.wiz.WizardSlide = Class.extend({ | |||
} | |||
} | |||
}, | |||
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(); | |||
@@ -360,233 +426,302 @@ frappe.wiz.WizardSlide = Class.extend({ | |||
}, | |||
}); | |||
function load_frappe_slides() { | |||
// language selection | |||
frappe.wiz.welcome = { | |||
var frappe_slides = [ | |||
{ | |||
// Welcome (language) slide | |||
name: "welcome", | |||
domains: ["all"], | |||
title: __("Welcome"), | |||
title: __("Hello!"), | |||
icon: "fa fa-world", | |||
help: __("Let's prepare the system for first use."), | |||
fields: [ | |||
{ fieldname: "language", label: __("Select Your Language"), reqd:1, | |||
fieldtype: "Select", "default": "english" }, | |||
{ fieldname: "language", label: __("Your Language"), | |||
fieldtype: "Select", "default": "English" } | |||
], | |||
onload: function(slide) { | |||
if (!frappe.wiz.welcome.data) { | |||
frappe.wiz.welcome.load_languages(slide); | |||
if (frappe.setup.data.lang) { | |||
this.setup_fields(slide); | |||
} else { | |||
frappe.wiz.welcome.setup_fields(slide); | |||
utils.load_languages(slide, this.setup_fields); | |||
} | |||
}, | |||
css_class: "single-column", | |||
load_languages: function(slide) { | |||
frappe.call({ | |||
method: "frappe.desk.page.setup_wizard.setup_wizard.load_languages", | |||
freeze: true, | |||
callback: function(r) { | |||
frappe.wiz.welcome.data = r.message; | |||
frappe.wiz.welcome.setup_fields(slide); | |||
var language_field = slide.get_field("language"); | |||
language_field.set_input(frappe.wiz.welcome.data.default_language || "english"); | |||
if (!frappe.wiz._from_load_messages) { | |||
language_field.$input.trigger("change"); | |||
} | |||
delete frappe.wiz._from_load_messages; | |||
moment.locale("en"); | |||
} | |||
}); | |||
}, | |||
setup_fields: function(slide) { | |||
var select = slide.get_field("language"); | |||
select.df.options = frappe.wiz.welcome.data.languages; | |||
select.refresh(); | |||
frappe.wiz.welcome.bind_events(slide); | |||
utils.setup_language_field(slide); | |||
utils.bind_language_events(slide); | |||
}, | |||
bind_events: function(slide) { | |||
slide.get_input("language").unbind("change").on("change", function() { | |||
var lang = $(this).val() || "english"; | |||
frappe._messages = {}; | |||
frappe.call({ | |||
method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages", | |||
freeze: true, | |||
args: { | |||
language: lang | |||
}, | |||
callback: function(r) { | |||
frappe.wiz._from_load_messages = true; | |||
frappe.wizard.refresh_slides(); | |||
} | |||
}); | |||
}); | |||
} | |||
}, | |||
// region selection | |||
frappe.wiz.region = { | |||
{ | |||
// Region slide | |||
name: 'region', | |||
domains: ["all"], | |||
title: __("Region"), | |||
title: __("Select Your Region"), | |||
icon: "fa fa-flag", | |||
help: __("Select your Country, Time Zone and Currency"), | |||
fields: [ | |||
{ fieldname: "country", label: __("Country"), reqd:1, | |||
{ fieldname: "country", label: __("Your Country"), reqd:1, | |||
fieldtype: "Select" }, | |||
{ fieldtype: "Section Break" }, | |||
{ fieldname: "timezone", label: __("Time Zone"), reqd:1, | |||
fieldtype: "Select" }, | |||
{ fieldtype: "Column Break" }, | |||
{ fieldname: "currency", label: __("Currency"), reqd:1, | |||
fieldtype: "Select" }, | |||
fieldtype: "Select" } | |||
], | |||
onload: function(slide) { | |||
var _setup = function() { | |||
frappe.wiz.region.setup_fields(slide); | |||
frappe.wiz.region.bind_events(slide); | |||
}; | |||
if(frappe.wiz.regional_data) { | |||
_setup(); | |||
if(frappe.setup.data.regional_data) { | |||
this.setup_fields(slide); | |||
} else { | |||
frappe.call({ | |||
method:"frappe.geo.country_info.get_country_timezone_info", | |||
callback: function(data) { | |||
frappe.wiz.regional_data = data.message; | |||
_setup(); | |||
} | |||
}); | |||
utils.load_regional_data(slide, this.setup_fields); | |||
} | |||
}, | |||
css_class: "single-column", | |||
setup_fields: function(slide) { | |||
var data = frappe.wiz.regional_data; | |||
utils.setup_region_fields(slide); | |||
utils.bind_region_events(slide); | |||
} | |||
}, | |||
slide.get_input("country").empty() | |||
.add_options([""].concat(Object.keys(data.country_info).sort())); | |||
{ | |||
// Profile slide | |||
name: 'user', | |||
domains: ["all"], | |||
title: __("The First User: You"), | |||
icon: "fa fa-user", | |||
fields: [ | |||
{ "fieldtype":"Attach Image", "fieldname":"attach_user_image", | |||
label: __("Attach Your Picture"), is_private: 0}, | |||
{ "fieldname": "full_name", "label": __("Full Name"), "fieldtype": "Data", | |||
reqd:1}, | |||
{ "fieldname": "email", "label": __("Email Address") + ' <i>(' + __("Will be your login ID") + ')</i>', | |||
"fieldtype": "Data", reqd:1, "options":"Email"}, | |||
{ "fieldname": "password", "label": __("Password"), "fieldtype": "Password", reqd:1 } | |||
], | |||
help: __('The first user will become the System Manager (you can change this later).'), | |||
onload: function(slide) { | |||
if(frappe.session.user!=="Administrator") { | |||
// remove password field | |||
delete slide.form.fields_dict.password; | |||
slide.form.fields_dict.email.$wrapper.toggle(false); | |||
if(frappe.boot.user.first_name || frappe.boot.user.last_name) { | |||
slide.form.fields_dict.full_name.set_input( | |||
[frappe.boot.user.first_name, frappe.boot.user.last_name].join(' ').trim()); | |||
} | |||
slide.get_input("currency").empty() | |||
.add_options(frappe.utils.unique([""].concat($.map(data.country_info, | |||
function(opts, country) { return opts.currency; }))).sort()); | |||
var user_image = frappe.get_cookie("user_image"); | |||
var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper; | |||
slide.get_input("timezone").empty() | |||
.add_options([""].concat(data.all_timezones)); | |||
if(user_image) { | |||
$attach_user_image.find(".missing-image").toggle(false); | |||
$attach_user_image.find("img").attr("src", decodeURIComponent(user_image)).toggle(true); | |||
} | |||
delete slide.form.fields_dict.email; | |||
// set values if present | |||
if(frappe.wizard.values.country) { | |||
slide.get_field("country").set_input(frappe.wizard.values.country); | |||
} else if (data.default_country) { | |||
slide.get_field("country").set_input(data.default_country); | |||
} else { | |||
utils.load_user_details(slide, this.setup_fields); | |||
} | |||
}, | |||
if(frappe.wizard.values.currency) { | |||
slide.get_field("currency").set_input(frappe.wizard.values.currency); | |||
setup_fields: function(slide) { | |||
if(frappe.setup.data.full_name) { | |||
slide.form.fields_dict.full_name.set_input(frappe.setup.data.full_name); | |||
} | |||
if(frappe.wizard.values.timezone) { | |||
slide.get_field("timezone").set_input(frappe.wizard.values.timezone); | |||
if(frappe.setup.data.email) { | |||
let email = frappe.setup.data.email; | |||
slide.form.fields_dict.email.set_input(email); | |||
if (frappe.get_gravatar(email, 200)) { | |||
var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper; | |||
$attach_user_image.find(".missing-image").toggle(false); | |||
$attach_user_image.find("img").attr("src", frappe.get_gravatar(email, 200)); | |||
$attach_user_image.find(".img-container").toggle(true); | |||
} | |||
} | |||
}, | |||
}, | |||
]; | |||
bind_events: function(slide) { | |||
slide.get_input("country").on("change", function() { | |||
var country = slide.get_input("country").val(); | |||
var $timezone = slide.get_input("timezone"); | |||
var data = frappe.wiz.regional_data; | |||
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); | |||
$timezone.empty(); | |||
var language_field = slide.get_field("language"); | |||
// add country specific timezones first | |||
if(country) { | |||
var timezone_list = data.country_info[country].timezones || []; | |||
$timezone.add_options(timezone_list.sort()); | |||
slide.get_field("currency").set_input(data.country_info[country].currency); | |||
slide.get_field("currency").$input.trigger("change"); | |||
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"); | |||
} | |||
}); | |||
}, | |||
load_regional_data: function(slide, callback) { | |||
frappe.call({ | |||
method:"frappe.geo.country_info.get_country_timezone_info", | |||
callback: function(data) { | |||
frappe.setup.data.regional_data = data.message; | |||
callback(slide); | |||
} | |||
}); | |||
}, | |||
// add all timezones at the end, so that user has the option to change it to any timezone | |||
$timezone.add_options([""].concat(data.all_timezones)); | |||
load_user_details: function(slide, callback) { | |||
frappe.call({ | |||
method: "frappe.desk.page.setup_wizard.setup_wizard.load_user_details", | |||
freeze: true, | |||
callback: function(r) { | |||
frappe.setup.data.full_name = r.message.full_name; | |||
frappe.setup.data.email = r.message.email; | |||
callback(slide); | |||
} | |||
}) | |||
}, | |||
slide.get_field("timezone").set_input($timezone.val()); | |||
setup_language_field: function(slide) { | |||
var language_field = slide.get_field("language"); | |||
language_field.df.options = frappe.setup.data.lang.languages; | |||
language_field.refresh(); | |||
}, | |||
// temporarily set date format | |||
frappe.boot.sysdefaults.date_format = (data.country_info[country].date_format | |||
|| "dd-mm-yyyy"); | |||
}); | |||
setup_region_fields: function(slide) { | |||
/* | |||
Set a slide's country, timezone and currency fields | |||
*/ | |||
var data = frappe.setup.data.regional_data; | |||
slide.get_input("currency").on("change", function() { | |||
var currency = slide.get_input("currency").val(); | |||
if (!currency) return; | |||
frappe.model.with_doc("Currency", currency, function() { | |||
frappe.provide("locals.:Currency." + currency); | |||
var currency_doc = frappe.model.get_doc("Currency", currency); | |||
var number_format = currency_doc.number_format; | |||
if (number_format==="#.###") { | |||
number_format = "#.###,##"; | |||
} else if (number_format==="#,###") { | |||
number_format = "#,###.##" | |||
} | |||
frappe.boot.sysdefaults.number_format = number_format; | |||
locals[":Currency"][currency] = $.extend({}, currency_doc); | |||
}); | |||
}); | |||
var country_field = slide.get_field('country'); | |||
slide.get_input("country").empty() | |||
.add_options([""].concat(Object.keys(data.country_info).sort())); | |||
slide.get_input("currency").empty() | |||
.add_options(frappe.utils.unique([""].concat($.map(data.country_info, | |||
function(opts, country) { return opts.currency; }))).sort()); | |||
slide.get_input("timezone").empty() | |||
.add_options([""].concat(data.all_timezones)); | |||
// set values if present | |||
if(frappe.wizard.values.country) { | |||
country_field.set_input(frappe.wizard.values.country); | |||
} else if (data.default_country) { | |||
country_field.set_input(data.default_country); | |||
} | |||
}, | |||
if(frappe.wizard.values.currency) { | |||
slide.get_field("currency").set_input(frappe.wizard.values.currency); | |||
} | |||
frappe.wiz.user = { | |||
domains: ["all"], | |||
title: __("The First User: You"), | |||
icon: "fa fa-user", | |||
fields: [ | |||
{"fieldname": "full_name", "label": __("Full Name"), "fieldtype": "Data", | |||
reqd:1}, | |||
{"fieldname": "email", "label": __("Email Address"), "fieldtype": "Data", | |||
reqd:1, "description": __("Login id"), "options":"Email"}, | |||
{"fieldname": "password", "label": __("Password"), "fieldtype": "Password", | |||
reqd:1}, | |||
{fieldtype:"Attach Image", fieldname:"attach_user", | |||
label: __("Attach Your Picture"), is_private: 0}, | |||
], | |||
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.password.$wrapper.toggle(false); | |||
slide.form.fields_dict.email.$wrapper.toggle(false); | |||
if(frappe.boot.user.first_name || frappe.boot.user.last_name) { | |||
slide.form.fields_dict.full_name.set_input( | |||
[frappe.boot.user.first_name, frappe.boot.user.last_name].join(' ').trim()); | |||
if(frappe.wizard.values.timezone) { | |||
slide.get_field("timezone").set_input(frappe.wizard.values.timezone); | |||
} | |||
country_field.df.description = 'fetching country...'; | |||
country_field.set_description(); | |||
// get location from IP (unreliable) | |||
frappe.call({ | |||
method:"frappe.desk.page.setup_wizard.setup_wizard.load_country", | |||
callback: function(r) { | |||
if(r.message) { | |||
slide.get_field("country").set_input(r.message); | |||
slide.get_input("country").trigger('change'); | |||
} | |||
country_field.df.description = ''; | |||
country_field.set_description(); | |||
} | |||
}); | |||
}, | |||
var user_image = frappe.get_cookie("user_image"); | |||
if(user_image) { | |||
var $attach_user = slide.form.fields_dict.attach_user.$wrapper; | |||
$attach_user.find(".missing-image").toggle(false); | |||
$attach_user.find("img").attr("src", decodeURIComponent(user_image)).toggle(true); | |||
bind_language_events: function(slide) { | |||
slide.get_input("language").unbind("change").on("change", function() { | |||
var lang = $(this).val() || "English"; | |||
frappe._messages = {}; | |||
frappe.call({ | |||
method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages", | |||
freeze: true, | |||
args: { | |||
language: lang | |||
}, | |||
callback: function(r) { | |||
frappe.setup._from_load_messages = true; | |||
frappe.wizard.refresh_slides(); | |||
} | |||
}); | |||
}); | |||
}, | |||
delete slide.form.fields_dict.email; | |||
delete slide.form.fields_dict.password; | |||
bind_region_events: function(slide) { | |||
/* | |||
Bind a slide's country, timezone and currency fields | |||
*/ | |||
slide.get_input("country").on("change", function() { | |||
var country = slide.get_input("country").val(); | |||
var $timezone = slide.get_input("timezone"); | |||
var data = frappe.setup.data.regional_data; | |||
$timezone.empty(); | |||
// add country specific timezones first | |||
if(country) { | |||
var timezone_list = data.country_info[country].timezones || []; | |||
$timezone.add_options(timezone_list.sort()); | |||
slide.get_field("currency").set_input(data.country_info[country].currency); | |||
slide.get_field("currency").$input.trigger("change"); | |||
} | |||
}, | |||
css_class: "single-column" | |||
}; | |||
// add all timezones at the end, so that user has the option to change it to any timezone | |||
$timezone.add_options([""].concat(data.all_timezones)); | |||
slide.get_field("timezone").set_input($timezone.val()); | |||
// temporarily set date format | |||
frappe.boot.sysdefaults.date_format = (data.country_info[country].date_format | |||
|| "dd-mm-yyyy"); | |||
}); | |||
slide.get_input("currency").on("change", function() { | |||
var currency = slide.get_input("currency").val(); | |||
if (!currency) return; | |||
frappe.model.with_doc("Currency", currency, function() { | |||
frappe.provide("locals.:Currency." + currency); | |||
var currency_doc = frappe.model.get_doc("Currency", currency); | |||
var number_format = currency_doc.number_format; | |||
if (number_format==="#.###") { | |||
number_format = "#.###,##"; | |||
} else if (number_format==="#,###") { | |||
number_format = "#,###.##" | |||
} | |||
frappe.boot.sysdefaults.number_format = number_format; | |||
locals[":Currency"][currency] = $.extend({}, currency_doc); | |||
}); | |||
}); | |||
}, | |||
} | |||
frappe.wiz.on("before_load", function() { | |||
load_frappe_slides(); | |||
frappe.setup.on("before_load", function() { | |||
// load slides | |||
frappe_slides.map(frappe.setup.add_slide); | |||
// add welcome slide | |||
frappe.wiz.add_slide(frappe.wiz.welcome); | |||
frappe.wiz.add_slide(frappe.wiz.region); | |||
frappe.wiz.add_slide(frappe.wiz.user); | |||
// set header image | |||
let $icon = $('header .setup-wizard-brand'); | |||
if($icon.length === 0) { | |||
$('header').append(`<div class="setup-wizard-brand""> | |||
<img src="/assets/frappe/images/frappe-bird-grey.svg" | |||
class="brand-icon frappe-icon" style="width:36px;"></div>`); | |||
} | |||
}); |
@@ -179,11 +179,27 @@ def load_messages(language): | |||
@frappe.whitelist() | |||
def load_languages(): | |||
language_codes = frappe.db.sql('select language_code, language_name from tabLanguage order by name', as_dict=True) | |||
codes_to_names = {} | |||
for d in language_codes: | |||
codes_to_names[d.language_code] = d.language_name | |||
return { | |||
"default_language": frappe.db.get_value('Language', frappe.local.lang, 'language_name') or frappe.local.lang, | |||
"languages": sorted(frappe.db.sql_list('select language_name from tabLanguage order by name')) | |||
"languages": sorted(frappe.db.sql_list('select language_name from tabLanguage order by name')), | |||
"codes_to_names": codes_to_names | |||
} | |||
@frappe.whitelist() | |||
def load_country(): | |||
from frappe.sessions import get_geo_ip_country | |||
return get_geo_ip_country(frappe.local.request_ip) if frappe.local.request_ip else None | |||
@frappe.whitelist() | |||
def load_user_details(): | |||
return { | |||
"full_name": frappe.cache().hget("full_name", "signup"), | |||
"email": frappe.cache().hget("email", "signup") | |||
} | |||
def prettify_args(args): | |||
# remove attachments | |||
@@ -1,14 +1,18 @@ | |||
<div class="container setup-wizard-slide {%= css_class %} with-form" data-slide-name="{%= name %}"> | |||
<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++) { %} | |||
<i class="fa fa-fw fa-circle{% if (i+1<=step) { %} active {% } %}"></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="text-center lead">{%= title %}</p> | |||
<p class="lead">{%= title %}</p> | |||
<div class="row"> | |||
<div class="col-sm-12"> | |||
<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"> | |||
@@ -0,0 +1,7 @@ | |||
# Automated Testing | |||
Frappé Provides you a test framework to write and execute tests that can be run directly on a Continuous Integration Tool like Travis | |||
You can write server-side unit tests or UI tests | |||
{index} |
@@ -0,0 +1,3 @@ | |||
unit-testing | |||
integration-testing | |||
qunit-testing |
@@ -0,0 +1,49 @@ | |||
# UI Integration Testing | |||
You can write integration tests using the Selenium Driver. `frappe.utils.selenium_driver` gives you a friendly API to write selenium based tests | |||
To write integration tests, create a standard test case by creating a python file starting with `test_` | |||
All integration tests will be run at the end of the unittests. | |||
### Example | |||
Here is an example of an integration test to check insertion of a To Do | |||
from __future__ import print_function | |||
from frappe.utils.selenium_testdriver import TestDriver | |||
import unittest | |||
import time | |||
class TestToDo(unittest.TestCase): | |||
def setUp(self): | |||
self.driver = TestDriver() | |||
def test_todo(self): | |||
self.driver.login() | |||
# list view | |||
self.driver.set_route('List', 'ToDo') | |||
time.sleep(2) | |||
# new | |||
self.driver.click_primary_action() | |||
time.sleep(2) | |||
# set input | |||
self.driver.set_text_editor('description', 'hello') | |||
# save | |||
self.driver.click_modal_primary_action() | |||
time.sleep(2) | |||
self.assertTrue(self.driver.get_visible_element('.result-list') | |||
.find_element_by_css_selector('.list-item') | |||
.find_element_by_css_selector('.list-id').text=='hello') | |||
def tearDown(self): | |||
self.driver.close() | |||
@@ -0,0 +1,46 @@ | |||
# UI Testing with Frappe API | |||
You can either write integration tests, or directly write tests in Javascript using [QUnit](http://api.qunitjs.com/) | |||
QUnit helps you write UI tests using the UQuit framework and native frappe API. As you might have guessed, this is a much faster way of writing tests. | |||
### Test Runner | |||
To write QUnit based tests, add your tests in the `tests/ui` folder of your application. Your test files must begin with `test_` and end with `.js` extension. | |||
To run your files, you can use the **Test Runner**. The **Test Runner** gives a user interface to load all your QUnit tests and run them in the browser. | |||
In the CI, all QUnit tests are run by the **Test Runner** using `frappe/tests/test_test_runner.py` | |||
<img src="{{docs_base_url}}/assets/img/app-development/test-runner.png" class="screenshot"> | |||
### Example QUnit Test | |||
Here is the example of the To Do test in QUnit | |||
QUnit.test("test quick entry", function(assert) { | |||
assert.expect(2); | |||
let done = assert.async(); | |||
let random = frappe.utils.get_random(10); | |||
frappe.set_route('List', 'ToDo') | |||
.then(() => { | |||
return frappe.new_doc('ToDo'); | |||
}) | |||
.then(() => { | |||
frappe.quick_entry.dialog.set_value('description', random); | |||
return frappe.quick_entry.insert(); | |||
}) | |||
.then((doc) => { | |||
assert.ok(doc && !doc.__islocal); | |||
return frappe.set_route('Form', 'ToDo', doc.name); | |||
}) | |||
.then(() => { | |||
assert.ok(cur_frm.doc.description.includes(random)); | |||
done(); | |||
}); | |||
}); | |||
### Writing Test Friendly Code with Promises | |||
Promises are a great way to write test-friendly code. If your method calls an aysnchronous call (ajax), then you should return an `Promise` object. While writing tests, if you encounter a function that does not return a `Promise` object, you should update the code to return a `Promise` object. |
@@ -1,4 +1,4 @@ | |||
# Writing Tests Guide | |||
# Unit Testing | |||
## 1.Introduction | |||
@@ -16,12 +16,12 @@ Frappe provides some basic tooling to quickly write automated tests. There are s | |||
This function will build all the test dependencies and run your tests. | |||
You should run tests from "frappe_bench" folder. Without options all tests will be run. | |||
bench run-tests | |||
bench run-tests | |||
If you need more information about test execution - you can use verbose log level for bench. | |||
bench --verbose run-tests | |||
### Options: | |||
--app <AppName> | |||
@@ -30,9 +30,9 @@ If you need more information about test execution - you can use verbose log leve | |||
--module <Module> (Run a particular module that has tests) | |||
--profile (Runs a Python profiler on the test) | |||
--junit-xml-output<PathToXML> (The command provides test results in the standard XUnit XML format) | |||
#### 2.1. Example for app: | |||
All applications are located in folder: "~/frappe-bench/apps". | |||
All applications are located in folder: "~/frappe-bench/apps". | |||
We can run tests for each application. | |||
- frappe-bench/apps/erpnext/ | |||
@@ -50,7 +50,7 @@ We can run tests for each application. | |||
. | |||
---------------------------------------------------------------------- | |||
Ran 1 test in 0.008s | |||
OK | |||
#### 2.3. Example for test: | |||
@@ -60,44 +60,44 @@ Run a specific case in User: | |||
. | |||
---------------------------------------------------------------------- | |||
Ran 1 test in 0.005s | |||
OK | |||
#### 2.4. Example for module: | |||
If we want to run tests in the module: | |||
/home/frappe/frappe-bench/apps/erpnext/erpnext/support/doctype/issue/test_issue.py | |||
We should use module name like this (related to application folder) | |||
erpnext.support.doctype.issue.test_issue | |||
#####EXAMPLE: | |||
frappe@erpnext:~/frappe-bench$ bench run-tests --module "erpnext.stock.doctype.stock_entry.test_stock_entry" | |||
........................... | |||
---------------------------------------------------------------------- | |||
Ran 27 tests in 30.549s | |||
#### 2.5. Example for profile: | |||
frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost" --profile | |||
. | |||
---------------------------------------------------------------------- | |||
Ran 1 test in 0.010s | |||
OK | |||
9133 function calls (8912 primitive calls) in 0.011 seconds | |||
Ordered by: cumulative time | |||
ncalls tottime percall cumtime percall filename:lineno(function) | |||
2 0.000 0.000 0.008 0.004 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:187(insert) | |||
1 0.000 0.000 0.003 0.003 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:386(_validate) | |||
13 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/database.py:77(sql) | |||
255 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py:91(get) | |||
12 0.000 0.000 0.002 0.000 | |||
12 0.000 0.000 0.002 0.000 | |||
#### 2.6. Example for XUnit XML: | |||
@@ -118,7 +118,7 @@ We should use module name like this (related to application folder) | |||
It’s designed for the CI Jenkins, but will work for anything else that understands an XUnit-formatted XML representation of test results. | |||
#### Jenkins configuration support: | |||
1. You should install xUnit plugin - https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin | |||
1. You should install xUnit plugin - https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin | |||
2. After installation open Jenkins job configuration, click the box named “Publish JUnit test result report” under the "Post-build Actions" and enter path to XML report: | |||
(Example: _reports/*.xml_) | |||
@@ -197,9 +197,3 @@ It’s designed for the CI Jenkins, but will work for anything else that underst | |||
self.assertTrue("_Test Event 3" in subjects) | |||
self.assertFalse("_Test Event 2" in subjects) | |||
## 4. Client Side Testing (Using Selenium) | |||
This feature is still under development. | |||
For an example see, [https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py](https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py) |
@@ -585,7 +585,7 @@ | |||
"label": "Format", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "HTML\nXLS\nCSV", | |||
"options": "HTML\nXLSX\nCSV", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
@@ -669,7 +669,7 @@ | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-04-25 03:31:55.214149", | |||
"modified": "2017-06-30 12:54:13.350902", | |||
"modified_by": "Administrator", | |||
"module": "Email", | |||
"name": "Auto Email Report", | |||
@@ -8,7 +8,7 @@ from frappe import _ | |||
from frappe.model.document import Document | |||
from datetime import timedelta | |||
import frappe.utils | |||
from frappe.utils.xlsutils import get_xls | |||
from frappe.utils.xlsxutils import make_xlsx | |||
from frappe.utils.csvutils import to_csv | |||
max_reports_per_user = 3 | |||
@@ -43,7 +43,7 @@ class AutoEmailReport(Document): | |||
def validate_report_format(self): | |||
""" check if user has select correct report format """ | |||
valid_report_formats = ["HTML", "XLS", "CSV"] | |||
valid_report_formats = ["HTML", "XLSX", "CSV"] | |||
if self.format not in valid_report_formats: | |||
frappe.throw(_("%s is not a valid report format. Report format should \ | |||
one of the following %s"%(frappe.bold(self.format), frappe.bold(", ".join(valid_report_formats))))) | |||
@@ -70,11 +70,14 @@ class AutoEmailReport(Document): | |||
if self.format == 'HTML': | |||
return self.get_html_table(columns, data) | |||
elif self.format == 'XLS': | |||
return get_xls(columns, data) | |||
elif self.format == 'XLSX': | |||
spreadsheet_data = self.get_spreadsheet_data(columns, data) | |||
xlsx_file = make_xlsx(spreadsheet_data, "Auto Email Report") | |||
return xlsx_file.getvalue() | |||
elif self.format == 'CSV': | |||
return self.get_csv(columns, data) | |||
spreadsheet_data = self.get_spreadsheet_data(columns, data) | |||
return to_csv(spreadsheet_data) | |||
else: | |||
frappe.throw(_('Invalid Output Format')) | |||
@@ -85,7 +88,8 @@ class AutoEmailReport(Document): | |||
'data': data | |||
}) | |||
def get_csv(self, columns, data): | |||
@staticmethod | |||
def get_spreadsheet_data(columns, data): | |||
out = [[df.label for df in columns], ] | |||
for row in data: | |||
new_row = [] | |||
@@ -93,7 +97,7 @@ class AutoEmailReport(Document): | |||
for df in columns: | |||
new_row.append(frappe.format(row[df.fieldname], df, row)) | |||
return to_csv(out) | |||
return out | |||
def get_file_name(self): | |||
return "{0}.{1}".format(self.report.replace(" ", "-").replace("/", "-"), self.format.lower()) | |||
@@ -33,7 +33,7 @@ class TestAutoEmailReport(unittest.TestCase): | |||
data = auto_email_report.get_report_content() | |||
self.assertTrue('"Language","Core"' in data) | |||
auto_email_report.format = 'XLS' | |||
auto_email_report.format = 'XLSX' | |||
data = auto_email_report.get_report_content() | |||
@@ -298,8 +298,13 @@ def get_footer(email_account, footer=None): | |||
company_address = frappe.db.get_default("email_footer_address") | |||
if company_address: | |||
footer += '<div style="margin: 15px auto; text-align: center; color: #8d99a6">{0}</div>'\ | |||
.format(company_address.replace("\n", "<br>")) | |||
company_address = company_address.splitlines(True) | |||
footer += '<table width="100%" border=0>' | |||
footer += '<tr><td height=20></td></tr>' | |||
for x in company_address: | |||
footer += '<tr style="margin: 15px auto; text-align: center; color: #8d99a6"><td>{0}</td></tr>'\ | |||
.format(x) | |||
footer += "</table>" | |||
if not cint(frappe.db.get_default("disable_standard_email_footer")): | |||
for default_mail_footer in frappe.get_hooks("default_mail_footer"): | |||
@@ -272,6 +272,9 @@ def set_dropbox_access_token(access_token): | |||
frappe.db.commit() | |||
def generate_oauth2_access_token_from_oauth1_token(dropbox_settings=None): | |||
if not dropbox_settings.get("access_key") or not dropbox_settings.get("access_secret"): | |||
return {} | |||
url = "https://api.dropboxapi.com/2/auth/token/from_oauth1" | |||
headers = {"Content-Type": "application/json"} | |||
auth = (dropbox_settings["app_key"], dropbox_settings["app_secret"]) | |||
@@ -423,12 +423,11 @@ class BaseDocument(object): | |||
return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label)) | |||
elif self.parentfield: | |||
return "{}: {} {} #{}: {}: {}".format(_("Error"), frappe.bold(_(self.doctype)), | |||
_("Row"), self.idx, _("Value missing for"), _(df.label)) | |||
else: | |||
return "{}: {}: {}".format(_("Error"), _("Value missing for"), _(df.label)) | |||
return _("Error: Value missing for {0}: {1}").format(_(df.parent), _(df.label)) | |||
missing = [] | |||
@@ -423,7 +423,6 @@ class DatabaseQuery(object): | |||
def add_user_permissions(self, user_permissions, user_permission_doctypes=None): | |||
user_permission_doctypes = frappe.permissions.get_user_permission_doctypes(user_permission_doctypes, user_permissions) | |||
meta = frappe.get_meta(self.doctype) | |||
for doctypes in user_permission_doctypes: | |||
match_filters = {} | |||
match_conditions = [] | |||
@@ -431,12 +430,18 @@ class DatabaseQuery(object): | |||
for df in meta.get_fields_to_check_permissions(doctypes): | |||
user_permission_values = user_permissions.get(df.options, []) | |||
condition = 'ifnull(`tab{doctype}`.`{fieldname}`, "")=""'.format(doctype=self.doctype, fieldname=df.fieldname) | |||
cond = 'ifnull(`tab{doctype}`.`{fieldname}`, "")=""'.format(doctype=self.doctype, fieldname=df.fieldname) | |||
if user_permission_values: | |||
condition += """ or `tab{doctype}`.`{fieldname}` in ({values})""".format( | |||
if not cint(frappe.get_system_settings("apply_strict_user_permissions")): | |||
condition = cond + " or " | |||
else: | |||
condition = "" | |||
condition += """`tab{doctype}`.`{fieldname}` in ({values})""".format( | |||
doctype=self.doctype, fieldname=df.fieldname, | |||
values=", ".join([('"'+frappe.db.escape(v, percent=False)+'"') for v in user_permission_values]) | |||
) | |||
values=", ".join([('"'+frappe.db.escape(v, percent=False)+'"') for v in user_permission_values])) | |||
else: | |||
condition = cond | |||
match_conditions.append("({condition})".format(condition=condition)) | |||
match_filters[df.options] = user_permission_values | |||
@@ -563,6 +563,13 @@ def validate_column_name(n): | |||
frappe.throw(_("Fieldname {0} cannot have special characters like {1}").format(cstr(n), special_characters), InvalidColumnName) | |||
return n | |||
def validate_column_length(fieldname): | |||
""" In MySQL maximum column length is 64 characters, | |||
ref: https://dev.mysql.com/doc/refman/5.5/en/identifiers.html""" | |||
if len(fieldname) > 64: | |||
frappe.throw(_("Fieldname is limited to 64 characters ({0})").format(fieldname)) | |||
def remove_all_foreign_keys(): | |||
frappe.db.sql("set foreign_key_checks = 0") | |||
frappe.db.commit() | |||
@@ -162,6 +162,7 @@ def update_link_field_values(link_fields, old, new, doctype): | |||
single_doc.set(field['fieldname'], new) | |||
# update single docs using ORM rather then query | |||
# as single docs also sometimes sets defaults! | |||
single_doc.flags.ignore_mandatory = True | |||
single_doc.save(ignore_permissions=True) | |||
except ImportError: | |||
# fails in patches where the doctype has been renamed | |||
@@ -180,8 +180,8 @@ def load_doctype_module(doctype, module=None, prefix="", suffix=""): | |||
try: | |||
if key not in doctype_python_modules: | |||
doctype_python_modules[key] = frappe.get_module(module_name) | |||
except ImportError: | |||
raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name)) | |||
except ImportError, e: | |||
raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name + ' Error: ' + str(e))) | |||
return doctype_python_modules[key] | |||
@@ -1,12 +0,0 @@ | |||
var chromedriver = require('chromedriver'); | |||
module.exports = { | |||
before: function (done) { | |||
chromedriver.start(); | |||
done(); | |||
}, | |||
after: function (done) { | |||
chromedriver.stop(); | |||
done(); | |||
} | |||
}; |
@@ -1,96 +0,0 @@ | |||
const fs = require('fs'); | |||
const ci_mode = get_cli_arg('env') === 'ci_server'; | |||
const site_name = get_cli_arg('site'); | |||
let app_name = get_cli_arg('app'); | |||
if(!app_name) { | |||
console.log('Please specify app to run tests'); | |||
return; | |||
} | |||
if(!ci_mode && !site_name) { | |||
console.log('Please specify site to run tests'); | |||
return; | |||
} | |||
// site url | |||
let site_url; | |||
if(site_name) { | |||
site_url = 'http://' + site_name + ':' + get_port(); | |||
} | |||
// multiple apps | |||
if(app_name.includes(',')) { | |||
app_name = app_name.split(','); | |||
} else { | |||
app_name = [app_name]; | |||
} | |||
let test_folders = []; | |||
let page_objects = []; | |||
for(const app of app_name) { | |||
const test_folder = `apps/${app}/${app}/tests/ui`; | |||
const page_object = test_folder + '/page_objects'; | |||
if(!fs.existsSync(test_folder)) { | |||
console.log(`No test folder found for "${app}"`); | |||
continue; | |||
} | |||
test_folders.push(test_folder); | |||
if(fs.existsSync(page_object)) { | |||
page_objects.push(); | |||
} | |||
} | |||
const config = { | |||
"src_folders": test_folders, | |||
"globals_path" : 'apps/frappe/frappe/nightwatch.global.js', | |||
"page_objects_path": page_objects, | |||
"selenium": { | |||
"start_process": false | |||
}, | |||
"test_settings": { | |||
"default": { | |||
"launch_url": site_url, | |||
"selenium_port": 9515, | |||
"selenium_host": "127.0.0.1", | |||
"default_path_prefix": "", | |||
"silent": true, | |||
// "screenshots": { | |||
// "enabled": true, | |||
// "path": SCREENSHOT_PATH | |||
// }, | |||
"globals": { | |||
"waitForConditionTimeout": 15000 | |||
}, | |||
"desiredCapabilities": { | |||
"browserName": "chrome", | |||
"chromeOptions": { | |||
"args": ["--no-sandbox", "--start-maximized"] | |||
}, | |||
"javascriptEnabled": true, | |||
"acceptSslCerts": true | |||
} | |||
}, | |||
"ci_server": { | |||
"launch_url": 'http://localhost:8000' | |||
} | |||
} | |||
} | |||
module.exports = config; | |||
function get_cli_arg(key) { | |||
var args = process.argv; | |||
var i = args.indexOf('--' + key); | |||
if(i === -1) { | |||
return null; | |||
} | |||
return args[i + 1]; | |||
} | |||
function get_port() { | |||
var bench_config = JSON.parse(fs.readFileSync('sites/common_site_config.json')); | |||
return bench_config.webserver_port; | |||
} |
@@ -186,3 +186,4 @@ frappe.patches.v8_0.update_desktop_icons | |||
frappe.patches.v8_0.update_gender_and_salutation | |||
execute:frappe.db.sql('update tabReport set module="Desk" where name="ToDo"') | |||
frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings | |||
frappe.patches.v8_1.update_format_options_in_auto_email_report |
@@ -0,0 +1,16 @@ | |||
# Copyright (c) 2017, Frappe and Contributors | |||
# License: GNU General Public License v3. See license.txt | |||
from __future__ import unicode_literals | |||
import frappe | |||
def execute(): | |||
""" change the XLS option as XLSX in the auto email report """ | |||
frappe.reload_doc("email", "doctype", "auto_email_report") | |||
auto_email_list = frappe.get_all("Auto Email Report", filters={"format": "XLS"}) | |||
for auto_email in auto_email_list: | |||
doc = frappe.get_doc("Auto Email Report", auto_email.name) | |||
doc.format = "XLSX" | |||
doc.save() |
@@ -1,6 +1,6 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# See license.txt | |||
from __future__ import unicode_literals | |||
from __future__ import unicode_literals, print_function | |||
import frappe | |||
import unittest | |||
@@ -101,6 +101,7 @@ | |||
"public/js/frappe/ui/page.html", | |||
"public/js/frappe/ui/page.js", | |||
"public/js/frappe/ui/find.js", | |||
"public/js/frappe/ui/iconbar.js", | |||
"public/js/frappe/form/layout.js", | |||
"public/js/frappe/ui/field_group.js", | |||
@@ -194,6 +195,8 @@ | |||
"public/js/frappe/form/save.js", | |||
"public/js/frappe/form/script_manager.js", | |||
"public/js/frappe/form/grid.js", | |||
"public/js/frappe/form/grid_row.js", | |||
"public/js/frappe/form/grid_row_form.js", | |||
"public/js/frappe/form/linked_with.js", | |||
"public/js/frappe/form/workflow.js", | |||
"public/js/frappe/form/print.js", | |||
@@ -496,6 +496,7 @@ h6.uppercase, | |||
} | |||
.like-disabled-input.for-description { | |||
font-weight: normal; | |||
font-size: 12px; | |||
} | |||
.frappe-control { | |||
margin-bottom: 10px; | |||
@@ -530,6 +531,18 @@ select.form-control { | |||
font-weight: bold; | |||
background-color: #fffdf4; | |||
} | |||
.form-control[data-fieldtype="Password"] { | |||
position: inherit; | |||
} | |||
.password-strength-indicator { | |||
float: right; | |||
padding: 15px; | |||
margin-top: -41px; | |||
margin-right: -7px; | |||
} | |||
.password-strength-message { | |||
margin-top: -10px; | |||
} | |||
.form-headline { | |||
padding: 0px 15px; | |||
margin: 0px; | |||
@@ -195,6 +195,23 @@ frappe.ellipsis = function(text, max) { | |||
return text; | |||
}; | |||
frappe.run_serially = function(tasks) { | |||
var result = Promise.resolve(); | |||
tasks.forEach(task => { | |||
if(task) { | |||
result = result.then ? result.then(task) : Promise.resolve(); | |||
} | |||
}); | |||
return result; | |||
}; | |||
frappe.timeout = seconds => { | |||
return new Promise((resolve) => { | |||
setTimeout(() => resolve(), seconds * 1000); | |||
}); | |||
}; | |||
frappe.get_modal = function(title, content) { | |||
return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body); | |||
}; | |||
@@ -98,32 +98,49 @@ frappe.ui.form.Control = Class.extend({ | |||
} | |||
}, | |||
set_value: function(value) { | |||
this.parse_validate_and_set_in_model(value); | |||
return this.validate_and_set_in_model(value); | |||
}, | |||
parse_validate_and_set_in_model: function(value, e) { | |||
if(this.parse) { | |||
value = this.parse(value); | |||
} | |||
return this.validate_and_set_in_model(value, e); | |||
}, | |||
validate_and_set_in_model: function(value, e) { | |||
var me = this; | |||
if(this.inside_change_event) return; | |||
this.inside_change_event = true; | |||
if(this.parse) value = this.parse(value); | |||
var set = function(value) { | |||
me.set_model_value(value); | |||
me.inside_change_event = false; | |||
me.set_mandatory && me.set_mandatory(value); | |||
if(me.df.change || me.df.onchange) { | |||
// onchange event specified in df | |||
(me.df.change || me.df.onchange).apply(me, [e]); | |||
return new Promise(resolve => { | |||
if(this.inside_change_event) { | |||
resolve(); | |||
return; | |||
} | |||
this.inside_change_event = true; | |||
var set = function(value) { | |||
me.inside_change_event = false; | |||
me.set_model_value(value) | |||
.then(() => { | |||
me.set_mandatory && me.set_mandatory(value); | |||
if(me.df.change || me.df.onchange) { | |||
// onchange event specified in df | |||
let _promise = (me.df.change || me.df.onchange).apply(me, [e]); | |||
if(_promise && _promise.then) { | |||
_promise.then(() => { resolve(); }); | |||
} else { | |||
resolve(); | |||
} | |||
} else { | |||
resolve(); | |||
} | |||
}); | |||
} | |||
} | |||
this.validate ? this.validate(value, set) : set(value); | |||
this.validate ? this.validate(value, set) : set(value); | |||
}); | |||
}, | |||
get_parsed_value: function() { | |||
var me = this; | |||
get_value: function() { | |||
if(this.get_status()==='Write') { | |||
return this.get_value ? | |||
(this.parse ? this.parse(this.get_value()) : this.get_value()) : | |||
return this.get_input_value ? | |||
(this.parse ? this.parse(this.get_input_value()) : this.get_input_value()) : | |||
undefined; | |||
} else if(this.get_status()==='Read') { | |||
return this.value || undefined; | |||
@@ -132,17 +149,20 @@ frappe.ui.form.Control = Class.extend({ | |||
} | |||
}, | |||
set_model_value: function(value) { | |||
if(this.doctype && this.docname) { | |||
if(frappe.model.set_value(this.doctype, this.docname, this.df.fieldname, | |||
value, this.df.fieldtype)) { | |||
return new Promise(resolve => { | |||
if(this.doctype && this.docname) { | |||
frappe.model.set_value(this.doctype, this.docname, this.df.fieldname, | |||
value, this.df.fieldtype) | |||
.then(() => resolve()); | |||
this.last_value = value; | |||
} else { | |||
if(this.doc) { | |||
this.doc[this.df.fieldname] = value; | |||
} | |||
this.set_input(value); | |||
resolve(); | |||
} | |||
} else { | |||
if(this.doc) { | |||
this.doc[this.df.fieldname] = value; | |||
} | |||
this.set_input(value); | |||
} | |||
}); | |||
}, | |||
set_focus: function() { | |||
if(this.$input) { | |||
@@ -195,7 +215,6 @@ frappe.ui.form.ControlImage = frappe.ui.form.Control.extend({ | |||
this.$body = $("<div></div>").appendTo(this.$wrapper) | |||
.css({"margin-bottom": "10px"}) | |||
this.$wrapper.on("refresh", function() { | |||
var doc = null; | |||
me.$body.empty(); | |||
var doc = me.get_doc(); | |||
@@ -324,7 +343,7 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ | |||
}, | |||
set_disp_area: function() { | |||
let value = this.get_value(); | |||
let value = this.get_input_value(); | |||
if(in_list(["Currency", "Int", "Float"], this.df.fieldtype) && (this.value === 0 || value === 0)) { | |||
// to set the 0 value in readonly for currency, int, float field | |||
value = 0; | |||
@@ -333,13 +352,13 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ | |||
} | |||
this.disp_area && $(this.disp_area) | |||
.html(frappe.format(value, this.df, {no_icon:true, inline:true}, | |||
this.doc || (this.frm && this.frm.doc))); | |||
this.doc || (this.frm && this.frm.doc))); | |||
}, | |||
bind_change_event: function() { | |||
var me = this; | |||
this.$input && this.$input.on("change", this.change || function(e) { | |||
me.parse_validate_and_set_in_model(me.get_value(), e); | |||
me.parse_validate_and_set_in_model(me.get_input_value(), e); | |||
}); | |||
}, | |||
bind_focusout: function() { | |||
@@ -445,7 +464,7 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ | |||
set_formatted_input: function(value) { | |||
this.$input && this.$input.val(this.format_for_input(value)); | |||
}, | |||
get_value: function() { | |||
get_input_value: function() { | |||
return this.$input ? this.$input.val() : undefined; | |||
}, | |||
format_for_input: function(val) { | |||
@@ -517,9 +536,57 @@ frappe.ui.form.ControlReadOnly = frappe.ui.form.ControlData.extend({ | |||
}, | |||
}); | |||
frappe.ui.form.ControlPassword = frappe.ui.form.ControlData.extend({ | |||
input_type: "password" | |||
input_type: "password", | |||
make: function() { | |||
this._super(); | |||
}, | |||
make_input: function() { | |||
var me = this; | |||
this._super(); | |||
this.$input.parent().append($('<span class="password-strength-indicator indicator"></span>')); | |||
this.$wrapper.find('.control-input-wrapper').append($('<p class="password-strength-message text-muted small hidden"></p>')); | |||
this.indicator = this.$wrapper.find('.password-strength-indicator'); | |||
this.message = this.$wrapper.find('.help-box'); | |||
this.$input.on('input', () => { | |||
var $this = $(this); | |||
clearTimeout($this.data('timeout')); | |||
$this.data('timeout', setTimeout(() => { | |||
var txt = me.$input.val(); | |||
me.get_password_strength(txt); | |||
}), 300); | |||
}); | |||
}, | |||
get_password_strength: function(value) { | |||
var me = this; | |||
frappe.call({ | |||
type: 'GET', | |||
method: 'frappe.core.doctype.user.user.test_password_strength', | |||
args: { | |||
new_password: value || '' | |||
}, | |||
callback: function(r) { | |||
if (r.message && r.message.entropy) { | |||
var score = r.message.score, | |||
feedback = r.message.feedback; | |||
feedback.crack_time_display = r.message.crack_time_display; | |||
var indicators = ['grey', 'red', 'orange', 'yellow', 'green']; | |||
me.set_strength_indicator(indicators[score]); | |||
} | |||
} | |||
}); | |||
}, | |||
set_strength_indicator: function(color) { | |||
var message = __("Include symbols, numbers and capital letters in the password"); | |||
this.indicator.removeClass().addClass('password-strength-indicator indicator ' + color); | |||
this.message.html(message).removeClass('hidden'); | |||
} | |||
}); | |||
frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({ | |||
@@ -644,14 +711,11 @@ frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({ | |||
this.$input.on("keydown", function(e) { | |||
if(e.which===84) { // 84 === t | |||
if(me.df.fieldtype=='Date') { | |||
me.set_value(frappe.datetime.str_to_user( | |||
frappe.datetime.nowdate())); | |||
me.set_value(frappe.datetime.nowdate()); | |||
} if(me.df.fieldtype=='Datetime') { | |||
me.set_value(frappe.datetime.str_to_user( | |||
frappe.datetime.now_datetime())); | |||
me.set_value(frappe.datetime.now_datetime()); | |||
} if(me.df.fieldtype=='Time') { | |||
me.set_value(frappe.datetime.str_to_user( | |||
frappe.datetime.now_time())); | |||
me.set_value(frappe.datetime.now_time()); | |||
} | |||
return false; | |||
} | |||
@@ -840,7 +904,7 @@ frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({ | |||
this._super(); | |||
this.$input.removeClass("form-control"); | |||
}, | |||
parse: function(value) { | |||
get_input_value: function() { | |||
return this.input.checked ? 1 : 0; | |||
}, | |||
validate: function(value, callback) { | |||
@@ -854,7 +918,7 @@ frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({ | |||
this.set_mandatory(value); | |||
this.set_disp_area(); | |||
}, | |||
get_value: function() { | |||
get_input_value: function() { | |||
if (!this.$input) { | |||
return; | |||
} | |||
@@ -878,7 +942,7 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({ | |||
}, | |||
onclick: function() { | |||
if(this.frm && this.frm.doc) { | |||
if(this.frm.script_manager.get_handlers(this.df.fieldname, this.doctype, this.docname).length) { | |||
if(this.frm.script_manager.has_handlers(this.df.fieldname, this.doctype)) { | |||
this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname); | |||
} else { | |||
this.frm.runscript(this.df.options, this); | |||
@@ -1094,31 +1158,55 @@ frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({ | |||
make: function() { | |||
var me = this; | |||
this._super(); | |||
this.img_wrapper = $('<div style="margin: 7px 0px;">\ | |||
<div class="missing-image attach-missing-image"><i class="octicon octicon-circle-slash"></i></div></div>') | |||
this.img_wrapper = $('<div style="width: 100%; height: calc(100% - 40px); position: relative;">\ | |||
<div class="missing-image attach-missing-image"><i class="octicon octicon-device-camera"></i></div></div>') | |||
.appendTo(this.wrapper); | |||
this.img = $("<img class='img-responsive attach-image-display'>") | |||
.appendTo(this.img_wrapper).toggle(false); | |||
this.img_container = $(`<div class='img-container'></div>`); | |||
this.img = $(`<img class='img-responsive attach-image-display'>`) | |||
.appendTo(this.img_container); | |||
this.img_overlay = $(`<div class='img-overlay'> | |||
<span class="overlay-text">Change</span> | |||
</div>`).appendTo(this.img_container); | |||
this.remove_image_link = $('<a style="font-size: 12px;color: #8D99A6;">Remove</a>'); | |||
this.img_wrapper.append(this.img_container).append(this.remove_image_link); | |||
// this.img.toggle(false); | |||
// this.img_overlay.toggle(false); | |||
this.img_container.toggle(false); | |||
this.remove_image_link.toggle(false); | |||
// propagate click to Attach button | |||
this.img_wrapper.find(".missing-image").on("click", function() { me.$input.click(); }); | |||
this.img.on("click", function() { me.$input.click(); }); | |||
this.img_container.on("click", function() { me.$input.click(); }); | |||
this.remove_image_link.on("click", function() { me.$value.find(".close").click(); }); | |||
this.$wrapper.on("refresh", function() { | |||
$(me.wrapper).find('.btn-attach').addClass('hidden'); | |||
me.set_image(); | |||
if(me.get_status()=="Read") { | |||
$(me.disp_area).toggle(false); | |||
} | |||
}); | |||
this.set_image(); | |||
}, | |||
set_image: function() { | |||
if(this.get_value()) { | |||
$(this.img_wrapper).find(".missing-image").toggle(false); | |||
this.img.attr("src", this.dataurl ? this.dataurl : this.value).toggle(true); | |||
// this.img.attr("src", this.dataurl ? this.dataurl : this.value).toggle(true); | |||
// this.img_overlay.toggle(true); | |||
this.img.attr("src", this.dataurl ? this.dataurl : this.value); | |||
this.img_container.toggle(true); | |||
this.remove_image_link.toggle(true); | |||
} else { | |||
$(this.img_wrapper).find(".missing-image").toggle(true); | |||
this.img.toggle(false); | |||
// this.img.toggle(false); | |||
// this.img_overlay.toggle(false); | |||
this.img_container.toggle(false); | |||
this.remove_image_link.toggle(false); | |||
} | |||
} | |||
}); | |||
@@ -1266,7 +1354,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ | |||
new frappe.ui.form.LinkSelector({ | |||
doctype: doctype, | |||
target: this, | |||
txt: this.get_value() | |||
txt: this.get_input_value() | |||
}); | |||
return false; | |||
}, | |||
@@ -1290,14 +1378,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ | |||
frappe._from_link = this; | |||
frappe._from_link_scrollY = $(document).scrollTop(); | |||
var trimmed_doctype = doctype.replace(/ /g, ''); | |||
var controller_name = "QuickEntryForm"; | |||
if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){ | |||
controller_name = trimmed_doctype + "QuickEntryForm"; | |||
} | |||
new frappe.ui.form[controller_name](doctype, function(doc) { | |||
frappe.ui.form.make_quick_entry(doctype, (doc) => { | |||
if(me.frm) { | |||
me.parse_validate_and_set_in_model(doc.name); | |||
} else { | |||
@@ -1411,7 +1492,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ | |||
me.selected = false; | |||
return; | |||
} | |||
var value = me.get_value(); | |||
var value = me.get_input_value(); | |||
if(value!==me.last_value) { | |||
me.parse_validate_and_set_in_model(value); | |||
} | |||
@@ -1681,6 +1762,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ | |||
me.parse_validate_and_set_in_model(value); | |||
}, | |||
onKeydown: function(e) { | |||
this._last_change_on = new Date(); | |||
var key = frappe.ui.keys.get_key(e); | |||
// prevent 'New DocType (Ctrl + B)' shortcut in editor | |||
if(['ctrl+b', 'meta+b'].indexOf(key) !== -1) { | |||
@@ -1787,17 +1869,33 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ | |||
.attr('data-original-title', ''); | |||
} | |||
}, | |||
get_value: function() { | |||
get_input_value: function() { | |||
return this.editor? this.editor.summernote('code'): ''; | |||
}, | |||
set_input: function(value) { | |||
if(value == null) value = ""; | |||
value = frappe.dom.remove_script_and_style(value); | |||
if(value !== this.get_value()) { | |||
this.editor.summernote('code', value); | |||
if(value !== this.get_input_value()) { | |||
this.set_in_editor(value); | |||
} | |||
this.last_value = value; | |||
}, | |||
set_in_editor: function(value) { | |||
// set value after user has stopped editing | |||
if(!this._last_change_on || (moment() - moment(this._last_change_on) > 3000)) { | |||
this.editor.summernote('code', value); | |||
} else { | |||
if(!this._setting_value) { | |||
this._setting_value = setInterval(() => { | |||
if(moment() - moment(this._last_change_on) > 3000) { | |||
this.editor.summernote('code', this.last_value); | |||
clearInterval(this._setting_value); | |||
this._setting_value = null; | |||
} | |||
}, 1000); | |||
} | |||
} | |||
}, | |||
set_focus: function() { | |||
return this.editor.summernote('focus'); | |||
}, | |||
@@ -1913,7 +2011,7 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ | |||
return false; | |||
}); | |||
}, | |||
get_parsed_value: function() { | |||
get_value: function() { | |||
if(this.grid) { | |||
return this.grid.get_data(); | |||
} | |||
@@ -110,7 +110,7 @@ | |||
{% $.each(data.attachments, function(i, a) { %} | |||
<div class="ellipsis"> | |||
<a href="{%= encodeURI(a.file_url).replace(/#/g, \'%23\') %}" | |||
class="text-muted small" target="_blank"> | |||
class="text-muted small" target="_blank" rel="noopener noreferrer"> | |||
<i class="fa fa-paperclip"></i> | |||
{%= a.file_url.split("/").slice(-1)[0] %} | |||
{% if (a.is_private) { %} | |||
@@ -53,6 +53,17 @@ frappe.form.formatters = { | |||
Currency: function(value, docfield, options, doc) { | |||
var currency = frappe.meta.get_field_currency(docfield, doc); | |||
var precision = docfield.precision || cint(frappe.boot.sysdefaults.currency_precision) || 2; | |||
if (precision > 2) { | |||
let parts = cstr(value).split('.'); | |||
let decimals = parts.length > 1 ? parts[1] : ''; | |||
if (decimals.length < 3) { | |||
// min precision 2 | |||
precision = 2; | |||
} else if (decimals.length < precision) { | |||
// or min decimals | |||
precision = decimals.length; | |||
} | |||
} | |||
return frappe.form.formatters._right((value==null || value==="") | |||
? "" : format_currency(value, currency, precision), options); | |||
}, | |||
@@ -108,6 +108,11 @@ frappe.ui.form.Grid = Class.extend({ | |||
select_row: function(name) { | |||
this.grid_rows_by_docname[name].select(); | |||
}, | |||
remove_all: function() { | |||
this.grid_rows.forEach(row => { | |||
row.remove(); | |||
}); | |||
}, | |||
refresh_remove_rows_button: function() { | |||
this.remove_rows_button.toggleClass('hide', | |||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); | |||
@@ -257,7 +262,7 @@ frappe.ui.form.Grid = Class.extend({ | |||
if (this.frm && this.frm.docname) { | |||
// use doc specific docfield object | |||
this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname, | |||
this.frm.docname); | |||
this.frm.docname); | |||
} else { | |||
// use non-doc specific docfield | |||
if(this.df.options) { | |||
@@ -360,8 +365,19 @@ frappe.ui.form.Grid = Class.extend({ | |||
get_docfield: function(fieldname) { | |||
return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null); | |||
}, | |||
get_grid_row: function(docname) { | |||
return this.grid_rows_by_docname[docname]; | |||
get_row: function(key) { | |||
if(typeof key == 'number') { | |||
if(key < 0) { | |||
return this.grid_rows[this.grid_rows.length + key]; | |||
} else { | |||
return this.grid_rows[key]; | |||
} | |||
} else { | |||
return this.grid_rows_by_docname[key]; | |||
} | |||
}, | |||
get_grid_row: function(key) { | |||
return this.get_row(key); | |||
}, | |||
get_field: function(fieldname) { | |||
// Note: workaround for get_query | |||
@@ -435,21 +451,21 @@ frappe.ui.form.Grid = Class.extend({ | |||
&& (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm) | |||
&& !in_list(frappe.model.layout_fields, df.fieldtype)) { | |||
if(df.columns) { | |||
df.colsize=df.columns; | |||
} | |||
else { | |||
var colsize=2; | |||
switch(df.fieldtype){ | |||
case"Text": | |||
case"Small Text": | |||
colsize=3; | |||
break; | |||
case"Check": | |||
colsize=1 | |||
} | |||
df.colsize=colsize | |||
if(df.columns) { | |||
df.colsize=df.columns; | |||
} | |||
else { | |||
var colsize=2; | |||
switch(df.fieldtype) { | |||
case"Text": | |||
case"Small Text": | |||
colsize=3; | |||
break; | |||
case"Check": | |||
colsize=1 | |||
} | |||
df.colsize=colsize; | |||
} | |||
if(df.columns) { | |||
df.colsize=df.columns; | |||
@@ -641,673 +657,4 @@ frappe.ui.form.Grid = Class.extend({ | |||
// hide all custom buttons | |||
this.grid_buttons.find('.btn-custom').addClass('hidden'); | |||
} | |||
}); | |||
frappe.ui.form.GridRow = Class.extend({ | |||
init: function(opts) { | |||
this.on_grid_fields_dict = {}; | |||
this.on_grid_fields = []; | |||
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">'; | |||
this.columns = {}; | |||
this.columns_list = []; | |||
$.extend(this, opts); | |||
this.make(); | |||
}, | |||
make: function() { | |||
var me = this; | |||
this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this); | |||
this.row = $('<div class="data-row row"></div>').appendTo(this.wrapper) | |||
.on("click", function(e) { | |||
if($(e.target).hasClass('grid-row-check') || $(e.target).hasClass('row-index') || $(e.target).parent().hasClass('row-index')) { | |||
return; | |||
} | |||
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) { | |||
// pass | |||
} else { | |||
me.toggle_view(); | |||
return false; | |||
} | |||
}); | |||
// no checkboxes if too small | |||
// if(this.is_too_small()) { | |||
// this.row_check_html = ''; | |||
// } | |||
if(this.grid.template && !this.grid.meta.editable_grid) { | |||
this.render_template(); | |||
} else { | |||
this.render_row(); | |||
} | |||
if(this.doc) { | |||
this.set_data(); | |||
} | |||
}, | |||
set_data: function() { | |||
this.wrapper.data({ | |||
"doc": this.doc | |||
}) | |||
}, | |||
set_row_index: function() { | |||
if(this.doc) { | |||
this.wrapper | |||
.attr('data-name', this.doc.name) | |||
.attr("data-idx", this.doc.idx) | |||
.find(".row-index span, .grid-form-row-index").html(this.doc.idx) | |||
} | |||
}, | |||
select: function(checked) { | |||
this.doc.__checked = checked ? 1 : 0; | |||
}, | |||
refresh_check: function() { | |||
this.wrapper.find('.grid-row-check').prop('checked', this.doc ? !!this.doc.__checked : false); | |||
this.grid.refresh_remove_rows_button(); | |||
}, | |||
remove: function() { | |||
var me = this; | |||
if(this.grid.is_editable()) { | |||
if(this.frm) { | |||
if(this.get_open_form()) { | |||
this.hide_form(); | |||
} | |||
this.frm.script_manager.trigger("before_" + this.grid.df.fieldname + "_remove", | |||
this.doc.doctype, this.doc.name); | |||
//this.wrapper.toggle(false); | |||
frappe.model.clear_doc(this.doc.doctype, this.doc.name); | |||
this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove", | |||
this.doc.doctype, this.doc.name); | |||
this.frm.dirty(); | |||
} else { | |||
this.grid.df.data = this.grid.df.data.filter(function(d) { | |||
return d.name !== me.doc.name; | |||
}) | |||
// remap idxs | |||
this.grid.df.data.forEach(function(d, i) { | |||
d.idx = i+1; | |||
}); | |||
} | |||
this.grid.refresh(); | |||
} | |||
}, | |||
insert: function(show, below) { | |||
var idx = this.doc.idx; | |||
if(below) idx ++; | |||
this.toggle_view(false); | |||
this.grid.add_new_row(idx, null, show); | |||
}, | |||
refresh: function() { | |||
if(this.frm && this.doc) { | |||
this.doc = locals[this.doc.doctype][this.doc.name]; | |||
} | |||
// re write columns | |||
this.visible_columns = null; | |||
if(this.grid.template && !this.grid.meta.editable_grid) { | |||
this.render_template(); | |||
} else { | |||
this.render_row(true); | |||
} | |||
// refersh form fields | |||
if(this.grid_form) { | |||
this.grid_form.layout && this.grid_form.layout.refresh(this.doc); | |||
} | |||
}, | |||
render_template: function() { | |||
this.set_row_index(); | |||
if(this.row_display) { | |||
this.row_display.remove(); | |||
} | |||
var index_html = ''; | |||
// row index | |||
if(this.doc) { | |||
if(!this.row_index) { | |||
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \ | |||
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row); | |||
} | |||
this.row_index.find('span').html(this.doc.idx); | |||
} | |||
this.row_display = $('<div class="row-data sortable-handle template-row">'+ | |||
+'</div>').appendTo(this.row) | |||
.html(frappe.render(this.grid.template, { | |||
doc: this.doc ? frappe.get_format_helper(this.doc) : null, | |||
frm: this.frm, | |||
row: this | |||
})); | |||
}, | |||
render_row: function(refresh) { | |||
var me = this; | |||
this.set_row_index(); | |||
// index (1, 2, 3 etc) | |||
if(!this.row_index) { | |||
var txt = (this.doc ? this.doc.idx : " "); | |||
this.row_index = $( | |||
`<div class="row-index sortable-handle col col-xs-1"> | |||
${this.row_check_html} | |||
<span>${txt}</span></div>`) | |||
.appendTo(this.row) | |||
.on('click', function(e) { | |||
if(!$(e.target).hasClass('grid-row-check')) { | |||
me.toggle_view(); | |||
} | |||
}); | |||
} else { | |||
this.row_index.find('span').html(txt); | |||
} | |||
this.setup_columns(); | |||
this.add_open_form_button(); | |||
this.refresh_check(); | |||
if(this.frm && this.doc) { | |||
$(this.frm.wrapper).trigger("grid-row-render", [this]); | |||
} | |||
}, | |||
make_editable: function() { | |||
this.row.toggleClass('editable-row', this.grid.is_editable()); | |||
}, | |||
is_too_small: function() { | |||
return this.row.width() ? this.row.width() < 300 : false; | |||
}, | |||
add_open_form_button: function() { | |||
var me = this; | |||
if(this.doc && !this.grid.df.in_place_edit) { | |||
// remove row | |||
if(!this.open_form_button) { | |||
this.open_form_button = $('<a class="close btn-open-row">\ | |||
<span class="octicon octicon-triangle-down"></span></a>') | |||
.appendTo($('<div class="col col-xs-1 sortable-handle"></div>').appendTo(this.row)) | |||
.on('click', function() { me.toggle_view(); return false; }); | |||
if(this.is_too_small()) { | |||
// narrow | |||
this.open_form_button.css({'margin-right': '-2px'}); | |||
} | |||
} | |||
} | |||
}, | |||
setup_columns: function() { | |||
var me = this; | |||
this.focus_set = false; | |||
this.grid.setup_visible_columns(); | |||
for(var ci in this.grid.visible_columns) { | |||
var df = this.grid.visible_columns[ci][0], | |||
colsize = this.grid.visible_columns[ci][1], | |||
txt = this.doc ? | |||
frappe.format(this.doc[df.fieldname], df, null, this.doc) : | |||
__(df.label); | |||
if(this.doc && df.fieldtype === "Select") { | |||
txt = __(txt); | |||
} | |||
if(!this.columns[df.fieldname]) { | |||
var column = this.make_column(df, colsize, txt, ci); | |||
} else { | |||
var column = this.columns[df.fieldname]; | |||
this.refresh_field(df.fieldname, txt); | |||
} | |||
// background color for cellz | |||
if(this.doc) { | |||
if(df.reqd && !txt) { | |||
column.addClass('error'); | |||
} | |||
if (df.reqd || df.bold) { | |||
column.addClass('bold'); | |||
} | |||
} | |||
} | |||
}, | |||
make_column: function(df, colsize, txt, ci) { | |||
var me = this; | |||
var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ? | |||
" grid-overflow-no-ellipsis" : ""); | |||
add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ? | |||
" text-right": ""; | |||
add_class += (["Check"].indexOf(df.fieldtype)!==-1) ? | |||
" text-center": ""; | |||
var $col = $('<div class="col grid-static-col col-xs-'+colsize+' '+add_class+'"></div>') | |||
.attr("data-fieldname", df.fieldname) | |||
.attr("data-fieldtype", df.fieldtype) | |||
.data("df", df) | |||
.appendTo(this.row) | |||
.on('click', function() { | |||
if(frappe.ui.form.editable_row===me) { | |||
return; | |||
} | |||
var out = me.toggle_editable_row(); | |||
var col = this; | |||
setTimeout(function() { | |||
$(col).find('input[type="Text"]:first').focus(); | |||
}, 500); | |||
return out; | |||
}); | |||
$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false); | |||
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt); | |||
$col.df = df; | |||
$col.column_index = ci; | |||
this.columns[df.fieldname] = $col; | |||
this.columns_list.push($col); | |||
return $col; | |||
}, | |||
toggle_editable_row: function(show) { | |||
var me = this; | |||
// show static for field based on | |||
// whether grid is editable | |||
if(this.grid.allow_on_grid_editing() && this.grid.is_editable() && this.doc && show !== false) { | |||
// disable other editale row | |||
if(frappe.ui.form.editable_row | |||
&& frappe.ui.form.editable_row !== this) { | |||
frappe.ui.form.editable_row.toggle_editable_row(false); | |||
} | |||
this.row.toggleClass('editable-row', true); | |||
// setup controls | |||
this.columns_list.forEach(function(column) { | |||
me.make_control(column); | |||
column.static_area.toggle(false); | |||
column.field_area.toggle(true); | |||
}); | |||
frappe.ui.form.editable_row = this; | |||
return false; | |||
} else { | |||
this.row.toggleClass('editable-row', false); | |||
this.columns_list.forEach(function(column) { | |||
column.static_area.toggle(true); | |||
column.field_area && column.field_area.toggle(false); | |||
}); | |||
frappe.ui.form.editable_row = null; | |||
} | |||
}, | |||
make_control: function(column) { | |||
if(column.field) return; | |||
var me = this, | |||
parent = column.field_area, | |||
df = column.df; | |||
// no text editor in grid | |||
if (df.fieldtype=='Text Editor') { | |||
df.fieldtype = 'Text'; | |||
} | |||
var field = frappe.ui.form.make_control({ | |||
df: df, | |||
parent: parent, | |||
only_input: true, | |||
with_link_btn: true, | |||
doc: this.doc, | |||
doctype: this.doc.doctype, | |||
docname: this.doc.name, | |||
frm: this.grid.frm, | |||
grid: this.grid, | |||
grid_row: this, | |||
value: this.doc[df.fieldname] | |||
}); | |||
// sync get_query | |||
field.get_query = this.grid.get_field(df.fieldname).get_query; | |||
field.refresh(); | |||
if(field.$input) { | |||
field.$input | |||
.addClass('input-sm') | |||
.attr('data-col-idx', column.column_index) | |||
.attr('placeholder', __(df.label)); | |||
// flag list input | |||
if (this.columns_list && this.columns_list.slice(-1)[0]===column) { | |||
field.$input.attr('data-last-input', 1); | |||
} | |||
} | |||
this.set_arrow_keys(field); | |||
column.field = field; | |||
this.on_grid_fields_dict[df.fieldname] = field; | |||
this.on_grid_fields.push(field); | |||
}, | |||
set_arrow_keys: function(field) { | |||
var me = this; | |||
if(field.$input) { | |||
field.$input.on('keydown', function(e) { | |||
var { TAB, UP_ARROW, DOWN_ARROW } = frappe.ui.keyCode; | |||
if(!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) { | |||
return; | |||
} | |||
var values = me.grid.get_data(); | |||
var fieldname = $(this).attr('data-fieldname'); | |||
var fieldtype = $(this).attr('data-fieldtype'); | |||
var move_up_down = function(base) { | |||
if(in_list(['Text', 'Small Text'], fieldtype)) { | |||
return; | |||
} | |||
base.toggle_editable_row(); | |||
setTimeout(function() { | |||
var input = base.columns[fieldname].field.$input; | |||
if(input) { | |||
input.focus(); | |||
} | |||
}, 400) | |||
} | |||
// TAB | |||
if(e.which==TAB && !e.shiftKey) { | |||
// last column | |||
if($(this).attr('data-last-input') || | |||
me.grid.wrapper.find('.grid-row :input:enabled:last').get(0)===this) { | |||
setTimeout(function() { | |||
if(me.doc.idx === values.length) { | |||
// last row | |||
me.grid.add_new_row(null, null, true); | |||
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row(); | |||
me.grid.set_focus_on_row(); | |||
} else { | |||
me.grid.grid_rows[me.doc.idx].toggle_editable_row(); | |||
me.grid.set_focus_on_row(me.doc.idx+1); | |||
} | |||
}, 500); | |||
} | |||
} else if(e.which==UP_ARROW) { | |||
if(me.doc.idx > 1) { | |||
var prev = me.grid.grid_rows[me.doc.idx-2]; | |||
move_up_down(prev); | |||
} | |||
} else if(e.which==DOWN_ARROW) { | |||
if(me.doc.idx < values.length) { | |||
var next = me.grid.grid_rows[me.doc.idx]; | |||
move_up_down(next); | |||
} | |||
} | |||
}); | |||
} | |||
}, | |||
get_open_form: function() { | |||
return frappe.ui.form.get_open_grid_form(); | |||
}, | |||
toggle_view: function(show, callback) { | |||
if(!this.doc) { | |||
return this; | |||
} | |||
if(this.frm) { | |||
// reload doc | |||
this.doc = locals[this.doc.doctype][this.doc.name]; | |||
} | |||
// hide other | |||
var open_row = this.get_open_form(); | |||
if (show===undefined) show = !!!open_row; | |||
// call blur | |||
document.activeElement && document.activeElement.blur(); | |||
if(show && open_row) { | |||
if(open_row==this) { | |||
// already open, do nothing | |||
callback && callback(); | |||
return; | |||
} else { | |||
// close other views | |||
open_row.toggle_view(false); | |||
} | |||
} | |||
if(show) { | |||
this.show_form(); | |||
} else { | |||
this.hide_form(); | |||
} | |||
callback && callback(); | |||
return this; | |||
}, | |||
show_form: function() { | |||
if(!this.grid_form) { | |||
this.grid_form = new frappe.ui.form.GridRowForm({ | |||
row: this | |||
}); | |||
} | |||
this.grid_form.render(); | |||
this.row.toggle(false); | |||
// this.form_panel.toggle(true); | |||
frappe.dom.freeze("", "dark"); | |||
cur_frm.cur_grid = this; | |||
this.wrapper.addClass("grid-row-open"); | |||
if(!frappe.dom.is_element_in_viewport(this.wrapper)) { | |||
frappe.utils.scroll_to(this.wrapper, true, 15); | |||
} | |||
if(this.frm) { | |||
this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered"); | |||
this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name); | |||
} | |||
}, | |||
hide_form: function() { | |||
frappe.dom.unfreeze(); | |||
this.row.toggle(true); | |||
this.refresh(); | |||
cur_frm.cur_grid = null; | |||
this.wrapper.removeClass("grid-row-open"); | |||
}, | |||
open_prev: function() { | |||
if(this.grid.grid_rows[this.doc.idx-2]) { | |||
this.grid.grid_rows[this.doc.idx-2].toggle_view(true); | |||
} | |||
}, | |||
open_next: function() { | |||
if(this.grid.grid_rows[this.doc.idx]) { | |||
this.grid.grid_rows[this.doc.idx].toggle_view(true); | |||
} else { | |||
this.grid.add_new_row(null, null, true); | |||
} | |||
}, | |||
refresh_field: function(fieldname, txt) { | |||
var df = this.grid.get_docfield(fieldname) || undefined; | |||
// format values if no frm | |||
if(!df) { | |||
df = this.grid.visible_columns.find((col) => { | |||
return col[0].fieldname === fieldname; | |||
}); | |||
if(df && this.doc) { | |||
var txt = frappe.format(this.doc[fieldname], df[0], | |||
null, this.doc); | |||
} | |||
} | |||
if(txt===undefined && this.frm) { | |||
var txt = frappe.format(this.doc[fieldname], df, | |||
null, this.frm.doc); | |||
} | |||
// reset static value | |||
var column = this.columns[fieldname]; | |||
if(column) { | |||
column.static_area.html(txt || ""); | |||
if(df && df.reqd) { | |||
column.toggleClass('error', !!(txt===null || txt==='')); | |||
} | |||
} | |||
// reset field value | |||
var field = this.on_grid_fields_dict[fieldname]; | |||
if(field) { | |||
field.docname = this.doc.name; | |||
field.refresh(); | |||
} | |||
// in form | |||
if(this.grid_form) { | |||
this.grid_form.refresh_field(fieldname); | |||
} | |||
}, | |||
get_visible_columns: function(blacklist) { | |||
var me = this; | |||
var visible_columns = $.map(this.docfields, function(df) { | |||
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read") | |||
&& !in_list(frappe.model.layout_fields, df.fieldtype) && !in_list(blacklist, df.fieldname); | |||
return visible ? df : null; | |||
}); | |||
return visible_columns; | |||
}, | |||
set_field_property: function(fieldname, property, value) { | |||
// set a field property for open form / grid form | |||
var me = this; | |||
var set_property = function(field) { | |||
if(!field) return; | |||
field.df[property] = value; | |||
field.refresh(); | |||
} | |||
// set property in grid form | |||
if(this.grid_form) { | |||
set_property(this.grid_form.fields_dict[fieldname]); | |||
this.grid_form.layout && this.grid_form.layout.refresh_sections(); | |||
} | |||
// set property in on grid fields | |||
set_property(this.on_grid_fields_dict[fieldname]); | |||
}, | |||
toggle_reqd: function(fieldname, reqd) { | |||
this.set_field_property(fieldname, 'reqd', reqd ? 1 : 0); | |||
}, | |||
toggle_display: function(fieldname, show) { | |||
this.set_field_property(fieldname, 'hidden', show ? 0 : 1); | |||
}, | |||
toggle_editable: function(fieldname, editable) { | |||
this.set_field_property(fieldname, 'read_only', editable ? 0 : 1); | |||
}, | |||
}); | |||
frappe.ui.form.GridRowForm = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.wrapper = $('<div class="form-in-grid"></div>') | |||
.appendTo(this.row.wrapper); | |||
}, | |||
render: function() { | |||
var me = this; | |||
this.make_form(); | |||
this.form_area.empty(); | |||
this.layout = new frappe.ui.form.Layout({ | |||
fields: this.row.docfields, | |||
body: this.form_area, | |||
no_submit_on_enter: true, | |||
frm: this.row.frm, | |||
}); | |||
this.layout.make(); | |||
this.fields = this.layout.fields; | |||
this.fields_dict = this.layout.fields_dict; | |||
this.layout.refresh(this.row.doc); | |||
// copy get_query to fields | |||
for(var fieldname in (this.row.grid.fieldinfo || {})) { | |||
var fi = this.row.grid.fieldinfo[fieldname]; | |||
$.extend(me.fields_dict[fieldname], fi); | |||
} | |||
this.toggle_add_delete_button_display(this.wrapper); | |||
this.row.grid.open_grid_row = this; | |||
this.set_focus(); | |||
}, | |||
make_form: function() { | |||
if(!this.form_area) { | |||
$(frappe.render_template("grid_form", {grid:this})).appendTo(this.wrapper); | |||
this.form_area = this.wrapper.find(".form-area"); | |||
this.row.set_row_index(); | |||
this.set_form_events(); | |||
} | |||
}, | |||
set_form_events: function() { | |||
var me = this; | |||
this.wrapper.find(".grid-delete-row") | |||
.on('click', function() { | |||
me.row.remove(); return false; | |||
}); | |||
this.wrapper.find(".grid-insert-row") | |||
.on('click', function() { | |||
me.row.insert(true); return false; | |||
}); | |||
this.wrapper.find(".grid-insert-row-below") | |||
.on('click', function() { | |||
me.row.insert(true, true); return false; | |||
}); | |||
this.wrapper.find(".grid-append-row") | |||
.on('click', function() { | |||
me.row.toggle_view(false); | |||
me.row.grid.add_new_row(me.row.doc.idx+1, null, true); | |||
return false; | |||
}); | |||
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function() { | |||
me.row.toggle_view(); | |||
return false; | |||
}); | |||
}, | |||
toggle_add_delete_button_display: function($parent) { | |||
$parent.find(".grid-header-toolbar .btn, .grid-footer-toolbar .btn") | |||
.toggle(this.row.grid.is_editable()); | |||
}, | |||
refresh_field: function(fieldname) { | |||
if(this.fields_dict[fieldname]) { | |||
this.fields_dict[fieldname].refresh(); | |||
this.layout && this.layout.refresh_dependency(); | |||
} | |||
}, | |||
set_focus: function() { | |||
// wait for animation and then focus on the first row | |||
var me = this; | |||
setTimeout(function() { | |||
if(me.row.frm && me.row.frm.doc.docstatus===0 || !me.row.frm) { | |||
var first = me.form_area.find("input:first"); | |||
if(first.length && !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))) { | |||
try { | |||
first.get(0).focus(); | |||
} catch(e) { | |||
// | |||
} | |||
} | |||
} | |||
}, 500); | |||
}, | |||
}); | |||
}); |
@@ -0,0 +1,586 @@ | |||
frappe.ui.form.GridRow = Class.extend({ | |||
init: function(opts) { | |||
this.on_grid_fields_dict = {}; | |||
this.on_grid_fields = []; | |||
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">'; | |||
this.columns = {}; | |||
this.columns_list = []; | |||
$.extend(this, opts); | |||
this.make(); | |||
}, | |||
make: function() { | |||
var me = this; | |||
this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this); | |||
this.row = $('<div class="data-row row"></div>').appendTo(this.wrapper) | |||
.on("click", function(e) { | |||
if($(e.target).hasClass('grid-row-check') || $(e.target).hasClass('row-index') || $(e.target).parent().hasClass('row-index')) { | |||
return; | |||
} | |||
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) { | |||
// pass | |||
} else { | |||
me.toggle_view(); | |||
return false; | |||
} | |||
}); | |||
// no checkboxes if too small | |||
// if(this.is_too_small()) { | |||
// this.row_check_html = ''; | |||
// } | |||
if(this.grid.template && !this.grid.meta.editable_grid) { | |||
this.render_template(); | |||
} else { | |||
this.render_row(); | |||
} | |||
if(this.doc) { | |||
this.set_data(); | |||
} | |||
}, | |||
set_data: function() { | |||
this.wrapper.data({ | |||
"doc": this.doc | |||
}) | |||
}, | |||
set_row_index: function() { | |||
if(this.doc) { | |||
this.wrapper | |||
.attr('data-name', this.doc.name) | |||
.attr("data-idx", this.doc.idx) | |||
.find(".row-index span, .grid-form-row-index").html(this.doc.idx) | |||
} | |||
}, | |||
select: function(checked) { | |||
this.doc.__checked = checked ? 1 : 0; | |||
}, | |||
refresh_check: function() { | |||
this.wrapper.find('.grid-row-check').prop('checked', this.doc ? !!this.doc.__checked : false); | |||
this.grid.refresh_remove_rows_button(); | |||
}, | |||
remove: function() { | |||
var me = this; | |||
if(this.grid.is_editable()) { | |||
if(this.frm) { | |||
if(this.get_open_form()) { | |||
this.hide_form(); | |||
} | |||
this.frm.script_manager.trigger("before_" + this.grid.df.fieldname + "_remove", | |||
this.doc.doctype, this.doc.name); | |||
//this.wrapper.toggle(false); | |||
frappe.model.clear_doc(this.doc.doctype, this.doc.name); | |||
this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove", | |||
this.doc.doctype, this.doc.name); | |||
this.frm.dirty(); | |||
} else { | |||
this.grid.df.data = this.grid.df.data.filter(function(d) { | |||
return d.name !== me.doc.name; | |||
}) | |||
// remap idxs | |||
this.grid.df.data.forEach(function(d, i) { | |||
d.idx = i+1; | |||
}); | |||
} | |||
this.grid.refresh(); | |||
} | |||
}, | |||
insert: function(show, below) { | |||
var idx = this.doc.idx; | |||
if(below) idx ++; | |||
this.toggle_view(false); | |||
this.grid.add_new_row(idx, null, show); | |||
}, | |||
refresh: function() { | |||
if(this.frm && this.doc) { | |||
this.doc = locals[this.doc.doctype][this.doc.name]; | |||
} | |||
// re write columns | |||
this.visible_columns = null; | |||
if(this.grid.template && !this.grid.meta.editable_grid) { | |||
this.render_template(); | |||
} else { | |||
this.render_row(true); | |||
} | |||
// refersh form fields | |||
if(this.grid_form) { | |||
this.grid_form.layout && this.grid_form.layout.refresh(this.doc); | |||
} | |||
}, | |||
render_template: function() { | |||
this.set_row_index(); | |||
if(this.row_display) { | |||
this.row_display.remove(); | |||
} | |||
var index_html = ''; | |||
// row index | |||
if(this.doc) { | |||
if(!this.row_index) { | |||
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \ | |||
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row); | |||
} | |||
this.row_index.find('span').html(this.doc.idx); | |||
} | |||
this.row_display = $('<div class="row-data sortable-handle template-row">'+ | |||
+'</div>').appendTo(this.row) | |||
.html(frappe.render(this.grid.template, { | |||
doc: this.doc ? frappe.get_format_helper(this.doc) : null, | |||
frm: this.frm, | |||
row: this | |||
})); | |||
}, | |||
render_row: function(refresh) { | |||
var me = this; | |||
this.set_row_index(); | |||
// index (1, 2, 3 etc) | |||
if(!this.row_index) { | |||
var txt = (this.doc ? this.doc.idx : " "); | |||
this.row_index = $( | |||
`<div class="row-index sortable-handle col col-xs-1"> | |||
${this.row_check_html} | |||
<span>${txt}</span></div>`) | |||
.appendTo(this.row) | |||
.on('click', function(e) { | |||
if(!$(e.target).hasClass('grid-row-check')) { | |||
me.toggle_view(); | |||
} | |||
}); | |||
} else { | |||
this.row_index.find('span').html(txt); | |||
} | |||
this.setup_columns(); | |||
this.add_open_form_button(); | |||
this.refresh_check(); | |||
if(this.frm && this.doc) { | |||
$(this.frm.wrapper).trigger("grid-row-render", [this]); | |||
} | |||
}, | |||
make_editable: function() { | |||
this.row.toggleClass('editable-row', this.grid.is_editable()); | |||
}, | |||
is_too_small: function() { | |||
return this.row.width() ? this.row.width() < 300 : false; | |||
}, | |||
add_open_form_button: function() { | |||
var me = this; | |||
if(this.doc && !this.grid.df.in_place_edit) { | |||
// remove row | |||
if(!this.open_form_button) { | |||
this.open_form_button = $('<a class="close btn-open-row">\ | |||
<span class="octicon octicon-triangle-down"></span></a>') | |||
.appendTo($('<div class="col col-xs-1 sortable-handle"></div>').appendTo(this.row)) | |||
.on('click', function() { me.toggle_view(); return false; }); | |||
if(this.is_too_small()) { | |||
// narrow | |||
this.open_form_button.css({'margin-right': '-2px'}); | |||
} | |||
} | |||
} | |||
}, | |||
setup_columns: function() { | |||
var me = this; | |||
this.focus_set = false; | |||
this.grid.setup_visible_columns(); | |||
for(var ci in this.grid.visible_columns) { | |||
var df = this.grid.visible_columns[ci][0], | |||
colsize = this.grid.visible_columns[ci][1], | |||
txt = this.doc ? | |||
frappe.format(this.doc[df.fieldname], df, null, this.doc) : | |||
__(df.label); | |||
if(this.doc && df.fieldtype === "Select") { | |||
txt = __(txt); | |||
} | |||
if(!this.columns[df.fieldname]) { | |||
var column = this.make_column(df, colsize, txt, ci); | |||
} else { | |||
var column = this.columns[df.fieldname]; | |||
this.refresh_field(df.fieldname, txt); | |||
} | |||
// background color for cellz | |||
if(this.doc) { | |||
if(df.reqd && !txt) { | |||
column.addClass('error'); | |||
} | |||
if (df.reqd || df.bold) { | |||
column.addClass('bold'); | |||
} | |||
} | |||
} | |||
}, | |||
make_column: function(df, colsize, txt, ci) { | |||
var me = this; | |||
var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ? | |||
" grid-overflow-no-ellipsis" : ""); | |||
add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ? | |||
" text-right": ""; | |||
add_class += (["Check"].indexOf(df.fieldtype)!==-1) ? | |||
" text-center": ""; | |||
var $col = $('<div class="col grid-static-col col-xs-'+colsize+' '+add_class+'"></div>') | |||
.attr("data-fieldname", df.fieldname) | |||
.attr("data-fieldtype", df.fieldtype) | |||
.data("df", df) | |||
.appendTo(this.row) | |||
.on('click', function() { | |||
if(frappe.ui.form.editable_row===me) { | |||
return; | |||
} | |||
var out = me.toggle_editable_row(); | |||
var col = this; | |||
setTimeout(function() { | |||
$(col).find('input[type="Text"]:first').focus(); | |||
}, 500); | |||
return out; | |||
}); | |||
$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false); | |||
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt); | |||
$col.df = df; | |||
$col.column_index = ci; | |||
this.columns[df.fieldname] = $col; | |||
this.columns_list.push($col); | |||
return $col; | |||
}, | |||
activate: function() { | |||
this.toggle_editable_row(true); | |||
return this; | |||
}, | |||
toggle_editable_row: function(show) { | |||
var me = this; | |||
// show static for field based on | |||
// whether grid is editable | |||
if(this.grid.allow_on_grid_editing() && this.grid.is_editable() && this.doc && show !== false) { | |||
// disable other editale row | |||
if(frappe.ui.form.editable_row | |||
&& frappe.ui.form.editable_row !== this) { | |||
frappe.ui.form.editable_row.toggle_editable_row(false); | |||
} | |||
this.row.toggleClass('editable-row', true); | |||
// setup controls | |||
this.columns_list.forEach(function(column) { | |||
me.make_control(column); | |||
column.static_area.toggle(false); | |||
column.field_area.toggle(true); | |||
}); | |||
frappe.ui.form.editable_row = this; | |||
return false; | |||
} else { | |||
this.row.toggleClass('editable-row', false); | |||
this.columns_list.forEach(function(column) { | |||
column.static_area.toggle(true); | |||
column.field_area && column.field_area.toggle(false); | |||
}); | |||
frappe.ui.form.editable_row = null; | |||
} | |||
}, | |||
make_control: function(column) { | |||
if(column.field) return; | |||
var me = this, | |||
parent = column.field_area, | |||
df = column.df; | |||
// no text editor in grid | |||
if (df.fieldtype=='Text Editor') { | |||
df.fieldtype = 'Text'; | |||
} | |||
var field = frappe.ui.form.make_control({ | |||
df: df, | |||
parent: parent, | |||
only_input: true, | |||
with_link_btn: true, | |||
doc: this.doc, | |||
doctype: this.doc.doctype, | |||
docname: this.doc.name, | |||
frm: this.grid.frm, | |||
grid: this.grid, | |||
grid_row: this, | |||
value: this.doc[df.fieldname] | |||
}); | |||
// sync get_query | |||
field.get_query = this.grid.get_field(df.fieldname).get_query; | |||
field.refresh(); | |||
if(field.$input) { | |||
field.$input | |||
.addClass('input-sm') | |||
.attr('data-col-idx', column.column_index) | |||
.attr('placeholder', __(df.label)); | |||
// flag list input | |||
if (this.columns_list && this.columns_list.slice(-1)[0]===column) { | |||
field.$input.attr('data-last-input', 1); | |||
} | |||
} | |||
this.set_arrow_keys(field); | |||
column.field = field; | |||
this.on_grid_fields_dict[df.fieldname] = field; | |||
this.on_grid_fields.push(field); | |||
}, | |||
set_arrow_keys: function(field) { | |||
var me = this; | |||
if(field.$input) { | |||
field.$input.on('keydown', function(e) { | |||
var { TAB, UP_ARROW, DOWN_ARROW } = frappe.ui.keyCode; | |||
if(!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) { | |||
return; | |||
} | |||
var values = me.grid.get_data(); | |||
var fieldname = $(this).attr('data-fieldname'); | |||
var fieldtype = $(this).attr('data-fieldtype'); | |||
var move_up_down = function(base) { | |||
if(in_list(['Text', 'Small Text'], fieldtype)) { | |||
return; | |||
} | |||
base.toggle_editable_row(); | |||
setTimeout(function() { | |||
var input = base.columns[fieldname].field.$input; | |||
if(input) { | |||
input.focus(); | |||
} | |||
}, 400) | |||
} | |||
// TAB | |||
if(e.which==TAB && !e.shiftKey) { | |||
// last column | |||
if($(this).attr('data-last-input') || | |||
me.grid.wrapper.find('.grid-row :input:enabled:last').get(0)===this) { | |||
setTimeout(function() { | |||
if(me.doc.idx === values.length) { | |||
// last row | |||
me.grid.add_new_row(null, null, true); | |||
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row(); | |||
me.grid.set_focus_on_row(); | |||
} else { | |||
me.grid.grid_rows[me.doc.idx].toggle_editable_row(); | |||
me.grid.set_focus_on_row(me.doc.idx+1); | |||
} | |||
}, 500); | |||
} | |||
} else if(e.which==UP_ARROW) { | |||
if(me.doc.idx > 1) { | |||
var prev = me.grid.grid_rows[me.doc.idx-2]; | |||
move_up_down(prev); | |||
} | |||
} else if(e.which==DOWN_ARROW) { | |||
if(me.doc.idx < values.length) { | |||
var next = me.grid.grid_rows[me.doc.idx]; | |||
move_up_down(next); | |||
} | |||
} | |||
}); | |||
} | |||
}, | |||
get_open_form: function() { | |||
return frappe.ui.form.get_open_grid_form(); | |||
}, | |||
toggle_view: function(show, callback) { | |||
if(!this.doc) { | |||
return this; | |||
} | |||
if(this.frm) { | |||
// reload doc | |||
this.doc = locals[this.doc.doctype][this.doc.name]; | |||
} | |||
// hide other | |||
var open_row = this.get_open_form(); | |||
if (show===undefined) show = !!!open_row; | |||
// call blur | |||
document.activeElement && document.activeElement.blur(); | |||
if(show && open_row) { | |||
if(open_row==this) { | |||
// already open, do nothing | |||
callback && callback(); | |||
return; | |||
} else { | |||
// close other views | |||
open_row.toggle_view(false); | |||
} | |||
} | |||
if(show) { | |||
this.show_form(); | |||
} else { | |||
this.hide_form(); | |||
} | |||
callback && callback(); | |||
return this; | |||
}, | |||
show_form: function() { | |||
if(!this.grid_form) { | |||
this.grid_form = new frappe.ui.form.GridRowForm({ | |||
row: this | |||
}); | |||
} | |||
this.grid_form.render(); | |||
this.row.toggle(false); | |||
// this.form_panel.toggle(true); | |||
frappe.dom.freeze("", "dark"); | |||
cur_frm.cur_grid = this; | |||
this.wrapper.addClass("grid-row-open"); | |||
if(!frappe.dom.is_element_in_viewport(this.wrapper)) { | |||
frappe.utils.scroll_to(this.wrapper, true, 15); | |||
} | |||
if(this.frm) { | |||
this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered"); | |||
this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name); | |||
} | |||
}, | |||
hide_form: function() { | |||
frappe.dom.unfreeze(); | |||
this.row.toggle(true); | |||
this.refresh(); | |||
cur_frm.cur_grid = null; | |||
this.wrapper.removeClass("grid-row-open"); | |||
}, | |||
open_prev: function() { | |||
if(this.grid.grid_rows[this.doc.idx-2]) { | |||
this.grid.grid_rows[this.doc.idx-2].toggle_view(true); | |||
} | |||
}, | |||
open_next: function() { | |||
if(this.grid.grid_rows[this.doc.idx]) { | |||
this.grid.grid_rows[this.doc.idx].toggle_view(true); | |||
} else { | |||
this.grid.add_new_row(null, null, true); | |||
} | |||
}, | |||
refresh_field: function(fieldname, txt) { | |||
var df = this.grid.get_docfield(fieldname) || undefined; | |||
// format values if no frm | |||
if(!df) { | |||
df = this.grid.visible_columns.find((col) => { | |||
return col[0].fieldname === fieldname; | |||
}); | |||
if(df && this.doc) { | |||
var txt = frappe.format(this.doc[fieldname], df[0], | |||
null, this.doc); | |||
} | |||
} | |||
if(txt===undefined && this.frm) { | |||
var txt = frappe.format(this.doc[fieldname], df, | |||
null, this.frm.doc); | |||
} | |||
// reset static value | |||
var column = this.columns[fieldname]; | |||
if(column) { | |||
column.static_area.html(txt || ""); | |||
if(df && df.reqd) { | |||
column.toggleClass('error', !!(txt===null || txt==='')); | |||
} | |||
} | |||
// reset field value | |||
var field = this.on_grid_fields_dict[fieldname]; | |||
if(field) { | |||
field.docname = this.doc.name; | |||
field.refresh(); | |||
} | |||
// in form | |||
if(this.grid_form) { | |||
this.grid_form.refresh_field(fieldname); | |||
} | |||
}, | |||
get_field: function(fieldname) { | |||
let field = this.on_grid_fields_dict[fieldname]; | |||
if (field) { | |||
return field; | |||
} else if(this.grid_form) { | |||
return this.grid_form.fields_dict[fieldname]; | |||
} else { | |||
throw `fieldname ${fieldname} not found`; | |||
} | |||
}, | |||
get_visible_columns: function(blacklist) { | |||
var me = this; | |||
var visible_columns = $.map(this.docfields, function(df) { | |||
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read") | |||
&& !in_list(frappe.model.layout_fields, df.fieldtype) && !in_list(blacklist, df.fieldname); | |||
return visible ? df : null; | |||
}); | |||
return visible_columns; | |||
}, | |||
set_field_property: function(fieldname, property, value) { | |||
// set a field property for open form / grid form | |||
var me = this; | |||
var set_property = function(field) { | |||
if(!field) return; | |||
field.df[property] = value; | |||
field.refresh(); | |||
} | |||
// set property in grid form | |||
if(this.grid_form) { | |||
set_property(this.grid_form.fields_dict[fieldname]); | |||
this.grid_form.layout && this.grid_form.layout.refresh_sections(); | |||
} | |||
// set property in on grid fields | |||
set_property(this.on_grid_fields_dict[fieldname]); | |||
}, | |||
toggle_reqd: function(fieldname, reqd) { | |||
this.set_field_property(fieldname, 'reqd', reqd ? 1 : 0); | |||
}, | |||
toggle_display: function(fieldname, show) { | |||
this.set_field_property(fieldname, 'hidden', show ? 0 : 1); | |||
}, | |||
toggle_editable: function(fieldname, editable) { | |||
this.set_field_property(fieldname, 'read_only', editable ? 0 : 1); | |||
}, | |||
}); |
@@ -0,0 +1,97 @@ | |||
frappe.ui.form.GridRowForm = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.wrapper = $('<div class="form-in-grid"></div>') | |||
.appendTo(this.row.wrapper); | |||
}, | |||
render: function() { | |||
var me = this; | |||
this.make_form(); | |||
this.form_area.empty(); | |||
this.layout = new frappe.ui.form.Layout({ | |||
fields: this.row.docfields, | |||
body: this.form_area, | |||
no_submit_on_enter: true, | |||
frm: this.row.frm, | |||
}); | |||
this.layout.make(); | |||
this.fields = this.layout.fields; | |||
this.fields_dict = this.layout.fields_dict; | |||
this.layout.refresh(this.row.doc); | |||
// copy get_query to fields | |||
for(var fieldname in (this.row.grid.fieldinfo || {})) { | |||
var fi = this.row.grid.fieldinfo[fieldname]; | |||
$.extend(me.fields_dict[fieldname], fi); | |||
} | |||
this.toggle_add_delete_button_display(this.wrapper); | |||
this.row.grid.open_grid_row = this; | |||
this.set_focus(); | |||
}, | |||
make_form: function() { | |||
if(!this.form_area) { | |||
$(frappe.render_template("grid_form", {grid:this})).appendTo(this.wrapper); | |||
this.form_area = this.wrapper.find(".form-area"); | |||
this.row.set_row_index(); | |||
this.set_form_events(); | |||
} | |||
}, | |||
set_form_events: function() { | |||
var me = this; | |||
this.wrapper.find(".grid-delete-row") | |||
.on('click', function() { | |||
me.row.remove(); return false; | |||
}); | |||
this.wrapper.find(".grid-insert-row") | |||
.on('click', function() { | |||
me.row.insert(true); return false; | |||
}); | |||
this.wrapper.find(".grid-insert-row-below") | |||
.on('click', function() { | |||
me.row.insert(true, true); return false; | |||
}); | |||
this.wrapper.find(".grid-append-row") | |||
.on('click', function() { | |||
me.row.toggle_view(false); | |||
me.row.grid.add_new_row(me.row.doc.idx+1, null, true); | |||
return false; | |||
}); | |||
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function() { | |||
me.row.toggle_view(); | |||
return false; | |||
}); | |||
}, | |||
toggle_add_delete_button_display: function($parent) { | |||
$parent.find(".grid-header-toolbar .btn, .grid-footer-toolbar .btn") | |||
.toggle(this.row.grid.is_editable()); | |||
}, | |||
refresh_field: function(fieldname) { | |||
if(this.fields_dict[fieldname]) { | |||
this.fields_dict[fieldname].refresh(); | |||
this.layout && this.layout.refresh_dependency(); | |||
} | |||
}, | |||
set_focus: function() { | |||
// wait for animation and then focus on the first row | |||
var me = this; | |||
setTimeout(function() { | |||
if(me.row.frm && me.row.frm.doc.docstatus===0 || !me.row.frm) { | |||
var first = me.form_area.find("input:first"); | |||
if(first.length && !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))) { | |||
try { | |||
first.get(0).focus(); | |||
} catch(e) { | |||
// | |||
} | |||
} | |||
} | |||
}, 500); | |||
}, | |||
}); |
@@ -44,29 +44,33 @@ frappe.ui.form.Layout = Class.extend({ | |||
this.message.empty().addClass('hidden'); | |||
} | |||
}, | |||
render: function() { | |||
render: function(new_fields) { | |||
var me = this; | |||
var fields = new_fields || this.fields; | |||
this.section = null; | |||
this.column = null; | |||
if((this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length) { | |||
if((fields[0] && fields[0].fieldtype!="Section Break") || !fields.length) { | |||
this.make_section(); | |||
} | |||
$.each(this.fields, function(i, df) { | |||
if(df.fieldtype === "Fold") { | |||
me.make_page(df); | |||
} else if (df.fieldtype === "Section Break") { | |||
me.make_section(df); | |||
} else if (df.fieldtype === "Column Break") { | |||
me.make_column(df); | |||
} else { | |||
me.make_field(df); | |||
$.each(fields, function(i, df) { | |||
switch(df.fieldtype) { | |||
case "Fold": | |||
me.make_page(df); | |||
break; | |||
case "Section Break": | |||
me.make_section(df); | |||
break; | |||
case "Column Break": | |||
me.make_column(df); | |||
break; | |||
default: | |||
me.make_field(df); | |||
} | |||
}); | |||
}, | |||
make_field: function(df, colspan) { | |||
make_field: function(df, colspan, render = false) { | |||
!this.section && this.make_section(); | |||
!this.column && this.make_column(); | |||
@@ -74,7 +78,8 @@ frappe.ui.form.Layout = Class.extend({ | |||
df: df, | |||
doctype: this.doctype, | |||
parent: this.column.wrapper.get(0), | |||
frm: this.frm | |||
frm: this.frm, | |||
render_input: render | |||
}); | |||
fieldobj.layout = this; | |||
@@ -226,6 +231,18 @@ frappe.ui.form.Layout = Class.extend({ | |||
} | |||
}, | |||
refresh_fields: function(fields) { | |||
let fieldnames = fields.map((field) => { | |||
if(field.label) return field.label; | |||
}); | |||
this.fields_list.map(fieldobj => { | |||
if(fieldnames.includes(fieldobj._label)) { | |||
fieldobj.refresh(); | |||
} | |||
}); | |||
}, | |||
refresh_section_count: function() { | |||
this.wrapper.find(".section-count-label:visible").each(function(i) { | |||
$(this).html(i+1); | |||
@@ -1,20 +1,37 @@ | |||
frappe.provide('frappe.ui.form'); | |||
frappe.ui.form.make_quick_entry = (doctype, after_insert) => { | |||
var trimmed_doctype = doctype.replace(/ /g, ''); | |||
var controller_name = "QuickEntryForm"; | |||
if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){ | |||
controller_name = trimmed_doctype + "QuickEntryForm"; | |||
} | |||
frappe.quick_entry = new frappe.ui.form[controller_name](doctype, after_insert); | |||
return frappe.quick_entry.setup(); | |||
}; | |||
frappe.ui.form.QuickEntryForm = Class.extend({ | |||
init: function(doctype, success_function){ | |||
init: function(doctype, after_insert){ | |||
this.doctype = doctype; | |||
this.success_function = success_function; | |||
this.setup(); | |||
this.after_insert = after_insert; | |||
}, | |||
setup: function(){ | |||
var me = this; | |||
frappe.model.with_doctype(this.doctype, function() { | |||
me.set_meta_and_mandatory_fields(); | |||
var validate_flag = me.validate_quick_entry(); | |||
if(!validate_flag){ | |||
me.render_dialog(); | |||
} | |||
setup: function() { | |||
let me = this; | |||
return new Promise(resolve => { | |||
frappe.model.with_doctype(this.doctype, function() { | |||
me.set_meta_and_mandatory_fields(); | |||
if(me.is_quick_entry()) { | |||
me.render_dialog(); | |||
resolve(me); | |||
} else { | |||
frappe.quick_entry = null; | |||
frappe.set_route('Form', me.doctype, me.doc.name) | |||
.then(() => resolve(me)); | |||
} | |||
}); | |||
}); | |||
}, | |||
@@ -25,34 +42,34 @@ frappe.ui.form.QuickEntryForm = Class.extend({ | |||
this.doc = frappe.model.get_new_doc(this.doctype, null, null, true); | |||
}, | |||
validate_quick_entry: function(){ | |||
is_quick_entry: function(){ | |||
if(this.meta.quick_entry != 1) { | |||
frappe.set_route('Form', this.doctype, this.doc.name); | |||
return true; | |||
return false; | |||
} | |||
var mandatory_flag = this.validate_mandatory_length(); | |||
var child_table_flag = this.validate_for_child_table(); | |||
if (mandatory_flag || child_table_flag){ | |||
return true; | |||
if (this.too_many_mandatory_fields() || this.has_child_table()) { | |||
return false; | |||
} | |||
this.validate_for_prompt_autoname(); | |||
return true; | |||
}, | |||
validate_mandatory_length: function(){ | |||
too_many_mandatory_fields: function(){ | |||
if(this.mandatory.length > 7) { | |||
// too many fields, show form | |||
frappe.set_route('Form', this.doctype, this.doc.name); | |||
return true; | |||
} | |||
return false; | |||
}, | |||
validate_for_child_table: function(){ | |||
if($.map(this.mandatory, function(d) { return d.fieldtype==='Table' ? d : null; }).length) { | |||
has_child_table: function(){ | |||
if($.map(this.mandatory, function(d) { | |||
return d.fieldtype==='Table' ? d : null; }).length) { | |||
// has mandatory table, quit! | |||
frappe.set_route('Form', this.doctype, this.doc.name); | |||
return true; | |||
} | |||
return false; | |||
}, | |||
validate_for_prompt_autoname: function(){ | |||
@@ -86,6 +103,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({ | |||
} | |||
}); | |||
this.dialog.onhide = () => frappe.quick_entry = null; | |||
this.dialog.show(); | |||
this.set_defaults(); | |||
}, | |||
@@ -93,44 +111,62 @@ frappe.ui.form.QuickEntryForm = Class.extend({ | |||
register_primary_action: function(){ | |||
var me = this; | |||
this.dialog.set_primary_action(__('Save'), function() { | |||
if(me.dialog.working) return; | |||
if(me.dialog.working) { | |||
return; | |||
} | |||
var data = me.dialog.get_values(); | |||
if(data) { | |||
me.dialog.working = true; | |||
var values = me.update_doc(); | |||
me.insert_document(values); | |||
me.insert(); | |||
} | |||
}); | |||
}, | |||
insert_document: function(values){ | |||
var me = this; | |||
frappe.call({ | |||
method: "frappe.client.insert", | |||
args: { | |||
doc: values | |||
}, | |||
callback: function(r) { | |||
me.dialog.hide(); | |||
// delete the old doc | |||
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name); | |||
var doc = r.message; | |||
if(me.success_function) { | |||
me.success_function(doc); | |||
} | |||
frappe.ui.form.update_calling_link(doc.name); | |||
}, | |||
error: function() { | |||
me.open_doc(); | |||
}, | |||
always: function() { | |||
me.dialog.working = false; | |||
}, | |||
freeze: true | |||
insert: function() { | |||
let me = this; | |||
return new Promise(resolve => { | |||
me.update_doc(); | |||
frappe.call({ | |||
method: "frappe.client.insert", | |||
args: { | |||
doc: me.dialog.doc | |||
}, | |||
callback: function(r) { | |||
me.dialog.hide(); | |||
// delete the old doc | |||
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name); | |||
me.dialog.doc = r.message; | |||
if(frappe._from_link) { | |||
frappe.ui.form.update_calling_link(me.dialog.doc.name); | |||
} else { | |||
if(me.after_insert) { | |||
me.after_insert(me.dialig.doc); | |||
} else { | |||
me.open_from_if_not_list(); | |||
} | |||
} | |||
}, | |||
error: function() { | |||
me.open_doc(); | |||
}, | |||
always: function() { | |||
me.dialog.working = false; | |||
resolve(me.dialog.doc); | |||
}, | |||
freeze: true | |||
}); | |||
}); | |||
}, | |||
open_from_if_not_list: function() { | |||
let route = frappe.get_route(); | |||
let doc = this.dialog.doc; | |||
if(route && !(route[0]==='List' && route[1]===doc.doctype)) { | |||
frappe.set_route('Form', doc.doctype, doc.name); | |||
} | |||
}, | |||
update_doc: function(){ | |||
var me = this; | |||
var data = this.dialog.get_values(true); | |||
@@ -226,8 +226,11 @@ frappe.ui.form.update_calling_link = function (newdoc) { | |||
// if from form, switch | |||
if (frappe._from_link.frm) { | |||
frappe.set_route("Form", frappe._from_link.frm.doctype, frappe._from_link.frm.docname); | |||
setTimeout(function () { frappe.utils.scroll_to(frappe._from_link_scrollY); }, 100); | |||
frappe.set_route("Form", | |||
frappe._from_link.frm.doctype, frappe._from_link.frm.docname) | |||
.then(() => { | |||
frappe.utils.scroll_to(frappe._from_link_scrollY); | |||
}); | |||
} | |||
frappe._from_link = null; | |||
@@ -55,8 +55,8 @@ frappe.ui.form.off = function(doctype, fieldname, handler) { | |||
} | |||
frappe.ui.form.trigger = function(doctype, fieldname, callback) { | |||
cur_frm.script_manager.trigger(fieldname, doctype, null, callback); | |||
frappe.ui.form.trigger = function(doctype, fieldname) { | |||
cur_frm.script_manager.trigger(fieldname, doctype); | |||
} | |||
frappe.ui.form.ScriptManager = Class.extend({ | |||
@@ -64,32 +64,76 @@ frappe.ui.form.ScriptManager = Class.extend({ | |||
$.extend(this, opts); | |||
}, | |||
make: function(ControllerClass) { | |||
this.frm.cscript = $.extend(this.frm.cscript, new ControllerClass({frm: this.frm})); | |||
this.frm.cscript = $.extend(this.frm.cscript, | |||
new ControllerClass({frm: this.frm})); | |||
}, | |||
trigger: function(event_name, doctype, name, callback) { | |||
var me = this; | |||
doctype = doctype || this.frm.doctype; | |||
name = name || this.frm.docname; | |||
var handlers = this.get_handlers(event_name, doctype, name, callback); | |||
if(callback) handlers.push(callback); | |||
trigger: function(event_name, doctype, name) { | |||
// trigger all the form level events that | |||
// are bound to this event_name | |||
let me = this; | |||
return new Promise(resolve => { | |||
doctype = doctype || this.frm.doctype; | |||
name = name || this.frm.docname; | |||
let tasks = []; | |||
let handlers = this.get_handlers(event_name, doctype); | |||
// helper for child table | |||
this.frm.selected_doc = frappe.get_doc(doctype, name); | |||
let runner = (_function, is_old_style) => { | |||
let _promise = null; | |||
if(is_old_style) { | |||
// old style arguments (doc, cdt, cdn) | |||
_promise = me.frm.cscript[_function](me.frm.doc, doctype, name); | |||
} else { | |||
// new style (frm, doctype, name) | |||
_promise = _function(me.frm, doctype, name); | |||
} | |||
// if the trigger returns a promise, return it, | |||
// or use the default promise frappe.after_ajax | |||
if (_promise && _promise.then) { | |||
return _promise; | |||
} else { | |||
return frappe.after_server_call(); | |||
} | |||
}; | |||
// make list of functions to be run serially | |||
handlers.new_style.forEach((_function) => { | |||
tasks.push(() => runner(_function, false)); | |||
}); | |||
this.frm.selected_doc = frappe.get_doc(doctype, name); | |||
handlers.old_style.forEach((_function) => { | |||
tasks.push(() => runner(_function, true)); | |||
}); | |||
return $.when.apply($, $.map(handlers, function(fn) { return fn(); })); | |||
// run them serially | |||
frappe.run_serially(tasks).then(resolve()); | |||
}); | |||
}, | |||
get_handlers: function(event_name, doctype, name, callback) { | |||
var handlers = []; | |||
var me = this; | |||
has_handlers: function(event_name, doctype) { | |||
let handlers = this.get_handlers(event_name, doctype); | |||
return handlers && (handlers.old_style.length || handlers.new_style.length); | |||
}, | |||
get_handlers: function(event_name, doctype) { | |||
// returns list of all functions to be called (old style and new style) | |||
let me = this; | |||
let handlers = { | |||
old_style: [], | |||
new_style: [] | |||
}; | |||
if(frappe.ui.form.handlers[doctype] && frappe.ui.form.handlers[doctype][event_name]) { | |||
$.each(frappe.ui.form.handlers[doctype][event_name], function(i, fn) { | |||
handlers.push(function() { return fn(me.frm, doctype, name) }); | |||
handlers.new_style.push(fn); | |||
}); | |||
} | |||
if(this.frm.cscript[event_name]) { | |||
handlers.push(function() { return me.frm.cscript[event_name](me.frm.doc, doctype, name); }); | |||
handlers.old_style.push(event_name); | |||
} | |||
if(this.frm.cscript["custom_" + event_name]) { | |||
handlers.push(function() { return me.frm.cscript["custom_" + event_name](me.frm.doc, doctype, name); }); | |||
handlers.old_style.push("custom_" + event_name); | |||
} | |||
return handlers; | |||
}, | |||
@@ -4,7 +4,6 @@ frappe.avatar = function(user, css_class, title) { | |||
if(user) { | |||
// desk | |||
var user_info = frappe.user_info(user); | |||
var image = frappe.utils.get_file_link(user_info.image); | |||
} else { | |||
// website | |||
user_info = { | |||
@@ -82,9 +81,11 @@ frappe.get_abbr = function(txt, max_length) { | |||
} | |||
frappe.gravatars = {}; | |||
frappe.get_gravatar = function(email_id) { | |||
frappe.get_gravatar = function(email_id, size = 0) { | |||
var param = size ? ('s=' + size) : 'd=retro'; | |||
if(!frappe.gravatars[email_id]) { | |||
frappe.gravatars[email_id] = "https://secure.gravatar.com/avatar/" + md5(email_id) + "?d=retro"; | |||
// TODO: check if gravatar exists | |||
frappe.gravatars[email_id] = "https://secure.gravatar.com/avatar/" + md5(email_id) + "?" + param; | |||
} | |||
return frappe.gravatars[email_id]; | |||
} | |||
@@ -313,32 +313,20 @@ $.extend(frappe.model, { | |||
frappe.create_routes = {}; | |||
frappe.new_doc = function (doctype, opts) { | |||
if(opts && $.isPlainObject(opts)) { frappe.route_options = opts; } | |||
frappe.model.with_doctype(doctype, function() { | |||
if(frappe.create_routes[doctype]) { | |||
frappe.set_route(frappe.create_routes[doctype]); | |||
} else { | |||
var trimmed_doctype = doctype.replace(/ /g, ''); | |||
var controller_name = "QuickEntryForm"; | |||
if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){ | |||
controller_name = trimmed_doctype + "QuickEntryForm"; | |||
return new Promise(resolve => { | |||
if(opts && $.isPlainObject(opts)) { | |||
frappe.route_options = opts; | |||
} | |||
frappe.model.with_doctype(doctype, function() { | |||
if(frappe.create_routes[doctype]) { | |||
frappe.set_route(frappe.create_routes[doctype]) | |||
.then(() => resolve()); | |||
} else { | |||
frappe.ui.form.make_quick_entry(doctype) | |||
.then(() => resolve()); | |||
} | |||
}); | |||
new frappe.ui.form[controller_name](doctype, function(doc) { | |||
//frappe.set_route('List', doctype); | |||
var title = doc.name; | |||
var title_field = frappe.get_meta(doc.doctype).title_field; | |||
if (title_field) { | |||
title = doc[title_field]; | |||
} | |||
var route = frappe.get_route(); | |||
if(route && !(route[0]==='List' && route[1]===doc.doctype)) { | |||
frappe.set_route('Form', doc.doctype, doc.name); | |||
} | |||
}); | |||
} | |||
}); | |||
} | |||
@@ -164,7 +164,8 @@ $.extend(frappe.meta, { | |||
}); | |||
if(!out) { | |||
frappe.msgprint(__('Warning: Unable to find {0} in any table related to {1}', [ | |||
// eslint-disable-next-line | |||
console.log(__('Warning: Unable to find {0} in any table related to {1}', [ | |||
key, __(doctype)])); | |||
} | |||
} | |||
@@ -327,9 +327,11 @@ $.extend(frappe.model, { | |||
set_value: function(doctype, docname, fieldname, value, fieldtype) { | |||
/* help: Set a value locally (if changed) and execute triggers */ | |||
var doc = locals[doctype] && locals[doctype][docname]; | |||
var to_update = fieldname; | |||
let tasks = []; | |||
if(!$.isPlainObject(to_update)) { | |||
to_update = {}; | |||
to_update[fieldname] = value; | |||
@@ -343,14 +345,16 @@ $.extend(frappe.model, { | |||
} | |||
doc[key] = value; | |||
frappe.model.trigger(key, value, doc); | |||
tasks.push(() => frappe.model.trigger(key, value, doc)); | |||
} else { | |||
// execute link triggers (want to reselect to execute triggers) | |||
if(fieldtype=="Link" && doc) { | |||
frappe.model.trigger(key, value, doc); | |||
tasks.push(() => frappe.model.trigger(key, value, doc)); | |||
} | |||
} | |||
}); | |||
return frappe.run_serially(tasks); | |||
}, | |||
on: function(doctype, fieldname, fn) { | |||
@@ -371,21 +375,34 @@ $.extend(frappe.model, { | |||
}, | |||
trigger: function(fieldname, value, doc) { | |||
var run = function(events, event_doc) { | |||
let tasks = []; | |||
var runner = function(events, event_doc) { | |||
$.each(events || [], function(i, fn) { | |||
fn && fn(fieldname, value, event_doc || doc); | |||
if(fn) { | |||
let _promise = fn(fieldname, value, event_doc || doc); | |||
// if the trigger returns a promise, return it, | |||
// or use the default promise frappe.after_ajax | |||
if (_promise && _promise.then) { | |||
return _promise; | |||
} else { | |||
return frappe.after_server_call(); | |||
} | |||
} | |||
}); | |||
}; | |||
if(frappe.model.events[doc.doctype]) { | |||
tasks.push(() => { | |||
return runner(frappe.model.events[doc.doctype][fieldname]); | |||
}); | |||
// field-level | |||
run(frappe.model.events[doc.doctype][fieldname]); | |||
// doctype-level | |||
run(frappe.model.events[doc.doctype]['*']); | |||
tasks.push(() => { | |||
runner(frappe.model.events[doc.doctype]['*']); | |||
}); | |||
} | |||
frappe.run_serially(tasks); | |||
}, | |||
get_doc: function(doctype, name) { | |||
@@ -26,4 +26,4 @@ frappe.provide("frappe.utils"); | |||
frappe.provide("frappe.ui"); | |||
frappe.provide("frappe.modules"); | |||
frappe.provide("frappe.templates"); | |||
frappe.provide("frappe.test_data"); |
@@ -23,7 +23,7 @@ function get_query_params(query_string) { | |||
} | |||
if (key in query_params) { | |||
if (typeof query_params[key] === undefined) { | |||
if (typeof query_params[key] === "undefined") { | |||
query_params[key] = []; | |||
} else if (typeof query_params[key] === "string") { | |||
query_params[key] = [query_params[key]]; | |||
@@ -10,8 +10,9 @@ frappe.request.waiting_for_ajax = []; | |||
// generic server call (call page, object) | |||
frappe.call = function(opts) { | |||
if(opts.quiet) | |||
if(opts.quiet) { | |||
opts.no_spinner = true; | |||
} | |||
var args = $.extend({}, opts.args); | |||
// cmd | |||
@@ -302,13 +303,31 @@ frappe.request.cleanup = function(opts, r) { | |||
} | |||
} | |||
frappe.after_ajax = function(fn) { | |||
frappe.after_server_call = () => { | |||
if(frappe.request.ajax_count) { | |||
frappe.request.waiting_for_ajax.push(fn); | |||
return new Promise(resolve => { | |||
frappe.request.waiting_for_ajax.push(() => { | |||
resolve(); | |||
}); | |||
}); | |||
} else { | |||
fn(); | |||
return null; | |||
} | |||
} | |||
}; | |||
frappe.after_ajax = function(fn) { | |||
return new Promise(resolve => { | |||
if(frappe.request.ajax_count) { | |||
frappe.request.waiting_for_ajax.push(() => { | |||
if(fn) fn(); | |||
resolve(); | |||
}); | |||
} else { | |||
if(fn) fn(); | |||
resolve(); | |||
} | |||
}); | |||
}; | |||
frappe.request.report_error = function(xhr, request_opts) { | |||
var data = JSON.parse(xhr.responseText); | |||
@@ -123,24 +123,31 @@ frappe.get_route_str = function(route) { | |||
} | |||
frappe.set_route = function() { | |||
var params = arguments; | |||
if(params.length===1 && $.isArray(params[0])) { | |||
params = params[0]; | |||
} | |||
var route = $.map(params, function(a) { | |||
if($.isPlainObject(a)) { | |||
frappe.route_options = a; | |||
return null; | |||
} else { | |||
return a; | |||
// return a ? encodeURIComponent(a) : null; | |||
return new Promise(resolve => { | |||
var params = arguments; | |||
if(params.length===1 && $.isArray(params[0])) { | |||
params = params[0]; | |||
} | |||
}).join('/'); | |||
window.location.hash = route; | |||
// Set favicon (app.js) | |||
frappe.app.set_favicon && frappe.app.set_favicon(); | |||
var route = $.map(params, function(a) { | |||
if($.isPlainObject(a)) { | |||
frappe.route_options = a; | |||
return null; | |||
} else { | |||
return a; | |||
// return a ? encodeURIComponent(a) : null; | |||
} | |||
}).join('/'); | |||
window.location.hash = route; | |||
// Set favicon (app.js) | |||
frappe.app.set_favicon && frappe.app.set_favicon(); | |||
setTimeout(() => { | |||
frappe.after_ajax(() => { | |||
resolve(); | |||
}); | |||
}, 100); | |||
}); | |||
} | |||
frappe.set_re_route = function() { | |||
@@ -3,8 +3,6 @@ | |||
$(document).on("toolbar_setup", function() { | |||
var help_links = []; | |||
var support_link = "#upgrade"; | |||
var chat_link = "#upgrade"; | |||
var limits = frappe.boot.limits; | |||
if(frappe.boot.expiry_message) { | |||
@@ -24,7 +22,7 @@ $(document).on("toolbar_setup", function() { | |||
} | |||
if(limits.support_email) { | |||
support_link = 'mailto:'+frappe.boot.limits.support_email; | |||
var support_link = 'mailto:'+frappe.boot.limits.support_email; | |||
help_links.push('<li><a href="'+support_link+'">' + frappe._('Email Support') + '</a></li>'); | |||
} | |||
@@ -221,15 +221,10 @@ frappe.ui.BaseList = Class.extend({ | |||
}); | |||
} | |||
this.page.page_form.on('change', ':input', function() { | |||
me.refresh(true); | |||
}); | |||
this.standard_filters_added = true; | |||
}, | |||
update_standard_filters: function(filters) { | |||
let values = {}; | |||
let me = this; | |||
for(let key in this.page.fields_dict) { | |||
let field = this.page.fields_dict[key]; | |||
@@ -466,7 +461,7 @@ frappe.ui.BaseList = Class.extend({ | |||
set_filter: function (fieldname, label, no_run, no_duplicate) { | |||
var filter = this.filter_list.get_filter(fieldname); | |||
if (filter) { | |||
var value = cstr(filter.field.get_parsed_value()); | |||
var value = cstr(filter.field.get_value()); | |||
if (value.includes(label)) { | |||
// already set | |||
return false | |||
@@ -41,6 +41,10 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ | |||
}) | |||
} | |||
}, | |||
add_fields: function(fields) { | |||
this.render(fields); | |||
this.refresh_fields(fields); | |||
}, | |||
first_button: false, | |||
catch_enter_as_submit: function() { | |||
var me = this; | |||
@@ -65,8 +69,8 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ | |||
var errors = []; | |||
for(var key in this.fields_dict) { | |||
var f = this.fields_dict[key]; | |||
if(f.get_parsed_value) { | |||
var v = f.get_parsed_value(); | |||
if(f.get_value) { | |||
var v = f.get_value(); | |||
if(f.df.reqd && is_null(v)) | |||
errors.push(__(f.df.label)); | |||
@@ -86,14 +90,21 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ | |||
}, | |||
get_value: function(key) { | |||
var f = this.fields_dict[key]; | |||
return f && (f.get_parsed_value ? f.get_parsed_value() : null); | |||
return f && (f.get_value ? f.get_value() : null); | |||
}, | |||
set_value: function(key, val){ | |||
var f = this.fields_dict[key]; | |||
if(f) { | |||
f.set_input(val); | |||
this.refresh_dependency(); | |||
} | |||
return new Promise(resolve => { | |||
var f = this.fields_dict[key]; | |||
if(f) { | |||
f.set_value(val).then(() => { | |||
f.set_input(val); | |||
this.refresh_dependency(); | |||
resolve(); | |||
}); | |||
} else { | |||
resolve(); | |||
} | |||
}); | |||
}, | |||
set_input: function(key, val) { | |||
return this.set_value(key, val); | |||
@@ -112,5 +123,5 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ | |||
f.set_input(f.df['default'] || ''); | |||
} | |||
} | |||
} | |||
}, | |||
}); |
@@ -19,8 +19,11 @@ | |||
<div class="col-sm-6 col-xs-12"> | |||
<div class="filter_field pull-left" style="width: calc(100% - 70px)"></div> | |||
<div class="filter-actions pull-left"> | |||
<a class="set-filter-and-run btn btn-primary pull-left"><i class=" fa fa-check"></i></a> | |||
<a class="small grey remove-filter pull-left"><i class="octicon octicon-trashcan visible-xs"></i> | |||
<a class="set-filter-and-run btn btn-sm btn-primary pull-left"> | |||
<i class=" fa fa-check visible-xs"></i> | |||
<span class="hidden-xs">{%= __("Apply") %}</span></a> | |||
<a class="small grey remove-filter pull-left"> | |||
<i class="octicon octicon-trashcan visible-xs"></i> | |||
<span class="hidden-xs">{%= __("Remove") %}</span></a> | |||
</div> | |||
<div class="clearfix"></div> | |||
@@ -251,8 +251,12 @@ frappe.ui.Filter = Class.extend({ | |||
else if(value==1) value = 'Yes'; | |||
} | |||
if(condition) this.wrapper.find('.condition').val(condition).change(); | |||
if(value!=null) this.field.set_input(value); | |||
if(condition) { | |||
this.wrapper.find('.condition').val(condition).change(); | |||
} | |||
if(value!=null) { | |||
this.field.set_value(value); | |||
} | |||
}, | |||
set_field: function(doctype, fieldname, fieldtype, condition) { | |||
@@ -294,7 +298,7 @@ frappe.ui.Filter = Class.extend({ | |||
// save old text | |||
var old_text = null; | |||
if(me.field) { | |||
old_text = me.field.get_parsed_value(); | |||
old_text = me.field.get_value(); | |||
} | |||
var field_area = me.wrapper.find('.filter_field').empty().get(0); | |||
@@ -376,7 +380,7 @@ frappe.ui.Filter = Class.extend({ | |||
}, | |||
get_selected_value: function() { | |||
var val = this.field.get_parsed_value(); | |||
var val = this.field.get_value(); | |||
if(typeof val==='string') { | |||
val = strip(val); | |||
@@ -451,8 +455,6 @@ frappe.ui.Filter = Class.extend({ | |||
value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value; | |||
} else if(this.field.df.original_type==="Check") { | |||
value = {0:"No", 1:"Yes"}[cint(value)]; | |||
} else if (in_list(["Date", "Datetime"], this.field.df.fieldtype)) { | |||
value = frappe.datetime.str_to_user(value); | |||
} else { | |||
value = this.field.get_value(); | |||
} | |||
@@ -0,0 +1,16 @@ | |||
frappe.find = { | |||
page_primary_action: () => { | |||
return $('.page-actions:visible .btn-primary'); | |||
}, | |||
field: (fieldname, value) => { | |||
return new Promise(resolve => { | |||
let input = $(`[data-fieldname="${fieldname}"] :input`); | |||
if(value) { | |||
input.val(value).trigger('change'); | |||
frappe.after_ajax(() => { resolve(input); }); | |||
} else { | |||
resolve(input); | |||
} | |||
}); | |||
} | |||
}; |
@@ -21,10 +21,6 @@ frappe.ui.keys.get_key = function(e) { | |||
var keycode = e.keyCode || e.which; | |||
var key = frappe.ui.keys.key_map[keycode] || String.fromCharCode(keycode); | |||
if(key.substr(0, 5) === 'Arrow') { | |||
// ArrowDown -> down | |||
key = key.substr(5).toLowerCase(); | |||
} | |||
if(e.ctrlKey || e.metaKey) { | |||
// add ctrl+ the key | |||
key = 'ctrl+' + key; | |||
@@ -100,7 +96,12 @@ frappe.ui.keys.key_map = { | |||
17: 'ctrl', | |||
91: 'meta', | |||
18: 'alt', | |||
27: 'escape' | |||
27: 'escape', | |||
37: 'left', | |||
39: 'right', | |||
38: 'up', | |||
40: 'down', | |||
32: 'space' | |||
} | |||
// keyCode map | |||
@@ -187,7 +187,7 @@ frappe.search.AwesomeBar = Class.extend({ | |||
routes.push(str_route); | |||
} else { | |||
var old = routes.indexOf(str_route); | |||
if(out[old].index > option.index) { | |||
if(out[old].index < option.index) { | |||
out[old] = option; | |||
} | |||
} | |||
@@ -28,7 +28,7 @@ | |||
{%= __("My Settings") %}</a></li> | |||
<li><a href="#" onclick="return frappe.ui.toolbar.clear_cache();"> | |||
{%= __("Reload") %}</a></li> | |||
<li><a href="/index" target="_blank"> | |||
<li><a href="/index" target="_blank" rel="noopener noreferrer"> | |||
{%= __("View Website") %}</a></li> | |||
<li><a href="#background_jobs"> | |||
{%= __("Background Jobs") %}</a></li> | |||
@@ -54,7 +54,7 @@ | |||
<li class="divider"></li> | |||
<li> | |||
<a data-link-type="documentation" | |||
data-path="/documentation/index" target="_blank">{{ __("Documentation") }}</a> | |||
data-path="/documentation/index" target="_blank" rel="noopener noreferrer">{{ __("Documentation") }}</a> | |||
</li> | |||
<li class="divider documentation-links"></li> | |||
<li><a href="#" onclick="return frappe.ui.toolbar.show_about();"> | |||
@@ -152,7 +152,7 @@ frappe.search.utils = { | |||
type: "New", | |||
label: __("New {0}", [me.bolden_match_part(__(item), keywords)]), | |||
value: __("New {0}", [__(item)]), | |||
index: level + 0.01, | |||
index: level + 0.015, | |||
match: item, | |||
onclick: function () { frappe.new_doc(match, true); } | |||
}); | |||
@@ -337,7 +337,7 @@ frappe.views.Calendar = Class.extend({ | |||
if(this.filters) { | |||
$.each(this.filters, function(i, df) { | |||
filter_vals[df.fieldname || df.label] = | |||
me.page.fields_dict[df.fieldname || df.label].get_parsed_value(); | |||
me.page.fields_dict[df.fieldname || df.label].get_value(); | |||
}); | |||
} | |||
return filter_vals; | |||
@@ -767,7 +767,6 @@ frappe.views.TreeGridReport = frappe.views.GridReportWithPlot.extend({ | |||
}, | |||
tree_formatter: function (row, cell, value, columnDef, dataContext) { | |||
var me = frappe.cur_grid_report; | |||
value = value.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); | |||
var data = me.data; | |||
var spacer = "<span style='display:inline-block;height:1px;width:" + | |||
(15 * dataContext["indent"]) + "px'></span>"; | |||
@@ -228,7 +228,6 @@ frappe.views.QueryReport = Class.extend({ | |||
//Render Report in HTML | |||
var html = frappe.render_template("print_template", { | |||
columns:columns, | |||
content:content, | |||
title:__(this.report_name), | |||
base_url: base_url, | |||
@@ -424,7 +423,7 @@ frappe.views.QueryReport = Class.extend({ | |||
var filters = {}; | |||
var mandatory_fields = []; | |||
$.each(this.filters || [], function(i, f) { | |||
var v = f.get_parsed_value(); | |||
var v = f.get_value(); | |||
// TODO: hidden fields dont have $input | |||
if(f.df.hidden) v = f.value; | |||
if(v === '%') v = null; | |||
@@ -264,7 +264,7 @@ frappe.views.ReportView = frappe.ui.BaseList.extend({ | |||
return { | |||
doctype: this.doctype, | |||
fields: $.map(this.columns, function(v) { return me.get_full_column_name(v) }), | |||
fields: $.map(this.columns || [], function(v) { return me.get_full_column_name(v); }), | |||
order_by: this.get_order_by(), | |||
add_total_row: this.add_total_row, | |||
filters: filters, | |||
@@ -1,27 +0,0 @@ | |||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
// MIT License. See license.txt | |||
frappe.standard_pages["test-runner"] = function() { | |||
var wrapper = frappe.container.add_page('test-runner'); | |||
frappe.ui.make_app_page({ | |||
parent: wrapper, | |||
single_column: true, | |||
title: __("Test Runner") | |||
}); | |||
$("<div id='qunit'></div>").appendTo($(wrapper).find(".layout-main")); | |||
var route = frappe.get_route(); | |||
if(route.length < 2) { | |||
frappe.msgprint(__("To run a test add the module name in the route after '{0}'. For example, {1}", ['test-runner/', '#test-runner/lib/js/frappe/test_app.js'])); | |||
return; | |||
} | |||
var requires = ["assets/frappe/js/lib/jquery/qunit.js", | |||
"assets/frappe/js/lib/jquery/qunit.css"].concat(route.splice(1).join("/")); | |||
frappe.require(requires, function() { | |||
QUnit.load(); | |||
}); | |||
} |
@@ -368,13 +368,13 @@ _f.Frm.prototype.set_read_only = function() { | |||
} | |||
_f.Frm.prototype.trigger = function(event) { | |||
this.script_manager.trigger(event); | |||
return this.script_manager.trigger(event); | |||
}; | |||
_f.Frm.prototype.get_formatted = function(fieldname) { | |||
return frappe.format(this.doc[fieldname], | |||
frappe.meta.get_docfield(this.doctype, fieldname, this.docname), | |||
{no_icon:true}, this.doc); | |||
frappe.meta.get_docfield(this.doctype, fieldname, this.docname), | |||
{no_icon:true}, this.doc); | |||
} | |||
_f.Frm.prototype.open_grid_row = function() { | |||
@@ -606,18 +606,19 @@ _f.Frm.prototype.setnewdoc = function() { | |||
var me = this; | |||
// hide any open grid | |||
this.script_manager.trigger("before_load", this.doctype, this.docname, function() { | |||
me.script_manager.trigger("onload"); | |||
me.opendocs[me.docname] = true; | |||
me.render_form(); | |||
this.script_manager.trigger("before_load", this.doctype, this.docname) | |||
.then(() => { | |||
me.script_manager.trigger("onload"); | |||
me.opendocs[me.docname] = true; | |||
me.render_form(); | |||
frappe.after_ajax(function() { | |||
me.trigger_link_fields(); | |||
}); | |||
frappe.after_ajax(function() { | |||
me.trigger_link_fields(); | |||
frappe.breadcrumbs.add(me.meta.module, me.doctype) | |||
}); | |||
frappe.breadcrumbs.add(me.meta.module, me.doctype) | |||
}); | |||
// update seen | |||
if(this.meta.track_seen) { | |||
$('.list-id[data-name="'+ me.docname +'"]').addClass('seen'); | |||
@@ -705,17 +706,21 @@ Object.defineProperty(window, 'validated', { | |||
}); | |||
_f.Frm.prototype.save = function(save_action, callback, btn, on_error) { | |||
btn && $(btn).prop("disabled", true); | |||
$(document.activeElement).blur(); | |||
let me = this; | |||
return new Promise(resolve => { | |||
btn && $(btn).prop("disabled", true); | |||
$(document.activeElement).blur(); | |||
frappe.ui.form.close_grid_form(); | |||
frappe.ui.form.close_grid_form(); | |||
// let any pending js process finish | |||
var me = this; | |||
setTimeout(function() { me._save(save_action, callback, btn, on_error) }, 100); | |||
// let any pending js process finish | |||
setTimeout(function() { | |||
me._save(save_action, callback, btn, on_error, resolve); | |||
}, 100); | |||
}); | |||
} | |||
_f.Frm.prototype._save = function(save_action, callback, btn, on_error) { | |||
_f.Frm.prototype._save = function(save_action, callback, btn, on_error, resolve) { | |||
var me = this; | |||
if(!save_action) save_action = "Save"; | |||
this.validate_form_action(save_action); | |||
@@ -736,26 +741,29 @@ _f.Frm.prototype._save = function(save_action, callback, btn, on_error) { | |||
on_error(); | |||
} | |||
callback && callback(r); | |||
resolve(); | |||
} | |||
if(save_action != "Update") { | |||
// validate | |||
frappe.validated = true; | |||
$.when(this.script_manager.trigger("validate"), this.script_manager.trigger("before_save")) | |||
.done(function() { | |||
// done is called after all ajaxes in validate & before_save are completed :) | |||
if(!frappe.validated) { | |||
btn && $(btn).prop("disabled", false); | |||
if(on_error) { | |||
on_error(); | |||
} | |||
return; | |||
} | |||
Promise.all([ | |||
this.script_manager.trigger("validate"), | |||
this.script_manager.trigger("before_save") | |||
]).then(() => { | |||
// done is called after all ajaxes in validate & before_save are completed :) | |||
frappe.ui.form.save(me, save_action, after_save, btn); | |||
}); | |||
if(!frappe.validated) { | |||
btn && $(btn).prop("disabled", false); | |||
if(on_error) { | |||
on_error(); | |||
} | |||
resolve(); | |||
return; | |||
} | |||
frappe.ui.form.save(me, save_action, after_save, btn); | |||
}); | |||
} else { | |||
frappe.ui.form.save(me, save_action, after_save, btn); | |||
} | |||
@@ -767,7 +775,7 @@ _f.Frm.prototype.savesubmit = function(btn, callback, on_error) { | |||
this.validate_form_action("Submit"); | |||
frappe.confirm(__("Permanently Submit {0}?", [this.docname]), function() { | |||
frappe.validated = true; | |||
me.script_manager.trigger("before_submit").done(function() { | |||
me.script_manager.trigger("before_submit").then(function() { | |||
if(!frappe.validated) { | |||
if(on_error) | |||
on_error(); | |||
@@ -790,7 +798,7 @@ _f.Frm.prototype.savecancel = function(btn, callback, on_error) { | |||
this.validate_form_action('Cancel'); | |||
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), function() { | |||
frappe.validated = true; | |||
me.script_manager.trigger("before_cancel").done(function() { | |||
me.script_manager.trigger("before_cancel").then(function() { | |||
if(!frappe.validated) { | |||
if(on_error) | |||
on_error(); | |||
@@ -964,10 +972,6 @@ _f.Frm.prototype.validate_form_action = function(action) { | |||
} | |||
}; | |||
_f.Frm.prototype.get_handlers = function(fieldname, doctype, docname) { | |||
return this.script_manager.get_handlers(fieldname, doctype || this.doctype, docname || this.docname) | |||
} | |||
_f.Frm.prototype.has_perm = function(ptype) { | |||
return frappe.perm.has_perm(this.doctype, 0, ptype, this.doc); | |||
} | |||
@@ -1,12 +1,12 @@ | |||
/*! | |||
* QUnit 2.0.0 | |||
* QUnit 2.3.3 | |||
* https://qunitjs.com/ | |||
* | |||
* Copyright jQuery Foundation and other contributors | |||
* Released under the MIT license | |||
* https://jquery.org/license | |||
* | |||
* Date: 2016-06-16T17:09Z | |||
* Date: 2017-06-02T14:07Z | |||
*/ | |||
/** Font Family and Sizes */ | |||
@@ -226,7 +226,8 @@ | |||
#qunit-tests li.running, | |||
#qunit-tests li.pass, | |||
#qunit-tests li.fail, | |||
#qunit-tests li.skipped { | |||
#qunit-tests li.skipped, | |||
#qunit-tests li.aborted { | |||
display: list-item; | |||
} | |||
@@ -235,7 +236,7 @@ | |||
} | |||
#qunit-tests.hidepass li.running, | |||
#qunit-tests.hidepass li.pass { | |||
#qunit-tests.hidepass li.pass:not(.todo) { | |||
visibility: hidden; | |||
position: absolute; | |||
width: 0; | |||
@@ -374,12 +375,16 @@ | |||
#qunit-banner.qunit-fail { background-color: #EE5757; } | |||
/*** Aborted tests */ | |||
#qunit-tests .aborted { color: #000; background-color: orange; } | |||
/*** Skipped tests */ | |||
#qunit-tests .skipped { | |||
background-color: #EBECE9; | |||
} | |||
#qunit-tests .qunit-todo-label, | |||
#qunit-tests .qunit-skipped-label { | |||
background-color: #F4FF77; | |||
display: inline-block; | |||
@@ -390,19 +395,35 @@ | |||
margin: -0.4em 0.4em -0.4em 0; | |||
} | |||
#qunit-tests .qunit-todo-label { | |||
background-color: #EEE; | |||
} | |||
/** Result */ | |||
#qunit-testresult { | |||
padding: 0.5em 1em 0.5em 1em; | |||
color: #2B81AF; | |||
background-color: #D2E0E6; | |||
border-bottom: 1px solid #FFF; | |||
} | |||
#qunit-testresult .clearfix { | |||
height: 0; | |||
clear: both; | |||
} | |||
#qunit-testresult .module-name { | |||
font-weight: 700; | |||
} | |||
#qunit-testresult-display { | |||
padding: 0.5em 1em 0.5em 1em; | |||
width: 85%; | |||
float:left; | |||
} | |||
#qunit-testresult-controls { | |||
padding: 0.5em 1em 0.5em 1em; | |||
width: 10%; | |||
float:left; | |||
} | |||
/** Fixture */ | |||
@@ -412,4 +433,4 @@ | |||
left: -10000px; | |||
width: 1000px; | |||
height: 1000px; | |||
} | |||
} |
@@ -623,6 +623,7 @@ h6.uppercase, .h6.uppercase { | |||
.like-disabled-input.for-description { | |||
font-weight: normal; | |||
font-size: 12px; | |||
} | |||
.frappe-control& { | |||
@@ -666,6 +667,21 @@ select.form-control { | |||
background-color: @extra-light-yellow; | |||
} | |||
.form-control[data-fieldtype="Password"] { | |||
position: inherit; | |||
} | |||
.password-strength-indicator { | |||
float: right; | |||
padding: 15px; | |||
margin-top: -41px; | |||
margin-right: -7px; | |||
} | |||
.password-strength-message { | |||
margin-top: -10px; | |||
} | |||
.form-headline { | |||
padding: 0px 15px; | |||
margin: 0px; | |||
@@ -1,15 +1,12 @@ | |||
{% if parents and parents|length > 0 and (parents[-1].route) %} | |||
<ul class="breadcrumb"> | |||
<li> | |||
<span class="fa fa-angle-left"></span> | |||
<a href="{{ url_prefix }}{{ parents[-1].route | abs_url }}"> | |||
{{ _(parents[-1].title) or _(parents[-1].label) or "" }}</a> | |||
</li> | |||
{# | |||
<!-- {% for parent in parents %} | |||
<li><a href="{{ parent.name }}">{{ parent.page_title or parent.title or "" }}</a></li> | |||
{% if parents and parents|length > 0 %} | |||
<ul class="breadcrumb" itemscope itemtype="http://data-vocabulary.org/Breadcrumb"> | |||
{% for parent in parents %} | |||
<li> | |||
<a href="{{ url_prefix }}{{ parent.route | abs_url }}" itemprop="url"> | |||
<span itemprop="title">{{ parent.title or parent.label or parent.name or "" }}</span> | |||
</a> | |||
</li> | |||
{% endfor %} | |||
<li class="active">{{ title or "" }}</li> --> | |||
#} | |||
<li class="active"><span itemprop="title">{{ title or "" }}</span></li> | |||
</ul> | |||
{% endif %} |
@@ -22,7 +22,8 @@ def xmlrunner_wrapper(output): | |||
return xmlrunner.XMLTestRunner(*args, **kwargs) | |||
return _runner | |||
def main(app=None, module=None, doctype=None, verbose=False, tests=(), force=False, profile=False, junit_xml_output=None): | |||
def main(app=None, module=None, doctype=None, verbose=False, tests=(), | |||
force=False, profile=False, junit_xml_output=None, ui_tests=False): | |||
global unittest_runner | |||
xmloutput_fh = None | |||
@@ -57,7 +58,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(), force=Fal | |||
elif module: | |||
ret = run_tests_for_module(module, verbose, tests, profile) | |||
else: | |||
ret = run_all_tests(app, verbose, profile) | |||
ret = run_all_tests(app, verbose, profile, ui_tests) | |||
frappe.db.commit() | |||
@@ -80,7 +81,7 @@ def set_test_email_config(): | |||
"admin_password": "admin" | |||
}) | |||
def run_all_tests(app=None, verbose=False, profile=False): | |||
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False): | |||
import os | |||
apps = [app] if app else frappe.get_installed_apps() | |||
@@ -95,9 +96,11 @@ def run_all_tests(app=None, verbose=False, profile=False): | |||
# print path | |||
for filename in files: | |||
filename = cstr(filename) | |||
if filename.startswith("test_") and filename.endswith(".py"): | |||
if filename.startswith("test_") and filename.endswith(".py")\ | |||
and filename != 'test_runner.py': | |||
# print filename[:-3] | |||
_add_test(app, path, filename, verbose, test_suite=test_suite) | |||
_add_test(app, path, filename, verbose, | |||
test_suite, ui_tests) | |||
if profile: | |||
pr = cProfile.Profile() | |||
@@ -163,7 +166,7 @@ def _run_unittest(module, verbose=False, tests=(), profile=False): | |||
return out | |||
def _add_test(app, path, filename, verbose, test_suite=None): | |||
def _add_test(app, path, filename, verbose, test_suite=None, ui_tests=False): | |||
import os | |||
if os.path.sep.join(["doctype", "doctype", "boilerplate"]) in path: | |||
@@ -179,8 +182,9 @@ def _add_test(app, path, filename, verbose, test_suite=None): | |||
relative_path=relative_path.replace('/', '.'), module_name=filename[:-3]) | |||
module = frappe.get_module(module_name) | |||
is_ui_test = True if hasattr(module, 'TestDriver') else False | |||
if getattr(module, "selenium_tests", False) and not frappe.conf.run_selenium_tests: | |||
if is_ui_test != ui_tests: | |||
return | |||
if not test_suite: | |||
@@ -325,3 +329,5 @@ def print_mandatory_fields(doctype): | |||
for d in meta.get("fields", {"reqd":1}): | |||
print(d.parent + ":" + d.fieldname + " | " + d.fieldtype + " | " + (d.options or "")) | |||
print() | |||
@@ -1,27 +0,0 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# MIT License. See license.txt | |||
from __future__ import unicode_literals | |||
import unittest, frappe | |||
from frappe.utils import sel | |||
selenium_tests = True | |||
class TestLogin(unittest.TestCase): | |||
def setUp(self): | |||
return | |||
sel.login() | |||
def test_login(self): | |||
return | |||
self.assertEquals(sel._driver.current_url, sel.get_localhost() + "/desk") | |||
def test_to_do(self): | |||
return | |||
# too unpredictable in travis | |||
sel.go_to_module("ToDo") | |||
sel.primary_action() | |||
sel.wait_for_page("Form/ToDo") | |||
sel.set_field("description", "test description", "textarea") | |||
sel.primary_action() | |||
self.assertTrue(sel.wait_for_state("clean")) |
@@ -15,11 +15,12 @@ from frappe.core.page.permission_manager.permission_manager import update, reset | |||
test_records = frappe.get_test_records('Blog Post') | |||
test_dependencies = ["User"] | |||
test_dependencies = ["User", "Contact", "Salutation"] | |||
class TestPermissions(unittest.TestCase): | |||
def setUp(self): | |||
frappe.clear_cache(doctype="Blog Post") | |||
frappe.clear_cache(doctype="Contact") | |||
user = frappe.get_doc("User", "test1@example.com") | |||
user.add_roles("Website Manager") | |||
@@ -27,8 +28,13 @@ class TestPermissions(unittest.TestCase): | |||
user = frappe.get_doc("User", "test2@example.com") | |||
user.add_roles("Blogger") | |||
user = frappe.get_doc("User", "test3@example.com") | |||
user.add_roles("Sales User") | |||
reset('Blogger') | |||
reset('Blog Post') | |||
reset('Contact') | |||
reset('Salutation') | |||
self.set_ignore_user_permissions_if_missing(0) | |||
@@ -41,18 +47,30 @@ class TestPermissions(unittest.TestCase): | |||
clear_user_permissions_for_doctype("Blog Category") | |||
clear_user_permissions_for_doctype("Blog Post") | |||
clear_user_permissions_for_doctype("Blogger") | |||
clear_user_permissions_for_doctype("Contact") | |||
clear_user_permissions_for_doctype("Salutation") | |||
reset('Blogger') | |||
reset('Blog Post') | |||
reset('Contact') | |||
reset('Salutation') | |||
self.set_ignore_user_permissions_if_missing(0) | |||
def set_ignore_user_permissions_if_missing(self, ignore): | |||
@staticmethod | |||
def set_ignore_user_permissions_if_missing(ignore): | |||
ss = frappe.get_doc("System Settings") | |||
ss.ignore_user_permissions_if_missing = ignore | |||
ss.flags.ignore_mandatory = 1 | |||
ss.save() | |||
@staticmethod | |||
def set_strict_user_permissions(ignore): | |||
ss = frappe.get_doc("System Settings") | |||
ss.apply_strict_user_permissions = ignore | |||
ss.flags.ignore_mandatory = 1 | |||
ss.save() | |||
def test_basic_permission(self): | |||
post = frappe.get_doc("Blog Post", "-test-blog-post") | |||
self.assertTrue(post.has_permission("read")) | |||
@@ -275,6 +293,30 @@ class TestPermissions(unittest.TestCase): | |||
frappe.set_user("test2@example.com") | |||
self.assertTrue(doc.has_permission("write")) | |||
def test_strict_user_permissions(self): | |||
"""If `Strict User Permissions` is checked in System Settings, show records even if User Permissions are missing for a linked doctype""" | |||
set_user_permission_doctypes(doctype="Contact", role="Sales User", | |||
apply_user_permissions=1, user_permission_doctypes=['Salutation']) | |||
set_user_permission_doctypes(doctype="Salutation", role="All", | |||
apply_user_permissions=1, user_permission_doctypes=['Salutation']) | |||
frappe.set_user("Administrator") | |||
frappe.permissions.add_user_permission("Salutation", "Mr", "test3@example.com") | |||
self.set_strict_user_permissions(0) | |||
frappe.set_user("test3@example.com") | |||
self.assertEquals(len(frappe.get_list("Contact")),2) | |||
frappe.set_user("Administrator") | |||
self.set_strict_user_permissions(1) | |||
frappe.set_user("test3@example.com") | |||
self.assertTrue(len(frappe.get_list("Contact")),1) | |||
frappe.set_user("Administrator") | |||
self.set_strict_user_permissions(0) | |||
def set_user_permission_doctypes(doctype, role, apply_user_permissions, user_permission_doctypes): | |||
user_permission_doctypes = None if not user_permission_doctypes else json.dumps(user_permission_doctypes) | |||
@@ -1,19 +0,0 @@ | |||
module.exports = { | |||
beforeEach: browser => { | |||
browser | |||
.url(browser.launch_url + '/login') | |||
.waitForElementVisible('body', 5000) | |||
}, | |||
'Login': browser => { | |||
browser | |||
.assert.title('Login') | |||
.assert.visible('#login_email', 'Check if login box is visible') | |||
.setValue("#login_email", "Administrator") | |||
.setValue("#login_password", "admin") | |||
.click(".btn-login") | |||
.waitForElementVisible("#body_div", 15000); | |||
}, | |||
after: browser => { | |||
browser.end(); | |||
}, | |||
}; |
@@ -0,0 +1,43 @@ | |||
var login = require("./login.js")['Login']; | |||
module.exports = { | |||
before: browser => { | |||
browser | |||
.url(browser.launch_url + '/login') | |||
.waitForElementVisible('body', 5000); | |||
}, | |||
'Login': login, | |||
'Welcome': browser => { | |||
let slide_selector = '[data-slide-name="welcome"]'; | |||
browser | |||
.assert.title('Frappe Desk') | |||
.pause(5000) | |||
.assert.visible(slide_selector, 'Check if welcome slide is visible') | |||
.assert.value('select[data-fieldname="language"]', 'English') | |||
.click(slide_selector + ' .next-btn'); | |||
}, | |||
'Region': browser => { | |||
let slide_selector = '[data-slide-name="region"]'; | |||
browser | |||
.waitForElementVisible(slide_selector , 2000) | |||
.pause(6000) | |||
.setValue('select[data-fieldname="language"]', "India") | |||
.pause(4000) | |||
.assert.containsText('div[data-fieldname="timezone"]', 'India Time - Asia/Kolkata') | |||
.click(slide_selector + ' .next-btn'); | |||
}, | |||
'User': browser => { | |||
let slide_selector = '[data-slide-name="user"]'; | |||
browser | |||
.waitForElementVisible(slide_selector, 2000) | |||
.pause(3000) | |||
.setValue('input[data-fieldname="full_name"]', "John Doe") | |||
.setValue('input[data-fieldname="email"]', "john@example.com") | |||
.setValue('input[data-fieldname="password"]', "vbjwearghu") | |||
.click(slide_selector + ' .next-btn'); | |||
}, | |||
after: browser => { | |||
browser.end(); | |||
}, | |||
}; |
@@ -0,0 +1,93 @@ | |||
frappe.tests = { | |||
data: {}, | |||
get_fixture_names: (doctype) => { | |||
return Object.keys(frappe.test_data[doctype]); | |||
}, | |||
make: function(doctype, data) { | |||
return frappe.run_serially([ | |||
() => frappe.set_route('List', doctype), | |||
() => frappe.new_doc(doctype), | |||
() => { | |||
let frm = frappe.quick_entry ? frappe.quick_entry.dialog : cur_frm; | |||
return frappe.tests.set_form_values(frm, data); | |||
}, | |||
() => frappe.timeout(1), | |||
() => (frappe.quick_entry ? frappe.quick_entry.insert() : cur_frm.save()) | |||
]); | |||
}, | |||
set_form_values: (frm, data) => { | |||
let tasks = []; | |||
data.forEach(item => { | |||
for (let key in item) { | |||
let task = () => { | |||
let value = item[key]; | |||
if ($.isArray(value)) { | |||
return frappe.tests.set_grid_values(frm, key, value); | |||
} else { | |||
// single value | |||
return frm.set_value(key, value); | |||
} | |||
}; | |||
tasks.push(task); | |||
} | |||
}); | |||
// set values | |||
return frappe.run_serially(tasks); | |||
}, | |||
set_grid_values: (frm, key, value) => { | |||
// set value in grid | |||
let grid = frm.get_field(key).grid; | |||
grid.remove_all(); | |||
let grid_row_tasks = []; | |||
// build tasks for each row | |||
value.forEach(d => { | |||
grid_row_tasks.push(() => { | |||
grid.add_new_row(); | |||
let grid_row = grid.get_row(-1).toggle_view(true); | |||
let grid_value_tasks = []; | |||
// build tasks to set each row value | |||
d.forEach(child_value => { | |||
for (let child_key in child_value) { | |||
grid_value_tasks.push(() => { | |||
return frappe.model.set_value(grid_row.doc.doctype, | |||
grid_row.doc.name, child_key, child_value[child_key]); | |||
}); | |||
} | |||
}); | |||
return frappe.run_serially(grid_value_tasks); | |||
}); | |||
}); | |||
return frappe.run_serially(grid_row_tasks); | |||
}, | |||
setup_doctype: (doctype) => { | |||
return frappe.set_route('List', doctype) | |||
.then(() => { | |||
frappe.tests.data[doctype] = []; | |||
let expected = frappe.tests.get_fixture_names(doctype); | |||
cur_list.data.forEach((d) => { | |||
frappe.tests.data[doctype].push(d.name); | |||
if(expected.indexOf(d.name) !== -1) { | |||
expected[expected.indexOf(d.name)] = null; | |||
} | |||
}); | |||
let tasks = []; | |||
expected.forEach(function(d) { | |||
if(d) { | |||
tasks.push(() => frappe.tests.make(doctype, | |||
frappe.test_data[doctype][d])); | |||
} | |||
}); | |||
return frappe.run_serially(tasks); | |||
}); | |||
} | |||
}; |