@@ -117,6 +117,7 @@ | |||||
"set_field_options": true, | "set_field_options": true, | ||||
"getCookie": true, | "getCookie": true, | ||||
"getCookies": true, | "getCookies": true, | ||||
"get_url_arg": true | |||||
"get_url_arg": true, | |||||
"QUnit": true | |||||
} | } | ||||
} | } |
@@ -1,9 +1,5 @@ | |||||
language: python | language: python | ||||
dist: trusty | dist: trusty | ||||
group: deprecated-2017Q2 | |||||
python: | |||||
- "2.7" | |||||
addons: | addons: | ||||
apt: | apt: | ||||
@@ -12,14 +8,14 @@ addons: | |||||
packages: | packages: | ||||
- google-chrome-stable | - google-chrome-stable | ||||
python: | |||||
- "2.7" | |||||
services: | services: | ||||
- mysql | - mysql | ||||
before_install: | |||||
- export DISPLAY=:99.0 | |||||
- sh -e /etc/init.d/xvfb start | |||||
install: | install: | ||||
- sudo rm /etc/apt/sources.list.d/docker.list | |||||
- sudo apt-get purge -y mysql-common mysql-server mysql-client | - sudo apt-get purge -y mysql-common mysql-server mysql-client | ||||
- nvm install v7.10.0 | - nvm install v7.10.0 | ||||
- wget https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py | - 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/ | - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ | ||||
before_script: | 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' | - 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;\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 | - echo "USE mysql;\nGRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost';\n" | mysql -u root -ptravis | ||||
- cd ~/frappe-bench | - cd ~/frappe-bench | ||||
- bench use test_site | - bench use test_site | ||||
- bench reinstall --yes | - bench reinstall --yes | ||||
- bench scheduler disable | |||||
- bench start & | - bench start & | ||||
- sleep 10 | - sleep 10 | ||||
script: | script: | ||||
- set -e | - set -e | ||||
- bench --verbose run-tests | - 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 | # public | ||||
from .exceptions import * | 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" | __title__ = "Frappe Framework" | ||||
local = Local() | local = Local() | ||||
@@ -138,8 +138,7 @@ def init(site, sites_path=None, new_site=False): | |||||
local.module_app = None | local.module_app = None | ||||
local.app_modules = None | local.app_modules = None | ||||
local.system_settings = None | |||||
local.system_country = None | |||||
local.system_settings = _dict() | |||||
local.user = None | local.user = None | ||||
local.user_perms = 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, | 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, | 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, | 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**. | """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 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 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 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 | message = content or message | ||||
if as_markdown: | if as_markdown: | ||||
@@ -416,13 +423,13 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message | |||||
import email.queue | import email.queue | ||||
email.queue.send(recipients=recipients, sender=sender, | 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, | reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, | ||||
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message, | 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, | 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, | send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, | ||||
communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification, | communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification, | ||||
inline_images=inline_images) | |||||
inline_images=inline_images, header=header) | |||||
whitelisted = [] | whitelisted = [] | ||||
guest_methods = [] | guest_methods = [] | ||||
@@ -1364,7 +1371,7 @@ def get_active_domains(): | |||||
return 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)) { | if (sources.includes(filename)) { | ||||
pack(target, sources); | pack(target, sources); | ||||
ondirty && ondirty(target); | ondirty && ondirty(target); | ||||
// break; | |||||
} | } | ||||
} | } | ||||
}); | }); | ||||
@@ -298,11 +298,13 @@ def console(context): | |||||
@click.option('--doctype', help="For DocType") | @click.option('--doctype', help="For DocType") | ||||
@click.option('--test', multiple=True, help="Specific test") | @click.option('--test', multiple=True, help="Specific test") | ||||
@click.option('--driver', help="For Travis") | @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('--module', help="Run tests in a module") | ||||
@click.option('--profile', is_flag=True, default=False) | @click.option('--profile', is_flag=True, default=False) | ||||
@click.option('--junit-xml-output', help="Destination file path for junit xml report") | @click.option('--junit-xml-output', help="Destination file path for junit xml report") | ||||
@pass_context | @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" | "Run tests" | ||||
import frappe.test_runner | import frappe.test_runner | ||||
tests = test | tests = test | ||||
@@ -311,7 +313,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None | |||||
frappe.init(site=site) | frappe.init(site=site) | ||||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, | 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: | if len(ret.failures) == 0 and len(ret.errors) == 0: | ||||
ret = 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.command('run-ui-tests') | ||||
@click.option('--app', help="App to run tests on, leave blank for all apps") | @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 | @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" | "Run UI tests" | ||||
import subprocess | |||||
import frappe.test_runner | |||||
site = get_site(context) | site = get_site(context) | ||||
frappe.init(site=site) | 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.command('serve') | ||||
@click.option('--port', default=8000) | @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); | 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] | address_as_dict = address[0] | ||||
name, address_template = get_address_templates(address_as_dict) | name, address_template = get_address_templates(address_as_dict) | ||||
return address_as_dict.get("name"), frappe.render_template(address_template, 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): | def address_query(doctype, txt, searchfield, start, page_len, filters): | ||||
from frappe.desk.reportview import get_match_cond | 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", | "doctype": "Contact", | ||||
"salutation": "Mr", | |||||
"email_id": "test_conctact@example.com", | "email_id": "test_conctact@example.com", | ||||
"first_name": "_Test Contact For _Test Customer", | "first_name": "_Test Contact For _Test Customer", | ||||
"is_primary_contact": 1, | "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 | from frappe.auth import LoginManager, CookieManager | ||||
# test user login log | # test user login log | ||||
frappe.local.form_dict = { 'cmd': 'login' } | |||||
frappe.form_dict = { | |||||
frappe.local.form_dict = frappe._dict({ | |||||
'cmd': 'login', | |||||
'sid': 'Guest', | 'sid': 'Guest', | ||||
'pwd': 'admin', | 'pwd': 'admin', | ||||
'usr': 'Administrator' | 'usr': 'Administrator' | ||||
} | |||||
}) | |||||
frappe.local.cookie_manager = CookieManager() | frappe.local.cookie_manager = CookieManager() | ||||
frappe.local.login_manager = LoginManager() | frappe.local.login_manager = LoginManager() | ||||
@@ -38,9 +37,11 @@ class TestAuthenticationLog(unittest.TestCase): | |||||
auth_log = self.get_auth_log() | auth_log = self.get_auth_log() | ||||
self.assertEquals(auth_log.status, 'Failed') | self.assertEquals(auth_log.status, 'Failed') | ||||
frappe.local.form_dict = frappe._dict() | |||||
def get_auth_log(self, operation='Login'): | 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)) | creation desc""".format(operation=operation)) | ||||
name = names[0] | name = names[0] | ||||
@@ -11,8 +11,10 @@ | |||||
"doctype": "DocType", | "doctype": "DocType", | ||||
"document_type": "Setup", | "document_type": "Setup", | ||||
"editable_grid": 1, | "editable_grid": 1, | ||||
"engine": "InnoDB", | |||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -41,6 +43,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 1, | "bold": 1, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -73,6 +76,7 @@ | |||||
"width": "163" | "width": "163" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 1, | "bold": 1, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -92,7 +96,7 @@ | |||||
"no_copy": 0, | "no_copy": 0, | ||||
"oldfieldname": "fieldtype", | "oldfieldname": "fieldtype", | ||||
"oldfieldtype": "Select", | "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, | "permlevel": 0, | ||||
"print_hide": 0, | "print_hide": 0, | ||||
"print_hide_if_no_value": 0, | "print_hide_if_no_value": 0, | ||||
@@ -105,6 +109,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 1, | "bold": 1, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -135,6 +140,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -167,6 +173,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -198,6 +205,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -228,6 +236,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -260,6 +269,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -290,6 +300,7 @@ | |||||
"width": "70px" | "width": "70px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -319,6 +330,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -349,6 +361,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -378,6 +391,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -408,6 +422,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -438,6 +453,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -465,6 +481,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -496,6 +513,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -526,6 +544,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -554,6 +573,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -584,6 +604,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -616,6 +637,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -646,6 +668,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -675,6 +698,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -704,6 +728,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -734,6 +759,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -761,6 +787,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -794,6 +821,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -823,6 +851,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -855,6 +884,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -887,6 +917,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -917,6 +948,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -947,6 +979,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -975,6 +1008,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1007,6 +1041,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1039,6 +1074,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1071,6 +1107,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1101,6 +1138,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1129,6 +1167,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1161,6 +1200,7 @@ | |||||
"width": "50px" | "width": "50px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1192,6 +1232,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1219,6 +1260,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1251,6 +1293,7 @@ | |||||
"width": "300px" | "width": "300px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1280,6 +1323,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -1319,7 +1363,7 @@ | |||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 1, | "istable": 1, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-04-21 16:56:04.023296", | |||||
"modified": "2017-07-06 12:36:21.248293", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "DocField", | "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.custom.doctype.property_setter.property_setter import make_property_setter | ||||
from frappe.desk.notifications import delete_notification_count_for | from frappe.desk.notifications import delete_notification_count_for | ||||
from frappe.modules import make_boilerplate | 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 | import frappe.website.render | ||||
class InvalidFieldNameError(frappe.ValidationError): pass | 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]: | if not [d.fieldname for d in self.fields if d.in_list_view]: | ||||
cnt = 0 | cnt = 0 | ||||
for d in self.fields: | 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 | d.in_list_view = 1 | ||||
cnt += 1 | cnt += 1 | ||||
if cnt == 4: break | if cnt == 4: break | ||||
@@ -385,9 +385,10 @@ def validate_fields(meta): | |||||
1. There are no illegal characters in fieldnames | 1. There are no illegal characters in fieldnames | ||||
2. If fieldnames are unique. | 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. | 7. `Check` type field has default as 0 or 1. | ||||
8. `Dynamic Links` are correctly defined. | 8. `Dynamic Links` are correctly defined. | ||||
9. Precision is set in numeric fields and is between 1 & 6. | 9. Precision is set in numeric fields and is between 1 & 6. | ||||
@@ -406,6 +407,9 @@ def validate_fields(meta): | |||||
if len(duplicates) > 1: | if len(duplicates) > 1: | ||||
frappe.throw(_("Fieldname {0} appears multiple times in rows {1}").format(fieldname, ", ".join(duplicates))) | 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): | def check_illegal_mandatory(d): | ||||
if (d.fieldtype in no_value_fields) and d.fieldtype!="Table" and d.reqd: | 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)) | 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), | frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname), | ||||
InvalidFieldNameError) | InvalidFieldNameError) | ||||
fields = meta.get("fields") | fields = meta.get("fields") | ||||
fieldname_list = [d.fieldname for d in fields] | fieldname_list = [d.fieldname for d in fields] | ||||
@@ -598,6 +601,7 @@ def validate_fields(meta): | |||||
d.fieldname = d.fieldname.lower() | d.fieldname = d.fieldname.lower() | ||||
check_illegal_characters(d.fieldname) | check_illegal_characters(d.fieldname) | ||||
check_unique_fieldname(d.fieldname) | check_unique_fieldname(d.fieldname) | ||||
check_fieldname_length(d.fieldname) | |||||
check_illegal_mandatory(d) | check_illegal_mandatory(d) | ||||
check_link_table_options(d) | check_link_table_options(d) | ||||
check_dynamic_link_options(d) | check_dynamic_link_options(d) | ||||
@@ -766,3 +770,10 @@ def init_list(doctype): | |||||
doc = frappe.get_meta(doctype) | doc = frappe.get_meta(doctype) | ||||
make_boilerplate("controller_list.js", doc) | make_boilerplate("controller_list.js", doc) | ||||
make_boilerplate("controller_list.html", 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) | delete_custom_role('report', self.name) | ||||
def set_doctype_roles(self): | 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) | meta = frappe.get_meta(self.ref_doctype) | ||||
roles = [{'role': d.role} for d in meta.permissions if d.permlevel==0] | roles = [{'role': d.role} for d in meta.permissions if d.permlevel==0] | ||||
self.set('roles', roles) | self.set('roles', roles) | ||||
@@ -810,7 +810,7 @@ | |||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
"columns": 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", | "fieldname": "ignore_user_permissions_if_missing", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"hidden": 0, | "hidden": 0, | ||||
@@ -835,6 +835,38 @@ | |||||
"set_only_once": 0, | "set_only_once": 0, | ||||
"unique": 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_bulk_edit": 0, | ||||
"allow_on_submit": 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", | "new_password": "Eastern_43A1W", | ||||
"enabled": 1 | "enabled": 1 | ||||
}, | }, | ||||
{ | |||||
"doctype": "User", | |||||
"email": "test3@example.com", | |||||
"first_name": "_Test3", | |||||
"new_password": "Eastern_43A1W", | |||||
"enabled": 1 | |||||
}, | |||||
{ | { | ||||
"doctype": "User", | "doctype": "User", | ||||
"email": "testperm@example.com", | "email": "testperm@example.com", | ||||
@@ -1949,7 +1949,7 @@ | |||||
"precision": "", | "precision": "", | ||||
"print_hide": 0, | "print_hide": 0, | ||||
"print_hide_if_no_value": 0, | "print_hide_if_no_value": 0, | ||||
"read_only": 0, | |||||
"read_only": 1, | |||||
"remember_last_selected_value": 0, | "remember_last_selected_value": 0, | ||||
"report_hide": 0, | "report_hide": 0, | ||||
"reqd": 0, | "reqd": 0, | ||||
@@ -225,11 +225,11 @@ class User(Document): | |||||
def password_reset_mail(self, link): | def password_reset_mail(self, link): | ||||
self.send_login_mail(_("Password Reset"), | 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): | def password_update_mail(self, password): | ||||
self.send_login_mail(_("Password Update"), | 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): | def send_welcome_mail_to_user(self): | ||||
from frappe.utils import get_url | from frappe.utils import get_url | ||||
@@ -248,7 +248,7 @@ class User(Document): | |||||
else: | else: | ||||
subject = _("Complete Registration") | subject = _("Complete Registration") | ||||
self.send_login_mail(subject, "templates/emails/new_user.html", | |||||
self.send_login_mail(subject, "new_user", | |||||
dict( | dict( | ||||
link=link, | link=link, | ||||
site_url=get_url(), | 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 | 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, | 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) | delayed=(not now) if now!=None else self.flags.delay_emails, retry=3) | ||||
def a_system_manager_should_exist(self): | 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=[]): | 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 | 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", "minimum_password_score"], as_dict=True) or {} | ||||
enable_password_policy = cint(password_policy.get("enable_password_policy", 0)) | 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 {} | return {} | ||||
if not user_data: | 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']) | ['first_name', 'middle_name', 'last_name', 'email', 'birth_date']) | ||||
if new_password: | 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> | <li>{%= __("Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.") %}</li> | ||||
</ol> | </ol> | ||||
<p>{%= __("If these instructions where not helpful, please add in your suggestions on GitHub Issues.") %} | <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> | </p> | ||||
</div> | </div> |
@@ -11,7 +11,7 @@ frappe.ui.form.on('Custom Field', { | |||||
['DocType', 'issingle', '=', 0], | ['DocType', 'issingle', '=', 0], | ||||
]; | ]; | ||||
if(frappe.session.user!=="Administrator") { | if(frappe.session.user!=="Administrator") { | ||||
filters.push(['DocType', 'module', '!=', 'Core']) | |||||
filters.push(['DocType', 'module', 'not in', ['Core', 'Custom']]) | |||||
} | } | ||||
return { | return { | ||||
"filters": filters | "filters": filters | ||||
@@ -11,6 +11,7 @@ | |||||
"doctype": "DocType", | "doctype": "DocType", | ||||
"document_type": "Setup", | "document_type": "Setup", | ||||
"editable_grid": 0, | "editable_grid": 0, | ||||
"engine": "InnoDB", | |||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | "allow_bulk_edit": 0, | ||||
@@ -219,7 +220,7 @@ | |||||
"no_copy": 0, | "no_copy": 0, | ||||
"oldfieldname": "fieldtype", | "oldfieldname": "fieldtype", | ||||
"oldfieldtype": "Select", | "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, | "permlevel": 0, | ||||
"print_hide": 0, | "print_hide": 0, | ||||
"print_hide_if_no_value": 0, | "print_hide_if_no_value": 0, | ||||
@@ -1160,7 +1161,7 @@ | |||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-06-13 09:52:49.692096", | |||||
"modified": "2017-07-06 17:23:43.835189", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "Custom Field", | "name": "Custom Field", | ||||
@@ -39,6 +39,10 @@ class CustomField(Document): | |||||
if not self.fieldname: | if not self.fieldname: | ||||
frappe.throw(_("Fieldname not set for Custom Field")) | 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): | def on_update(self): | ||||
frappe.clear_cache(doctype=self.dt) | frappe.clear_cache(doctype=self.dt) | ||||
if not self.flags.ignore_validate: | if not self.flags.ignore_validate: | ||||
@@ -15,7 +15,7 @@ frappe.ui.form.on("Customize Form", { | |||||
['DocType', 'custom', '=', 0], | ['DocType', 'custom', '=', 0], | ||||
['DocType', 'name', 'not in', 'DocType, DocField, DocPerm, User, Role, Has Role, \ | ['DocType', 'name', 'not in', 'DocType, DocField, DocPerm, User, Role, Has Role, \ | ||||
Page, Has Role, Module Def, Print Format, Report, Customize Form, \ | 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] | ['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', 'Data'), ('Text', 'Text Editor', 'Code', 'Signature'), ('Data', 'Select'), | ||||
('Text', 'Small Text')) | ('Text', 'Small Text')) | ||||
allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select',) | |||||
class CustomizeForm(Document): | class CustomizeForm(Document): | ||||
def on_update(self): | def on_update(self): | ||||
frappe.db.sql("delete from tabSingles where doctype='Customize Form'") | 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)) | frappe.msgprint(_("You cannot unset 'Read Only' for field {0}").format(df.label)) | ||||
continue | 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), | self.make_property_setter(property=property, value=df.get(property), | ||||
property_type=docfield_properties[property], fieldname=df.fieldname) | property_type=docfield_properties[property], fieldname=df.fieldname) | ||||
@@ -165,22 +165,22 @@ class TestCustomizeForm(unittest.TestCase): | |||||
df = d.get("fields", {"fieldname": "title"})[0] | df = d.get("fields", {"fieldname": "title"})[0] | ||||
# invalid fieldname | # invalid fieldname | ||||
df.options = """{doc_type} - {introduction_test}""" | |||||
df.default = """{doc_type} - {introduction_test}""" | |||||
self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") | self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") | ||||
# space in formatter | # space in formatter | ||||
df.options = """{doc_type} - {introduction text}""" | |||||
df.default = """{doc_type} - {introduction text}""" | |||||
self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") | self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") | ||||
# valid fieldname | # valid fieldname | ||||
df.options = """{doc_type} - {introduction_text}""" | |||||
df.default = """{doc_type} - {introduction_text}""" | |||||
d.run_method("save_customization") | d.run_method("save_customization") | ||||
# valid fieldname with escaped curlies | # valid fieldname with escaped curlies | ||||
df.options = """{{ {doc_type} }} - {introduction_text}""" | |||||
df.default = """{{ {doc_type} }} - {introduction_text}""" | |||||
d.run_method("save_customization") | d.run_method("save_customization") | ||||
# undo | # undo | ||||
df.options = None | |||||
df.default = None | |||||
d.run_method("save_customization") | d.run_method("save_customization") | ||||
@@ -94,7 +94,7 @@ | |||||
"no_copy": 0, | "no_copy": 0, | ||||
"oldfieldname": "fieldtype", | "oldfieldname": "fieldtype", | ||||
"oldfieldtype": "Select", | "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, | "permlevel": 0, | ||||
"print_hide": 0, | "print_hide": 0, | ||||
"print_hide_if_no_value": 0, | "print_hide_if_no_value": 0, | ||||
@@ -1202,7 +1202,7 @@ | |||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 1, | "istable": 1, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-04-21 17:02:14.903382", | |||||
"modified": "2017-07-06 17:24:03.665171", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "Customize Form Field", | "name": "Customize Form Field", | ||||
@@ -895,8 +895,8 @@ | |||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 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", | "module": "Desk", | ||||
"name": "Event", | "name": "Event", | ||||
"owner": "Administrator", | "owner": "Administrator", | ||||
@@ -14,6 +14,7 @@ | |||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -42,6 +43,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -72,6 +74,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -83,9 +86,9 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 1, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | |||||
"in_standard_filter": 1, | |||||
"label": "Priority", | "label": "Priority", | ||||
"length": 0, | "length": 0, | ||||
"no_copy": 0, | "no_copy": 0, | ||||
@@ -104,6 +107,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -131,6 +135,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -143,7 +148,7 @@ | |||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | "in_global_search": 0, | ||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | |||||
"in_standard_filter": 1, | |||||
"label": "Due Date", | "label": "Due Date", | ||||
"length": 0, | "length": 0, | ||||
"no_copy": 0, | "no_copy": 0, | ||||
@@ -161,6 +166,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -190,6 +196,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -219,6 +226,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -251,6 +259,7 @@ | |||||
"width": "300px" | "width": "300px" | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -279,6 +288,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -310,6 +320,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -341,6 +352,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -368,6 +380,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -399,6 +412,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -428,6 +442,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -458,6 +473,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -498,7 +514,7 @@ | |||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-03-08 14:39:02.027528", | |||||
"modified": "2017-07-13 17:44:54.369254", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Desk", | "module": "Desk", | ||||
"name": "ToDo", | "name": "ToDo", | ||||
@@ -65,7 +65,7 @@ class FormMeta(Meta): | |||||
def _get_path(fname): | def _get_path(fname): | ||||
return os.path.join(path, scrub(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') | self._add_code(_get_path(self.name + '.js'), '__js') | ||||
if system_country: | if system_country: | ||||
@@ -82,6 +82,7 @@ class FormMeta(Meta): | |||||
self.add_code_via_hook("doctype_js", "__js") | self.add_code_via_hook("doctype_js", "__js") | ||||
self.add_code_via_hook("doctype_list_js", "__list_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_tree_js", "__tree_js") | ||||
self.add_code_via_hook("doctype_calendar_js", "__calendar_js") | |||||
self.add_custom_script() | self.add_custom_script() | ||||
self.add_html_templates(path) | self.add_html_templates(path) | ||||
@@ -5,7 +5,7 @@ | |||||
<div class="media"> | <div class="media"> | ||||
<div class="pull-right app-buttons"> | <div class="pull-right app-buttons"> | ||||
<a class="btn btn-default btn-xs" | <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) { %} | {% if (app.installed) { %} | ||||
<button class="btn btn-danger btn-xs btn-remove" | <button class="btn btn-danger btn-xs btn-remove" | ||||
data-title="{{ app.app_title }}" | data-title="{{ app.app_title }}" | ||||
@@ -21,7 +21,7 @@ | |||||
{{ f[1] }} | {{ f[1] }} | ||||
</td> | </td> | ||||
<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> | ||||
<td> | <td> | ||||
{{ f[2] }} | {{ f[2] }} | ||||
@@ -9,6 +9,13 @@ frappe.pages['backups'].on_page_load = function(wrapper) { | |||||
frappe.set_route('Form', 'System Settings'); | 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.breadcrumbs.add("Setup"); | ||||
$(frappe.render_template("backups")).appendTo(page.body.addClass("no-border")); | $(frappe.render_template("backups")).appendTo(page.body.addClass("no-border")); | ||||
@@ -1,6 +1,7 @@ | |||||
import os | import os | ||||
import frappe | 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 | from frappe.utils.data import convert_utc_to_user_timezone | ||||
import datetime | import datetime | ||||
@@ -57,3 +58,29 @@ def delete_downloadable_backups(): | |||||
if len(files) > backup_limit: | if len(files) > backup_limit: | ||||
cleanup_old_backups(path, 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 { | .setup-wizard-slide { | ||||
padding-left: 0px; | padding-left: 0px; | ||||
padding-right: 0px; | padding-right: 0px; | ||||
@@ -14,22 +37,60 @@ | |||||
} | } | ||||
.setup-wizard-slide .lead { | .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; | 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 { | .setup-wizard-slide.with-form { | ||||
margin: 40px auto; | |||||
margin: 30px auto; | |||||
padding: 10px 50px; | |||||
border: 1px solid #d1d8dd; | border: 1px solid #d1d8dd; | ||||
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.1); | box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.1); | ||||
} | } | ||||
.setup-wizard-slide .footer { | .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 { | .setup-wizard-progress { | ||||
padding: 15px; | |||||
padding: 15px; | |||||
} | } | ||||
.setup-wizard-slide .fa-fw { | .setup-wizard-slide .fa-fw { | ||||
@@ -50,16 +111,28 @@ | |||||
} | } | ||||
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] { | .setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] { | ||||
width: 140px; | |||||
height: 180px; /*depends on presence of heading*/ | |||||
text-align: center; | 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 .missing-image, | ||||
.setup-wizard-slide .attach-image-display { | .setup-wizard-slide .attach-image-display { | ||||
display: block; | display: block; | ||||
position: relative; | 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 { | .setup-wizard-slide .missing-image .octicon { | ||||
@@ -69,6 +142,38 @@ | |||||
-webkit-transform: translate(0px, -50%); | -webkit-transform: translate(0px, -50%); | ||||
} | } | ||||
.setup-wizard-slide .img-container { | |||||
height: 100%; | |||||
width: 100%; | |||||
padding: 2px; | |||||
display: flex; | |||||
align-items: center; | |||||
justify-content: center; | |||||
position: relative; | |||||
border: 1px solid #d1d8dd; | |||||
border-radius: 6px; | |||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); | |||||
} | |||||
.setup-wizard-slide .img-overlay { | |||||
display: flex; | |||||
align-items: center; | |||||
justify-content: center; | |||||
position: absolute; | |||||
width: 100%; | |||||
height: 100%; | |||||
color: #777777; | |||||
background-color: rgba(255, 255, 255, 0.7); | |||||
opacity: 0; | |||||
} | |||||
.setup-wizard-slide .img-overlay:hover { | |||||
opacity: 1; | |||||
cursor: pointer; | |||||
} | |||||
.setup-wizard-message-image { | .setup-wizard-message-image { | ||||
margin: 15px auto; | margin: 15px auto; | ||||
} | } |
@@ -1,22 +1,25 @@ | |||||
frappe.provide("frappe.wiz"); | frappe.provide("frappe.wiz"); | ||||
frappe.provide("frappe.wiz.events"); | |||||
frappe.provide("frappe.setup.events"); | |||||
frappe.wiz = { | |||||
frappe.setup = { | |||||
slides: [], | slides: [], | ||||
events: {}, | events: {}, | ||||
data: {}, | |||||
utils: {}, | |||||
remove_app_slides: [], | remove_app_slides: [], | ||||
on: function(event, fn) { | 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) { | add_slide: function(slide) { | ||||
frappe.wiz.slides.push(slide); | |||||
frappe.setup.slides.push(slide); | |||||
}, | }, | ||||
run_event: function(event) { | run_event: function(event) { | ||||
$.each(frappe.wiz.events[event] || [], function(i, fn) { | |||||
$.each(frappe.setup.events[event] || [], function(i, fn) { | |||||
fn(); | fn(); | ||||
}); | }); | ||||
} | } | ||||
@@ -25,21 +28,21 @@ frappe.wiz = { | |||||
frappe.pages['setup-wizard'].on_page_load = function(wrapper) { | frappe.pages['setup-wizard'].on_page_load = function(wrapper) { | ||||
// setup page ui | // setup page ui | ||||
$(".navbar:first").toggle(false); | $(".navbar:first").toggle(false); | ||||
$("body").css({"padding-top":"30px"}); | |||||
var requires = ["/assets/frappe/css/animate.min.css"].concat(frappe.boot.setup_wizard_requires || []); | var requires = ["/assets/frappe/css/animate.min.css"].concat(frappe.boot.setup_wizard_requires || []); | ||||
frappe.require(requires, function() { | frappe.require(requires, function() { | ||||
frappe.wiz.run_event("before_load"); | |||||
frappe.setup.run_event("before_load"); | |||||
var wizard_settings = { | var wizard_settings = { | ||||
page_name: "setup-wizard", | page_name: "setup-wizard", | ||||
parent: wrapper, | parent: wrapper, | ||||
slides: frappe.wiz.slides, | |||||
slides: frappe.setup.slides, | |||||
title: __("Welcome") | 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; | // 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) { | init: function(opts) { | ||||
$.extend(this, opts); | $.extend(this, opts); | ||||
this.make(); | this.make(); | ||||
@@ -75,6 +78,7 @@ frappe.wiz.Wizard = Class.extend({ | |||||
</div>', {html:html})) | </div>', {html:html})) | ||||
}, | }, | ||||
show_working: function() { | show_working: function() { | ||||
$('header').find('.setup-wizard-brand').hide(); | |||||
this.hide_current_slide(); | this.hide_current_slide(); | ||||
frappe.set_route(this.page_name); | frappe.set_route(this.page_name); | ||||
this.current_slide = {"$wrapper": this.get_message(this.working_html()).appendTo(this.parent)}; | 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(); | this.update_values(); | ||||
if(!this.slide_dict[id]) { | 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(); | this.slide_dict[id].make(); | ||||
} | } | ||||
@@ -147,8 +151,8 @@ frappe.wiz.Wizard = Class.extend({ | |||||
args: {args: this.values}, | args: {args: this.values}, | ||||
callback: function(r) { | callback: function(r) { | ||||
me.show_complete(); | 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() { | setTimeout(function() { | ||||
window.location = "/desk"; | window.location = "/desk"; | ||||
@@ -181,26 +185,27 @@ frappe.wiz.Wizard = Class.extend({ | |||||
this.update_values(); | 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 | // remove slides listed in remove_app_slides | ||||
var new_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; | var domains = slide.domains; | ||||
if (domains.indexOf('all') !== -1 || | if (domains.indexOf('all') !== -1 || | ||||
domains.indexOf(frappe.wiz.domain.toLowerCase()) !== -1) { | |||||
domains.indexOf(frappe.setup.domain.toLowerCase()) !== -1) { | |||||
new_slides.push(slide); | new_slides.push(slide); | ||||
} | } | ||||
} else { | } else { | ||||
new_slides.push(slide); | 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 | // re-render all slides | ||||
this.slide_dict = {}; | 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) { | init: function(opts) { | ||||
$.extend(this, opts); | $.extend(this, opts); | ||||
this.$wrapper = $('<div class="slide-wrapper hidden"></div>') | this.$wrapper = $('<div class="slide-wrapper hidden"></div>') | ||||
@@ -224,6 +229,24 @@ frappe.wiz.WizardSlide = Class.extend({ | |||||
var me = this; | var me = this; | ||||
if(this.$body) this.$body.remove(); | 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) { | if(this.before_load) { | ||||
this.before_load(this); | this.before_load(this); | ||||
} | } | ||||
@@ -234,7 +257,6 @@ frappe.wiz.WizardSlide = Class.extend({ | |||||
main_title:__(this.wiz.title), | main_title:__(this.wiz.title), | ||||
step: this.id + 1, | step: this.id + 1, | ||||
name: this.name, | name: this.name, | ||||
css_class: this.css_class || "", | |||||
slides_count: this.wiz.slides.length | slides_count: this.wiz.slides.length | ||||
})).appendTo(this.$wrapper); | })).appendTo(this.$wrapper); | ||||
@@ -242,7 +264,7 @@ frappe.wiz.WizardSlide = Class.extend({ | |||||
if(this.fields) { | if(this.fields) { | ||||
this.form = new frappe.ui.FieldGroup({ | this.form = new frappe.ui.FieldGroup({ | ||||
fields: this.fields, | |||||
fields: fields, | |||||
body: this.body, | body: this.body, | ||||
no_submit_on_enter: true | no_submit_on_enter: true | ||||
}); | }); | ||||
@@ -251,18 +273,36 @@ frappe.wiz.WizardSlide = Class.extend({ | |||||
$(this.body).html(this.html); | $(this.body).html(this.html); | ||||
} | } | ||||
this.set_reqd_fields(); | |||||
this.set_init_values(); | this.set_init_values(); | ||||
this.make_prev_next_buttons(); | 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) { | if(this.onload) { | ||||
this.onload(this); | 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() { | set_init_values: function() { | ||||
var me = this; | var me = this; | ||||
// set values from frappe.wiz.values | |||||
// set values from frappe.setup.values | |||||
if(frappe.wizard.values && this.fields) { | if(frappe.wizard.values && this.fields) { | ||||
this.fields.forEach(function(f) { | this.fields.forEach(function(f) { | ||||
var value = frappe.wizard.values[f.fieldname]; | var value = frappe.wizard.values[f.fieldname]; | ||||
@@ -284,6 +324,25 @@ frappe.wiz.WizardSlide = Class.extend({ | |||||
return true; | 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() { | make_prev_next_buttons: function() { | ||||
var me = this; | var me = this; | ||||
@@ -311,7 +370,7 @@ frappe.wiz.WizardSlide = Class.extend({ | |||||
.click(this.next_or_complete.bind(this)); | .click(this.next_or_complete.bind(this)); | ||||
} | } | ||||
//setup mousefree navigation | |||||
// setup mousefree navigation | |||||
this.$body.on('keypress', function(e) { | this.$body.on('keypress', function(e) { | ||||
if(e.which === 13) { | if(e.which === 13) { | ||||
var $target = $(e.target); | 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() { | next_or_complete: function() { | ||||
if(this.set_values()) { | if(this.set_values()) { | ||||
if(this.id+1 < this.wiz.slides.length) { | 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() { | focus_first_input: function() { | ||||
setTimeout(function() { | setTimeout(function() { | ||||
this.$body.find('.form-control').first().focus(); | 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", | name: "welcome", | ||||
domains: ["all"], | domains: ["all"], | ||||
title: __("Welcome"), | |||||
title: __("Hello!"), | |||||
icon: "fa fa-world", | icon: "fa fa-world", | ||||
help: __("Let's prepare the system for first use."), | help: __("Let's prepare the system for first use."), | ||||
fields: [ | 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) { | onload: function(slide) { | ||||
if (!frappe.wiz.welcome.data) { | |||||
frappe.wiz.welcome.load_languages(slide); | |||||
if (frappe.setup.data.lang) { | |||||
this.setup_fields(slide); | |||||
} else { | } 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) { | 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"], | domains: ["all"], | ||||
title: __("Region"), | |||||
title: __("Select Your Region"), | |||||
icon: "fa fa-flag", | icon: "fa fa-flag", | ||||
help: __("Select your Country, Time Zone and Currency"), | help: __("Select your Country, Time Zone and Currency"), | ||||
fields: [ | fields: [ | ||||
{ fieldname: "country", label: __("Country"), reqd:1, | |||||
{ fieldname: "country", label: __("Your Country"), reqd:1, | |||||
fieldtype: "Select" }, | fieldtype: "Select" }, | ||||
{ fieldtype: "Section Break" }, | |||||
{ fieldname: "timezone", label: __("Time Zone"), reqd:1, | { fieldname: "timezone", label: __("Time Zone"), reqd:1, | ||||
fieldtype: "Select" }, | fieldtype: "Select" }, | ||||
{ fieldtype: "Column Break" }, | |||||
{ fieldname: "currency", label: __("Currency"), reqd:1, | { fieldname: "currency", label: __("Currency"), reqd:1, | ||||
fieldtype: "Select" }, | |||||
fieldtype: "Select" } | |||||
], | ], | ||||
onload: function(slide) { | 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 { | } 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) { | 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() | @frappe.whitelist() | ||||
def load_languages(): | 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 { | return { | ||||
"default_language": frappe.db.get_value('Language', frappe.local.lang, 'language_name') or frappe.local.lang, | "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): | def prettify_args(args): | ||||
# remove attachments | # 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"> | <div class="text-center setup-wizard-progress text-extra-muted"> | ||||
{% for (var i=0; i < slides_count; i++) { %} | {% 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> | </div> | ||||
<p class="text-center lead">{%= title %}</p> | |||||
<p class="lead">{%= title %}</p> | |||||
<div class="row"> | <div class="row"> | ||||
<div class="col-sm-12"> | |||||
<div class="setup-wizard-body col-sm-12"> | |||||
<!-- {% if (help) { %} <p class="text-center">{%= help %}</p> {% } %} --> | <!-- {% if (help) { %} <p class="text-center">{%= help %}</p> {% } %} --> | ||||
<div class="form"></div> | <div class="form"></div> | ||||
<a class="more-btn hide btn btn-default btn-sm" style="margin-left: 41%;">{%= __("Add More") %}</a> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="footer text-right"> | <div class="footer text-right"> | ||||
@@ -159,7 +159,7 @@ def export_query(): | |||||
elif not row: | elif not row: | ||||
result.append([]) | result.append([]) | ||||
else: | 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 | from frappe.utils.xlsxutils import make_xlsx | ||||
xlsx_file = make_xlsx(result, "Query Report") | 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 | ## 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. | 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. | 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. | If you need more information about test execution - you can use verbose log level for bench. | ||||
bench --verbose run-tests | bench --verbose run-tests | ||||
### Options: | ### Options: | ||||
--app <AppName> | --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) | --module <Module> (Run a particular module that has tests) | ||||
--profile (Runs a Python profiler on the test) | --profile (Runs a Python profiler on the test) | ||||
--junit-xml-output<PathToXML> (The command provides test results in the standard XUnit XML format) | --junit-xml-output<PathToXML> (The command provides test results in the standard XUnit XML format) | ||||
#### 2.1. Example for app: | #### 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. | We can run tests for each application. | ||||
- frappe-bench/apps/erpnext/ | - frappe-bench/apps/erpnext/ | ||||
@@ -50,7 +50,7 @@ We can run tests for each application. | |||||
. | . | ||||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||||
Ran 1 test in 0.008s | Ran 1 test in 0.008s | ||||
OK | OK | ||||
#### 2.3. Example for test: | #### 2.3. Example for test: | ||||
@@ -60,44 +60,44 @@ Run a specific case in User: | |||||
. | . | ||||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||||
Ran 1 test in 0.005s | Ran 1 test in 0.005s | ||||
OK | OK | ||||
#### 2.4. Example for module: | #### 2.4. Example for module: | ||||
If we want to run tests in the module: | If we want to run tests in the module: | ||||
/home/frappe/frappe-bench/apps/erpnext/erpnext/support/doctype/issue/test_issue.py | /home/frappe/frappe-bench/apps/erpnext/erpnext/support/doctype/issue/test_issue.py | ||||
We should use module name like this (related to application folder) | We should use module name like this (related to application folder) | ||||
erpnext.support.doctype.issue.test_issue | erpnext.support.doctype.issue.test_issue | ||||
#####EXAMPLE: | #####EXAMPLE: | ||||
frappe@erpnext:~/frappe-bench$ bench run-tests --module "erpnext.stock.doctype.stock_entry.test_stock_entry" | frappe@erpnext:~/frappe-bench$ bench run-tests --module "erpnext.stock.doctype.stock_entry.test_stock_entry" | ||||
........................... | ........................... | ||||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||||
Ran 27 tests in 30.549s | Ran 27 tests in 30.549s | ||||
#### 2.5. Example for profile: | #### 2.5. Example for profile: | ||||
frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost" --profile | frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost" --profile | ||||
. | . | ||||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||||
Ran 1 test in 0.010s | Ran 1 test in 0.010s | ||||
OK | OK | ||||
9133 function calls (8912 primitive calls) in 0.011 seconds | 9133 function calls (8912 primitive calls) in 0.011 seconds | ||||
Ordered by: cumulative time | Ordered by: cumulative time | ||||
ncalls tottime percall cumtime percall filename:lineno(function) | 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) | 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) | 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) | 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) | 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: | #### 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. | 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: | #### 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: | 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_) | (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.assertTrue("_Test Event 3" in subjects) | ||||
self.assertFalse("_Test Event 2" 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. | 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 Name: sample_app | ||||
App Title: Sample App | App Title: Sample App | ||||
App Description: This is a sample app. | App Description: This is a sample app. | ||||
@@ -1,7 +1,7 @@ | |||||
# Les vues web | # Les vues web | ||||
Frappe a deux principaux environnements, le **bureau** et **le web**. Le **bureau** est un environnement riche AJAX alors | 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 | 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. | accès au desk. | ||||
@@ -585,7 +585,7 @@ | |||||
"label": "Format", | "label": "Format", | ||||
"length": 0, | "length": 0, | ||||
"no_copy": 0, | "no_copy": 0, | ||||
"options": "HTML\nXLS\nCSV", | |||||
"options": "HTML\nXLSX\nCSV", | |||||
"permlevel": 0, | "permlevel": 0, | ||||
"precision": "", | "precision": "", | ||||
"print_hide": 0, | "print_hide": 0, | ||||
@@ -669,7 +669,7 @@ | |||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-04-25 03:31:55.214149", | |||||
"modified": "2017-06-30 12:54:13.350902", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Email", | "module": "Email", | ||||
"name": "Auto Email Report", | "name": "Auto Email Report", | ||||
@@ -8,7 +8,7 @@ from frappe import _ | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from datetime import timedelta | from datetime import timedelta | ||||
import frappe.utils | import frappe.utils | ||||
from frappe.utils.xlsutils import get_xls | |||||
from frappe.utils.xlsxutils import make_xlsx | |||||
from frappe.utils.csvutils import to_csv | from frappe.utils.csvutils import to_csv | ||||
max_reports_per_user = 3 | max_reports_per_user = 3 | ||||
@@ -43,7 +43,7 @@ class AutoEmailReport(Document): | |||||
def validate_report_format(self): | def validate_report_format(self): | ||||
""" check if user has select correct report format """ | """ 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: | if self.format not in valid_report_formats: | ||||
frappe.throw(_("%s is not a valid report format. Report format should \ | 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))))) | 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': | if self.format == 'HTML': | ||||
return self.get_html_table(columns, data) | 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': | elif self.format == 'CSV': | ||||
return self.get_csv(columns, data) | |||||
spreadsheet_data = self.get_spreadsheet_data(columns, data) | |||||
return to_csv(spreadsheet_data) | |||||
else: | else: | ||||
frappe.throw(_('Invalid Output Format')) | frappe.throw(_('Invalid Output Format')) | ||||
@@ -85,7 +88,8 @@ class AutoEmailReport(Document): | |||||
'data': data | 'data': data | ||||
}) | }) | ||||
def get_csv(self, columns, data): | |||||
@staticmethod | |||||
def get_spreadsheet_data(columns, data): | |||||
out = [[df.label for df in columns], ] | out = [[df.label for df in columns], ] | ||||
for row in data: | for row in data: | ||||
new_row = [] | new_row = [] | ||||
@@ -93,7 +97,7 @@ class AutoEmailReport(Document): | |||||
for df in columns: | for df in columns: | ||||
new_row.append(frappe.format(row[df.fieldname], df, row)) | new_row.append(frappe.format(row[df.fieldname], df, row)) | ||||
return to_csv(out) | |||||
return out | |||||
def get_file_name(self): | def get_file_name(self): | ||||
return "{0}.{1}".format(self.report.replace(" ", "-").replace("/", "-"), self.format.lower()) | 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() | data = auto_email_report.get_report_content() | ||||
self.assertTrue('"Language","Core"' in data) | self.assertTrue('"Language","Core"' in data) | ||||
auto_email_report.format = 'XLS' | |||||
auto_email_report.format = 'XLSX' | |||||
data = auto_email_report.get_report_content() | 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) { | 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 \ | frm.dashboard.set_headline_alert('Gmail will only work if you allow access for less secure \ | ||||
apps in Gmail settings. <a target="_blank" \ | apps in Gmail settings. <a target="_blank" \ | ||||
href="https://support.google.com/accounts/answer/6010255?hl=en">Read this for details</a>'); | 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 | # gmail shows sent emails in inbox | ||||
# and we don't want emails sent by us to be pulled back into the system again | # 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 | # 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 | raise SentEmailInInbox | ||||
if email.message_id: | if email.message_id: | ||||
@@ -472,7 +474,6 @@ class EmailAccount(Document): | |||||
parent = frappe._dict(doctype=self.append_to, name=parent[0].name) | parent = frappe._dict(doctype=self.append_to, name=parent[0].name) | ||||
return parent | return parent | ||||
def create_new_parent(self, communication, email): | def create_new_parent(self, communication, email): | ||||
'''If no parent found, create a new reference document''' | '''If no parent found, create a new reference document''' | ||||
@@ -18,6 +18,7 @@ frappe.email_alert = { | |||||
// set value changed options | // set value changed options | ||||
frm.set_df_property("value_changed", "options", [""].concat(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 | // set date changed options | ||||
frm.set_df_property("date_changed", "options", $.map(fields, | frm.set_df_property("date_changed", "options", $.map(fields, | ||||
@@ -1,5 +1,6 @@ | |||||
{ | { | ||||
"allow_copy": 0, | "allow_copy": 0, | ||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | "allow_import": 0, | ||||
"allow_rename": 1, | "allow_rename": 1, | ||||
"autoname": "Prompt", | "autoname": "Prompt", | ||||
@@ -13,6 +14,7 @@ | |||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -24,6 +26,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Enabled", | "label": "Enabled", | ||||
@@ -41,6 +44,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -51,6 +55,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Filters", | "label": "Filters", | ||||
@@ -68,6 +73,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -79,6 +85,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 1, | "ignore_xss_filter": 1, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Subject", | "label": "Subject", | ||||
@@ -96,6 +103,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -106,6 +114,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 1, | "in_standard_filter": 1, | ||||
"label": "Document Type", | "label": "Document Type", | ||||
@@ -124,6 +133,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -134,6 +144,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Is Standard", | "label": "Is Standard", | ||||
@@ -152,6 +163,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -163,6 +175,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 1, | "in_standard_filter": 1, | ||||
"label": "Module", | "label": "Module", | ||||
@@ -182,6 +195,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -192,6 +206,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"length": 0, | "length": 0, | ||||
@@ -209,6 +224,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -219,6 +235,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Send Alert On", | "label": "Send Alert On", | ||||
@@ -237,6 +254,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -249,6 +267,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Trigger Method", | "label": "Trigger Method", | ||||
@@ -267,6 +286,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -279,6 +299,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Reference Date", | "label": "Reference Date", | ||||
@@ -296,6 +317,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -309,6 +331,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Days Before or After", | "label": "Days Before or After", | ||||
@@ -326,6 +349,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -338,6 +362,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Value Changed", | "label": "Value Changed", | ||||
@@ -355,6 +380,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -365,6 +391,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"length": 0, | "length": 0, | ||||
@@ -382,6 +409,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -394,6 +422,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 1, | "ignore_xss_filter": 1, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Condition", | "label": "Condition", | ||||
@@ -411,6 +440,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -421,6 +451,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"length": 0, | "length": 0, | ||||
@@ -437,6 +468,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -447,6 +479,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"length": 0, | "length": 0, | ||||
@@ -464,6 +497,97 @@ | |||||
"unique": 0 | "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, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -474,6 +598,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Recipients", | "label": "Recipients", | ||||
@@ -491,6 +616,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -501,6 +627,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Recipients", | "label": "Recipients", | ||||
@@ -519,6 +646,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -529,6 +657,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Message", | "label": "Message", | ||||
@@ -546,18 +675,20 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
"columns": 0, | "columns": 0, | ||||
"default": "Add your message here", | "default": "Add your message here", | ||||
"depends_on": "", | |||||
"depends_on": "eval:!doc.is_standard", | |||||
"fieldname": "message", | "fieldname": "message", | ||||
"fieldtype": "Code", | "fieldtype": "Code", | ||||
"hidden": 0, | "hidden": 0, | ||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 1, | "ignore_xss_filter": 1, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Message", | "label": "Message", | ||||
@@ -575,6 +706,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -585,6 +717,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Attach Print", | "label": "Attach Print", | ||||
@@ -603,6 +736,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -613,6 +747,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Message Examples", | "label": "Message Examples", | ||||
@@ -631,6 +766,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -641,6 +777,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_global_search": 0, | |||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "View Properties (via Customize Form)", | "label": "View Properties (via Customize Form)", | ||||
@@ -659,19 +796,19 @@ | |||||
"unique": 0 | "unique": 0 | ||||
} | } | ||||
], | ], | ||||
"has_web_view": 0, | |||||
"hide_heading": 0, | "hide_heading": 0, | ||||
"hide_toolbar": 0, | "hide_toolbar": 0, | ||||
"icon": "fa fa-envelope", | "icon": "fa fa-envelope", | ||||
"idx": 0, | "idx": 0, | ||||
"image_view": 0, | "image_view": 0, | ||||
"in_create": 0, | "in_create": 0, | ||||
"in_dialog": 0, | |||||
"is_submittable": 0, | "is_submittable": 0, | ||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"menu_index": 0, | "menu_index": 0, | ||||
"modified": "2016-12-29 14:40:25.782293", | |||||
"modified": "2017-07-07 16:09:48.804218", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Email", | "module": "Email", | ||||
"name": "Email Alert", | "name": "Email Alert", | ||||
@@ -688,7 +825,6 @@ | |||||
"export": 1, | "export": 1, | ||||
"if_owner": 0, | "if_owner": 0, | ||||
"import": 0, | "import": 0, | ||||
"is_custom": 0, | |||||
"permlevel": 0, | "permlevel": 0, | ||||
"print": 0, | "print": 0, | ||||
"read": 1, | "read": 1, | ||||
@@ -703,6 +839,7 @@ | |||||
"quick_entry": 0, | "quick_entry": 0, | ||||
"read_only": 0, | "read_only": 0, | ||||
"read_only_onload": 0, | "read_only_onload": 0, | ||||
"show_name_in_global_search": 0, | |||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"title_field": "subject", | "title_field": "subject", | ||||
@@ -159,6 +159,11 @@ def get_context(context): | |||||
reference_name = doc.name, | reference_name = doc.name, | ||||
attachments = attachments) | 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): | def load_standard_properties(self, context): | ||||
module = get_doc_module(self.module, self.doctype, self.name) | module = get_doc_module(self.module, self.doctype, self.name) | ||||
if module: | if module: | ||||
@@ -7,6 +7,8 @@ import unittest | |||||
test_records = frappe.get_test_records('Email Alert') | test_records = frappe.get_test_records('Email Alert') | ||||
test_dependencies = ["User"] | |||||
class TestEmailAlert(unittest.TestCase): | class TestEmailAlert(unittest.TestCase): | ||||
def setUp(self): | def setUp(self): | ||||
frappe.db.sql("""delete from `tabEmail Queue`""") | 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", | self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Communication", | ||||
"reference_name": communication.name, "status":"Not Sent"})) | "reference_name": communication.name, "status":"Not Sent"})) | ||||
self.assertEquals(frappe.db.get_value('Communication', | |||||
communication.name, 'subject'), '__testing__') | |||||
def test_condition(self): | def test_condition(self): | ||||
event = frappe.new_doc("Event") | event = frappe.new_doc("Event") | ||||
event.subject = "test", | event.subject = "test", | ||||
@@ -137,7 +142,7 @@ class TestEmailAlert(unittest.TestCase): | |||||
event.save() | event.save() | ||||
# Value Change email alert alert will be trigger as description is not changed | # 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", | self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", | ||||
"reference_name": event.name, "status":"Not Sent"})) | "reference_name": event.name, "status":"Not Sent"})) | ||||
@@ -21,7 +21,9 @@ | |||||
"condition": "doc.communication_type=='Comment'", | "condition": "doc.communication_type=='Comment'", | ||||
"recipients": [ | "recipients": [ | ||||
{ "email_by_document_field": "owner" } | { "email_by_document_field": "owner" } | ||||
] | |||||
], | |||||
"set_property_after_alert": "subject", | |||||
"property_value": "__testing__" | |||||
}, | }, | ||||
{ | { | ||||
"doctype": "Email Alert", | "doctype": "Email Alert", | ||||
@@ -26,7 +26,7 @@ class EmailGroup(Document): | |||||
for user in frappe.db.get_all(doctype, [email_field, unsubscribed_field or "name"]): | for user in frappe.db.get_all(doctype, [email_field, unsubscribed_field or "name"]): | ||||
try: | 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: | if email: | ||||
frappe.get_doc({ | frappe.get_doc({ | ||||
"doctype": "Email Group Member", | "doctype": "Email Group Member", | ||||
@@ -1,5 +1,6 @@ | |||||
{ | { | ||||
"allow_copy": 0, | "allow_copy": 0, | ||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | "allow_import": 0, | ||||
"allow_rename": 0, | "allow_rename": 0, | ||||
"autoname": "hash", | "autoname": "hash", | ||||
@@ -14,6 +15,7 @@ | |||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -43,6 +45,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -72,6 +75,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -101,6 +105,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -129,6 +134,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -159,6 +165,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -187,6 +194,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -216,6 +224,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -245,6 +254,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -273,6 +283,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -303,6 +314,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -332,6 +344,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -362,6 +375,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -392,6 +406,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -421,6 +436,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -450,6 +466,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -477,20 +494,50 @@ | |||||
"search_index": 0, | "search_index": 0, | ||||
"set_only_once": 0, | "set_only_once": 0, | ||||
"unique": 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_heading": 0, | ||||
"hide_toolbar": 0, | "hide_toolbar": 0, | ||||
"icon": "fa fa-envelope", | "icon": "fa fa-envelope", | ||||
"idx": 1, | "idx": 1, | ||||
"image_view": 0, | "image_view": 0, | ||||
"in_create": 1, | "in_create": 1, | ||||
"in_dialog": 0, | |||||
"is_submittable": 0, | "is_submittable": 0, | ||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-02-24 17:42:10.878546", | |||||
"modified": "2017-07-07 16:29:15.780393", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Email", | "module": "Email", | ||||
"name": "Email Queue", | "name": "Email Queue", | ||||
@@ -70,8 +70,9 @@ class Newsletter(Document): | |||||
for file in files: | for file in files: | ||||
try: | 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: | except IOError: | ||||
frappe.throw(_("Unable to find attachment {0}").format(a)) | frappe.throw(_("Unable to find attachment {0}").format(a)) | ||||
@@ -2,7 +2,7 @@ | |||||
# MIT License. See license.txt | # MIT License. See license.txt | ||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe, re | |||||
import frappe, re, os | |||||
from frappe.utils.pdf import get_pdf | from frappe.utils.pdf import get_pdf | ||||
from frappe.email.smtp import get_outgoing_email_account | from frappe.email.smtp import get_outgoing_email_account | ||||
from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint, | 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]', | def get_email(recipients, sender='', msg='', subject='[No Subject]', | ||||
text_content = None, footer=None, print_html=None, formatted=None, attachments=None, | text_content = None, footer=None, print_html=None, formatted=None, attachments=None, | ||||
content=None, reply_to=None, cc=[], email_account=None, expose_recipients=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 | content = content or msg | ||||
emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, email_account=email_account, expose_recipients=expose_recipients) | emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, email_account=email_account, expose_recipients=expose_recipients) | ||||
if not content.strip().startswith("<"): | if not content.strip().startswith("<"): | ||||
content = markdown(content) | 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) | print_html=print_html, formatted=formatted, inline_images=inline_images) | ||||
if isinstance(attachments, dict): | if isinstance(attachments, dict): | ||||
attachments = [attachments] | attachments = [attachments] | ||||
for attach in (attachments or []): | for attach in (attachments or []): | ||||
# cannot attach if no filecontent | |||||
if attach.get('fcontent') is None: continue | |||||
emailobj.add_attachment(**attach) | emailobj.add_attachment(**attach) | ||||
return emailobj | return emailobj | ||||
@@ -58,18 +68,19 @@ class EMail: | |||||
self.expose_recipients = expose_recipients | self.expose_recipients = expose_recipients | ||||
self.msg_root = MIMEMultipart('mixed') | 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.cc = cc or [] | ||||
self.html_set = False | self.html_set = False | ||||
self.email_account = email_account or get_outgoing_email_account() | self.email_account = email_account or get_outgoing_email_account() | ||||
def set_html(self, message, text_content = None, footer=None, print_html=None, | 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""" | """Attach message in the html portion of multipart/alternative""" | ||||
if not formatted: | 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, | # this is the first html part of a multi-part message, | ||||
# convert to text well | # convert to text well | ||||
@@ -88,33 +99,33 @@ class EMail: | |||||
""" | """ | ||||
from email.mime.text import MIMEText | from email.mime.text import MIMEText | ||||
part = MIMEText(message, 'plain', 'utf-8') | part = MIMEText(message, 'plain', 'utf-8') | ||||
self.msg_multipart.attach(part) | |||||
self.msg_alternative.attach(part) | |||||
def set_part_html(self, message, inline_images): | def set_part_html(self, message, inline_images): | ||||
from email.mime.text import MIMEText | 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') | 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: | 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): | def set_html_as_text(self, html): | ||||
"""return html2text""" | |||||
"""Set plain text from HTML""" | |||||
self.set_text(to_markdown(html)) | self.set_text(to_markdown(html)) | ||||
def set_message(self, message, mime_type='text/html', as_attachment=0, filename='attachment.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]) | self.add_attachment(res[0], res[1]) | ||||
def add_attachment(self, fname, fcontent, content_type=None, | def add_attachment(self, fname, fcontent, content_type=None, | ||||
parent=None, content_id=None): | |||||
parent=None, content_id=None, inline=False): | |||||
"""add attachment""" | """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: | if not parent: | ||||
parent = self.msg_root | 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): | def add_pdf_attachment(self, name, html, options=None): | ||||
self.add_attachment(name, get_pdf(html, options), 'application/octet-stream') | self.add_attachment(name, get_pdf(html, options), 'application/octet-stream') | ||||
@@ -259,11 +233,12 @@ class EMail: | |||||
self.make() | self.make() | ||||
return self.msg_root.as_string() | 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: | if not email_account: | ||||
email_account = get_outgoing_email_account(False) | email_account = get_outgoing_email_account(False) | ||||
rendered_email = frappe.get_template("templates/emails/standard.html").render({ | rendered_email = frappe.get_template("templates/emails/standard.html").render({ | ||||
"header": get_header() if header else None, | |||||
"content": message, | "content": message, | ||||
"signature": get_signature(email_account), | "signature": get_signature(email_account), | ||||
"footer": get_footer(email_account, footer), | "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) | 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(): | def get_message_id(): | ||||
'''Returns Message ID created from doctype and name''' | '''Returns Message ID created from doctype and name''' | ||||
return "<{unique}@{site}>".format( | return "<{unique}@{site}>".format( | ||||
@@ -298,11 +319,100 @@ def get_footer(email_account, footer=None): | |||||
company_address = frappe.db.get_default("email_footer_address") | company_address = frappe.db.get_default("email_footer_address") | ||||
if company_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")): | if not cint(frappe.db.get_default("disable_standard_email_footer")): | ||||
for default_mail_footer in frappe.get_hooks("default_mail_footer"): | for default_mail_footer in frappe.get_hooks("default_mail_footer"): | ||||
footer += '<div style="margin: 15px auto;">{0}</div>'.format(default_mail_footer) | footer += '<div style="margin: 15px auto;">{0}</div>'.format(default_mail_footer) | ||||
return 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 | from six.moves import range | ||||
import frappe | import frappe | ||||
import HTMLParser | import HTMLParser | ||||
import smtplib, quopri | |||||
import smtplib, quopri, json | |||||
from frappe import msgprint, throw, _ | from frappe import msgprint, throw, _ | ||||
from frappe.email.smtp import SMTPServer, get_outgoing_email_account | 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 frappe.utils.verified_command import get_signed_params, verify_request | ||||
from html2text import html2text | from html2text import html2text | ||||
from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint | 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 rq.timeouts import JobTimeoutException | ||||
from frappe.utils.scheduler import log | from frappe.utils.scheduler import log | ||||
class EmailLimitCrossedError(frappe.ValidationError): pass | 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, | 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, | 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, | 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) | """Add email to sending queue (Email Queue) | ||||
:param recipients: List of recipients. | :param recipients: List of recipients. | ||||
:param sender: Email sender. | :param sender: Email sender. | ||||
:param subject: Email subject. | :param subject: Email subject. | ||||
:param message: Email message. | :param message: Email message. | ||||
:param text_content: Text version of email message. | |||||
:param reference_doctype: Reference DocType of caller document. | :param reference_doctype: Reference DocType of caller document. | ||||
:param reference_name: Reference name of caller document. | :param reference_name: Reference name of caller document. | ||||
:param send_priority: Priority for Email Queue, default 1. | :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 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 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 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: | if not unsubscribe_method: | ||||
unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe" | 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) | 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: | if reference_doctype and reference_name: | ||||
unsubscribed = [d.email for d in frappe.db.get_all("Email Unsubscribe", "email", | 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, | queue_separately=queue_separately, | ||||
is_notification = is_notification, | is_notification = is_notification, | ||||
inline_images = inline_images, | inline_images = inline_images, | ||||
header=header, | |||||
now=now) | now=now) | ||||
@@ -143,6 +149,14 @@ def get_email_queue(recipients, sender, subject, **kwargs): | |||||
'''Make Email Queue object''' | '''Make Email Queue object''' | ||||
e = frappe.new_doc('Email Queue') | e = frappe.new_doc('Email Queue') | ||||
e.priority = kwargs.get('send_priority') | 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: | try: | ||||
mail = get_email(recipients, | mail = get_email(recipients, | ||||
@@ -155,7 +169,8 @@ def get_email_queue(recipients, sender, subject, **kwargs): | |||||
cc=kwargs.get('cc'), | cc=kwargs.get('cc'), | ||||
email_account=kwargs.get('email_account'), | email_account=kwargs.get('email_account'), | ||||
expose_recipients=kwargs.get('expose_recipients'), | 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')) | mail.set_message_id(kwargs.get('message_id'),kwargs.get('is_notification')) | ||||
if kwargs.get('read_receipt'): | 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 | email = frappe.db.sql('''select | ||||
name, status, communication, message, sender, reference_doctype, | name, status, communication, message, sender, reference_doctype, | ||||
reference_name, unsubscribe_param, unsubscribe_method, expose_recipients, | reference_name, unsubscribe_param, unsubscribe_method, expose_recipients, | ||||
show_as_cc, add_unsubscribe_link | |||||
show_as_cc, add_unsubscribe_link, attachments | |||||
from | from | ||||
`tabEmail Queue` | `tabEmail Queue` | ||||
where | 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) | frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit) | ||||
if now: | if now: | ||||
print(frappe.get_traceback()) | |||||
raise e | raise e | ||||
else: | 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("<!--cc message-->", quopri.encodestring(email_sent_message)) | ||||
message = message.replace("<!--recipient-->", recipient) | 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(): | def clear_outbox(): | ||||
"""Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days. | """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)""" | frappe.db.sql("""delete from `tabEmail Queue Recipient` where parent in (%s)""" | ||||
% ','.join(['%s']*len(email_queues)), tuple(email_queues)) | % ','.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 doctype: `doctype` to be deleted | ||||
:param name: `name` of document to be deleted''' | :param name: `name` of document to be deleted''' | ||||
return self.post_request({ | return self.post_request({ | ||||
"cmd": "frappe.model.delete_doc", | |||||
"cmd": "frappe.client.delete", | |||||
"doctype": doctype, | "doctype": doctype, | ||||
"name": name | "name": name | ||||
}) | }) | ||||
@@ -272,6 +272,9 @@ def set_dropbox_access_token(access_token): | |||||
frappe.db.commit() | frappe.db.commit() | ||||
def generate_oauth2_access_token_from_oauth1_token(dropbox_settings=None): | 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" | url = "https://api.dropboxapi.com/2/auth/token/from_oauth1" | ||||
headers = {"Content-Type": "application/json"} | headers = {"Content-Type": "application/json"} | ||||
auth = (dropbox_settings["app_key"], dropbox_settings["app_secret"]) | 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)) | return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label)) | ||||
elif self.parentfield: | elif self.parentfield: | ||||
return "{}: {} {} #{}: {}: {}".format(_("Error"), frappe.bold(_(self.doctype)), | return "{}: {} {} #{}: {}: {}".format(_("Error"), frappe.bold(_(self.doctype)), | ||||
_("Row"), self.idx, _("Value missing for"), _(df.label)) | _("Row"), self.idx, _("Value missing for"), _(df.label)) | ||||
else: | else: | ||||
return "{}: {}: {}".format(_("Error"), _("Value missing for"), _(df.label)) | |||||
return _("Error: Value missing for {0}: {1}").format(_(df.parent), _(df.label)) | |||||
missing = [] | missing = [] | ||||
@@ -423,7 +423,6 @@ class DatabaseQuery(object): | |||||
def add_user_permissions(self, user_permissions, user_permission_doctypes=None): | 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) | user_permission_doctypes = frappe.permissions.get_user_permission_doctypes(user_permission_doctypes, user_permissions) | ||||
meta = frappe.get_meta(self.doctype) | meta = frappe.get_meta(self.doctype) | ||||
for doctypes in user_permission_doctypes: | for doctypes in user_permission_doctypes: | ||||
match_filters = {} | match_filters = {} | ||||
match_conditions = [] | match_conditions = [] | ||||
@@ -431,12 +430,18 @@ class DatabaseQuery(object): | |||||
for df in meta.get_fields_to_check_permissions(doctypes): | for df in meta.get_fields_to_check_permissions(doctypes): | ||||
user_permission_values = user_permissions.get(df.options, []) | 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: | 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, | 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_conditions.append("({condition})".format(condition=condition)) | ||||
match_filters[df.options] = user_permission_values | match_filters[df.options] = user_permission_values | ||||
@@ -43,6 +43,7 @@ type_map = { | |||||
,'Attach': ('text', '') | ,'Attach': ('text', '') | ||||
,'Attach Image':('text', '') | ,'Attach Image':('text', '') | ||||
,'Signature': ('longtext', '') | ,'Signature': ('longtext', '') | ||||
,'Color': ('varchar', varchar_len) | |||||
} | } | ||||
default_columns = ['name', 'creation', 'modified', 'modified_by', 'owner', | 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" % | if not frappe.db.sql("show index from `%s` where key_name = %s" % | ||||
(self.name, '%s'), col.fieldname): | (self.name, '%s'), col.fieldname): | ||||
query.append("add index `{}`(`{}`)".format(col.fieldname, col.fieldname)) | query.append("add index `{}`(`{}`)".format(col.fieldname, col.fieldname)) | ||||
for col in self.drop_index: | for col in self.drop_index: | ||||
if col.fieldname != 'name': # primary key | if col.fieldname != 'name': # primary key | ||||
# if index key exists | # 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) | frappe.throw(_("Fieldname {0} cannot have special characters like {1}").format(cstr(n), special_characters), InvalidColumnName) | ||||
return n | 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(): | def remove_all_foreign_keys(): | ||||
frappe.db.sql("set foreign_key_checks = 0") | frappe.db.sql("set foreign_key_checks = 0") | ||||
frappe.db.commit() | 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 `tabCustom Script` where dt = %s", name) | ||||
frappe.db.sql("delete from `tabProperty Setter` where doc_type = %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 `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) | 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 = ''): | def parse_naming_series(parts, doctype= '', doc = ''): | ||||
n = '' | n = '' | ||||
if isinstance(parts, basestring): | |||||
parts = parts.split('.') | |||||
series_set = False | series_set = False | ||||
today = now_datetime() | today = now_datetime() | ||||
for e in parts: | for e in parts: | ||||
@@ -142,6 +145,9 @@ def getseries(key, digits, doctype=''): | |||||
def revert_series_if_last(key, name): | def revert_series_if_last(key, name): | ||||
if ".#" in key: | if ".#" in key: | ||||
prefix, hashes = key.rsplit(".", 1) | prefix, hashes = key.rsplit(".", 1) | ||||
if '.' in prefix: | |||||
prefix = parse_naming_series(prefix.split('.')) | |||||
if "#" not in hashes: | if "#" not in hashes: | ||||
return | return | ||||
else: | else: | ||||
@@ -162,6 +162,7 @@ def update_link_field_values(link_fields, old, new, doctype): | |||||
single_doc.set(field['fieldname'], new) | single_doc.set(field['fieldname'], new) | ||||
# update single docs using ORM rather then query | # update single docs using ORM rather then query | ||||
# as single docs also sometimes sets defaults! | # as single docs also sometimes sets defaults! | ||||
single_doc.flags.ignore_mandatory = True | |||||
single_doc.save(ignore_permissions=True) | single_doc.save(ignore_permissions=True) | ||||
except ImportError: | except ImportError: | ||||
# fails in patches where the doctype has been renamed | # fails in patches where the doctype has been renamed | ||||
@@ -180,8 +180,8 @@ def load_doctype_module(doctype, module=None, prefix="", suffix=""): | |||||
try: | try: | ||||
if key not in doctype_python_modules: | if key not in doctype_python_modules: | ||||
doctype_python_modules[key] = frappe.get_module(module_name) | 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] | 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 | frappe.patches.v8_0.update_gender_and_salutation | ||||
execute:frappe.db.sql('update tabReport set module="Desk" where name="ToDo"') | 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.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 | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# See license.txt | # See license.txt | ||||
from __future__ import unicode_literals | |||||
from __future__ import unicode_literals, print_function | |||||
import frappe | import frappe | ||||
import unittest | import unittest | ||||
@@ -104,6 +104,7 @@ | |||||
"public/js/frappe/ui/page.html", | "public/js/frappe/ui/page.html", | ||||
"public/js/frappe/ui/page.js", | "public/js/frappe/ui/page.js", | ||||
"public/js/frappe/ui/find.js", | |||||
"public/js/frappe/ui/iconbar.js", | "public/js/frappe/ui/iconbar.js", | ||||
"public/js/frappe/form/layout.js", | "public/js/frappe/form/layout.js", | ||||
"public/js/frappe/ui/field_group.js", | "public/js/frappe/ui/field_group.js", | ||||
@@ -197,6 +198,8 @@ | |||||
"public/js/frappe/form/save.js", | "public/js/frappe/form/save.js", | ||||
"public/js/frappe/form/script_manager.js", | "public/js/frappe/form/script_manager.js", | ||||
"public/js/frappe/form/grid.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/linked_with.js", | ||||
"public/js/frappe/form/workflow.js", | "public/js/frappe/form/workflow.js", | ||||
"public/js/frappe/form/print.js", | "public/js/frappe/form/print.js", | ||||
@@ -995,3 +995,51 @@ input[type="checkbox"]:checked:before { | |||||
visibility: visible; | 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; | border-top: 1px solid #d1d8dd; | ||||
} | } | ||||
.form-message { | .form-message { | ||||
padding: 15px; | |||||
padding: 15px 30px; | |||||
border-bottom: 1px solid #d1d8dd; | |||||
} | } | ||||
.document-flow-wrapper { | .document-flow-wrapper { | ||||
padding: 40px 15px 30px; | padding: 40px 15px 30px; | ||||
@@ -73,21 +74,24 @@ | |||||
} | } | ||||
.form-dashboard { | .form-dashboard { | ||||
background-color: #fafbfc; | background-color: #fafbfc; | ||||
border-bottom: 1px solid #d1d8dd; | |||||
} | |||||
.form-dashboard-wrapper { | |||||
margin: -15px 0px; | |||||
} | } | ||||
.form-documents h6 { | .form-documents h6 { | ||||
margin-top: 15px; | margin-top: 15px; | ||||
} | } | ||||
.form-dashboard-section { | .form-dashboard-section { | ||||
margin: 0px -15px; | |||||
padding: 15px 30px; | padding: 15px 30px; | ||||
border-bottom: 1px solid #EBEFF2; | border-bottom: 1px solid #EBEFF2; | ||||
} | } | ||||
.form-dashboard-section:first-child { | |||||
padding-top: 0px; | |||||
} | |||||
.form-dashboard-section:last-child { | .form-dashboard-section:last-child { | ||||
border-bottom: none; | border-bottom: none; | ||||
} | } | ||||
.form-heatmap { | |||||
padding-top: 30px; | |||||
} | |||||
.form-heatmap .heatmap-message { | .form-heatmap .heatmap-message { | ||||
margin-top: 10px; | margin-top: 10px; | ||||
} | } | ||||
@@ -496,6 +500,7 @@ h6.uppercase, | |||||
} | } | ||||
.like-disabled-input.for-description { | .like-disabled-input.for-description { | ||||
font-weight: normal; | font-weight: normal; | ||||
font-size: 12px; | |||||
} | } | ||||
.frappe-control { | .frappe-control { | ||||
margin-bottom: 10px; | margin-bottom: 10px; | ||||
@@ -530,19 +535,17 @@ select.form-control { | |||||
font-weight: bold; | font-weight: bold; | ||||
background-color: #fffdf4; | 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 { | .delivery-status-indicator { | ||||
display: inline-block; | display: inline-block; | ||||
@@ -195,6 +195,26 @@ frappe.ellipsis = function(text, max) { | |||||
return text; | 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) { | frappe.get_modal = function(title, content) { | ||||
return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body); | return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body); | ||||
}; | }; | ||||
@@ -4,10 +4,11 @@ | |||||
frappe.ui.form.Dashboard = Class.extend({ | frappe.ui.form.Dashboard = Class.extend({ | ||||
init: function(opts) { | init: function(opts) { | ||||
$.extend(this, 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', | 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.progress_area = this.wrapper.find(".progress-area"); | ||||
this.heatmap_area = this.wrapper.find('.form-heatmap'); | this.heatmap_area = this.wrapper.find('.form-heatmap'); | ||||
this.chart_area = this.wrapper.find('.form-chart'); | this.chart_area = this.wrapper.find('.form-chart'); | ||||
@@ -18,7 +19,7 @@ frappe.ui.form.Dashboard = Class.extend({ | |||||
}, | }, | ||||
reset: function() { | reset: function() { | ||||
this.wrapper.addClass('hidden'); | |||||
this.section.addClass('hidden'); | |||||
this.clear_headline(); | this.clear_headline(); | ||||
// clear progress | // clear progress | ||||
@@ -36,13 +37,10 @@ frappe.ui.form.Dashboard = Class.extend({ | |||||
this.wrapper.find('.custom').remove(); | this.wrapper.find('.custom').remove(); | ||||
}, | }, | ||||
set_headline: function(html) { | set_headline: function(html) { | ||||
this.headline.html(html).removeClass('hidden'); | |||||
this.show(); | |||||
this.frm.layout.show_message(html); | |||||
}, | }, | ||||
clear_headline: function() { | clear_headline: function() { | ||||
if(this.headline) { | |||||
this.headline.empty().addClass('hidden'); | |||||
} | |||||
this.frm.layout.show_message(); | |||||
}, | }, | ||||
add_comment: function(text, permanent) { | add_comment: function(text, permanent) { | ||||
@@ -59,13 +57,12 @@ frappe.ui.form.Dashboard = Class.extend({ | |||||
this.clear_headline(); | 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(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 { | } else { | ||||
this.clear_headline(); | this.clear_headline(); | ||||
} | } | ||||
@@ -406,6 +403,6 @@ frappe.ui.form.Dashboard = Class.extend({ | |||||
} | } | ||||
}, | }, | ||||
show: function() { | 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'))); | out.push(me.get_version_comment(version, __('cancelled this document'))); | ||||
} | } | ||||
} else { | } else { | ||||
var df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname); | var df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname); | ||||
if(df && !df.hidden) { | if(df && !df.hidden) { | ||||
@@ -448,8 +448,8 @@ frappe.ui.form.Timeline = Class.extend({ | |||||
var parts = [], count = 0; | var parts = [], count = 0; | ||||
data.row_changed.every(function(row) { | data.row_changed.every(function(row) { | ||||
row[3].every(function(p) { | 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); | p[0], me.frm.docname); | ||||
if(df && !df.hidden) { | if(df && !df.hidden) { | ||||
@@ -73,7 +73,8 @@ | |||||
</a> | </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" | <a class="text-muted reply-link pull-right timeline-content-show" | ||||
data-name="{%= data.name %}">{%= __("Reply") %}</a> | data-name="{%= data.name %}">{%= __("Reply") %}</a> | ||||
{% } %} | {% } %} | ||||
@@ -110,7 +111,7 @@ | |||||
{% $.each(data.attachments, function(i, a) { %} | {% $.each(data.attachments, function(i, a) { %} | ||||
<div class="ellipsis"> | <div class="ellipsis"> | ||||
<a href="{%= encodeURI(a.file_url).replace(/#/g, \'%23\') %}" | <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> | <i class="fa fa-paperclip"></i> | ||||
{%= a.file_url.split("/").slice(-1)[0] %} | {%= a.file_url.split("/").slice(-1)[0] %} | ||||
{% if (a.is_private) { %} | {% if (a.is_private) { %} | ||||
@@ -53,6 +53,17 @@ frappe.form.formatters = { | |||||
Currency: function(value, docfield, options, doc) { | Currency: function(value, docfield, options, doc) { | ||||
var currency = frappe.meta.get_field_currency(docfield, doc); | var currency = frappe.meta.get_field_currency(docfield, doc); | ||||
var precision = docfield.precision || cint(frappe.boot.sysdefaults.currency_precision) || 2; | 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==="") | return frappe.form.formatters._right((value==null || value==="") | ||||
? "" : format_currency(value, currency, precision), options); | ? "" : format_currency(value, currency, precision), options); | ||||
}, | }, | ||||
@@ -108,6 +119,16 @@ frappe.form.formatters = { | |||||
return value || ""; | 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) { | Datetime: function(value) { | ||||
if(value) { | if(value) { | ||||
var m = moment(frappe.datetime.convert_to_user_tz(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) { | select_row: function(name) { | ||||
this.grid_rows_by_docname[name].select(); | this.grid_rows_by_docname[name].select(); | ||||
}, | }, | ||||
remove_all: function() { | |||||
this.grid_rows.forEach(row => { | |||||
row.remove(); | |||||
}); | |||||
}, | |||||
refresh_remove_rows_button: function() { | refresh_remove_rows_button: function() { | ||||
this.remove_rows_button.toggleClass('hide', | this.remove_rows_button.toggleClass('hide', | ||||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); | 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) { | if (this.frm && this.frm.docname) { | ||||
// use doc specific docfield object | // use doc specific docfield object | ||||
this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname, | this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname, | ||||
this.frm.docname); | |||||
this.frm.docname); | |||||
} else { | } else { | ||||
// use non-doc specific docfield | // use non-doc specific docfield | ||||
if(this.df.options) { | if(this.df.options) { | ||||
@@ -360,8 +365,19 @@ frappe.ui.form.Grid = Class.extend({ | |||||
get_docfield: function(fieldname) { | get_docfield: function(fieldname) { | ||||
return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null); | 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) { | get_field: function(fieldname) { | ||||
// Note: workaround for get_query | // 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) | && (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm) | ||||
&& !in_list(frappe.model.layout_fields, df.fieldtype)) { | && !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) { | if(df.columns) { | ||||
df.colsize=df.columns; | df.colsize=df.columns; | ||||
@@ -641,673 +657,4 @@ frappe.ui.form.Grid = Class.extend({ | |||||
// hide all custom buttons | // hide all custom buttons | ||||
this.grid_buttons.find('.btn-custom').addClass('hidden'); | 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); | $.extend(this, opts); | ||||
}, | }, | ||||
make: function() { | make: function() { | ||||
if(!this.parent && this.body) | |||||
if(!this.parent && this.body) { | |||||
this.parent = this.body; | this.parent = this.body; | ||||
} | |||||
this.wrapper = $('<div class="form-layout">').appendTo(this.parent); | this.wrapper = $('<div class="form-layout">').appendTo(this.parent); | ||||
this.message = $('<div class="form-message text-muted small hidden"></div>').appendTo(this.wrapper); | 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.fields = frappe.meta.sort_docfields(frappe.meta.docfield_map[this.doctype]); | ||||
} | |||||
this.setup_tabbing(); | this.setup_tabbing(); | ||||
this.render(); | this.render(); | ||||
}, | }, | ||||
@@ -44,29 +46,58 @@ frappe.ui.form.Layout = Class.extend({ | |||||
this.message.empty().addClass('hidden'); | this.message.empty().addClass('hidden'); | ||||
} | } | ||||
}, | }, | ||||
render: function() { | |||||
render: function(new_fields) { | |||||
var me = this; | var me = this; | ||||
var fields = new_fields || this.fields; | |||||
this.section = null; | this.section = null; | ||||
this.column = 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(); | 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.section && this.make_section(); | ||||
!this.column && this.make_column(); | !this.column && this.make_column(); | ||||
@@ -74,7 +105,8 @@ frappe.ui.form.Layout = Class.extend({ | |||||
df: df, | df: df, | ||||
doctype: this.doctype, | doctype: this.doctype, | ||||
parent: this.column.wrapper.get(0), | parent: this.column.wrapper.get(0), | ||||
frm: this.frm | |||||
frm: this.frm, | |||||
render_input: render | |||||
}); | }); | ||||
fieldobj.layout = this; | fieldobj.layout = this; | ||||
@@ -171,13 +203,14 @@ frappe.ui.form.Layout = Class.extend({ | |||||
var $this = $(this).removeClass("empty-section") | var $this = $(this).removeClass("empty-section") | ||||
.removeClass("visible-section") | .removeClass("visible-section") | ||||
.removeClass("shaded-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 | // nothing visible, hide the section | ||||
$(this).addClass("empty-section"); | |||||
$this.addClass("empty-section"); | |||||
} else { | } else { | ||||
$(this).addClass("visible-section"); | |||||
$this.addClass("visible-section"); | |||||
if(cnt % 2) { | if(cnt % 2) { | ||||
$(this).addClass("shaded-section"); | |||||
$this.addClass("shaded-section"); | |||||
} | } | ||||
cnt ++; | cnt ++; | ||||
} | } | ||||
@@ -201,6 +234,10 @@ frappe.ui.form.Layout = Class.extend({ | |||||
collapse = false; | collapse = false; | ||||
} | } | ||||
if(df.fieldname === '_form_dashboard') { | |||||
collapse = false; | |||||
} | |||||
section.collapse(collapse); | 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() { | refresh_section_count: function() { | ||||
this.wrapper.find(".section-count-label:visible").each(function(i) { | this.wrapper.find(".section-count-label:visible").each(function(i) { | ||||
$(this).html(i+1); | $(this).html(i+1); | ||||
@@ -241,7 +290,7 @@ frappe.ui.form.Layout = Class.extend({ | |||||
if(doctype) | if(doctype) | ||||
return me.handle_tab(doctype, fieldname, ev.shiftKey); | return me.handle_tab(doctype, fieldname, ev.shiftKey); | ||||
} | } | ||||
}) | |||||
}); | |||||
}, | }, | ||||
handle_tab: function(doctype, fieldname, shift) { | handle_tab: function(doctype, fieldname, shift) { | ||||
var me = this, | var me = this, | ||||
@@ -264,7 +313,7 @@ frappe.ui.form.Layout = Class.extend({ | |||||
if(fields[i].df.fieldname==fieldname) { | if(fields[i].df.fieldname==fieldname) { | ||||
if(shift) { | if(shift) { | ||||
if(prev) { | if(prev) { | ||||
this.set_focus(prev) | |||||
this.set_focus(prev); | |||||
} else { | } else { | ||||
$(this.primary_button).focus(); | $(this.primary_button).focus(); | ||||
} | } | ||||
@@ -290,7 +339,7 @@ frappe.ui.form.Layout = Class.extend({ | |||||
// last row, close it and find next field | // last row, close it and find next field | ||||
grid_row.toggle_view(false, function() { | grid_row.toggle_view(false, function() { | ||||
grid_row.grid.frm.layout.handle_tab(grid_row.grid.df.parent, grid_row.grid.df.fieldname); | grid_row.grid.frm.layout.handle_tab(grid_row.grid.df.parent, grid_row.grid.df.fieldname); | ||||
}) | |||||
}); | |||||
} else { | } else { | ||||
// next row | // next row | ||||
grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true); | 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) { | 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) { | set_focus: function(field) { | ||||
// next is table, show the table | // next is table, show the table | ||||
@@ -450,17 +499,20 @@ frappe.ui.form.Section = Class.extend({ | |||||
.appendTo(this.layout.page); | .appendTo(this.layout.page); | ||||
this.layout.sections.push(this); | this.layout.sections.push(this); | ||||
var section = this.wrapper[0]; | |||||
if(this.df) { | if(this.df) { | ||||
if(this.df.label) { | if(this.df.label) { | ||||
this.make_head(); | this.make_head(); | ||||
} | } | ||||
if(this.df.description) { | if(this.df.description) { | ||||
$('<div class="col-sm-12 small text-muted form-section-description">' + __(this.df.description) + '</div>') | $('<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 | // for bc | ||||
this.body = $('<div class="section-body">').appendTo(this.wrapper); | this.body = $('<div class="section-body">').appendTo(this.wrapper); | ||||
}, | }, | ||||
@@ -469,7 +521,7 @@ frappe.ui.form.Section = Class.extend({ | |||||
if(!this.df.collapsible) { | if(!this.df.collapsible) { | ||||
$('<div class="col-sm-12"><h6 class="form-section-heading uppercase">' | $('<div class="col-sm-12"><h6 class="form-section-heading uppercase">' | ||||
+ __(this.df.label) + '</h6></div>') | + __(this.df.label) + '</h6></div>') | ||||
.appendTo(this.wrapper); | |||||
.appendTo(this.wrapper); | |||||
} else { | } else { | ||||
this.head = $('<div class="section-head"><a class="h6 uppercase">' | 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); | +__(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; | return missing_mandatory; | ||||
} | } | ||||
}) | |||||
}); | |||||
frappe.ui.form.Column = Class.extend({ | frappe.ui.form.Column = Class.extend({ | ||||
init: function(section, df) { | init: function(section, df) { | ||||
@@ -538,7 +590,7 @@ frappe.ui.form.Column = Class.extend({ | |||||
</form>\ | </form>\ | ||||
</div>').appendTo(this.section.body) | </div>').appendTo(this.section.body) | ||||
.find("form") | .find("form") | ||||
.on("submit", function() { return false; }) | |||||
.on("submit", function() { return false; }); | |||||
if(this.df.label) { | if(this.df.label) { | ||||
$('<label class="control-label">'+ __(this.df.label) | $('<label class="control-label">'+ __(this.df.label) | ||||
@@ -557,4 +609,4 @@ frappe.ui.form.Column = Class.extend({ | |||||
refresh: function() { | refresh: function() { | ||||
this.section.refresh(); | this.section.refresh(); | ||||
} | } | ||||
}) | |||||
}); |
@@ -1,20 +1,37 @@ | |||||
frappe.provide('frappe.ui.form'); | 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({ | frappe.ui.form.QuickEntryForm = Class.extend({ | ||||
init: function(doctype, success_function){ | |||||
init: function(doctype, after_insert){ | |||||
this.doctype = doctype; | 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); | 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) { | 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(); | this.validate_for_prompt_autoname(); | ||||
return true; | |||||
}, | }, | ||||
validate_mandatory_length: function(){ | |||||
too_many_mandatory_fields: function(){ | |||||
if(this.mandatory.length > 7) { | if(this.mandatory.length > 7) { | ||||
// too many fields, show form | // too many fields, show form | ||||
frappe.set_route('Form', this.doctype, this.doc.name); | |||||
return true; | 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! | // has mandatory table, quit! | ||||
frappe.set_route('Form', this.doctype, this.doc.name); | |||||
return true; | return true; | ||||
} | } | ||||
return false; | |||||
}, | }, | ||||
validate_for_prompt_autoname: function(){ | 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.dialog.show(); | ||||
this.set_defaults(); | this.set_defaults(); | ||||
}, | }, | ||||
@@ -93,44 +111,62 @@ frappe.ui.form.QuickEntryForm = Class.extend({ | |||||
register_primary_action: function(){ | register_primary_action: function(){ | ||||
var me = this; | var me = this; | ||||
this.dialog.set_primary_action(__('Save'), function() { | this.dialog.set_primary_action(__('Save'), function() { | ||||
if(me.dialog.working) return; | |||||
if(me.dialog.working) { | |||||
return; | |||||
} | |||||
var data = me.dialog.get_values(); | var data = me.dialog.get_values(); | ||||
if(data) { | if(data) { | ||||
me.dialog.working = true; | 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(){ | update_doc: function(){ | ||||
var me = this; | var me = this; | ||||
var data = this.dialog.get_values(true); | 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.") | console.log("Already saving. Please wait a few moments.") | ||||
throw "saving"; | throw "saving"; | ||||
} | } | ||||
frappe.ui.form.remove_old_form_route(); | |||||
frappe.ui.form.is_saving = true; | frappe.ui.form.is_saving = true; | ||||
return frappe.call({ | 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) { | if (frappe._from_link && newdoc.doctype === frappe._from_link.df.options) { | ||||
var doc = frappe.get_doc(frappe._from_link.doctype, frappe._from_link.docname); | var doc = frappe.get_doc(frappe._from_link.doctype, frappe._from_link.docname); | ||||
// set value | // set value | ||||
@@ -226,8 +239,11 @@ frappe.ui.form.update_calling_link = function (newdoc) { | |||||
// if from form, switch | // if from form, switch | ||||
if (frappe._from_link.frm) { | 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; | 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({ | frappe.ui.form.ScriptManager = Class.extend({ | ||||
@@ -64,32 +64,84 @@ frappe.ui.form.ScriptManager = Class.extend({ | |||||
$.extend(this, opts); | $.extend(this, opts); | ||||
}, | }, | ||||
make: function(ControllerClass) { | 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; | doctype = doctype || this.frm.doctype; | ||||
name = name || this.frm.docname; | 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); | 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]) { | if(frappe.ui.form.handlers[doctype] && frappe.ui.form.handlers[doctype][event_name]) { | ||||
$.each(frappe.ui.form.handlers[doctype][event_name], function(i, fn) { | $.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]) { | 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]) { | 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; | return handlers; | ||||
}, | }, | ||||
@@ -105,7 +157,7 @@ frappe.ui.form.ScriptManager = Class.extend({ | |||||
if(doctype.__custom_js) { | if(doctype.__custom_js) { | ||||
try { | try { | ||||
eval(doctype.__custom_js) | |||||
eval(doctype.__custom_js); | |||||
} catch(e) { | } catch(e) { | ||||
frappe.msgprint({ | frappe.msgprint({ | ||||
title: __('Error in Custom Script'), | 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 class="progress-area hidden form-dashboard-section"> | ||||
</div> | </div> | ||||
<div class="form-heatmap hidden form-dashboard-section"> | <div class="form-heatmap hidden form-dashboard-section"> | ||||
@@ -1,9 +1,8 @@ | |||||
<div class="form-documents"> | <div class="form-documents"> | ||||
<h5 style="margin: 5px 0px;">{{__("Related Documents")}}</h5> | |||||
{% for (var i=0; i < transactions.length; i++) { %} | {% for (var i=0; i < transactions.length; i++) { %} | ||||
{% if((i % 2)===0) { %}<div class="row">{% } %} | {% if((i % 2)===0) { %}<div class="row">{% } %} | ||||
<div class="col-xs-6"> | <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++) { | {% for (var j=0; j < transactions[i].items.length; j++) { | ||||
var doctype = transactions[i].items[j]; %} | var doctype = transactions[i].items[j]; %} | ||||
<div class="document-link" | <div class="document-link" | ||||