Pārlūkot izejas kodu

Merge branch 'develop' into twofactor

version-14
ckosiegbu pirms 8 gadiem
vecāks
revīzija
03718b9ba3
100 mainītis faili ar 3312 papildinājumiem un 1640 dzēšanām
  1. +2
    -1
      .eslintrc
  2. +17
    -11
      .travis.yml
  3. +18
    -11
      frappe/__init__.py
  4. +1
    -0
      frappe/build.js
  5. +16
    -19
      frappe/commands/utils.py
  6. +7
    -0
      frappe/contacts/doctype/address/address.js
  7. +7
    -0
      frappe/contacts/doctype/address/address.py
  8. +3
    -0
      frappe/contacts/doctype/contact/contact_list.js
  9. +1
    -0
      frappe/contacts/doctype/contact/test_records.json
  10. +8
    -0
      frappe/contacts/doctype/salutation/test_records.json
  11. +7
    -6
      frappe/core/doctype/authentication_log/test_authentication_log.py
  12. +46
    -2
      frappe/core/doctype/docfield/docfield.json
  13. +17
    -6
      frappe/core/doctype/doctype/doctype.py
  14. +1
    -1
      frappe/core/doctype/report/report.py
  15. +33
    -1
      frappe/core/doctype/system_settings/system_settings.json
  16. +0
    -0
      frappe/core/doctype/test_runner/__init__.py
  17. +74
    -0
      frappe/core/doctype/test_runner/test_runner.js
  18. +122
    -0
      frappe/core/doctype/test_runner/test_runner.json
  19. +49
    -0
      frappe/core/doctype/test_runner/test_runner.py
  20. +7
    -0
      frappe/core/doctype/user/test_records.json
  21. +1
    -1
      frappe/core/doctype/user/user.json
  22. +6
    -6
      frappe/core/doctype/user/user.py
  23. +1
    -1
      frappe/core/page/permission_manager/permission_manager_help.html
  24. +1
    -1
      frappe/custom/doctype/custom_field/custom_field.js
  25. +3
    -2
      frappe/custom/doctype/custom_field/custom_field.json
  26. +4
    -0
      frappe/custom/doctype/custom_field/custom_field.py
  27. +1
    -1
      frappe/custom/doctype/customize_form/customize_form.js
  28. +6
    -0
      frappe/custom/doctype/customize_form/customize_form.py
  29. +5
    -5
      frappe/custom/doctype/customize_form/test_customize_form.py
  30. +2
    -2
      frappe/custom/doctype/customize_form_field/customize_form_field.json
  31. +2
    -2
      frappe/desk/doctype/event/event.json
  32. +20
    -4
      frappe/desk/doctype/todo/todo.json
  33. +2
    -1
      frappe/desk/form/meta.py
  34. +1
    -1
      frappe/desk/page/applications/application_row.html
  35. +1
    -1
      frappe/desk/page/backups/backups.html
  36. +7
    -0
      frappe/desk/page/backups/backups.js
  37. +28
    -1
      frappe/desk/page/backups/backups.py
  38. +112
    -7
      frappe/desk/page/setup_wizard/setup_wizard.css
  39. +352
    -197
      frappe/desk/page/setup_wizard/setup_wizard.js
  40. +17
    -1
      frappe/desk/page/setup_wizard/setup_wizard.py
  41. +8
    -4
      frappe/desk/page/setup_wizard/setup_wizard_page.html
  42. +1
    -1
      frappe/desk/query_report.py
  43. Binārs
      frappe/docs/assets/img/app-development/test-runner.png
  44. +0
    -0
      frappe/docs/user/en/guides/automated-testing/__init__.py
  45. +7
    -0
      frappe/docs/user/en/guides/automated-testing/index.md
  46. +3
    -0
      frappe/docs/user/en/guides/automated-testing/index.txt
  47. +49
    -0
      frappe/docs/user/en/guides/automated-testing/integration-testing.md
  48. +70
    -0
      frappe/docs/user/en/guides/automated-testing/qunit-testing.md
  49. +16
    -22
      frappe/docs/user/en/guides/automated-testing/unit-testing.md
  50. +1
    -1
      frappe/docs/user/en/guides/basics/apps.md
  51. +1
    -1
      frappe/docs/user/fr/tutorial/web-views.md
  52. +2
    -2
      frappe/email/doctype/auto_email_report/auto_email_report.json
  53. +11
    -7
      frappe/email/doctype/auto_email_report/auto_email_report.py
  54. +1
    -1
      frappe/email/doctype/auto_email_report/test_auto_email_report.py
  55. +2
    -1
      frappe/email/doctype/email_account/email_account.js
  56. +2
    -1
      frappe/email/doctype/email_account/email_account.py
  57. +1
    -0
      frappe/email/doctype/email_alert/email_alert.js
  58. +141
    -4
      frappe/email/doctype/email_alert/email_alert.json
  59. +5
    -0
      frappe/email/doctype/email_alert/email_alert.py
  60. +6
    -1
      frappe/email/doctype/email_alert/test_email_alert.py
  61. +3
    -1
      frappe/email/doctype/email_alert/test_records.json
  62. +1
    -1
      frappe/email/doctype/email_group/email_group.py
  63. +49
    -2
      frappe/email/doctype/email_queue/email_queue.json
  64. +3
    -2
      frappe/email/doctype/newsletter/newsletter.py
  65. +175
    -65
      frappe/email/email_body.py
  66. +56
    -17
      frappe/email/queue.py
  67. +101
    -0
      frappe/email/test_email_body.py
  68. +1
    -1
      frappe/frappeclient.py
  69. +3
    -0
      frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
  70. +1
    -2
      frappe/model/base_document.py
  71. +10
    -5
      frappe/model/db_query.py
  72. +9
    -1
      frappe/model/db_schema.py
  73. +1
    -0
      frappe/model/delete_doc.py
  74. +6
    -0
      frappe/model/naming.py
  75. +1
    -0
      frappe/model/rename_doc.py
  76. +2
    -2
      frappe/modules/utils.py
  77. +0
    -12
      frappe/nightwatch.global.js
  78. +0
    -96
      frappe/nightwatch.js
  79. +2
    -0
      frappe/patches.txt
  80. +6
    -0
      frappe/patches/v8_1/delete_custom_docperm_if_doctype_not_exists.py
  81. +14
    -0
      frappe/patches/v8_1/update_format_options_in_auto_email_report.py
  82. +1
    -1
      frappe/printing/doctype/print_format/test_print_format.py
  83. +3
    -0
      frappe/public/build.json
  84. +48
    -0
      frappe/public/css/desk.css
  85. +20
    -17
      frappe/public/css/form.css
  86. +20
    -0
      frappe/public/js/frappe/dom.js
  87. +428
    -266
      frappe/public/js/frappe/form/control.js
  88. +12
    -15
      frappe/public/js/frappe/form/dashboard.js
  89. +3
    -3
      frappe/public/js/frappe/form/footer/timeline.js
  90. +3
    -2
      frappe/public/js/frappe/form/footer/timeline_item.html
  91. +21
    -0
      frappe/public/js/frappe/form/formatters.js
  92. +34
    -687
      frappe/public/js/frappe/form/grid.js
  93. +586
    -0
      frappe/public/js/frappe/form/grid_row.js
  94. +97
    -0
      frappe/public/js/frappe/form/grid_row_form.js
  95. +83
    -31
      frappe/public/js/frappe/form/layout.js
  96. +86
    -50
      frappe/public/js/frappe/form/quick_entry.js
  97. +19
    -3
      frappe/public/js/frappe/form/save.js
  98. +67
    -15
      frappe/public/js/frappe/form/script_manager.js
  99. +1
    -3
      frappe/public/js/frappe/form/templates/form_dashboard.html
  100. +1
    -2
      frappe/public/js/frappe/form/templates/form_links.html

+ 2
- 1
.eslintrc Parādīt failu

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

+ 17
- 11
.travis.yml Parādīt failu

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

+ 18
- 11
frappe/__init__.py Parādīt failu

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

+ 1
- 0
frappe/build.js Parādīt failu

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


+ 16
- 19
frappe/commands/utils.py Parādīt failu

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


+ 7
- 0
frappe/contacts/doctype/address/address.js Parādīt failu

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

+ 7
- 0
frappe/contacts/doctype/address/address.py Parādīt failu

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


+ 3
- 0
frappe/contacts/doctype/contact/contact_list.js Parādīt failu

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

+ 1
- 0
frappe/contacts/doctype/contact/test_records.json Parādīt failu

@@ -1,6 +1,7 @@
[
{
"doctype": "Contact",
"salutation": "Mr",
"email_id": "test_conctact@example.com",
"first_name": "_Test Contact For _Test Customer",
"is_primary_contact": 1,


+ 8
- 0
frappe/contacts/doctype/salutation/test_records.json Parādīt failu

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

+ 7
- 6
frappe/core/doctype/authentication_log/test_authentication_log.py Parādīt failu

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


+ 46
- 2
frappe/core/doctype/docfield/docfield.json Parādīt failu

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


+ 17
- 6
frappe/core/doctype/doctype/doctype.py Parādīt failu

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

+ 1
- 1
frappe/core/doctype/report/report.py Parādīt failu

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


+ 33
- 1
frappe/core/doctype/system_settings/system_settings.json Parādīt failu

@@ -810,7 +810,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "eg. If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User",
"description": "If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User",
"fieldname": "ignore_user_permissions_if_missing",
"fieldtype": "Check",
"hidden": 0,
@@ -835,6 +835,38 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User",
"fieldname": "apply_strict_user_permissions",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Apply Strict User Permissions",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,


+ 0
- 0
frappe/core/doctype/test_runner/__init__.py Parādīt failu


+ 74
- 0
frappe/core/doctype/test_runner/test_runner.js Parādīt failu

@@ -0,0 +1,74 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt

frappe.ui.form.on('Test Runner', {
refresh: (frm) => {
frm.disable_save();
frm.page.set_primary_action(__("Run Tests"), () => {
return new Promise(resolve => {
let wrapper = $(frm.fields_dict.output.wrapper).empty();
$("<p>Loading...</p>").appendTo(wrapper);

// all tests
frappe.call({
method: 'frappe.core.doctype.test_runner.test_runner.get_test_js'
}).always((data) => {
$("<div id='qunit'></div>").appendTo(wrapper.empty());
frm.events.run_tests(frm, data.message);
resolve();
});
});
});

},
run_tests: function(frm, files) {
let require_list = [
"assets/frappe/js/lib/jquery/qunit.js",
"assets/frappe/js/lib/jquery/qunit.css"
].concat();

frappe.require(require_list, () => {
files.forEach((f) => {
frappe.dom.eval(f.script);
});

// if(frm.doc.module_name) {
// QUnit.module.only(frm.doc.module_name);
// }

QUnit.testDone(function(details) {
var result = {
"Module name": details.module,
"Test name": details.name,
"Assertions": {
"Total": details.total,
"Passed": details.passed,
"Failed": details.failed
},
"Skipped": details.skipped,
"Todo": details.todo,
"Runtime": details.runtime
};

// eslint-disable-next-line
console.log(JSON.stringify(result, null, 2));
});
QUnit.load();

QUnit.done(({ total, failed, passed, runtime }) => {
// flag for selenium that test is done
$('<div id="frappe-qunit-done"></div>').appendTo($('body'));

console.log( `Total: ${total}, Failed: ${failed}, Passed: ${passed}, Runtime: ${runtime}` ); // eslint-disable-line

if(failed) {
console.log('Tests Failed'); // eslint-disable-line
} else {
console.log('Tests Passed'); // eslint-disable-line
}
frappe.set_route('Form', 'Test Runner', 'Test Runner');
});
});

}
});

+ 122
- 0
frappe/core/doctype/test_runner/test_runner.json Parādīt failu

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

+ 49
- 0
frappe/core/doctype/test_runner/test_runner.py Parādīt failu

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

+ 7
- 0
frappe/core/doctype/user/test_records.json Parādīt failu

@@ -31,6 +31,13 @@
"new_password": "Eastern_43A1W",
"enabled": 1
},
{
"doctype": "User",
"email": "test3@example.com",
"first_name": "_Test3",
"new_password": "Eastern_43A1W",
"enabled": 1
},
{
"doctype": "User",
"email": "testperm@example.com",


+ 1
- 1
frappe/core/doctype/user/user.json Parādīt failu

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


+ 6
- 6
frappe/core/doctype/user/user.py Parādīt failu

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


+ 1
- 1
frappe/core/page/permission_manager/permission_manager_help.html Parādīt failu

@@ -36,6 +36,6 @@
<li>{%= __("Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.") %}</li>
</ol>
<p>{%= __("If these instructions where not helpful, please add in your suggestions on GitHub Issues.") %}
<a href="https://github.com/frappe/frappe/issues" target="_blank">{%= __("Submit an Issue") %}</a>
<a href="https://github.com/frappe/frappe/issues" target="_blank" rel="noopener noreferrer">{%= __("Submit an Issue") %}</a>
</p>
</div>

+ 1
- 1
frappe/custom/doctype/custom_field/custom_field.js Parādīt failu

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


+ 3
- 2
frappe/custom/doctype/custom_field/custom_field.json Parādīt failu

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


+ 4
- 0
frappe/custom/doctype/custom_field/custom_field.py Parādīt failu

@@ -39,6 +39,10 @@ class CustomField(Document):
if not self.fieldname:
frappe.throw(_("Fieldname not set for Custom Field"))

if not self.flags.ignore_validate:
from frappe.core.doctype.doctype.doctype import check_if_fieldname_conflicts_with_methods
check_if_fieldname_conflicts_with_methods(self.dt, self.fieldname)

def on_update(self):
frappe.clear_cache(doctype=self.dt)
if not self.flags.ignore_validate:


+ 1
- 1
frappe/custom/doctype/customize_form/customize_form.js Parādīt failu

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


+ 6
- 0
frappe/custom/doctype/customize_form/customize_form.py Parādīt failu

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



+ 5
- 5
frappe/custom/doctype/customize_form/test_customize_form.py Parādīt failu

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


+ 2
- 2
frappe/custom/doctype/customize_form_field/customize_form_field.json Parādīt failu

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


+ 2
- 2
frappe/desk/doctype/event/event.json Parādīt failu

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


+ 20
- 4
frappe/desk/doctype/todo/todo.json Parādīt failu

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


+ 2
- 1
frappe/desk/form/meta.py Parādīt failu

@@ -65,7 +65,7 @@ class FormMeta(Meta):
def _get_path(fname):
return os.path.join(path, scrub(fname))

system_country = frappe.get_system_country()
system_country = frappe.get_system_settings("country")

self._add_code(_get_path(self.name + '.js'), '__js')
if system_country:
@@ -82,6 +82,7 @@ class FormMeta(Meta):
self.add_code_via_hook("doctype_js", "__js")
self.add_code_via_hook("doctype_list_js", "__list_js")
self.add_code_via_hook("doctype_tree_js", "__tree_js")
self.add_code_via_hook("doctype_calendar_js", "__calendar_js")
self.add_custom_script()
self.add_html_templates(path)



+ 1
- 1
frappe/desk/page/applications/application_row.html Parādīt failu

@@ -5,7 +5,7 @@
<div class="media">
<div class="pull-right app-buttons">
<a class="btn btn-default btn-xs"
href="{{ app.app_url }}" target="_blank">{{ __("Website") }}</a>
href="{{ app.app_url }}" target="_blank" rel="noopener noreferrer">{{ __("Website") }}</a>
{% if (app.installed) { %}
<button class="btn btn-danger btn-xs btn-remove"
data-title="{{ app.app_title }}"


+ 1
- 1
frappe/desk/page/backups/backups.html Parādīt failu

@@ -21,7 +21,7 @@
{{ f[1] }}
</td>
<td>
<a href="{{ f[0] }}" target="_blank">{{ f[0] }}</a>
<a href="{{ f[0] }}" target="_blank" rel="noopener noreferrer">{{ f[0] }}</a>
</td>
<td>
{{ f[2] }}


+ 7
- 0
frappe/desk/page/backups/backups.js Parādīt failu

@@ -9,6 +9,13 @@ frappe.pages['backups'].on_page_load = function(wrapper) {
frappe.set_route('Form', 'System Settings');
});

page.add_inner_button(__("Download Files Backup"), function () {
frappe.call({
method:"frappe.desk.page.backups.backups.schedule_files_backup",
args: {"user_email": frappe.session.user_email}
});
});

frappe.breadcrumbs.add("Setup");

$(frappe.render_template("backups")).appendTo(page.body.addClass("no-border"));


+ 28
- 1
frappe/desk/page/backups/backups.py Parādīt failu

@@ -1,6 +1,7 @@
import os
import frappe
from frappe.utils import get_site_path, cint
from frappe import _
from frappe.utils import get_site_path, cint, get_url
from frappe.utils.data import convert_utc_to_user_timezone
import datetime

@@ -57,3 +58,29 @@ def delete_downloadable_backups():

if len(files) > backup_limit:
cleanup_old_backups(path, files, backup_limit)

@frappe.whitelist()
def schedule_files_backup(user_email):
from frappe.utils.background_jobs import enqueue, get_jobs
queued_jobs = get_jobs(site=frappe.local.site, queue="long")
method = 'frappe.desk.page.backups.backups.backup_files_and_notify_user'

if method not in queued_jobs[frappe.local.site]:
enqueue("frappe.desk.page.backups.backups.backup_files_and_notify_user", queue='long', user_email=user_email)
frappe.msgprint(_("Queued for backup. You will receive an email with the download link"))
else:
frappe.msgprint(_("Backup job is already queued. You will receive an email with the download link"))

def backup_files_and_notify_user(user_email=None):
from frappe.utils.backups import backup
backup_files = backup(with_files=True)
get_downloadable_links(backup_files)

subject = "File backup is ready"
message = frappe.render_template('frappe/templates/emails/file_backup_notification.html', backup_files, is_path=True)
frappe.sendmail(recipients=[user_email], subject=subject, message=message)

def get_downloadable_links(backup_files):
for key in ['backup_path_files', 'backup_path_private_files']:
path = backup_files[key]
backup_files[key] = get_url('/'.join(path.split('/')[-2:]))

+ 112
- 7
frappe/desk/page/setup_wizard/setup_wizard.css Parādīt failu

@@ -1,3 +1,26 @@
#page-setup-wizard {
margin-top: 30px;
}

.setup-wizard-brand {
margin: 30px;
text-align: center;
display: flex;
justify-content: center;
align-items: center
}

.setup-wizard-brand .brand-icon {
width: 36px;
height: 36px;
}

.setup-wizard-brand .brand-name {
font-size: 20px;
margin-left: 8px;
color: #36414C;
}

.setup-wizard-slide {
padding-left: 0px;
padding-right: 0px;
@@ -14,22 +37,60 @@
}

.setup-wizard-slide .lead {
margin-bottom: 10px;
margin: 30px;
color: #777777;
text-align: center;
font-size: 24px;
}

.setup-wizard-slide .col-sm-12 {
padding: 0px;
}

.setup-wizard-slide .section-body .col-sm-6:first-child {
padding-left: 0px;
}

.setup-wizard-slide .section-body .col-sm-6:last-child {
padding-right: 0px;
}

.setup-wizard-slide .form-control {
font-weight: 500;
}

.setup-wizard-slide .has-error .control-label {
color: #ffa00a;
}

.setup-wizard-slide .has-error .form-control{
border-color: #ffa00a;
}

.setup-wizard-slide .form-control.bold {
background-color: #fff;
}

.setup-wizard-slide.with-form {
margin: 40px auto;
margin: 30px auto;
padding: 10px 50px;
border: 1px solid #d1d8dd;
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.1);
}

.setup-wizard-slide .footer {
padding: 30px;
padding: 30px 0px;
}

.setup-wizard-slide a.next-btn.disabled,
.setup-wizard-slide a.complete-btn.disabled {
background-color: #b1bdca;
color: #fff;
border-color: #b1bdca;
}

.setup-wizard-progress {
padding: 15px;
padding: 15px;
}

.setup-wizard-slide .fa-fw {
@@ -50,16 +111,28 @@
}

.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] {
width: 140px;
height: 180px; /*depends on presence of heading*/
text-align: center;
margin-left: calc((100% - 140px)/2);
}

.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] .form-group,
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] .clearfix {
display: none;
}

.setup-wizard-slide .missing-image,
.setup-wizard-slide .attach-image-display {
display: block;
position: relative;
left: 50%;
transform: translate(-50%, 0);
-webkit-transform: translate(-50%, 0);
border-radius: 4px;
}

.setup-wizard-slide .missing-image {
border: 1px solid #d1d8dd;
border-radius: 6px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
}

.setup-wizard-slide .missing-image .octicon {
@@ -69,6 +142,38 @@
-webkit-transform: translate(0px, -50%);
}


.setup-wizard-slide .img-container {
height: 100%;
width: 100%;
padding: 2px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border: 1px solid #d1d8dd;
border-radius: 6px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
}

.setup-wizard-slide .img-overlay {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
color: #777777;
background-color: rgba(255, 255, 255, 0.7);
opacity: 0;
}

.setup-wizard-slide .img-overlay:hover {
opacity: 1;
cursor: pointer;
}


.setup-wizard-message-image {
margin: 15px auto;
}

+ 352
- 197
frappe/desk/page/setup_wizard/setup_wizard.js Parādīt failu

@@ -1,22 +1,25 @@
frappe.provide("frappe.wiz");
frappe.provide("frappe.wiz.events");
frappe.provide("frappe.setup.events");

frappe.wiz = {
frappe.setup = {
slides: [],
events: {},
data: {},
utils: {},

remove_app_slides: [],
on: function(event, fn) {
if(!frappe.wiz.events[event]) {
frappe.wiz.events[event] = [];
if(!frappe.setup.events[event]) {
frappe.setup.events[event] = [];
}
frappe.wiz.events[event].push(fn);
frappe.setup.events[event].push(fn);
},
add_slide: function(slide) {
frappe.wiz.slides.push(slide);
frappe.setup.slides.push(slide);
},

run_event: function(event) {
$.each(frappe.wiz.events[event] || [], function(i, fn) {
$.each(frappe.setup.events[event] || [], function(i, fn) {
fn();
});
}
@@ -25,21 +28,21 @@ frappe.wiz = {
frappe.pages['setup-wizard'].on_page_load = function(wrapper) {
// setup page ui
$(".navbar:first").toggle(false);
$("body").css({"padding-top":"30px"});

var requires = ["/assets/frappe/css/animate.min.css"].concat(frappe.boot.setup_wizard_requires || []);

frappe.require(requires, function() {
frappe.wiz.run_event("before_load");
frappe.setup.run_event("before_load");

var wizard_settings = {
page_name: "setup-wizard",
parent: wrapper,
slides: frappe.wiz.slides,
slides: frappe.setup.slides,
title: __("Welcome")
}

frappe.wizard = new frappe.wiz.Wizard(wizard_settings);
frappe.wiz.run_event("after_load");
frappe.wizard = new frappe.setup.Wizard(wizard_settings);
frappe.setup.run_event("after_load");

// frappe.wizard.values = test_values_edu;

@@ -56,7 +59,7 @@ frappe.pages['setup-wizard'].on_page_show = function(wrapper) {
}
}

frappe.wiz.Wizard = Class.extend({
frappe.setup.Wizard = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.make();
@@ -75,6 +78,7 @@ frappe.wiz.Wizard = Class.extend({
</div>', {html:html}))
},
show_working: function() {
$('header').find('.setup-wizard-brand').hide();
this.hide_current_slide();
frappe.set_route(this.page_name);
this.current_slide = {"$wrapper": this.get_message(this.working_html()).appendTo(this.parent)};
@@ -96,7 +100,7 @@ frappe.wiz.Wizard = Class.extend({
this.update_values();

if(!this.slide_dict[id]) {
this.slide_dict[id] = new frappe.wiz.WizardSlide($.extend(this.slides[id], {wiz:this, id:id}));
this.slide_dict[id] = new frappe.setup.WizardSlide($.extend(this.slides[id], {wiz:this, id:id}));
this.slide_dict[id].make();
}

@@ -147,8 +151,8 @@ frappe.wiz.Wizard = Class.extend({
args: {args: this.values},
callback: function(r) {
me.show_complete();
if(frappe.wiz.welcome_page) {
localStorage.setItem("session_last_route", frappe.wiz.welcome_page);
if(frappe.setup.welcome_page) {
localStorage.setItem("session_last_route", frappe.setup.welcome_page);
}
setTimeout(function() {
window.location = "/desk";
@@ -181,26 +185,27 @@ frappe.wiz.Wizard = Class.extend({

this.update_values();

frappe.wiz.slides = [];
frappe.wiz.run_event("before_load");
frappe.setup.slides = [];
frappe.setup.run_event("before_load");

// remove slides listed in remove_app_slides
var new_slides = [];
frappe.wiz.slides.forEach(function(slide) {
if(frappe.wiz.domain) {
frappe.setup.slides.forEach(function(slide) {
if(frappe.setup.domain) {
var domains = slide.domains;
if (domains.indexOf('all') !== -1 ||
domains.indexOf(frappe.wiz.domain.toLowerCase()) !== -1) {
domains.indexOf(frappe.setup.domain.toLowerCase()) !== -1) {
new_slides.push(slide);
}
} else {
new_slides.push(slide);
}
})
frappe.wiz.slides = new_slides;

this.slides = frappe.wiz.slides;
frappe.wiz.run_event("after_load");
frappe.setup.slides = new_slides;

this.slides = frappe.setup.slides;
frappe.setup.run_event("after_load");

// re-render all slides
this.slide_dict = {};
@@ -213,7 +218,7 @@ frappe.wiz.Wizard = Class.extend({
}
});

frappe.wiz.WizardSlide = Class.extend({
frappe.setup.WizardSlide = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.$wrapper = $('<div class="slide-wrapper hidden"></div>')
@@ -224,6 +229,24 @@ frappe.wiz.WizardSlide = Class.extend({
var me = this;
if(this.$body) this.$body.remove();

var fields = JSON.parse(JSON.stringify(this.fields));

if(this.add_more) {
this.count = 1;
fields = fields.map((field, i) => {
if(field.fieldname) {
field.fieldname += '_1';
}
if(i === 1 && this.mandatory_entry) {
field.reqd = 1;
}
if(!field.static) {
if(field.label) field.label += ' 1';
}
return field;
});
}

if(this.before_load) {
this.before_load(this);
}
@@ -234,7 +257,6 @@ frappe.wiz.WizardSlide = Class.extend({
main_title:__(this.wiz.title),
step: this.id + 1,
name: this.name,
css_class: this.css_class || "",
slides_count: this.wiz.slides.length
})).appendTo(this.$wrapper);

@@ -242,7 +264,7 @@ frappe.wiz.WizardSlide = Class.extend({

if(this.fields) {
this.form = new frappe.ui.FieldGroup({
fields: this.fields,
fields: fields,
body: this.body,
no_submit_on_enter: true
});
@@ -251,18 +273,36 @@ frappe.wiz.WizardSlide = Class.extend({
$(this.body).html(this.html);
}

this.set_reqd_fields();
this.set_init_values();
this.make_prev_next_buttons();
if(this.add_more) this.bind_more_button();

var $primary_btn = this.$next ? this.$next : this.$complete;

this.bind_fields_to_next($primary_btn);

if(this.onload) {
this.onload(this);
}
this.focus_first_input();
this.set_reqd_fields();
this.bind_fields_to_next($primary_btn);

this.reset_next($primary_btn);
this.focus_first_input();
},
set_reqd_fields: function() {
var dict = this.form.fields_dict;
this.reqd_fields = [];
Object.keys(dict).map(key => {
if(dict[key].df.reqd) {
this.reqd_fields.push(dict[key]);
}
});
},
set_init_values: function() {
var me = this;
// set values from frappe.wiz.values
// set values from frappe.setup.values
if(frappe.wizard.values && this.fields) {
this.fields.forEach(function(f) {
var value = frappe.wizard.values[f.fieldname];
@@ -284,6 +324,25 @@ frappe.wiz.WizardSlide = Class.extend({
return true;
},

bind_more_button: function() {
this.$more = this.$body.find('.more-btn');
this.$more.removeClass('hide')
.on('click', () => {
this.count++;
var fields = JSON.parse(JSON.stringify(this.fields));
this.form.add_fields(fields.map(field => {
if(field.fieldname) field.fieldname += '_' + this.count;
if(!field.static) {
if(field.label) field.label += ' ' + this.count;
}
return field;
}));
if(this.count === this.max_count) {
this.$more.addClass('hide');
}
});
},

make_prev_next_buttons: function() {
var me = this;

@@ -311,7 +370,7 @@ frappe.wiz.WizardSlide = Class.extend({
.click(this.next_or_complete.bind(this));
}

//setup mousefree navigation
// setup mousefree navigation
this.$body.on('keypress', function(e) {
if(e.which === 13) {
var $target = $(e.target);
@@ -326,6 +385,14 @@ frappe.wiz.WizardSlide = Class.extend({
}
});
},
bind_fields_to_next: function($primary_btn) {
var me = this;
this.reqd_fields.map((field) => {
field.$wrapper.on('change input', () => {
me.reset_next($primary_btn);
});
});
},
next_or_complete: function() {
if(this.set_values()) {
if(this.id+1 < this.wiz.slides.length) {
@@ -335,6 +402,17 @@ frappe.wiz.WizardSlide = Class.extend({
}
}
},
reset_next: function($primary_btn) {
var empty_fields = this.reqd_fields.filter((field) => {
return !field.get_value();
})

if(empty_fields.length) {
$primary_btn.addClass('disabled');
} else {
$primary_btn.removeClass('disabled');
}
},
focus_first_input: function() {
setTimeout(function() {
this.$body.find('.form-control').first().focus();
@@ -360,233 +438,310 @@ frappe.wiz.WizardSlide = Class.extend({
},
});

function load_frappe_slides() {
// language selection
frappe.wiz.welcome = {
var frappe_slides = [
{
// Welcome (language) slide
name: "welcome",
domains: ["all"],
title: __("Welcome"),
title: __("Hello!"),
icon: "fa fa-world",
help: __("Let's prepare the system for first use."),

fields: [
{ fieldname: "language", label: __("Select Your Language"), reqd:1,
fieldtype: "Select", "default": "english" },
{ fieldname: "language", label: __("Your Language"),
fieldtype: "Select", reqd: 1}
],

onload: function(slide) {
if (!frappe.wiz.welcome.data) {
frappe.wiz.welcome.load_languages(slide);
if (frappe.setup.data.lang) {
this.setup_fields(slide);
} else {
frappe.wiz.welcome.setup_fields(slide);
utils.load_languages(slide, this.setup_fields);
}
},

css_class: "single-column",
load_languages: function(slide) {
frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.load_languages",
freeze: true,
callback: function(r) {
frappe.wiz.welcome.data = r.message;
frappe.wiz.welcome.setup_fields(slide);

var language_field = slide.get_field("language");
language_field.set_input(frappe.wiz.welcome.data.default_language || "english");

if (!frappe.wiz._from_load_messages) {
language_field.$input.trigger("change");
}

delete frappe.wiz._from_load_messages;

moment.locale("en");
}
});
},

setup_fields: function(slide) {
var select = slide.get_field("language");
select.df.options = frappe.wiz.welcome.data.languages;
select.refresh();
frappe.wiz.welcome.bind_events(slide);
utils.setup_language_field(slide);
utils.bind_language_events(slide);
},

bind_events: function(slide) {
slide.get_input("language").unbind("change").on("change", function() {
var lang = $(this).val() || "english";
frappe._messages = {};
frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages",
freeze: true,
args: {
language: lang
},
callback: function(r) {
frappe.wiz._from_load_messages = true;
frappe.wizard.refresh_slides();
}
});
});
}
},

// region selection
frappe.wiz.region = {
{
// Region slide
name: 'region',
domains: ["all"],
title: __("Region"),
title: __("Select Your Region"),
icon: "fa fa-flag",
help: __("Select your Country, Time Zone and Currency"),
fields: [
{ fieldname: "country", label: __("Country"), reqd:1,
{ fieldname: "country", label: __("Your Country"), reqd:1,
fieldtype: "Select" },
{ fieldtype: "Section Break" },
{ fieldname: "timezone", label: __("Time Zone"), reqd:1,
fieldtype: "Select" },
{ fieldtype: "Column Break" },
{ fieldname: "currency", label: __("Currency"), reqd:1,
fieldtype: "Select" },
fieldtype: "Select" }
],

onload: function(slide) {
var _setup = function() {
frappe.wiz.region.setup_fields(slide);
frappe.wiz.region.bind_events(slide);
};

if(frappe.wiz.regional_data) {
_setup();
if(frappe.setup.data.regional_data) {
this.setup_fields(slide);
} else {
frappe.call({
method:"frappe.geo.country_info.get_country_timezone_info",
callback: function(data) {
frappe.wiz.regional_data = data.message;
_setup();
}
});
utils.load_regional_data(slide, this.setup_fields);
}
},
css_class: "single-column",

setup_fields: function(slide) {
var data = frappe.wiz.regional_data;
utils.setup_region_fields(slide);
utils.bind_region_events(slide);
}
},

slide.get_input("country").empty()
.add_options([""].concat(Object.keys(data.country_info).sort()));
{
// Profile slide
name: 'user',
domains: ["all"],
title: __("The First User: You"),
icon: "fa fa-user",
fields: [
{ "fieldtype":"Attach Image", "fieldname":"attach_user_image",
label: __("Attach Your Picture"), is_private: 0},
{ "fieldname": "full_name", "label": __("Full Name"), "fieldtype": "Data",
reqd:1},
{ "fieldname": "email", "label": __("Email Address") + ' (' + __("Will be your login ID") + ')',
"fieldtype": "Data", "options":"Email"},
{ "fieldname": "password", "label": __("Password"), "fieldtype": "Password" }
],
help: __('The first user will become the System Manager (you can change this later).'),
onload: function(slide) {
if(frappe.session.user!=="Administrator") {
slide.form.fields_dict.email.$wrapper.toggle(false);
slide.form.fields_dict.password.$wrapper.toggle(false);

// remove password field
delete slide.form.fields_dict.password;

slide.get_input("currency").empty()
.add_options(frappe.utils.unique([""].concat($.map(data.country_info,
function(opts, country) { return opts.currency; }))).sort());
if(frappe.boot.user.first_name || frappe.boot.user.last_name) {
slide.form.fields_dict.full_name.set_input(
[frappe.boot.user.first_name, frappe.boot.user.last_name].join(' ').trim());
}

slide.get_input("timezone").empty()
.add_options([""].concat(data.all_timezones));
var user_image = frappe.get_cookie("user_image");
var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper;

// set values if present
if(frappe.wizard.values.country) {
slide.get_field("country").set_input(frappe.wizard.values.country);
} else if (data.default_country) {
slide.get_field("country").set_input(data.default_country);
}
if(user_image) {
$attach_user_image.find(".missing-image").toggle(false);
$attach_user_image.find("img").attr("src", decodeURIComponent(user_image));
$attach_user_image.find(".img-container").toggle(true);
}
delete slide.form.fields_dict.email;

if(frappe.wizard.values.currency) {
slide.get_field("currency").set_input(frappe.wizard.values.currency);
}
} else {
slide.form.fields_dict.email.df.reqd = 1;
slide.form.fields_dict.email.refresh();
slide.form.fields_dict.password.df.reqd = 1;
slide.form.fields_dict.password.refresh();

if(frappe.wizard.values.timezone) {
slide.get_field("timezone").set_input(frappe.wizard.values.timezone);
utils.load_user_details(slide, this.setup_fields);
}
},

setup_fields: function(slide) {
if(frappe.setup.data.full_name) {
slide.form.fields_dict.full_name.set_input(frappe.setup.data.full_name);
}
if(frappe.setup.data.email) {
let email = frappe.setup.data.email;
slide.form.fields_dict.email.set_input(email);
if (frappe.get_gravatar(email, 200)) {
var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper;
$attach_user_image.find(".missing-image").toggle(false);
$attach_user_image.find("img").attr("src", frappe.get_gravatar(email, 200));
$attach_user_image.find(".img-container").toggle(true);
}
}
},
},
];

bind_events: function(slide) {
slide.get_input("country").on("change", function() {
var country = slide.get_input("country").val();
var $timezone = slide.get_input("timezone");
var data = frappe.wiz.regional_data;
var utils = {
load_languages: function(slide, callback) {
frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.load_languages",
freeze: true,
callback: function(r) {
frappe.setup.data.lang = r.message;
callback(slide);

$timezone.empty();
var language_field = slide.get_field("language");

// add country specific timezones first
if(country) {
var timezone_list = data.country_info[country].timezones || [];
$timezone.add_options(timezone_list.sort());
slide.get_field("currency").set_input(data.country_info[country].currency);
slide.get_field("currency").$input.trigger("change");
language_field.set_input(frappe.setup.data.default_language || "English");

if (!frappe.setup._from_load_messages) {
language_field.$input.trigger("change");
}
delete frappe.setup._from_load_messages;
moment.locale("en");
}
});
},

load_regional_data: function(slide, callback) {
frappe.call({
method:"frappe.geo.country_info.get_country_timezone_info",
callback: function(data) {
frappe.setup.data.regional_data = data.message;
callback(slide);
}
});
},

load_user_details: function(slide, callback) {
frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.load_user_details",
freeze: true,
callback: function(r) {
frappe.setup.data.full_name = r.message.full_name;
frappe.setup.data.email = r.message.email;
callback(slide);
}
})
},

// add all timezones at the end, so that user has the option to change it to any timezone
$timezone.add_options([""].concat(data.all_timezones));
setup_language_field: function(slide) {
var language_field = slide.get_field("language");
language_field.df.options = frappe.setup.data.lang.languages;
language_field.refresh();
},

slide.get_field("timezone").set_input($timezone.val());
setup_region_fields: function(slide) {
/*
Set a slide's country, timezone and currency fields
*/
var data = frappe.setup.data.regional_data;

// temporarily set date format
frappe.boot.sysdefaults.date_format = (data.country_info[country].date_format
|| "dd-mm-yyyy");
});
var country_field = slide.get_field('country');

slide.get_input("currency").on("change", function() {
var currency = slide.get_input("currency").val();
if (!currency) return;
frappe.model.with_doc("Currency", currency, function() {
frappe.provide("locals.:Currency." + currency);
var currency_doc = frappe.model.get_doc("Currency", currency);
var number_format = currency_doc.number_format;
if (number_format==="#.###") {
number_format = "#.###,##";
} else if (number_format==="#,###") {
number_format = "#,###.##"
}
slide.get_input("country").empty()
.add_options([""].concat(Object.keys(data.country_info).sort()));

frappe.boot.sysdefaults.number_format = number_format;
locals[":Currency"][currency] = $.extend({}, currency_doc);
});
});
slide.get_input("currency").empty()
.add_options(frappe.utils.unique([""].concat($.map(data.country_info,
function(opts, country) { return opts.currency; }))).sort());

slide.get_input("timezone").empty()
.add_options([""].concat(data.all_timezones));

// set values if present
if(frappe.wizard.values.country) {
country_field.set_input(frappe.wizard.values.country);
} else if (data.default_country) {
country_field.set_input(data.default_country);
}
},

if(frappe.wizard.values.currency) {
slide.get_field("currency").set_input(frappe.wizard.values.currency);
}

frappe.wiz.user = {
domains: ["all"],
title: __("The First User: You"),
icon: "fa fa-user",
fields: [
{"fieldname": "full_name", "label": __("Full Name"), "fieldtype": "Data",
reqd:1},
{"fieldname": "email", "label": __("Email Address"), "fieldtype": "Data",
reqd:1, "description": __("Login id"), "options":"Email"},
{"fieldname": "password", "label": __("Password"), "fieldtype": "Password",
reqd:1},
{fieldtype:"Attach Image", fieldname:"attach_user",
label: __("Attach Your Picture"), is_private: 0},
],
help: __('The first user will become the System Manager (you can change this later).'),
onload: function(slide) {
if(frappe.session.user!=="Administrator") {
slide.form.fields_dict.password.$wrapper.toggle(false);
slide.form.fields_dict.email.$wrapper.toggle(false);
if(frappe.boot.user.first_name || frappe.boot.user.last_name) {
slide.form.fields_dict.full_name.set_input(
[frappe.boot.user.first_name, frappe.boot.user.last_name].join(' ').trim());
if(frappe.wizard.values.timezone) {
slide.get_field("timezone").set_input(frappe.wizard.values.timezone);
}

country_field.df.description = 'fetching country...';
country_field.set_description();

// get location from IP (unreliable)
frappe.call({
method:"frappe.desk.page.setup_wizard.setup_wizard.load_country",
callback: function(r) {
if(r.message) {
slide.get_field("country").set_input(r.message);
slide.get_input("country").trigger('change');
}
country_field.df.description = '';
country_field.set_description();
}
});
},

var user_image = frappe.get_cookie("user_image");
if(user_image) {
var $attach_user = slide.form.fields_dict.attach_user.$wrapper;
$attach_user.find(".missing-image").toggle(false);
$attach_user.find("img").attr("src", decodeURIComponent(user_image)).toggle(true);
bind_language_events: function(slide) {
slide.get_input("language").unbind("change").on("change", function() {
var lang = $(this).val() || "English";
frappe._messages = {};
frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages",
freeze: true,
args: {
language: lang
},
callback: function(r) {
frappe.setup._from_load_messages = true;
frappe.wizard.refresh_slides();
}
});
});
},

delete slide.form.fields_dict.email;
delete slide.form.fields_dict.password;
bind_region_events: function(slide) {
/*
Bind a slide's country, timezone and currency fields
*/
slide.get_input("country").on("change", function() {
var country = slide.get_input("country").val();
var $timezone = slide.get_input("timezone");
var data = frappe.setup.data.regional_data;

$timezone.empty();

// add country specific timezones first
if(country) {
var timezone_list = data.country_info[country].timezones || [];
$timezone.add_options(timezone_list.sort());
slide.get_field("currency").set_input(data.country_info[country].currency);
slide.get_field("currency").$input.trigger("change");
}
},
css_class: "single-column"
};

// add all timezones at the end, so that user has the option to change it to any timezone
$timezone.add_options([""].concat(data.all_timezones));

slide.get_field("timezone").set_input($timezone.val());

// temporarily set date format
frappe.boot.sysdefaults.date_format = (data.country_info[country].date_format
|| "dd-mm-yyyy");
});

slide.get_input("currency").on("change", function() {
var currency = slide.get_input("currency").val();
if (!currency) return;
frappe.model.with_doc("Currency", currency, function() {
frappe.provide("locals.:Currency." + currency);
var currency_doc = frappe.model.get_doc("Currency", currency);
var number_format = currency_doc.number_format;
if (number_format==="#.###") {
number_format = "#.###,##";
} else if (number_format==="#,###") {
number_format = "#,###.##"
}

frappe.boot.sysdefaults.number_format = number_format;
locals[":Currency"][currency] = $.extend({}, currency_doc);
});
});
},

}

frappe.wiz.on("before_load", function() {
load_frappe_slides();
frappe.setup.on("before_load", function() {
// load slides
frappe_slides.map(frappe.setup.add_slide);

// add welcome slide
frappe.wiz.add_slide(frappe.wiz.welcome);
frappe.wiz.add_slide(frappe.wiz.region);
frappe.wiz.add_slide(frappe.wiz.user);
// set header image
let $icon = $('header .setup-wizard-brand');
if($icon.length === 0) {
$('header').append(`<div class="setup-wizard-brand"">
<img src="/assets/frappe/images/frappe-bird-grey.svg"
class="brand-icon frappe-icon" style="width:36px;"></div>`);
}
});

+ 17
- 1
frappe/desk/page/setup_wizard/setup_wizard.py Parādīt failu

@@ -179,11 +179,27 @@ def load_messages(language):

@frappe.whitelist()
def load_languages():
language_codes = frappe.db.sql('select language_code, language_name from tabLanguage order by name', as_dict=True)
codes_to_names = {}
for d in language_codes:
codes_to_names[d.language_code] = d.language_name
return {
"default_language": frappe.db.get_value('Language', frappe.local.lang, 'language_name') or frappe.local.lang,
"languages": sorted(frappe.db.sql_list('select language_name from tabLanguage order by name'))
"languages": sorted(frappe.db.sql_list('select language_name from tabLanguage order by name')),
"codes_to_names": codes_to_names
}

@frappe.whitelist()
def load_country():
from frappe.sessions import get_geo_ip_country
return get_geo_ip_country(frappe.local.request_ip) if frappe.local.request_ip else None

@frappe.whitelist()
def load_user_details():
return {
"full_name": frappe.cache().hget("full_name", "signup"),
"email": frappe.cache().hget("email", "signup")
}

def prettify_args(args):
# remove attachments


+ 8
- 4
frappe/desk/page/setup_wizard/setup_wizard_page.html Parādīt failu

@@ -1,14 +1,18 @@
<div class="container setup-wizard-slide {%= css_class %} with-form" data-slide-name="{%= name %}">
<div class="container setup-wizard-slide single-column with-form" data-slide-name="{%= name %}">
<div class="text-center setup-wizard-progress text-extra-muted">
{% for (var i=0; i < slides_count; i++) { %}
<i class="fa fa-fw fa-circle{% if (i+1<=step) { %} active {% } %}"></i>
<!--dev_mode: link progress bubbles-->
<!--<a href="http://erpnext.domainify:8000/desk#setup-wizard/{%= i %}">-->
<i class="fa fa-fw fa-circle{% if (i+1<=step) { %} active {% } %}"></i>
<!--</a>-->
{% } %}
</div>
<p class="text-center lead">{%= title %}</p>
<p class="lead">{%= title %}</p>
<div class="row">
<div class="col-sm-12">
<div class="setup-wizard-body col-sm-12">
<!-- {% if (help) { %} <p class="text-center">{%= help %}</p> {% } %} -->
<div class="form"></div>
<a class="more-btn hide btn btn-default btn-sm" style="margin-left: 41%;">{%= __("Add More") %}</a>
</div>
</div>
<div class="footer text-right">


+ 1
- 1
frappe/desk/query_report.py Parādīt failu

@@ -159,7 +159,7 @@ def export_query():
elif not row:
result.append([])
else:
result = result + data.result
result = result + [d for i,d in enumerate(data.result) if (i+1 in visible_idx)]

from frappe.utils.xlsxutils import make_xlsx
xlsx_file = make_xlsx(result, "Query Report")


Binārs
frappe/docs/assets/img/app-development/test-runner.png Parādīt failu

Pirms Pēc
Platums: 1972  |  Augstums: 1116  |  Izmērs: 258 KiB

+ 0
- 0
frappe/docs/user/en/guides/automated-testing/__init__.py Parādīt failu


+ 7
- 0
frappe/docs/user/en/guides/automated-testing/index.md Parādīt failu

@@ -0,0 +1,7 @@
# Automated Testing

Frappé Provides you a test framework to write and execute tests that can be run directly on a Continuous Integration Tool like Travis

You can write server-side unit tests or UI tests

{index}

+ 3
- 0
frappe/docs/user/en/guides/automated-testing/index.txt Parādīt failu

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

+ 49
- 0
frappe/docs/user/en/guides/automated-testing/integration-testing.md Parādīt failu

@@ -0,0 +1,49 @@
# UI Integration Testing

You can write integration tests using the Selenium Driver. `frappe.utils.selenium_driver` gives you a friendly API to write selenium based tests

To write integration tests, create a standard test case by creating a python file starting with `test_`

All integration tests will be run at the end of the unittests.

### Example

Here is an example of an integration test to check insertion of a To Do

from __future__ import print_function
from frappe.utils.selenium_testdriver import TestDriver
import unittest
import time

class TestToDo(unittest.TestCase):
def setUp(self):
self.driver = TestDriver()

def test_todo(self):
self.driver.login()

# list view
self.driver.set_route('List', 'ToDo')

time.sleep(2)

# new
self.driver.click_primary_action()

time.sleep(2)

# set input
self.driver.set_text_editor('description', 'hello')

# save
self.driver.click_modal_primary_action()

time.sleep(2)

self.assertTrue(self.driver.get_visible_element('.result-list')
.find_element_by_css_selector('.list-item')
.find_element_by_css_selector('.list-id').text=='hello')

def tearDown(self):
self.driver.close()


+ 70
- 0
frappe/docs/user/en/guides/automated-testing/qunit-testing.md Parādīt failu

@@ -0,0 +1,70 @@
# UI Testing with Frappe API

You can either write integration tests, or directly write tests in Javascript using [QUnit](http://api.qunitjs.com/)

QUnit helps you write UI tests using the UQuit framework and native frappe API. As you might have guessed, this is a much faster way of writing tests.

### Test Runner

To write QUnit based tests, add your tests in the `tests/ui` folder of your application. Your test files must begin with `test_` and end with `.js` extension.

To run your files, you can use the **Test Runner**. The **Test Runner** gives a user interface to load all your QUnit tests and run them in the browser.

In the CI, all QUnit tests are run by the **Test Runner** using `frappe/tests/test_test_runner.py`

<img src="{{docs_base_url}}/assets/img/app-development/test-runner.png" class="screenshot">

### Running Tests

To run a Test Runner based test, use the `run-ui-tests` bench command by passing the name of the file you want to run.

bench run-ui-tests --test frappe/tests/ui/test_list.js

This will pass the filename to `test_test_runner.py` that will load the required JS in the browser and execute the tests

### Adding Fixtures / Test Data

You can also add data that you require for all tests in the `tests/ui/data` folder of your app. All the files in this folder will be loaded in the browser before running the test.

The file `frappe/tests/ui/data/test_lib.js`, which contains library functions for testing is always loaded.

### Running All UI Tests

To run all UI tests together for your app run

bench run-ui-tests --app [app_name]

This will run all the files in your `tests/ui` folder one by one.

### Example QUnit Test

Here is the example of the To Do test in QUnit

QUnit.test("Test quick entry", function(assert) {
assert.expect(2);
let done = assert.async();
let random_text = frappe.utils.get_random(10);

frappe.run_serially([
() => frappe.set_route('List', 'ToDo'),
() => frappe.new_doc('ToDo'),
() => frappe.quick_entry.dialog.set_value('description', random_text),
() => frappe.quick_entry.insert(),
(doc) => {
assert.ok(doc && !doc.__islocal);
return frappe.set_route('Form', 'ToDo', doc.name);
},
() => assert.ok(cur_frm.doc.description.includes(random_text)),

// Delete the created ToDo
() => frappe.tests.click_page_head_item('Menu'),
() => frappe.tests.click_dropdown_item('Delete'),
() => frappe.tests.click_page_head_item('Yes'),

() => done()
]);
});

### Writing Test Friendly Code with Promises

Promises are a great way to write test-friendly code. If your method calls an aysnchronous call (ajax), then you should return an `Promise` object. While writing tests, if you encounter a function that does not return a `Promise` object, you should update the code to return a `Promise` object.

frappe/docs/user/en/guides/basics/writing-tests.md → frappe/docs/user/en/guides/automated-testing/unit-testing.md Parādīt failu

@@ -1,4 +1,4 @@
# Writing Tests Guide
# Unit Testing

## 1.Introduction

@@ -16,12 +16,12 @@ Frappe provides some basic tooling to quickly write automated tests. There are s
This function will build all the test dependencies and run your tests.
You should run tests from "frappe_bench" folder. Without options all tests will be run.

bench run-tests
bench run-tests

If you need more information about test execution - you can use verbose log level for bench.

bench --verbose run-tests
### Options:

--app <AppName>
@@ -30,9 +30,9 @@ If you need more information about test execution - you can use verbose log leve
--module <Module> (Run a particular module that has tests)
--profile (Runs a Python profiler on the test)
--junit-xml-output<PathToXML> (The command provides test results in the standard XUnit XML format)
#### 2.1. Example for app:
All applications are located in folder: "~/frappe-bench/apps".
All applications are located in folder: "~/frappe-bench/apps".
We can run tests for each application.

- frappe-bench/apps/erpnext/
@@ -50,7 +50,7 @@ We can run tests for each application.
.
----------------------------------------------------------------------
Ran 1 test in 0.008s
OK

#### 2.3. Example for test:
@@ -60,44 +60,44 @@ Run a specific case in User:
.
----------------------------------------------------------------------
Ran 1 test in 0.005s
OK

#### 2.4. Example for module:
If we want to run tests in the module:

/home/frappe/frappe-bench/apps/erpnext/erpnext/support/doctype/issue/test_issue.py
We should use module name like this (related to application folder)

erpnext.support.doctype.issue.test_issue
#####EXAMPLE:
frappe@erpnext:~/frappe-bench$ bench run-tests --module "erpnext.stock.doctype.stock_entry.test_stock_entry"
...........................
----------------------------------------------------------------------
Ran 27 tests in 30.549s

#### 2.5. Example for profile:

frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost" --profile
.
----------------------------------------------------------------------
Ran 1 test in 0.010s
OK
9133 function calls (8912 primitive calls) in 0.011 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
2 0.000 0.000 0.008 0.004 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:187(insert)
1 0.000 0.000 0.003 0.003 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:386(_validate)
13 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/database.py:77(sql)
255 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py:91(get)
12 0.000 0.000 0.002 0.000
12 0.000 0.000 0.002 0.000

#### 2.6. Example for XUnit XML:

@@ -118,7 +118,7 @@ We should use module name like this (related to application folder)
It’s designed for the CI Jenkins, but will work for anything else that understands an XUnit-formatted XML representation of test results.

#### Jenkins configuration support:
1. You should install xUnit plugin - https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin
1. You should install xUnit plugin - https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin
2. After installation open Jenkins job configuration, click the box named “Publish JUnit test result report” under the "Post-build Actions" and enter path to XML report:
(Example: _reports/*.xml_)

@@ -197,9 +197,3 @@ It’s designed for the CI Jenkins, but will work for anything else that underst
self.assertTrue("_Test Event 3" in subjects)
self.assertFalse("_Test Event 2" in subjects)


## 4. Client Side Testing (Using Selenium)

This feature is still under development.

For an example see, [https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py](https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py)

+ 1
- 1
frappe/docs/user/en/guides/basics/apps.md Parādīt failu

@@ -12,7 +12,7 @@ Frappe ships with a boiler plate for a new app. The command `bench make-app
app-name` helps you start a new app by starting an interactive shell.


% bench make-app sample_app
% bench new-app sample_app
App Name: sample_app
App Title: Sample App
App Description: This is a sample app.


+ 1
- 1
frappe/docs/user/fr/tutorial/web-views.md Parādīt failu

@@ -1,7 +1,7 @@
# Les vues web

Frappe a deux principaux environnements, le **bureau** et **le web**. Le **bureau** est un environnement riche AJAX alors
que **le web** est une collection plus traditionnelle de fichers HTML pour la consultation publique. Les vues web peuvent
que **le web** est une collection plus traditionnelle de fichiers HTML pour la consultation publique. Les vues web peuvent
aussi être générées pour créer des vues plus controllées pour les utilisateurs qui peuvent se connecter mais qui n'ont pas
accès au desk.



+ 2
- 2
frappe/email/doctype/auto_email_report/auto_email_report.json Parādīt failu

@@ -585,7 +585,7 @@
"label": "Format",
"length": 0,
"no_copy": 0,
"options": "HTML\nXLS\nCSV",
"options": "HTML\nXLSX\nCSV",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -669,7 +669,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-04-25 03:31:55.214149",
"modified": "2017-06-30 12:54:13.350902",
"modified_by": "Administrator",
"module": "Email",
"name": "Auto Email Report",


+ 11
- 7
frappe/email/doctype/auto_email_report/auto_email_report.py Parādīt failu

@@ -8,7 +8,7 @@ from frappe import _
from frappe.model.document import Document
from datetime import timedelta
import frappe.utils
from frappe.utils.xlsutils import get_xls
from frappe.utils.xlsxutils import make_xlsx
from frappe.utils.csvutils import to_csv

max_reports_per_user = 3
@@ -43,7 +43,7 @@ class AutoEmailReport(Document):

def validate_report_format(self):
""" check if user has select correct report format """
valid_report_formats = ["HTML", "XLS", "CSV"]
valid_report_formats = ["HTML", "XLSX", "CSV"]
if self.format not in valid_report_formats:
frappe.throw(_("%s is not a valid report format. Report format should \
one of the following %s"%(frappe.bold(self.format), frappe.bold(", ".join(valid_report_formats)))))
@@ -70,11 +70,14 @@ class AutoEmailReport(Document):
if self.format == 'HTML':
return self.get_html_table(columns, data)

elif self.format == 'XLS':
return get_xls(columns, data)
elif self.format == 'XLSX':
spreadsheet_data = self.get_spreadsheet_data(columns, data)
xlsx_file = make_xlsx(spreadsheet_data, "Auto Email Report")
return xlsx_file.getvalue()

elif self.format == 'CSV':
return self.get_csv(columns, data)
spreadsheet_data = self.get_spreadsheet_data(columns, data)
return to_csv(spreadsheet_data)

else:
frappe.throw(_('Invalid Output Format'))
@@ -85,7 +88,8 @@ class AutoEmailReport(Document):
'data': data
})

def get_csv(self, columns, data):
@staticmethod
def get_spreadsheet_data(columns, data):
out = [[df.label for df in columns], ]
for row in data:
new_row = []
@@ -93,7 +97,7 @@ class AutoEmailReport(Document):
for df in columns:
new_row.append(frappe.format(row[df.fieldname], df, row))

return to_csv(out)
return out

def get_file_name(self):
return "{0}.{1}".format(self.report.replace(" ", "-").replace("/", "-"), self.format.lower())


+ 1
- 1
frappe/email/doctype/auto_email_report/test_auto_email_report.py Parādīt failu

@@ -33,7 +33,7 @@ class TestAutoEmailReport(unittest.TestCase):
data = auto_email_report.get_report_content()
self.assertTrue('"Language","Core"' in data)

auto_email_report.format = 'XLS'
auto_email_report.format = 'XLSX'

data = auto_email_report.get_report_content()


+ 2
- 1
frappe/email/doctype/email_account/email_account.js Parādīt failu

@@ -119,7 +119,8 @@ frappe.ui.form.on("Email Account", {
},

show_gmail_message_for_less_secure_apps: function(frm) {
if(frm.doc.service==="Gmail") {
frm.dashboard.clear_headline();
if(frm.doc.service==="GMail") {
frm.dashboard.set_headline_alert('Gmail will only work if you allow access for less secure \
apps in Gmail settings. <a target="_blank" \
href="https://support.google.com/accounts/answer/6010255?hl=en">Read this for details</a>');


+ 2
- 1
frappe/email/doctype/email_account/email_account.py Parādīt failu

@@ -330,6 +330,8 @@ class EmailAccount(Document):
# gmail shows sent emails in inbox
# and we don't want emails sent by us to be pulled back into the system again
# dont count emails sent by the system get those
if frappe.flags.in_test:
print('WARN: Cannot pull email. Sender sames as recipient inbox')
raise SentEmailInInbox

if email.message_id:
@@ -472,7 +474,6 @@ class EmailAccount(Document):
parent = frappe._dict(doctype=self.append_to, name=parent[0].name)
return parent


def create_new_parent(self, communication, email):
'''If no parent found, create a new reference document'''



+ 1
- 0
frappe/email/doctype/email_alert/email_alert.js Parādīt failu

@@ -18,6 +18,7 @@ frappe.email_alert = {

// set value changed options
frm.set_df_property("value_changed", "options", [""].concat(options));
frm.set_df_property("set_property_after_alert", "options", [""].concat(options));

// set date changed options
frm.set_df_property("date_changed", "options", $.map(fields,


+ 141
- 4
frappe/email/doctype/email_alert/email_alert.json Parādīt failu

@@ -1,5 +1,6 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "Prompt",
@@ -13,6 +14,7 @@
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -24,6 +26,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enabled",
@@ -41,6 +44,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -51,6 +55,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Filters",
@@ -68,6 +73,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -79,6 +85,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 1,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Subject",
@@ -96,6 +103,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -106,6 +114,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Document Type",
@@ -124,6 +133,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -134,6 +144,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Standard",
@@ -152,6 +163,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -163,6 +175,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Module",
@@ -182,6 +195,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -192,6 +206,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
@@ -209,6 +224,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -219,6 +235,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Send Alert On",
@@ -237,6 +254,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -249,6 +267,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Trigger Method",
@@ -267,6 +286,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -279,6 +299,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference Date",
@@ -296,6 +317,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -309,6 +331,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Days Before or After",
@@ -326,6 +349,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -338,6 +362,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Value Changed",
@@ -355,6 +380,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -365,6 +391,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
@@ -382,6 +409,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -394,6 +422,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 1,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Condition",
@@ -411,6 +440,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -421,6 +451,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
@@ -437,6 +468,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -447,6 +479,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
@@ -464,6 +497,97 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "property_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Set Property After Alert",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "set_property_after_alert",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Set Property After Alert",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "property_value",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Value To Be Set",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -474,6 +598,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Recipients",
@@ -491,6 +616,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -501,6 +627,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Recipients",
@@ -519,6 +646,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -529,6 +657,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
@@ -546,18 +675,20 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Add your message here",
"depends_on": "",
"depends_on": "eval:!doc.is_standard",
"fieldname": "message",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 1,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
@@ -575,6 +706,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -585,6 +717,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Attach Print",
@@ -603,6 +736,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -613,6 +747,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message Examples",
@@ -631,6 +766,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -641,6 +777,7 @@
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "View Properties (via Customize Form)",
@@ -659,19 +796,19 @@
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-envelope",
"idx": 0,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2016-12-29 14:40:25.782293",
"modified": "2017-07-07 16:09:48.804218",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Alert",
@@ -688,7 +825,6 @@
"export": 1,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
@@ -703,6 +839,7 @@
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",


+ 5
- 0
frappe/email/doctype/email_alert/email_alert.py Parādīt failu

@@ -159,6 +159,11 @@ def get_context(context):
reference_name = doc.name,
attachments = attachments)

if self.set_property_after_alert:
frappe.db.set_value(doc.doctype, doc.name, self.set_property_after_alert,
self.property_value, update_modified = False)
doc.set(self.set_property_after_alert, self.property_value)

def load_standard_properties(self, context):
module = get_doc_module(self.module, self.doctype, self.name)
if module:


+ 6
- 1
frappe/email/doctype/email_alert/test_email_alert.py Parādīt failu

@@ -7,6 +7,8 @@ import unittest

test_records = frappe.get_test_records('Email Alert')

test_dependencies = ["User"]

class TestEmailAlert(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabEmail Queue`""")
@@ -32,6 +34,9 @@ class TestEmailAlert(unittest.TestCase):
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Communication",
"reference_name": communication.name, "status":"Not Sent"}))

self.assertEquals(frappe.db.get_value('Communication',
communication.name, 'subject'), '__testing__')

def test_condition(self):
event = frappe.new_doc("Event")
event.subject = "test",
@@ -137,7 +142,7 @@ class TestEmailAlert(unittest.TestCase):
event.save()

# Value Change email alert alert will be trigger as description is not changed
# mail will not be sent
# mail will not be sent
self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))



+ 3
- 1
frappe/email/doctype/email_alert/test_records.json Parādīt failu

@@ -21,7 +21,9 @@
"condition": "doc.communication_type=='Comment'",
"recipients": [
{ "email_by_document_field": "owner" }
]
],
"set_property_after_alert": "subject",
"property_value": "__testing__"
},
{
"doctype": "Email Alert",


+ 1
- 1
frappe/email/doctype/email_group/email_group.py Parādīt failu

@@ -26,7 +26,7 @@ class EmailGroup(Document):

for user in frappe.db.get_all(doctype, [email_field, unsubscribed_field or "name"]):
try:
email = parse_addr(user.get(email_field))[1]
email = parse_addr(user.get(email_field))[1] if user.get(email_field) else None
if email:
frappe.get_doc({
"doctype": "Email Group Member",


+ 49
- 2
frappe/email/doctype/email_queue/email_queue.json Parādīt failu

@@ -1,5 +1,6 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash",
@@ -14,6 +15,7 @@
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -43,6 +45,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -72,6 +75,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -101,6 +105,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -129,6 +134,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -159,6 +165,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -187,6 +194,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -216,6 +224,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -245,6 +254,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -273,6 +283,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -303,6 +314,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -332,6 +344,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -362,6 +375,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -392,6 +406,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -421,6 +436,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -450,6 +466,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -477,20 +494,50 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "attachments",
"fieldtype": "Code",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Attachments",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-envelope",
"idx": 1,
"image_view": 0,
"in_create": 1,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-02-24 17:42:10.878546",
"modified": "2017-07-07 16:29:15.780393",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Queue",


+ 3
- 2
frappe/email/doctype/newsletter/newsletter.py Parādīt failu

@@ -70,8 +70,9 @@ class Newsletter(Document):

for file in files:
try:
file = get_file(file.name)
attachments.append({"fname": file[0], "fcontent": file[1]})
# these attachments will be attached on-demand
# and won't be stored in the message
attachments.append({"fid": file.name})
except IOError:
frappe.throw(_("Unable to find attachment {0}").format(a))



+ 175
- 65
frappe/email/email_body.py Parādīt failu

@@ -2,7 +2,7 @@
# MIT License. See license.txt

from __future__ import unicode_literals
import frappe, re
import frappe, re, os
from frappe.utils.pdf import get_pdf
from frappe.email.smtp import get_outgoing_email_account
from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint,
@@ -15,21 +15,31 @@ from email.mime.multipart import MIMEMultipart
def get_email(recipients, sender='', msg='', subject='[No Subject]',
text_content = None, footer=None, print_html=None, formatted=None, attachments=None,
content=None, reply_to=None, cc=[], email_account=None, expose_recipients=None,
inline_images=[]):
"""send an html email as multipart with attachments and all"""
inline_images=[], header=False):
""" Prepare an email with the following format:
- multipart/mixed
- multipart/alternative
- text/plain
- multipart/related
- text/html
- inline image
- attachment
"""
content = content or msg
emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, email_account=email_account, expose_recipients=expose_recipients)

if not content.strip().startswith("<"):
content = markdown(content)

emailobj.set_html(content, text_content, footer=footer,
emailobj.set_html(content, text_content, footer=footer, header=header,
print_html=print_html, formatted=formatted, inline_images=inline_images)

if isinstance(attachments, dict):
attachments = [attachments]

for attach in (attachments or []):
# cannot attach if no filecontent
if attach.get('fcontent') is None: continue
emailobj.add_attachment(**attach)

return emailobj
@@ -58,18 +68,19 @@ class EMail:
self.expose_recipients = expose_recipients

self.msg_root = MIMEMultipart('mixed')
self.msg_multipart = MIMEMultipart('alternative')
self.msg_root.attach(self.msg_multipart)
self.msg_alternative = MIMEMultipart('alternative')
self.msg_root.attach(self.msg_alternative)
self.cc = cc or []
self.html_set = False

self.email_account = email_account or get_outgoing_email_account()

def set_html(self, message, text_content = None, footer=None, print_html=None,
formatted=None, inline_images=None):
formatted=None, inline_images=None, header=False):
"""Attach message in the html portion of multipart/alternative"""
if not formatted:
formatted = get_formatted_html(self.subject, message, footer, print_html, email_account=self.email_account)
formatted = get_formatted_html(self.subject, message, footer, print_html,
email_account=self.email_account, header=header)

# this is the first html part of a multi-part message,
# convert to text well
@@ -88,33 +99,33 @@ class EMail:
"""
from email.mime.text import MIMEText
part = MIMEText(message, 'plain', 'utf-8')
self.msg_multipart.attach(part)
self.msg_alternative.attach(part)

def set_part_html(self, message, inline_images):
from email.mime.text import MIMEText
if inline_images:
related = MIMEMultipart('related')

for image in inline_images:
# images in dict like {filename:'', filecontent:'raw'}
content_id = random_string(10)
has_inline_images = re.search('''embed=['"].*?['"]''', message)

# replace filename in message with CID
message = re.sub('''src=['"]{0}['"]'''.format(image.get('filename')),
'src="cid:{0}"'.format(content_id), message)
if has_inline_images:
# process inline images
message, _inline_images = replace_filename_with_cid(message)

self.add_attachment(image.get('filename'), image.get('filecontent'),
None, content_id=content_id, parent=related)
# prepare parts
msg_related = MIMEMultipart('related')

html_part = MIMEText(message, 'html', 'utf-8')
related.attach(html_part)
msg_related.attach(html_part)

self.msg_multipart.attach(related)
for image in _inline_images:
self.add_attachment(image.get('filename'), image.get('filecontent'),
content_id=image.get('content_id'), parent=msg_related, inline=True)

self.msg_alternative.attach(msg_related)
else:
self.msg_multipart.attach(MIMEText(message, 'html', 'utf-8'))
self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8'))

def set_html_as_text(self, html):
"""return html2text"""
"""Set plain text from HTML"""
self.set_text(to_markdown(html))

def set_message(self, message, mime_type='text/html', as_attachment=0, filename='attachment.html'):
@@ -139,50 +150,13 @@ class EMail:
self.add_attachment(res[0], res[1])

def add_attachment(self, fname, fcontent, content_type=None,
parent=None, content_id=None):
parent=None, content_id=None, inline=False):
"""add attachment"""
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.text import MIMEText

import mimetypes
if not content_type:
content_type, encoding = mimetypes.guess_type(fname)

if content_type is None:
# No guess could be made, or the file is encoded (compressed), so
# use a generic bag-of-bits type.
content_type = 'application/octet-stream'

maintype, subtype = content_type.split('/', 1)
if maintype == 'text':
# Note: we should handle calculating the charset
if isinstance(fcontent, unicode):
fcontent = fcontent.encode("utf-8")
part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8")
elif maintype == 'image':
part = MIMEImage(fcontent, _subtype=subtype)
elif maintype == 'audio':
part = MIMEAudio(fcontent, _subtype=subtype)
else:
part = MIMEBase(maintype, subtype)
part.set_payload(fcontent)
# Encode the payload using Base64
from email import encoders
encoders.encode_base64(part)

# Set the filename parameter
if fname:
part.add_header(b'Content-Disposition',
("attachment; filename=\"%s\"" % fname).encode('utf-8'))
if content_id:
part.add_header(b'Content-ID', '<{0}>'.format(content_id))

if not parent:
parent = self.msg_root

parent.attach(part)
add_attachment(fname, fcontent, content_type, parent, content_id, inline)

def add_pdf_attachment(self, name, html, options=None):
self.add_attachment(name, get_pdf(html, options), 'application/octet-stream')
@@ -259,11 +233,12 @@ class EMail:
self.make()
return self.msg_root.as_string()

def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None):
def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=False):
if not email_account:
email_account = get_outgoing_email_account(False)

rendered_email = frappe.get_template("templates/emails/standard.html").render({
"header": get_header() if header else None,
"content": message,
"signature": get_signature(email_account),
"footer": get_footer(email_account, footer),
@@ -274,6 +249,52 @@ def get_formatted_html(subject, message, footer=None, print_html=None, email_acc

return scrub_urls(rendered_email)

def add_attachment(fname, fcontent, content_type=None,
parent=None, content_id=None, inline=False):
"""Add attachment to parent which must an email object"""
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.text import MIMEText

import mimetypes
if not content_type:
content_type, encoding = mimetypes.guess_type(fname)

if not parent:
return

if content_type is None:
# No guess could be made, or the file is encoded (compressed), so
# use a generic bag-of-bits type.
content_type = 'application/octet-stream'

maintype, subtype = content_type.split('/', 1)
if maintype == 'text':
# Note: we should handle calculating the charset
if isinstance(fcontent, unicode):
fcontent = fcontent.encode("utf-8")
part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8")
elif maintype == 'image':
part = MIMEImage(fcontent, _subtype=subtype)
elif maintype == 'audio':
part = MIMEAudio(fcontent, _subtype=subtype)
else:
part = MIMEBase(maintype, subtype)
part.set_payload(fcontent)
# Encode the payload using Base64
from email import encoders
encoders.encode_base64(part)

# Set the filename parameter
if fname:
attachment_type = 'inline' if inline else 'attachment'
part.add_header(b'Content-Disposition', attachment_type, filename=fname.encode('utf=8'))
if content_id:
part.add_header(b'Content-ID', '<{0}>'.format(content_id))

parent.attach(part)

def get_message_id():
'''Returns Message ID created from doctype and name'''
return "<{unique}@{site}>".format(
@@ -298,11 +319,100 @@ def get_footer(email_account, footer=None):
company_address = frappe.db.get_default("email_footer_address")

if company_address:
footer += '<div style="margin: 15px auto; text-align: center; color: #8d99a6">{0}</div>'\
.format(company_address.replace("\n", "<br>"))
company_address = company_address.splitlines(True)
footer += '<table width="100%" border=0>'
footer += '<tr><td height=20></td></tr>'
for x in company_address:
footer += '<tr style="margin: 15px auto; text-align: center; color: #8d99a6"><td>{0}</td></tr>'\
.format(x)
footer += "</table>"

if not cint(frappe.db.get_default("disable_standard_email_footer")):
for default_mail_footer in frappe.get_hooks("default_mail_footer"):
footer += '<div style="margin: 15px auto;">{0}</div>'.format(default_mail_footer)

return footer

def replace_filename_with_cid(message):
""" Replaces <img embed="assets/frappe/images/filename.jpg" ...> with
<img src="cid:content_id" ...> and return the modified message and
a list of inline_images with {filename, filecontent, content_id}
"""

inline_images = []

while True:
matches = re.search('''embed=["'](.*?)["']''', message)
if not matches: break
groups = matches.groups()

# found match
img_path = groups[0]
filename = img_path.rsplit('/')[-1]

filecontent = get_filecontent_from_path(img_path)
if not filecontent:
message = re.sub('''embed=['"]{0}['"]'''.format(img_path), '', message)
continue

content_id = random_string(10)

inline_images.append({
'filename': filename,
'filecontent': filecontent,
'content_id': content_id
})

message = re.sub('''embed=['"]{0}['"]'''.format(img_path),
'src="cid:{0}"'.format(content_id), message)

return (message, inline_images)

def get_filecontent_from_path(path):
if not path: return

if path.startswith('/'):
path = path[1:]

if path.startswith('assets/'):
# from public folder
full_path = os.path.abspath(path)
elif path.startswith('files/'):
# public file
full_path = frappe.get_site_path('public', path)
elif path.startswith('private/files/'):
# private file
full_path = frappe.get_site_path(path)
else:
full_path = path

if os.path.exists(full_path):
with open(full_path) as f:
filecontent = f.read()

return filecontent
else:
print(full_path + ' doesn\'t exists')
return None


def get_header():
""" Build header from template """
from frappe.utils.jinja import get_email_from_template

default_brand_image = 'assets/frappe/images/favicon.png' # svg doesn't work in email
email_brand_image = frappe.get_hooks('email_brand_image')
if len(email_brand_image):
email_brand_image = email_brand_image[-1]
else:
email_brand_image = default_brand_image

email_brand_image = default_brand_image
brand_text = frappe.get_hooks('app_title')[-1]

email_header, text = get_email_from_template('email_header', {
'brand_image': email_brand_image,
'brand_text': brand_text
})

return email_header

+ 56
- 17
frappe/email/queue.py Parādīt failu

@@ -5,29 +5,32 @@ from __future__ import unicode_literals
from six.moves import range
import frappe
import HTMLParser
import smtplib, quopri
import smtplib, quopri, json
from frappe import msgprint, throw, _
from frappe.email.smtp import SMTPServer, get_outgoing_email_account
from frappe.email.email_body import get_email, get_formatted_html
from frappe.email.email_body import get_email, get_formatted_html, add_attachment
from frappe.utils.verified_command import get_signed_params, verify_request
from html2text import html2text
from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint
from frappe.utils.file_manager import get_file
from rq.timeouts import JobTimeoutException
from frappe.utils.scheduler import log

class EmailLimitCrossedError(frappe.ValidationError): pass

def send(recipients=None, sender=None, subject=None, message=None, reference_doctype=None,
def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None,
reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
attachments=None, reply_to=None, cc=[], message_id=None, in_reply_to=None, send_after=None,
expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None,
queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None):
queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None,
header=False):
"""Add email to sending queue (Email Queue)

:param recipients: List of recipients.
:param sender: Email sender.
:param subject: Email subject.
:param message: Email message.
:param text_content: Text version of email message.
:param reference_doctype: Reference DocType of caller document.
:param reference_name: Reference name of caller document.
:param send_priority: Priority for Email Queue, default 1.
@@ -43,6 +46,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
:param is_notification: Marks email as notification so will not trigger notifications from system
:param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1.
:param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id
:param header: Append header in email (boolean)
"""
if not unsubscribe_method:
unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe"
@@ -65,12 +69,13 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc

check_email_limit(recipients)

formatted = get_formatted_html(subject, message, email_account=email_account)
if not text_content:
try:
text_content = html2text(message)
except HTMLParser.HTMLParseError:
text_content = "See html attachment"

try:
text_content = html2text(formatted)
except HTMLParser.HTMLParseError:
text_content = "See html attachment"
formatted = get_formatted_html(subject, message, email_account=email_account, header=header)

if reference_doctype and reference_name:
unsubscribed = [d.email for d in frappe.db.get_all("Email Unsubscribe", "email",
@@ -114,6 +119,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
queue_separately=queue_separately,
is_notification = is_notification,
inline_images = inline_images,
header=header,
now=now)


@@ -143,6 +149,14 @@ def get_email_queue(recipients, sender, subject, **kwargs):
'''Make Email Queue object'''
e = frappe.new_doc('Email Queue')
e.priority = kwargs.get('send_priority')
attachments = kwargs.get('attachments')
if attachments:
# store attachments with fid, to be attached on-demand later
_attachments = []
for att in attachments:
if att.get('fid'):
_attachments.append(att)
e.attachments = json.dumps(_attachments)

try:
mail = get_email(recipients,
@@ -155,7 +169,8 @@ def get_email_queue(recipients, sender, subject, **kwargs):
cc=kwargs.get('cc'),
email_account=kwargs.get('email_account'),
expose_recipients=kwargs.get('expose_recipients'),
inline_images=kwargs.get('inline_images'))
inline_images=kwargs.get('inline_images'),
header=kwargs.get('header'))

mail.set_message_id(kwargs.get('message_id'),kwargs.get('is_notification'))
if kwargs.get('read_receipt'):
@@ -331,7 +346,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
email = frappe.db.sql('''select
name, status, communication, message, sender, reference_doctype,
reference_name, unsubscribe_param, unsubscribe_method, expose_recipients,
show_as_cc, add_unsubscribe_link
show_as_cc, add_unsubscribe_link, attachments
from
`tabEmail Queue`
where
@@ -424,6 +439,7 @@ where name=%s""", (unicode(e), email.name), auto_commit=auto_commit)
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)

if now:
print(frappe.get_traceback())
raise e

else:
@@ -457,7 +473,31 @@ def prepare_message(email, recipient, recipients_list):
message = message.replace("<!--cc message-->", quopri.encodestring(email_sent_message))

message = message.replace("<!--recipient-->", recipient)
return message

if not email.attachments:
return message

# On-demand attachments
from email.parser import Parser

msg_obj = Parser().parsestr(message)
attachments = json.loads(email.attachments)

for attachment in attachments:
if attachment.get('fcontent'): continue

fid = attachment.get('fid')
if not fid: continue

fname, fcontent = get_file(fid)
attachment.update({
'fname': fname,
'fcontent': fcontent,
'parent': msg_obj
})
add_attachment(**attachment)

return msg_obj.as_string()

def clear_outbox():
"""Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days.
@@ -475,8 +515,7 @@ def clear_outbox():
frappe.db.sql("""delete from `tabEmail Queue Recipient` where parent in (%s)"""
% ','.join(['%s']*len(email_queues)), tuple(email_queues))

for dt in ("Email Queue", "Email Queue Recipient"):
frappe.db.sql("""
update `tab{0}`
set status='Expired'
where datediff(curdate(), modified) > 7 and status='Not Sent'""".format(dt))
frappe.db.sql("""
update `tabEmail Queue`
set status='Expired'
where datediff(curdate(), modified) > 7 and status='Not Sent' and (send_after is null or send_after < %(now)s)""", { 'now': now_datetime() })

+ 101
- 0
frappe/email/test_email_body.py Parādīt failu

@@ -0,0 +1,101 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals

import frappe, unittest, os, base64
from frappe.email.email_body import replace_filename_with_cid, get_email

class TestEmailBody(unittest.TestCase):
def setUp(self):
email_html = '''
<div>
<h3>Hey John Doe!</h3>
<p>This is embedded image you asked for</p>
<img embed="assets/frappe/images/favicon.png" />
</div>
'''
email_text = '''
Hey John Doe!
This is the text version of this email
'''

img_path = os.path.abspath('assets/frappe/images/favicon.png')
with open(img_path) as f:
img_content = f.read()
img_base64 = base64.b64encode(img_content)

# email body keeps 76 characters on one line
self.img_base64 = fixed_column_width(img_base64, 76)

self.email_string = get_email(
recipients=['test@example.com'],
sender='me@example.com',
subject='Test Subject',
content=email_html,
text_content=email_text
).as_string()


def test_image(self):
img_signature = '''
Content-Type: image/png
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="favicon.png"
'''

self.assertTrue(img_signature in self.email_string)
self.assertTrue(self.img_base64 in self.email_string)


def test_text_content(self):
text_content = '''
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable


Hey John Doe!
This is the text version of this email
'''
self.assertTrue(text_content in self.email_string)


def test_email_content(self):
html_head = '''
Content-Type: text/html; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.=
w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns=3D"http://www.w3.org/1999/xhtml">
'''

html = '''<h3>Hey John Doe!</h3>'''

self.assertTrue(html_head in self.email_string)
self.assertTrue(html in self.email_string)


def test_replace_filename_with_cid(self):
original_message = '''
<div>
<img embed="assets/frappe/images/favicon.png" alt="test" />
<img embed="notexists.jpg" />
</div>
'''
message, inline_images = replace_filename_with_cid(original_message)

processed_message = '''
<div>
<img src="cid:{0}" alt="test" />
<img />
</div>
'''.format(inline_images[0].get('content_id'))
self.assertEquals(message, processed_message)


def fixed_column_width(string, chunk_size):
parts = [string[0+i:chunk_size+i] for i in range(0, len(string), chunk_size)]
return '\n'.join(parts)

+ 1
- 1
frappe/frappeclient.py Parādīt failu

@@ -102,7 +102,7 @@ class FrappeClient(object):
:param doctype: `doctype` to be deleted
:param name: `name` of document to be deleted'''
return self.post_request({
"cmd": "frappe.model.delete_doc",
"cmd": "frappe.client.delete",
"doctype": doctype,
"name": name
})


+ 3
- 0
frappe/integrations/doctype/dropbox_settings/dropbox_settings.py Parādīt failu

@@ -272,6 +272,9 @@ def set_dropbox_access_token(access_token):
frappe.db.commit()

def generate_oauth2_access_token_from_oauth1_token(dropbox_settings=None):
if not dropbox_settings.get("access_key") or not dropbox_settings.get("access_secret"):
return {}

url = "https://api.dropboxapi.com/2/auth/token/from_oauth1"
headers = {"Content-Type": "application/json"}
auth = (dropbox_settings["app_key"], dropbox_settings["app_secret"])


+ 1
- 2
frappe/model/base_document.py Parādīt failu

@@ -423,12 +423,11 @@ class BaseDocument(object):
return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label))

elif self.parentfield:

return "{}: {} {} #{}: {}: {}".format(_("Error"), frappe.bold(_(self.doctype)),
_("Row"), self.idx, _("Value missing for"), _(df.label))

else:
return "{}: {}: {}".format(_("Error"), _("Value missing for"), _(df.label))
return _("Error: Value missing for {0}: {1}").format(_(df.parent), _(df.label))

missing = []



+ 10
- 5
frappe/model/db_query.py Parādīt failu

@@ -423,7 +423,6 @@ class DatabaseQuery(object):
def add_user_permissions(self, user_permissions, user_permission_doctypes=None):
user_permission_doctypes = frappe.permissions.get_user_permission_doctypes(user_permission_doctypes, user_permissions)
meta = frappe.get_meta(self.doctype)

for doctypes in user_permission_doctypes:
match_filters = {}
match_conditions = []
@@ -431,12 +430,18 @@ class DatabaseQuery(object):
for df in meta.get_fields_to_check_permissions(doctypes):
user_permission_values = user_permissions.get(df.options, [])

condition = 'ifnull(`tab{doctype}`.`{fieldname}`, "")=""'.format(doctype=self.doctype, fieldname=df.fieldname)
cond = 'ifnull(`tab{doctype}`.`{fieldname}`, "")=""'.format(doctype=self.doctype, fieldname=df.fieldname)
if user_permission_values:
condition += """ or `tab{doctype}`.`{fieldname}` in ({values})""".format(
if not cint(frappe.get_system_settings("apply_strict_user_permissions")):
condition = cond + " or "
else:
condition = ""
condition += """`tab{doctype}`.`{fieldname}` in ({values})""".format(
doctype=self.doctype, fieldname=df.fieldname,
values=", ".join([('"'+frappe.db.escape(v, percent=False)+'"') for v in user_permission_values])
)
values=", ".join([('"'+frappe.db.escape(v, percent=False)+'"') for v in user_permission_values]))
else:
condition = cond

match_conditions.append("({condition})".format(condition=condition))

match_filters[df.options] = user_permission_values


+ 9
- 1
frappe/model/db_schema.py Parādīt failu

@@ -43,6 +43,7 @@ type_map = {
,'Attach': ('text', '')
,'Attach Image':('text', '')
,'Signature': ('longtext', '')
,'Color': ('varchar', varchar_len)
}

default_columns = ['name', 'creation', 'modified', 'modified_by', 'owner',
@@ -307,7 +308,7 @@ class DbTable:
if not frappe.db.sql("show index from `%s` where key_name = %s" %
(self.name, '%s'), col.fieldname):
query.append("add index `{}`(`{}`)".format(col.fieldname, col.fieldname))
for col in self.drop_index:
if col.fieldname != 'name': # primary key
# if index key exists
@@ -563,6 +564,13 @@ def validate_column_name(n):
frappe.throw(_("Fieldname {0} cannot have special characters like {1}").format(cstr(n), special_characters), InvalidColumnName)
return n

def validate_column_length(fieldname):
""" In MySQL maximum column length is 64 characters,
ref: https://dev.mysql.com/doc/refman/5.5/en/identifiers.html"""

if len(fieldname) > 64:
frappe.throw(_("Fieldname is limited to 64 characters ({0})").format(fieldname))

def remove_all_foreign_keys():
frappe.db.sql("set foreign_key_checks = 0")
frappe.db.commit()


+ 1
- 0
frappe/model/delete_doc.py Parādīt failu

@@ -59,6 +59,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
frappe.db.sql("delete from `tabCustom Script` where dt = %s", name)
frappe.db.sql("delete from `tabProperty Setter` where doc_type = %s", name)
frappe.db.sql("delete from `tabReport` where ref_doctype=%s", name)
frappe.db.sql("delete from `tabCustom DocPerm` where parent=%s", name)

delete_from_table(doctype, name, ignore_doctypes, None)



+ 6
- 0
frappe/model/naming.py Parādīt failu

@@ -99,6 +99,9 @@ def make_autoname(key='', doctype='', doc=''):

def parse_naming_series(parts, doctype= '', doc = ''):
n = ''
if isinstance(parts, basestring):
parts = parts.split('.')

series_set = False
today = now_datetime()
for e in parts:
@@ -142,6 +145,9 @@ def getseries(key, digits, doctype=''):
def revert_series_if_last(key, name):
if ".#" in key:
prefix, hashes = key.rsplit(".", 1)
if '.' in prefix:
prefix = parse_naming_series(prefix.split('.'))

if "#" not in hashes:
return
else:


+ 1
- 0
frappe/model/rename_doc.py Parādīt failu

@@ -162,6 +162,7 @@ def update_link_field_values(link_fields, old, new, doctype):
single_doc.set(field['fieldname'], new)
# update single docs using ORM rather then query
# as single docs also sometimes sets defaults!
single_doc.flags.ignore_mandatory = True
single_doc.save(ignore_permissions=True)
except ImportError:
# fails in patches where the doctype has been renamed


+ 2
- 2
frappe/modules/utils.py Parādīt failu

@@ -180,8 +180,8 @@ def load_doctype_module(doctype, module=None, prefix="", suffix=""):
try:
if key not in doctype_python_modules:
doctype_python_modules[key] = frappe.get_module(module_name)
except ImportError:
raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name))
except ImportError, e:
raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name + ' Error: ' + str(e)))

return doctype_python_modules[key]



+ 0
- 12
frappe/nightwatch.global.js Parādīt failu

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

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

+ 0
- 96
frappe/nightwatch.js Parādīt failu

@@ -1,96 +0,0 @@
const fs = require('fs');

const ci_mode = get_cli_arg('env') === 'ci_server';
const site_name = get_cli_arg('site');
let app_name = get_cli_arg('app');

if(!app_name) {
console.log('Please specify app to run tests');
return;
}

if(!ci_mode && !site_name) {
console.log('Please specify site to run tests');
return;
}

// site url
let site_url;
if(site_name) {
site_url = 'http://' + site_name + ':' + get_port();
}

// multiple apps
if(app_name.includes(',')) {
app_name = app_name.split(',');
} else {
app_name = [app_name];
}

let test_folders = [];
let page_objects = [];
for(const app of app_name) {
const test_folder = `apps/${app}/${app}/tests/ui`;
const page_object = test_folder + '/page_objects';

if(!fs.existsSync(test_folder)) {
console.log(`No test folder found for "${app}"`);
continue;
}
test_folders.push(test_folder);

if(fs.existsSync(page_object)) {
page_objects.push();
}
}

const config = {
"src_folders": test_folders,
"globals_path" : 'apps/frappe/frappe/nightwatch.global.js',
"page_objects_path": page_objects,
"selenium": {
"start_process": false
},
"test_settings": {
"default": {
"launch_url": site_url,
"selenium_port": 9515,
"selenium_host": "127.0.0.1",
"default_path_prefix": "",
"silent": true,
// "screenshots": {
// "enabled": true,
// "path": SCREENSHOT_PATH
// },
"globals": {
"waitForConditionTimeout": 15000
},
"desiredCapabilities": {
"browserName": "chrome",
"chromeOptions": {
"args": ["--no-sandbox", "--start-maximized"]
},
"javascriptEnabled": true,
"acceptSslCerts": true
}
},
"ci_server": {
"launch_url": 'http://localhost:8000'
}
}
}
module.exports = config;

function get_cli_arg(key) {
var args = process.argv;
var i = args.indexOf('--' + key);
if(i === -1) {
return null;
}
return args[i + 1];
}

function get_port() {
var bench_config = JSON.parse(fs.readFileSync('sites/common_site_config.json'));
return bench_config.webserver_port;
}

+ 2
- 0
frappe/patches.txt Parādīt failu

@@ -186,3 +186,5 @@ frappe.patches.v8_0.update_desktop_icons
frappe.patches.v8_0.update_gender_and_salutation
execute:frappe.db.sql('update tabReport set module="Desk" where name="ToDo"')
frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings
frappe.patches.v8_1.update_format_options_in_auto_email_report
frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists

+ 6
- 0
frappe/patches/v8_1/delete_custom_docperm_if_doctype_not_exists.py Parādīt failu

@@ -0,0 +1,6 @@
import frappe

def execute():
frappe.db.sql("""delete from `tabCustom DocPerm`
where parent not in ( select name from `tabDocType` )
""")

+ 14
- 0
frappe/patches/v8_1/update_format_options_in_auto_email_report.py Parādīt failu

@@ -0,0 +1,14 @@
# Copyright (c) 2017, Frappe and Contributors
# License: GNU General Public License v3. See license.txt

from __future__ import unicode_literals
import frappe

def execute():
""" change the XLS option as XLSX in the auto email report """

frappe.reload_doc("email", "doctype", "auto_email_report")

auto_email_list = frappe.get_all("Auto Email Report", filters={"format": "XLS"})
for auto_email in auto_email_list:
frappe.db.set_value("Auto Email Report", auto_email.name, "format", "XLSX")

+ 1
- 1
frappe/printing/doctype/print_format/test_print_format.py Parādīt failu

@@ -1,6 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
from __future__ import unicode_literals, print_function

import frappe
import unittest


+ 3
- 0
frappe/public/build.json Parādīt failu

@@ -104,6 +104,7 @@

"public/js/frappe/ui/page.html",
"public/js/frappe/ui/page.js",
"public/js/frappe/ui/find.js",
"public/js/frappe/ui/iconbar.js",
"public/js/frappe/form/layout.js",
"public/js/frappe/ui/field_group.js",
@@ -197,6 +198,8 @@
"public/js/frappe/form/save.js",
"public/js/frappe/form/script_manager.js",
"public/js/frappe/form/grid.js",
"public/js/frappe/form/grid_row.js",
"public/js/frappe/form/grid_row_form.js",
"public/js/frappe/form/linked_with.js",
"public/js/frappe/form/workflow.js",
"public/js/frappe/form/print.js",


+ 48
- 0
frappe/public/css/desk.css Parādīt failu

@@ -995,3 +995,51 @@ input[type="checkbox"]:checked:before {
visibility: visible;
}
}
.color-picker {
position: relative;
z-index: 999;
}
.color-picker .color-picker-pallete {
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background: #fff;
border: 1px solid #d1d8dd;
width: 290px;
height: 106px;
padding-top: 10px;
padding-left: 5px;
position: absolute;
top: 0;
left: 0;
}
.color-picker .color-picker-pallete:after,
.color-picker .color-picker-pallete:before {
border: solid transparent;
content: " ";
height: 0;
width: 0;
pointer-events: none;
position: absolute;
bottom: 100%;
left: 30px;
}
.color-picker .color-picker-pallete:after {
border-color: rgba(255, 255, 255, 0);
border-bottom-color: #fff;
border-width: 8px;
margin-left: -8px;
}
.color-picker .color-picker-pallete:before {
border-color: rgba(221, 221, 221, 0);
border-bottom-color: #d1d8dd;
border-width: 9px;
margin-left: -9px;
}
.color-picker .color-box {
cursor: pointer;
display: inline-block;
width: 20px;
height: 20px;
margin: -2px 0 0 3px;
border: 1px solid rgba(0, 0, 0, 0.25);
}

+ 20
- 17
frappe/public/css/form.css Parādīt failu

@@ -28,7 +28,8 @@
border-top: 1px solid #d1d8dd;
}
.form-message {
padding: 15px;
padding: 15px 30px;
border-bottom: 1px solid #d1d8dd;
}
.document-flow-wrapper {
padding: 40px 15px 30px;
@@ -73,21 +74,24 @@
}
.form-dashboard {
background-color: #fafbfc;
border-bottom: 1px solid #d1d8dd;
}
.form-dashboard-wrapper {
margin: -15px 0px;
}
.form-documents h6 {
margin-top: 15px;
}
.form-dashboard-section {
margin: 0px -15px;
padding: 15px 30px;
border-bottom: 1px solid #EBEFF2;
}
.form-dashboard-section:first-child {
padding-top: 0px;
}
.form-dashboard-section:last-child {
border-bottom: none;
}
.form-heatmap {
padding-top: 30px;
}
.form-heatmap .heatmap-message {
margin-top: 10px;
}
@@ -496,6 +500,7 @@ h6.uppercase,
}
.like-disabled-input.for-description {
font-weight: normal;
font-size: 12px;
}
.frappe-control {
margin-bottom: 10px;
@@ -530,19 +535,17 @@ select.form-control {
font-weight: bold;
background-color: #fffdf4;
}
.form-headline {
padding: 0px 15px;
margin: 0px;
.form-control[data-fieldtype="Password"] {
position: inherit;
}
.form-headline .alert {
font-size: 12px;
background-color: #fffce7;
font-weight: normal !important;
border: 0px;
border-radius: 0px;
margin-bottom: 0px;
margin: 0px -15px;
padding: 10px 30px;
.password-strength-indicator {
float: right;
padding: 15px;
margin-top: -41px;
margin-right: -7px;
}
.password-strength-message {
margin-top: -10px;
}
.delivery-status-indicator {
display: inline-block;


+ 20
- 0
frappe/public/js/frappe/dom.js Parādīt failu

@@ -195,6 +195,26 @@ frappe.ellipsis = function(text, max) {
return text;
};

frappe.run_serially = function(tasks) {
var result = Promise.resolve();
tasks.forEach(task => {
if(task) {
result = result.then ? result.then(task) : Promise.resolve();
}
});
return result;
};

frappe.timeout = seconds => {
return new Promise((resolve) => {
setTimeout(() => resolve(), seconds * 1000);
});
};

frappe.scrub = function(text) {
return text.replace(/ /g, "_").toLowerCase();
};

frappe.get_modal = function(title, content) {
return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body);
};


+ 428
- 266
frappe/public/js/frappe/form/control.js
Failā izmaiņas netiks attēlotas, jo tās ir par lielu
Parādīt failu


+ 12
- 15
frappe/public/js/frappe/form/dashboard.js Parādīt failu

@@ -4,10 +4,11 @@
frappe.ui.form.Dashboard = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.section = this.frm.fields_dict._form_dashboard.wrapper;
this.parent = this.section.find('.section-body');
this.wrapper = $(frappe.render_template('form_dashboard',
{frm: this.frm})).prependTo(this.frm.layout.wrapper);
{frm: this.frm})).appendTo(this.parent);

this.headline = this.wrapper.find('.form-headline');
this.progress_area = this.wrapper.find(".progress-area");
this.heatmap_area = this.wrapper.find('.form-heatmap');
this.chart_area = this.wrapper.find('.form-chart');
@@ -18,7 +19,7 @@ frappe.ui.form.Dashboard = Class.extend({

},
reset: function() {
this.wrapper.addClass('hidden');
this.section.addClass('hidden');
this.clear_headline();

// clear progress
@@ -36,13 +37,10 @@ frappe.ui.form.Dashboard = Class.extend({
this.wrapper.find('.custom').remove();
},
set_headline: function(html) {
this.headline.html(html).removeClass('hidden');
this.show();
this.frm.layout.show_message(html);
},
clear_headline: function() {
if(this.headline) {
this.headline.empty().addClass('hidden');
}
this.frm.layout.show_message();
},

add_comment: function(text, permanent) {
@@ -59,13 +57,12 @@ frappe.ui.form.Dashboard = Class.extend({
this.clear_headline();
},

set_headline_alert: function(text, alert_class) {
set_headline_alert: function(text, indicator_color) {
if (!indicator_color) {
indicator_color = 'orange';
}
if(text) {
if(!alert_class) alert_class = "alert-warning";
this.set_headline(repl('<div class="alert %(alert_class)s">%(text)s</div>', {
"alert_class": alert_class || "",
"text": text
}));
this.set_headline(`<div><span class="indicator ${indicator_color}">${text}</span></div>`);
} else {
this.clear_headline();
}
@@ -406,6 +403,6 @@ frappe.ui.form.Dashboard = Class.extend({
}
},
show: function() {
this.wrapper.removeClass('hidden');
this.section.removeClass('hidden');
}
});

+ 3
- 3
frappe/public/js/frappe/form/footer/timeline.js Parādīt failu

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


+ 3
- 2
frappe/public/js/frappe/form/footer/timeline_item.html Parādīt failu

@@ -73,7 +73,8 @@
</a>
{% } %}

{% if (data.communication_medium === "Email" && data.sender !== user_email) { %}
{% if (data.communication_medium === "Email"
&& data.sender !== frappe.session.user_email) { %}
<a class="text-muted reply-link pull-right timeline-content-show"
data-name="{%= data.name %}">{%= __("Reply") %}</a>
{% } %}
@@ -110,7 +111,7 @@
{% $.each(data.attachments, function(i, a) { %}
<div class="ellipsis">
<a href="{%= encodeURI(a.file_url).replace(/#/g, \'%23\') %}"
class="text-muted small" target="_blank">
class="text-muted small" target="_blank" rel="noopener noreferrer">
<i class="fa fa-paperclip"></i>
{%= a.file_url.split("/").slice(-1)[0] %}
{% if (a.is_private) { %}


+ 21
- 0
frappe/public/js/frappe/form/formatters.js Parādīt failu

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


+ 34
- 687
frappe/public/js/frappe/form/grid.js Parādīt failu

@@ -108,6 +108,11 @@ frappe.ui.form.Grid = Class.extend({
select_row: function(name) {
this.grid_rows_by_docname[name].select();
},
remove_all: function() {
this.grid_rows.forEach(row => {
row.remove();
});
},
refresh_remove_rows_button: function() {
this.remove_rows_button.toggleClass('hide',
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
@@ -257,7 +262,7 @@ frappe.ui.form.Grid = Class.extend({
if (this.frm && this.frm.docname) {
// use doc specific docfield object
this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname,
this.frm.docname);
this.frm.docname);
} else {
// use non-doc specific docfield
if(this.df.options) {
@@ -360,8 +365,19 @@ frappe.ui.form.Grid = Class.extend({
get_docfield: function(fieldname) {
return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null);
},
get_grid_row: function(docname) {
return this.grid_rows_by_docname[docname];
get_row: function(key) {
if(typeof key == 'number') {
if(key < 0) {
return this.grid_rows[this.grid_rows.length + key];
} else {
return this.grid_rows[key];
}
} else {
return this.grid_rows_by_docname[key];
}
},
get_grid_row: function(key) {
return this.get_row(key);
},
get_field: function(fieldname) {
// Note: workaround for get_query
@@ -435,21 +451,21 @@ frappe.ui.form.Grid = Class.extend({
&& (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm)
&& !in_list(frappe.model.layout_fields, df.fieldtype)) {

if(df.columns) {
df.colsize=df.columns;
}
else {
var colsize=2;
switch(df.fieldtype){
case"Text":
case"Small Text":
colsize=3;
break;
case"Check":
colsize=1
}
df.colsize=colsize
if(df.columns) {
df.colsize=df.columns;
}
else {
var colsize=2;
switch(df.fieldtype) {
case"Text":
case"Small Text":
colsize=3;
break;
case"Check":
colsize=1
}
df.colsize=colsize;
}

if(df.columns) {
df.colsize=df.columns;
@@ -641,673 +657,4 @@ frappe.ui.form.Grid = Class.extend({
// hide all custom buttons
this.grid_buttons.find('.btn-custom').addClass('hidden');
}
});

frappe.ui.form.GridRow = Class.extend({
init: function(opts) {
this.on_grid_fields_dict = {};
this.on_grid_fields = [];
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
this.columns = {};
this.columns_list = [];
$.extend(this, opts);
this.make();
},
make: function() {
var me = this;

this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this);
this.row = $('<div class="data-row row"></div>').appendTo(this.wrapper)
.on("click", function(e) {
if($(e.target).hasClass('grid-row-check') || $(e.target).hasClass('row-index') || $(e.target).parent().hasClass('row-index')) {
return;
}
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) {
// pass
} else {
me.toggle_view();
return false;
}
});

// no checkboxes if too small
// if(this.is_too_small()) {
// this.row_check_html = '';
// }

if(this.grid.template && !this.grid.meta.editable_grid) {
this.render_template();
} else {
this.render_row();
}
if(this.doc) {
this.set_data();
}
},
set_data: function() {
this.wrapper.data({
"doc": this.doc
})
},
set_row_index: function() {
if(this.doc) {
this.wrapper
.attr('data-name', this.doc.name)
.attr("data-idx", this.doc.idx)
.find(".row-index span, .grid-form-row-index").html(this.doc.idx)

}
},
select: function(checked) {
this.doc.__checked = checked ? 1 : 0;
},
refresh_check: function() {
this.wrapper.find('.grid-row-check').prop('checked', this.doc ? !!this.doc.__checked : false);
this.grid.refresh_remove_rows_button();
},
remove: function() {
var me = this;
if(this.grid.is_editable()) {
if(this.frm) {
if(this.get_open_form()) {
this.hide_form();
}

this.frm.script_manager.trigger("before_" + this.grid.df.fieldname + "_remove",
this.doc.doctype, this.doc.name);

//this.wrapper.toggle(false);
frappe.model.clear_doc(this.doc.doctype, this.doc.name);

this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove",
this.doc.doctype, this.doc.name);
this.frm.dirty();
} else {
this.grid.df.data = this.grid.df.data.filter(function(d) {
return d.name !== me.doc.name;
})
// remap idxs
this.grid.df.data.forEach(function(d, i) {
d.idx = i+1;
});
}
this.grid.refresh();
}
},
insert: function(show, below) {
var idx = this.doc.idx;
if(below) idx ++;
this.toggle_view(false);
this.grid.add_new_row(idx, null, show);
},
refresh: function() {
if(this.frm && this.doc) {
this.doc = locals[this.doc.doctype][this.doc.name];
}
// re write columns
this.visible_columns = null;

if(this.grid.template && !this.grid.meta.editable_grid) {
this.render_template();
} else {
this.render_row(true);
}

// refersh form fields
if(this.grid_form) {
this.grid_form.layout && this.grid_form.layout.refresh(this.doc);
}
},
render_template: function() {
this.set_row_index();

if(this.row_display) {
this.row_display.remove();
}
var index_html = '';

// row index
if(this.doc) {
if(!this.row_index) {
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row);
}
this.row_index.find('span').html(this.doc.idx);
}

this.row_display = $('<div class="row-data sortable-handle template-row">'+
+'</div>').appendTo(this.row)
.html(frappe.render(this.grid.template, {
doc: this.doc ? frappe.get_format_helper(this.doc) : null,
frm: this.frm,
row: this
}));
},
render_row: function(refresh) {
var me = this;
this.set_row_index();

// index (1, 2, 3 etc)
if(!this.row_index) {
var txt = (this.doc ? this.doc.idx : "&nbsp;");
this.row_index = $(
`<div class="row-index sortable-handle col col-xs-1">
${this.row_check_html}
<span>${txt}</span></div>`)
.appendTo(this.row)
.on('click', function(e) {
if(!$(e.target).hasClass('grid-row-check')) {
me.toggle_view();
}
});
} else {
this.row_index.find('span').html(txt);
}

this.setup_columns();
this.add_open_form_button();
this.refresh_check();

if(this.frm && this.doc) {
$(this.frm.wrapper).trigger("grid-row-render", [this]);
}
},

make_editable: function() {
this.row.toggleClass('editable-row', this.grid.is_editable());
},

is_too_small: function() {
return this.row.width() ? this.row.width() < 300 : false;
},

add_open_form_button: function() {
var me = this;
if(this.doc && !this.grid.df.in_place_edit) {
// remove row
if(!this.open_form_button) {
this.open_form_button = $('<a class="close btn-open-row">\
<span class="octicon octicon-triangle-down"></span></a>')
.appendTo($('<div class="col col-xs-1 sortable-handle"></div>').appendTo(this.row))
.on('click', function() { me.toggle_view(); return false; });

if(this.is_too_small()) {
// narrow
this.open_form_button.css({'margin-right': '-2px'});
}
}
}
},

setup_columns: function() {
var me = this;
this.focus_set = false;
this.grid.setup_visible_columns();

for(var ci in this.grid.visible_columns) {
var df = this.grid.visible_columns[ci][0],
colsize = this.grid.visible_columns[ci][1],
txt = this.doc ?
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
__(df.label);

if(this.doc && df.fieldtype === "Select") {
txt = __(txt);
}

if(!this.columns[df.fieldname]) {
var column = this.make_column(df, colsize, txt, ci);
} else {
var column = this.columns[df.fieldname];
this.refresh_field(df.fieldname, txt);
}

// background color for cellz
if(this.doc) {
if(df.reqd && !txt) {
column.addClass('error');
}
if (df.reqd || df.bold) {
column.addClass('bold');
}
}
}
},

make_column: function(df, colsize, txt, ci) {
var me = this;
var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ?
" grid-overflow-no-ellipsis" : "");
add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ?
" text-right": "";
add_class += (["Check"].indexOf(df.fieldtype)!==-1) ?
" text-center": "";

var $col = $('<div class="col grid-static-col col-xs-'+colsize+' '+add_class+'"></div>')
.attr("data-fieldname", df.fieldname)
.attr("data-fieldtype", df.fieldtype)
.data("df", df)
.appendTo(this.row)
.on('click', function() {
if(frappe.ui.form.editable_row===me) {
return;
}
var out = me.toggle_editable_row();
var col = this;
setTimeout(function() {
$(col).find('input[type="Text"]:first').focus();
}, 500);
return out;
});

$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false);
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt);
$col.df = df;
$col.column_index = ci;

this.columns[df.fieldname] = $col;
this.columns_list.push($col);

return $col;
},

toggle_editable_row: function(show) {
var me = this;
// show static for field based on
// whether grid is editable
if(this.grid.allow_on_grid_editing() && this.grid.is_editable() && this.doc && show !== false) {

// disable other editale row
if(frappe.ui.form.editable_row
&& frappe.ui.form.editable_row !== this) {
frappe.ui.form.editable_row.toggle_editable_row(false);
}

this.row.toggleClass('editable-row', true);

// setup controls
this.columns_list.forEach(function(column) {
me.make_control(column);
column.static_area.toggle(false);
column.field_area.toggle(true);
});

frappe.ui.form.editable_row = this;
return false;
} else {
this.row.toggleClass('editable-row', false);
this.columns_list.forEach(function(column) {
column.static_area.toggle(true);
column.field_area && column.field_area.toggle(false);
});
frappe.ui.form.editable_row = null;
}
},

make_control: function(column) {
if(column.field) return;

var me = this,
parent = column.field_area,
df = column.df;

// no text editor in grid
if (df.fieldtype=='Text Editor') {
df.fieldtype = 'Text';
}

var field = frappe.ui.form.make_control({
df: df,
parent: parent,
only_input: true,
with_link_btn: true,
doc: this.doc,
doctype: this.doc.doctype,
docname: this.doc.name,
frm: this.grid.frm,
grid: this.grid,
grid_row: this,
value: this.doc[df.fieldname]
});

// sync get_query
field.get_query = this.grid.get_field(df.fieldname).get_query;
field.refresh();
if(field.$input) {
field.$input
.addClass('input-sm')
.attr('data-col-idx', column.column_index)
.attr('placeholder', __(df.label));
// flag list input
if (this.columns_list && this.columns_list.slice(-1)[0]===column) {
field.$input.attr('data-last-input', 1);
}
}

this.set_arrow_keys(field);
column.field = field;
this.on_grid_fields_dict[df.fieldname] = field;
this.on_grid_fields.push(field);

},

set_arrow_keys: function(field) {
var me = this;
if(field.$input) {
field.$input.on('keydown', function(e) {
var { TAB, UP_ARROW, DOWN_ARROW } = frappe.ui.keyCode;
if(!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) {
return;
}

var values = me.grid.get_data();
var fieldname = $(this).attr('data-fieldname');
var fieldtype = $(this).attr('data-fieldtype');

var move_up_down = function(base) {
if(in_list(['Text', 'Small Text'], fieldtype)) {
return;
}

base.toggle_editable_row();
setTimeout(function() {
var input = base.columns[fieldname].field.$input;
if(input) {
input.focus();
}
}, 400)

}

// TAB
if(e.which==TAB && !e.shiftKey) {
// last column
if($(this).attr('data-last-input') ||
me.grid.wrapper.find('.grid-row :input:enabled:last').get(0)===this) {
setTimeout(function() {
if(me.doc.idx === values.length) {
// last row
me.grid.add_new_row(null, null, true);
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row();
me.grid.set_focus_on_row();
} else {
me.grid.grid_rows[me.doc.idx].toggle_editable_row();
me.grid.set_focus_on_row(me.doc.idx+1);
}
}, 500);
}
} else if(e.which==UP_ARROW) {
if(me.doc.idx > 1) {
var prev = me.grid.grid_rows[me.doc.idx-2];
move_up_down(prev);
}
} else if(e.which==DOWN_ARROW) {
if(me.doc.idx < values.length) {
var next = me.grid.grid_rows[me.doc.idx];
move_up_down(next);
}
}

});
}
},

get_open_form: function() {
return frappe.ui.form.get_open_grid_form();
},

toggle_view: function(show, callback) {
if(!this.doc) {
return this;
}

if(this.frm) {
// reload doc
this.doc = locals[this.doc.doctype][this.doc.name];
}

// hide other
var open_row = this.get_open_form();

if (show===undefined) show = !!!open_row;

// call blur
document.activeElement && document.activeElement.blur();

if(show && open_row) {
if(open_row==this) {
// already open, do nothing
callback && callback();
return;
} else {
// close other views
open_row.toggle_view(false);
}
}

if(show) {
this.show_form();
} else {
this.hide_form();
}
callback && callback();

return this;
},
show_form: function() {
if(!this.grid_form) {
this.grid_form = new frappe.ui.form.GridRowForm({
row: this
});
}
this.grid_form.render();
this.row.toggle(false);
// this.form_panel.toggle(true);
frappe.dom.freeze("", "dark");
cur_frm.cur_grid = this;
this.wrapper.addClass("grid-row-open");
if(!frappe.dom.is_element_in_viewport(this.wrapper)) {
frappe.utils.scroll_to(this.wrapper, true, 15);
}

if(this.frm) {
this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered");
this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name);
}
},
hide_form: function() {
frappe.dom.unfreeze();
this.row.toggle(true);
this.refresh();
cur_frm.cur_grid = null;
this.wrapper.removeClass("grid-row-open");
},
open_prev: function() {
if(this.grid.grid_rows[this.doc.idx-2]) {
this.grid.grid_rows[this.doc.idx-2].toggle_view(true);
}
},
open_next: function() {
if(this.grid.grid_rows[this.doc.idx]) {
this.grid.grid_rows[this.doc.idx].toggle_view(true);
} else {
this.grid.add_new_row(null, null, true);
}
},
refresh_field: function(fieldname, txt) {
var df = this.grid.get_docfield(fieldname) || undefined;

// format values if no frm
if(!df) {
df = this.grid.visible_columns.find((col) => {
return col[0].fieldname === fieldname;
});
if(df && this.doc) {
var txt = frappe.format(this.doc[fieldname], df[0],
null, this.doc);
}
}

if(txt===undefined && this.frm) {
var txt = frappe.format(this.doc[fieldname], df,
null, this.frm.doc);
}

// reset static value
var column = this.columns[fieldname];
if(column) {
column.static_area.html(txt || "");
if(df && df.reqd) {
column.toggleClass('error', !!(txt===null || txt===''));
}
}

// reset field value
var field = this.on_grid_fields_dict[fieldname];
if(field) {
field.docname = this.doc.name;
field.refresh();
}

// in form
if(this.grid_form) {
this.grid_form.refresh_field(fieldname);
}
},
get_visible_columns: function(blacklist) {
var me = this;
var visible_columns = $.map(this.docfields, function(df) {
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read")
&& !in_list(frappe.model.layout_fields, df.fieldtype) && !in_list(blacklist, df.fieldname);

return visible ? df : null;
});
return visible_columns;
},
set_field_property: function(fieldname, property, value) {
// set a field property for open form / grid form
var me = this;

var set_property = function(field) {
if(!field) return;
field.df[property] = value;
field.refresh();
}

// set property in grid form
if(this.grid_form) {
set_property(this.grid_form.fields_dict[fieldname]);
this.grid_form.layout && this.grid_form.layout.refresh_sections();
}

// set property in on grid fields
set_property(this.on_grid_fields_dict[fieldname]);
},
toggle_reqd: function(fieldname, reqd) {
this.set_field_property(fieldname, 'reqd', reqd ? 1 : 0);
},
toggle_display: function(fieldname, show) {
this.set_field_property(fieldname, 'hidden', show ? 0 : 1);
},
toggle_editable: function(fieldname, editable) {
this.set_field_property(fieldname, 'read_only', editable ? 0 : 1);
},
});

frappe.ui.form.GridRowForm = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.wrapper = $('<div class="form-in-grid"></div>')
.appendTo(this.row.wrapper);

},
render: function() {
var me = this;
this.make_form();
this.form_area.empty();

this.layout = new frappe.ui.form.Layout({
fields: this.row.docfields,
body: this.form_area,
no_submit_on_enter: true,
frm: this.row.frm,
});
this.layout.make();

this.fields = this.layout.fields;
this.fields_dict = this.layout.fields_dict;

this.layout.refresh(this.row.doc);

// copy get_query to fields
for(var fieldname in (this.row.grid.fieldinfo || {})) {
var fi = this.row.grid.fieldinfo[fieldname];
$.extend(me.fields_dict[fieldname], fi);
}

this.toggle_add_delete_button_display(this.wrapper);

this.row.grid.open_grid_row = this;

this.set_focus();
},
make_form: function() {
if(!this.form_area) {
$(frappe.render_template("grid_form", {grid:this})).appendTo(this.wrapper);
this.form_area = this.wrapper.find(".form-area");
this.row.set_row_index();
this.set_form_events();
}
},
set_form_events: function() {
var me = this;
this.wrapper.find(".grid-delete-row")
.on('click', function() {
me.row.remove(); return false;
});
this.wrapper.find(".grid-insert-row")
.on('click', function() {
me.row.insert(true); return false;
});
this.wrapper.find(".grid-insert-row-below")
.on('click', function() {
me.row.insert(true, true); return false;
});
this.wrapper.find(".grid-append-row")
.on('click', function() {
me.row.toggle_view(false);
me.row.grid.add_new_row(me.row.doc.idx+1, null, true);
return false;
});
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function() {
me.row.toggle_view();
return false;
});
},
toggle_add_delete_button_display: function($parent) {
$parent.find(".grid-header-toolbar .btn, .grid-footer-toolbar .btn")
.toggle(this.row.grid.is_editable());
},
refresh_field: function(fieldname) {
if(this.fields_dict[fieldname]) {
this.fields_dict[fieldname].refresh();
this.layout && this.layout.refresh_dependency();
}
},
set_focus: function() {
// wait for animation and then focus on the first row
var me = this;
setTimeout(function() {
if(me.row.frm && me.row.frm.doc.docstatus===0 || !me.row.frm) {
var first = me.form_area.find("input:first");
if(first.length && !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))) {
try {
first.get(0).focus();
} catch(e) {
//
}
}
}
}, 500);
},
});
});

+ 586
- 0
frappe/public/js/frappe/form/grid_row.js Parādīt failu

@@ -0,0 +1,586 @@
frappe.ui.form.GridRow = Class.extend({
init: function(opts) {
this.on_grid_fields_dict = {};
this.on_grid_fields = [];
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
this.columns = {};
this.columns_list = [];
$.extend(this, opts);
this.make();
},
make: function() {
var me = this;

this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this);
this.row = $('<div class="data-row row"></div>').appendTo(this.wrapper)
.on("click", function(e) {
if($(e.target).hasClass('grid-row-check') || $(e.target).hasClass('row-index') || $(e.target).parent().hasClass('row-index')) {
return;
}
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) {
// pass
} else {
me.toggle_view();
return false;
}
});

// no checkboxes if too small
// if(this.is_too_small()) {
// this.row_check_html = '';
// }

if(this.grid.template && !this.grid.meta.editable_grid) {
this.render_template();
} else {
this.render_row();
}
if(this.doc) {
this.set_data();
}
},
set_data: function() {
this.wrapper.data({
"doc": this.doc
})
},
set_row_index: function() {
if(this.doc) {
this.wrapper
.attr('data-name', this.doc.name)
.attr("data-idx", this.doc.idx)
.find(".row-index span, .grid-form-row-index").html(this.doc.idx)

}
},
select: function(checked) {
this.doc.__checked = checked ? 1 : 0;
},
refresh_check: function() {
this.wrapper.find('.grid-row-check').prop('checked', this.doc ? !!this.doc.__checked : false);
this.grid.refresh_remove_rows_button();
},
remove: function() {
var me = this;
if(this.grid.is_editable()) {
if(this.frm) {
if(this.get_open_form()) {
this.hide_form();
}

this.frm.script_manager.trigger("before_" + this.grid.df.fieldname + "_remove",
this.doc.doctype, this.doc.name);

//this.wrapper.toggle(false);
frappe.model.clear_doc(this.doc.doctype, this.doc.name);

this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove",
this.doc.doctype, this.doc.name);
this.frm.dirty();
} else {
this.grid.df.data = this.grid.df.data.filter(function(d) {
return d.name !== me.doc.name;
})
// remap idxs
this.grid.df.data.forEach(function(d, i) {
d.idx = i+1;
});
}
this.grid.refresh();
}
},
insert: function(show, below) {
var idx = this.doc.idx;
if(below) idx ++;
this.toggle_view(false);
this.grid.add_new_row(idx, null, show);
},
refresh: function() {
if(this.frm && this.doc) {
this.doc = locals[this.doc.doctype][this.doc.name];
}
// re write columns
this.visible_columns = null;

if(this.grid.template && !this.grid.meta.editable_grid) {
this.render_template();
} else {
this.render_row(true);
}

// refersh form fields
if(this.grid_form) {
this.grid_form.layout && this.grid_form.layout.refresh(this.doc);
}
},
render_template: function() {
this.set_row_index();

if(this.row_display) {
this.row_display.remove();
}
var index_html = '';

// row index
if(this.doc) {
if(!this.row_index) {
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row);
}
this.row_index.find('span').html(this.doc.idx);
}

this.row_display = $('<div class="row-data sortable-handle template-row">'+
+'</div>').appendTo(this.row)
.html(frappe.render(this.grid.template, {
doc: this.doc ? frappe.get_format_helper(this.doc) : null,
frm: this.frm,
row: this
}));
},
render_row: function(refresh) {
var me = this;
this.set_row_index();

// index (1, 2, 3 etc)
if(!this.row_index) {
var txt = (this.doc ? this.doc.idx : "&nbsp;");
this.row_index = $(
`<div class="row-index sortable-handle col col-xs-1">
${this.row_check_html}
<span>${txt}</span></div>`)
.appendTo(this.row)
.on('click', function(e) {
if(!$(e.target).hasClass('grid-row-check')) {
me.toggle_view();
}
});
} else {
this.row_index.find('span').html(txt);
}

this.setup_columns();
this.add_open_form_button();
this.refresh_check();

if(this.frm && this.doc) {
$(this.frm.wrapper).trigger("grid-row-render", [this]);
}
},

make_editable: function() {
this.row.toggleClass('editable-row', this.grid.is_editable());
},

is_too_small: function() {
return this.row.width() ? this.row.width() < 300 : false;
},

add_open_form_button: function() {
var me = this;
if(this.doc && !this.grid.df.in_place_edit) {
// remove row
if(!this.open_form_button) {
this.open_form_button = $('<a class="close btn-open-row">\
<span class="octicon octicon-triangle-down"></span></a>')
.appendTo($('<div class="col col-xs-1 sortable-handle"></div>').appendTo(this.row))
.on('click', function() { me.toggle_view(); return false; });

if(this.is_too_small()) {
// narrow
this.open_form_button.css({'margin-right': '-2px'});
}
}
}
},

setup_columns: function() {
var me = this;
this.focus_set = false;
this.grid.setup_visible_columns();

for(var ci in this.grid.visible_columns) {
var df = this.grid.visible_columns[ci][0],
colsize = this.grid.visible_columns[ci][1],
txt = this.doc ?
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
__(df.label);

if(this.doc && df.fieldtype === "Select") {
txt = __(txt);
}

if(!this.columns[df.fieldname]) {
var column = this.make_column(df, colsize, txt, ci);
} else {
var column = this.columns[df.fieldname];
this.refresh_field(df.fieldname, txt);
}

// background color for cellz
if(this.doc) {
if(df.reqd && !txt) {
column.addClass('error');
}
if (df.reqd || df.bold) {
column.addClass('bold');
}
}
}
},

make_column: function(df, colsize, txt, ci) {
var me = this;
var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ?
" grid-overflow-no-ellipsis" : "");
add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ?
" text-right": "";
add_class += (["Check"].indexOf(df.fieldtype)!==-1) ?
" text-center": "";

var $col = $('<div class="col grid-static-col col-xs-'+colsize+' '+add_class+'"></div>')
.attr("data-fieldname", df.fieldname)
.attr("data-fieldtype", df.fieldtype)
.data("df", df)
.appendTo(this.row)
.on('click', function() {
if(frappe.ui.form.editable_row===me) {
return;
}
var out = me.toggle_editable_row();
var col = this;
setTimeout(function() {
$(col).find('input[type="Text"]:first').focus();
}, 500);
return out;
});

$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false);
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt);
$col.df = df;
$col.column_index = ci;

this.columns[df.fieldname] = $col;
this.columns_list.push($col);

return $col;
},

activate: function() {
this.toggle_editable_row(true);
return this;
},

toggle_editable_row: function(show) {
var me = this;
// show static for field based on
// whether grid is editable
if(this.grid.allow_on_grid_editing() && this.grid.is_editable() && this.doc && show !== false) {

// disable other editale row
if(frappe.ui.form.editable_row
&& frappe.ui.form.editable_row !== this) {
frappe.ui.form.editable_row.toggle_editable_row(false);
}

this.row.toggleClass('editable-row', true);

// setup controls
this.columns_list.forEach(function(column) {
me.make_control(column);
column.static_area.toggle(false);
column.field_area.toggle(true);
});

frappe.ui.form.editable_row = this;
return false;
} else {
this.row.toggleClass('editable-row', false);
this.columns_list.forEach(function(column) {
column.static_area.toggle(true);
column.field_area && column.field_area.toggle(false);
});
frappe.ui.form.editable_row = null;
}
},

make_control: function(column) {
if(column.field) return;

var me = this,
parent = column.field_area,
df = column.df;

// no text editor in grid
if (df.fieldtype=='Text Editor') {
df.fieldtype = 'Text';
}

var field = frappe.ui.form.make_control({
df: df,
parent: parent,
only_input: true,
with_link_btn: true,
doc: this.doc,
doctype: this.doc.doctype,
docname: this.doc.name,
frm: this.grid.frm,
grid: this.grid,
grid_row: this,
value: this.doc[df.fieldname]
});

// sync get_query
field.get_query = this.grid.get_field(df.fieldname).get_query;
field.refresh();
if(field.$input) {
field.$input
.addClass('input-sm')
.attr('data-col-idx', column.column_index)
.attr('placeholder', __(df.label));
// flag list input
if (this.columns_list && this.columns_list.slice(-1)[0]===column) {
field.$input.attr('data-last-input', 1);
}
}

this.set_arrow_keys(field);
column.field = field;
this.on_grid_fields_dict[df.fieldname] = field;
this.on_grid_fields.push(field);

},

set_arrow_keys: function(field) {
var me = this;
if(field.$input) {
field.$input.on('keydown', function(e) {
var { TAB, UP_ARROW, DOWN_ARROW } = frappe.ui.keyCode;
if(!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) {
return;
}

var values = me.grid.get_data();
var fieldname = $(this).attr('data-fieldname');
var fieldtype = $(this).attr('data-fieldtype');

var move_up_down = function(base) {
if(in_list(['Text', 'Small Text'], fieldtype)) {
return;
}

base.toggle_editable_row();
setTimeout(function() {
var input = base.columns[fieldname].field.$input;
if(input) {
input.focus();
}
}, 400)

}

// TAB
if(e.which==TAB && !e.shiftKey) {
// last column
if($(this).attr('data-last-input') ||
me.grid.wrapper.find('.grid-row :input:enabled:last').get(0)===this) {
setTimeout(function() {
if(me.doc.idx === values.length) {
// last row
me.grid.add_new_row(null, null, true);
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row();
me.grid.set_focus_on_row();
} else {
me.grid.grid_rows[me.doc.idx].toggle_editable_row();
me.grid.set_focus_on_row(me.doc.idx+1);
}
}, 500);
}
} else if(e.which==UP_ARROW) {
if(me.doc.idx > 1) {
var prev = me.grid.grid_rows[me.doc.idx-2];
move_up_down(prev);
}
} else if(e.which==DOWN_ARROW) {
if(me.doc.idx < values.length) {
var next = me.grid.grid_rows[me.doc.idx];
move_up_down(next);
}
}

});
}
},

get_open_form: function() {
return frappe.ui.form.get_open_grid_form();
},

toggle_view: function(show, callback) {
if(!this.doc) {
return this;
}

if(this.frm) {
// reload doc
this.doc = locals[this.doc.doctype][this.doc.name];
}

// hide other
var open_row = this.get_open_form();

if (show===undefined) show = !!!open_row;

// call blur
document.activeElement && document.activeElement.blur();

if(show && open_row) {
if(open_row==this) {
// already open, do nothing
callback && callback();
return;
} else {
// close other views
open_row.toggle_view(false);
}
}

if(show) {
this.show_form();
} else {
this.hide_form();
}
callback && callback();

return this;
},
show_form: function() {
if(!this.grid_form) {
this.grid_form = new frappe.ui.form.GridRowForm({
row: this
});
}
this.grid_form.render();
this.row.toggle(false);
// this.form_panel.toggle(true);
frappe.dom.freeze("", "dark");
cur_frm.cur_grid = this;
this.wrapper.addClass("grid-row-open");
if(!frappe.dom.is_element_in_viewport(this.wrapper)) {
frappe.utils.scroll_to(this.wrapper, true, 15);
}

if(this.frm) {
this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered");
this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name);
}
},
hide_form: function() {
frappe.dom.unfreeze();
this.row.toggle(true);
this.refresh();
cur_frm.cur_grid = null;
this.wrapper.removeClass("grid-row-open");
},
open_prev: function() {
if(this.grid.grid_rows[this.doc.idx-2]) {
this.grid.grid_rows[this.doc.idx-2].toggle_view(true);
}
},
open_next: function() {
if(this.grid.grid_rows[this.doc.idx]) {
this.grid.grid_rows[this.doc.idx].toggle_view(true);
} else {
this.grid.add_new_row(null, null, true);
}
},
refresh_field: function(fieldname, txt) {
var df = this.grid.get_docfield(fieldname) || undefined;

// format values if no frm
if(!df) {
df = this.grid.visible_columns.find((col) => {
return col[0].fieldname === fieldname;
});
if(df && this.doc) {
var txt = frappe.format(this.doc[fieldname], df[0],
null, this.doc);
}
}

if(txt===undefined && this.frm) {
var txt = frappe.format(this.doc[fieldname], df,
null, this.frm.doc);
}

// reset static value
var column = this.columns[fieldname];
if(column) {
column.static_area.html(txt || "");
if(df && df.reqd) {
column.toggleClass('error', !!(txt===null || txt===''));
}
}

// reset field value
var field = this.on_grid_fields_dict[fieldname];
if(field) {
field.docname = this.doc.name;
field.refresh();
}

// in form
if(this.grid_form) {
this.grid_form.refresh_field(fieldname);
}
},
get_field: function(fieldname) {
let field = this.on_grid_fields_dict[fieldname];
if (field) {
return field;
} else if(this.grid_form) {
return this.grid_form.fields_dict[fieldname];
} else {
throw `fieldname ${fieldname} not found`;
}
},

get_visible_columns: function(blacklist) {
var me = this;
var visible_columns = $.map(this.docfields, function(df) {
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read")
&& !in_list(frappe.model.layout_fields, df.fieldtype) && !in_list(blacklist, df.fieldname);

return visible ? df : null;
});
return visible_columns;
},
set_field_property: function(fieldname, property, value) {
// set a field property for open form / grid form
var me = this;

var set_property = function(field) {
if(!field) return;
field.df[property] = value;
field.refresh();
}

// set property in grid form
if(this.grid_form) {
set_property(this.grid_form.fields_dict[fieldname]);
this.grid_form.layout && this.grid_form.layout.refresh_sections();
}

// set property in on grid fields
set_property(this.on_grid_fields_dict[fieldname]);
},
toggle_reqd: function(fieldname, reqd) {
this.set_field_property(fieldname, 'reqd', reqd ? 1 : 0);
},
toggle_display: function(fieldname, show) {
this.set_field_property(fieldname, 'hidden', show ? 0 : 1);
},
toggle_editable: function(fieldname, editable) {
this.set_field_property(fieldname, 'read_only', editable ? 0 : 1);
},
});

+ 97
- 0
frappe/public/js/frappe/form/grid_row_form.js Parādīt failu

@@ -0,0 +1,97 @@
frappe.ui.form.GridRowForm = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.wrapper = $('<div class="form-in-grid"></div>')
.appendTo(this.row.wrapper);

},
render: function() {
var me = this;
this.make_form();
this.form_area.empty();

this.layout = new frappe.ui.form.Layout({
fields: this.row.docfields,
body: this.form_area,
no_submit_on_enter: true,
frm: this.row.frm,
});
this.layout.make();

this.fields = this.layout.fields;
this.fields_dict = this.layout.fields_dict;

this.layout.refresh(this.row.doc);

// copy get_query to fields
for(var fieldname in (this.row.grid.fieldinfo || {})) {
var fi = this.row.grid.fieldinfo[fieldname];
$.extend(me.fields_dict[fieldname], fi);
}

this.toggle_add_delete_button_display(this.wrapper);

this.row.grid.open_grid_row = this;

this.set_focus();
},
make_form: function() {
if(!this.form_area) {
$(frappe.render_template("grid_form", {grid:this})).appendTo(this.wrapper);
this.form_area = this.wrapper.find(".form-area");
this.row.set_row_index();
this.set_form_events();
}
},
set_form_events: function() {
var me = this;
this.wrapper.find(".grid-delete-row")
.on('click', function() {
me.row.remove(); return false;
});
this.wrapper.find(".grid-insert-row")
.on('click', function() {
me.row.insert(true); return false;
});
this.wrapper.find(".grid-insert-row-below")
.on('click', function() {
me.row.insert(true, true); return false;
});
this.wrapper.find(".grid-append-row")
.on('click', function() {
me.row.toggle_view(false);
me.row.grid.add_new_row(me.row.doc.idx+1, null, true);
return false;
});
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function() {
me.row.toggle_view();
return false;
});
},
toggle_add_delete_button_display: function($parent) {
$parent.find(".grid-header-toolbar .btn, .grid-footer-toolbar .btn")
.toggle(this.row.grid.is_editable());
},
refresh_field: function(fieldname) {
if(this.fields_dict[fieldname]) {
this.fields_dict[fieldname].refresh();
this.layout && this.layout.refresh_dependency();
}
},
set_focus: function() {
// wait for animation and then focus on the first row
var me = this;
setTimeout(function() {
if(me.row.frm && me.row.frm.doc.docstatus===0 || !me.row.frm) {
var first = me.form_area.find("input:first");
if(first.length && !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))) {
try {
first.get(0).focus();
} catch(e) {
//
}
}
}
}, 500);
},
});

+ 83
- 31
frappe/public/js/frappe/form/layout.js Parādīt failu

@@ -19,12 +19,14 @@ frappe.ui.form.Layout = Class.extend({
$.extend(this, opts);
},
make: function() {
if(!this.parent && this.body)
if(!this.parent && this.body) {
this.parent = this.body;
}
this.wrapper = $('<div class="form-layout">').appendTo(this.parent);
this.message = $('<div class="form-message text-muted small hidden"></div>').appendTo(this.wrapper);
if(!this.fields)
if(!this.fields) {
this.fields = frappe.meta.sort_docfields(frappe.meta.docfield_map[this.doctype]);
}
this.setup_tabbing();
this.render();
},
@@ -44,29 +46,58 @@ frappe.ui.form.Layout = Class.extend({
this.message.empty().addClass('hidden');
}
},
render: function() {
render: function(new_fields) {
var me = this;
var fields = new_fields || this.fields;

this.section = null;
this.column = null;
if((this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length) {

if (this.with_dashboard) {
this.setup_dashboard_section();
}

if (this.no_opening_section()) {
this.make_section();
}
$.each(this.fields, function(i, df) {
if(df.fieldtype === "Fold") {
me.make_page(df);
} else if (df.fieldtype === "Section Break") {
me.make_section(df);
} else if (df.fieldtype === "Column Break") {
me.make_column(df);
} else {
me.make_field(df);
$.each(fields, function(i, df) {
switch(df.fieldtype) {
case "Fold":
me.make_page(df);
break;
case "Section Break":
me.make_section(df);
break;
case "Column Break":
me.make_column(df);
break;
default:
me.make_field(df);
}
});

},
make_field: function(df, colspan) {

no_opening_section: function() {
return (this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length;
},

setup_dashboard_section: function() {
if (this.no_opening_section()) {
this.fields.unshift({fieldtype: 'Section Break'});
}

this.fields.unshift({
fieldtype: 'Section Break',
fieldname: '_form_dashboard',
label: __('Dashboard'),
cssClass: 'form-dashboard',
collapsible: 1,
//hidden: 1
});
},

make_field: function(df, colspan, render = false) {
!this.section && this.make_section();
!this.column && this.make_column();

@@ -74,7 +105,8 @@ frappe.ui.form.Layout = Class.extend({
df: df,
doctype: this.doctype,
parent: this.column.wrapper.get(0),
frm: this.frm
frm: this.frm,
render_input: render
});

fieldobj.layout = this;
@@ -171,13 +203,14 @@ frappe.ui.form.Layout = Class.extend({
var $this = $(this).removeClass("empty-section")
.removeClass("visible-section")
.removeClass("shaded-section");
if(!$(this).find(".frappe-control:not(.hide-control)").length) {
if(!$this.find(".frappe-control:not(.hide-control)").length
&& !$this.hasClass('form-dashboard')) {
// nothing visible, hide the section
$(this).addClass("empty-section");
$this.addClass("empty-section");
} else {
$(this).addClass("visible-section");
$this.addClass("visible-section");
if(cnt % 2) {
$(this).addClass("shaded-section");
$this.addClass("shaded-section");
}
cnt ++;
}
@@ -201,6 +234,10 @@ frappe.ui.form.Layout = Class.extend({
collapse = false;
}

if(df.fieldname === '_form_dashboard') {
collapse = false;
}

section.collapse(collapse);
}
}
@@ -226,6 +263,18 @@ frappe.ui.form.Layout = Class.extend({
}
},

refresh_fields: function(fields) {
let fieldnames = fields.map((field) => {
if(field.label) return field.label;
});

this.fields_list.map(fieldobj => {
if(fieldnames.includes(fieldobj._label)) {
fieldobj.refresh();
}
});
},

refresh_section_count: function() {
this.wrapper.find(".section-count-label:visible").each(function(i) {
$(this).html(i+1);
@@ -241,7 +290,7 @@ frappe.ui.form.Layout = Class.extend({
if(doctype)
return me.handle_tab(doctype, fieldname, ev.shiftKey);
}
})
});
},
handle_tab: function(doctype, fieldname, shift) {
var me = this,
@@ -264,7 +313,7 @@ frappe.ui.form.Layout = Class.extend({
if(fields[i].df.fieldname==fieldname) {
if(shift) {
if(prev) {
this.set_focus(prev)
this.set_focus(prev);
} else {
$(this.primary_button).focus();
}
@@ -290,7 +339,7 @@ frappe.ui.form.Layout = Class.extend({
// last row, close it and find next field
grid_row.toggle_view(false, function() {
grid_row.grid.frm.layout.handle_tab(grid_row.grid.df.parent, grid_row.grid.df.fieldname);
})
});
} else {
// next row
grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true);
@@ -325,7 +374,7 @@ frappe.ui.form.Layout = Class.extend({
}
},
is_visible: function(field) {
return field.disp_status==="Write" && (field.$wrapper && field.$wrapper.is(":visible"))
return field.disp_status==="Write" && (field.$wrapper && field.$wrapper.is(":visible"));
},
set_focus: function(field) {
// next is table, show the table
@@ -450,17 +499,20 @@ frappe.ui.form.Section = Class.extend({
.appendTo(this.layout.page);
this.layout.sections.push(this);

var section = this.wrapper[0];

if(this.df) {
if(this.df.label) {
this.make_head();
}
if(this.df.description) {
$('<div class="col-sm-12 small text-muted form-section-description">' + __(this.df.description) + '</div>')
.appendTo(this.wrapper);
.appendTo(this.wrapper);
}
if(this.df.cssClass) {
this.wrapper.addClass(this.df.cssClass);
}
}


// for bc
this.body = $('<div class="section-body">').appendTo(this.wrapper);
},
@@ -469,7 +521,7 @@ frappe.ui.form.Section = Class.extend({
if(!this.df.collapsible) {
$('<div class="col-sm-12"><h6 class="form-section-heading uppercase">'
+ __(this.df.label) + '</h6></div>')
.appendTo(this.wrapper);
.appendTo(this.wrapper);
} else {
this.head = $('<div class="section-head"><a class="h6 uppercase">'
+__(this.df.label)+'</a><span class="octicon octicon-chevron-down collapse-indicator"></span></div>').appendTo(this.wrapper);
@@ -521,7 +573,7 @@ frappe.ui.form.Section = Class.extend({
}
return missing_mandatory;
}
})
});

frappe.ui.form.Column = Class.extend({
init: function(section, df) {
@@ -538,7 +590,7 @@ frappe.ui.form.Column = Class.extend({
</form>\
</div>').appendTo(this.section.body)
.find("form")
.on("submit", function() { return false; })
.on("submit", function() { return false; });

if(this.df.label) {
$('<label class="control-label">'+ __(this.df.label)
@@ -557,4 +609,4 @@ frappe.ui.form.Column = Class.extend({
refresh: function() {
this.section.refresh();
}
})
});

+ 86
- 50
frappe/public/js/frappe/form/quick_entry.js Parādīt failu

@@ -1,20 +1,37 @@
frappe.provide('frappe.ui.form');

frappe.ui.form.make_quick_entry = (doctype, after_insert) => {
var trimmed_doctype = doctype.replace(/ /g, '');
var controller_name = "QuickEntryForm";

if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){
controller_name = trimmed_doctype + "QuickEntryForm";
}

frappe.quick_entry = new frappe.ui.form[controller_name](doctype, after_insert);
return frappe.quick_entry.setup();
};

frappe.ui.form.QuickEntryForm = Class.extend({
init: function(doctype, success_function){
init: function(doctype, after_insert){
this.doctype = doctype;
this.success_function = success_function;
this.setup();
this.after_insert = after_insert;
},

setup: function(){
var me = this;
frappe.model.with_doctype(this.doctype, function() {
me.set_meta_and_mandatory_fields();
var validate_flag = me.validate_quick_entry();
if(!validate_flag){
me.render_dialog();
}
setup: function() {
let me = this;
return new Promise(resolve => {
frappe.model.with_doctype(this.doctype, function() {
me.set_meta_and_mandatory_fields();
if(me.is_quick_entry()) {
me.render_dialog();
resolve(me);
} else {
frappe.quick_entry = null;
frappe.set_route('Form', me.doctype, me.doc.name)
.then(() => resolve(me));
}
});
});
},

@@ -25,34 +42,34 @@ frappe.ui.form.QuickEntryForm = Class.extend({
this.doc = frappe.model.get_new_doc(this.doctype, null, null, true);
},

validate_quick_entry: function(){
is_quick_entry: function(){
if(this.meta.quick_entry != 1) {
frappe.set_route('Form', this.doctype, this.doc.name);
return true;
return false;
}
var mandatory_flag = this.validate_mandatory_length();
var child_table_flag = this.validate_for_child_table();

if (mandatory_flag || child_table_flag){
return true;
if (this.too_many_mandatory_fields() || this.has_child_table()) {
return false;
}

this.validate_for_prompt_autoname();
return true;
},

validate_mandatory_length: function(){
too_many_mandatory_fields: function(){
if(this.mandatory.length > 7) {
// too many fields, show form
frappe.set_route('Form', this.doctype, this.doc.name);
return true;
}
return false;
},

validate_for_child_table: function(){
if($.map(this.mandatory, function(d) { return d.fieldtype==='Table' ? d : null; }).length) {
has_child_table: function(){
if($.map(this.mandatory, function(d) {
return d.fieldtype==='Table' ? d : null; }).length) {
// has mandatory table, quit!
frappe.set_route('Form', this.doctype, this.doc.name);
return true;
}
return false;
},

validate_for_prompt_autoname: function(){
@@ -86,6 +103,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
}
});

this.dialog.onhide = () => frappe.quick_entry = null;
this.dialog.show();
this.set_defaults();
},
@@ -93,44 +111,62 @@ frappe.ui.form.QuickEntryForm = Class.extend({
register_primary_action: function(){
var me = this;
this.dialog.set_primary_action(__('Save'), function() {
if(me.dialog.working) return;
if(me.dialog.working) {
return;
}
var data = me.dialog.get_values();

if(data) {
me.dialog.working = true;
var values = me.update_doc();
me.insert_document(values);
me.insert();
}
});
},

insert_document: function(values){
var me = this;
frappe.call({
method: "frappe.client.insert",
args: {
doc: values
},
callback: function(r) {
me.dialog.hide();
// delete the old doc
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name);
var doc = r.message;
if(me.success_function) {
me.success_function(doc);
}
frappe.ui.form.update_calling_link(doc.name);
},
error: function() {
me.open_doc();
},
always: function() {
me.dialog.working = false;
},
freeze: true
insert: function() {
let me = this;
return new Promise(resolve => {
me.update_doc();
frappe.call({
method: "frappe.client.insert",
args: {
doc: me.dialog.doc
},
callback: function(r) {
me.dialog.hide();
// delete the old doc
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name);
me.dialog.doc = r.message;
if(frappe._from_link) {
frappe.ui.form.update_calling_link(me.dialog.doc);
} else {
if(me.after_insert) {
me.after_insert(me.dialig.doc);
} else {
me.open_from_if_not_list();
}
}
},
error: function() {
me.open_doc();
},
always: function() {
me.dialog.working = false;
resolve(me.dialog.doc);
},
freeze: true
});
});
},

open_from_if_not_list: function() {
let route = frappe.get_route();
let doc = this.dialog.doc;
if(route && !(route[0]==='List' && route[1]===doc.doctype)) {
frappe.set_route('Form', doc.doctype, doc.name);
}
},

update_doc: function(){
var me = this;
var data = this.dialog.get_values(true);


+ 19
- 3
frappe/public/js/frappe/form/save.js Parādīt failu

@@ -176,6 +176,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
console.log("Already saving. Please wait a few moments.")
throw "saving";
}

frappe.ui.form.remove_old_form_route();
frappe.ui.form.is_saving = true;

return frappe.call({
@@ -206,7 +208,18 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
}
}

frappe.ui.form.update_calling_link = function (newdoc) {
frappe.ui.form.remove_old_form_route = () => {
let index = -1;
let current_route = frappe.get_route();
frappe.route_history.map((arr, i) => {
if (arr.join("/") === current_route.join("/")) {
index = i;
}
});
frappe.route_history.splice(index, 1);
}

frappe.ui.form.update_calling_link = (newdoc) => {
if (frappe._from_link && newdoc.doctype === frappe._from_link.df.options) {
var doc = frappe.get_doc(frappe._from_link.doctype, frappe._from_link.docname);
// set value
@@ -226,8 +239,11 @@ frappe.ui.form.update_calling_link = function (newdoc) {

// if from form, switch
if (frappe._from_link.frm) {
frappe.set_route("Form", frappe._from_link.frm.doctype, frappe._from_link.frm.docname);
setTimeout(function () { frappe.utils.scroll_to(frappe._from_link_scrollY); }, 100);
frappe.set_route("Form",
frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
.then(() => {
frappe.utils.scroll_to(frappe._from_link_scrollY);
});
}

frappe._from_link = null;


+ 67
- 15
frappe/public/js/frappe/form/script_manager.js Parādīt failu

@@ -55,8 +55,8 @@ frappe.ui.form.off = function(doctype, fieldname, handler) {
}


frappe.ui.form.trigger = function(doctype, fieldname, callback) {
cur_frm.script_manager.trigger(fieldname, doctype, null, callback);
frappe.ui.form.trigger = function(doctype, fieldname) {
cur_frm.script_manager.trigger(fieldname, doctype);
}

frappe.ui.form.ScriptManager = Class.extend({
@@ -64,32 +64,84 @@ frappe.ui.form.ScriptManager = Class.extend({
$.extend(this, opts);
},
make: function(ControllerClass) {
this.frm.cscript = $.extend(this.frm.cscript, new ControllerClass({frm: this.frm}));
this.frm.cscript = $.extend(this.frm.cscript,
new ControllerClass({frm: this.frm}));
},
trigger: function(event_name, doctype, name, callback) {
var me = this;
trigger: function(event_name, doctype, name) {
// trigger all the form level events that
// are bound to this event_name
let me = this;
doctype = doctype || this.frm.doctype;
name = name || this.frm.docname;
var handlers = this.get_handlers(event_name, doctype, name, callback);
if(callback) handlers.push(callback);

let tasks = [];
let handlers = this.get_handlers(event_name, doctype);

// helper for child table
this.frm.selected_doc = frappe.get_doc(doctype, name);

return $.when.apply($, $.map(handlers, function(fn) { return fn(); }));
let runner = (_function, is_old_style) => {
let _promise = null;
if(is_old_style) {
// old style arguments (doc, cdt, cdn)
_promise = me.frm.cscript[_function](me.frm.doc, doctype, name);
} else {
// new style (frm, doctype, name)
_promise = _function(me.frm, doctype, name);
}

// if the trigger returns a promise, return it,
// or use the default promise frappe.after_ajax
if (_promise && _promise.then) {
return _promise;
} else {
return frappe.after_server_call();
}
};

// make list of functions to be run serially
handlers.new_style.forEach((_function) => {
if(event_name==='setup') {
// setup must be called immediately
runner(_function, false);
} else {
tasks.push(() => runner(_function, false));
}
});

handlers.old_style.forEach((_function) => {
if(event_name==='setup') {
// setup must be called immediately
runner(_function, true);
} else {
tasks.push(() => runner(_function, true));
}
});

// run them serially
return frappe.run_serially(tasks);
},
get_handlers: function(event_name, doctype, name, callback) {
var handlers = [];
var me = this;
has_handlers: function(event_name, doctype) {
let handlers = this.get_handlers(event_name, doctype);
return handlers && (handlers.old_style.length || handlers.new_style.length);
},
get_handlers: function(event_name, doctype) {
// returns list of all functions to be called (old style and new style)
let me = this;
let handlers = {
old_style: [],
new_style: []
};
if(frappe.ui.form.handlers[doctype] && frappe.ui.form.handlers[doctype][event_name]) {
$.each(frappe.ui.form.handlers[doctype][event_name], function(i, fn) {
handlers.push(function() { return fn(me.frm, doctype, name) });
handlers.new_style.push(fn);
});
}
if(this.frm.cscript[event_name]) {
handlers.push(function() { return me.frm.cscript[event_name](me.frm.doc, doctype, name); });
handlers.old_style.push(event_name);
}
if(this.frm.cscript["custom_" + event_name]) {
handlers.push(function() { return me.frm.cscript["custom_" + event_name](me.frm.doc, doctype, name); });
handlers.old_style.push("custom_" + event_name);
}
return handlers;
},
@@ -105,7 +157,7 @@ frappe.ui.form.ScriptManager = Class.extend({

if(doctype.__custom_js) {
try {
eval(doctype.__custom_js)
eval(doctype.__custom_js);
} catch(e) {
frappe.msgprint({
title: __('Error in Custom Script'),


+ 1
- 3
frappe/public/js/frappe/form/templates/form_dashboard.html Parādīt failu

@@ -1,6 +1,4 @@
<div class="form-dashboard hidden">
<h4 class="form-headline hidden form-dashboard-section">
</h4>
<div class="form-dashboard-wrapper">
<div class="progress-area hidden form-dashboard-section">
</div>
<div class="form-heatmap hidden form-dashboard-section">


+ 1
- 2
frappe/public/js/frappe/form/templates/form_links.html Parādīt failu

@@ -1,9 +1,8 @@
<div class="form-documents">
<h5 style="margin: 5px 0px;">{{__("Related Documents")}}</h5>
{% for (var i=0; i < transactions.length; i++) { %}
{% if((i % 2)===0) { %}<div class="row">{% } %}
<div class="col-xs-6">
<h6 class="uppercase">{{ transactions[i].label }}</h5>
<h6>{{ transactions[i].label }}</h6>
{% for (var j=0; j < transactions[i].items.length; j++) {
var doctype = transactions[i].items[j]; %}
<div class="document-link"


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels

Notiek ielāde…
Atcelt
Saglabāt