diff --git a/.eslintrc b/.eslintrc index c8efd4375e..44af7b458f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -117,6 +117,7 @@ "set_field_options": true, "getCookie": true, "getCookies": true, - "get_url_arg": true + "get_url_arg": true, + "QUnit": true } } diff --git a/.travis.yml b/.travis.yml index 0b9f8293df..30b21ecf18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,5 @@ language: python dist: trusty -group: deprecated-2017Q2 - -python: - - "2.7" addons: apt: @@ -12,14 +8,14 @@ addons: packages: - google-chrome-stable +python: + - "2.7" + services: - mysql -before_install: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start - install: + - sudo rm /etc/apt/sources.list.d/docker.list - sudo apt-get purge -y mysql-common mysql-server mysql-client - nvm install v7.10.0 - wget https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py @@ -31,18 +27,28 @@ install: - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ before_script: + - wget http://chromedriver.storage.googleapis.com/2.27/chromedriver_linux64.zip + - unzip chromedriver_linux64.zip + - sudo apt-get install libnss3 + - sudo apt-get --only-upgrade install google-chrome-stable + - sudo cp chromedriver /usr/local/bin/. + - sudo chmod +x /usr/local/bin/chromedriver + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + - sleep 3 - mysql -u root -ptravis -e 'create database test_frappe' - echo "USE mysql;\nCREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe';\nFLUSH PRIVILEGES;\n" | mysql -u root -ptravis - echo "USE mysql;\nGRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost';\n" | mysql -u root -ptravis - + - cd ~/frappe-bench - bench use test_site - bench reinstall --yes + - bench scheduler disable - bench start & - sleep 10 script: - set -e - bench --verbose run-tests - - bench reinstall --yes - - bench run-ui-tests --ci + - sleep 5 + - bench --verbose run-ui-tests --app frappe diff --git a/frappe/__init__.py b/frappe/__init__.py index 49a924b7a9..9b31e15fee 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -12,9 +12,9 @@ import os, sys, importlib, inspect, json # public from .exceptions import * -from .utils.jinja import get_jenv, get_template, render_template +from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template -__version__ = '8.2.2' +__version__ = '8.4.0' __title__ = "Frappe Framework" local = Local() @@ -138,8 +138,7 @@ def init(site, sites_path=None, new_site=False): local.module_app = None local.app_modules = None - local.system_settings = None - local.system_country = None + local.system_settings = _dict() local.user = None local.user_perms = None @@ -381,7 +380,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message attachments=None, content=None, doctype=None, name=None, reply_to=None, cc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False, - inline_images=None): + inline_images=None, template=None, args=None, header=False): """Send email using user's default **Email Account** or global default **Email Account**. @@ -404,7 +403,15 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message :param expose_recipients: Display all recipients in the footer message - "This email was sent to" :param communication: Communication link to be set in Email Queue record :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id + :param template: Name of html template from templates/emails folder + :param args: Arguments for rendering the template + :param header: Append header in email """ + + text_content = None + if template: + message, text_content = get_email_from_template(template, args) + message = content or message if as_markdown: @@ -416,13 +423,13 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message import email.queue email.queue.send(recipients=recipients, sender=sender, - subject=subject, message=message, + subject=subject, message=message, text_content=text_content, reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message, attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, in_reply_to=in_reply_to, send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification, - inline_images=inline_images) + inline_images=inline_images, header=header) whitelisted = [] guest_methods = [] @@ -1364,7 +1371,7 @@ def get_active_domains(): return active_domains -def get_system_country(): - if local.system_country is None: - local.system_country = db.get_single_value('System Settings', 'country') or '' - return local.system_country +def get_system_settings(key): + if not local.system_settings.has_key(key): + local.system_settings.update({key: db.get_single_value('System Settings', key)}) + return local.system_settings.get(key) diff --git a/frappe/build.js b/frappe/build.js index 50e3d7ee8c..be24a6b0cd 100644 --- a/frappe/build.js +++ b/frappe/build.js @@ -272,6 +272,7 @@ function watch_js(ondirty) { if (sources.includes(filename)) { pack(target, sources); ondirty && ondirty(target); + // break; } } }); diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 550073f663..74da084beb 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -298,11 +298,13 @@ def console(context): @click.option('--doctype', help="For DocType") @click.option('--test', multiple=True, help="Specific test") @click.option('--driver', help="For Travis") +@click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests") @click.option('--module', help="Run tests in a module") @click.option('--profile', is_flag=True, default=False) @click.option('--junit-xml-output', help="Destination file path for junit xml report") @pass_context -def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None, profile=False, junit_xml_output=False): +def run_tests(context, app=None, module=None, doctype=None, test=(), + driver=None, profile=False, junit_xml_output=False, ui_tests = False): "Run tests" import frappe.test_runner tests = test @@ -311,7 +313,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None frappe.init(site=site) ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, - force=context.force, profile=profile, junit_xml_output=junit_xml_output) + force=context.force, profile=profile, junit_xml_output=junit_xml_output, + ui_tests = ui_tests) if len(ret.failures) == 0 and len(ret.errors) == 0: ret = 0 @@ -320,30 +323,24 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None @click.command('run-ui-tests') @click.option('--app', help="App to run tests on, leave blank for all apps") -@click.option('--ci', is_flag=True, default=False, help="Run in CI environment") +@click.option('--test', help="File name of the test you want to run") +@click.option('--profile', is_flag=True, default=False) @pass_context -def run_ui_tests(context, app=None, ci=False): +def run_ui_tests(context, app=None, test=False, profile=False): "Run UI tests" - import subprocess + import frappe.test_runner site = get_site(context) frappe.init(site=site) + frappe.connect() - if app is None: - app = ",".join(frappe.get_installed_apps()) - - cmd = [ - './node_modules/.bin/nightwatch', - '--config', './apps/frappe/frappe/nightwatch.js', - '--app', app, - '--site', site - ] - - if ci: - cmd.extend(['--env', 'ci_server']) + ret = frappe.test_runner.run_ui_tests(app=app, test=test, verbose=context.verbose, + profile=profile) + if len(ret.failures) == 0 and len(ret.errors) == 0: + ret = 0 - bench_path = frappe.utils.get_bench_path() - subprocess.call(cmd, cwd=bench_path) + if os.environ.get('CI'): + sys.exit(ret) @click.command('serve') @click.option('--port', default=8000) diff --git a/frappe/contacts/doctype/address/address.js b/frappe/contacts/doctype/address/address.js index f20093a21f..7809f426ea 100644 --- a/frappe/contacts/doctype/address/address.js +++ b/frappe/contacts/doctype/address/address.js @@ -30,5 +30,12 @@ frappe.ui.form.on("Address", { frappe.model.remove_from_locals(d.link_doctype, d.link_name); }); } + }, + after_save: function() { + var last_route = frappe.route_history.slice(-2, -1)[0]; + if(frappe.dynamic_link && frappe.dynamic_link.doc + && frappe.dynamic_link.doc.name == last_route[2]){ + frappe.set_route(last_route[0], last_route[1], last_route[2]); + } } }); diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 4446805952..5455e77468 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -185,6 +185,13 @@ def get_shipping_address(company): address_as_dict = address[0] name, address_template = get_address_templates(address_as_dict) return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict) + +def get_company_address(company): + ret = frappe._dict() + ret.company_address = get_default_address('Company', company) + ret.company_address_display = get_address_display(ret.company_address) + + return ret def address_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond diff --git a/frappe/contacts/doctype/contact/contact_list.js b/frappe/contacts/doctype/contact/contact_list.js new file mode 100644 index 0000000000..a93b3f0d73 --- /dev/null +++ b/frappe/contacts/doctype/contact/contact_list.js @@ -0,0 +1,3 @@ +frappe.listview_settings['Contact'] = { + add_fields: ["image"], +}; \ No newline at end of file diff --git a/frappe/contacts/doctype/contact/test_records.json b/frappe/contacts/doctype/contact/test_records.json index 95b267ebaf..e028c366f3 100644 --- a/frappe/contacts/doctype/contact/test_records.json +++ b/frappe/contacts/doctype/contact/test_records.json @@ -1,6 +1,7 @@ [ { "doctype": "Contact", + "salutation": "Mr", "email_id": "test_conctact@example.com", "first_name": "_Test Contact For _Test Customer", "is_primary_contact": 1, diff --git a/frappe/contacts/doctype/salutation/test_records.json b/frappe/contacts/doctype/salutation/test_records.json new file mode 100644 index 0000000000..3a87fffff0 --- /dev/null +++ b/frappe/contacts/doctype/salutation/test_records.json @@ -0,0 +1,8 @@ +[ + { + "salutation": "Mr" + }, + { + "salutation": "Mrs" + } +] \ No newline at end of file diff --git a/frappe/core/doctype/authentication_log/test_authentication_log.py b/frappe/core/doctype/authentication_log/test_authentication_log.py index 58c230184d..2a95a77254 100644 --- a/frappe/core/doctype/authentication_log/test_authentication_log.py +++ b/frappe/core/doctype/authentication_log/test_authentication_log.py @@ -13,13 +13,12 @@ class TestAuthenticationLog(unittest.TestCase): from frappe.auth import LoginManager, CookieManager # test user login log - frappe.local.form_dict = { 'cmd': 'login' } - - frappe.form_dict = { + frappe.local.form_dict = frappe._dict({ + 'cmd': 'login', 'sid': 'Guest', 'pwd': 'admin', 'usr': 'Administrator' - } + }) frappe.local.cookie_manager = CookieManager() frappe.local.login_manager = LoginManager() @@ -38,9 +37,11 @@ class TestAuthenticationLog(unittest.TestCase): auth_log = self.get_auth_log() self.assertEquals(auth_log.status, 'Failed') + frappe.local.form_dict = frappe._dict() + def get_auth_log(self, operation='Login'): - names = frappe.db.sql_list("""select name from `tabAuthentication Log` - where user='Administrator' and operation='{operation}' order by + names = frappe.db.sql_list("""select name from `tabAuthentication Log` + where user='Administrator' and operation='{operation}' order by creation desc""".format(operation=operation)) name = names[0] diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 5f686e7ac9..dc435fe4f7 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -11,8 +11,10 @@ "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, + "engine": "InnoDB", "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -41,6 +43,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 1, "collapsible": 0, @@ -73,6 +76,7 @@ "width": "163" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 1, "collapsible": 0, @@ -92,7 +96,7 @@ "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nButton\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", + "options": "Attach\nAttach Image\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", "permlevel": 0, "print_hide": 0, "print_hide_if_no_value": 0, @@ -105,6 +109,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 1, "collapsible": 0, @@ -135,6 +140,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -167,6 +173,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -198,6 +205,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -228,6 +236,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -260,6 +269,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -290,6 +300,7 @@ "width": "70px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -319,6 +330,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -349,6 +361,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -378,6 +391,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -408,6 +422,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -438,6 +453,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -465,6 +481,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -496,6 +513,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -526,6 +544,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -554,6 +573,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -584,6 +604,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -616,6 +637,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -646,6 +668,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -675,6 +698,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -704,6 +728,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -734,6 +759,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -761,6 +787,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -794,6 +821,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -823,6 +851,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -855,6 +884,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -887,6 +917,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -917,6 +948,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -947,6 +979,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -975,6 +1008,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1007,6 +1041,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1039,6 +1074,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1071,6 +1107,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1101,6 +1138,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1129,6 +1167,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1161,6 +1200,7 @@ "width": "50px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1192,6 +1232,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1219,6 +1260,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1251,6 +1293,7 @@ "width": "300px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1280,6 +1323,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1319,7 +1363,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2017-04-21 16:56:04.023296", + "modified": "2017-07-06 12:36:21.248293", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 60cf53ef22..f4563876ed 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -14,7 +14,7 @@ from frappe.model.document import Document from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.desk.notifications import delete_notification_count_for from frappe.modules import make_boilerplate -from frappe.model.db_schema import validate_column_name +from frappe.model.db_schema import validate_column_name, validate_column_length import frappe.website.render class InvalidFieldNameError(frappe.ValidationError): pass @@ -78,7 +78,7 @@ class DocType(Document): if not [d.fieldname for d in self.fields if d.in_list_view]: cnt = 0 for d in self.fields: - if d.reqd and not d.hidden: + if d.reqd and not d.hidden and not d.fieldtype == "Table": d.in_list_view = 1 cnt += 1 if cnt == 4: break @@ -385,9 +385,10 @@ def validate_fields(meta): 1. There are no illegal characters in fieldnames 2. If fieldnames are unique. - 3. Fields that do have database columns are not mandatory. - 4. `Link` and `Table` options are valid. - 5. **Hidden** and **Mandatory** are not set simultaneously. + 3. Validate column length. + 4. Fields that do have database columns are not mandatory. + 5. `Link` and `Table` options are valid. + 6. **Hidden** and **Mandatory** are not set simultaneously. 7. `Check` type field has default as 0 or 1. 8. `Dynamic Links` are correctly defined. 9. Precision is set in numeric fields and is between 1 & 6. @@ -406,6 +407,9 @@ def validate_fields(meta): if len(duplicates) > 1: frappe.throw(_("Fieldname {0} appears multiple times in rows {1}").format(fieldname, ", ".join(duplicates))) + def check_fieldname_length(fieldname): + validate_column_length(fieldname) + def check_illegal_mandatory(d): if (d.fieldtype in no_value_fields) and d.fieldtype!="Table" and d.reqd: frappe.throw(_("Field {0} of type {1} cannot be mandatory").format(d.label, d.fieldtype)) @@ -581,7 +585,6 @@ def validate_fields(meta): frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname), InvalidFieldNameError) - fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -598,6 +601,7 @@ def validate_fields(meta): d.fieldname = d.fieldname.lower() check_illegal_characters(d.fieldname) check_unique_fieldname(d.fieldname) + check_fieldname_length(d.fieldname) check_illegal_mandatory(d) check_link_table_options(d) check_dynamic_link_options(d) @@ -766,3 +770,10 @@ def init_list(doctype): doc = frappe.get_meta(doctype) make_boilerplate("controller_list.js", doc) make_boilerplate("controller_list.html", doc) + +def check_if_fieldname_conflicts_with_methods(doctype, fieldname): + doc = frappe.get_doc({"doctype": doctype}) + method_list = [method for method in dir(doc) if isinstance(method, str) and callable(getattr(doc, method))] + + if fieldname in method_list: + frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname)) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 6151890183..7c18d5fd27 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -49,7 +49,7 @@ class Report(Document): delete_custom_role('report', self.name) def set_doctype_roles(self): - if not self.get('roles'): + if not self.get('roles') and self.is_standard == 'No': meta = frappe.get_meta(self.ref_doctype) roles = [{'role': d.role} for d in meta.permissions if d.permlevel==0] self.set('roles', roles) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 6649aad4f2..7f2ab54e0c 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -810,7 +810,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "eg. If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User", + "description": "If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User", "fieldname": "ignore_user_permissions_if_missing", "fieldtype": "Check", "hidden": 0, @@ -835,6 +835,38 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User", + "fieldname": "apply_strict_user_permissions", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Apply Strict User Permissions", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, diff --git a/frappe/core/doctype/test_runner/__init__.py b/frappe/core/doctype/test_runner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/test_runner/test_runner.js b/frappe/core/doctype/test_runner/test_runner.js new file mode 100644 index 0000000000..f7d4128c50 --- /dev/null +++ b/frappe/core/doctype/test_runner/test_runner.js @@ -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(); + $("

Loading...

").appendTo(wrapper); + + // all tests + frappe.call({ + method: 'frappe.core.doctype.test_runner.test_runner.get_test_js' + }).always((data) => { + $("
").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 + $('
').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'); + }); + }); + + } +}); diff --git a/frappe/core/doctype/test_runner/test_runner.json b/frappe/core/doctype/test_runner/test_runner.json new file mode 100644 index 0000000000..8396d5df43 --- /dev/null +++ b/frappe/core/doctype/test_runner/test_runner.json @@ -0,0 +1,122 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-06-26 10:57:19.976624", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "module_path", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Module Path", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "output", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Output", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2017-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 +} \ No newline at end of file diff --git a/frappe/core/doctype/test_runner/test_runner.py b/frappe/core/doctype/test_runner/test_runner.py new file mode 100644 index 0000000000..a59ddc69a5 --- /dev/null +++ b/frappe/core/doctype/test_runner/test_runner.py @@ -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 diff --git a/frappe/core/doctype/user/test_records.json b/frappe/core/doctype/user/test_records.json index 3f5dd87e55..93fcca5517 100644 --- a/frappe/core/doctype/user/test_records.json +++ b/frappe/core/doctype/user/test_records.json @@ -31,6 +31,13 @@ "new_password": "Eastern_43A1W", "enabled": 1 }, + { + "doctype": "User", + "email": "test3@example.com", + "first_name": "_Test3", + "new_password": "Eastern_43A1W", + "enabled": 1 + }, { "doctype": "User", "email": "testperm@example.com", diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index aca7bcab3d..31714b7116 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1949,7 +1949,7 @@ "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 0, + "read_only": 1, "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 4ad45849fd..d2343672fb 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -225,11 +225,11 @@ class User(Document): def password_reset_mail(self, link): self.send_login_mail(_("Password Reset"), - "templates/emails/password_reset.html", {"link": link}, now=True) + "password_reset", {"link": link}, now=True) def password_update_mail(self, password): self.send_login_mail(_("Password Update"), - "templates/emails/password_update.html", {"new_password": password}, now=True) + "password_update", {"new_password": password}, now=True) def send_welcome_mail_to_user(self): from frappe.utils import get_url @@ -248,7 +248,7 @@ class User(Document): else: subject = _("Complete Registration") - self.send_login_mail(subject, "templates/emails/new_user.html", + self.send_login_mail(subject, "new_user", dict( link=link, site_url=get_url(), @@ -279,7 +279,7 @@ class User(Document): sender = frappe.session.user not in STANDARD_USERS and get_formatted_email(frappe.session.user) or None frappe.sendmail(recipients=self.email, sender=sender, subject=subject, - message=frappe.get_template(template).render(args), + template=template, args=args, delayed=(not now) if now!=None else self.flags.delay_emails, retry=3) def a_system_manager_should_exist(self): @@ -579,7 +579,7 @@ def update_password(new_password, key=None, old_password=None): def test_password_strength(new_password, key=None, old_password=None, user_data=[]): from frappe.utils.password_strength import test_password_strength as _test_password_strength - password_policy = frappe.db.get_value("System Settings", None, + password_policy = frappe.db.get_value("System Settings", None, ["enable_password_policy", "minimum_password_score"], as_dict=True) or {} enable_password_policy = cint(password_policy.get("enable_password_policy", 0)) @@ -589,7 +589,7 @@ def test_password_strength(new_password, key=None, old_password=None, user_data= return {} if not user_data: - user_data = frappe.db.get_value('User', frappe.session.user, + user_data = frappe.db.get_value('User', frappe.session.user, ['first_name', 'middle_name', 'last_name', 'email', 'birth_date']) if new_password: diff --git a/frappe/core/page/permission_manager/permission_manager_help.html b/frappe/core/page/permission_manager/permission_manager_help.html index 9c88fbcd01..d2f4136082 100644 --- a/frappe/core/page/permission_manager/permission_manager_help.html +++ b/frappe/core/page/permission_manager/permission_manager_help.html @@ -36,6 +36,6 @@
  • {%= __("Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.") %}
  • {%= __("If these instructions where not helpful, please add in your suggestions on GitHub Issues.") %} - {%= __("Submit an Issue") %} + {%= __("Submit an Issue") %}

    diff --git a/frappe/custom/doctype/custom_field/custom_field.js b/frappe/custom/doctype/custom_field/custom_field.js index a74e7bd640..c430d12752 100644 --- a/frappe/custom/doctype/custom_field/custom_field.js +++ b/frappe/custom/doctype/custom_field/custom_field.js @@ -11,7 +11,7 @@ frappe.ui.form.on('Custom Field', { ['DocType', 'issingle', '=', 0], ]; if(frappe.session.user!=="Administrator") { - filters.push(['DocType', 'module', '!=', 'Core']) + filters.push(['DocType', 'module', 'not in', ['Core', 'Custom']]) } return { "filters": filters diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 1d74bbd87c..a4bec7ec1f 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -11,6 +11,7 @@ "doctype": "DocType", "document_type": "Setup", "editable_grid": 0, + "engine": "InnoDB", "fields": [ { "allow_bulk_edit": 0, @@ -219,7 +220,7 @@ "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nButton\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", + "options": "Attach\nAttach Image\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", "permlevel": 0, "print_hide": 0, "print_hide_if_no_value": 0, @@ -1160,7 +1161,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-13 09:52:49.692096", + "modified": "2017-07-06 17:23:43.835189", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index 57ea7a2b0b..2d0ad96c26 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -39,6 +39,10 @@ class CustomField(Document): if not self.fieldname: frappe.throw(_("Fieldname not set for Custom Field")) + if not self.flags.ignore_validate: + from frappe.core.doctype.doctype.doctype import check_if_fieldname_conflicts_with_methods + check_if_fieldname_conflicts_with_methods(self.dt, self.fieldname) + def on_update(self): frappe.clear_cache(doctype=self.dt) if not self.flags.ignore_validate: diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 77b489241b..ea9835525b 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -15,7 +15,7 @@ frappe.ui.form.on("Customize Form", { ['DocType', 'custom', '=', 0], ['DocType', 'name', 'not in', 'DocType, DocField, DocPerm, User, Role, Has Role, \ Page, Has Role, Module Def, Print Format, Report, Customize Form, \ - Customize Form Field'], + Customize Form Field, Property Setter, Custom Field, Custom Script'], ['DocType', 'restrict_to_domain', 'in', frappe.boot.active_domains] ] }; diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index e42f617a39..085e632545 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -68,6 +68,8 @@ allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Da ('Text', 'Data'), ('Text', 'Text Editor', 'Code', 'Signature'), ('Data', 'Select'), ('Text', 'Small Text')) +allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select',) + class CustomizeForm(Document): def on_update(self): frappe.db.sql("delete from tabSingles where doctype='Customize Form'") @@ -197,6 +199,10 @@ class CustomizeForm(Document): frappe.msgprint(_("You cannot unset 'Read Only' for field {0}").format(df.label)) continue + elif property == "options" and df.get("fieldtype") not in allowed_fieldtype_for_options_change: + frappe.msgprint(_("You can't set 'Options' for field {0}").format(df.label)) + continue + self.make_property_setter(property=property, value=df.get(property), property_type=docfield_properties[property], fieldname=df.fieldname) diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index ffa82c3b00..87f0a6324f 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -165,22 +165,22 @@ class TestCustomizeForm(unittest.TestCase): df = d.get("fields", {"fieldname": "title"})[0] # invalid fieldname - df.options = """{doc_type} - {introduction_test}""" + df.default = """{doc_type} - {introduction_test}""" self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") # space in formatter - df.options = """{doc_type} - {introduction text}""" + df.default = """{doc_type} - {introduction text}""" self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") # valid fieldname - df.options = """{doc_type} - {introduction_text}""" + df.default = """{doc_type} - {introduction_text}""" d.run_method("save_customization") # valid fieldname with escaped curlies - df.options = """{{ {doc_type} }} - {introduction_text}""" + df.default = """{{ {doc_type} }} - {introduction_text}""" d.run_method("save_customization") # undo - df.options = None + df.default = None d.run_method("save_customization") diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index f6c2d91aac..4b9ab79eaa 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -94,7 +94,7 @@ "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nButton\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nText\nText Editor\nTime", + "options": "Attach\nAttach Image\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nText\nText Editor\nTime", "permlevel": 0, "print_hide": 0, "print_hide_if_no_value": 0, @@ -1202,7 +1202,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2017-04-21 17:02:14.903382", + "modified": "2017-07-06 17:24:03.665171", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json index 41de06dbee..75e949e90f 100644 --- a/frappe/desk/doctype/event/event.json +++ b/frappe/desk/doctype/event/event.json @@ -895,8 +895,8 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-05-01 15:27:39.217961", - "modified_by": "vartakashwini@gmail.com", + "modified": "2017-07-06 12:37:44.036819", + "modified_by": "Administrator", "module": "Desk", "name": "Event", "owner": "Administrator", diff --git a/frappe/desk/doctype/todo/todo.json b/frappe/desk/doctype/todo/todo.json index 95a664215e..487dcbd3d8 100644 --- a/frappe/desk/doctype/todo/todo.json +++ b/frappe/desk/doctype/todo/todo.json @@ -14,6 +14,7 @@ "engine": "InnoDB", "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -42,6 +43,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -72,6 +74,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -83,9 +86,9 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, - "in_global_search": 1, + "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, + "in_standard_filter": 1, "label": "Priority", "length": 0, "no_copy": 0, @@ -104,6 +107,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -131,6 +135,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -143,7 +148,7 @@ "in_filter": 0, "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, + "in_standard_filter": 1, "label": "Due Date", "length": 0, "no_copy": 0, @@ -161,6 +166,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -190,6 +196,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -219,6 +226,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -251,6 +259,7 @@ "width": "300px" }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -279,6 +288,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -310,6 +320,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -341,6 +352,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -368,6 +380,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -399,6 +412,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -428,6 +442,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -458,6 +473,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -498,7 +514,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-03-08 14:39:02.027528", + "modified": "2017-07-13 17:44:54.369254", "modified_by": "Administrator", "module": "Desk", "name": "ToDo", diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index 48cacab3f6..4efcf30a7a 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -65,7 +65,7 @@ class FormMeta(Meta): def _get_path(fname): return os.path.join(path, scrub(fname)) - system_country = frappe.get_system_country() + system_country = frappe.get_system_settings("country") self._add_code(_get_path(self.name + '.js'), '__js') if system_country: @@ -82,6 +82,7 @@ class FormMeta(Meta): self.add_code_via_hook("doctype_js", "__js") self.add_code_via_hook("doctype_list_js", "__list_js") self.add_code_via_hook("doctype_tree_js", "__tree_js") + self.add_code_via_hook("doctype_calendar_js", "__calendar_js") self.add_custom_script() self.add_html_templates(path) diff --git a/frappe/desk/page/applications/application_row.html b/frappe/desk/page/applications/application_row.html index 3d04c40349..bac68e2d60 100644 --- a/frappe/desk/page/applications/application_row.html +++ b/frappe/desk/page/applications/application_row.html @@ -5,7 +5,7 @@
    {{ __("Website") }} + href="{{ app.app_url }}" target="_blank" rel="noopener noreferrer">{{ __("Website") }} {% if (app.installed) { %}
    ', {html:html})) }, show_working: function() { + $('header').find('.setup-wizard-brand').hide(); this.hide_current_slide(); frappe.set_route(this.page_name); this.current_slide = {"$wrapper": this.get_message(this.working_html()).appendTo(this.parent)}; @@ -96,7 +100,7 @@ frappe.wiz.Wizard = Class.extend({ this.update_values(); if(!this.slide_dict[id]) { - this.slide_dict[id] = new frappe.wiz.WizardSlide($.extend(this.slides[id], {wiz:this, id:id})); + this.slide_dict[id] = new frappe.setup.WizardSlide($.extend(this.slides[id], {wiz:this, id:id})); this.slide_dict[id].make(); } @@ -147,8 +151,8 @@ frappe.wiz.Wizard = Class.extend({ args: {args: this.values}, callback: function(r) { me.show_complete(); - if(frappe.wiz.welcome_page) { - localStorage.setItem("session_last_route", frappe.wiz.welcome_page); + if(frappe.setup.welcome_page) { + localStorage.setItem("session_last_route", frappe.setup.welcome_page); } setTimeout(function() { window.location = "/desk"; @@ -181,26 +185,27 @@ frappe.wiz.Wizard = Class.extend({ this.update_values(); - frappe.wiz.slides = []; - frappe.wiz.run_event("before_load"); + frappe.setup.slides = []; + frappe.setup.run_event("before_load"); // remove slides listed in remove_app_slides var new_slides = []; - frappe.wiz.slides.forEach(function(slide) { - if(frappe.wiz.domain) { + frappe.setup.slides.forEach(function(slide) { + if(frappe.setup.domain) { var domains = slide.domains; if (domains.indexOf('all') !== -1 || - domains.indexOf(frappe.wiz.domain.toLowerCase()) !== -1) { + domains.indexOf(frappe.setup.domain.toLowerCase()) !== -1) { new_slides.push(slide); } } else { new_slides.push(slide); } }) - frappe.wiz.slides = new_slides; - this.slides = frappe.wiz.slides; - frappe.wiz.run_event("after_load"); + frappe.setup.slides = new_slides; + + this.slides = frappe.setup.slides; + frappe.setup.run_event("after_load"); // re-render all slides this.slide_dict = {}; @@ -213,7 +218,7 @@ frappe.wiz.Wizard = Class.extend({ } }); -frappe.wiz.WizardSlide = Class.extend({ +frappe.setup.WizardSlide = Class.extend({ init: function(opts) { $.extend(this, opts); this.$wrapper = $('') @@ -224,6 +229,24 @@ frappe.wiz.WizardSlide = Class.extend({ var me = this; if(this.$body) this.$body.remove(); + var fields = JSON.parse(JSON.stringify(this.fields)); + + if(this.add_more) { + this.count = 1; + fields = fields.map((field, i) => { + if(field.fieldname) { + field.fieldname += '_1'; + } + if(i === 1 && this.mandatory_entry) { + field.reqd = 1; + } + if(!field.static) { + if(field.label) field.label += ' 1'; + } + return field; + }); + } + if(this.before_load) { this.before_load(this); } @@ -234,7 +257,6 @@ frappe.wiz.WizardSlide = Class.extend({ main_title:__(this.wiz.title), step: this.id + 1, name: this.name, - css_class: this.css_class || "", slides_count: this.wiz.slides.length })).appendTo(this.$wrapper); @@ -242,7 +264,7 @@ frappe.wiz.WizardSlide = Class.extend({ if(this.fields) { this.form = new frappe.ui.FieldGroup({ - fields: this.fields, + fields: fields, body: this.body, no_submit_on_enter: true }); @@ -251,18 +273,36 @@ frappe.wiz.WizardSlide = Class.extend({ $(this.body).html(this.html); } + this.set_reqd_fields(); this.set_init_values(); this.make_prev_next_buttons(); + if(this.add_more) this.bind_more_button(); + + var $primary_btn = this.$next ? this.$next : this.$complete; + + this.bind_fields_to_next($primary_btn); if(this.onload) { this.onload(this); } - this.focus_first_input(); + this.set_reqd_fields(); + this.bind_fields_to_next($primary_btn); + this.reset_next($primary_btn); + this.focus_first_input(); + }, + set_reqd_fields: function() { + var dict = this.form.fields_dict; + this.reqd_fields = []; + Object.keys(dict).map(key => { + if(dict[key].df.reqd) { + this.reqd_fields.push(dict[key]); + } + }); }, set_init_values: function() { var me = this; - // set values from frappe.wiz.values + // set values from frappe.setup.values if(frappe.wizard.values && this.fields) { this.fields.forEach(function(f) { var value = frappe.wizard.values[f.fieldname]; @@ -284,6 +324,25 @@ frappe.wiz.WizardSlide = Class.extend({ return true; }, + bind_more_button: function() { + this.$more = this.$body.find('.more-btn'); + this.$more.removeClass('hide') + .on('click', () => { + this.count++; + var fields = JSON.parse(JSON.stringify(this.fields)); + this.form.add_fields(fields.map(field => { + if(field.fieldname) field.fieldname += '_' + this.count; + if(!field.static) { + if(field.label) field.label += ' ' + this.count; + } + return field; + })); + if(this.count === this.max_count) { + this.$more.addClass('hide'); + } + }); + }, + make_prev_next_buttons: function() { var me = this; @@ -311,7 +370,7 @@ frappe.wiz.WizardSlide = Class.extend({ .click(this.next_or_complete.bind(this)); } - //setup mousefree navigation + // setup mousefree navigation this.$body.on('keypress', function(e) { if(e.which === 13) { var $target = $(e.target); @@ -326,6 +385,14 @@ frappe.wiz.WizardSlide = Class.extend({ } }); }, + bind_fields_to_next: function($primary_btn) { + var me = this; + this.reqd_fields.map((field) => { + field.$wrapper.on('change input', () => { + me.reset_next($primary_btn); + }); + }); + }, next_or_complete: function() { if(this.set_values()) { if(this.id+1 < this.wiz.slides.length) { @@ -335,6 +402,17 @@ frappe.wiz.WizardSlide = Class.extend({ } } }, + reset_next: function($primary_btn) { + var empty_fields = this.reqd_fields.filter((field) => { + return !field.get_value(); + }) + + if(empty_fields.length) { + $primary_btn.addClass('disabled'); + } else { + $primary_btn.removeClass('disabled'); + } + }, focus_first_input: function() { setTimeout(function() { this.$body.find('.form-control').first().focus(); @@ -360,233 +438,310 @@ frappe.wiz.WizardSlide = Class.extend({ }, }); -function load_frappe_slides() { - // language selection - frappe.wiz.welcome = { +var frappe_slides = [ + { + // Welcome (language) slide name: "welcome", domains: ["all"], - title: __("Welcome"), + title: __("Hello!"), icon: "fa fa-world", help: __("Let's prepare the system for first use."), fields: [ - { fieldname: "language", label: __("Select Your Language"), reqd:1, - fieldtype: "Select", "default": "english" }, + { fieldname: "language", label: __("Your Language"), + fieldtype: "Select", reqd: 1} ], onload: function(slide) { - if (!frappe.wiz.welcome.data) { - frappe.wiz.welcome.load_languages(slide); + if (frappe.setup.data.lang) { + this.setup_fields(slide); } else { - frappe.wiz.welcome.setup_fields(slide); + utils.load_languages(slide, this.setup_fields); } }, - css_class: "single-column", - load_languages: function(slide) { - frappe.call({ - method: "frappe.desk.page.setup_wizard.setup_wizard.load_languages", - freeze: true, - callback: function(r) { - frappe.wiz.welcome.data = r.message; - frappe.wiz.welcome.setup_fields(slide); - - var language_field = slide.get_field("language"); - language_field.set_input(frappe.wiz.welcome.data.default_language || "english"); - - if (!frappe.wiz._from_load_messages) { - language_field.$input.trigger("change"); - } - - delete frappe.wiz._from_load_messages; - - moment.locale("en"); - } - }); - }, - setup_fields: function(slide) { - var select = slide.get_field("language"); - select.df.options = frappe.wiz.welcome.data.languages; - select.refresh(); - frappe.wiz.welcome.bind_events(slide); + utils.setup_language_field(slide); + utils.bind_language_events(slide); }, - - bind_events: function(slide) { - slide.get_input("language").unbind("change").on("change", function() { - var lang = $(this).val() || "english"; - frappe._messages = {}; - frappe.call({ - method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages", - freeze: true, - args: { - language: lang - }, - callback: function(r) { - frappe.wiz._from_load_messages = true; - frappe.wizard.refresh_slides(); - } - }); - }); - } }, - // region selection - frappe.wiz.region = { + { + // Region slide + name: 'region', domains: ["all"], - title: __("Region"), + title: __("Select Your Region"), icon: "fa fa-flag", help: __("Select your Country, Time Zone and Currency"), fields: [ - { fieldname: "country", label: __("Country"), reqd:1, + { fieldname: "country", label: __("Your Country"), reqd:1, fieldtype: "Select" }, + { fieldtype: "Section Break" }, { fieldname: "timezone", label: __("Time Zone"), reqd:1, fieldtype: "Select" }, + { fieldtype: "Column Break" }, { fieldname: "currency", label: __("Currency"), reqd:1, - fieldtype: "Select" }, + fieldtype: "Select" } ], onload: function(slide) { - var _setup = function() { - frappe.wiz.region.setup_fields(slide); - frappe.wiz.region.bind_events(slide); - }; - - if(frappe.wiz.regional_data) { - _setup(); + if(frappe.setup.data.regional_data) { + this.setup_fields(slide); } else { - frappe.call({ - method:"frappe.geo.country_info.get_country_timezone_info", - callback: function(data) { - frappe.wiz.regional_data = data.message; - _setup(); - } - }); + utils.load_regional_data(slide, this.setup_fields); } }, - css_class: "single-column", + setup_fields: function(slide) { - var data = frappe.wiz.regional_data; + utils.setup_region_fields(slide); + utils.bind_region_events(slide); + } + }, - slide.get_input("country").empty() - .add_options([""].concat(Object.keys(data.country_info).sort())); + { + // Profile slide + name: 'user', + domains: ["all"], + title: __("The First User: You"), + icon: "fa fa-user", + fields: [ + { "fieldtype":"Attach Image", "fieldname":"attach_user_image", + label: __("Attach Your Picture"), is_private: 0}, + { "fieldname": "full_name", "label": __("Full Name"), "fieldtype": "Data", + reqd:1}, + { "fieldname": "email", "label": __("Email Address") + ' (' + __("Will be your login ID") + ')', + "fieldtype": "Data", "options":"Email"}, + { "fieldname": "password", "label": __("Password"), "fieldtype": "Password" } + ], + help: __('The first user will become the System Manager (you can change this later).'), + onload: function(slide) { + if(frappe.session.user!=="Administrator") { + slide.form.fields_dict.email.$wrapper.toggle(false); + slide.form.fields_dict.password.$wrapper.toggle(false); + // remove password field + delete slide.form.fields_dict.password; - slide.get_input("currency").empty() - .add_options(frappe.utils.unique([""].concat($.map(data.country_info, - function(opts, country) { return opts.currency; }))).sort()); + if(frappe.boot.user.first_name || frappe.boot.user.last_name) { + slide.form.fields_dict.full_name.set_input( + [frappe.boot.user.first_name, frappe.boot.user.last_name].join(' ').trim()); + } - slide.get_input("timezone").empty() - .add_options([""].concat(data.all_timezones)); + var user_image = frappe.get_cookie("user_image"); + var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper; - // set values if present - if(frappe.wizard.values.country) { - slide.get_field("country").set_input(frappe.wizard.values.country); - } else if (data.default_country) { - slide.get_field("country").set_input(data.default_country); - } + if(user_image) { + $attach_user_image.find(".missing-image").toggle(false); + $attach_user_image.find("img").attr("src", decodeURIComponent(user_image)); + $attach_user_image.find(".img-container").toggle(true); + } + delete slide.form.fields_dict.email; - if(frappe.wizard.values.currency) { - slide.get_field("currency").set_input(frappe.wizard.values.currency); - } + } else { + slide.form.fields_dict.email.df.reqd = 1; + slide.form.fields_dict.email.refresh(); + slide.form.fields_dict.password.df.reqd = 1; + slide.form.fields_dict.password.refresh(); - if(frappe.wizard.values.timezone) { - slide.get_field("timezone").set_input(frappe.wizard.values.timezone); + utils.load_user_details(slide, this.setup_fields); } + }, + setup_fields: function(slide) { + if(frappe.setup.data.full_name) { + slide.form.fields_dict.full_name.set_input(frappe.setup.data.full_name); + } + if(frappe.setup.data.email) { + let email = frappe.setup.data.email; + slide.form.fields_dict.email.set_input(email); + if (frappe.get_gravatar(email, 200)) { + var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper; + $attach_user_image.find(".missing-image").toggle(false); + $attach_user_image.find("img").attr("src", frappe.get_gravatar(email, 200)); + $attach_user_image.find(".img-container").toggle(true); + } + } }, + }, +]; - bind_events: function(slide) { - slide.get_input("country").on("change", function() { - var country = slide.get_input("country").val(); - var $timezone = slide.get_input("timezone"); - var data = frappe.wiz.regional_data; +var utils = { + load_languages: function(slide, callback) { + frappe.call({ + method: "frappe.desk.page.setup_wizard.setup_wizard.load_languages", + freeze: true, + callback: function(r) { + frappe.setup.data.lang = r.message; + callback(slide); - $timezone.empty(); + var language_field = slide.get_field("language"); - // add country specific timezones first - if(country) { - var timezone_list = data.country_info[country].timezones || []; - $timezone.add_options(timezone_list.sort()); - slide.get_field("currency").set_input(data.country_info[country].currency); - slide.get_field("currency").$input.trigger("change"); + language_field.set_input(frappe.setup.data.default_language || "English"); + + if (!frappe.setup._from_load_messages) { + language_field.$input.trigger("change"); } + delete frappe.setup._from_load_messages; + moment.locale("en"); + } + }); + }, + + load_regional_data: function(slide, callback) { + frappe.call({ + method:"frappe.geo.country_info.get_country_timezone_info", + callback: function(data) { + frappe.setup.data.regional_data = data.message; + callback(slide); + } + }); + }, + + load_user_details: function(slide, callback) { + frappe.call({ + method: "frappe.desk.page.setup_wizard.setup_wizard.load_user_details", + freeze: true, + callback: function(r) { + frappe.setup.data.full_name = r.message.full_name; + frappe.setup.data.email = r.message.email; + callback(slide); + } + }) + }, - // add all timezones at the end, so that user has the option to change it to any timezone - $timezone.add_options([""].concat(data.all_timezones)); + setup_language_field: function(slide) { + var language_field = slide.get_field("language"); + language_field.df.options = frappe.setup.data.lang.languages; + language_field.refresh(); + }, - slide.get_field("timezone").set_input($timezone.val()); + setup_region_fields: function(slide) { + /* + Set a slide's country, timezone and currency fields + */ + var data = frappe.setup.data.regional_data; - // temporarily set date format - frappe.boot.sysdefaults.date_format = (data.country_info[country].date_format - || "dd-mm-yyyy"); - }); + var country_field = slide.get_field('country'); - slide.get_input("currency").on("change", function() { - var currency = slide.get_input("currency").val(); - if (!currency) return; - frappe.model.with_doc("Currency", currency, function() { - frappe.provide("locals.:Currency." + currency); - var currency_doc = frappe.model.get_doc("Currency", currency); - var number_format = currency_doc.number_format; - if (number_format==="#.###") { - number_format = "#.###,##"; - } else if (number_format==="#,###") { - number_format = "#,###.##" - } + slide.get_input("country").empty() + .add_options([""].concat(Object.keys(data.country_info).sort())); - frappe.boot.sysdefaults.number_format = number_format; - locals[":Currency"][currency] = $.extend({}, currency_doc); - }); - }); + slide.get_input("currency").empty() + .add_options(frappe.utils.unique([""].concat($.map(data.country_info, + function(opts, country) { return opts.currency; }))).sort()); + + slide.get_input("timezone").empty() + .add_options([""].concat(data.all_timezones)); + + // set values if present + if(frappe.wizard.values.country) { + country_field.set_input(frappe.wizard.values.country); + } else if (data.default_country) { + country_field.set_input(data.default_country); } - }, + if(frappe.wizard.values.currency) { + slide.get_field("currency").set_input(frappe.wizard.values.currency); + } - frappe.wiz.user = { - domains: ["all"], - title: __("The First User: You"), - icon: "fa fa-user", - fields: [ - {"fieldname": "full_name", "label": __("Full Name"), "fieldtype": "Data", - reqd:1}, - {"fieldname": "email", "label": __("Email Address"), "fieldtype": "Data", - reqd:1, "description": __("Login id"), "options":"Email"}, - {"fieldname": "password", "label": __("Password"), "fieldtype": "Password", - reqd:1}, - {fieldtype:"Attach Image", fieldname:"attach_user", - label: __("Attach Your Picture"), is_private: 0}, - ], - help: __('The first user will become the System Manager (you can change this later).'), - onload: function(slide) { - if(frappe.session.user!=="Administrator") { - slide.form.fields_dict.password.$wrapper.toggle(false); - slide.form.fields_dict.email.$wrapper.toggle(false); - if(frappe.boot.user.first_name || frappe.boot.user.last_name) { - slide.form.fields_dict.full_name.set_input( - [frappe.boot.user.first_name, frappe.boot.user.last_name].join(' ').trim()); + if(frappe.wizard.values.timezone) { + slide.get_field("timezone").set_input(frappe.wizard.values.timezone); + } + + country_field.df.description = 'fetching country...'; + country_field.set_description(); + + // get location from IP (unreliable) + frappe.call({ + method:"frappe.desk.page.setup_wizard.setup_wizard.load_country", + callback: function(r) { + if(r.message) { + slide.get_field("country").set_input(r.message); + slide.get_input("country").trigger('change'); } + country_field.df.description = ''; + country_field.set_description(); + } + }); + }, - var user_image = frappe.get_cookie("user_image"); - if(user_image) { - var $attach_user = slide.form.fields_dict.attach_user.$wrapper; - $attach_user.find(".missing-image").toggle(false); - $attach_user.find("img").attr("src", decodeURIComponent(user_image)).toggle(true); + bind_language_events: function(slide) { + slide.get_input("language").unbind("change").on("change", function() { + var lang = $(this).val() || "English"; + frappe._messages = {}; + frappe.call({ + method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages", + freeze: true, + args: { + language: lang + }, + callback: function(r) { + frappe.setup._from_load_messages = true; + frappe.wizard.refresh_slides(); } + }); + }); + }, - delete slide.form.fields_dict.email; - delete slide.form.fields_dict.password; + bind_region_events: function(slide) { + /* + Bind a slide's country, timezone and currency fields + */ + slide.get_input("country").on("change", function() { + var country = slide.get_input("country").val(); + var $timezone = slide.get_input("timezone"); + var data = frappe.setup.data.regional_data; + + $timezone.empty(); + + // add country specific timezones first + if(country) { + var timezone_list = data.country_info[country].timezones || []; + $timezone.add_options(timezone_list.sort()); + slide.get_field("currency").set_input(data.country_info[country].currency); + slide.get_field("currency").$input.trigger("change"); } - }, - css_class: "single-column" - }; + + // add all timezones at the end, so that user has the option to change it to any timezone + $timezone.add_options([""].concat(data.all_timezones)); + + slide.get_field("timezone").set_input($timezone.val()); + + // temporarily set date format + frappe.boot.sysdefaults.date_format = (data.country_info[country].date_format + || "dd-mm-yyyy"); + }); + + slide.get_input("currency").on("change", function() { + var currency = slide.get_input("currency").val(); + if (!currency) return; + frappe.model.with_doc("Currency", currency, function() { + frappe.provide("locals.:Currency." + currency); + var currency_doc = frappe.model.get_doc("Currency", currency); + var number_format = currency_doc.number_format; + if (number_format==="#.###") { + number_format = "#.###,##"; + } else if (number_format==="#,###") { + number_format = "#,###.##" + } + + frappe.boot.sysdefaults.number_format = number_format; + locals[":Currency"][currency] = $.extend({}, currency_doc); + }); + }); + }, + } -frappe.wiz.on("before_load", function() { - load_frappe_slides(); +frappe.setup.on("before_load", function() { + // load slides + frappe_slides.map(frappe.setup.add_slide); - // add welcome slide - frappe.wiz.add_slide(frappe.wiz.welcome); - frappe.wiz.add_slide(frappe.wiz.region); - frappe.wiz.add_slide(frappe.wiz.user); + // set header image + let $icon = $('header .setup-wizard-brand'); + if($icon.length === 0) { + $('header').append(`
    +
    `); + } }); diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 9a11894ce0..8e8fef3359 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -179,11 +179,27 @@ def load_messages(language): @frappe.whitelist() def load_languages(): + language_codes = frappe.db.sql('select language_code, language_name from tabLanguage order by name', as_dict=True) + codes_to_names = {} + for d in language_codes: + codes_to_names[d.language_code] = d.language_name return { "default_language": frappe.db.get_value('Language', frappe.local.lang, 'language_name') or frappe.local.lang, - "languages": sorted(frappe.db.sql_list('select language_name from tabLanguage order by name')) + "languages": sorted(frappe.db.sql_list('select language_name from tabLanguage order by name')), + "codes_to_names": codes_to_names } +@frappe.whitelist() +def load_country(): + from frappe.sessions import get_geo_ip_country + return get_geo_ip_country(frappe.local.request_ip) if frappe.local.request_ip else None + +@frappe.whitelist() +def load_user_details(): + return { + "full_name": frappe.cache().hget("full_name", "signup"), + "email": frappe.cache().hget("email", "signup") + } def prettify_args(args): # remove attachments diff --git a/frappe/desk/page/setup_wizard/setup_wizard_page.html b/frappe/desk/page/setup_wizard/setup_wizard_page.html index f52301b149..565dab563a 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard_page.html +++ b/frappe/desk/page/setup_wizard/setup_wizard_page.html @@ -1,14 +1,18 @@ -
    +
    {% for (var i=0; i < slides_count; i++) { %} - + + + + {% } %}
    -

    {%= title %}

    +

    {%= title %}

    - \ -
    ').appendTo(this.parent) +
    ').appendTo(this.parent); }, set_input_areas: function() { this.label_area = this.label_span = this.$wrapper.find(".label-area").get(0); @@ -840,11 +972,11 @@ frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({ this._super(); this.$input.removeClass("form-control"); }, - parse: function(value) { - return this.input.checked ? 1 : 0; + get_input_value: function() { + return this.input && this.input.checked ? 1 : 0; }, - validate: function(value, callback) { - return callback(cint(value)); + validate: function(value) { + return cint(value); }, set_input: function(value) { if(this.input) { @@ -853,14 +985,7 @@ frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({ this.last_value = value; this.set_mandatory(value); this.set_disp_area(); - }, - get_value: function() { - if (!this.$input) { - return; - } - - return this.$input.prop("checked") ? 1 : 0; - }, + } }); frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({ @@ -878,7 +1003,7 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({ }, onclick: function() { if(this.frm && this.frm.doc) { - if(this.frm.script_manager.get_handlers(this.df.fieldname, this.doctype, this.docname).length) { + if(this.frm.script_manager.has_handlers(this.df.fieldname, this.doctype)) { this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname); } else { this.frm.runscript(this.df.options, this); @@ -925,7 +1050,7 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({ this.$value.find(".close").on("click", function() { me.clear_attachment(); - }) + }); }, clear_attachment: function() { var me = this; @@ -976,7 +1101,7 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({ var attachments = this.frm && this.frm.attachments.get_attachments() || []; var select = this.dialog.get_field("select"); if(attachments.length) { - attachments = $.map(attachments, function(o) { return o.file_url; }) + attachments = $.map(attachments, function(o) { return o.file_url; }); select.df.options = [""].concat(attachments); select.toggle(true); this.dialog.get_field("or_attach").toggle(true); @@ -1023,7 +1148,7 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({ onerror: function() { me.dialog.hide(); } - } + }; if ("is_private" in this.df) { this.upload_options.is_private = this.df.is_private; @@ -1034,20 +1159,20 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({ from_form: 1, doctype: this.frm.doctype, docname: this.frm.docname - } + }; } else { this.upload_options.on_attach = function(fileobj, dataurl) { me.dialog.hide(); me.fileobj = fileobj; me.dataurl = dataurl; if(me.on_attach) { - me.on_attach() + me.on_attach(); } if(me.df.on_attach) { me.df.on_attach(fileobj, dataurl); } me.on_upload_complete(); - } + }; } }, @@ -1094,31 +1219,55 @@ frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({ make: function() { var me = this; this._super(); - this.img_wrapper = $('
    \ -
    ') + this.img_wrapper = $('
    \ +
    ') .appendTo(this.wrapper); - this.img = $("") - .appendTo(this.img_wrapper).toggle(false); + + this.img_container = $(`
    `); + this.img = $(``) + .appendTo(this.img_container); + + this.img_overlay = $(`
    + Change +
    `).appendTo(this.img_container); + + this.remove_image_link = $('Remove'); + + this.img_wrapper.append(this.img_container).append(this.remove_image_link); + // this.img.toggle(false); + // this.img_overlay.toggle(false); + this.img_container.toggle(false); + this.remove_image_link.toggle(false); // propagate click to Attach button this.img_wrapper.find(".missing-image").on("click", function() { me.$input.click(); }); - this.img.on("click", function() { me.$input.click(); }); + this.img_container.on("click", function() { me.$input.click(); }); + this.remove_image_link.on("click", function() { me.$value.find(".close").click(); }); - this.$wrapper.on("refresh", function() { - me.set_image(); - if(me.get_status()=="Read") { - $(me.disp_area).toggle(false); - } - }); this.set_image(); }, + refresh_input: function() { + this._super(); + $(this.wrapper).find('.btn-attach').addClass('hidden'); + this.set_image(); + if(this.get_status()=="Read") { + $(this.disp_area).toggle(false); + } + }, set_image: function() { if(this.get_value()) { $(this.img_wrapper).find(".missing-image").toggle(false); - this.img.attr("src", this.dataurl ? this.dataurl : this.value).toggle(true); + // this.img.attr("src", this.dataurl ? this.dataurl : this.value).toggle(true); + // this.img_overlay.toggle(true); + this.img.attr("src", this.dataurl ? this.dataurl : this.value); + this.img_container.toggle(true); + this.remove_image_link.toggle(true); } else { $(this.img_wrapper).find(".missing-image").toggle(true); - this.img.toggle(false); + // this.img.toggle(false); + // this.img_overlay.toggle(false); + this.img_container.toggle(false); + this.remove_image_link.toggle(false); } } }); @@ -1127,39 +1276,31 @@ frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({ html_element: "select", make_input: function() { - var me = this; this._super(); this.set_options(); }, - set_input: function(value) { + set_formatted_input: function(value) { // refresh options first - (new ones??) - this.set_options(value || ""); + if(value==null) value = ''; + this.set_options(value); + + // set in the input element + this._super(value); - var input_value = null; + // check if the value to be set is selected + var input_value = ''; if(this.$input) { - var input_value = this.$input.val(); + input_value = this.$input.val(); } - // not a possible option, repair - if(this.doctype && this.docname) { - // model value is not an option, - // set the default option (displayed) - var model_value = frappe.model.get_value(this.doctype, this.docname, this.df.fieldname); - if(model_value == null && (input_value || "") != (model_value || "")) { - this.set_model_value(input_value); - } else { - this.last_value = value; - } - } else { - if(value !== input_value) { - this.set_value(input_value); - } + if(value && input_value && value !== input_value) { + // trying to set a non-existant value + // model value must be same as whatever the input is + this.set_model_value(input_value); } - - this._super(value); - }, set_options: function(value) { + // reset options, if something new is set var options = this.df.options || []; if(typeof this.df.options==="string") { options = this.df.options.split("\n"); @@ -1187,11 +1328,11 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({ this.set_description(""); var options = [""]; $.each(fl.attachments, function(i, f) { - options.push(f.file_url) + options.push(f.file_url); }); return options; } else { - this.set_description(__("Please attach a file first.")) + this.set_description(__("Please attach a file first.")); return [""]; } } @@ -1266,7 +1407,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ new frappe.ui.form.LinkSelector({ doctype: doctype, target: this, - txt: this.get_value() + txt: this.get_input_value() }); return false; }, @@ -1290,19 +1431,8 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ frappe._from_link = this; frappe._from_link_scrollY = $(document).scrollTop(); - var trimmed_doctype = doctype.replace(/ /g, ''); - var controller_name = "QuickEntryForm"; - - if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){ - controller_name = trimmed_doctype + "QuickEntryForm"; - } - - new frappe.ui.form[controller_name](doctype, function(doc) { - if(me.frm) { - me.parse_validate_and_set_in_model(doc.name); - } else { - me.set_value(doc.name); - } + frappe.ui.form.make_quick_entry(doctype, (doc) => { + return me.set_value(doc.name); }); return false; @@ -1388,7 +1518,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ + "", value: "create_new__link_option", action: me.new_doc - }) + }); } // advanced search r.results.push({ @@ -1398,7 +1528,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ + "", value: "advanced_search__link_option", action: me.open_advanced_search - }) + }); } me.$input.cache[doctype][term] = r.results; me.awesomplete.list = me.$input.cache[doctype][term]; @@ -1411,7 +1541,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ me.selected = false; return; } - var value = me.get_value(); + var value = me.get_input_value(); if(value!==me.last_value) { me.parse_validate_and_set_in_model(value); } @@ -1473,7 +1603,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } }); return obj; - } + }; if(this.get_query || this.df.get_query) { var get_query = this.get_query || this.df.get_query; if($.isPlainObject(get_query)) { @@ -1534,50 +1664,47 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ $.extend(args.filters, this.df.filters); } }, - validate: function(value, callback) { + validate: function(value) { // validate the value just entered - var me = this; - if(this.df.options=="[Select]" || this.df.ignore_link_validation) { - callback(value); - return; + return value; } - this.validate_link_and_fetch(this.df, this.get_options(), - this.docname, value, callback); + return this.validate_link_and_fetch(this.df, this.get_options(), + this.docname, value); }, - validate_link_and_fetch: function(df, doctype, docname, value, callback) { + validate_link_and_fetch: function(df, doctype, docname, value) { var me = this; if(value) { - var fetch = ''; + return new Promise((resolve) => { + var fetch = ''; - if(this.frm && this.frm.fetch_dict[df.fieldname]) { - fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); - } + if(this.frm && this.frm.fetch_dict[df.fieldname]) { + fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); + } - return frappe.call({ - method:'frappe.desk.form.utils.validate_link', - type: "GET", - args: { - 'value': value, - 'options': doctype, - 'fetch': fetch - }, - no_spinner: true, - callback: function(r) { - if(r.message=='Ok') { - if(r.fetch_values && docname) { - me.set_fetch_values(df, docname, r.fetch_values); + return frappe.call({ + method:'frappe.desk.form.utils.validate_link', + type: "GET", + args: { + 'value': value, + 'options': doctype, + 'fetch': fetch + }, + no_spinner: true, + callback: function(r) { + if(r.message=='Ok') { + if(r.fetch_values && docname) { + me.set_fetch_values(df, docname, r.fetch_values); + } + resolve(r.valid_value); + } else { + resolve(""); } - if(callback) callback(r.valid_value); - } else { - if(callback) callback(""); } - } + }); }); - } else if(callback) { - callback(value); } }, set_fetch_values: function(df, docname, fetch_values) { @@ -1593,7 +1720,7 @@ if(Awesomplete) { return this._list.find(function(item) { return item.value === value; }); - } + }; } frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({ @@ -1603,11 +1730,11 @@ frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({ } if (this.docname==null && cur_dialog) { //for dialog box - return cur_dialog.get_value(this.df.options) + return cur_dialog.get_value(this.df.options); } if (cur_frm==null && cur_list){ //for list page - return cur_list.wrapper.find("input[data-fieldname*="+this.df.options+"]").val() + return cur_list.wrapper.find("input[data-fieldname*="+this.df.options+"]").val(); } var options = frappe.model.get_value(this.df.parent, this.docname, this.df.options); // if(!options) { @@ -1634,6 +1761,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ this.hide_elements_on_mobile(); this.setup_drag_drop(); this.setup_image_dialog(); + this.setting_count = 0; }, make_editor: function() { var me = this; @@ -1681,6 +1809,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ me.parse_validate_and_set_in_model(value); }, onKeydown: function(e) { + me._last_change_on = new Date(); var key = frappe.ui.keys.get_key(e); // prevent 'New DocType (Ctrl + B)' shortcut in editor if(['ctrl+b', 'meta+b'].indexOf(key) !== -1) { @@ -1739,7 +1868,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ }); this.note_editor = $(this.input_area).find('.note-editor'); // to fix

    on enter - this.set_input('


    '); + //this.set_formatted_input('

    '); }, setup_drag_drop: function() { var me = this; @@ -1773,7 +1902,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ parts[0] += ";filename=" + fileobj.name; dataurl = parts[0] + ',' + parts[1]; callback(dataurl); - } + }; freader.readAsDataURL(fileobj); }, hide_elements_on_mobile: function() { @@ -1787,16 +1916,54 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ .attr('data-original-title', ''); } }, - get_value: function() { + get_input_value: function() { return this.editor? this.editor.summernote('code'): ''; }, - set_input: function(value) { + parse: function(value) { if(value == null) value = ""; - value = frappe.dom.remove_script_and_style(value); - if(value !== this.get_value()) { - this.editor.summernote('code', value); + return frappe.dom.remove_script_and_style(value); + }, + set_formatted_input: function(value) { + if(value !== this.get_input_value()) { + this.set_in_editor(value); + } + }, + set_in_editor: function(value) { + // set values in editor only if + // 1. value not be set in the last 500ms + // 2. user has not typed anything in the last 3seconds + // --- + // we will attempt to cleanup the user's DOM, hence if this happens + // in the middle of the user is typing, it creates a lot of issues + // also firefox tends to reset the cursor for some reason if the values + // are reset + + if(this.setting_count > 2) { + // we don't understand how the internal triggers work, + // so if someone is setting the value third time, then quit + return; + } + + this.setting_count += 1; + + let time_since_last_keystroke = moment() - moment(this._last_change_on); + + if(!this._last_change_on || (time_since_last_keystroke > 3000)) { + setTimeout(() => this.setting_count = 0, 500); + this.editor.summernote('code', value || ''); + } else { + this._setting_value = setInterval(() => { + if(time_since_last_keystroke > 3000) { + if(this.last_value !== this.get_input_value()) { + // if not already in sync, reset + this.editor.summernote('code', this.last_value || ''); + } + clearInterval(this._setting_value); + this._setting_value = null; + this.setting_count = 0; + } + }, 1000); } - this.last_value = value; }, set_focus: function() { return this.editor.summernote('focus'); @@ -1828,7 +1995,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ onerror: function() { me.image_dialog.hide(); } - } + }; if ("is_private" in this.df) { this.upload_options.is_private = this.df.is_private; @@ -1839,13 +2006,13 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ from_form: 1, doctype: this.frm.doctype, docname: this.frm.docname - } + }; } else { this.upload_options.on_attach = function(fileobj, dataurl) { me.editor.summernote('insertImage', dataurl); me.image_dialog.hide(); frappe.hide_progress(); - } + }; } }, @@ -1869,7 +2036,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ var attachments = this.frm && this.frm.attachments.get_attachments() || []; var select = this.image_dialog.get_field("select"); if(attachments.length) { - attachments = $.map(attachments, function(o) { return o.file_url; }) + attachments = $.map(attachments, function(o) { return o.file_url; }); select.df.options = [""].concat(attachments); select.toggle(true); this.image_dialog.get_field("or_attach").toggle(true); @@ -1896,7 +2063,7 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ df: this.df, perm: this.perm || (this.frm && this.frm.perm) || this.df.perm, parent: this.wrapper - }) + }); if(this.frm) { this.frm.grids[this.frm.grids.length] = this; } @@ -1906,14 +2073,11 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ $('

    ' + __(this.df.description) + '

    ') .appendTo(this.wrapper); } - - var me = this; - this.$wrapper.on("refresh", function() { - me.grid.refresh(); - return false; - }); }, - get_parsed_value: function() { + refresh_input: function() { + this.grid.refresh(); + }, + get_value: function() { if(this.grid) { return this.grid.get_data(); } @@ -1950,10 +2114,8 @@ frappe.ui.form.ControlSignature = frappe.ui.form.ControlData.extend({ me.on_reset_sign(); return false; }); - // handle refresh by reloading the pad - this.$wrapper.on("refresh", this.on_refresh.bind(this)); }, - on_refresh: function(e) { + refresh_input: function(e) { // prevent to load the second time this.$wrapper.find(".control-input").toggle(false); this.set_editable(this.get_status()=="Write"); diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 6b5f5b239e..fc3c31fce2 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -4,10 +4,11 @@ frappe.ui.form.Dashboard = Class.extend({ init: function(opts) { $.extend(this, opts); + this.section = this.frm.fields_dict._form_dashboard.wrapper; + this.parent = this.section.find('.section-body'); this.wrapper = $(frappe.render_template('form_dashboard', - {frm: this.frm})).prependTo(this.frm.layout.wrapper); + {frm: this.frm})).appendTo(this.parent); - this.headline = this.wrapper.find('.form-headline'); this.progress_area = this.wrapper.find(".progress-area"); this.heatmap_area = this.wrapper.find('.form-heatmap'); this.chart_area = this.wrapper.find('.form-chart'); @@ -18,7 +19,7 @@ frappe.ui.form.Dashboard = Class.extend({ }, reset: function() { - this.wrapper.addClass('hidden'); + this.section.addClass('hidden'); this.clear_headline(); // clear progress @@ -36,13 +37,10 @@ frappe.ui.form.Dashboard = Class.extend({ this.wrapper.find('.custom').remove(); }, set_headline: function(html) { - this.headline.html(html).removeClass('hidden'); - this.show(); + this.frm.layout.show_message(html); }, clear_headline: function() { - if(this.headline) { - this.headline.empty().addClass('hidden'); - } + this.frm.layout.show_message(); }, add_comment: function(text, permanent) { @@ -59,13 +57,12 @@ frappe.ui.form.Dashboard = Class.extend({ this.clear_headline(); }, - set_headline_alert: function(text, alert_class) { + set_headline_alert: function(text, indicator_color) { + if (!indicator_color) { + indicator_color = 'orange'; + } if(text) { - if(!alert_class) alert_class = "alert-warning"; - this.set_headline(repl('
    %(text)s
    ', { - "alert_class": alert_class || "", - "text": text - })); + this.set_headline(`
    ${text}
    `); } else { this.clear_headline(); } @@ -406,6 +403,6 @@ frappe.ui.form.Dashboard = Class.extend({ } }, show: function() { - this.wrapper.removeClass('hidden'); + this.section.removeClass('hidden'); } }); diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 7eb1ecf120..6dbbae65db 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -421,7 +421,7 @@ frappe.ui.form.Timeline = Class.extend({ out.push(me.get_version_comment(version, __('cancelled this document'))); } } else { - + var df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname); if(df && !df.hidden) { @@ -448,8 +448,8 @@ frappe.ui.form.Timeline = Class.extend({ var parts = [], count = 0; data.row_changed.every(function(row) { row[3].every(function(p) { - var df = me.frm.fields_dict[row[0]] && - frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, + var df = me.frm.fields_dict[row[0]] && + frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, p[0], me.frm.docname); if(df && !df.hidden) { diff --git a/frappe/public/js/frappe/form/footer/timeline_item.html b/frappe/public/js/frappe/form/footer/timeline_item.html index 214a18e33b..c35ffa26d6 100755 --- a/frappe/public/js/frappe/form/footer/timeline_item.html +++ b/frappe/public/js/frappe/form/footer/timeline_item.html @@ -73,7 +73,8 @@ {% } %} - {% if (data.communication_medium === "Email" && data.sender !== user_email) { %} + {% if (data.communication_medium === "Email" + && data.sender !== frappe.session.user_email) { %} {%= __("Reply") %} {% } %} @@ -110,7 +111,7 @@ {% $.each(data.attachments, function(i, a) { %}
    + class="text-muted small" target="_blank" rel="noopener noreferrer"> {%= a.file_url.split("/").slice(-1)[0] %} {% if (a.is_private) { %} diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index c23d440f47..504155f461 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -53,6 +53,17 @@ frappe.form.formatters = { Currency: function(value, docfield, options, doc) { var currency = frappe.meta.get_field_currency(docfield, doc); var precision = docfield.precision || cint(frappe.boot.sysdefaults.currency_precision) || 2; + if (precision > 2) { + let parts = cstr(value).split('.'); + let decimals = parts.length > 1 ? parts[1] : ''; + if (decimals.length < 3) { + // min precision 2 + precision = 2; + } else if (decimals.length < precision) { + // or min decimals + precision = decimals.length; + } + } return frappe.form.formatters._right((value==null || value==="") ? "" : format_currency(value, currency, precision), options); }, @@ -108,6 +119,16 @@ frappe.form.formatters = { return value || ""; }, + DateRange: function(value) { + if($.isArray(value)) { + return __("{0} to {1}").format([ + frappe.datetime.str_to_user(value[0]), + frappe.datetime.str_to_user(value[1]) + ]); + } else { + return value || ""; + } + }, Datetime: function(value) { if(value) { var m = moment(frappe.datetime.convert_to_user_tz(value)); diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 8aa7abfef1..5e0437100a 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -108,6 +108,11 @@ frappe.ui.form.Grid = Class.extend({ select_row: function(name) { this.grid_rows_by_docname[name].select(); }, + remove_all: function() { + this.grid_rows.forEach(row => { + row.remove(); + }); + }, refresh_remove_rows_button: function() { this.remove_rows_button.toggleClass('hide', this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); @@ -257,7 +262,7 @@ frappe.ui.form.Grid = Class.extend({ if (this.frm && this.frm.docname) { // use doc specific docfield object this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname, - this.frm.docname); + this.frm.docname); } else { // use non-doc specific docfield if(this.df.options) { @@ -360,8 +365,19 @@ frappe.ui.form.Grid = Class.extend({ get_docfield: function(fieldname) { return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null); }, - get_grid_row: function(docname) { - return this.grid_rows_by_docname[docname]; + get_row: function(key) { + if(typeof key == 'number') { + if(key < 0) { + return this.grid_rows[this.grid_rows.length + key]; + } else { + return this.grid_rows[key]; + } + } else { + return this.grid_rows_by_docname[key]; + } + }, + get_grid_row: function(key) { + return this.get_row(key); }, get_field: function(fieldname) { // Note: workaround for get_query @@ -435,21 +451,21 @@ frappe.ui.form.Grid = Class.extend({ && (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm) && !in_list(frappe.model.layout_fields, df.fieldtype)) { - if(df.columns) { - df.colsize=df.columns; - } - else { - var colsize=2; - switch(df.fieldtype){ - case"Text": - case"Small Text": - colsize=3; - break; - case"Check": - colsize=1 - } - df.colsize=colsize + if(df.columns) { + df.colsize=df.columns; + } + else { + var colsize=2; + switch(df.fieldtype) { + case"Text": + case"Small Text": + colsize=3; + break; + case"Check": + colsize=1 } + df.colsize=colsize; + } if(df.columns) { df.colsize=df.columns; @@ -641,673 +657,4 @@ frappe.ui.form.Grid = Class.extend({ // hide all custom buttons this.grid_buttons.find('.btn-custom').addClass('hidden'); } -}); - -frappe.ui.form.GridRow = Class.extend({ - init: function(opts) { - this.on_grid_fields_dict = {}; - this.on_grid_fields = []; - this.row_check_html = ''; - this.columns = {}; - this.columns_list = []; - $.extend(this, opts); - this.make(); - }, - make: function() { - var me = this; - - this.wrapper = $('
    ').appendTo(this.parent).data("grid_row", this); - this.row = $('
    ').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 = $('
    '+this.row_check_html+'
    ').appendTo(this.row); - } - this.row_index.find('span').html(this.doc.idx); - } - - this.row_display = $('
    '+ - +'
    ').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 = $( - `
    - ${this.row_check_html} - ${txt}
    `) - .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 = $('
    \ - ') - .appendTo($('
    ').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 = $('
    ') - .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 = $('
    ').appendTo($col).toggle(false); - $col.static_area = $('
    ').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 = $('
    ') - .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); - }, -}); +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js new file mode 100644 index 0000000000..3ff9f85198 --- /dev/null +++ b/frappe/public/js/frappe/form/grid_row.js @@ -0,0 +1,586 @@ +frappe.ui.form.GridRow = Class.extend({ + init: function(opts) { + this.on_grid_fields_dict = {}; + this.on_grid_fields = []; + this.row_check_html = ''; + this.columns = {}; + this.columns_list = []; + $.extend(this, opts); + this.make(); + }, + make: function() { + var me = this; + + this.wrapper = $('
    ').appendTo(this.parent).data("grid_row", this); + this.row = $('
    ').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 = $('
    '+this.row_check_html+'
    ').appendTo(this.row); + } + this.row_index.find('span').html(this.doc.idx); + } + + this.row_display = $('
    '+ + +'
    ').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 = $( + `
    + ${this.row_check_html} + ${txt}
    `) + .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 = $('\ + ') + .appendTo($('
    ').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 = $('
    ') + .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 = $('
    ').appendTo($col).toggle(false); + $col.static_area = $('
    ').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); + }, +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/form/grid_row_form.js b/frappe/public/js/frappe/form/grid_row_form.js new file mode 100644 index 0000000000..d1bd3c6baa --- /dev/null +++ b/frappe/public/js/frappe/form/grid_row_form.js @@ -0,0 +1,97 @@ +frappe.ui.form.GridRowForm = Class.extend({ + init: function(opts) { + $.extend(this, opts); + this.wrapper = $('
    ') + .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); + }, +}); diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index d44ecdaa7b..c6458d8ee0 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -19,12 +19,14 @@ frappe.ui.form.Layout = Class.extend({ $.extend(this, opts); }, make: function() { - if(!this.parent && this.body) + if(!this.parent && this.body) { this.parent = this.body; + } this.wrapper = $('
    ').appendTo(this.parent); this.message = $('').appendTo(this.wrapper); - if(!this.fields) + if(!this.fields) { this.fields = frappe.meta.sort_docfields(frappe.meta.docfield_map[this.doctype]); + } this.setup_tabbing(); this.render(); }, @@ -44,29 +46,58 @@ frappe.ui.form.Layout = Class.extend({ this.message.empty().addClass('hidden'); } }, - render: function() { + render: function(new_fields) { var me = this; - + var fields = new_fields || this.fields; this.section = null; this.column = null; - if((this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length) { + + if (this.with_dashboard) { + this.setup_dashboard_section(); + } + + if (this.no_opening_section()) { this.make_section(); } - $.each(this.fields, function(i, df) { - if(df.fieldtype === "Fold") { - me.make_page(df); - } else if (df.fieldtype === "Section Break") { - me.make_section(df); - } else if (df.fieldtype === "Column Break") { - me.make_column(df); - } else { - me.make_field(df); + $.each(fields, function(i, df) { + switch(df.fieldtype) { + case "Fold": + me.make_page(df); + break; + case "Section Break": + me.make_section(df); + break; + case "Column Break": + me.make_column(df); + break; + default: + me.make_field(df); } }); }, - make_field: function(df, colspan) { + + no_opening_section: function() { + return (this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length; + }, + + setup_dashboard_section: function() { + if (this.no_opening_section()) { + this.fields.unshift({fieldtype: 'Section Break'}); + } + + this.fields.unshift({ + fieldtype: 'Section Break', + fieldname: '_form_dashboard', + label: __('Dashboard'), + cssClass: 'form-dashboard', + collapsible: 1, + //hidden: 1 + }); + }, + + make_field: function(df, colspan, render = false) { !this.section && this.make_section(); !this.column && this.make_column(); @@ -74,7 +105,8 @@ frappe.ui.form.Layout = Class.extend({ df: df, doctype: this.doctype, parent: this.column.wrapper.get(0), - frm: this.frm + frm: this.frm, + render_input: render }); fieldobj.layout = this; @@ -171,13 +203,14 @@ frappe.ui.form.Layout = Class.extend({ var $this = $(this).removeClass("empty-section") .removeClass("visible-section") .removeClass("shaded-section"); - if(!$(this).find(".frappe-control:not(.hide-control)").length) { + if(!$this.find(".frappe-control:not(.hide-control)").length + && !$this.hasClass('form-dashboard')) { // nothing visible, hide the section - $(this).addClass("empty-section"); + $this.addClass("empty-section"); } else { - $(this).addClass("visible-section"); + $this.addClass("visible-section"); if(cnt % 2) { - $(this).addClass("shaded-section"); + $this.addClass("shaded-section"); } cnt ++; } @@ -201,6 +234,10 @@ frappe.ui.form.Layout = Class.extend({ collapse = false; } + if(df.fieldname === '_form_dashboard') { + collapse = false; + } + section.collapse(collapse); } } @@ -226,6 +263,18 @@ frappe.ui.form.Layout = Class.extend({ } }, + refresh_fields: function(fields) { + let fieldnames = fields.map((field) => { + if(field.label) return field.label; + }); + + this.fields_list.map(fieldobj => { + if(fieldnames.includes(fieldobj._label)) { + fieldobj.refresh(); + } + }); + }, + refresh_section_count: function() { this.wrapper.find(".section-count-label:visible").each(function(i) { $(this).html(i+1); @@ -241,7 +290,7 @@ frappe.ui.form.Layout = Class.extend({ if(doctype) return me.handle_tab(doctype, fieldname, ev.shiftKey); } - }) + }); }, handle_tab: function(doctype, fieldname, shift) { var me = this, @@ -264,7 +313,7 @@ frappe.ui.form.Layout = Class.extend({ if(fields[i].df.fieldname==fieldname) { if(shift) { if(prev) { - this.set_focus(prev) + this.set_focus(prev); } else { $(this.primary_button).focus(); } @@ -290,7 +339,7 @@ frappe.ui.form.Layout = Class.extend({ // last row, close it and find next field grid_row.toggle_view(false, function() { grid_row.grid.frm.layout.handle_tab(grid_row.grid.df.parent, grid_row.grid.df.fieldname); - }) + }); } else { // next row grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true); @@ -325,7 +374,7 @@ frappe.ui.form.Layout = Class.extend({ } }, is_visible: function(field) { - return field.disp_status==="Write" && (field.$wrapper && field.$wrapper.is(":visible")) + return field.disp_status==="Write" && (field.$wrapper && field.$wrapper.is(":visible")); }, set_focus: function(field) { // next is table, show the table @@ -450,17 +499,20 @@ frappe.ui.form.Section = Class.extend({ .appendTo(this.layout.page); this.layout.sections.push(this); - var section = this.wrapper[0]; - if(this.df) { if(this.df.label) { this.make_head(); } if(this.df.description) { $('
    ' + __(this.df.description) + '
    ') - .appendTo(this.wrapper); + .appendTo(this.wrapper); + } + if(this.df.cssClass) { + this.wrapper.addClass(this.df.cssClass); } } + + // for bc this.body = $('
    ').appendTo(this.wrapper); }, @@ -469,7 +521,7 @@ frappe.ui.form.Section = Class.extend({ if(!this.df.collapsible) { $('
    ' + __(this.df.label) + '
    ') - .appendTo(this.wrapper); + .appendTo(this.wrapper); } else { this.head = $('').appendTo(this.wrapper); @@ -521,7 +573,7 @@ frappe.ui.form.Section = Class.extend({ } return missing_mandatory; } -}) +}); frappe.ui.form.Column = Class.extend({ init: function(section, df) { @@ -538,7 +590,7 @@ frappe.ui.form.Column = Class.extend({ \
    ').appendTo(this.section.body) .find("form") - .on("submit", function() { return false; }) + .on("submit", function() { return false; }); if(this.df.label) { $('