@@ -117,6 +117,7 @@ | |||
"set_field_options": true, | |||
"getCookie": true, | |||
"getCookies": true, | |||
"get_url_arg": true | |||
"get_url_arg": true, | |||
"QUnit": true | |||
} | |||
} |
@@ -1,9 +1,5 @@ | |||
language: python | |||
dist: trusty | |||
group: deprecated-2017Q2 | |||
python: | |||
- "2.7" | |||
addons: | |||
apt: | |||
@@ -12,14 +8,14 @@ addons: | |||
packages: | |||
- google-chrome-stable | |||
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,18 +27,28 @@ 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 | |||
- bench scheduler disable | |||
- bench start & | |||
- sleep 10 | |||
script: | |||
- set -e | |||
- bench --verbose run-tests | |||
- bench reinstall --yes | |||
- bench run-ui-tests --ci | |||
- sleep 5 | |||
- bench --verbose run-ui-tests --app frappe |
@@ -12,9 +12,9 @@ import os, sys, importlib, inspect, json | |||
# public | |||
from .exceptions import * | |||
from .utils.jinja import get_jenv, get_template, render_template | |||
from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template | |||
__version__ = '8.2.2' | |||
__version__ = '8.4.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 | |||
@@ -381,7 +380,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message | |||
attachments=None, content=None, doctype=None, name=None, reply_to=None, | |||
cc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, | |||
send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False, | |||
inline_images=None): | |||
inline_images=None, template=None, args=None, header=False): | |||
"""Send email using user's default **Email Account** or global default **Email Account**. | |||
@@ -404,7 +403,15 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message | |||
:param expose_recipients: Display all recipients in the footer message - "This email was sent to" | |||
:param communication: Communication link to be set in Email Queue record | |||
:param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id | |||
:param template: Name of html template from templates/emails folder | |||
:param args: Arguments for rendering the template | |||
:param header: Append header in email | |||
""" | |||
text_content = None | |||
if template: | |||
message, text_content = get_email_from_template(template, args) | |||
message = content or message | |||
if as_markdown: | |||
@@ -416,13 +423,13 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message | |||
import email.queue | |||
email.queue.send(recipients=recipients, sender=sender, | |||
subject=subject, message=message, | |||
subject=subject, message=message, text_content=text_content, | |||
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, | |||
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message, | |||
attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, in_reply_to=in_reply_to, | |||
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, | |||
communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification, | |||
inline_images=inline_images) | |||
inline_images=inline_images, header=header) | |||
whitelisted = [] | |||
guest_methods = [] | |||
@@ -1364,7 +1371,7 @@ def get_active_domains(): | |||
return active_domains | |||
def get_system_country(): | |||
if local.system_country is None: | |||
local.system_country = db.get_single_value('System Settings', 'country') or '' | |||
return local.system_country | |||
def get_system_settings(key): | |||
if not local.system_settings.has_key(key): | |||
local.system_settings.update({key: db.get_single_value('System Settings', key)}) | |||
return local.system_settings.get(key) |
@@ -272,6 +272,7 @@ function watch_js(ondirty) { | |||
if (sources.includes(filename)) { | |||
pack(target, sources); | |||
ondirty && ondirty(target); | |||
// break; | |||
} | |||
} | |||
}); | |||
@@ -298,11 +298,13 @@ def console(context): | |||
@click.option('--doctype', help="For DocType") | |||
@click.option('--test', multiple=True, help="Specific test") | |||
@click.option('--driver', help="For Travis") | |||
@click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests") | |||
@click.option('--module', help="Run tests in a module") | |||
@click.option('--profile', is_flag=True, default=False) | |||
@click.option('--junit-xml-output', help="Destination file path for junit xml report") | |||
@pass_context | |||
def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None, profile=False, junit_xml_output=False): | |||
def run_tests(context, app=None, module=None, doctype=None, test=(), | |||
driver=None, profile=False, junit_xml_output=False, ui_tests = False): | |||
"Run tests" | |||
import frappe.test_runner | |||
tests = test | |||
@@ -311,7 +313,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None | |||
frappe.init(site=site) | |||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, | |||
force=context.force, profile=profile, junit_xml_output=junit_xml_output) | |||
force=context.force, profile=profile, junit_xml_output=junit_xml_output, | |||
ui_tests = ui_tests) | |||
if len(ret.failures) == 0 and len(ret.errors) == 0: | |||
ret = 0 | |||
@@ -320,30 +323,24 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None | |||
@click.command('run-ui-tests') | |||
@click.option('--app', help="App to run tests on, leave blank for all apps") | |||
@click.option('--ci', is_flag=True, default=False, help="Run in CI environment") | |||
@click.option('--test', help="File name of the test you want to run") | |||
@click.option('--profile', is_flag=True, default=False) | |||
@pass_context | |||
def run_ui_tests(context, app=None, ci=False): | |||
def run_ui_tests(context, app=None, test=False, profile=False): | |||
"Run UI tests" | |||
import subprocess | |||
import frappe.test_runner | |||
site = get_site(context) | |||
frappe.init(site=site) | |||
frappe.connect() | |||
if app is None: | |||
app = ",".join(frappe.get_installed_apps()) | |||
cmd = [ | |||
'./node_modules/.bin/nightwatch', | |||
'--config', './apps/frappe/frappe/nightwatch.js', | |||
'--app', app, | |||
'--site', site | |||
] | |||
if ci: | |||
cmd.extend(['--env', 'ci_server']) | |||
ret = frappe.test_runner.run_ui_tests(app=app, test=test, verbose=context.verbose, | |||
profile=profile) | |||
if len(ret.failures) == 0 and len(ret.errors) == 0: | |||
ret = 0 | |||
bench_path = frappe.utils.get_bench_path() | |||
subprocess.call(cmd, cwd=bench_path) | |||
if os.environ.get('CI'): | |||
sys.exit(ret) | |||
@click.command('serve') | |||
@click.option('--port', default=8000) | |||
@@ -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]); | |||
} | |||
} | |||
}); |
@@ -185,6 +185,13 @@ def get_shipping_address(company): | |||
address_as_dict = address[0] | |||
name, address_template = get_address_templates(address_as_dict) | |||
return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict) | |||
def get_company_address(company): | |||
ret = frappe._dict() | |||
ret.company_address = get_default_address('Company', company) | |||
ret.company_address_display = get_address_display(ret.company_address) | |||
return ret | |||
def address_query(doctype, txt, searchfield, start, page_len, filters): | |||
from frappe.desk.reportview import get_match_cond | |||
@@ -0,0 +1,3 @@ | |||
frappe.listview_settings['Contact'] = { | |||
add_fields: ["image"], | |||
}; |
@@ -1,6 +1,7 @@ | |||
[ | |||
{ | |||
"doctype": "Contact", | |||
"salutation": "Mr", | |||
"email_id": "test_conctact@example.com", | |||
"first_name": "_Test Contact For _Test Customer", | |||
"is_primary_contact": 1, | |||
@@ -0,0 +1,8 @@ | |||
[ | |||
{ | |||
"salutation": "Mr" | |||
}, | |||
{ | |||
"salutation": "Mrs" | |||
} | |||
] |
@@ -13,13 +13,12 @@ class TestAuthenticationLog(unittest.TestCase): | |||
from frappe.auth import LoginManager, CookieManager | |||
# test user login log | |||
frappe.local.form_dict = { 'cmd': 'login' } | |||
frappe.form_dict = { | |||
frappe.local.form_dict = frappe._dict({ | |||
'cmd': 'login', | |||
'sid': 'Guest', | |||
'pwd': 'admin', | |||
'usr': 'Administrator' | |||
} | |||
}) | |||
frappe.local.cookie_manager = CookieManager() | |||
frappe.local.login_manager = LoginManager() | |||
@@ -38,9 +37,11 @@ class TestAuthenticationLog(unittest.TestCase): | |||
auth_log = self.get_auth_log() | |||
self.assertEquals(auth_log.status, 'Failed') | |||
frappe.local.form_dict = frappe._dict() | |||
def get_auth_log(self, operation='Login'): | |||
names = frappe.db.sql_list("""select name from `tabAuthentication Log` | |||
where user='Administrator' and operation='{operation}' order by | |||
names = frappe.db.sql_list("""select name from `tabAuthentication Log` | |||
where user='Administrator' and operation='{operation}' order by | |||
creation desc""".format(operation=operation)) | |||
name = names[0] | |||
@@ -11,8 +11,10 @@ | |||
"doctype": "DocType", | |||
"document_type": "Setup", | |||
"editable_grid": 1, | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -41,6 +43,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 1, | |||
"collapsible": 0, | |||
@@ -73,6 +76,7 @@ | |||
"width": "163" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 1, | |||
"collapsible": 0, | |||
@@ -92,7 +96,7 @@ | |||
"no_copy": 0, | |||
"oldfieldname": "fieldtype", | |||
"oldfieldtype": "Select", | |||
"options": "Attach\nAttach Image\nButton\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", | |||
"options": "Attach\nAttach Image\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", | |||
"permlevel": 0, | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
@@ -105,6 +109,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 1, | |||
"collapsible": 0, | |||
@@ -135,6 +140,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -167,6 +173,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -198,6 +205,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -228,6 +236,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -260,6 +269,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -290,6 +300,7 @@ | |||
"width": "70px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -319,6 +330,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -349,6 +361,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -378,6 +391,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -408,6 +422,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -438,6 +453,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -465,6 +481,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -496,6 +513,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -526,6 +544,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -554,6 +573,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -584,6 +604,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -616,6 +637,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -646,6 +668,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -675,6 +698,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -704,6 +728,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -734,6 +759,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -761,6 +787,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -794,6 +821,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -823,6 +851,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -855,6 +884,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -887,6 +917,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -917,6 +948,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -947,6 +979,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -975,6 +1008,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1007,6 +1041,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1039,6 +1074,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1071,6 +1107,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1101,6 +1138,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1129,6 +1167,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1161,6 +1200,7 @@ | |||
"width": "50px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1192,6 +1232,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1219,6 +1260,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1251,6 +1293,7 @@ | |||
"width": "300px" | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1280,6 +1323,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -1319,7 +1363,7 @@ | |||
"issingle": 0, | |||
"istable": 1, | |||
"max_attachments": 0, | |||
"modified": "2017-04-21 16:56:04.023296", | |||
"modified": "2017-07-06 12:36:21.248293", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "DocField", | |||
@@ -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,10 @@ 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 isinstance(method, str) and callable(getattr(doc, method))] | |||
if fieldname in method_list: | |||
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname)) |
@@ -49,7 +49,7 @@ class Report(Document): | |||
delete_custom_role('report', self.name) | |||
def set_doctype_roles(self): | |||
if not self.get('roles'): | |||
if not self.get('roles') and self.is_standard == 'No': | |||
meta = frappe.get_meta(self.ref_doctype) | |||
roles = [{'role': d.role} for d in meta.permissions if d.permlevel==0] | |||
self.set('roles', roles) | |||
@@ -810,7 +810,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, | |||
@@ -835,6 +835,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, | |||
@@ -0,0 +1,74 @@ | |||
// 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_test_js' | |||
}).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(({ total, failed, passed, runtime }) => { | |||
// flag for selenium that test is done | |||
$('<div id="frappe-qunit-done"></div>').appendTo($('body')); | |||
console.log( `Total: ${total}, Failed: ${failed}, Passed: ${passed}, Runtime: ${runtime}` ); // eslint-disable-line | |||
if(failed) { | |||
console.log('Tests Failed'); // eslint-disable-line | |||
} else { | |||
console.log('Tests Passed'); // eslint-disable-line | |||
} | |||
frappe.set_route('Form', 'Test Runner', 'Test Runner'); | |||
}); | |||
}); | |||
} | |||
}); |
@@ -0,0 +1,122 @@ | |||
{ | |||
"allow_copy": 0, | |||
"allow_guest_to_view": 0, | |||
"allow_import": 0, | |||
"allow_rename": 0, | |||
"beta": 0, | |||
"creation": "2017-06-26 10:57:19.976624", | |||
"custom": 0, | |||
"docstatus": 0, | |||
"doctype": "DocType", | |||
"document_type": "", | |||
"editable_grid": 1, | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "module_path", | |||
"fieldtype": "Data", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Module Path", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "output", | |||
"fieldtype": "HTML", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Output", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
} | |||
], | |||
"has_web_view": 0, | |||
"hide_heading": 0, | |||
"hide_toolbar": 0, | |||
"idx": 0, | |||
"image_view": 0, | |||
"in_create": 0, | |||
"is_submittable": 0, | |||
"issingle": 1, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-07-12 23:16:15.910891", | |||
"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": "Administrator", | |||
"set_user_permissions": 0, | |||
"share": 1, | |||
"submit": 0, | |||
"write": 1 | |||
} | |||
], | |||
"quick_entry": 1, | |||
"read_only": 0, | |||
"read_only_onload": 0, | |||
"show_name_in_global_search": 0, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1, | |||
"track_seen": 0 | |||
} |
@@ -0,0 +1,49 @@ | |||
# -*- 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_test_js(): | |||
'''Get test + data for app, example: app/tests/ui/test_name.js''' | |||
test_path = frappe.db.get_single_value('Test Runner', 'module_path') | |||
# split | |||
app, test_path = test_path.split(os.path.sep, 1) | |||
test_js = get_test_data(app) | |||
# full path | |||
test_path = frappe.get_app_path(app, test_path) | |||
with open(test_path, 'r') as fileobj: | |||
test_js.append(dict( | |||
script = fileobj.read() | |||
)) | |||
return test_js | |||
def get_test_data(app): | |||
'''Get the test fixtures from all js files in app/tests/ui/data''' | |||
test_js = [] | |||
def add_file(path): | |||
with open(path, 'r') as fileobj: | |||
test_js.append(dict( | |||
script = fileobj.read() | |||
)) | |||
data_path = frappe.get_app_path(app, 'tests', 'ui', 'data') | |||
if os.path.exists(data_path): | |||
for fname in os.listdir(data_path): | |||
if fname.endswith('.js'): | |||
add_file(os.path.join(data_path, fname)) | |||
if app != 'frappe': | |||
add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js')) | |||
return test_js |
@@ -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", | |||
@@ -1949,7 +1949,7 @@ | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"read_only": 1, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
@@ -225,11 +225,11 @@ class User(Document): | |||
def password_reset_mail(self, link): | |||
self.send_login_mail(_("Password Reset"), | |||
"templates/emails/password_reset.html", {"link": link}, now=True) | |||
"password_reset", {"link": link}, now=True) | |||
def password_update_mail(self, password): | |||
self.send_login_mail(_("Password Update"), | |||
"templates/emails/password_update.html", {"new_password": password}, now=True) | |||
"password_update", {"new_password": password}, now=True) | |||
def send_welcome_mail_to_user(self): | |||
from frappe.utils import get_url | |||
@@ -248,7 +248,7 @@ class User(Document): | |||
else: | |||
subject = _("Complete Registration") | |||
self.send_login_mail(subject, "templates/emails/new_user.html", | |||
self.send_login_mail(subject, "new_user", | |||
dict( | |||
link=link, | |||
site_url=get_url(), | |||
@@ -279,7 +279,7 @@ class User(Document): | |||
sender = frappe.session.user not in STANDARD_USERS and get_formatted_email(frappe.session.user) or None | |||
frappe.sendmail(recipients=self.email, sender=sender, subject=subject, | |||
message=frappe.get_template(template).render(args), | |||
template=template, args=args, | |||
delayed=(not now) if now!=None else self.flags.delay_emails, retry=3) | |||
def a_system_manager_should_exist(self): | |||
@@ -579,7 +579,7 @@ def update_password(new_password, key=None, old_password=None): | |||
def test_password_strength(new_password, key=None, old_password=None, user_data=[]): | |||
from frappe.utils.password_strength import test_password_strength as _test_password_strength | |||
password_policy = frappe.db.get_value("System Settings", None, | |||
password_policy = frappe.db.get_value("System Settings", None, | |||
["enable_password_policy", "minimum_password_score"], as_dict=True) or {} | |||
enable_password_policy = cint(password_policy.get("enable_password_policy", 0)) | |||
@@ -589,7 +589,7 @@ def test_password_strength(new_password, key=None, old_password=None, user_data= | |||
return {} | |||
if not user_data: | |||
user_data = frappe.db.get_value('User', frappe.session.user, | |||
user_data = frappe.db.get_value('User', frappe.session.user, | |||
['first_name', 'middle_name', 'last_name', 'email', 'birth_date']) | |||
if new_password: | |||
@@ -36,6 +36,6 @@ | |||
<li>{%= __("Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.") %}</li> | |||
</ol> | |||
<p>{%= __("If these instructions where not helpful, please add in your suggestions on GitHub Issues.") %} | |||
<a href="https://github.com/frappe/frappe/issues" target="_blank">{%= __("Submit an Issue") %}</a> | |||
<a href="https://github.com/frappe/frappe/issues" target="_blank" rel="noopener noreferrer">{%= __("Submit an Issue") %}</a> | |||
</p> | |||
</div> |
@@ -11,7 +11,7 @@ frappe.ui.form.on('Custom Field', { | |||
['DocType', 'issingle', '=', 0], | |||
]; | |||
if(frappe.session.user!=="Administrator") { | |||
filters.push(['DocType', 'module', '!=', 'Core']) | |||
filters.push(['DocType', 'module', 'not in', ['Core', 'Custom']]) | |||
} | |||
return { | |||
"filters": filters | |||
@@ -11,6 +11,7 @@ | |||
"doctype": "DocType", | |||
"document_type": "Setup", | |||
"editable_grid": 0, | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
@@ -219,7 +220,7 @@ | |||
"no_copy": 0, | |||
"oldfieldname": "fieldtype", | |||
"oldfieldtype": "Select", | |||
"options": "Attach\nAttach Image\nButton\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", | |||
"options": "Attach\nAttach Image\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", | |||
"permlevel": 0, | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
@@ -1160,7 +1161,7 @@ | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-06-13 09:52:49.692096", | |||
"modified": "2017-07-06 17:23:43.835189", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"name": "Custom Field", | |||
@@ -39,6 +39,10 @@ class CustomField(Document): | |||
if not self.fieldname: | |||
frappe.throw(_("Fieldname not set for Custom Field")) | |||
if not self.flags.ignore_validate: | |||
from frappe.core.doctype.doctype.doctype import check_if_fieldname_conflicts_with_methods | |||
check_if_fieldname_conflicts_with_methods(self.dt, self.fieldname) | |||
def on_update(self): | |||
frappe.clear_cache(doctype=self.dt) | |||
if not self.flags.ignore_validate: | |||
@@ -15,7 +15,7 @@ frappe.ui.form.on("Customize Form", { | |||
['DocType', 'custom', '=', 0], | |||
['DocType', 'name', 'not in', 'DocType, DocField, DocPerm, User, Role, Has Role, \ | |||
Page, Has Role, Module Def, Print Format, Report, Customize Form, \ | |||
Customize Form Field'], | |||
Customize Form Field, Property Setter, Custom Field, Custom Script'], | |||
['DocType', 'restrict_to_domain', 'in', frappe.boot.active_domains] | |||
] | |||
}; | |||
@@ -68,6 +68,8 @@ allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Da | |||
('Text', 'Data'), ('Text', 'Text Editor', 'Code', 'Signature'), ('Data', 'Select'), | |||
('Text', 'Small Text')) | |||
allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select',) | |||
class CustomizeForm(Document): | |||
def on_update(self): | |||
frappe.db.sql("delete from tabSingles where doctype='Customize Form'") | |||
@@ -197,6 +199,10 @@ class CustomizeForm(Document): | |||
frappe.msgprint(_("You cannot unset 'Read Only' for field {0}").format(df.label)) | |||
continue | |||
elif property == "options" and df.get("fieldtype") not in allowed_fieldtype_for_options_change: | |||
frappe.msgprint(_("You can't set 'Options' for field {0}").format(df.label)) | |||
continue | |||
self.make_property_setter(property=property, value=df.get(property), | |||
property_type=docfield_properties[property], fieldname=df.fieldname) | |||
@@ -165,22 +165,22 @@ class TestCustomizeForm(unittest.TestCase): | |||
df = d.get("fields", {"fieldname": "title"})[0] | |||
# invalid fieldname | |||
df.options = """{doc_type} - {introduction_test}""" | |||
df.default = """{doc_type} - {introduction_test}""" | |||
self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") | |||
# space in formatter | |||
df.options = """{doc_type} - {introduction text}""" | |||
df.default = """{doc_type} - {introduction text}""" | |||
self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") | |||
# valid fieldname | |||
df.options = """{doc_type} - {introduction_text}""" | |||
df.default = """{doc_type} - {introduction_text}""" | |||
d.run_method("save_customization") | |||
# valid fieldname with escaped curlies | |||
df.options = """{{ {doc_type} }} - {introduction_text}""" | |||
df.default = """{{ {doc_type} }} - {introduction_text}""" | |||
d.run_method("save_customization") | |||
# undo | |||
df.options = None | |||
df.default = None | |||
d.run_method("save_customization") | |||
@@ -94,7 +94,7 @@ | |||
"no_copy": 0, | |||
"oldfieldname": "fieldtype", | |||
"oldfieldtype": "Select", | |||
"options": "Attach\nAttach Image\nButton\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nText\nText Editor\nTime", | |||
"options": "Attach\nAttach Image\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nText\nText Editor\nTime", | |||
"permlevel": 0, | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
@@ -1202,7 +1202,7 @@ | |||
"issingle": 0, | |||
"istable": 1, | |||
"max_attachments": 0, | |||
"modified": "2017-04-21 17:02:14.903382", | |||
"modified": "2017-07-06 17:24:03.665171", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"name": "Customize Form Field", | |||
@@ -895,8 +895,8 @@ | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-05-01 15:27:39.217961", | |||
"modified_by": "vartakashwini@gmail.com", | |||
"modified": "2017-07-06 12:37:44.036819", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "Event", | |||
"owner": "Administrator", | |||
@@ -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-13 17:44:54.369254", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "ToDo", | |||
@@ -65,7 +65,7 @@ class FormMeta(Meta): | |||
def _get_path(fname): | |||
return os.path.join(path, scrub(fname)) | |||
system_country = frappe.get_system_country() | |||
system_country = frappe.get_system_settings("country") | |||
self._add_code(_get_path(self.name + '.js'), '__js') | |||
if system_country: | |||
@@ -82,6 +82,7 @@ class FormMeta(Meta): | |||
self.add_code_via_hook("doctype_js", "__js") | |||
self.add_code_via_hook("doctype_list_js", "__list_js") | |||
self.add_code_via_hook("doctype_tree_js", "__tree_js") | |||
self.add_code_via_hook("doctype_calendar_js", "__calendar_js") | |||
self.add_custom_script() | |||
self.add_html_templates(path) | |||
@@ -5,7 +5,7 @@ | |||
<div class="media"> | |||
<div class="pull-right app-buttons"> | |||
<a class="btn btn-default btn-xs" | |||
href="{{ app.app_url }}" target="_blank">{{ __("Website") }}</a> | |||
href="{{ app.app_url }}" target="_blank" rel="noopener noreferrer">{{ __("Website") }}</a> | |||
{% if (app.installed) { %} | |||
<button class="btn btn-danger btn-xs btn-remove" | |||
data-title="{{ app.app_title }}" | |||
@@ -21,7 +21,7 @@ | |||
{{ f[1] }} | |||
</td> | |||
<td> | |||
<a href="{{ f[0] }}" target="_blank">{{ f[0] }}</a> | |||
<a href="{{ f[0] }}" target="_blank" rel="noopener noreferrer">{{ f[0] }}</a> | |||
</td> | |||
<td> | |||
{{ f[2] }} | |||
@@ -9,6 +9,13 @@ frappe.pages['backups'].on_page_load = function(wrapper) { | |||
frappe.set_route('Form', 'System Settings'); | |||
}); | |||
page.add_inner_button(__("Download Files Backup"), function () { | |||
frappe.call({ | |||
method:"frappe.desk.page.backups.backups.schedule_files_backup", | |||
args: {"user_email": frappe.session.user_email} | |||
}); | |||
}); | |||
frappe.breadcrumbs.add("Setup"); | |||
$(frappe.render_template("backups")).appendTo(page.body.addClass("no-border")); | |||
@@ -1,6 +1,7 @@ | |||
import os | |||
import frappe | |||
from frappe.utils import get_site_path, cint | |||
from frappe import _ | |||
from frappe.utils import get_site_path, cint, get_url | |||
from frappe.utils.data import convert_utc_to_user_timezone | |||
import datetime | |||
@@ -57,3 +58,29 @@ def delete_downloadable_backups(): | |||
if len(files) > backup_limit: | |||
cleanup_old_backups(path, files, backup_limit) | |||
@frappe.whitelist() | |||
def schedule_files_backup(user_email): | |||
from frappe.utils.background_jobs import enqueue, get_jobs | |||
queued_jobs = get_jobs(site=frappe.local.site, queue="long") | |||
method = 'frappe.desk.page.backups.backups.backup_files_and_notify_user' | |||
if method not in queued_jobs[frappe.local.site]: | |||
enqueue("frappe.desk.page.backups.backups.backup_files_and_notify_user", queue='long', user_email=user_email) | |||
frappe.msgprint(_("Queued for backup. You will receive an email with the download link")) | |||
else: | |||
frappe.msgprint(_("Backup job is already queued. You will receive an email with the download link")) | |||
def backup_files_and_notify_user(user_email=None): | |||
from frappe.utils.backups import backup | |||
backup_files = backup(with_files=True) | |||
get_downloadable_links(backup_files) | |||
subject = "File backup is ready" | |||
message = frappe.render_template('frappe/templates/emails/file_backup_notification.html', backup_files, is_path=True) | |||
frappe.sendmail(recipients=[user_email], subject=subject, message=message) | |||
def get_downloadable_links(backup_files): | |||
for key in ['backup_path_files', 'backup_path_private_files']: | |||
path = backup_files[key] | |||
backup_files[key] = get_url('/'.join(path.split('/')[-2:])) |
@@ -1,3 +1,26 @@ | |||
#page-setup-wizard { | |||
margin-top: 30px; | |||
} | |||
.setup-wizard-brand { | |||
margin: 30px; | |||
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 +37,60 @@ | |||
} | |||
.setup-wizard-slide .lead { | |||
margin-bottom: 10px; | |||
margin: 30px; | |||
color: #777777; | |||
text-align: center; | |||
font-size: 24px; | |||
} | |||
.setup-wizard-slide .col-sm-12 { | |||
padding: 0px; | |||
} | |||
.setup-wizard-slide .section-body .col-sm-6:first-child { | |||
padding-left: 0px; | |||
} | |||
.setup-wizard-slide .section-body .col-sm-6:last-child { | |||
padding-right: 0px; | |||
} | |||
.setup-wizard-slide .form-control { | |||
font-weight: 500; | |||
} | |||
.setup-wizard-slide .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; | |||
margin: 30px 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.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 +111,28 @@ | |||
} | |||
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] { | |||
width: 140px; | |||
height: 180px; /*depends on presence of heading*/ | |||
text-align: center; | |||
margin-left: calc((100% - 140px)/2); | |||
} | |||
.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 +142,38 @@ | |||
-webkit-transform: translate(0px, -50%); | |||
} | |||
.setup-wizard-slide .img-container { | |||
height: 100%; | |||
width: 100%; | |||
padding: 2px; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
position: relative; | |||
border: 1px solid #d1d8dd; | |||
border-radius: 6px; | |||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); | |||
} | |||
.setup-wizard-slide .img-overlay { | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
position: absolute; | |||
width: 100%; | |||
height: 100%; | |||
color: #777777; | |||
background-color: rgba(255, 255, 255, 0.7); | |||
opacity: 0; | |||
} | |||
.setup-wizard-slide .img-overlay:hover { | |||
opacity: 1; | |||
cursor: pointer; | |||
} | |||
.setup-wizard-message-image { | |||
margin: 15px auto; | |||
} |
@@ -1,22 +1,25 @@ | |||
frappe.provide("frappe.wiz"); | |||
frappe.provide("frappe.wiz.events"); | |||
frappe.provide("frappe.setup.events"); | |||
frappe.wiz = { | |||
frappe.setup = { | |||
slides: [], | |||
events: {}, | |||
data: {}, | |||
utils: {}, | |||
remove_app_slides: [], | |||
on: function(event, fn) { | |||
if(!frappe.wiz.events[event]) { | |||
frappe.wiz.events[event] = []; | |||
if(!frappe.setup.events[event]) { | |||
frappe.setup.events[event] = []; | |||
} | |||
frappe.wiz.events[event].push(fn); | |||
frappe.setup.events[event].push(fn); | |||
}, | |||
add_slide: function(slide) { | |||
frappe.wiz.slides.push(slide); | |||
frappe.setup.slides.push(slide); | |||
}, | |||
run_event: function(event) { | |||
$.each(frappe.wiz.events[event] || [], function(i, fn) { | |||
$.each(frappe.setup.events[event] || [], function(i, fn) { | |||
fn(); | |||
}); | |||
} | |||
@@ -25,21 +28,21 @@ frappe.wiz = { | |||
frappe.pages['setup-wizard'].on_page_load = function(wrapper) { | |||
// setup page ui | |||
$(".navbar:first").toggle(false); | |||
$("body").css({"padding-top":"30px"}); | |||
var requires = ["/assets/frappe/css/animate.min.css"].concat(frappe.boot.setup_wizard_requires || []); | |||
frappe.require(requires, function() { | |||
frappe.wiz.run_event("before_load"); | |||
frappe.setup.run_event("before_load"); | |||
var wizard_settings = { | |||
page_name: "setup-wizard", | |||
parent: wrapper, | |||
slides: frappe.wiz.slides, | |||
slides: frappe.setup.slides, | |||
title: __("Welcome") | |||
} | |||
frappe.wizard = new frappe.wiz.Wizard(wizard_settings); | |||
frappe.wiz.run_event("after_load"); | |||
frappe.wizard = new frappe.setup.Wizard(wizard_settings); | |||
frappe.setup.run_event("after_load"); | |||
// frappe.wizard.values = test_values_edu; | |||
@@ -56,7 +59,7 @@ frappe.pages['setup-wizard'].on_page_show = function(wrapper) { | |||
} | |||
} | |||
frappe.wiz.Wizard = Class.extend({ | |||
frappe.setup.Wizard = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.make(); | |||
@@ -75,6 +78,7 @@ frappe.wiz.Wizard = Class.extend({ | |||
</div>', {html:html})) | |||
}, | |||
show_working: function() { | |||
$('header').find('.setup-wizard-brand').hide(); | |||
this.hide_current_slide(); | |||
frappe.set_route(this.page_name); | |||
this.current_slide = {"$wrapper": this.get_message(this.working_html()).appendTo(this.parent)}; | |||
@@ -96,7 +100,7 @@ frappe.wiz.Wizard = Class.extend({ | |||
this.update_values(); | |||
if(!this.slide_dict[id]) { | |||
this.slide_dict[id] = new frappe.wiz.WizardSlide($.extend(this.slides[id], {wiz:this, id:id})); | |||
this.slide_dict[id] = new frappe.setup.WizardSlide($.extend(this.slides[id], {wiz:this, id:id})); | |||
this.slide_dict[id].make(); | |||
} | |||
@@ -147,8 +151,8 @@ frappe.wiz.Wizard = Class.extend({ | |||
args: {args: this.values}, | |||
callback: function(r) { | |||
me.show_complete(); | |||
if(frappe.wiz.welcome_page) { | |||
localStorage.setItem("session_last_route", frappe.wiz.welcome_page); | |||
if(frappe.setup.welcome_page) { | |||
localStorage.setItem("session_last_route", frappe.setup.welcome_page); | |||
} | |||
setTimeout(function() { | |||
window.location = "/desk"; | |||
@@ -181,26 +185,27 @@ frappe.wiz.Wizard = Class.extend({ | |||
this.update_values(); | |||
frappe.wiz.slides = []; | |||
frappe.wiz.run_event("before_load"); | |||
frappe.setup.slides = []; | |||
frappe.setup.run_event("before_load"); | |||
// remove slides listed in remove_app_slides | |||
var new_slides = []; | |||
frappe.wiz.slides.forEach(function(slide) { | |||
if(frappe.wiz.domain) { | |||
frappe.setup.slides.forEach(function(slide) { | |||
if(frappe.setup.domain) { | |||
var domains = slide.domains; | |||
if (domains.indexOf('all') !== -1 || | |||
domains.indexOf(frappe.wiz.domain.toLowerCase()) !== -1) { | |||
domains.indexOf(frappe.setup.domain.toLowerCase()) !== -1) { | |||
new_slides.push(slide); | |||
} | |||
} else { | |||
new_slides.push(slide); | |||
} | |||
}) | |||
frappe.wiz.slides = new_slides; | |||
this.slides = frappe.wiz.slides; | |||
frappe.wiz.run_event("after_load"); | |||
frappe.setup.slides = new_slides; | |||
this.slides = frappe.setup.slides; | |||
frappe.setup.run_event("after_load"); | |||
// re-render all slides | |||
this.slide_dict = {}; | |||
@@ -213,7 +218,7 @@ frappe.wiz.Wizard = Class.extend({ | |||
} | |||
}); | |||
frappe.wiz.WizardSlide = Class.extend({ | |||
frappe.setup.WizardSlide = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.$wrapper = $('<div class="slide-wrapper hidden"></div>') | |||
@@ -224,6 +229,24 @@ 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, i) => { | |||
if(field.fieldname) { | |||
field.fieldname += '_1'; | |||
} | |||
if(i === 1 && this.mandatory_entry) { | |||
field.reqd = 1; | |||
} | |||
if(!field.static) { | |||
if(field.label) field.label += ' 1'; | |||
} | |||
return field; | |||
}); | |||
} | |||
if(this.before_load) { | |||
this.before_load(this); | |||
} | |||
@@ -234,7 +257,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 +264,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 +273,36 @@ 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.focus_first_input(); | |||
this.set_reqd_fields(); | |||
this.bind_fields_to_next($primary_btn); | |||
this.reset_next($primary_btn); | |||
this.focus_first_input(); | |||
}, | |||
set_reqd_fields: function() { | |||
var dict = this.form.fields_dict; | |||
this.reqd_fields = []; | |||
Object.keys(dict).map(key => { | |||
if(dict[key].df.reqd) { | |||
this.reqd_fields.push(dict[key]); | |||
} | |||
}); | |||
}, | |||
set_init_values: function() { | |||
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 +324,25 @@ 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.static) { | |||
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 +370,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 +385,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 +402,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 +438,310 @@ 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", reqd: 1} | |||
], | |||
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") + ' (' + __("Will be your login ID") + ')', | |||
"fieldtype": "Data", "options":"Email"}, | |||
{ "fieldname": "password", "label": __("Password"), "fieldtype": "Password" } | |||
], | |||
help: __('The first user will become the System Manager (you can change this later).'), | |||
onload: function(slide) { | |||
if(frappe.session.user!=="Administrator") { | |||
slide.form.fields_dict.email.$wrapper.toggle(false); | |||
slide.form.fields_dict.password.$wrapper.toggle(false); | |||
// remove password field | |||
delete slide.form.fields_dict.password; | |||
slide.get_input("currency").empty() | |||
.add_options(frappe.utils.unique([""].concat($.map(data.country_info, | |||
function(opts, country) { return opts.currency; }))).sort()); | |||
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("timezone").empty() | |||
.add_options([""].concat(data.all_timezones)); | |||
var user_image = frappe.get_cookie("user_image"); | |||
var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper; | |||
// 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); | |||
} | |||
if(user_image) { | |||
$attach_user_image.find(".missing-image").toggle(false); | |||
$attach_user_image.find("img").attr("src", decodeURIComponent(user_image)); | |||
$attach_user_image.find(".img-container").toggle(true); | |||
} | |||
delete slide.form.fields_dict.email; | |||
if(frappe.wizard.values.currency) { | |||
slide.get_field("currency").set_input(frappe.wizard.values.currency); | |||
} | |||
} else { | |||
slide.form.fields_dict.email.df.reqd = 1; | |||
slide.form.fields_dict.email.refresh(); | |||
slide.form.fields_dict.password.df.reqd = 1; | |||
slide.form.fields_dict.password.refresh(); | |||
if(frappe.wizard.values.timezone) { | |||
slide.get_field("timezone").set_input(frappe.wizard.values.timezone); | |||
utils.load_user_details(slide, this.setup_fields); | |||
} | |||
}, | |||
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.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); | |||
} | |||
}); | |||
}, | |||
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); | |||
} | |||
}) | |||
}, | |||
// 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)); | |||
setup_language_field: function(slide) { | |||
var language_field = slide.get_field("language"); | |||
language_field.df.options = frappe.setup.data.lang.languages; | |||
language_field.refresh(); | |||
}, | |||
slide.get_field("timezone").set_input($timezone.val()); | |||
setup_region_fields: function(slide) { | |||
/* | |||
Set a slide's country, timezone and currency fields | |||
*/ | |||
var data = frappe.setup.data.regional_data; | |||
// temporarily set date format | |||
frappe.boot.sysdefaults.date_format = (data.country_info[country].date_format | |||
|| "dd-mm-yyyy"); | |||
}); | |||
var country_field = slide.get_field('country'); | |||
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 = "#,###.##" | |||
} | |||
slide.get_input("country").empty() | |||
.add_options([""].concat(Object.keys(data.country_info).sort())); | |||
frappe.boot.sysdefaults.number_format = number_format; | |||
locals[":Currency"][currency] = $.extend({}, currency_doc); | |||
}); | |||
}); | |||
slide.get_input("currency").empty() | |||
.add_options(frappe.utils.unique([""].concat($.map(data.country_info, | |||
function(opts, country) { return opts.currency; }))).sort()); | |||
slide.get_input("timezone").empty() | |||
.add_options([""].concat(data.all_timezones)); | |||
// set values if present | |||
if(frappe.wizard.values.country) { | |||
country_field.set_input(frappe.wizard.values.country); | |||
} else if (data.default_country) { | |||
country_field.set_input(data.default_country); | |||
} | |||
}, | |||
if(frappe.wizard.values.currency) { | |||
slide.get_field("currency").set_input(frappe.wizard.values.currency); | |||
} | |||
frappe.wiz.user = { | |||
domains: ["all"], | |||
title: __("The First User: You"), | |||
icon: "fa fa-user", | |||
fields: [ | |||
{"fieldname": "full_name", "label": __("Full Name"), "fieldtype": "Data", | |||
reqd:1}, | |||
{"fieldname": "email", "label": __("Email Address"), "fieldtype": "Data", | |||
reqd:1, "description": __("Login id"), "options":"Email"}, | |||
{"fieldname": "password", "label": __("Password"), "fieldtype": "Password", | |||
reqd:1}, | |||
{fieldtype:"Attach Image", fieldname:"attach_user", | |||
label: __("Attach Your Picture"), is_private: 0}, | |||
], | |||
help: __('The first user will become the System Manager (you can change this later).'), | |||
onload: function(slide) { | |||
if(frappe.session.user!=="Administrator") { | |||
slide.form.fields_dict.password.$wrapper.toggle(false); | |||
slide.form.fields_dict.email.$wrapper.toggle(false); | |||
if(frappe.boot.user.first_name || frappe.boot.user.last_name) { | |||
slide.form.fields_dict.full_name.set_input( | |||
[frappe.boot.user.first_name, frappe.boot.user.last_name].join(' ').trim()); | |||
if(frappe.wizard.values.timezone) { | |||
slide.get_field("timezone").set_input(frappe.wizard.values.timezone); | |||
} | |||
country_field.df.description = 'fetching country...'; | |||
country_field.set_description(); | |||
// get location from IP (unreliable) | |||
frappe.call({ | |||
method:"frappe.desk.page.setup_wizard.setup_wizard.load_country", | |||
callback: function(r) { | |||
if(r.message) { | |||
slide.get_field("country").set_input(r.message); | |||
slide.get_input("country").trigger('change'); | |||
} | |||
country_field.df.description = ''; | |||
country_field.set_description(); | |||
} | |||
}); | |||
}, | |||
var user_image = frappe.get_cookie("user_image"); | |||
if(user_image) { | |||
var $attach_user = slide.form.fields_dict.attach_user.$wrapper; | |||
$attach_user.find(".missing-image").toggle(false); | |||
$attach_user.find("img").attr("src", decodeURIComponent(user_image)).toggle(true); | |||
bind_language_events: function(slide) { | |||
slide.get_input("language").unbind("change").on("change", function() { | |||
var lang = $(this).val() || "English"; | |||
frappe._messages = {}; | |||
frappe.call({ | |||
method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages", | |||
freeze: true, | |||
args: { | |||
language: lang | |||
}, | |||
callback: function(r) { | |||
frappe.setup._from_load_messages = true; | |||
frappe.wizard.refresh_slides(); | |||
} | |||
}); | |||
}); | |||
}, | |||
delete slide.form.fields_dict.email; | |||
delete slide.form.fields_dict.password; | |||
bind_region_events: function(slide) { | |||
/* | |||
Bind a slide's country, timezone and currency fields | |||
*/ | |||
slide.get_input("country").on("change", function() { | |||
var country = slide.get_input("country").val(); | |||
var $timezone = slide.get_input("timezone"); | |||
var data = frappe.setup.data.regional_data; | |||
$timezone.empty(); | |||
// add country specific timezones first | |||
if(country) { | |||
var timezone_list = data.country_info[country].timezones || []; | |||
$timezone.add_options(timezone_list.sort()); | |||
slide.get_field("currency").set_input(data.country_info[country].currency); | |||
slide.get_field("currency").$input.trigger("change"); | |||
} | |||
}, | |||
css_class: "single-column" | |||
}; | |||
// add all timezones at the end, so that user has the option to change it to any timezone | |||
$timezone.add_options([""].concat(data.all_timezones)); | |||
slide.get_field("timezone").set_input($timezone.val()); | |||
// temporarily set date format | |||
frappe.boot.sysdefaults.date_format = (data.country_info[country].date_format | |||
|| "dd-mm-yyyy"); | |||
}); | |||
slide.get_input("currency").on("change", function() { | |||
var currency = slide.get_input("currency").val(); | |||
if (!currency) return; | |||
frappe.model.with_doc("Currency", currency, function() { | |||
frappe.provide("locals.:Currency." + currency); | |||
var currency_doc = frappe.model.get_doc("Currency", currency); | |||
var number_format = currency_doc.number_format; | |||
if (number_format==="#.###") { | |||
number_format = "#.###,##"; | |||
} else if (number_format==="#,###") { | |||
number_format = "#,###.##" | |||
} | |||
frappe.boot.sysdefaults.number_format = number_format; | |||
locals[":Currency"][currency] = $.extend({}, currency_doc); | |||
}); | |||
}); | |||
}, | |||
} | |||
frappe.wiz.on("before_load", function() { | |||
load_frappe_slides(); | |||
frappe.setup.on("before_load", function() { | |||
// load slides | |||
frappe_slides.map(frappe.setup.add_slide); | |||
// add welcome slide | |||
frappe.wiz.add_slide(frappe.wiz.welcome); | |||
frappe.wiz.add_slide(frappe.wiz.region); | |||
frappe.wiz.add_slide(frappe.wiz.user); | |||
// set header image | |||
let $icon = $('header .setup-wizard-brand'); | |||
if($icon.length === 0) { | |||
$('header').append(`<div class="setup-wizard-brand""> | |||
<img src="/assets/frappe/images/frappe-bird-grey.svg" | |||
class="brand-icon frappe-icon" style="width:36px;"></div>`); | |||
} | |||
}); |
@@ -179,11 +179,27 @@ def load_messages(language): | |||
@frappe.whitelist() | |||
def load_languages(): | |||
language_codes = frappe.db.sql('select language_code, language_name from tabLanguage order by name', as_dict=True) | |||
codes_to_names = {} | |||
for d in language_codes: | |||
codes_to_names[d.language_code] = d.language_name | |||
return { | |||
"default_language": frappe.db.get_value('Language', frappe.local.lang, 'language_name') or frappe.local.lang, | |||
"languages": sorted(frappe.db.sql_list('select language_name from tabLanguage order by name')) | |||
"languages": sorted(frappe.db.sql_list('select language_name from tabLanguage order by name')), | |||
"codes_to_names": codes_to_names | |||
} | |||
@frappe.whitelist() | |||
def load_country(): | |||
from frappe.sessions import get_geo_ip_country | |||
return get_geo_ip_country(frappe.local.request_ip) if frappe.local.request_ip else None | |||
@frappe.whitelist() | |||
def load_user_details(): | |||
return { | |||
"full_name": frappe.cache().hget("full_name", "signup"), | |||
"email": frappe.cache().hget("email", "signup") | |||
} | |||
def prettify_args(args): | |||
# remove attachments | |||
@@ -1,14 +1,18 @@ | |||
<div class="container setup-wizard-slide {%= css_class %} with-form" data-slide-name="{%= name %}"> | |||
<div class="container setup-wizard-slide single-column with-form" data-slide-name="{%= name %}"> | |||
<div class="text-center setup-wizard-progress text-extra-muted"> | |||
{% for (var i=0; i < slides_count; i++) { %} | |||
<i class="fa fa-fw fa-circle{% if (i+1<=step) { %} active {% } %}"></i> | |||
<!--dev_mode: link progress bubbles--> | |||
<!--<a href="http://erpnext.domainify:8000/desk#setup-wizard/{%= i %}">--> | |||
<i class="fa fa-fw fa-circle{% if (i+1<=step) { %} active {% } %}"></i> | |||
<!--</a>--> | |||
{% } %} | |||
</div> | |||
<p class="text-center lead">{%= title %}</p> | |||
<p class="lead">{%= title %}</p> | |||
<div class="row"> | |||
<div class="col-sm-12"> | |||
<div class="setup-wizard-body col-sm-12"> | |||
<!-- {% if (help) { %} <p class="text-center">{%= help %}</p> {% } %} --> | |||
<div class="form"></div> | |||
<a class="more-btn hide btn btn-default btn-sm" style="margin-left: 41%;">{%= __("Add More") %}</a> | |||
</div> | |||
</div> | |||
<div class="footer text-right"> | |||
@@ -159,7 +159,7 @@ def export_query(): | |||
elif not row: | |||
result.append([]) | |||
else: | |||
result = result + data.result | |||
result = result + [d for i,d in enumerate(data.result) if (i+1 in visible_idx)] | |||
from frappe.utils.xlsxutils import make_xlsx | |||
xlsx_file = make_xlsx(result, "Query Report") | |||
@@ -0,0 +1,7 @@ | |||
# Automated Testing | |||
Frappé Provides you a test framework to write and execute tests that can be run directly on a Continuous Integration Tool like Travis | |||
You can write server-side unit tests or UI tests | |||
{index} |
@@ -0,0 +1,3 @@ | |||
unit-testing | |||
integration-testing | |||
qunit-testing |
@@ -0,0 +1,49 @@ | |||
# UI Integration Testing | |||
You can write integration tests using the Selenium Driver. `frappe.utils.selenium_driver` gives you a friendly API to write selenium based tests | |||
To write integration tests, create a standard test case by creating a python file starting with `test_` | |||
All integration tests will be run at the end of the unittests. | |||
### Example | |||
Here is an example of an integration test to check insertion of a To Do | |||
from __future__ import print_function | |||
from frappe.utils.selenium_testdriver import TestDriver | |||
import unittest | |||
import time | |||
class TestToDo(unittest.TestCase): | |||
def setUp(self): | |||
self.driver = TestDriver() | |||
def test_todo(self): | |||
self.driver.login() | |||
# list view | |||
self.driver.set_route('List', 'ToDo') | |||
time.sleep(2) | |||
# new | |||
self.driver.click_primary_action() | |||
time.sleep(2) | |||
# set input | |||
self.driver.set_text_editor('description', 'hello') | |||
# save | |||
self.driver.click_modal_primary_action() | |||
time.sleep(2) | |||
self.assertTrue(self.driver.get_visible_element('.result-list') | |||
.find_element_by_css_selector('.list-item') | |||
.find_element_by_css_selector('.list-id').text=='hello') | |||
def tearDown(self): | |||
self.driver.close() | |||
@@ -0,0 +1,70 @@ | |||
# 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"> | |||
### Running Tests | |||
To run a Test Runner based test, use the `run-ui-tests` bench command by passing the name of the file you want to run. | |||
bench run-ui-tests --test frappe/tests/ui/test_list.js | |||
This will pass the filename to `test_test_runner.py` that will load the required JS in the browser and execute the tests | |||
### Adding Fixtures / Test Data | |||
You can also add data that you require for all tests in the `tests/ui/data` folder of your app. All the files in this folder will be loaded in the browser before running the test. | |||
The file `frappe/tests/ui/data/test_lib.js`, which contains library functions for testing is always loaded. | |||
### Running All UI Tests | |||
To run all UI tests together for your app run | |||
bench run-ui-tests --app [app_name] | |||
This will run all the files in your `tests/ui` folder one by one. | |||
### 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_text = frappe.utils.get_random(10); | |||
frappe.run_serially([ | |||
() => frappe.set_route('List', 'ToDo'), | |||
() => frappe.new_doc('ToDo'), | |||
() => frappe.quick_entry.dialog.set_value('description', random_text), | |||
() => frappe.quick_entry.insert(), | |||
(doc) => { | |||
assert.ok(doc && !doc.__islocal); | |||
return frappe.set_route('Form', 'ToDo', doc.name); | |||
}, | |||
() => assert.ok(cur_frm.doc.description.includes(random_text)), | |||
// Delete the created ToDo | |||
() => frappe.tests.click_page_head_item('Menu'), | |||
() => frappe.tests.click_dropdown_item('Delete'), | |||
() => frappe.tests.click_page_head_item('Yes'), | |||
() => done() | |||
]); | |||
}); | |||
### Writing Test Friendly Code with Promises | |||
Promises are a great way to write test-friendly code. If your method calls an aysnchronous call (ajax), then you should return an `Promise` object. While writing tests, if you encounter a function that does not return a `Promise` object, you should update the code to return a `Promise` object. |
@@ -1,4 +1,4 @@ | |||
# Writing Tests Guide | |||
# Unit Testing | |||
## 1.Introduction | |||
@@ -16,12 +16,12 @@ Frappe provides some basic tooling to quickly write automated tests. There are s | |||
This function will build all the test dependencies and run your tests. | |||
You should run tests from "frappe_bench" folder. Without options all tests will be run. | |||
bench run-tests | |||
bench run-tests | |||
If you need more information about test execution - you can use verbose log level for bench. | |||
bench --verbose run-tests | |||
### Options: | |||
--app <AppName> | |||
@@ -30,9 +30,9 @@ If you need more information about test execution - you can use verbose log leve | |||
--module <Module> (Run a particular module that has tests) | |||
--profile (Runs a Python profiler on the test) | |||
--junit-xml-output<PathToXML> (The command provides test results in the standard XUnit XML format) | |||
#### 2.1. Example for app: | |||
All applications are located in folder: "~/frappe-bench/apps". | |||
All applications are located in folder: "~/frappe-bench/apps". | |||
We can run tests for each application. | |||
- frappe-bench/apps/erpnext/ | |||
@@ -50,7 +50,7 @@ We can run tests for each application. | |||
. | |||
---------------------------------------------------------------------- | |||
Ran 1 test in 0.008s | |||
OK | |||
#### 2.3. Example for test: | |||
@@ -60,44 +60,44 @@ Run a specific case in User: | |||
. | |||
---------------------------------------------------------------------- | |||
Ran 1 test in 0.005s | |||
OK | |||
#### 2.4. Example for module: | |||
If we want to run tests in the module: | |||
/home/frappe/frappe-bench/apps/erpnext/erpnext/support/doctype/issue/test_issue.py | |||
We should use module name like this (related to application folder) | |||
erpnext.support.doctype.issue.test_issue | |||
#####EXAMPLE: | |||
frappe@erpnext:~/frappe-bench$ bench run-tests --module "erpnext.stock.doctype.stock_entry.test_stock_entry" | |||
........................... | |||
---------------------------------------------------------------------- | |||
Ran 27 tests in 30.549s | |||
#### 2.5. Example for profile: | |||
frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost" --profile | |||
. | |||
---------------------------------------------------------------------- | |||
Ran 1 test in 0.010s | |||
OK | |||
9133 function calls (8912 primitive calls) in 0.011 seconds | |||
Ordered by: cumulative time | |||
ncalls tottime percall cumtime percall filename:lineno(function) | |||
2 0.000 0.000 0.008 0.004 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:187(insert) | |||
1 0.000 0.000 0.003 0.003 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:386(_validate) | |||
13 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/database.py:77(sql) | |||
255 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py:91(get) | |||
12 0.000 0.000 0.002 0.000 | |||
12 0.000 0.000 0.002 0.000 | |||
#### 2.6. Example for XUnit XML: | |||
@@ -118,7 +118,7 @@ We should use module name like this (related to application folder) | |||
It’s designed for the CI Jenkins, but will work for anything else that understands an XUnit-formatted XML representation of test results. | |||
#### Jenkins configuration support: | |||
1. You should install xUnit plugin - https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin | |||
1. You should install xUnit plugin - https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin | |||
2. After installation open Jenkins job configuration, click the box named “Publish JUnit test result report” under the "Post-build Actions" and enter path to XML report: | |||
(Example: _reports/*.xml_) | |||
@@ -197,9 +197,3 @@ It’s designed for the CI Jenkins, but will work for anything else that underst | |||
self.assertTrue("_Test Event 3" in subjects) | |||
self.assertFalse("_Test Event 2" in subjects) | |||
## 4. Client Side Testing (Using Selenium) | |||
This feature is still under development. | |||
For an example see, [https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py](https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py) |
@@ -12,7 +12,7 @@ Frappe ships with a boiler plate for a new app. The command `bench make-app | |||
app-name` helps you start a new app by starting an interactive shell. | |||
% bench make-app sample_app | |||
% bench new-app sample_app | |||
App Name: sample_app | |||
App Title: Sample App | |||
App Description: This is a sample app. | |||
@@ -1,7 +1,7 @@ | |||
# Les vues web | |||
Frappe a deux principaux environnements, le **bureau** et **le web**. Le **bureau** est un environnement riche AJAX alors | |||
que **le web** est une collection plus traditionnelle de fichers HTML pour la consultation publique. Les vues web peuvent | |||
que **le web** est une collection plus traditionnelle de fichiers HTML pour la consultation publique. Les vues web peuvent | |||
aussi être générées pour créer des vues plus controllées pour les utilisateurs qui peuvent se connecter mais qui n'ont pas | |||
accès au desk. | |||
@@ -585,7 +585,7 @@ | |||
"label": "Format", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "HTML\nXLS\nCSV", | |||
"options": "HTML\nXLSX\nCSV", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
@@ -669,7 +669,7 @@ | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-04-25 03:31:55.214149", | |||
"modified": "2017-06-30 12:54:13.350902", | |||
"modified_by": "Administrator", | |||
"module": "Email", | |||
"name": "Auto Email Report", | |||
@@ -8,7 +8,7 @@ from frappe import _ | |||
from frappe.model.document import Document | |||
from datetime import timedelta | |||
import frappe.utils | |||
from frappe.utils.xlsutils import get_xls | |||
from frappe.utils.xlsxutils import make_xlsx | |||
from frappe.utils.csvutils import to_csv | |||
max_reports_per_user = 3 | |||
@@ -43,7 +43,7 @@ class AutoEmailReport(Document): | |||
def validate_report_format(self): | |||
""" check if user has select correct report format """ | |||
valid_report_formats = ["HTML", "XLS", "CSV"] | |||
valid_report_formats = ["HTML", "XLSX", "CSV"] | |||
if self.format not in valid_report_formats: | |||
frappe.throw(_("%s is not a valid report format. Report format should \ | |||
one of the following %s"%(frappe.bold(self.format), frappe.bold(", ".join(valid_report_formats))))) | |||
@@ -70,11 +70,14 @@ class AutoEmailReport(Document): | |||
if self.format == 'HTML': | |||
return self.get_html_table(columns, data) | |||
elif self.format == 'XLS': | |||
return get_xls(columns, data) | |||
elif self.format == 'XLSX': | |||
spreadsheet_data = self.get_spreadsheet_data(columns, data) | |||
xlsx_file = make_xlsx(spreadsheet_data, "Auto Email Report") | |||
return xlsx_file.getvalue() | |||
elif self.format == 'CSV': | |||
return self.get_csv(columns, data) | |||
spreadsheet_data = self.get_spreadsheet_data(columns, data) | |||
return to_csv(spreadsheet_data) | |||
else: | |||
frappe.throw(_('Invalid Output Format')) | |||
@@ -85,7 +88,8 @@ class AutoEmailReport(Document): | |||
'data': data | |||
}) | |||
def get_csv(self, columns, data): | |||
@staticmethod | |||
def get_spreadsheet_data(columns, data): | |||
out = [[df.label for df in columns], ] | |||
for row in data: | |||
new_row = [] | |||
@@ -93,7 +97,7 @@ class AutoEmailReport(Document): | |||
for df in columns: | |||
new_row.append(frappe.format(row[df.fieldname], df, row)) | |||
return to_csv(out) | |||
return out | |||
def get_file_name(self): | |||
return "{0}.{1}".format(self.report.replace(" ", "-").replace("/", "-"), self.format.lower()) | |||
@@ -33,7 +33,7 @@ class TestAutoEmailReport(unittest.TestCase): | |||
data = auto_email_report.get_report_content() | |||
self.assertTrue('"Language","Core"' in data) | |||
auto_email_report.format = 'XLS' | |||
auto_email_report.format = 'XLSX' | |||
data = auto_email_report.get_report_content() | |||
@@ -119,7 +119,8 @@ frappe.ui.form.on("Email Account", { | |||
}, | |||
show_gmail_message_for_less_secure_apps: function(frm) { | |||
if(frm.doc.service==="Gmail") { | |||
frm.dashboard.clear_headline(); | |||
if(frm.doc.service==="GMail") { | |||
frm.dashboard.set_headline_alert('Gmail will only work if you allow access for less secure \ | |||
apps in Gmail settings. <a target="_blank" \ | |||
href="https://support.google.com/accounts/answer/6010255?hl=en">Read this for details</a>'); | |||
@@ -330,6 +330,8 @@ class EmailAccount(Document): | |||
# gmail shows sent emails in inbox | |||
# and we don't want emails sent by us to be pulled back into the system again | |||
# dont count emails sent by the system get those | |||
if frappe.flags.in_test: | |||
print('WARN: Cannot pull email. Sender sames as recipient inbox') | |||
raise SentEmailInInbox | |||
if email.message_id: | |||
@@ -472,7 +474,6 @@ class EmailAccount(Document): | |||
parent = frappe._dict(doctype=self.append_to, name=parent[0].name) | |||
return parent | |||
def create_new_parent(self, communication, email): | |||
'''If no parent found, create a new reference document''' | |||
@@ -18,6 +18,7 @@ frappe.email_alert = { | |||
// set value changed options | |||
frm.set_df_property("value_changed", "options", [""].concat(options)); | |||
frm.set_df_property("set_property_after_alert", "options", [""].concat(options)); | |||
// set date changed options | |||
frm.set_df_property("date_changed", "options", $.map(fields, | |||
@@ -1,5 +1,6 @@ | |||
{ | |||
"allow_copy": 0, | |||
"allow_guest_to_view": 0, | |||
"allow_import": 0, | |||
"allow_rename": 1, | |||
"autoname": "Prompt", | |||
@@ -13,6 +14,7 @@ | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -24,6 +26,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Enabled", | |||
@@ -41,6 +44,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -51,6 +55,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Filters", | |||
@@ -68,6 +73,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -79,6 +85,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 1, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Subject", | |||
@@ -96,6 +103,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -106,6 +114,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 1, | |||
"label": "Document Type", | |||
@@ -124,6 +133,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -134,6 +144,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Is Standard", | |||
@@ -152,6 +163,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -163,6 +175,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 1, | |||
"label": "Module", | |||
@@ -182,6 +195,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -192,6 +206,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"length": 0, | |||
@@ -209,6 +224,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -219,6 +235,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Send Alert On", | |||
@@ -237,6 +254,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -249,6 +267,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Trigger Method", | |||
@@ -267,6 +286,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -279,6 +299,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Reference Date", | |||
@@ -296,6 +317,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -309,6 +331,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Days Before or After", | |||
@@ -326,6 +349,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -338,6 +362,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Value Changed", | |||
@@ -355,6 +380,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -365,6 +391,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"length": 0, | |||
@@ -382,6 +409,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -394,6 +422,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 1, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Condition", | |||
@@ -411,6 +440,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -421,6 +451,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"length": 0, | |||
@@ -437,6 +468,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -447,6 +479,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"length": 0, | |||
@@ -464,6 +497,97 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 1, | |||
"columns": 0, | |||
"fieldname": "property_section", | |||
"fieldtype": "Section Break", | |||
"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": "Set Property After Alert", | |||
"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": "set_property_after_alert", | |||
"fieldtype": "Select", | |||
"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": "Set Property After Alert", | |||
"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": "property_value", | |||
"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": "Value To Be Set", | |||
"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, | |||
@@ -474,6 +598,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Recipients", | |||
@@ -491,6 +616,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -501,6 +627,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Recipients", | |||
@@ -519,6 +646,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -529,6 +657,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Message", | |||
@@ -546,18 +675,20 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"default": "Add your message here", | |||
"depends_on": "", | |||
"depends_on": "eval:!doc.is_standard", | |||
"fieldname": "message", | |||
"fieldtype": "Code", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 1, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Message", | |||
@@ -575,6 +706,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -585,6 +717,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Attach Print", | |||
@@ -603,6 +736,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -613,6 +747,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Message Examples", | |||
@@ -631,6 +766,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -641,6 +777,7 @@ | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "View Properties (via Customize Form)", | |||
@@ -659,19 +796,19 @@ | |||
"unique": 0 | |||
} | |||
], | |||
"has_web_view": 0, | |||
"hide_heading": 0, | |||
"hide_toolbar": 0, | |||
"icon": "fa fa-envelope", | |||
"idx": 0, | |||
"image_view": 0, | |||
"in_create": 0, | |||
"in_dialog": 0, | |||
"is_submittable": 0, | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"menu_index": 0, | |||
"modified": "2016-12-29 14:40:25.782293", | |||
"modified": "2017-07-07 16:09:48.804218", | |||
"modified_by": "Administrator", | |||
"module": "Email", | |||
"name": "Email Alert", | |||
@@ -688,7 +825,6 @@ | |||
"export": 1, | |||
"if_owner": 0, | |||
"import": 0, | |||
"is_custom": 0, | |||
"permlevel": 0, | |||
"print": 0, | |||
"read": 1, | |||
@@ -703,6 +839,7 @@ | |||
"quick_entry": 0, | |||
"read_only": 0, | |||
"read_only_onload": 0, | |||
"show_name_in_global_search": 0, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"title_field": "subject", | |||
@@ -159,6 +159,11 @@ def get_context(context): | |||
reference_name = doc.name, | |||
attachments = attachments) | |||
if self.set_property_after_alert: | |||
frappe.db.set_value(doc.doctype, doc.name, self.set_property_after_alert, | |||
self.property_value, update_modified = False) | |||
doc.set(self.set_property_after_alert, self.property_value) | |||
def load_standard_properties(self, context): | |||
module = get_doc_module(self.module, self.doctype, self.name) | |||
if module: | |||
@@ -7,6 +7,8 @@ import unittest | |||
test_records = frappe.get_test_records('Email Alert') | |||
test_dependencies = ["User"] | |||
class TestEmailAlert(unittest.TestCase): | |||
def setUp(self): | |||
frappe.db.sql("""delete from `tabEmail Queue`""") | |||
@@ -32,6 +34,9 @@ class TestEmailAlert(unittest.TestCase): | |||
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Communication", | |||
"reference_name": communication.name, "status":"Not Sent"})) | |||
self.assertEquals(frappe.db.get_value('Communication', | |||
communication.name, 'subject'), '__testing__') | |||
def test_condition(self): | |||
event = frappe.new_doc("Event") | |||
event.subject = "test", | |||
@@ -137,7 +142,7 @@ class TestEmailAlert(unittest.TestCase): | |||
event.save() | |||
# Value Change email alert alert will be trigger as description is not changed | |||
# mail will not be sent | |||
# mail will not be sent | |||
self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", | |||
"reference_name": event.name, "status":"Not Sent"})) | |||
@@ -21,7 +21,9 @@ | |||
"condition": "doc.communication_type=='Comment'", | |||
"recipients": [ | |||
{ "email_by_document_field": "owner" } | |||
] | |||
], | |||
"set_property_after_alert": "subject", | |||
"property_value": "__testing__" | |||
}, | |||
{ | |||
"doctype": "Email Alert", | |||
@@ -26,7 +26,7 @@ class EmailGroup(Document): | |||
for user in frappe.db.get_all(doctype, [email_field, unsubscribed_field or "name"]): | |||
try: | |||
email = parse_addr(user.get(email_field))[1] | |||
email = parse_addr(user.get(email_field))[1] if user.get(email_field) else None | |||
if email: | |||
frappe.get_doc({ | |||
"doctype": "Email Group Member", | |||
@@ -1,5 +1,6 @@ | |||
{ | |||
"allow_copy": 0, | |||
"allow_guest_to_view": 0, | |||
"allow_import": 0, | |||
"allow_rename": 0, | |||
"autoname": "hash", | |||
@@ -14,6 +15,7 @@ | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -43,6 +45,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -72,6 +75,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -101,6 +105,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -129,6 +134,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -159,6 +165,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -187,6 +194,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -216,6 +224,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -245,6 +254,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -273,6 +283,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -303,6 +314,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -332,6 +344,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -362,6 +375,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -392,6 +406,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -421,6 +436,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -450,6 +466,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -477,20 +494,50 @@ | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "attachments", | |||
"fieldtype": "Code", | |||
"hidden": 1, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Attachments", | |||
"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, | |||
"icon": "fa fa-envelope", | |||
"idx": 1, | |||
"image_view": 0, | |||
"in_create": 1, | |||
"in_dialog": 0, | |||
"is_submittable": 0, | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-02-24 17:42:10.878546", | |||
"modified": "2017-07-07 16:29:15.780393", | |||
"modified_by": "Administrator", | |||
"module": "Email", | |||
"name": "Email Queue", | |||
@@ -70,8 +70,9 @@ class Newsletter(Document): | |||
for file in files: | |||
try: | |||
file = get_file(file.name) | |||
attachments.append({"fname": file[0], "fcontent": file[1]}) | |||
# these attachments will be attached on-demand | |||
# and won't be stored in the message | |||
attachments.append({"fid": file.name}) | |||
except IOError: | |||
frappe.throw(_("Unable to find attachment {0}").format(a)) | |||
@@ -2,7 +2,7 @@ | |||
# MIT License. See license.txt | |||
from __future__ import unicode_literals | |||
import frappe, re | |||
import frappe, re, os | |||
from frappe.utils.pdf import get_pdf | |||
from frappe.email.smtp import get_outgoing_email_account | |||
from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint, | |||
@@ -15,21 +15,31 @@ from email.mime.multipart import MIMEMultipart | |||
def get_email(recipients, sender='', msg='', subject='[No Subject]', | |||
text_content = None, footer=None, print_html=None, formatted=None, attachments=None, | |||
content=None, reply_to=None, cc=[], email_account=None, expose_recipients=None, | |||
inline_images=[]): | |||
"""send an html email as multipart with attachments and all""" | |||
inline_images=[], header=False): | |||
""" Prepare an email with the following format: | |||
- multipart/mixed | |||
- multipart/alternative | |||
- text/plain | |||
- multipart/related | |||
- text/html | |||
- inline image | |||
- attachment | |||
""" | |||
content = content or msg | |||
emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, email_account=email_account, expose_recipients=expose_recipients) | |||
if not content.strip().startswith("<"): | |||
content = markdown(content) | |||
emailobj.set_html(content, text_content, footer=footer, | |||
emailobj.set_html(content, text_content, footer=footer, header=header, | |||
print_html=print_html, formatted=formatted, inline_images=inline_images) | |||
if isinstance(attachments, dict): | |||
attachments = [attachments] | |||
for attach in (attachments or []): | |||
# cannot attach if no filecontent | |||
if attach.get('fcontent') is None: continue | |||
emailobj.add_attachment(**attach) | |||
return emailobj | |||
@@ -58,18 +68,19 @@ class EMail: | |||
self.expose_recipients = expose_recipients | |||
self.msg_root = MIMEMultipart('mixed') | |||
self.msg_multipart = MIMEMultipart('alternative') | |||
self.msg_root.attach(self.msg_multipart) | |||
self.msg_alternative = MIMEMultipart('alternative') | |||
self.msg_root.attach(self.msg_alternative) | |||
self.cc = cc or [] | |||
self.html_set = False | |||
self.email_account = email_account or get_outgoing_email_account() | |||
def set_html(self, message, text_content = None, footer=None, print_html=None, | |||
formatted=None, inline_images=None): | |||
formatted=None, inline_images=None, header=False): | |||
"""Attach message in the html portion of multipart/alternative""" | |||
if not formatted: | |||
formatted = get_formatted_html(self.subject, message, footer, print_html, email_account=self.email_account) | |||
formatted = get_formatted_html(self.subject, message, footer, print_html, | |||
email_account=self.email_account, header=header) | |||
# this is the first html part of a multi-part message, | |||
# convert to text well | |||
@@ -88,33 +99,33 @@ class EMail: | |||
""" | |||
from email.mime.text import MIMEText | |||
part = MIMEText(message, 'plain', 'utf-8') | |||
self.msg_multipart.attach(part) | |||
self.msg_alternative.attach(part) | |||
def set_part_html(self, message, inline_images): | |||
from email.mime.text import MIMEText | |||
if inline_images: | |||
related = MIMEMultipart('related') | |||
for image in inline_images: | |||
# images in dict like {filename:'', filecontent:'raw'} | |||
content_id = random_string(10) | |||
has_inline_images = re.search('''embed=['"].*?['"]''', message) | |||
# replace filename in message with CID | |||
message = re.sub('''src=['"]{0}['"]'''.format(image.get('filename')), | |||
'src="cid:{0}"'.format(content_id), message) | |||
if has_inline_images: | |||
# process inline images | |||
message, _inline_images = replace_filename_with_cid(message) | |||
self.add_attachment(image.get('filename'), image.get('filecontent'), | |||
None, content_id=content_id, parent=related) | |||
# prepare parts | |||
msg_related = MIMEMultipart('related') | |||
html_part = MIMEText(message, 'html', 'utf-8') | |||
related.attach(html_part) | |||
msg_related.attach(html_part) | |||
self.msg_multipart.attach(related) | |||
for image in _inline_images: | |||
self.add_attachment(image.get('filename'), image.get('filecontent'), | |||
content_id=image.get('content_id'), parent=msg_related, inline=True) | |||
self.msg_alternative.attach(msg_related) | |||
else: | |||
self.msg_multipart.attach(MIMEText(message, 'html', 'utf-8')) | |||
self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8')) | |||
def set_html_as_text(self, html): | |||
"""return html2text""" | |||
"""Set plain text from HTML""" | |||
self.set_text(to_markdown(html)) | |||
def set_message(self, message, mime_type='text/html', as_attachment=0, filename='attachment.html'): | |||
@@ -139,50 +150,13 @@ class EMail: | |||
self.add_attachment(res[0], res[1]) | |||
def add_attachment(self, fname, fcontent, content_type=None, | |||
parent=None, content_id=None): | |||
parent=None, content_id=None, inline=False): | |||
"""add attachment""" | |||
from email.mime.audio import MIMEAudio | |||
from email.mime.base import MIMEBase | |||
from email.mime.image import MIMEImage | |||
from email.mime.text import MIMEText | |||
import mimetypes | |||
if not content_type: | |||
content_type, encoding = mimetypes.guess_type(fname) | |||
if content_type is None: | |||
# No guess could be made, or the file is encoded (compressed), so | |||
# use a generic bag-of-bits type. | |||
content_type = 'application/octet-stream' | |||
maintype, subtype = content_type.split('/', 1) | |||
if maintype == 'text': | |||
# Note: we should handle calculating the charset | |||
if isinstance(fcontent, unicode): | |||
fcontent = fcontent.encode("utf-8") | |||
part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8") | |||
elif maintype == 'image': | |||
part = MIMEImage(fcontent, _subtype=subtype) | |||
elif maintype == 'audio': | |||
part = MIMEAudio(fcontent, _subtype=subtype) | |||
else: | |||
part = MIMEBase(maintype, subtype) | |||
part.set_payload(fcontent) | |||
# Encode the payload using Base64 | |||
from email import encoders | |||
encoders.encode_base64(part) | |||
# Set the filename parameter | |||
if fname: | |||
part.add_header(b'Content-Disposition', | |||
("attachment; filename=\"%s\"" % fname).encode('utf-8')) | |||
if content_id: | |||
part.add_header(b'Content-ID', '<{0}>'.format(content_id)) | |||
if not parent: | |||
parent = self.msg_root | |||
parent.attach(part) | |||
add_attachment(fname, fcontent, content_type, parent, content_id, inline) | |||
def add_pdf_attachment(self, name, html, options=None): | |||
self.add_attachment(name, get_pdf(html, options), 'application/octet-stream') | |||
@@ -259,11 +233,12 @@ class EMail: | |||
self.make() | |||
return self.msg_root.as_string() | |||
def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None): | |||
def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=False): | |||
if not email_account: | |||
email_account = get_outgoing_email_account(False) | |||
rendered_email = frappe.get_template("templates/emails/standard.html").render({ | |||
"header": get_header() if header else None, | |||
"content": message, | |||
"signature": get_signature(email_account), | |||
"footer": get_footer(email_account, footer), | |||
@@ -274,6 +249,52 @@ def get_formatted_html(subject, message, footer=None, print_html=None, email_acc | |||
return scrub_urls(rendered_email) | |||
def add_attachment(fname, fcontent, content_type=None, | |||
parent=None, content_id=None, inline=False): | |||
"""Add attachment to parent which must an email object""" | |||
from email.mime.audio import MIMEAudio | |||
from email.mime.base import MIMEBase | |||
from email.mime.image import MIMEImage | |||
from email.mime.text import MIMEText | |||
import mimetypes | |||
if not content_type: | |||
content_type, encoding = mimetypes.guess_type(fname) | |||
if not parent: | |||
return | |||
if content_type is None: | |||
# No guess could be made, or the file is encoded (compressed), so | |||
# use a generic bag-of-bits type. | |||
content_type = 'application/octet-stream' | |||
maintype, subtype = content_type.split('/', 1) | |||
if maintype == 'text': | |||
# Note: we should handle calculating the charset | |||
if isinstance(fcontent, unicode): | |||
fcontent = fcontent.encode("utf-8") | |||
part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8") | |||
elif maintype == 'image': | |||
part = MIMEImage(fcontent, _subtype=subtype) | |||
elif maintype == 'audio': | |||
part = MIMEAudio(fcontent, _subtype=subtype) | |||
else: | |||
part = MIMEBase(maintype, subtype) | |||
part.set_payload(fcontent) | |||
# Encode the payload using Base64 | |||
from email import encoders | |||
encoders.encode_base64(part) | |||
# Set the filename parameter | |||
if fname: | |||
attachment_type = 'inline' if inline else 'attachment' | |||
part.add_header(b'Content-Disposition', attachment_type, filename=fname.encode('utf=8')) | |||
if content_id: | |||
part.add_header(b'Content-ID', '<{0}>'.format(content_id)) | |||
parent.attach(part) | |||
def get_message_id(): | |||
'''Returns Message ID created from doctype and name''' | |||
return "<{unique}@{site}>".format( | |||
@@ -298,11 +319,100 @@ 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"): | |||
footer += '<div style="margin: 15px auto;">{0}</div>'.format(default_mail_footer) | |||
return footer | |||
def replace_filename_with_cid(message): | |||
""" Replaces <img embed="assets/frappe/images/filename.jpg" ...> with | |||
<img src="cid:content_id" ...> and return the modified message and | |||
a list of inline_images with {filename, filecontent, content_id} | |||
""" | |||
inline_images = [] | |||
while True: | |||
matches = re.search('''embed=["'](.*?)["']''', message) | |||
if not matches: break | |||
groups = matches.groups() | |||
# found match | |||
img_path = groups[0] | |||
filename = img_path.rsplit('/')[-1] | |||
filecontent = get_filecontent_from_path(img_path) | |||
if not filecontent: | |||
message = re.sub('''embed=['"]{0}['"]'''.format(img_path), '', message) | |||
continue | |||
content_id = random_string(10) | |||
inline_images.append({ | |||
'filename': filename, | |||
'filecontent': filecontent, | |||
'content_id': content_id | |||
}) | |||
message = re.sub('''embed=['"]{0}['"]'''.format(img_path), | |||
'src="cid:{0}"'.format(content_id), message) | |||
return (message, inline_images) | |||
def get_filecontent_from_path(path): | |||
if not path: return | |||
if path.startswith('/'): | |||
path = path[1:] | |||
if path.startswith('assets/'): | |||
# from public folder | |||
full_path = os.path.abspath(path) | |||
elif path.startswith('files/'): | |||
# public file | |||
full_path = frappe.get_site_path('public', path) | |||
elif path.startswith('private/files/'): | |||
# private file | |||
full_path = frappe.get_site_path(path) | |||
else: | |||
full_path = path | |||
if os.path.exists(full_path): | |||
with open(full_path) as f: | |||
filecontent = f.read() | |||
return filecontent | |||
else: | |||
print(full_path + ' doesn\'t exists') | |||
return None | |||
def get_header(): | |||
""" Build header from template """ | |||
from frappe.utils.jinja import get_email_from_template | |||
default_brand_image = 'assets/frappe/images/favicon.png' # svg doesn't work in email | |||
email_brand_image = frappe.get_hooks('email_brand_image') | |||
if len(email_brand_image): | |||
email_brand_image = email_brand_image[-1] | |||
else: | |||
email_brand_image = default_brand_image | |||
email_brand_image = default_brand_image | |||
brand_text = frappe.get_hooks('app_title')[-1] | |||
email_header, text = get_email_from_template('email_header', { | |||
'brand_image': email_brand_image, | |||
'brand_text': brand_text | |||
}) | |||
return email_header |
@@ -5,29 +5,32 @@ from __future__ import unicode_literals | |||
from six.moves import range | |||
import frappe | |||
import HTMLParser | |||
import smtplib, quopri | |||
import smtplib, quopri, json | |||
from frappe import msgprint, throw, _ | |||
from frappe.email.smtp import SMTPServer, get_outgoing_email_account | |||
from frappe.email.email_body import get_email, get_formatted_html | |||
from frappe.email.email_body import get_email, get_formatted_html, add_attachment | |||
from frappe.utils.verified_command import get_signed_params, verify_request | |||
from html2text import html2text | |||
from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint | |||
from frappe.utils.file_manager import get_file | |||
from rq.timeouts import JobTimeoutException | |||
from frappe.utils.scheduler import log | |||
class EmailLimitCrossedError(frappe.ValidationError): pass | |||
def send(recipients=None, sender=None, subject=None, message=None, reference_doctype=None, | |||
def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None, | |||
reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, | |||
attachments=None, reply_to=None, cc=[], message_id=None, in_reply_to=None, send_after=None, | |||
expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None, | |||
queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None): | |||
queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None, | |||
header=False): | |||
"""Add email to sending queue (Email Queue) | |||
:param recipients: List of recipients. | |||
:param sender: Email sender. | |||
:param subject: Email subject. | |||
:param message: Email message. | |||
:param text_content: Text version of email message. | |||
:param reference_doctype: Reference DocType of caller document. | |||
:param reference_name: Reference name of caller document. | |||
:param send_priority: Priority for Email Queue, default 1. | |||
@@ -43,6 +46,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc | |||
:param is_notification: Marks email as notification so will not trigger notifications from system | |||
:param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1. | |||
:param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id | |||
:param header: Append header in email (boolean) | |||
""" | |||
if not unsubscribe_method: | |||
unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe" | |||
@@ -65,12 +69,13 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc | |||
check_email_limit(recipients) | |||
formatted = get_formatted_html(subject, message, email_account=email_account) | |||
if not text_content: | |||
try: | |||
text_content = html2text(message) | |||
except HTMLParser.HTMLParseError: | |||
text_content = "See html attachment" | |||
try: | |||
text_content = html2text(formatted) | |||
except HTMLParser.HTMLParseError: | |||
text_content = "See html attachment" | |||
formatted = get_formatted_html(subject, message, email_account=email_account, header=header) | |||
if reference_doctype and reference_name: | |||
unsubscribed = [d.email for d in frappe.db.get_all("Email Unsubscribe", "email", | |||
@@ -114,6 +119,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc | |||
queue_separately=queue_separately, | |||
is_notification = is_notification, | |||
inline_images = inline_images, | |||
header=header, | |||
now=now) | |||
@@ -143,6 +149,14 @@ def get_email_queue(recipients, sender, subject, **kwargs): | |||
'''Make Email Queue object''' | |||
e = frappe.new_doc('Email Queue') | |||
e.priority = kwargs.get('send_priority') | |||
attachments = kwargs.get('attachments') | |||
if attachments: | |||
# store attachments with fid, to be attached on-demand later | |||
_attachments = [] | |||
for att in attachments: | |||
if att.get('fid'): | |||
_attachments.append(att) | |||
e.attachments = json.dumps(_attachments) | |||
try: | |||
mail = get_email(recipients, | |||
@@ -155,7 +169,8 @@ def get_email_queue(recipients, sender, subject, **kwargs): | |||
cc=kwargs.get('cc'), | |||
email_account=kwargs.get('email_account'), | |||
expose_recipients=kwargs.get('expose_recipients'), | |||
inline_images=kwargs.get('inline_images')) | |||
inline_images=kwargs.get('inline_images'), | |||
header=kwargs.get('header')) | |||
mail.set_message_id(kwargs.get('message_id'),kwargs.get('is_notification')) | |||
if kwargs.get('read_receipt'): | |||
@@ -331,7 +346,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals | |||
email = frappe.db.sql('''select | |||
name, status, communication, message, sender, reference_doctype, | |||
reference_name, unsubscribe_param, unsubscribe_method, expose_recipients, | |||
show_as_cc, add_unsubscribe_link | |||
show_as_cc, add_unsubscribe_link, attachments | |||
from | |||
`tabEmail Queue` | |||
where | |||
@@ -424,6 +439,7 @@ where name=%s""", (unicode(e), email.name), auto_commit=auto_commit) | |||
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit) | |||
if now: | |||
print(frappe.get_traceback()) | |||
raise e | |||
else: | |||
@@ -457,7 +473,31 @@ def prepare_message(email, recipient, recipients_list): | |||
message = message.replace("<!--cc message-->", quopri.encodestring(email_sent_message)) | |||
message = message.replace("<!--recipient-->", recipient) | |||
return message | |||
if not email.attachments: | |||
return message | |||
# On-demand attachments | |||
from email.parser import Parser | |||
msg_obj = Parser().parsestr(message) | |||
attachments = json.loads(email.attachments) | |||
for attachment in attachments: | |||
if attachment.get('fcontent'): continue | |||
fid = attachment.get('fid') | |||
if not fid: continue | |||
fname, fcontent = get_file(fid) | |||
attachment.update({ | |||
'fname': fname, | |||
'fcontent': fcontent, | |||
'parent': msg_obj | |||
}) | |||
add_attachment(**attachment) | |||
return msg_obj.as_string() | |||
def clear_outbox(): | |||
"""Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days. | |||
@@ -475,8 +515,7 @@ def clear_outbox(): | |||
frappe.db.sql("""delete from `tabEmail Queue Recipient` where parent in (%s)""" | |||
% ','.join(['%s']*len(email_queues)), tuple(email_queues)) | |||
for dt in ("Email Queue", "Email Queue Recipient"): | |||
frappe.db.sql(""" | |||
update `tab{0}` | |||
set status='Expired' | |||
where datediff(curdate(), modified) > 7 and status='Not Sent'""".format(dt)) | |||
frappe.db.sql(""" | |||
update `tabEmail Queue` | |||
set status='Expired' | |||
where datediff(curdate(), modified) > 7 and status='Not Sent' and (send_after is null or send_after < %(now)s)""", { 'now': now_datetime() }) |
@@ -0,0 +1,101 @@ | |||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: GNU General Public License v3. See license.txt | |||
from __future__ import unicode_literals | |||
import frappe, unittest, os, base64 | |||
from frappe.email.email_body import replace_filename_with_cid, get_email | |||
class TestEmailBody(unittest.TestCase): | |||
def setUp(self): | |||
email_html = ''' | |||
<div> | |||
<h3>Hey John Doe!</h3> | |||
<p>This is embedded image you asked for</p> | |||
<img embed="assets/frappe/images/favicon.png" /> | |||
</div> | |||
''' | |||
email_text = ''' | |||
Hey John Doe! | |||
This is the text version of this email | |||
''' | |||
img_path = os.path.abspath('assets/frappe/images/favicon.png') | |||
with open(img_path) as f: | |||
img_content = f.read() | |||
img_base64 = base64.b64encode(img_content) | |||
# email body keeps 76 characters on one line | |||
self.img_base64 = fixed_column_width(img_base64, 76) | |||
self.email_string = get_email( | |||
recipients=['test@example.com'], | |||
sender='me@example.com', | |||
subject='Test Subject', | |||
content=email_html, | |||
text_content=email_text | |||
).as_string() | |||
def test_image(self): | |||
img_signature = ''' | |||
Content-Type: image/png | |||
MIME-Version: 1.0 | |||
Content-Transfer-Encoding: base64 | |||
Content-Disposition: inline; filename="favicon.png" | |||
''' | |||
self.assertTrue(img_signature in self.email_string) | |||
self.assertTrue(self.img_base64 in self.email_string) | |||
def test_text_content(self): | |||
text_content = ''' | |||
Content-Type: text/plain; charset="utf-8" | |||
MIME-Version: 1.0 | |||
Content-Transfer-Encoding: quoted-printable | |||
Hey John Doe! | |||
This is the text version of this email | |||
''' | |||
self.assertTrue(text_content in self.email_string) | |||
def test_email_content(self): | |||
html_head = ''' | |||
Content-Type: text/html; charset="utf-8" | |||
MIME-Version: 1.0 | |||
Content-Transfer-Encoding: quoted-printable | |||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.= | |||
w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |||
<html xmlns=3D"http://www.w3.org/1999/xhtml"> | |||
''' | |||
html = '''<h3>Hey John Doe!</h3>''' | |||
self.assertTrue(html_head in self.email_string) | |||
self.assertTrue(html in self.email_string) | |||
def test_replace_filename_with_cid(self): | |||
original_message = ''' | |||
<div> | |||
<img embed="assets/frappe/images/favicon.png" alt="test" /> | |||
<img embed="notexists.jpg" /> | |||
</div> | |||
''' | |||
message, inline_images = replace_filename_with_cid(original_message) | |||
processed_message = ''' | |||
<div> | |||
<img src="cid:{0}" alt="test" /> | |||
<img /> | |||
</div> | |||
'''.format(inline_images[0].get('content_id')) | |||
self.assertEquals(message, processed_message) | |||
def fixed_column_width(string, chunk_size): | |||
parts = [string[0+i:chunk_size+i] for i in range(0, len(string), chunk_size)] | |||
return '\n'.join(parts) |
@@ -102,7 +102,7 @@ class FrappeClient(object): | |||
:param doctype: `doctype` to be deleted | |||
:param name: `name` of document to be deleted''' | |||
return self.post_request({ | |||
"cmd": "frappe.model.delete_doc", | |||
"cmd": "frappe.client.delete", | |||
"doctype": doctype, | |||
"name": name | |||
}) | |||
@@ -272,6 +272,9 @@ def set_dropbox_access_token(access_token): | |||
frappe.db.commit() | |||
def generate_oauth2_access_token_from_oauth1_token(dropbox_settings=None): | |||
if not dropbox_settings.get("access_key") or not dropbox_settings.get("access_secret"): | |||
return {} | |||
url = "https://api.dropboxapi.com/2/auth/token/from_oauth1" | |||
headers = {"Content-Type": "application/json"} | |||
auth = (dropbox_settings["app_key"], dropbox_settings["app_secret"]) | |||
@@ -423,12 +423,11 @@ class BaseDocument(object): | |||
return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label)) | |||
elif self.parentfield: | |||
return "{}: {} {} #{}: {}: {}".format(_("Error"), frappe.bold(_(self.doctype)), | |||
_("Row"), self.idx, _("Value missing for"), _(df.label)) | |||
else: | |||
return "{}: {}: {}".format(_("Error"), _("Value missing for"), _(df.label)) | |||
return _("Error: Value missing for {0}: {1}").format(_(df.parent), _(df.label)) | |||
missing = [] | |||
@@ -423,7 +423,6 @@ class DatabaseQuery(object): | |||
def add_user_permissions(self, user_permissions, user_permission_doctypes=None): | |||
user_permission_doctypes = frappe.permissions.get_user_permission_doctypes(user_permission_doctypes, user_permissions) | |||
meta = frappe.get_meta(self.doctype) | |||
for doctypes in user_permission_doctypes: | |||
match_filters = {} | |||
match_conditions = [] | |||
@@ -431,12 +430,18 @@ class DatabaseQuery(object): | |||
for df in meta.get_fields_to_check_permissions(doctypes): | |||
user_permission_values = user_permissions.get(df.options, []) | |||
condition = 'ifnull(`tab{doctype}`.`{fieldname}`, "")=""'.format(doctype=self.doctype, fieldname=df.fieldname) | |||
cond = 'ifnull(`tab{doctype}`.`{fieldname}`, "")=""'.format(doctype=self.doctype, fieldname=df.fieldname) | |||
if user_permission_values: | |||
condition += """ or `tab{doctype}`.`{fieldname}` in ({values})""".format( | |||
if not cint(frappe.get_system_settings("apply_strict_user_permissions")): | |||
condition = cond + " or " | |||
else: | |||
condition = "" | |||
condition += """`tab{doctype}`.`{fieldname}` in ({values})""".format( | |||
doctype=self.doctype, fieldname=df.fieldname, | |||
values=", ".join([('"'+frappe.db.escape(v, percent=False)+'"') for v in user_permission_values]) | |||
) | |||
values=", ".join([('"'+frappe.db.escape(v, percent=False)+'"') for v in user_permission_values])) | |||
else: | |||
condition = cond | |||
match_conditions.append("({condition})".format(condition=condition)) | |||
match_filters[df.options] = user_permission_values | |||
@@ -43,6 +43,7 @@ type_map = { | |||
,'Attach': ('text', '') | |||
,'Attach Image':('text', '') | |||
,'Signature': ('longtext', '') | |||
,'Color': ('varchar', varchar_len) | |||
} | |||
default_columns = ['name', 'creation', 'modified', 'modified_by', 'owner', | |||
@@ -307,7 +308,7 @@ class DbTable: | |||
if not frappe.db.sql("show index from `%s` where key_name = %s" % | |||
(self.name, '%s'), col.fieldname): | |||
query.append("add index `{}`(`{}`)".format(col.fieldname, col.fieldname)) | |||
for col in self.drop_index: | |||
if col.fieldname != 'name': # primary key | |||
# if index key exists | |||
@@ -563,6 +564,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() | |||
@@ -59,6 +59,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa | |||
frappe.db.sql("delete from `tabCustom Script` where dt = %s", name) | |||
frappe.db.sql("delete from `tabProperty Setter` where doc_type = %s", name) | |||
frappe.db.sql("delete from `tabReport` where ref_doctype=%s", name) | |||
frappe.db.sql("delete from `tabCustom DocPerm` where parent=%s", name) | |||
delete_from_table(doctype, name, ignore_doctypes, None) | |||
@@ -99,6 +99,9 @@ def make_autoname(key='', doctype='', doc=''): | |||
def parse_naming_series(parts, doctype= '', doc = ''): | |||
n = '' | |||
if isinstance(parts, basestring): | |||
parts = parts.split('.') | |||
series_set = False | |||
today = now_datetime() | |||
for e in parts: | |||
@@ -142,6 +145,9 @@ def getseries(key, digits, doctype=''): | |||
def revert_series_if_last(key, name): | |||
if ".#" in key: | |||
prefix, hashes = key.rsplit(".", 1) | |||
if '.' in prefix: | |||
prefix = parse_naming_series(prefix.split('.')) | |||
if "#" not in hashes: | |||
return | |||
else: | |||
@@ -162,6 +162,7 @@ def update_link_field_values(link_fields, old, new, doctype): | |||
single_doc.set(field['fieldname'], new) | |||
# update single docs using ORM rather then query | |||
# as single docs also sometimes sets defaults! | |||
single_doc.flags.ignore_mandatory = True | |||
single_doc.save(ignore_permissions=True) | |||
except ImportError: | |||
# fails in patches where the doctype has been renamed | |||
@@ -180,8 +180,8 @@ def load_doctype_module(doctype, module=None, prefix="", suffix=""): | |||
try: | |||
if key not in doctype_python_modules: | |||
doctype_python_modules[key] = frappe.get_module(module_name) | |||
except ImportError: | |||
raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name)) | |||
except ImportError, e: | |||
raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name + ' Error: ' + str(e))) | |||
return doctype_python_modules[key] | |||
@@ -1,12 +0,0 @@ | |||
var chromedriver = require('chromedriver'); | |||
module.exports = { | |||
before: function (done) { | |||
chromedriver.start(); | |||
done(); | |||
}, | |||
after: function (done) { | |||
chromedriver.stop(); | |||
done(); | |||
} | |||
}; |
@@ -1,96 +0,0 @@ | |||
const fs = require('fs'); | |||
const ci_mode = get_cli_arg('env') === 'ci_server'; | |||
const site_name = get_cli_arg('site'); | |||
let app_name = get_cli_arg('app'); | |||
if(!app_name) { | |||
console.log('Please specify app to run tests'); | |||
return; | |||
} | |||
if(!ci_mode && !site_name) { | |||
console.log('Please specify site to run tests'); | |||
return; | |||
} | |||
// site url | |||
let site_url; | |||
if(site_name) { | |||
site_url = 'http://' + site_name + ':' + get_port(); | |||
} | |||
// multiple apps | |||
if(app_name.includes(',')) { | |||
app_name = app_name.split(','); | |||
} else { | |||
app_name = [app_name]; | |||
} | |||
let test_folders = []; | |||
let page_objects = []; | |||
for(const app of app_name) { | |||
const test_folder = `apps/${app}/${app}/tests/ui`; | |||
const page_object = test_folder + '/page_objects'; | |||
if(!fs.existsSync(test_folder)) { | |||
console.log(`No test folder found for "${app}"`); | |||
continue; | |||
} | |||
test_folders.push(test_folder); | |||
if(fs.existsSync(page_object)) { | |||
page_objects.push(); | |||
} | |||
} | |||
const config = { | |||
"src_folders": test_folders, | |||
"globals_path" : 'apps/frappe/frappe/nightwatch.global.js', | |||
"page_objects_path": page_objects, | |||
"selenium": { | |||
"start_process": false | |||
}, | |||
"test_settings": { | |||
"default": { | |||
"launch_url": site_url, | |||
"selenium_port": 9515, | |||
"selenium_host": "127.0.0.1", | |||
"default_path_prefix": "", | |||
"silent": true, | |||
// "screenshots": { | |||
// "enabled": true, | |||
// "path": SCREENSHOT_PATH | |||
// }, | |||
"globals": { | |||
"waitForConditionTimeout": 15000 | |||
}, | |||
"desiredCapabilities": { | |||
"browserName": "chrome", | |||
"chromeOptions": { | |||
"args": ["--no-sandbox", "--start-maximized"] | |||
}, | |||
"javascriptEnabled": true, | |||
"acceptSslCerts": true | |||
} | |||
}, | |||
"ci_server": { | |||
"launch_url": 'http://localhost:8000' | |||
} | |||
} | |||
} | |||
module.exports = config; | |||
function get_cli_arg(key) { | |||
var args = process.argv; | |||
var i = args.indexOf('--' + key); | |||
if(i === -1) { | |||
return null; | |||
} | |||
return args[i + 1]; | |||
} | |||
function get_port() { | |||
var bench_config = JSON.parse(fs.readFileSync('sites/common_site_config.json')); | |||
return bench_config.webserver_port; | |||
} |
@@ -186,3 +186,5 @@ 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 | |||
frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists |
@@ -0,0 +1,6 @@ | |||
import frappe | |||
def execute(): | |||
frappe.db.sql("""delete from `tabCustom DocPerm` | |||
where parent not in ( select name from `tabDocType` ) | |||
""") |
@@ -0,0 +1,14 @@ | |||
# 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: | |||
frappe.db.set_value("Auto Email Report", auto_email.name, "format", "XLSX") |
@@ -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 | |||
@@ -104,6 +104,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", | |||
@@ -197,6 +198,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", | |||
@@ -995,3 +995,51 @@ input[type="checkbox"]:checked:before { | |||
visibility: visible; | |||
} | |||
} | |||
.color-picker { | |||
position: relative; | |||
z-index: 999; | |||
} | |||
.color-picker .color-picker-pallete { | |||
border-radius: 4px; | |||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | |||
background: #fff; | |||
border: 1px solid #d1d8dd; | |||
width: 290px; | |||
height: 106px; | |||
padding-top: 10px; | |||
padding-left: 5px; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
} | |||
.color-picker .color-picker-pallete:after, | |||
.color-picker .color-picker-pallete:before { | |||
border: solid transparent; | |||
content: " "; | |||
height: 0; | |||
width: 0; | |||
pointer-events: none; | |||
position: absolute; | |||
bottom: 100%; | |||
left: 30px; | |||
} | |||
.color-picker .color-picker-pallete:after { | |||
border-color: rgba(255, 255, 255, 0); | |||
border-bottom-color: #fff; | |||
border-width: 8px; | |||
margin-left: -8px; | |||
} | |||
.color-picker .color-picker-pallete:before { | |||
border-color: rgba(221, 221, 221, 0); | |||
border-bottom-color: #d1d8dd; | |||
border-width: 9px; | |||
margin-left: -9px; | |||
} | |||
.color-picker .color-box { | |||
cursor: pointer; | |||
display: inline-block; | |||
width: 20px; | |||
height: 20px; | |||
margin: -2px 0 0 3px; | |||
border: 1px solid rgba(0, 0, 0, 0.25); | |||
} |
@@ -28,7 +28,8 @@ | |||
border-top: 1px solid #d1d8dd; | |||
} | |||
.form-message { | |||
padding: 15px; | |||
padding: 15px 30px; | |||
border-bottom: 1px solid #d1d8dd; | |||
} | |||
.document-flow-wrapper { | |||
padding: 40px 15px 30px; | |||
@@ -73,21 +74,24 @@ | |||
} | |||
.form-dashboard { | |||
background-color: #fafbfc; | |||
border-bottom: 1px solid #d1d8dd; | |||
} | |||
.form-dashboard-wrapper { | |||
margin: -15px 0px; | |||
} | |||
.form-documents h6 { | |||
margin-top: 15px; | |||
} | |||
.form-dashboard-section { | |||
margin: 0px -15px; | |||
padding: 15px 30px; | |||
border-bottom: 1px solid #EBEFF2; | |||
} | |||
.form-dashboard-section:first-child { | |||
padding-top: 0px; | |||
} | |||
.form-dashboard-section:last-child { | |||
border-bottom: none; | |||
} | |||
.form-heatmap { | |||
padding-top: 30px; | |||
} | |||
.form-heatmap .heatmap-message { | |||
margin-top: 10px; | |||
} | |||
@@ -496,6 +500,7 @@ h6.uppercase, | |||
} | |||
.like-disabled-input.for-description { | |||
font-weight: normal; | |||
font-size: 12px; | |||
} | |||
.frappe-control { | |||
margin-bottom: 10px; | |||
@@ -530,19 +535,17 @@ select.form-control { | |||
font-weight: bold; | |||
background-color: #fffdf4; | |||
} | |||
.form-headline { | |||
padding: 0px 15px; | |||
margin: 0px; | |||
.form-control[data-fieldtype="Password"] { | |||
position: inherit; | |||
} | |||
.form-headline .alert { | |||
font-size: 12px; | |||
background-color: #fffce7; | |||
font-weight: normal !important; | |||
border: 0px; | |||
border-radius: 0px; | |||
margin-bottom: 0px; | |||
margin: 0px -15px; | |||
padding: 10px 30px; | |||
.password-strength-indicator { | |||
float: right; | |||
padding: 15px; | |||
margin-top: -41px; | |||
margin-right: -7px; | |||
} | |||
.password-strength-message { | |||
margin-top: -10px; | |||
} | |||
.delivery-status-indicator { | |||
display: inline-block; | |||
@@ -195,6 +195,26 @@ 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.scrub = function(text) { | |||
return text.replace(/ /g, "_").toLowerCase(); | |||
}; | |||
frappe.get_modal = function(title, content) { | |||
return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body); | |||
}; | |||
@@ -4,10 +4,11 @@ | |||
frappe.ui.form.Dashboard = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.section = this.frm.fields_dict._form_dashboard.wrapper; | |||
this.parent = this.section.find('.section-body'); | |||
this.wrapper = $(frappe.render_template('form_dashboard', | |||
{frm: this.frm})).prependTo(this.frm.layout.wrapper); | |||
{frm: this.frm})).appendTo(this.parent); | |||
this.headline = this.wrapper.find('.form-headline'); | |||
this.progress_area = this.wrapper.find(".progress-area"); | |||
this.heatmap_area = this.wrapper.find('.form-heatmap'); | |||
this.chart_area = this.wrapper.find('.form-chart'); | |||
@@ -18,7 +19,7 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
}, | |||
reset: function() { | |||
this.wrapper.addClass('hidden'); | |||
this.section.addClass('hidden'); | |||
this.clear_headline(); | |||
// clear progress | |||
@@ -36,13 +37,10 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
this.wrapper.find('.custom').remove(); | |||
}, | |||
set_headline: function(html) { | |||
this.headline.html(html).removeClass('hidden'); | |||
this.show(); | |||
this.frm.layout.show_message(html); | |||
}, | |||
clear_headline: function() { | |||
if(this.headline) { | |||
this.headline.empty().addClass('hidden'); | |||
} | |||
this.frm.layout.show_message(); | |||
}, | |||
add_comment: function(text, permanent) { | |||
@@ -59,13 +57,12 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
this.clear_headline(); | |||
}, | |||
set_headline_alert: function(text, alert_class) { | |||
set_headline_alert: function(text, indicator_color) { | |||
if (!indicator_color) { | |||
indicator_color = 'orange'; | |||
} | |||
if(text) { | |||
if(!alert_class) alert_class = "alert-warning"; | |||
this.set_headline(repl('<div class="alert %(alert_class)s">%(text)s</div>', { | |||
"alert_class": alert_class || "", | |||
"text": text | |||
})); | |||
this.set_headline(`<div><span class="indicator ${indicator_color}">${text}</span></div>`); | |||
} else { | |||
this.clear_headline(); | |||
} | |||
@@ -406,6 +403,6 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
} | |||
}, | |||
show: function() { | |||
this.wrapper.removeClass('hidden'); | |||
this.section.removeClass('hidden'); | |||
} | |||
}); |
@@ -421,7 +421,7 @@ frappe.ui.form.Timeline = Class.extend({ | |||
out.push(me.get_version_comment(version, __('cancelled this document'))); | |||
} | |||
} else { | |||
var df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname); | |||
if(df && !df.hidden) { | |||
@@ -448,8 +448,8 @@ frappe.ui.form.Timeline = Class.extend({ | |||
var parts = [], count = 0; | |||
data.row_changed.every(function(row) { | |||
row[3].every(function(p) { | |||
var df = me.frm.fields_dict[row[0]] && | |||
frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, | |||
var df = me.frm.fields_dict[row[0]] && | |||
frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, | |||
p[0], me.frm.docname); | |||
if(df && !df.hidden) { | |||
@@ -73,7 +73,8 @@ | |||
</a> | |||
{% } %} | |||
{% if (data.communication_medium === "Email" && data.sender !== user_email) { %} | |||
{% if (data.communication_medium === "Email" | |||
&& data.sender !== frappe.session.user_email) { %} | |||
<a class="text-muted reply-link pull-right timeline-content-show" | |||
data-name="{%= data.name %}">{%= __("Reply") %}</a> | |||
{% } %} | |||
@@ -110,7 +111,7 @@ | |||
{% $.each(data.attachments, function(i, a) { %} | |||
<div class="ellipsis"> | |||
<a href="{%= encodeURI(a.file_url).replace(/#/g, \'%23\') %}" | |||
class="text-muted small" target="_blank"> | |||
class="text-muted small" target="_blank" rel="noopener noreferrer"> | |||
<i class="fa fa-paperclip"></i> | |||
{%= a.file_url.split("/").slice(-1)[0] %} | |||
{% if (a.is_private) { %} | |||
@@ -53,6 +53,17 @@ frappe.form.formatters = { | |||
Currency: function(value, docfield, options, doc) { | |||
var currency = frappe.meta.get_field_currency(docfield, doc); | |||
var precision = docfield.precision || cint(frappe.boot.sysdefaults.currency_precision) || 2; | |||
if (precision > 2) { | |||
let parts = cstr(value).split('.'); | |||
let decimals = parts.length > 1 ? parts[1] : ''; | |||
if (decimals.length < 3) { | |||
// min precision 2 | |||
precision = 2; | |||
} else if (decimals.length < precision) { | |||
// or min decimals | |||
precision = decimals.length; | |||
} | |||
} | |||
return frappe.form.formatters._right((value==null || value==="") | |||
? "" : format_currency(value, currency, precision), options); | |||
}, | |||
@@ -108,6 +119,16 @@ frappe.form.formatters = { | |||
return value || ""; | |||
}, | |||
DateRange: function(value) { | |||
if($.isArray(value)) { | |||
return __("{0} to {1}").format([ | |||
frappe.datetime.str_to_user(value[0]), | |||
frappe.datetime.str_to_user(value[1]) | |||
]); | |||
} else { | |||
return value || ""; | |||
} | |||
}, | |||
Datetime: function(value) { | |||
if(value) { | |||
var m = moment(frappe.datetime.convert_to_user_tz(value)); | |||
@@ -108,6 +108,11 @@ frappe.ui.form.Grid = Class.extend({ | |||
select_row: function(name) { | |||
this.grid_rows_by_docname[name].select(); | |||
}, | |||
remove_all: function() { | |||
this.grid_rows.forEach(row => { | |||
row.remove(); | |||
}); | |||
}, | |||
refresh_remove_rows_button: function() { | |||
this.remove_rows_button.toggleClass('hide', | |||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); | |||
@@ -257,7 +262,7 @@ frappe.ui.form.Grid = Class.extend({ | |||
if (this.frm && this.frm.docname) { | |||
// use doc specific docfield object | |||
this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname, | |||
this.frm.docname); | |||
this.frm.docname); | |||
} else { | |||
// use non-doc specific docfield | |||
if(this.df.options) { | |||
@@ -360,8 +365,19 @@ frappe.ui.form.Grid = Class.extend({ | |||
get_docfield: function(fieldname) { | |||
return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null); | |||
}, | |||
get_grid_row: function(docname) { | |||
return this.grid_rows_by_docname[docname]; | |||
get_row: function(key) { | |||
if(typeof key == 'number') { | |||
if(key < 0) { | |||
return this.grid_rows[this.grid_rows.length + key]; | |||
} else { | |||
return this.grid_rows[key]; | |||
} | |||
} else { | |||
return this.grid_rows_by_docname[key]; | |||
} | |||
}, | |||
get_grid_row: function(key) { | |||
return this.get_row(key); | |||
}, | |||
get_field: function(fieldname) { | |||
// Note: workaround for get_query | |||
@@ -435,21 +451,21 @@ frappe.ui.form.Grid = Class.extend({ | |||
&& (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm) | |||
&& !in_list(frappe.model.layout_fields, df.fieldtype)) { | |||
if(df.columns) { | |||
df.colsize=df.columns; | |||
} | |||
else { | |||
var colsize=2; | |||
switch(df.fieldtype){ | |||
case"Text": | |||
case"Small Text": | |||
colsize=3; | |||
break; | |||
case"Check": | |||
colsize=1 | |||
} | |||
df.colsize=colsize | |||
if(df.columns) { | |||
df.colsize=df.columns; | |||
} | |||
else { | |||
var colsize=2; | |||
switch(df.fieldtype) { | |||
case"Text": | |||
case"Small Text": | |||
colsize=3; | |||
break; | |||
case"Check": | |||
colsize=1 | |||
} | |||
df.colsize=colsize; | |||
} | |||
if(df.columns) { | |||
df.colsize=df.columns; | |||
@@ -641,673 +657,4 @@ frappe.ui.form.Grid = Class.extend({ | |||
// hide all custom buttons | |||
this.grid_buttons.find('.btn-custom').addClass('hidden'); | |||
} | |||
}); | |||
frappe.ui.form.GridRow = Class.extend({ | |||
init: function(opts) { | |||
this.on_grid_fields_dict = {}; | |||
this.on_grid_fields = []; | |||
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">'; | |||
this.columns = {}; | |||
this.columns_list = []; | |||
$.extend(this, opts); | |||
this.make(); | |||
}, | |||
make: function() { | |||
var me = this; | |||
this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this); | |||
this.row = $('<div class="data-row row"></div>').appendTo(this.wrapper) | |||
.on("click", function(e) { | |||
if($(e.target).hasClass('grid-row-check') || $(e.target).hasClass('row-index') || $(e.target).parent().hasClass('row-index')) { | |||
return; | |||
} | |||
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) { | |||
// pass | |||
} else { | |||
me.toggle_view(); | |||
return false; | |||
} | |||
}); | |||
// no checkboxes if too small | |||
// if(this.is_too_small()) { | |||
// this.row_check_html = ''; | |||
// } | |||
if(this.grid.template && !this.grid.meta.editable_grid) { | |||
this.render_template(); | |||
} else { | |||
this.render_row(); | |||
} | |||
if(this.doc) { | |||
this.set_data(); | |||
} | |||
}, | |||
set_data: function() { | |||
this.wrapper.data({ | |||
"doc": this.doc | |||
}) | |||
}, | |||
set_row_index: function() { | |||
if(this.doc) { | |||
this.wrapper | |||
.attr('data-name', this.doc.name) | |||
.attr("data-idx", this.doc.idx) | |||
.find(".row-index span, .grid-form-row-index").html(this.doc.idx) | |||
} | |||
}, | |||
select: function(checked) { | |||
this.doc.__checked = checked ? 1 : 0; | |||
}, | |||
refresh_check: function() { | |||
this.wrapper.find('.grid-row-check').prop('checked', this.doc ? !!this.doc.__checked : false); | |||
this.grid.refresh_remove_rows_button(); | |||
}, | |||
remove: function() { | |||
var me = this; | |||
if(this.grid.is_editable()) { | |||
if(this.frm) { | |||
if(this.get_open_form()) { | |||
this.hide_form(); | |||
} | |||
this.frm.script_manager.trigger("before_" + this.grid.df.fieldname + "_remove", | |||
this.doc.doctype, this.doc.name); | |||
//this.wrapper.toggle(false); | |||
frappe.model.clear_doc(this.doc.doctype, this.doc.name); | |||
this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove", | |||
this.doc.doctype, this.doc.name); | |||
this.frm.dirty(); | |||
} else { | |||
this.grid.df.data = this.grid.df.data.filter(function(d) { | |||
return d.name !== me.doc.name; | |||
}) | |||
// remap idxs | |||
this.grid.df.data.forEach(function(d, i) { | |||
d.idx = i+1; | |||
}); | |||
} | |||
this.grid.refresh(); | |||
} | |||
}, | |||
insert: function(show, below) { | |||
var idx = this.doc.idx; | |||
if(below) idx ++; | |||
this.toggle_view(false); | |||
this.grid.add_new_row(idx, null, show); | |||
}, | |||
refresh: function() { | |||
if(this.frm && this.doc) { | |||
this.doc = locals[this.doc.doctype][this.doc.name]; | |||
} | |||
// re write columns | |||
this.visible_columns = null; | |||
if(this.grid.template && !this.grid.meta.editable_grid) { | |||
this.render_template(); | |||
} else { | |||
this.render_row(true); | |||
} | |||
// refersh form fields | |||
if(this.grid_form) { | |||
this.grid_form.layout && this.grid_form.layout.refresh(this.doc); | |||
} | |||
}, | |||
render_template: function() { | |||
this.set_row_index(); | |||
if(this.row_display) { | |||
this.row_display.remove(); | |||
} | |||
var index_html = ''; | |||
// row index | |||
if(this.doc) { | |||
if(!this.row_index) { | |||
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \ | |||
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row); | |||
} | |||
this.row_index.find('span').html(this.doc.idx); | |||
} | |||
this.row_display = $('<div class="row-data sortable-handle template-row">'+ | |||
+'</div>').appendTo(this.row) | |||
.html(frappe.render(this.grid.template, { | |||
doc: this.doc ? frappe.get_format_helper(this.doc) : null, | |||
frm: this.frm, | |||
row: this | |||
})); | |||
}, | |||
render_row: function(refresh) { | |||
var me = this; | |||
this.set_row_index(); | |||
// index (1, 2, 3 etc) | |||
if(!this.row_index) { | |||
var txt = (this.doc ? this.doc.idx : " "); | |||
this.row_index = $( | |||
`<div class="row-index sortable-handle col col-xs-1"> | |||
${this.row_check_html} | |||
<span>${txt}</span></div>`) | |||
.appendTo(this.row) | |||
.on('click', function(e) { | |||
if(!$(e.target).hasClass('grid-row-check')) { | |||
me.toggle_view(); | |||
} | |||
}); | |||
} else { | |||
this.row_index.find('span').html(txt); | |||
} | |||
this.setup_columns(); | |||
this.add_open_form_button(); | |||
this.refresh_check(); | |||
if(this.frm && this.doc) { | |||
$(this.frm.wrapper).trigger("grid-row-render", [this]); | |||
} | |||
}, | |||
make_editable: function() { | |||
this.row.toggleClass('editable-row', this.grid.is_editable()); | |||
}, | |||
is_too_small: function() { | |||
return this.row.width() ? this.row.width() < 300 : false; | |||
}, | |||
add_open_form_button: function() { | |||
var me = this; | |||
if(this.doc && !this.grid.df.in_place_edit) { | |||
// remove row | |||
if(!this.open_form_button) { | |||
this.open_form_button = $('<a class="close btn-open-row">\ | |||
<span class="octicon octicon-triangle-down"></span></a>') | |||
.appendTo($('<div class="col col-xs-1 sortable-handle"></div>').appendTo(this.row)) | |||
.on('click', function() { me.toggle_view(); return false; }); | |||
if(this.is_too_small()) { | |||
// narrow | |||
this.open_form_button.css({'margin-right': '-2px'}); | |||
} | |||
} | |||
} | |||
}, | |||
setup_columns: function() { | |||
var me = this; | |||
this.focus_set = false; | |||
this.grid.setup_visible_columns(); | |||
for(var ci in this.grid.visible_columns) { | |||
var df = this.grid.visible_columns[ci][0], | |||
colsize = this.grid.visible_columns[ci][1], | |||
txt = this.doc ? | |||
frappe.format(this.doc[df.fieldname], df, null, this.doc) : | |||
__(df.label); | |||
if(this.doc && df.fieldtype === "Select") { | |||
txt = __(txt); | |||
} | |||
if(!this.columns[df.fieldname]) { | |||
var column = this.make_column(df, colsize, txt, ci); | |||
} else { | |||
var column = this.columns[df.fieldname]; | |||
this.refresh_field(df.fieldname, txt); | |||
} | |||
// background color for cellz | |||
if(this.doc) { | |||
if(df.reqd && !txt) { | |||
column.addClass('error'); | |||
} | |||
if (df.reqd || df.bold) { | |||
column.addClass('bold'); | |||
} | |||
} | |||
} | |||
}, | |||
make_column: function(df, colsize, txt, ci) { | |||
var me = this; | |||
var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ? | |||
" grid-overflow-no-ellipsis" : ""); | |||
add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ? | |||
" text-right": ""; | |||
add_class += (["Check"].indexOf(df.fieldtype)!==-1) ? | |||
" text-center": ""; | |||
var $col = $('<div class="col grid-static-col col-xs-'+colsize+' '+add_class+'"></div>') | |||
.attr("data-fieldname", df.fieldname) | |||
.attr("data-fieldtype", df.fieldtype) | |||
.data("df", df) | |||
.appendTo(this.row) | |||
.on('click', function() { | |||
if(frappe.ui.form.editable_row===me) { | |||
return; | |||
} | |||
var out = me.toggle_editable_row(); | |||
var col = this; | |||
setTimeout(function() { | |||
$(col).find('input[type="Text"]:first').focus(); | |||
}, 500); | |||
return out; | |||
}); | |||
$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false); | |||
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt); | |||
$col.df = df; | |||
$col.column_index = ci; | |||
this.columns[df.fieldname] = $col; | |||
this.columns_list.push($col); | |||
return $col; | |||
}, | |||
toggle_editable_row: function(show) { | |||
var me = this; | |||
// show static for field based on | |||
// whether grid is editable | |||
if(this.grid.allow_on_grid_editing() && this.grid.is_editable() && this.doc && show !== false) { | |||
// disable other editale row | |||
if(frappe.ui.form.editable_row | |||
&& frappe.ui.form.editable_row !== this) { | |||
frappe.ui.form.editable_row.toggle_editable_row(false); | |||
} | |||
this.row.toggleClass('editable-row', true); | |||
// setup controls | |||
this.columns_list.forEach(function(column) { | |||
me.make_control(column); | |||
column.static_area.toggle(false); | |||
column.field_area.toggle(true); | |||
}); | |||
frappe.ui.form.editable_row = this; | |||
return false; | |||
} else { | |||
this.row.toggleClass('editable-row', false); | |||
this.columns_list.forEach(function(column) { | |||
column.static_area.toggle(true); | |||
column.field_area && column.field_area.toggle(false); | |||
}); | |||
frappe.ui.form.editable_row = null; | |||
} | |||
}, | |||
make_control: function(column) { | |||
if(column.field) return; | |||
var me = this, | |||
parent = column.field_area, | |||
df = column.df; | |||
// no text editor in grid | |||
if (df.fieldtype=='Text Editor') { | |||
df.fieldtype = 'Text'; | |||
} | |||
var field = frappe.ui.form.make_control({ | |||
df: df, | |||
parent: parent, | |||
only_input: true, | |||
with_link_btn: true, | |||
doc: this.doc, | |||
doctype: this.doc.doctype, | |||
docname: this.doc.name, | |||
frm: this.grid.frm, | |||
grid: this.grid, | |||
grid_row: this, | |||
value: this.doc[df.fieldname] | |||
}); | |||
// sync get_query | |||
field.get_query = this.grid.get_field(df.fieldname).get_query; | |||
field.refresh(); | |||
if(field.$input) { | |||
field.$input | |||
.addClass('input-sm') | |||
.attr('data-col-idx', column.column_index) | |||
.attr('placeholder', __(df.label)); | |||
// flag list input | |||
if (this.columns_list && this.columns_list.slice(-1)[0]===column) { | |||
field.$input.attr('data-last-input', 1); | |||
} | |||
} | |||
this.set_arrow_keys(field); | |||
column.field = field; | |||
this.on_grid_fields_dict[df.fieldname] = field; | |||
this.on_grid_fields.push(field); | |||
}, | |||
set_arrow_keys: function(field) { | |||
var me = this; | |||
if(field.$input) { | |||
field.$input.on('keydown', function(e) { | |||
var { TAB, UP_ARROW, DOWN_ARROW } = frappe.ui.keyCode; | |||
if(!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) { | |||
return; | |||
} | |||
var values = me.grid.get_data(); | |||
var fieldname = $(this).attr('data-fieldname'); | |||
var fieldtype = $(this).attr('data-fieldtype'); | |||
var move_up_down = function(base) { | |||
if(in_list(['Text', 'Small Text'], fieldtype)) { | |||
return; | |||
} | |||
base.toggle_editable_row(); | |||
setTimeout(function() { | |||
var input = base.columns[fieldname].field.$input; | |||
if(input) { | |||
input.focus(); | |||
} | |||
}, 400) | |||
} | |||
// TAB | |||
if(e.which==TAB && !e.shiftKey) { | |||
// last column | |||
if($(this).attr('data-last-input') || | |||
me.grid.wrapper.find('.grid-row :input:enabled:last').get(0)===this) { | |||
setTimeout(function() { | |||
if(me.doc.idx === values.length) { | |||
// last row | |||
me.grid.add_new_row(null, null, true); | |||
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row(); | |||
me.grid.set_focus_on_row(); | |||
} else { | |||
me.grid.grid_rows[me.doc.idx].toggle_editable_row(); | |||
me.grid.set_focus_on_row(me.doc.idx+1); | |||
} | |||
}, 500); | |||
} | |||
} else if(e.which==UP_ARROW) { | |||
if(me.doc.idx > 1) { | |||
var prev = me.grid.grid_rows[me.doc.idx-2]; | |||
move_up_down(prev); | |||
} | |||
} else if(e.which==DOWN_ARROW) { | |||
if(me.doc.idx < values.length) { | |||
var next = me.grid.grid_rows[me.doc.idx]; | |||
move_up_down(next); | |||
} | |||
} | |||
}); | |||
} | |||
}, | |||
get_open_form: function() { | |||
return frappe.ui.form.get_open_grid_form(); | |||
}, | |||
toggle_view: function(show, callback) { | |||
if(!this.doc) { | |||
return this; | |||
} | |||
if(this.frm) { | |||
// reload doc | |||
this.doc = locals[this.doc.doctype][this.doc.name]; | |||
} | |||
// hide other | |||
var open_row = this.get_open_form(); | |||
if (show===undefined) show = !!!open_row; | |||
// call blur | |||
document.activeElement && document.activeElement.blur(); | |||
if(show && open_row) { | |||
if(open_row==this) { | |||
// already open, do nothing | |||
callback && callback(); | |||
return; | |||
} else { | |||
// close other views | |||
open_row.toggle_view(false); | |||
} | |||
} | |||
if(show) { | |||
this.show_form(); | |||
} else { | |||
this.hide_form(); | |||
} | |||
callback && callback(); | |||
return this; | |||
}, | |||
show_form: function() { | |||
if(!this.grid_form) { | |||
this.grid_form = new frappe.ui.form.GridRowForm({ | |||
row: this | |||
}); | |||
} | |||
this.grid_form.render(); | |||
this.row.toggle(false); | |||
// this.form_panel.toggle(true); | |||
frappe.dom.freeze("", "dark"); | |||
cur_frm.cur_grid = this; | |||
this.wrapper.addClass("grid-row-open"); | |||
if(!frappe.dom.is_element_in_viewport(this.wrapper)) { | |||
frappe.utils.scroll_to(this.wrapper, true, 15); | |||
} | |||
if(this.frm) { | |||
this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered"); | |||
this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name); | |||
} | |||
}, | |||
hide_form: function() { | |||
frappe.dom.unfreeze(); | |||
this.row.toggle(true); | |||
this.refresh(); | |||
cur_frm.cur_grid = null; | |||
this.wrapper.removeClass("grid-row-open"); | |||
}, | |||
open_prev: function() { | |||
if(this.grid.grid_rows[this.doc.idx-2]) { | |||
this.grid.grid_rows[this.doc.idx-2].toggle_view(true); | |||
} | |||
}, | |||
open_next: function() { | |||
if(this.grid.grid_rows[this.doc.idx]) { | |||
this.grid.grid_rows[this.doc.idx].toggle_view(true); | |||
} else { | |||
this.grid.add_new_row(null, null, true); | |||
} | |||
}, | |||
refresh_field: function(fieldname, txt) { | |||
var df = this.grid.get_docfield(fieldname) || undefined; | |||
// format values if no frm | |||
if(!df) { | |||
df = this.grid.visible_columns.find((col) => { | |||
return col[0].fieldname === fieldname; | |||
}); | |||
if(df && this.doc) { | |||
var txt = frappe.format(this.doc[fieldname], df[0], | |||
null, this.doc); | |||
} | |||
} | |||
if(txt===undefined && this.frm) { | |||
var txt = frappe.format(this.doc[fieldname], df, | |||
null, this.frm.doc); | |||
} | |||
// reset static value | |||
var column = this.columns[fieldname]; | |||
if(column) { | |||
column.static_area.html(txt || ""); | |||
if(df && df.reqd) { | |||
column.toggleClass('error', !!(txt===null || txt==='')); | |||
} | |||
} | |||
// reset field value | |||
var field = this.on_grid_fields_dict[fieldname]; | |||
if(field) { | |||
field.docname = this.doc.name; | |||
field.refresh(); | |||
} | |||
// in form | |||
if(this.grid_form) { | |||
this.grid_form.refresh_field(fieldname); | |||
} | |||
}, | |||
get_visible_columns: function(blacklist) { | |||
var me = this; | |||
var visible_columns = $.map(this.docfields, function(df) { | |||
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read") | |||
&& !in_list(frappe.model.layout_fields, df.fieldtype) && !in_list(blacklist, df.fieldname); | |||
return visible ? df : null; | |||
}); | |||
return visible_columns; | |||
}, | |||
set_field_property: function(fieldname, property, value) { | |||
// set a field property for open form / grid form | |||
var me = this; | |||
var set_property = function(field) { | |||
if(!field) return; | |||
field.df[property] = value; | |||
field.refresh(); | |||
} | |||
// set property in grid form | |||
if(this.grid_form) { | |||
set_property(this.grid_form.fields_dict[fieldname]); | |||
this.grid_form.layout && this.grid_form.layout.refresh_sections(); | |||
} | |||
// set property in on grid fields | |||
set_property(this.on_grid_fields_dict[fieldname]); | |||
}, | |||
toggle_reqd: function(fieldname, reqd) { | |||
this.set_field_property(fieldname, 'reqd', reqd ? 1 : 0); | |||
}, | |||
toggle_display: function(fieldname, show) { | |||
this.set_field_property(fieldname, 'hidden', show ? 0 : 1); | |||
}, | |||
toggle_editable: function(fieldname, editable) { | |||
this.set_field_property(fieldname, 'read_only', editable ? 0 : 1); | |||
}, | |||
}); | |||
frappe.ui.form.GridRowForm = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.wrapper = $('<div class="form-in-grid"></div>') | |||
.appendTo(this.row.wrapper); | |||
}, | |||
render: function() { | |||
var me = this; | |||
this.make_form(); | |||
this.form_area.empty(); | |||
this.layout = new frappe.ui.form.Layout({ | |||
fields: this.row.docfields, | |||
body: this.form_area, | |||
no_submit_on_enter: true, | |||
frm: this.row.frm, | |||
}); | |||
this.layout.make(); | |||
this.fields = this.layout.fields; | |||
this.fields_dict = this.layout.fields_dict; | |||
this.layout.refresh(this.row.doc); | |||
// copy get_query to fields | |||
for(var fieldname in (this.row.grid.fieldinfo || {})) { | |||
var fi = this.row.grid.fieldinfo[fieldname]; | |||
$.extend(me.fields_dict[fieldname], fi); | |||
} | |||
this.toggle_add_delete_button_display(this.wrapper); | |||
this.row.grid.open_grid_row = this; | |||
this.set_focus(); | |||
}, | |||
make_form: function() { | |||
if(!this.form_area) { | |||
$(frappe.render_template("grid_form", {grid:this})).appendTo(this.wrapper); | |||
this.form_area = this.wrapper.find(".form-area"); | |||
this.row.set_row_index(); | |||
this.set_form_events(); | |||
} | |||
}, | |||
set_form_events: function() { | |||
var me = this; | |||
this.wrapper.find(".grid-delete-row") | |||
.on('click', function() { | |||
me.row.remove(); return false; | |||
}); | |||
this.wrapper.find(".grid-insert-row") | |||
.on('click', function() { | |||
me.row.insert(true); return false; | |||
}); | |||
this.wrapper.find(".grid-insert-row-below") | |||
.on('click', function() { | |||
me.row.insert(true, true); return false; | |||
}); | |||
this.wrapper.find(".grid-append-row") | |||
.on('click', function() { | |||
me.row.toggle_view(false); | |||
me.row.grid.add_new_row(me.row.doc.idx+1, null, true); | |||
return false; | |||
}); | |||
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function() { | |||
me.row.toggle_view(); | |||
return false; | |||
}); | |||
}, | |||
toggle_add_delete_button_display: function($parent) { | |||
$parent.find(".grid-header-toolbar .btn, .grid-footer-toolbar .btn") | |||
.toggle(this.row.grid.is_editable()); | |||
}, | |||
refresh_field: function(fieldname) { | |||
if(this.fields_dict[fieldname]) { | |||
this.fields_dict[fieldname].refresh(); | |||
this.layout && this.layout.refresh_dependency(); | |||
} | |||
}, | |||
set_focus: function() { | |||
// wait for animation and then focus on the first row | |||
var me = this; | |||
setTimeout(function() { | |||
if(me.row.frm && me.row.frm.doc.docstatus===0 || !me.row.frm) { | |||
var first = me.form_area.find("input:first"); | |||
if(first.length && !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))) { | |||
try { | |||
first.get(0).focus(); | |||
} catch(e) { | |||
// | |||
} | |||
} | |||
} | |||
}, 500); | |||
}, | |||
}); | |||
}); |
@@ -0,0 +1,586 @@ | |||
frappe.ui.form.GridRow = Class.extend({ | |||
init: function(opts) { | |||
this.on_grid_fields_dict = {}; | |||
this.on_grid_fields = []; | |||
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">'; | |||
this.columns = {}; | |||
this.columns_list = []; | |||
$.extend(this, opts); | |||
this.make(); | |||
}, | |||
make: function() { | |||
var me = this; | |||
this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this); | |||
this.row = $('<div class="data-row row"></div>').appendTo(this.wrapper) | |||
.on("click", function(e) { | |||
if($(e.target).hasClass('grid-row-check') || $(e.target).hasClass('row-index') || $(e.target).parent().hasClass('row-index')) { | |||
return; | |||
} | |||
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) { | |||
// pass | |||
} else { | |||
me.toggle_view(); | |||
return false; | |||
} | |||
}); | |||
// no checkboxes if too small | |||
// if(this.is_too_small()) { | |||
// this.row_check_html = ''; | |||
// } | |||
if(this.grid.template && !this.grid.meta.editable_grid) { | |||
this.render_template(); | |||
} else { | |||
this.render_row(); | |||
} | |||
if(this.doc) { | |||
this.set_data(); | |||
} | |||
}, | |||
set_data: function() { | |||
this.wrapper.data({ | |||
"doc": this.doc | |||
}) | |||
}, | |||
set_row_index: function() { | |||
if(this.doc) { | |||
this.wrapper | |||
.attr('data-name', this.doc.name) | |||
.attr("data-idx", this.doc.idx) | |||
.find(".row-index span, .grid-form-row-index").html(this.doc.idx) | |||
} | |||
}, | |||
select: function(checked) { | |||
this.doc.__checked = checked ? 1 : 0; | |||
}, | |||
refresh_check: function() { | |||
this.wrapper.find('.grid-row-check').prop('checked', this.doc ? !!this.doc.__checked : false); | |||
this.grid.refresh_remove_rows_button(); | |||
}, | |||
remove: function() { | |||
var me = this; | |||
if(this.grid.is_editable()) { | |||
if(this.frm) { | |||
if(this.get_open_form()) { | |||
this.hide_form(); | |||
} | |||
this.frm.script_manager.trigger("before_" + this.grid.df.fieldname + "_remove", | |||
this.doc.doctype, this.doc.name); | |||
//this.wrapper.toggle(false); | |||
frappe.model.clear_doc(this.doc.doctype, this.doc.name); | |||
this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove", | |||
this.doc.doctype, this.doc.name); | |||
this.frm.dirty(); | |||
} else { | |||
this.grid.df.data = this.grid.df.data.filter(function(d) { | |||
return d.name !== me.doc.name; | |||
}) | |||
// remap idxs | |||
this.grid.df.data.forEach(function(d, i) { | |||
d.idx = i+1; | |||
}); | |||
} | |||
this.grid.refresh(); | |||
} | |||
}, | |||
insert: function(show, below) { | |||
var idx = this.doc.idx; | |||
if(below) idx ++; | |||
this.toggle_view(false); | |||
this.grid.add_new_row(idx, null, show); | |||
}, | |||
refresh: function() { | |||
if(this.frm && this.doc) { | |||
this.doc = locals[this.doc.doctype][this.doc.name]; | |||
} | |||
// re write columns | |||
this.visible_columns = null; | |||
if(this.grid.template && !this.grid.meta.editable_grid) { | |||
this.render_template(); | |||
} else { | |||
this.render_row(true); | |||
} | |||
// refersh form fields | |||
if(this.grid_form) { | |||
this.grid_form.layout && this.grid_form.layout.refresh(this.doc); | |||
} | |||
}, | |||
render_template: function() { | |||
this.set_row_index(); | |||
if(this.row_display) { | |||
this.row_display.remove(); | |||
} | |||
var index_html = ''; | |||
// row index | |||
if(this.doc) { | |||
if(!this.row_index) { | |||
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \ | |||
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row); | |||
} | |||
this.row_index.find('span').html(this.doc.idx); | |||
} | |||
this.row_display = $('<div class="row-data sortable-handle template-row">'+ | |||
+'</div>').appendTo(this.row) | |||
.html(frappe.render(this.grid.template, { | |||
doc: this.doc ? frappe.get_format_helper(this.doc) : null, | |||
frm: this.frm, | |||
row: this | |||
})); | |||
}, | |||
render_row: function(refresh) { | |||
var me = this; | |||
this.set_row_index(); | |||
// index (1, 2, 3 etc) | |||
if(!this.row_index) { | |||
var txt = (this.doc ? this.doc.idx : " "); | |||
this.row_index = $( | |||
`<div class="row-index sortable-handle col col-xs-1"> | |||
${this.row_check_html} | |||
<span>${txt}</span></div>`) | |||
.appendTo(this.row) | |||
.on('click', function(e) { | |||
if(!$(e.target).hasClass('grid-row-check')) { | |||
me.toggle_view(); | |||
} | |||
}); | |||
} else { | |||
this.row_index.find('span').html(txt); | |||
} | |||
this.setup_columns(); | |||
this.add_open_form_button(); | |||
this.refresh_check(); | |||
if(this.frm && this.doc) { | |||
$(this.frm.wrapper).trigger("grid-row-render", [this]); | |||
} | |||
}, | |||
make_editable: function() { | |||
this.row.toggleClass('editable-row', this.grid.is_editable()); | |||
}, | |||
is_too_small: function() { | |||
return this.row.width() ? this.row.width() < 300 : false; | |||
}, | |||
add_open_form_button: function() { | |||
var me = this; | |||
if(this.doc && !this.grid.df.in_place_edit) { | |||
// remove row | |||
if(!this.open_form_button) { | |||
this.open_form_button = $('<a class="close btn-open-row">\ | |||
<span class="octicon octicon-triangle-down"></span></a>') | |||
.appendTo($('<div class="col col-xs-1 sortable-handle"></div>').appendTo(this.row)) | |||
.on('click', function() { me.toggle_view(); return false; }); | |||
if(this.is_too_small()) { | |||
// narrow | |||
this.open_form_button.css({'margin-right': '-2px'}); | |||
} | |||
} | |||
} | |||
}, | |||
setup_columns: function() { | |||
var me = this; | |||
this.focus_set = false; | |||
this.grid.setup_visible_columns(); | |||
for(var ci in this.grid.visible_columns) { | |||
var df = this.grid.visible_columns[ci][0], | |||
colsize = this.grid.visible_columns[ci][1], | |||
txt = this.doc ? | |||
frappe.format(this.doc[df.fieldname], df, null, this.doc) : | |||
__(df.label); | |||
if(this.doc && df.fieldtype === "Select") { | |||
txt = __(txt); | |||
} | |||
if(!this.columns[df.fieldname]) { | |||
var column = this.make_column(df, colsize, txt, ci); | |||
} else { | |||
var column = this.columns[df.fieldname]; | |||
this.refresh_field(df.fieldname, txt); | |||
} | |||
// background color for cellz | |||
if(this.doc) { | |||
if(df.reqd && !txt) { | |||
column.addClass('error'); | |||
} | |||
if (df.reqd || df.bold) { | |||
column.addClass('bold'); | |||
} | |||
} | |||
} | |||
}, | |||
make_column: function(df, colsize, txt, ci) { | |||
var me = this; | |||
var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ? | |||
" grid-overflow-no-ellipsis" : ""); | |||
add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ? | |||
" text-right": ""; | |||
add_class += (["Check"].indexOf(df.fieldtype)!==-1) ? | |||
" text-center": ""; | |||
var $col = $('<div class="col grid-static-col col-xs-'+colsize+' '+add_class+'"></div>') | |||
.attr("data-fieldname", df.fieldname) | |||
.attr("data-fieldtype", df.fieldtype) | |||
.data("df", df) | |||
.appendTo(this.row) | |||
.on('click', function() { | |||
if(frappe.ui.form.editable_row===me) { | |||
return; | |||
} | |||
var out = me.toggle_editable_row(); | |||
var col = this; | |||
setTimeout(function() { | |||
$(col).find('input[type="Text"]:first').focus(); | |||
}, 500); | |||
return out; | |||
}); | |||
$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false); | |||
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt); | |||
$col.df = df; | |||
$col.column_index = ci; | |||
this.columns[df.fieldname] = $col; | |||
this.columns_list.push($col); | |||
return $col; | |||
}, | |||
activate: function() { | |||
this.toggle_editable_row(true); | |||
return this; | |||
}, | |||
toggle_editable_row: function(show) { | |||
var me = this; | |||
// show static for field based on | |||
// whether grid is editable | |||
if(this.grid.allow_on_grid_editing() && this.grid.is_editable() && this.doc && show !== false) { | |||
// disable other editale row | |||
if(frappe.ui.form.editable_row | |||
&& frappe.ui.form.editable_row !== this) { | |||
frappe.ui.form.editable_row.toggle_editable_row(false); | |||
} | |||
this.row.toggleClass('editable-row', true); | |||
// setup controls | |||
this.columns_list.forEach(function(column) { | |||
me.make_control(column); | |||
column.static_area.toggle(false); | |||
column.field_area.toggle(true); | |||
}); | |||
frappe.ui.form.editable_row = this; | |||
return false; | |||
} else { | |||
this.row.toggleClass('editable-row', false); | |||
this.columns_list.forEach(function(column) { | |||
column.static_area.toggle(true); | |||
column.field_area && column.field_area.toggle(false); | |||
}); | |||
frappe.ui.form.editable_row = null; | |||
} | |||
}, | |||
make_control: function(column) { | |||
if(column.field) return; | |||
var me = this, | |||
parent = column.field_area, | |||
df = column.df; | |||
// no text editor in grid | |||
if (df.fieldtype=='Text Editor') { | |||
df.fieldtype = 'Text'; | |||
} | |||
var field = frappe.ui.form.make_control({ | |||
df: df, | |||
parent: parent, | |||
only_input: true, | |||
with_link_btn: true, | |||
doc: this.doc, | |||
doctype: this.doc.doctype, | |||
docname: this.doc.name, | |||
frm: this.grid.frm, | |||
grid: this.grid, | |||
grid_row: this, | |||
value: this.doc[df.fieldname] | |||
}); | |||
// sync get_query | |||
field.get_query = this.grid.get_field(df.fieldname).get_query; | |||
field.refresh(); | |||
if(field.$input) { | |||
field.$input | |||
.addClass('input-sm') | |||
.attr('data-col-idx', column.column_index) | |||
.attr('placeholder', __(df.label)); | |||
// flag list input | |||
if (this.columns_list && this.columns_list.slice(-1)[0]===column) { | |||
field.$input.attr('data-last-input', 1); | |||
} | |||
} | |||
this.set_arrow_keys(field); | |||
column.field = field; | |||
this.on_grid_fields_dict[df.fieldname] = field; | |||
this.on_grid_fields.push(field); | |||
}, | |||
set_arrow_keys: function(field) { | |||
var me = this; | |||
if(field.$input) { | |||
field.$input.on('keydown', function(e) { | |||
var { TAB, UP_ARROW, DOWN_ARROW } = frappe.ui.keyCode; | |||
if(!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) { | |||
return; | |||
} | |||
var values = me.grid.get_data(); | |||
var fieldname = $(this).attr('data-fieldname'); | |||
var fieldtype = $(this).attr('data-fieldtype'); | |||
var move_up_down = function(base) { | |||
if(in_list(['Text', 'Small Text'], fieldtype)) { | |||
return; | |||
} | |||
base.toggle_editable_row(); | |||
setTimeout(function() { | |||
var input = base.columns[fieldname].field.$input; | |||
if(input) { | |||
input.focus(); | |||
} | |||
}, 400) | |||
} | |||
// TAB | |||
if(e.which==TAB && !e.shiftKey) { | |||
// last column | |||
if($(this).attr('data-last-input') || | |||
me.grid.wrapper.find('.grid-row :input:enabled:last').get(0)===this) { | |||
setTimeout(function() { | |||
if(me.doc.idx === values.length) { | |||
// last row | |||
me.grid.add_new_row(null, null, true); | |||
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row(); | |||
me.grid.set_focus_on_row(); | |||
} else { | |||
me.grid.grid_rows[me.doc.idx].toggle_editable_row(); | |||
me.grid.set_focus_on_row(me.doc.idx+1); | |||
} | |||
}, 500); | |||
} | |||
} else if(e.which==UP_ARROW) { | |||
if(me.doc.idx > 1) { | |||
var prev = me.grid.grid_rows[me.doc.idx-2]; | |||
move_up_down(prev); | |||
} | |||
} else if(e.which==DOWN_ARROW) { | |||
if(me.doc.idx < values.length) { | |||
var next = me.grid.grid_rows[me.doc.idx]; | |||
move_up_down(next); | |||
} | |||
} | |||
}); | |||
} | |||
}, | |||
get_open_form: function() { | |||
return frappe.ui.form.get_open_grid_form(); | |||
}, | |||
toggle_view: function(show, callback) { | |||
if(!this.doc) { | |||
return this; | |||
} | |||
if(this.frm) { | |||
// reload doc | |||
this.doc = locals[this.doc.doctype][this.doc.name]; | |||
} | |||
// hide other | |||
var open_row = this.get_open_form(); | |||
if (show===undefined) show = !!!open_row; | |||
// call blur | |||
document.activeElement && document.activeElement.blur(); | |||
if(show && open_row) { | |||
if(open_row==this) { | |||
// already open, do nothing | |||
callback && callback(); | |||
return; | |||
} else { | |||
// close other views | |||
open_row.toggle_view(false); | |||
} | |||
} | |||
if(show) { | |||
this.show_form(); | |||
} else { | |||
this.hide_form(); | |||
} | |||
callback && callback(); | |||
return this; | |||
}, | |||
show_form: function() { | |||
if(!this.grid_form) { | |||
this.grid_form = new frappe.ui.form.GridRowForm({ | |||
row: this | |||
}); | |||
} | |||
this.grid_form.render(); | |||
this.row.toggle(false); | |||
// this.form_panel.toggle(true); | |||
frappe.dom.freeze("", "dark"); | |||
cur_frm.cur_grid = this; | |||
this.wrapper.addClass("grid-row-open"); | |||
if(!frappe.dom.is_element_in_viewport(this.wrapper)) { | |||
frappe.utils.scroll_to(this.wrapper, true, 15); | |||
} | |||
if(this.frm) { | |||
this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered"); | |||
this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name); | |||
} | |||
}, | |||
hide_form: function() { | |||
frappe.dom.unfreeze(); | |||
this.row.toggle(true); | |||
this.refresh(); | |||
cur_frm.cur_grid = null; | |||
this.wrapper.removeClass("grid-row-open"); | |||
}, | |||
open_prev: function() { | |||
if(this.grid.grid_rows[this.doc.idx-2]) { | |||
this.grid.grid_rows[this.doc.idx-2].toggle_view(true); | |||
} | |||
}, | |||
open_next: function() { | |||
if(this.grid.grid_rows[this.doc.idx]) { | |||
this.grid.grid_rows[this.doc.idx].toggle_view(true); | |||
} else { | |||
this.grid.add_new_row(null, null, true); | |||
} | |||
}, | |||
refresh_field: function(fieldname, txt) { | |||
var df = this.grid.get_docfield(fieldname) || undefined; | |||
// format values if no frm | |||
if(!df) { | |||
df = this.grid.visible_columns.find((col) => { | |||
return col[0].fieldname === fieldname; | |||
}); | |||
if(df && this.doc) { | |||
var txt = frappe.format(this.doc[fieldname], df[0], | |||
null, this.doc); | |||
} | |||
} | |||
if(txt===undefined && this.frm) { | |||
var txt = frappe.format(this.doc[fieldname], df, | |||
null, this.frm.doc); | |||
} | |||
// reset static value | |||
var column = this.columns[fieldname]; | |||
if(column) { | |||
column.static_area.html(txt || ""); | |||
if(df && df.reqd) { | |||
column.toggleClass('error', !!(txt===null || txt==='')); | |||
} | |||
} | |||
// reset field value | |||
var field = this.on_grid_fields_dict[fieldname]; | |||
if(field) { | |||
field.docname = this.doc.name; | |||
field.refresh(); | |||
} | |||
// in form | |||
if(this.grid_form) { | |||
this.grid_form.refresh_field(fieldname); | |||
} | |||
}, | |||
get_field: function(fieldname) { | |||
let field = this.on_grid_fields_dict[fieldname]; | |||
if (field) { | |||
return field; | |||
} else if(this.grid_form) { | |||
return this.grid_form.fields_dict[fieldname]; | |||
} else { | |||
throw `fieldname ${fieldname} not found`; | |||
} | |||
}, | |||
get_visible_columns: function(blacklist) { | |||
var me = this; | |||
var visible_columns = $.map(this.docfields, function(df) { | |||
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read") | |||
&& !in_list(frappe.model.layout_fields, df.fieldtype) && !in_list(blacklist, df.fieldname); | |||
return visible ? df : null; | |||
}); | |||
return visible_columns; | |||
}, | |||
set_field_property: function(fieldname, property, value) { | |||
// set a field property for open form / grid form | |||
var me = this; | |||
var set_property = function(field) { | |||
if(!field) return; | |||
field.df[property] = value; | |||
field.refresh(); | |||
} | |||
// set property in grid form | |||
if(this.grid_form) { | |||
set_property(this.grid_form.fields_dict[fieldname]); | |||
this.grid_form.layout && this.grid_form.layout.refresh_sections(); | |||
} | |||
// set property in on grid fields | |||
set_property(this.on_grid_fields_dict[fieldname]); | |||
}, | |||
toggle_reqd: function(fieldname, reqd) { | |||
this.set_field_property(fieldname, 'reqd', reqd ? 1 : 0); | |||
}, | |||
toggle_display: function(fieldname, show) { | |||
this.set_field_property(fieldname, 'hidden', show ? 0 : 1); | |||
}, | |||
toggle_editable: function(fieldname, editable) { | |||
this.set_field_property(fieldname, 'read_only', editable ? 0 : 1); | |||
}, | |||
}); |
@@ -0,0 +1,97 @@ | |||
frappe.ui.form.GridRowForm = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.wrapper = $('<div class="form-in-grid"></div>') | |||
.appendTo(this.row.wrapper); | |||
}, | |||
render: function() { | |||
var me = this; | |||
this.make_form(); | |||
this.form_area.empty(); | |||
this.layout = new frappe.ui.form.Layout({ | |||
fields: this.row.docfields, | |||
body: this.form_area, | |||
no_submit_on_enter: true, | |||
frm: this.row.frm, | |||
}); | |||
this.layout.make(); | |||
this.fields = this.layout.fields; | |||
this.fields_dict = this.layout.fields_dict; | |||
this.layout.refresh(this.row.doc); | |||
// copy get_query to fields | |||
for(var fieldname in (this.row.grid.fieldinfo || {})) { | |||
var fi = this.row.grid.fieldinfo[fieldname]; | |||
$.extend(me.fields_dict[fieldname], fi); | |||
} | |||
this.toggle_add_delete_button_display(this.wrapper); | |||
this.row.grid.open_grid_row = this; | |||
this.set_focus(); | |||
}, | |||
make_form: function() { | |||
if(!this.form_area) { | |||
$(frappe.render_template("grid_form", {grid:this})).appendTo(this.wrapper); | |||
this.form_area = this.wrapper.find(".form-area"); | |||
this.row.set_row_index(); | |||
this.set_form_events(); | |||
} | |||
}, | |||
set_form_events: function() { | |||
var me = this; | |||
this.wrapper.find(".grid-delete-row") | |||
.on('click', function() { | |||
me.row.remove(); return false; | |||
}); | |||
this.wrapper.find(".grid-insert-row") | |||
.on('click', function() { | |||
me.row.insert(true); return false; | |||
}); | |||
this.wrapper.find(".grid-insert-row-below") | |||
.on('click', function() { | |||
me.row.insert(true, true); return false; | |||
}); | |||
this.wrapper.find(".grid-append-row") | |||
.on('click', function() { | |||
me.row.toggle_view(false); | |||
me.row.grid.add_new_row(me.row.doc.idx+1, null, true); | |||
return false; | |||
}); | |||
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function() { | |||
me.row.toggle_view(); | |||
return false; | |||
}); | |||
}, | |||
toggle_add_delete_button_display: function($parent) { | |||
$parent.find(".grid-header-toolbar .btn, .grid-footer-toolbar .btn") | |||
.toggle(this.row.grid.is_editable()); | |||
}, | |||
refresh_field: function(fieldname) { | |||
if(this.fields_dict[fieldname]) { | |||
this.fields_dict[fieldname].refresh(); | |||
this.layout && this.layout.refresh_dependency(); | |||
} | |||
}, | |||
set_focus: function() { | |||
// wait for animation and then focus on the first row | |||
var me = this; | |||
setTimeout(function() { | |||
if(me.row.frm && me.row.frm.doc.docstatus===0 || !me.row.frm) { | |||
var first = me.form_area.find("input:first"); | |||
if(first.length && !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))) { | |||
try { | |||
first.get(0).focus(); | |||
} catch(e) { | |||
// | |||
} | |||
} | |||
} | |||
}, 500); | |||
}, | |||
}); |
@@ -19,12 +19,14 @@ frappe.ui.form.Layout = Class.extend({ | |||
$.extend(this, opts); | |||
}, | |||
make: function() { | |||
if(!this.parent && this.body) | |||
if(!this.parent && this.body) { | |||
this.parent = this.body; | |||
} | |||
this.wrapper = $('<div class="form-layout">').appendTo(this.parent); | |||
this.message = $('<div class="form-message text-muted small hidden"></div>').appendTo(this.wrapper); | |||
if(!this.fields) | |||
if(!this.fields) { | |||
this.fields = frappe.meta.sort_docfields(frappe.meta.docfield_map[this.doctype]); | |||
} | |||
this.setup_tabbing(); | |||
this.render(); | |||
}, | |||
@@ -44,29 +46,58 @@ 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 (this.with_dashboard) { | |||
this.setup_dashboard_section(); | |||
} | |||
if (this.no_opening_section()) { | |||
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) { | |||
no_opening_section: function() { | |||
return (this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length; | |||
}, | |||
setup_dashboard_section: function() { | |||
if (this.no_opening_section()) { | |||
this.fields.unshift({fieldtype: 'Section Break'}); | |||
} | |||
this.fields.unshift({ | |||
fieldtype: 'Section Break', | |||
fieldname: '_form_dashboard', | |||
label: __('Dashboard'), | |||
cssClass: 'form-dashboard', | |||
collapsible: 1, | |||
//hidden: 1 | |||
}); | |||
}, | |||
make_field: function(df, colspan, render = false) { | |||
!this.section && this.make_section(); | |||
!this.column && this.make_column(); | |||
@@ -74,7 +105,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; | |||
@@ -171,13 +203,14 @@ frappe.ui.form.Layout = Class.extend({ | |||
var $this = $(this).removeClass("empty-section") | |||
.removeClass("visible-section") | |||
.removeClass("shaded-section"); | |||
if(!$(this).find(".frappe-control:not(.hide-control)").length) { | |||
if(!$this.find(".frappe-control:not(.hide-control)").length | |||
&& !$this.hasClass('form-dashboard')) { | |||
// nothing visible, hide the section | |||
$(this).addClass("empty-section"); | |||
$this.addClass("empty-section"); | |||
} else { | |||
$(this).addClass("visible-section"); | |||
$this.addClass("visible-section"); | |||
if(cnt % 2) { | |||
$(this).addClass("shaded-section"); | |||
$this.addClass("shaded-section"); | |||
} | |||
cnt ++; | |||
} | |||
@@ -201,6 +234,10 @@ frappe.ui.form.Layout = Class.extend({ | |||
collapse = false; | |||
} | |||
if(df.fieldname === '_form_dashboard') { | |||
collapse = false; | |||
} | |||
section.collapse(collapse); | |||
} | |||
} | |||
@@ -226,6 +263,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); | |||
@@ -241,7 +290,7 @@ frappe.ui.form.Layout = Class.extend({ | |||
if(doctype) | |||
return me.handle_tab(doctype, fieldname, ev.shiftKey); | |||
} | |||
}) | |||
}); | |||
}, | |||
handle_tab: function(doctype, fieldname, shift) { | |||
var me = this, | |||
@@ -264,7 +313,7 @@ frappe.ui.form.Layout = Class.extend({ | |||
if(fields[i].df.fieldname==fieldname) { | |||
if(shift) { | |||
if(prev) { | |||
this.set_focus(prev) | |||
this.set_focus(prev); | |||
} else { | |||
$(this.primary_button).focus(); | |||
} | |||
@@ -290,7 +339,7 @@ frappe.ui.form.Layout = Class.extend({ | |||
// last row, close it and find next field | |||
grid_row.toggle_view(false, function() { | |||
grid_row.grid.frm.layout.handle_tab(grid_row.grid.df.parent, grid_row.grid.df.fieldname); | |||
}) | |||
}); | |||
} else { | |||
// next row | |||
grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true); | |||
@@ -325,7 +374,7 @@ frappe.ui.form.Layout = Class.extend({ | |||
} | |||
}, | |||
is_visible: function(field) { | |||
return field.disp_status==="Write" && (field.$wrapper && field.$wrapper.is(":visible")) | |||
return field.disp_status==="Write" && (field.$wrapper && field.$wrapper.is(":visible")); | |||
}, | |||
set_focus: function(field) { | |||
// next is table, show the table | |||
@@ -450,17 +499,20 @@ frappe.ui.form.Section = Class.extend({ | |||
.appendTo(this.layout.page); | |||
this.layout.sections.push(this); | |||
var section = this.wrapper[0]; | |||
if(this.df) { | |||
if(this.df.label) { | |||
this.make_head(); | |||
} | |||
if(this.df.description) { | |||
$('<div class="col-sm-12 small text-muted form-section-description">' + __(this.df.description) + '</div>') | |||
.appendTo(this.wrapper); | |||
.appendTo(this.wrapper); | |||
} | |||
if(this.df.cssClass) { | |||
this.wrapper.addClass(this.df.cssClass); | |||
} | |||
} | |||
// for bc | |||
this.body = $('<div class="section-body">').appendTo(this.wrapper); | |||
}, | |||
@@ -469,7 +521,7 @@ frappe.ui.form.Section = Class.extend({ | |||
if(!this.df.collapsible) { | |||
$('<div class="col-sm-12"><h6 class="form-section-heading uppercase">' | |||
+ __(this.df.label) + '</h6></div>') | |||
.appendTo(this.wrapper); | |||
.appendTo(this.wrapper); | |||
} else { | |||
this.head = $('<div class="section-head"><a class="h6 uppercase">' | |||
+__(this.df.label)+'</a><span class="octicon octicon-chevron-down collapse-indicator"></span></div>').appendTo(this.wrapper); | |||
@@ -521,7 +573,7 @@ frappe.ui.form.Section = Class.extend({ | |||
} | |||
return missing_mandatory; | |||
} | |||
}) | |||
}); | |||
frappe.ui.form.Column = Class.extend({ | |||
init: function(section, df) { | |||
@@ -538,7 +590,7 @@ frappe.ui.form.Column = Class.extend({ | |||
</form>\ | |||
</div>').appendTo(this.section.body) | |||
.find("form") | |||
.on("submit", function() { return false; }) | |||
.on("submit", function() { return false; }); | |||
if(this.df.label) { | |||
$('<label class="control-label">'+ __(this.df.label) | |||
@@ -557,4 +609,4 @@ frappe.ui.form.Column = Class.extend({ | |||
refresh: function() { | |||
this.section.refresh(); | |||
} | |||
}) | |||
}); |
@@ -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); | |||
} 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); | |||
@@ -176,6 +176,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) { | |||
console.log("Already saving. Please wait a few moments.") | |||
throw "saving"; | |||
} | |||
frappe.ui.form.remove_old_form_route(); | |||
frappe.ui.form.is_saving = true; | |||
return frappe.call({ | |||
@@ -206,7 +208,18 @@ frappe.ui.form.save = function (frm, action, callback, btn) { | |||
} | |||
} | |||
frappe.ui.form.update_calling_link = function (newdoc) { | |||
frappe.ui.form.remove_old_form_route = () => { | |||
let index = -1; | |||
let current_route = frappe.get_route(); | |||
frappe.route_history.map((arr, i) => { | |||
if (arr.join("/") === current_route.join("/")) { | |||
index = i; | |||
} | |||
}); | |||
frappe.route_history.splice(index, 1); | |||
} | |||
frappe.ui.form.update_calling_link = (newdoc) => { | |||
if (frappe._from_link && newdoc.doctype === frappe._from_link.df.options) { | |||
var doc = frappe.get_doc(frappe._from_link.doctype, frappe._from_link.docname); | |||
// set value | |||
@@ -226,8 +239,11 @@ frappe.ui.form.update_calling_link = function (newdoc) { | |||
// if from form, switch | |||
if (frappe._from_link.frm) { | |||
frappe.set_route("Form", frappe._from_link.frm.doctype, frappe._from_link.frm.docname); | |||
setTimeout(function () { frappe.utils.scroll_to(frappe._from_link_scrollY); }, 100); | |||
frappe.set_route("Form", | |||
frappe._from_link.frm.doctype, frappe._from_link.frm.docname) | |||
.then(() => { | |||
frappe.utils.scroll_to(frappe._from_link_scrollY); | |||
}); | |||
} | |||
frappe._from_link = null; | |||
@@ -55,8 +55,8 @@ frappe.ui.form.off = function(doctype, fieldname, handler) { | |||
} | |||
frappe.ui.form.trigger = function(doctype, fieldname, callback) { | |||
cur_frm.script_manager.trigger(fieldname, doctype, null, callback); | |||
frappe.ui.form.trigger = function(doctype, fieldname) { | |||
cur_frm.script_manager.trigger(fieldname, doctype); | |||
} | |||
frappe.ui.form.ScriptManager = Class.extend({ | |||
@@ -64,32 +64,84 @@ 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; | |||
trigger: function(event_name, doctype, name) { | |||
// trigger all the form level events that | |||
// are bound to this event_name | |||
let 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); | |||
let tasks = []; | |||
let handlers = this.get_handlers(event_name, doctype); | |||
// helper for child table | |||
this.frm.selected_doc = frappe.get_doc(doctype, name); | |||
return $.when.apply($, $.map(handlers, function(fn) { return fn(); })); | |||
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) => { | |||
if(event_name==='setup') { | |||
// setup must be called immediately | |||
runner(_function, false); | |||
} else { | |||
tasks.push(() => runner(_function, false)); | |||
} | |||
}); | |||
handlers.old_style.forEach((_function) => { | |||
if(event_name==='setup') { | |||
// setup must be called immediately | |||
runner(_function, true); | |||
} else { | |||
tasks.push(() => runner(_function, true)); | |||
} | |||
}); | |||
// run them serially | |||
return frappe.run_serially(tasks); | |||
}, | |||
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; | |||
}, | |||
@@ -105,7 +157,7 @@ frappe.ui.form.ScriptManager = Class.extend({ | |||
if(doctype.__custom_js) { | |||
try { | |||
eval(doctype.__custom_js) | |||
eval(doctype.__custom_js); | |||
} catch(e) { | |||
frappe.msgprint({ | |||
title: __('Error in Custom Script'), | |||
@@ -1,6 +1,4 @@ | |||
<div class="form-dashboard hidden"> | |||
<h4 class="form-headline hidden form-dashboard-section"> | |||
</h4> | |||
<div class="form-dashboard-wrapper"> | |||
<div class="progress-area hidden form-dashboard-section"> | |||
</div> | |||
<div class="form-heatmap hidden form-dashboard-section"> | |||
@@ -1,9 +1,8 @@ | |||
<div class="form-documents"> | |||
<h5 style="margin: 5px 0px;">{{__("Related Documents")}}</h5> | |||
{% for (var i=0; i < transactions.length; i++) { %} | |||
{% if((i % 2)===0) { %}<div class="row">{% } %} | |||
<div class="col-xs-6"> | |||
<h6 class="uppercase">{{ transactions[i].label }}</h5> | |||
<h6>{{ transactions[i].label }}</h6> | |||
{% for (var j=0; j < transactions[i].items.length; j++) { | |||
var doctype = transactions[i].items[j]; %} | |||
<div class="document-link" | |||