瀏覽代碼

Merge branch 'develop'

version-14
mbauskar 8 年之前
父節點
當前提交
a93acd8579
共有 100 個檔案被更改,包括 7486 行新增5890 行删除
  1. +2
    -1
      .eslintrc
  2. +19
    -11
      .travis.yml
  3. +6
    -7
      frappe/__init__.py
  4. +1
    -0
      frappe/build.js
  5. +5
    -2
      frappe/commands/utils.py
  6. +7
    -0
      frappe/contacts/doctype/address/address.js
  7. +3
    -0
      frappe/contacts/doctype/contact/contact_list.js
  8. +1
    -0
      frappe/contacts/doctype/contact/test_records.json
  9. +8
    -0
      frappe/contacts/doctype/salutation/test_records.json
  10. +7
    -6
      frappe/core/doctype/authentication_log/test_authentication_log.py
  11. +18
    -6
      frappe/core/doctype/doctype/doctype.py
  12. +34
    -2
      frappe/core/doctype/system_settings/system_settings.json
  13. +0
    -0
      frappe/core/doctype/test_runner/__init__.py
  14. +63
    -0
      frappe/core/doctype/test_runner/test_runner.js
  15. +122
    -0
      frappe/core/doctype/test_runner/test_runner.json
  16. +27
    -0
      frappe/core/doctype/test_runner/test_runner.py
  17. +7
    -0
      frappe/core/doctype/user/test_records.json
  18. +1
    -1
      frappe/core/page/permission_manager/permission_manager_help.html
  19. +1
    -1
      frappe/custom/doctype/custom_field/custom_field.js
  20. +4
    -0
      frappe/custom/doctype/custom_field/custom_field.py
  21. +1
    -1
      frappe/custom/doctype/customize_form/customize_form.js
  22. +20
    -4
      frappe/desk/doctype/todo/todo.json
  23. +2
    -1
      frappe/desk/form/meta.py
  24. +1
    -1
      frappe/desk/page/applications/application_row.html
  25. +1
    -1
      frappe/desk/page/backups/backups.html
  26. +114
    -6
      frappe/desk/page/setup_wizard/setup_wizard.css
  27. +333
    -198
      frappe/desk/page/setup_wizard/setup_wizard.js
  28. +17
    -1
      frappe/desk/page/setup_wizard/setup_wizard.py
  29. +8
    -4
      frappe/desk/page/setup_wizard/setup_wizard_page.html
  30. 二進制
      frappe/docs/assets/img/app-development/test-runner.png
  31. +0
    -0
      frappe/docs/user/en/guides/automated-testing/__init__.py
  32. +7
    -0
      frappe/docs/user/en/guides/automated-testing/index.md
  33. +3
    -0
      frappe/docs/user/en/guides/automated-testing/index.txt
  34. +49
    -0
      frappe/docs/user/en/guides/automated-testing/integration-testing.md
  35. +46
    -0
      frappe/docs/user/en/guides/automated-testing/qunit-testing.md
  36. +16
    -22
      frappe/docs/user/en/guides/automated-testing/unit-testing.md
  37. +2
    -2
      frappe/email/doctype/auto_email_report/auto_email_report.json
  38. +11
    -7
      frappe/email/doctype/auto_email_report/auto_email_report.py
  39. +1
    -1
      frappe/email/doctype/auto_email_report/test_auto_email_report.py
  40. +7
    -2
      frappe/email/email_body.py
  41. +3
    -0
      frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
  42. +1
    -2
      frappe/model/base_document.py
  43. +10
    -5
      frappe/model/db_query.py
  44. +7
    -0
      frappe/model/db_schema.py
  45. +1
    -0
      frappe/model/rename_doc.py
  46. +2
    -2
      frappe/modules/utils.py
  47. +0
    -12
      frappe/nightwatch.global.js
  48. +0
    -96
      frappe/nightwatch.js
  49. +1
    -0
      frappe/patches.txt
  50. +16
    -0
      frappe/patches/v8_1/update_format_options_in_auto_email_report.py
  51. +1
    -1
      frappe/printing/doctype/print_format/test_print_format.py
  52. +3
    -0
      frappe/public/build.json
  53. +13
    -0
      frappe/public/css/form.css
  54. +17
    -0
      frappe/public/js/frappe/dom.js
  55. +163
    -65
      frappe/public/js/frappe/form/control.js
  56. +1
    -1
      frappe/public/js/frappe/form/footer/timeline_item.html
  57. +11
    -0
      frappe/public/js/frappe/form/formatters.js
  58. +34
    -687
      frappe/public/js/frappe/form/grid.js
  59. +586
    -0
      frappe/public/js/frappe/form/grid_row.js
  60. +97
    -0
      frappe/public/js/frappe/form/grid_row_form.js
  61. +31
    -14
      frappe/public/js/frappe/form/layout.js
  62. +86
    -50
      frappe/public/js/frappe/form/quick_entry.js
  63. +5
    -2
      frappe/public/js/frappe/form/save.js
  64. +61
    -17
      frappe/public/js/frappe/form/script_manager.js
  65. +4
    -3
      frappe/public/js/frappe/misc/common.js
  66. +12
    -24
      frappe/public/js/frappe/model/create_new.js
  67. +2
    -1
      frappe/public/js/frappe/model/meta.js
  68. +27
    -10
      frappe/public/js/frappe/model/model.js
  69. +1
    -1
      frappe/public/js/frappe/provide.js
  70. +1
    -1
      frappe/public/js/frappe/query_string.js
  71. +24
    -5
      frappe/public/js/frappe/request.js
  72. +24
    -17
      frappe/public/js/frappe/router.js
  73. +1
    -3
      frappe/public/js/frappe/toolbar.js
  74. +1
    -6
      frappe/public/js/frappe/ui/base_list.js
  75. +20
    -9
      frappe/public/js/frappe/ui/field_group.js
  76. +5
    -2
      frappe/public/js/frappe/ui/filters/edit_filter.html
  77. +8
    -6
      frappe/public/js/frappe/ui/filters/filters.js
  78. +16
    -0
      frappe/public/js/frappe/ui/find.js
  79. +6
    -5
      frappe/public/js/frappe/ui/keyboard.js
  80. +1
    -1
      frappe/public/js/frappe/ui/toolbar/awesome_bar.js
  81. +2
    -2
      frappe/public/js/frappe/ui/toolbar/navbar.html
  82. +1
    -1
      frappe/public/js/frappe/ui/toolbar/search_utils.js
  83. +1
    -1
      frappe/public/js/frappe/views/calendar/calendar.js
  84. +0
    -1
      frappe/public/js/frappe/views/reports/grid_report.js
  85. +1
    -2
      frappe/public/js/frappe/views/reports/query_report.js
  86. +1
    -1
      frappe/public/js/frappe/views/reports/reportview.js
  87. +0
    -27
      frappe/public/js/frappe/views/test_runner.js
  88. +3
    -3
      frappe/public/js/legacy/clientscriptAPI.js
  89. +39
    -35
      frappe/public/js/legacy/form.js
  90. +28
    -7
      frappe/public/js/lib/jquery/qunit.css
  91. +4914
    -4407
      frappe/public/js/lib/jquery/qunit.js
  92. +16
    -0
      frappe/public/less/form.less
  93. +9
    -12
      frappe/templates/includes/breadcrumbs.html
  94. +13
    -7
      frappe/test_runner.py
  95. +0
    -27
      frappe/tests/test_client_login.py
  96. +44
    -2
      frappe/tests/test_permissions.py
  97. +0
    -0
      frappe/tests/ui/__init__.py
  98. +0
    -19
      frappe/tests/ui/login.js
  99. +43
    -0
      frappe/tests/ui/setup_wizard.js
  100. +93
    -0
      frappe/tests/ui/test_lib.js

+ 2
- 1
.eslintrc 查看文件

@@ -117,6 +117,7 @@
"set_field_options": true,
"getCookie": true,
"getCookies": true,
"get_url_arg": true
"get_url_arg": true,
"QUnit": true
}
}

+ 19
- 11
.travis.yml 查看文件

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

+ 6
- 7
frappe/__init__.py 查看文件

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

+ 1
- 0
frappe/build.js 查看文件

@@ -272,6 +272,7 @@ function watch_js(ondirty) {
if (sources.includes(filename)) {
pack(target, sources);
ondirty && ondirty(target);
// break;
}
}
});


+ 5
- 2
frappe/commands/utils.py 查看文件

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



+ 7
- 0
frappe/contacts/doctype/address/address.js 查看文件

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

+ 3
- 0
frappe/contacts/doctype/contact/contact_list.js 查看文件

@@ -0,0 +1,3 @@
frappe.listview_settings['Contact'] = {
add_fields: ["image"],
};

+ 1
- 0
frappe/contacts/doctype/contact/test_records.json 查看文件

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


+ 8
- 0
frappe/contacts/doctype/salutation/test_records.json 查看文件

@@ -0,0 +1,8 @@
[
{
"salutation": "Mr"
},
{
"salutation": "Mrs"
}
]

+ 7
- 6
frappe/core/doctype/authentication_log/test_authentication_log.py 查看文件

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


+ 18
- 6
frappe/core/doctype/doctype/doctype.py 查看文件

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


+ 34
- 2
frappe/core/doctype/system_settings/system_settings.json 查看文件

@@ -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
frappe/core/doctype/test_runner/__init__.py 查看文件


+ 63
- 0
frappe/core/doctype/test_runner/test_runner.js 查看文件

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

}
});

+ 122
- 0
frappe/core/doctype/test_runner/test_runner.json 查看文件

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

+ 27
- 0
frappe/core/doctype/test_runner/test_runner.py 查看文件

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

+ 7
- 0
frappe/core/doctype/user/test_records.json 查看文件

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


+ 1
- 1
frappe/core/page/permission_manager/permission_manager_help.html 查看文件

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

+ 1
- 1
frappe/custom/doctype/custom_field/custom_field.js 查看文件

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


+ 4
- 0
frappe/custom/doctype/custom_field/custom_field.py 查看文件

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


+ 1
- 1
frappe/custom/doctype/customize_form/customize_form.js 查看文件

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


+ 20
- 4
frappe/desk/doctype/todo/todo.json 查看文件

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


+ 2
- 1
frappe/desk/form/meta.py 查看文件

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



+ 1
- 1
frappe/desk/page/applications/application_row.html 查看文件

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


+ 1
- 1
frappe/desk/page/backups/backups.html 查看文件

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


+ 114
- 6
frappe/desk/page/setup_wizard/setup_wizard.css 查看文件

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

+ 333
- 198
frappe/desk/page/setup_wizard/setup_wizard.js 查看文件

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

+ 17
- 1
frappe/desk/page/setup_wizard/setup_wizard.py 查看文件

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


+ 8
- 4
frappe/desk/page/setup_wizard/setup_wizard_page.html 查看文件

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


二進制
frappe/docs/assets/img/app-development/test-runner.png 查看文件

Before After
Width: 1972  |  Height: 1116  |  Size: 258 KiB

+ 0
- 0
frappe/docs/user/en/guides/automated-testing/__init__.py 查看文件


+ 7
- 0
frappe/docs/user/en/guides/automated-testing/index.md 查看文件

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

+ 3
- 0
frappe/docs/user/en/guides/automated-testing/index.txt 查看文件

@@ -0,0 +1,3 @@
unit-testing
integration-testing
qunit-testing

+ 49
- 0
frappe/docs/user/en/guides/automated-testing/integration-testing.md 查看文件

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


+ 46
- 0
frappe/docs/user/en/guides/automated-testing/qunit-testing.md 查看文件

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

frappe/docs/user/en/guides/basics/writing-tests.md → frappe/docs/user/en/guides/automated-testing/unit-testing.md 查看文件

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

+ 2
- 2
frappe/email/doctype/auto_email_report/auto_email_report.json 查看文件

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


+ 11
- 7
frappe/email/doctype/auto_email_report/auto_email_report.py 查看文件

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


+ 1
- 1
frappe/email/doctype/auto_email_report/test_auto_email_report.py 查看文件

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


+ 7
- 2
frappe/email/email_body.py 查看文件

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


+ 3
- 0
frappe/integrations/doctype/dropbox_settings/dropbox_settings.py 查看文件

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


+ 1
- 2
frappe/model/base_document.py 查看文件

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



+ 10
- 5
frappe/model/db_query.py 查看文件

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


+ 7
- 0
frappe/model/db_schema.py 查看文件

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


+ 1
- 0
frappe/model/rename_doc.py 查看文件

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


+ 2
- 2
frappe/modules/utils.py 查看文件

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



+ 0
- 12
frappe/nightwatch.global.js 查看文件

@@ -1,12 +0,0 @@
var chromedriver = require('chromedriver');
module.exports = {
before: function (done) {
chromedriver.start();
done();
},

after: function (done) {
chromedriver.stop();
done();
}
};

+ 0
- 96
frappe/nightwatch.js 查看文件

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

+ 1
- 0
frappe/patches.txt 查看文件

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

+ 16
- 0
frappe/patches/v8_1/update_format_options_in_auto_email_report.py 查看文件

@@ -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
- 1
frappe/printing/doctype/print_format/test_print_format.py 查看文件

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


+ 3
- 0
frappe/public/build.json 查看文件

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


+ 13
- 0
frappe/public/css/form.css 查看文件

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


+ 17
- 0
frappe/public/js/frappe/dom.js 查看文件

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


+ 163
- 65
frappe/public/js/frappe/form/control.js 查看文件

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


+ 1
- 1
frappe/public/js/frappe/form/footer/timeline_item.html 查看文件

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


+ 11
- 0
frappe/public/js/frappe/form/formatters.js 查看文件

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


+ 34
- 687
frappe/public/js/frappe/form/grid.js 查看文件

@@ -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 : "&nbsp;");
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);
},
});
});

+ 586
- 0
frappe/public/js/frappe/form/grid_row.js 查看文件

@@ -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 : "&nbsp;");
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);
},
});

+ 97
- 0
frappe/public/js/frappe/form/grid_row_form.js 查看文件

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

+ 31
- 14
frappe/public/js/frappe/form/layout.js 查看文件

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


+ 86
- 50
frappe/public/js/frappe/form/quick_entry.js 查看文件

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


+ 5
- 2
frappe/public/js/frappe/form/save.js 查看文件

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


+ 61
- 17
frappe/public/js/frappe/form/script_manager.js 查看文件

@@ -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
- 3
frappe/public/js/frappe/misc/common.js 查看文件

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


+ 12
- 24
frappe/public/js/frappe/model/create_new.js 查看文件

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



+ 2
- 1
frappe/public/js/frappe/model/meta.js 查看文件

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


+ 27
- 10
frappe/public/js/frappe/model/model.js 查看文件

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


+ 1
- 1
frappe/public/js/frappe/provide.js 查看文件

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

+ 1
- 1
frappe/public/js/frappe/query_string.js 查看文件

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


+ 24
- 5
frappe/public/js/frappe/request.js 查看文件

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


+ 24
- 17
frappe/public/js/frappe/router.js 查看文件

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


+ 1
- 3
frappe/public/js/frappe/toolbar.js 查看文件

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



+ 1
- 6
frappe/public/js/frappe/ui/base_list.js 查看文件

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


+ 20
- 9
frappe/public/js/frappe/ui/field_group.js 查看文件

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

+ 5
- 2
frappe/public/js/frappe/ui/filters/edit_filter.html 查看文件

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


+ 8
- 6
frappe/public/js/frappe/ui/filters/filters.js 查看文件

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


+ 16
- 0
frappe/public/js/frappe/ui/find.js 查看文件

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

+ 6
- 5
frappe/public/js/frappe/ui/keyboard.js 查看文件

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


+ 1
- 1
frappe/public/js/frappe/ui/toolbar/awesome_bar.js 查看文件

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


+ 2
- 2
frappe/public/js/frappe/ui/toolbar/navbar.html 查看文件

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


+ 1
- 1
frappe/public/js/frappe/ui/toolbar/search_utils.js 查看文件

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


+ 1
- 1
frappe/public/js/frappe/views/calendar/calendar.js 查看文件

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


+ 0
- 1
frappe/public/js/frappe/views/reports/grid_report.js 查看文件

@@ -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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
var data = me.data;
var spacer = "<span style='display:inline-block;height:1px;width:" +
(15 * dataContext["indent"]) + "px'></span>";


+ 1
- 2
frappe/public/js/frappe/views/reports/query_report.js 查看文件

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


+ 1
- 1
frappe/public/js/frappe/views/reports/reportview.js 查看文件

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


+ 0
- 27
frappe/public/js/frappe/views/test_runner.js 查看文件

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

+ 3
- 3
frappe/public/js/legacy/clientscriptAPI.js 查看文件

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


+ 39
- 35
frappe/public/js/legacy/form.js 查看文件

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


+ 28
- 7
frappe/public/js/lib/jquery/qunit.css 查看文件

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

+ 4914
- 4407
frappe/public/js/lib/jquery/qunit.js
文件差異過大導致無法顯示
查看文件


+ 16
- 0
frappe/public/less/form.less 查看文件

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


+ 9
- 12
frappe/templates/includes/breadcrumbs.html 查看文件

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

+ 13
- 7
frappe/test_runner.py 查看文件

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



+ 0
- 27
frappe/tests/test_client_login.py 查看文件

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

+ 44
- 2
frappe/tests/test_permissions.py 查看文件

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



+ 0
- 0
frappe/tests/ui/__init__.py 查看文件


+ 0
- 19
frappe/tests/ui/login.js 查看文件

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

+ 43
- 0
frappe/tests/ui/setup_wizard.js 查看文件

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

+ 93
- 0
frappe/tests/ui/test_lib.js 查看文件

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

部分文件因文件數量過多而無法顯示

Loading…
取消
儲存