瀏覽代碼

Merge branch 'develop' into staging

version-14
Nabin Hait 7 年之前
父節點
當前提交
4ef61f3f30
共有 100 個檔案被更改,包括 3757 行新增2312 行删除
  1. +3
    -1
      .eslintrc
  2. +1
    -1
      .travis.yml
  3. +0
    -1
      README.md
  4. +2
    -2
      frappe/__init__.py
  5. +8
    -6
      frappe/app.py
  6. +46
    -21
      frappe/build.js
  7. +8
    -3
      frappe/commands/site.py
  8. +5
    -0
      frappe/config/setup.py
  9. +80
    -21
      frappe/core/doctype/communication/communication.json
  10. +4
    -4
      frappe/core/doctype/communication/communication.py
  11. +69
    -17
      frappe/core/doctype/communication/email.py
  12. +3
    -1
      frappe/core/doctype/deleted_document/deleted_document.py
  13. +2
    -2
      frappe/core/doctype/docfield/docfield.json
  14. +6
    -3
      frappe/core/doctype/doctype/doctype.py
  15. +37
    -14
      frappe/core/doctype/file/file.json
  16. +21
    -0
      frappe/core/doctype/file/file.py
  17. +0
    -0
      frappe/core/doctype/role_profile/__init__.py
  18. +23
    -0
      frappe/core/doctype/role_profile/role_profile.js
  19. +175
    -0
      frappe/core/doctype/role_profile/role_profile.json
  20. +16
    -0
      frappe/core/doctype/role_profile/role_profile.py
  21. +33
    -0
      frappe/core/doctype/role_profile/test_role_profile.js
  22. +24
    -0
      frappe/core/doctype/role_profile/test_role_profile.py
  23. +1
    -1
      frappe/core/doctype/user/test_user.js
  24. +35
    -0
      frappe/core/doctype/user/test_user_with_role_profile.js
  25. +32
    -7
      frappe/core/doctype/user/user.js
  26. +34
    -4
      frappe/core/doctype/user/user.json
  27. +23
    -1
      frappe/core/doctype/user/user.py
  28. +2
    -2
      frappe/custom/doctype/custom_field/custom_field.json
  29. +23
    -0
      frappe/custom/doctype/custom_field/test_custom_field.js
  30. +1
    -0
      frappe/custom/doctype/customize_form/customize_form.js
  31. +4
    -7
      frappe/custom/doctype/customize_form/customize_form.py
  32. +2
    -2
      frappe/custom/doctype/customize_form_field/customize_form_field.json
  33. +35
    -23
      frappe/database.py
  34. +17
    -0
      frappe/desk/calendar.py
  35. +0
    -0
      frappe/desk/doctype/calendar_view/__init__.py
  36. +35
    -0
      frappe/desk/doctype/calendar_view/calendar_view.js
  37. +184
    -0
      frappe/desk/doctype/calendar_view/calendar_view.json
  38. +9
    -0
      frappe/desk/doctype/calendar_view/calendar_view.py
  39. +3
    -2
      frappe/desk/page/activity/activity.js
  40. +5
    -3
      frappe/desk/query_builder.py
  41. +5
    -3
      frappe/desk/reportview.py
  42. +19
    -2
      frappe/docs/user/en/tutorial/task-runner.md
  43. +8
    -3
      frappe/email/doctype/email_alert/email_alert.py
  44. +7
    -4
      frappe/email/email_body.py
  45. +27
    -14
      frappe/email/queue.py
  46. +3
    -5
      frappe/exceptions.py
  47. +23
    -15
      frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
  48. +2
    -2
      frappe/integrations/doctype/gsuite_settings/gsuite_settings.json
  49. +6
    -3
      frappe/integrations/doctype/paypal_settings/paypal_settings.py
  50. +4
    -4
      frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
  51. +0
    -1
      frappe/model/base_document.py
  52. +67
    -9
      frappe/model/db_query.py
  53. +16
    -12
      frappe/model/db_schema.py
  54. +3
    -1
      frappe/modules/import_file.py
  55. +1
    -1
      frappe/patches.txt
  56. +16
    -6
      frappe/public/build.json
  57. +0
    -284
      frappe/public/css/charts.css
  58. +19
    -0
      frappe/public/css/list.css
  59. 二進制
      frappe/public/images/leaflet/layers-2x.png
  60. 二進制
      frappe/public/images/leaflet/layers.png
  61. 二進制
      frappe/public/images/leaflet/leafletmarker-icon.png
  62. 二進制
      frappe/public/images/leaflet/leafletmarker-shadow.png
  63. 二進制
      frappe/public/images/leaflet/lego.png
  64. 二進制
      frappe/public/images/leaflet/marker-icon-2x.png
  65. 二進制
      frappe/public/images/leaflet/marker-icon.png
  66. 二進制
      frappe/public/images/leaflet/marker-shadow.png
  67. 二進制
      frappe/public/images/leaflet/spritesheet-2x.png
  68. 二進制
      frappe/public/images/leaflet/spritesheet.png
  69. +156
    -0
      frappe/public/images/leaflet/spritesheet.svg
  70. +21
    -0
      frappe/public/js/frappe/db.js
  71. +5
    -0
      frappe/public/js/frappe/dom.js
  72. +20
    -18
      frappe/public/js/frappe/form/controls/autocomplete.js
  73. +184
    -0
      frappe/public/js/frappe/form/controls/geolocation.js
  74. +29
    -0
      frappe/public/js/frappe/form/controls/multiselect.js
  75. +2
    -2
      frappe/public/js/frappe/form/dashboard.js
  76. +3
    -1
      frappe/public/js/frappe/form/grid.js
  77. +0
    -2
      frappe/public/js/frappe/form/quick_entry.js
  78. +4
    -1
      frappe/public/js/frappe/form/toolbar.js
  79. +31
    -0
      frappe/public/js/frappe/list/list_renderer.js
  80. +40
    -0
      frappe/public/js/frappe/list/list_sidebar.js
  81. +7
    -0
      frappe/public/js/frappe/list/list_view.js
  82. +26
    -22
      frappe/public/js/frappe/roles_editor.js
  83. +0
    -1556
      frappe/public/js/frappe/ui/charts.js
  84. +3
    -1
      frappe/public/js/frappe/ui/colors.js
  85. +87
    -12
      frappe/public/js/frappe/views/calendar/calendar.js
  86. +8
    -2
      frappe/public/js/frappe/views/communication.js
  87. +157
    -82
      frappe/public/js/frappe/views/image/image_view.js
  88. +48
    -44
      frappe/public/js/frappe/views/image/photoswipe_dom.html
  89. +1
    -1
      frappe/public/js/frappe/views/reports/grid_report.js
  90. +1
    -1
      frappe/public/js/frappe/views/reports/query_report.js
  91. +12
    -8
      frappe/public/js/frappe/views/treeview.js
  92. +2
    -2
      frappe/public/js/legacy/form.js
  93. +0
    -39
      frappe/public/js/lib/Chart.min.js
  94. +2
    -0
      frappe/public/js/lib/frappe-charts/frappe-charts.min.js
  95. +12
    -0
      frappe/public/js/lib/leaflet/L.Control.Locate.css
  96. +591
    -0
      frappe/public/js/lib/leaflet/L.Control.Locate.js
  97. +56
    -0
      frappe/public/js/lib/leaflet/easy-button.css
  98. +370
    -0
      frappe/public/js/lib/leaflet/easy-button.js
  99. +632
    -0
      frappe/public/js/lib/leaflet/leaflet.css
  100. +10
    -0
      frappe/public/js/lib/leaflet/leaflet.draw.css

+ 3
- 1
.eslintrc 查看文件

@@ -119,6 +119,8 @@
"getCookies": true,
"get_url_arg": true,
"QUnit": true,
"JsBarcode": true
"JsBarcode": true,
"L": true,
"Chart": true
}
}

+ 1
- 1
.travis.yml 查看文件

@@ -56,4 +56,4 @@ script:
- set -e
- bench run-tests
- sleep 5
- bench run-ui-tests --app frappe
- bench run-ui-tests --app frappe

+ 0
- 1
README.md 查看文件

@@ -40,7 +40,6 @@ Full-stack web application framework that uses Python and MariaDB on the server
### Website

For details and documentation, see the website

[https://frappe.io](https://frappe.io)

### License


+ 2
- 2
frappe/__init__.py 查看文件

@@ -378,7 +378,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
as_markdown=False, delayed=True, reference_doctype=None, reference_name=None,
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
attachments=None, content=None, doctype=None, name=None, reply_to=None,
cc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None,
cc=[], bcc=[], 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, template=None, args=None, header=None):
"""Send email using user's default **Email Account** or global default **Email Account**.
@@ -426,7 +426,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No 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,
attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, 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, header=header)


+ 8
- 6
frappe/app.py 查看文件

@@ -4,7 +4,6 @@
from __future__ import unicode_literals

import os
import MySQLdb
from six import iteritems
import logging

@@ -27,6 +26,12 @@ from frappe.utils.error import make_error_snapshot
from frappe.core.doctype.communication.comment import update_comments_in_parent_after_request
from frappe import _

# imports - third-party imports
import pymysql
from pymysql.constants import ER

# imports - module imports

local_manager = LocalManager([frappe.local])

_site = None
@@ -134,11 +139,8 @@ def handle_exception(e):
response = frappe.utils.response.report_error(http_status_code)

elif (http_status_code==500
and isinstance(e, MySQLdb.OperationalError)
and e.args[0] in (1205, 1213)):
# 1205 = lock wait timeout
# 1213 = deadlock
# code 409 represents conflict
and isinstance(e, pymysql.InternalError)
and e.args[0] in (ER.LOCK_WAIT_TIMEOUT, ER.LOCK_DEADLOCK)):
http_status_code = 508

elif http_status_code==401:


+ 46
- 21
frappe/build.js 查看文件

@@ -20,6 +20,7 @@ const apps = apps_contents.split('\n');
const app_paths = apps.map(app => path_join(apps_path, app, app)) // base_path of each app
const assets_path = path_join(sites_path, 'assets');
let build_map = make_build_map();
let compiled_js_cache = {}; // cache each js file after it is compiled
const file_watcher_port = get_conf().file_watcher_port;

// command line args
@@ -60,11 +61,12 @@ function watch() {
io.emit('reload_css', filename);
}
});
// watch_js(function (filename) {
// if(socket_connection) {
// io.emit('reload_js', filename);
// }
// });
watch_js(//function (filename) {
// if(socket_connection) {
// io.emit('reload_js', filename);
// }
//}
);
watch_build_json();
});

@@ -77,9 +79,7 @@ function watch() {
});
}

function pack(output_path, inputs, minify) {
const output_type = output_path.split('.').pop();

function pack(output_path, inputs, minify, file_changed) {
let output_txt = '';
for (const file of inputs) {

@@ -88,25 +88,18 @@ function pack(output_path, inputs, minify) {
continue;
}

let file_content = fs.readFileSync(file, 'utf-8');

if (file.endsWith('.html') && output_type === 'js') {
file_content = html_to_js_template(file, file_content);
}

if(file.endsWith('class.js')) {
file_content = minify_js(file_content, file);
let force_compile = false;
if (file_changed) {
// if file_changed is passed and is equal to file, force_compile it
force_compile = file_changed === file;
}

if (file.endsWith('.js') && !file.includes('/lib/') && output_type === 'js' && !file.endsWith('class.js')) {
file_content = babelify(file_content, file, minify);
}
let file_content = get_compiled_file(file, output_path, minify, force_compile);

if(!minify) {
output_txt += `\n/*\n *\t${file}\n */\n`
}
output_txt += file_content;

output_txt = output_txt.replace(/['"]use strict['"];/, '');
}

@@ -122,6 +115,38 @@ function pack(output_path, inputs, minify) {
}
}

function get_compiled_file(file, output_path, minify, force_compile) {
const output_type = output_path.split('.').pop();

let file_content;

if (force_compile === false) {
// force compile is false
// attempt to get from cache
file_content = compiled_js_cache[file];
if (file_content) {
return file_content;
}
}

file_content = fs.readFileSync(file, 'utf-8');

if (file.endsWith('.html') && output_type === 'js') {
file_content = html_to_js_template(file, file_content);
}

if(file.endsWith('class.js')) {
file_content = minify_js(file_content, file);
}

if (file.endsWith('.js') && !file.includes('/lib/') && output_type === 'js' && !file.endsWith('class.js')) {
file_content = babelify(file_content, file, minify);
}

compiled_js_cache[file] = file_content;
return file_content;
}

function babelify(content, path, minify) {
let presets = ['env'];
// Minification doesn't work when loading Frappe Desk
@@ -262,7 +287,7 @@ function watch_js(ondirty) {
for (const target in build_map) {
const sources = build_map[target];
if (sources.includes(filename)) {
pack(target, sources);
pack(target, sources, null, filename);
ondirty && ondirty(target);
// break;
}


+ 8
- 3
frappe/commands/site.py 查看文件

@@ -3,7 +3,6 @@ import click
import hashlib, os, sys, compileall
import frappe
from frappe import _
from _mysql_exceptions import ProgrammingError
from frappe.commands import pass_context, get_site
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.limits import update_limits, get_limits
@@ -11,6 +10,12 @@ from frappe.installer import update_site_config
from frappe.utils import touch_file, get_site_path
from six import text_type

# imports - third-party imports
from pymysql.constants import ER

# imports - module imports
from frappe.exceptions import SQLError

@click.command('new-site')
@click.argument('site')
@click.option('--db-name', help='Database name')
@@ -348,8 +353,8 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=

try:
scheduled_backup(ignore_files=False, force=True)
except ProgrammingError as err:
if err[0] == 1146:
except SQLError as err:
if err[0] == ER.NO_SUCH_TABLE:
if force:
pass
else:


+ 5
- 0
frappe/config/setup.py 查看文件

@@ -17,6 +17,11 @@ def get_data():
"type": "doctype",
"name": "Role",
"description": _("User Roles")
},
{
"type": "doctype",
"name": "Role Profile",
"description": _("Role Profile")
}
]
},


+ 80
- 21
frappe/core/doctype/communication/communication.json 查看文件

@@ -15,6 +15,7 @@
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -43,6 +44,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
@@ -73,6 +75,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -104,6 +107,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -134,6 +138,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -162,6 +167,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -192,6 +198,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -223,6 +230,39 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.communication_medium===\"Email\"",
"fieldname": "bcc",
"fieldtype": "Code",
"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": "BCC",
"length": 0,
"no_copy": 0,
"options": "Email",
"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,
@@ -252,6 +292,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -283,6 +324,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -311,6 +353,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -340,6 +383,7 @@
"width": "400"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
@@ -369,6 +413,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -398,6 +443,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -429,6 +475,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -459,6 +506,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -487,6 +535,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -518,6 +567,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -548,6 +598,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
@@ -576,6 +627,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -605,6 +657,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -634,6 +687,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -662,6 +716,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -691,6 +746,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
@@ -720,6 +776,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -750,6 +807,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -780,6 +838,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -810,6 +869,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -841,6 +901,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -871,6 +932,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -901,6 +963,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -929,6 +992,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -959,6 +1023,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -989,6 +1054,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1019,6 +1085,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1049,6 +1116,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1079,6 +1147,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1109,6 +1178,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1138,6 +1208,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1166,6 +1237,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
@@ -1195,6 +1267,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1224,6 +1297,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
@@ -1253,6 +1327,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1283,6 +1358,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1312,6 +1388,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
@@ -1342,6 +1419,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1371,6 +1449,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -1411,7 +1490,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-03-29 23:06:16.469149",
"modified": "2017-10-25 12:53:49.547620",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",
@@ -1477,26 +1556,6 @@
"submit": 0,
"user_permission_doctypes": "[\"Email Account\"]",
"write": 0
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Super Email User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
}
],
"quick_entry": 0,


+ 4
- 4
frappe/core/doctype/communication/communication.py 查看文件

@@ -189,7 +189,7 @@ class Communication(Document):
self.notify(print_html, print_format, attachments, recipients)

def notify(self, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, fetched_from_email_account=False):
recipients=None, cc=None, bcc=None,fetched_from_email_account=False):
"""Calls a delayed task 'sendmail' that enqueus email in Email Queue queue

:param print_html: Send given value as HTML attachment
@@ -200,13 +200,13 @@ class Communication(Document):
:param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient

"""
notify(self, print_html, print_format, attachments, recipients, cc,
notify(self, print_html, print_format, attachments, recipients, cc, bcc,
fetched_from_email_account)

def _notify(self, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None):
recipients=None, cc=None, bcc=None):

_notify(self, print_html, print_format, attachments, recipients, cc)
_notify(self, print_html, print_format, attachments, recipients, cc, bcc)

def bot_reply(self):
if self.comment_type == 'Bot' and self.communication_type == 'Chat':


+ 69
- 17
frappe/core/doctype/communication/email.py 查看文件

@@ -14,15 +14,18 @@ from frappe.email.queue import check_email_limit
from frappe.utils.scheduler import log
from frappe.email.email_body import get_message_id
import frappe.email.smtp
import MySQLdb
import time
from frappe import _
from frappe.utils.background_jobs import enqueue

# imports - third-party imports
import pymysql
from pymysql.constants import ER

@frappe.whitelist()
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False,
print_html=None, print_format=None, attachments='[]', send_me_a_copy=False, cc=None, flags=None,read_receipt=None):
print_html=None, print_format=None, attachments='[]', send_me_a_copy=False, cc=None, bcc=None, flags=None,read_receipt=None):
"""Make a new communication.

:param doctype: Reference DocType.
@@ -58,6 +61,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
"sender_full_name":sender_full_name,
"recipients": recipients,
"cc": cc or None,
"bcc": bcc or None,
"communication_medium": communication_medium,
"sent_or_received": sent_or_received,
"reference_doctype": doctype,
@@ -102,10 +106,13 @@ def validate_email(doc):
for email in split_emails(doc.cc):
validate_email_add(email, throw=True)

for email in split_emails(doc.bcc):
validate_email_add(email, throw=True)

# validate sender

def notify(doc, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, fetched_from_email_account=False):
recipients=None, cc=None, bcc=None, fetched_from_email_account=False):
"""Calls a delayed task 'sendmail' that enqueus email in Email Queue queue

:param print_html: Send given value as HTML attachment
@@ -113,10 +120,11 @@ def notify(doc, print_html=None, print_format=None, attachments=None,
:param attachments: A list of filenames that should be attached when sending this email
:param recipients: Email recipients
:param cc: Send email as CC to
:param bcc: Send email as BCC to
:param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient

"""
recipients, cc = get_recipients_and_cc(doc, recipients, cc,
recipients, cc, bcc = get_recipients_cc_and_bcc(doc, recipients, cc, bcc,
fetched_from_email_account=fetched_from_email_account)

if not recipients:
@@ -127,16 +135,16 @@ def notify(doc, print_html=None, print_format=None, attachments=None,
if frappe.flags.in_test:
# for test cases, run synchronously
doc._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc)
recipients=recipients, cc=cc, bcc=None)
else:
check_email_limit(list(set(doc.sent_email_addresses)))
enqueue(sendmail, queue="default", timeout=300, event="sendmail",
communication_name=doc.name,
print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc, lang=frappe.local.lang, session=frappe.local.session)
recipients=recipients, cc=cc, bcc=bcc, lang=frappe.local.lang, session=frappe.local.session)

def _notify(doc, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None):
recipients=None, cc=None, bcc=None):

prepare_to_notify(doc, print_html, print_format, attachments)

@@ -148,6 +156,7 @@ def _notify(doc, print_html=None, print_format=None, attachments=None,
frappe.sendmail(
recipients=(recipients or []),
cc=(cc or []),
bcc=(bcc or []),
expose_recipients="header",
sender=doc.sender,
reply_to=doc.incoming_email_account,
@@ -190,7 +199,7 @@ def update_parent_mins_to_first_response(doc):
parent.run_method('notify_communication', doc)
parent.notify_update()

def get_recipients_and_cc(doc, recipients, cc, fetched_from_email_account=False):
def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_account=False):
doc.all_email_addresses = []
doc.sent_email_addresses = []
doc.previous_email_sender = None
@@ -201,6 +210,9 @@ def get_recipients_and_cc(doc, recipients, cc, fetched_from_email_account=False)
if not cc:
cc = get_cc(doc, recipients, fetched_from_email_account=fetched_from_email_account)

if not bcc:
bcc = get_bcc(doc, recipients, fetched_from_email_account=fetched_from_email_account)

if fetched_from_email_account:
# email was already sent to the original recipient by the sender's email service
original_recipients, recipients = recipients, []
@@ -216,10 +228,13 @@ def get_recipients_and_cc(doc, recipients, cc, fetched_from_email_account=False)
# don't cc to people who already received the mail from sender's email service
cc = list(set(cc) - set(original_cc) - set(original_recipients))

original_bcc = split_emails(doc.bcc)
bcc = list(set(bcc) - set(original_bcc) - set(original_recipients))

if 'Administrator' in recipients:
recipients.remove('Administrator')

return recipients, cc
return recipients, cc, bcc

def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None):
"""Prepare to make multipart MIME Email
@@ -247,8 +262,8 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
doc.attachments = []

if print_html or print_format:
doc.attachments.append(frappe.attach_print(doc.reference_doctype, doc.reference_name,
print_format=print_format, html=print_html))
doc.attachments.append({"print_format_attachment":1, "doctype":doc.reference_doctype,
"name":doc.reference_name, "print_format":print_format, "html":print_html})

if attachments:
if isinstance(attachments, string_types):
@@ -258,8 +273,11 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
if isinstance(a, string_types):
# is it a filename?
try:
# keep this for error handling
file = get_file(a)
doc.attachments.append({"fname": file[0], "fcontent": file[1]})
# these attachments will be attached on-demand
# and won't be stored in the message
doc.attachments.append({"fid": a})
except IOError:
frappe.throw(_("Unable to find attachment {0}").format(a))
else:
@@ -345,6 +363,34 @@ def get_cc(doc, recipients=None, fetched_from_email_account=False):

return cc

def get_bcc(doc, recipients=None, fetched_from_email_account=False):
"""Build a list of email addresses for BCC"""
bcc = split_emails(doc.bcc)

if doc.reference_doctype and doc.reference_name:
if fetched_from_email_account:
bcc.append(get_owner_email(doc))
bcc += get_assignees(doc)

if getattr(doc, "send_me_a_copy", False) and doc.sender not in bcc:
bcc.append(doc.sender)

if bcc:
exclude = []
exclude += [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)]
exclude += [(parse_addr(email)[1] or "").lower() for email in recipients]

if fetched_from_email_account:
# exclude sender when pulling email
exclude += [parse_addr(doc.sender)[1]]

if doc.reference_doctype and doc.reference_name:
exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"],
{"reference_doctype": doc.reference_doctype, "reference_name": doc.reference_name}, as_list=True)]

bcc = filter_email_list(doc, bcc, exclude, is_bcc=True)

return bcc

def add_attachments(name, attachments):
'''Add attachments to the given Communiction'''
@@ -360,7 +406,7 @@ def add_attachments(name, attachments):
save_url(attach.file_url, attach.file_name, "Communication", name,
"Home/Attachments", attach.is_private)

def filter_email_list(doc, email_list, exclude, is_cc=False):
def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False):
# temp variables
filtered = []
email_address_list = []
@@ -382,6 +428,11 @@ def filter_email_list(doc, email_list, exclude, is_cc=False):
# don't send to disabled users
continue

if is_bcc:
is_user_enabled = frappe.db.get_value("User", email_address, "enabled")
if is_user_enabled==0:
continue

# make sure of case-insensitive uniqueness of email address
if email_address not in email_address_list:
# append the full email i.e. "Human <human@example.com>"
@@ -416,7 +467,7 @@ def get_attach_link(doc, print_format):
})

def sendmail(communication_name, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, lang=None, session=None):
recipients=None, cc=None, bcc=None, lang=None, session=None):
try:

if lang:
@@ -432,11 +483,11 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments
try:
communication = frappe.get_doc("Communication", communication_name)
communication._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc)
recipients=recipients, cc=cc, bcc=bcc)

except MySQLdb.OperationalError as e:
except pymysql.InternalError as e:
# deadlock, try again
if e.args[0]==1213:
if e.args[0] == ER.LOCK_DEADLOCK:
frappe.db.rollback()
time.sleep(1)
continue
@@ -453,6 +504,7 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments
"attachments": attachments,
"recipients": recipients,
"cc": cc,
"bcc": bcc,
"lang": lang
}))
frappe.logger(__name__).error(traceback)


+ 3
- 1
frappe/core/doctype/deleted_document/deleted_document.py 查看文件

@@ -17,7 +17,9 @@ def restore(name):
try:
doc.insert()
except frappe.DocstatusTransitionError:
frappe.throw(_("Cannot restore Cancelled Document"))
frappe.msgprint(_("Cancelled Document restored as Draft"))
doc.docstatus = 0
doc.insert()

doc.add_comment('Edit', _('restored {0} as {1}').format(deleted.deleted_name, doc.name))



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

@@ -96,7 +96,7 @@
"no_copy": 0,
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\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",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\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,
@@ -1364,7 +1364,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-10-07 19:20:15.888708",
"modified": "2017-10-24 11:39:56.795852",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",


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

@@ -4,7 +4,6 @@
from __future__ import unicode_literals

import re, copy, os
import MySQLdb
import frappe
from frappe import _

@@ -17,6 +16,10 @@ from frappe.modules import make_boilerplate
from frappe.model.db_schema import validate_column_name, validate_column_length
import frappe.website.render

# imports - third-party imports
import pymysql
from pymysql.constants import ER

class InvalidFieldNameError(frappe.ValidationError): pass

form_grid_templates = {
@@ -482,8 +485,8 @@ def validate_fields(meta):
group by `{fieldname}` having count(*) > 1 limit 1""".format(
doctype=d.parent, fieldname=d.fieldname))

except MySQLdb.OperationalError as e:
if e.args and e.args[0]==1054:
except pymysql.InternalError as e:
if e.args and e.args[0] == ER.BAD_FIELD_ERROR:
# ignore if missing column, else raise
# this happens in case of Custom Field
pass


+ 37
- 14
frappe/core/doctype/file/file.json 查看文件

@@ -1,5 +1,6 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "",
@@ -11,6 +12,7 @@
"editable_grid": 0,
"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": 0,
"collapsible": 0,
@@ -71,6 +74,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -100,6 +104,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,
@@ -157,6 +163,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,
@@ -244,6 +253,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -272,6 +282,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -301,6 +312,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -330,6 +342,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -360,6 +373,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -389,6 +403,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -418,6 +433,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -447,6 +463,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -475,6 +492,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -503,12 +521,13 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "content_hash",
"fieldtype": "Data",
"fieldname": "lft",
"fieldtype": "Int",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -516,26 +535,28 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Content Hash",
"label": "lft",
"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": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "lft",
"fieldname": "rgt",
"fieldtype": "Int",
"hidden": 1,
"ignore_user_permissions": 0,
@@ -544,7 +565,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "lft",
"label": "rgt",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@@ -560,12 +581,13 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "rgt",
"fieldtype": "Int",
"fieldname": "old_parent",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -573,7 +595,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "rgt",
"label": "old_parent",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@@ -589,20 +611,21 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "old_parent",
"fieldname": "content_hash",
"fieldtype": "Data",
"hidden": 1,
"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": "old_parent",
"label": "Content Hash",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@@ -618,19 +641,19 @@
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-file",
"idx": 1,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2017-02-17 16:42:36.092962",
"modified": "2017-10-27 13:27:43.882914",
"modified_by": "Administrator",
"module": "Core",
"name": "File",


+ 21
- 0
frappe/core/doctype/file/file.py 查看文件

@@ -423,3 +423,24 @@ def unzip_file(name):
'''Unzip the given file and make file records for each of the extracted files'''
file_obj = frappe.get_doc('File', name)
file_obj.unzip()

@frappe.whitelist()
def get_attached_images(doctype, names):
'''get list of image urls attached in form
returns {name: ['image.jpg', 'image.png']}'''

if isinstance(names, string_types):
names = json.loads(names)

img_urls = frappe.db.get_list('File', filters={
'attached_to_doctype': doctype,
'attached_to_name': ('in', names),
'is_folder': 0
}, fields=['file_url', 'attached_to_name as docname'])

out = frappe._dict()
for i in img_urls:
out[i.docname] = out.get(i.docname, [])
out[i.docname].append(i.file_url)

return out

+ 0
- 0
frappe/core/doctype/role_profile/__init__.py 查看文件


+ 23
- 0
frappe/core/doctype/role_profile/role_profile.js 查看文件

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

frappe.ui.form.on('Role Profile', {
setup: function(frm) {
if(has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
if(!frm.roles_editor) {
var role_area = $('<div style="min-height: 300px">')
.appendTo(frm.fields_dict.roles_html.wrapper);
frm.roles_editor = new frappe.RoleEditor(role_area, frm);
frm.roles_editor.show();
} else {
frm.roles_editor.show();
}
}
},

validate: function(frm) {
if(frm.roles_editor) {
frm.roles_editor.set_roles_in_table();
}
}
});

+ 175
- 0
frappe/core/doctype/role_profile/role_profile.json 查看文件

@@ -0,0 +1,175 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "role_profile",
"beta": 0,
"creation": "2017-08-31 04:16:38.764465",
"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": "role_profile",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Role Name",
"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": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "roles_html",
"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": "Roles HTML",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"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": "roles",
"fieldtype": "Table",
"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": "Roles Assigned",
"length": 0,
"no_copy": 0,
"options": "Has Role",
"permlevel": 1,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"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": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-10-17 11:05:11.183066",
"modified_by": "Administrator",
"module": "Core",
"name": "Role Profile",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "role_profile",
"track_changes": 1,
"track_seen": 0
}

+ 16
- 0
frappe/core/doctype/role_profile/role_profile.py 查看文件

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt

from __future__ import unicode_literals
from frappe.model.document import Document

class RoleProfile(Document):
def autoname(self):
"""set name as Role Profile name"""
self.name = self.role_profile

def on_update(self):
""" Changes in role_profile reflected across all its user """
from frappe.core.doctype.user.user import update_roles
update_roles(self.name)

+ 33
- 0
frappe/core/doctype/role_profile/test_role_profile.js 查看文件

@@ -0,0 +1,33 @@
QUnit.module('Core');

QUnit.test("test: Role Profile", function (assert) {
let done = assert.async();

assert.expect(3);

frappe.run_serially([
// insert a new user
() => frappe.tests.make('Role Profile', [
{role_profile: 'Test 2'}
]),

() => {
$('input.box')[0].checked = true;
$('input.box')[2].checked = true;
$('input.box')[4].checked = true;
cur_frm.save();
},

() => frappe.timeout(1),
() => cur_frm.refresh(),
() => frappe.timeout(2),
() => {
assert.equal($('input.box')[0].checked, true);
assert.equal($('input.box')[2].checked, true);
assert.equal($('input.box')[4].checked, true);
},

() => done()
]);

});

+ 24
- 0
frappe/core/doctype/role_profile/test_role_profile.py 查看文件

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest

class TestRoleProfile(unittest.TestCase):
def test_make_new_role_profile(self):
new_role_profile = frappe.get_doc(dict(doctype='Role Profile', role_profile='Test 1')).insert()

self.assertEquals(new_role_profile.role_profile, 'Test 1')

# add role
new_role_profile.append("roles", {
"role": '_Test Role 2'
})
new_role_profile.save()
self.assertEquals(new_role_profile.roles[0].role, '_Test Role 2')

# clear roles
new_role_profile.roles = []
new_role_profile.save()
self.assertEquals(new_role_profile.roles, [])

+ 1
- 1
frappe/core/doctype/user/test_user.js 查看文件

@@ -20,4 +20,4 @@ QUnit.test("test: User", function (assert) {
() => done()
]);

});
});

+ 35
- 0
frappe/core/doctype/user/test_user_with_role_profile.js 查看文件

@@ -0,0 +1,35 @@
QUnit.module('Core');

QUnit.test("test: Set role profile in user", function (assert) {
let done = assert.async();

assert.expect(3);

frappe.run_serially([

// Insert a new user
() => frappe.tests.make('User', [
{email: 'test@test2.com'},
{first_name: 'Test 2'},
{send_welcome_email: 0}
]),

() => frappe.timeout(2),
() => {
return frappe.tests.set_form_values(cur_frm, [
{role_profile_name:'Test 2'}
]);
},

() => cur_frm.save(),
() => frappe.timeout(2),

() => {
assert.equal($('input.box')[0].checked, true);
assert.equal($('input.box')[2].checked, true);
assert.equal($('input.box')[4].checked, true);
},
() => done()
]);

});

+ 32
- 7
frappe/core/doctype/user/user.js 查看文件

@@ -17,8 +17,30 @@ frappe.ui.form.on('User', {
}

},

role_profile_name: function(frm) {
if(frm.doc.role_profile_name) {
frappe.call({
"method": "frappe.core.doctype.user.user.get_role_profile",
args: {
role_profile: frm.doc.role_profile_name
},
callback: function (data) {
frm.set_value("roles", []);
$.each(data.message || [], function(i, v){
var d = frm.add_child("roles");
d.role = v.role;
});
frm.roles_editor.show();
}
});
}
},

onload: function(frm) {
if(has_common(frappe.user_roles, ["Administrator", "System Manager"]) && !frm.doc.__islocal) {
frm.can_edit_roles = has_common(frappe.user_roles, ["Administrator", "System Manager"]);

if(frm.can_edit_roles && !frm.is_new()) {
if(!frm.roles_editor) {
var role_area = $('<div style="min-height: 300px">')
.appendTo(frm.fields_dict.roles_html.wrapper);
@@ -34,7 +56,10 @@ frappe.ui.form.on('User', {
},
refresh: function(frm) {
var doc = frm.doc;

if(!frm.is_new() && !frm.roles_editor && frm.can_edit_roles) {
frm.reload_doc();
return;
}
if(doc.name===frappe.session.user && !doc.__unsaved
&& frappe.all_timezones
&& (doc.language || frappe.boot.user.language)
@@ -45,7 +70,7 @@ frappe.ui.form.on('User', {

frm.toggle_display(['sb1', 'sb3', 'modules_access'], false);

if(!doc.__islocal){
if(!frm.is_new()) {
frm.add_custom_button(__("Set Desktop Icons"), function() {
frappe.route_options = {
"user": doc.name
@@ -89,8 +114,8 @@ frappe.ui.form.on('User', {

frm.trigger('enabled');

if (frm.roles_editor) {
frm.roles_editor.disabled = frm.doc.role_profile_name ? 1 : 0;
if (frm.roles_editor && frm.can_edit_roles) {
frm.roles_editor.disable = frm.doc.role_profile_name ? 1 : 0;
frm.roles_editor.show();
}

@@ -133,13 +158,13 @@ frappe.ui.form.on('User', {
},
enabled: function(frm) {
var doc = frm.doc;
if(!doc.__islocal && has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
if(!frm.is_new() && has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
frm.toggle_display(['sb1', 'sb3', 'modules_access'], doc.enabled);
frm.set_df_property('enabled', 'read_only', 0);
}

if(frappe.session.user!=="Administrator") {
frm.toggle_enable('email', doc.__islocal);
frm.toggle_enable('email', frm.is_new());
}
},
create_user_email:function(frm) {


+ 34
- 4
frappe/core/doctype/user/user.json 查看文件

@@ -503,6 +503,37 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "role_profile_name",
"fieldtype": "Link",
"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": "Role Profile",
"length": 0,
"no_copy": 0,
"options": "Role Profile",
"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,
@@ -1213,7 +1244,6 @@
"label": "Background Image",
"length": 0,
"no_copy": 0,
"options": "image",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -1389,7 +1419,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields, go to \"Customize Form\".",
"description": "Enter default value fields (keys) and values. If you add multiple values for a field,the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields,go to \"Customize Form\".",
"fieldname": "defaults",
"fieldtype": "Table",
"hidden": 1,
@@ -1483,7 +1513,7 @@
"collapsible": 0,
"columns": 0,
"default": "System User",
"description": "If the user has any role checked, then the user becomes a \"System User\". \"System User\" has access to the desktop",
"description": "If the user has any role checked,then the user becomes a \"System User\". \"System User\" has access to the desktop",
"fieldname": "user_type",
"fieldtype": "Select",
"hidden": 0,
@@ -2002,7 +2032,7 @@
"istable": 0,
"max_attachments": 5,
"menu_index": 0,
"modified": "2017-10-09 15:33:43.818915",
"modified": "2017-10-17 11:06:05.570463",
"modified_by": "Administrator",
"module": "Core",
"name": "User",


+ 23
- 1
frappe/core/doctype/user/user.py 查看文件

@@ -67,6 +67,7 @@ class User(Document):
self.remove_disabled_roles()
self.validate_user_email_inbox()
ask_pass_update()
self.validate_roles()

if self.language == "Loading...":
self.language = None
@@ -74,6 +75,12 @@ class User(Document):
if (self.name not in ["Administrator", "Guest"]) and (not self.frappe_userid):
self.frappe_userid = frappe.generate_hash(length=39)

def validate_roles(self):
if self.role_profile_name:
role_profile = frappe.get_doc('Role Profile', self.role_profile_name)
self.set('roles', [])
self.append_roles(*[role.role for role in role_profile.roles])

def on_update(self):
# clear new password
self.validate_user_limit()
@@ -84,6 +91,7 @@ class User(Document):
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)


def has_website_permission(self, ptype, verbose=False):
"""Returns true if current user is the session user"""
return self.name == frappe.session.user
@@ -983,4 +991,18 @@ def throttle_user_creation():
if frappe.flags.in_import:
return
if frappe.db.get_creation_count('User', 60) > 60:
frappe.throw(_('Throttled'))
frappe.throw(_('Throttled'))

@frappe.whitelist()
def get_role_profile(role_profile):
roles = frappe.get_doc('Role Profile', {'role_profile': role_profile})
return roles.roles

def update_roles(role_profile):
users = frappe.get_all('User', filters={'role_profile_name': role_profile})
role_profile = frappe.get_doc('Role Profile', role_profile)
roles = [role.role for role in role_profile.roles]
for d in users:
user = frappe.get_doc('User', d)
user.set('roles', [])
user.add_roles(*roles)

+ 2
- 2
frappe/custom/doctype/custom_field/custom_field.json 查看文件

@@ -220,7 +220,7 @@
"no_copy": 0,
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"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",
"options": "Attach\nAttach Image\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\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,
@@ -1161,7 +1161,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-06 17:23:43.835189",
"modified": "2017-10-24 11:40:37.986457",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",


+ 23
- 0
frappe/custom/doctype/custom_field/test_custom_field.js 查看文件

@@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line

QUnit.test("test: Custom Field", function (assert) {
let done = assert.async();

// number of asserts
assert.expect(1);

frappe.run_serially([
// insert a new Custom Field
() => frappe.tests.make('Custom Field', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);

});

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

@@ -176,6 +176,7 @@ frappe.customize_form.confirm = function(msg, frm) {
frappe.msgprint(r.exc);
} else {
d.hide();
frappe.show_alert({message:__('Customizations Reset'), indicator:'green'});
frappe.customize_form.clear_locals_and_refresh(frm);
}
}


+ 4
- 7
frappe/custom/doctype/customize_form/customize_form.py 查看文件

@@ -66,9 +66,9 @@ docfield_properties = {

allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
('Text', 'Data'), ('Text', 'Text Editor', 'Code', 'Signature'), ('Data', 'Select'),
('Text', 'Small Text'), ('Text', 'Data', 'Barcode'))
('Text', 'Small Text'), ('Text', 'Data', 'Barcode'), ('Code', 'Geolocation'))

allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select',)
allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select', 'Data')

class CustomizeForm(Document):
def on_update(self):
@@ -163,16 +163,13 @@ class CustomizeForm(Document):
property_type=doctype_properties[property])

for df in self.get("fields"):
if df.get("__islocal"):
continue

meta_df = meta.get("fields", {"fieldname": df.fieldname})

if not meta_df or meta_df[0].get("is_custom_field"):
continue

for property in docfield_properties:
if property != "idx" and df.get(property) != meta_df[0].get(property):
if property != "idx" and (df.get(property) or '') != (meta_df[0].get(property) or ''):
if property == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property))

@@ -329,6 +326,6 @@ class CustomizeForm(Document):
return

frappe.db.sql("""delete from `tabProperty Setter` where doc_type=%s
and ifnull(field_name, '')!='naming_series'""", self.doc_type)
and !(`field_name`='naming_series' and `property`='options')""", self.doc_type)
frappe.clear_cache(doctype=self.doc_type)
self.fetch_to_customize()

+ 2
- 2
frappe/custom/doctype/customize_form_field/customize_form_field.json 查看文件

@@ -94,7 +94,7 @@
"no_copy": 0,
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\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",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\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-10-11 06:45:20.172291",
"modified": "2017-10-24 11:41:31.075929",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",


+ 35
- 23
frappe/database.py 查看文件

@@ -5,9 +5,6 @@
# --------------------

from __future__ import unicode_literals
import MySQLdb
from MySQLdb.times import DateTimeDeltaType
from markdown2 import UnicodeWithAttrs
import warnings
import datetime
import frappe
@@ -17,11 +14,25 @@ import re
import frappe.model.meta
from frappe.utils import now, get_datetime, cstr
from frappe import _
from six import text_type, binary_type, string_types, integer_types
from frappe.model.utils.link_count import flush_local_link_count
from six import iteritems, text_type
from frappe.utils.background_jobs import execute_job, get_queue

# imports - compatibility imports
from six import (
integer_types,
string_types,
binary_type,
text_type,
iteritems
)

# imports - third-party imports
from markdown2 import UnicodeWithAttrs
from pymysql.times import TimeDelta
from pymysql.constants import ER, FIELD_TYPE
from pymysql.converters import conversions
import pymysql

class Database:
"""
Open a database connection with the given parmeters, if use_default is True, use the
@@ -50,7 +61,7 @@ class Database:

def connect(self):
"""Connects to a database as set in `site_config.json`."""
warnings.filterwarnings('ignore', category=MySQLdb.Warning)
warnings.filterwarnings('ignore', category=pymysql.Warning)
usessl = 0
if frappe.conf.db_ssl_ca and frappe.conf.db_ssl_cert and frappe.conf.db_ssl_key:
usessl = 1
@@ -59,19 +70,23 @@ class Database:
'cert':frappe.conf.db_ssl_cert,
'key':frappe.conf.db_ssl_key
}

conversions.update({
FIELD_TYPE.NEWDECIMAL: float,
FIELD_TYPE.DATETIME: get_datetime,
TimeDelta: conversions[binary_type],
UnicodeWithAttrs: conversions[text_type]
})

if usessl:
self._conn = MySQLdb.connect(self.host, self.user or '', self.password or '',
use_unicode=True, charset='utf8mb4', ssl=self.ssl)
self._conn = pymysql.connect(self.host, self.user or '', self.password or '',
charset='utf8mb4', use_unicode = True, ssl=self.ssl, conv = conversions)
else:
self._conn = MySQLdb.connect(self.host, self.user or '', self.password or '',
use_unicode=True, charset='utf8mb4')
self._conn.converter[246]=float
self._conn.converter[12]=get_datetime
self._conn.encoders[UnicodeWithAttrs] = self._conn.encoders[text_type]
self._conn.encoders[DateTimeDeltaType] = self._conn.encoders[binary_type]
self._conn = pymysql.connect(self.host, self.user or '', self.password or '',
charset='utf8mb4', use_unicode = True, conv = conversions)

MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1
self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF)
# MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1
# # self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF)

self._cursor = self._conn.cursor()
if self.user != 'root':
@@ -142,7 +157,6 @@ class Database:
frappe.errprint(query % values)
except TypeError:
frappe.errprint([query, values])

if (frappe.conf.get("logging") or False)==2:
frappe.log("<<<< query")
frappe.log(query)
@@ -150,7 +164,6 @@ class Database:
frappe.log(values)
frappe.log(">>>>")
self._cursor.execute(query, values)

else:
if debug:
self.explain_query(query)
@@ -163,8 +176,8 @@ class Database:
self._cursor.execute(query)

except Exception as e:
# ignore data definition errors
if ignore_ddl and e.args[0] in (1146,1054,1091):
if ignore_ddl and e.args[0] in (ER.BAD_FIELD_ERROR, ER.NO_SUCH_TABLE,
ER.CANT_DROP_FIELD_OR_KEY):
pass

# NOTE: causes deadlock
@@ -175,7 +188,6 @@ class Database:
# as_dict=as_dict, as_list=as_list, formatted=formatted,
# debug=debug, ignore_ddl=ignore_ddl, as_utf8=as_utf8,
# auto_commit=auto_commit, update=update)

else:
raise

@@ -861,7 +873,7 @@ class Database:
def close(self):
"""Close database connection."""
if self._conn:
self._cursor.close()
# self._cursor.close()
self._conn.close()
self._cursor = None
self._conn = None
@@ -871,7 +883,7 @@ class Database:
if isinstance(s, text_type):
s = (s or "").encode("utf-8")

s = text_type(MySQLdb.escape_string(s), "utf-8").replace("`", "\\`")
s = text_type(pymysql.escape_string(s), "utf-8").replace("`", "\\`")

# NOTE separating % escape, because % escape should only be done when using LIKE operator
# or when you use python format string to generate query that already has a %s


+ 17
- 0
frappe/desk/calendar.py 查看文件

@@ -24,3 +24,20 @@ def get_event_conditions(doctype, filters=None):
frappe.throw(_("Not Permitted"), frappe.PermissionError)

return get_filters_cond(doctype, filters, [], with_match_conditions = True)

@frappe.whitelist()
def get_events(doctype, start, end, field_map, filters=None, fields=None):
field_map = frappe._dict(json.loads(field_map))

if filters:
filters = json.loads(filters or '')

if not fields:
fields = [field_map.start, field_map.end, field_map.title, 'name']

filters += [
[doctype, field_map.start, '<=', end],
[doctype, field_map.end, '>=', start],
]

return frappe.get_list(doctype, fields=fields, filters=filters)

+ 0
- 0
frappe/desk/doctype/calendar_view/__init__.py 查看文件


+ 35
- 0
frappe/desk/doctype/calendar_view/calendar_view.js 查看文件

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

frappe.ui.form.on('Calendar View', {
onload: function(frm) {
frm.trigger('reference_doctype');
},
refresh: function(frm) {
if (!frm.is_new()) {
frm.add_custom_button(__('Show Calendar'),
() => frappe.set_route('List', frm.doc.reference_doctype, 'Calendar', frm.doc.name));
}
},
reference_doctype: function(frm) {
const { reference_doctype } = frm.doc;
if (!reference_doctype) return;

frappe.model.with_doctype(reference_doctype, () => {
const meta = frappe.get_meta(reference_doctype);

const subject_options = meta.fields.filter(
df => !frappe.model.no_value_type.includes(df.fieldtype)
).map(df => df.fieldname);

const date_options = meta.fields.filter(
df => ['Date', 'Datetime'].includes(df.fieldtype)
).map(df => df.fieldname);

frm.set_df_property('subject_field', 'options', subject_options);
frm.set_df_property('start_date_field', 'options', date_options);
frm.set_df_property('end_date_field', 'options', date_options);
frm.refresh();
});
}
});

+ 184
- 0
frappe/desk/doctype/calendar_view/calendar_view.json 查看文件

@@ -0,0 +1,184 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "Prompt",
"beta": 0,
"creation": "2017-10-23 13:02:10.295824",
"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": "reference_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Reference DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "subject_field",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Subject Field",
"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": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "start_date_field",
"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": "Start Date Field",
"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": "end_date_field",
"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": "End Date Field",
"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": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-10-23 13:32:33.994308",
"modified_by": "Administrator",
"module": "Desk",
"name": "Calendar View",
"name_case": "",
"owner": "faris@erpnext.com",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
}

+ 9
- 0
frappe/desk/doctype/calendar_view/calendar_view.py 查看文件

@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt

from __future__ import unicode_literals
from frappe.model.document import Document

class CalendarView(Document):
pass

+ 3
- 2
frappe/desk/page/activity/activity.js 查看文件

@@ -180,8 +180,9 @@ frappe.activity.render_heatmap = function(page) {
method: "frappe.desk.page.activity.activity.get_heatmap_data",
callback: function(r) {
if(r.message) {
var heatmap = new frappe.ui.HeatMap({
parent: $(".heatmap"),
var heatmap = new Chart({
parent: ".heatmap",
type: 'heatmap',
height: 100,
start: new Date(moment().subtract(1, 'year').toDate()),
count_label: "actions",


+ 5
- 3
frappe/desk/query_builder.py 查看文件

@@ -10,6 +10,9 @@ from frappe.utils import cint
import frappe.defaults
from six import text_type

# imports - third-party imports
import pymysql

def get_sql_tables(q):
if q.find('WHERE') != -1:
tl = q.split('FROM')[1].split('WHERE')[0].split(',')
@@ -82,10 +85,9 @@ def guess_type(m):
"""
Returns fieldtype depending on the MySQLdb Description
"""
import MySQLdb
if m in MySQLdb.NUMBER:
if m in pymysql.NUMBER:
return 'Currency'
elif m in MySQLdb.DATE:
elif m in pymysql.DATE:
return 'Date'
else:
return 'Data'


+ 5
- 3
frappe/desk/reportview.py 查看文件

@@ -7,11 +7,13 @@ from __future__ import unicode_literals
import frappe, json
from six.moves import range
import frappe.permissions
import MySQLdb
from frappe.model.db_query import DatabaseQuery
from frappe import _
from six import text_type, string_types, StringIO

# imports - third-party imports
import pymysql

@frappe.whitelist()
def get():
args = get_form_params()
@@ -244,7 +246,7 @@ def get_stats(stats, doctype, filters=[]):

try:
columns = frappe.db.get_table_columns(doctype)
except MySQLdb.OperationalError:
except pymysql.InternalError:
# raised when _user_tags column is added on the fly
columns = []

@@ -266,7 +268,7 @@ def get_stats(stats, doctype, filters=[]):
except frappe.SQLError:
# does not work for child tables
pass
except MySQLdb.OperationalError:
except pymysql.InternalError:
# raised when _user_tags column is added on the fly
pass
return stats


+ 19
- 2
frappe/docs/user/en/tutorial/task-runner.md 查看文件

@@ -1,8 +1,8 @@
# Scheduled Tasks

Finally, an application also has to send email notifications and do other kind of scheduled tasks. In Frappé, if you have setup the bench, the task / scheduler is setup via Celery using Redis Queue.
Finally, an application also has to send email notifications and do other kind of scheduled tasks. In Frappé, if you have setup the bench, the task / scheduler is setup via RQ using Redis Queue.

To add a new task handler, go to `hooks.py` and add a new handler. Default handlers are `all`, `daily`, `weekly`, `monthly`. The `all` handler is called every 3 minutes by default.
To add a new task handler, go to `hooks.py` and add a new handler. Default handlers are `all`, `daily`, `weekly`, `monthly`, `cron`. The `all` handler is called every 4 minutes by default.

# Scheduled Tasks
# ---------------
@@ -11,6 +11,15 @@ To add a new task handler, go to `hooks.py` and add a new handler. Default handl
"daily": [
"library_management.tasks.daily"
],
"cron": {
"0/10 * * * *": [
"library_management.task.run_every_ten_mins"
],
"15 18 * * *": [
"library_management.task.every_day_at_18_15"
]
}
}

Here we can point to a Python function and that function will be executed every day. Let us look what this function looks like:
@@ -21,6 +30,14 @@ Here we can point to a Python function and that function will be executed every
from __future__ import unicode_literals
import frappe
from frappe.utils import datediff, nowdate, format_date, add_days
def every_ten_minutes():
# stuff to do every 10 minutes
pass
def every_day_at_18_15():
# stuff to do every day at 6:15pm
pass

def daily():
loan_period = frappe.db.get_value("Library Management Settings",


+ 8
- 3
frappe/email/doctype/email_alert/email_alert.py 查看文件

@@ -13,6 +13,10 @@ from frappe.modules.utils import export_module_json, get_doc_module
from markdown2 import markdown
from six import string_types

# imports - third-party imports
import pymysql
from pymysql.constants import ER

class EmailAlert(Document):
def onload(self):
'''load message'''
@@ -117,7 +121,8 @@ def get_context(context):
please enable Allow Print For {0} in Print Settings""".format(status)),
title=_("Error in Email Alert"))
else:
return [frappe.attach_print(doc.doctype, doc.name, None, self.print_format)]
return [{"print_format_attachment":1, "doctype":doc.doctype, "name": doc.name,
"print_format":self.print_format}]

context = get_context(doc)
recipients = []
@@ -237,8 +242,8 @@ def evaluate_alert(doc, alert, event):
if event=="Value Change" and not doc.is_new():
try:
db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed)
except frappe.DatabaseOperationalError as e:
if e.args[0]==1054:
except pymysql.InternalError as e:
if e.args[0]== ER.BAD_FIELD_ERROR:
alert.db_set('enabled', 0)
frappe.log_error('Email Alert {0} has been disabled due to missing field'.format(alert.name))
return


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

@@ -15,7 +15,7 @@ from email.header import Header

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,
content=None, reply_to=None, cc=[], bcc=[], email_account=None, expose_recipients=None,
inline_images=[], header=None):
""" Prepare an email with the following format:
- multipart/mixed
@@ -27,7 +27,7 @@ def get_email(recipients, sender='', msg='', subject='[No Subject]',
- attachment
"""
content = content or msg
emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, email_account=email_account, expose_recipients=expose_recipients)
emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, bcc=bcc, email_account=email_account, expose_recipients=expose_recipients)

if not content.strip().startswith("<"):
content = markdown(content)
@@ -51,7 +51,7 @@ class EMail:
Also provides a clean way to add binary `FileData` attachments
Also sets all messages as multipart/alternative for cleaner reading in text-only clients
"""
def __init__(self, sender='', recipients=(), subject='', alternative=0, reply_to=None, cc=(), email_account=None, expose_recipients=None):
def __init__(self, sender='', recipients=(), subject='', alternative=0, reply_to=None, cc=(), bcc=(), email_account=None, expose_recipients=None):
from email import charset as Charset
Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')

@@ -72,6 +72,7 @@ class EMail:
self.msg_alternative = MIMEMultipart('alternative')
self.msg_root.attach(self.msg_alternative)
self.cc = cc or []
self.bcc = bcc or []
self.html_set = False

self.email_account = email_account or get_outgoing_email_account(sender=sender)
@@ -176,8 +177,9 @@ class EMail:

self.recipients = [strip(r) for r in self.recipients]
self.cc = [strip(r) for r in self.cc]
self.bcc = [strip(r) for r in self.bcc]

for e in self.recipients + (self.cc or []):
for e in self.recipients + (self.cc or []) + (self.bcc or []):
validate_email_add(e, True)

def replace_sender(self):
@@ -207,6 +209,7 @@ class EMail:
"To": ', '.join(self.recipients) if self.expose_recipients=="header" else "<!--recipient-->",
"Date": email.utils.formatdate(),
"Reply-To": self.reply_to if self.reply_to else None,
"Bcc": ', '.join(self.bcc) if self.bcc else None,
"CC": ', '.join(self.cc) if self.cc and self.expose_recipients=="header" else None,
'X-Frappe-Site': get_url(),
}


+ 27
- 14
frappe/email/queue.py 查看文件

@@ -21,7 +21,7 @@ class EmailLimitCrossedError(frappe.ValidationError): pass

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,
attachments=None, reply_to=None, cc=[], bcc=[], 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,
header=None):
@@ -61,6 +61,9 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content=
if isinstance(cc, string_types):
cc = split_emails(cc)

if isinstance(bcc, string_types):
bcc = split_emails(bcc)

if isinstance(send_after, int):
send_after = add_days(nowdate(), send_after)

@@ -112,6 +115,7 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content=
attachments=attachments,
reply_to=reply_to,
cc=cc,
bcc=bcc,
message_id=message_id,
in_reply_to=in_reply_to,
send_after=send_after,
@@ -158,11 +162,13 @@ def get_email_queue(recipients, sender, subject, **kwargs):
e.priority = kwargs.get('send_priority')
attachments = kwargs.get('attachments')
if attachments:
# store attachments with fid, to be attached on-demand later
# store attachments with fid or print format details, to be attached on-demand later
_attachments = []
for att in attachments:
if att.get('fid'):
_attachments.append(att)
elif att.get("print_format_attachment") == 1:
_attachments.append(att)
e.attachments = json.dumps(_attachments)

try:
@@ -174,6 +180,7 @@ def get_email_queue(recipients, sender, subject, **kwargs):
attachments=kwargs.get('attachments'),
reply_to=kwargs.get('reply_to'),
cc=kwargs.get('cc'),
bcc=kwargs.get('bcc'),
email_account=kwargs.get('email_account'),
expose_recipients=kwargs.get('expose_recipients'),
inline_images=kwargs.get('inline_images'),
@@ -194,7 +201,7 @@ def get_email_queue(recipients, sender, subject, **kwargs):
frappe.log_error('Invalid Email ID Sender: {0}, Recipients: {1}'.format(mail.sender,
', '.join(mail.recipients)), 'Email Not Sent')

e.set_recipients(recipients + kwargs.get('cc', []))
e.set_recipients(recipients + kwargs.get('cc', []) + kwargs.get('bcc', []))
e.reference_doctype = kwargs.get('reference_doctype')
e.reference_name = kwargs.get('reference_name')
e.add_unsubscribe_link = kwargs.get("add_unsubscribe_link")
@@ -204,6 +211,7 @@ def get_email_queue(recipients, sender, subject, **kwargs):
e.communication = kwargs.get('communication')
e.send_after = kwargs.get('send_after')
e.show_as_cc = ",".join(kwargs.get('cc', []))
e.show_as_bcc = ",".join(kwargs.get('bcc', []))
e.insert(ignore_permissions=True)

return e
@@ -511,17 +519,22 @@ def prepare_message(email, recipient, recipients_list):
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
})
attachment.pop("fid", None)
add_attachment(**attachment)
fid = attachment.get("fid")
if fid:
fname, fcontent = get_file(fid)
attachment.update({
'fname': fname,
'fcontent': fcontent,
'parent': msg_obj
})
attachment.pop("fid", None)
add_attachment(**attachment)

elif attachment.get("print_format_attachment") == 1:
attachment.pop("print_format_attachment", None)
print_format_file = frappe.attach_print(**attachment)
print_format_file.update({"parent": msg_obj})
add_attachment(**print_format_file)

return msg_obj.as_string()



+ 3
- 5
frappe/exceptions.py 查看文件

@@ -4,11 +4,11 @@
from __future__ import unicode_literals

# BEWARE don't put anything in this file except exceptions

from werkzeug.exceptions import NotFound
from MySQLdb import ProgrammingError as SQLError, Error
from MySQLdb import OperationalError as DatabaseOperationalError

# imports - third-party imports
from pymysql import ProgrammingError as SQLError, Error
# from pymysql import OperationalError as DatabaseOperationalError

class ValidationError(Exception):
http_status_code = 417
@@ -46,7 +46,6 @@ class Redirect(Exception):
class CSRFTokenError(Exception):
http_status_code = 400


class ImproperDBConfigurationError(Error):
"""
Used when frappe detects that database or tables are not properly
@@ -58,7 +57,6 @@ class ImproperDBConfigurationError(Error):
super(ImproperDBConfigurationError, self).__init__(msg)
self.reason = reason


class DuplicateEntryError(NameError):pass
class DataError(ValidationError): pass
class UnknownDomainError(Exception): pass


+ 23
- 15
frappe/integrations/doctype/dropbox_settings/dropbox_settings.py 查看文件

@@ -13,7 +13,7 @@ from frappe.utils.background_jobs import enqueue
from six.moves.urllib.parse import urlparse, parse_qs
from frappe.integrations.utils import make_post_request
from frappe.utils import (cint, split_emails, get_request_site_address, cstr,
get_files_path, get_backups_path, encode, get_url)
get_files_path, get_backups_path, get_url, encode)

ignore_list = [".DS_Store"]

@@ -152,19 +152,27 @@ def upload_file_to_dropbox(filename, folder, dropbox_client):
f = open(encode(filename), 'rb')
path = "{0}/{1}".format(folder, os.path.basename(filename))

if file_size <= chunk_size:
dropbox_client.files_upload(f.read(), path, mode)
else:
upload_session_start_result = dropbox_client.files_upload_session_start(f.read(chunk_size))
cursor = dropbox.files.UploadSessionCursor(session_id=upload_session_start_result.session_id, offset=f.tell())
commit = dropbox.files.CommitInfo(path=path, mode=mode)

while f.tell() < file_size:
if ((file_size - f.tell()) <= chunk_size):
dropbox_client.files_upload_session_finish(f.read(chunk_size), cursor, commit)
else:
dropbox_client.files_upload_session_append(f.read(chunk_size), cursor.session_id,cursor.offset)
cursor.offset = f.tell()
try:
if file_size <= chunk_size:
dropbox_client.files_upload(f.read(), path, mode)
else:
upload_session_start_result = dropbox_client.files_upload_session_start(f.read(chunk_size))
cursor = dropbox.files.UploadSessionCursor(session_id=upload_session_start_result.session_id, offset=f.tell())
commit = dropbox.files.CommitInfo(path=path, mode=mode)

while f.tell() < file_size:
if ((file_size - f.tell()) <= chunk_size):
dropbox_client.files_upload_session_finish(f.read(chunk_size), cursor, commit)
else:
dropbox_client.files_upload_session_append(f.read(chunk_size), cursor.session_id,cursor.offset)
cursor.offset = f.tell()
except dropbox.exceptions.ApiError as e:
if isinstance(e.error, dropbox.files.UploadError):
error = "File Path: {path}\n".foramt(path=path)
error += frappe.get_traceback()
frappe.log_error(error)
else:
raise

def create_folder_if_not_exists(folder, dropbox_client):
try:
@@ -210,7 +218,7 @@ def get_redirect_url():
if response.get("message"):
return response["message"]

except Exception as e:
except Exception:
frappe.log_error()
frappe.throw(
_("Something went wrong while generating dropbox access token. Please check error log for more details.")


+ 2
- 2
frappe/integrations/doctype/gsuite_settings/gsuite_settings.json 查看文件

@@ -250,7 +250,7 @@
"label": "Script Code",
"length": 0,
"no_copy": 0,
"options": "<code>// ERPNEXT GSuite integration\n//\n\nfunction doGet(e){\n return ContentService.createTextOutput('ok');\n}\n\nfunction doPost(e) {\n var p = JSON.parse(e.postData.contents);\n\n switch(p.exec){\n case 'new':\n var url = createDoc(p);\n result = { 'url': url };\n break;\n case 'test':\n result = { 'test':'ping' , 'version':'1.0'}\n }\n return ContentService.createTextOutput(JSON.stringify(result)).setMimeType(ContentService.MimeType.JSON);\n}\n\nfunction replaceVars(body,p){\n for (key in p) {\n if (p.hasOwnProperty(key)) {\n if (p[key] != null) {\n body.replaceText('{{'+key+'}}', p[key]);\n }\n }\n } \n}\n\nfunction createDoc(p) {\n if(p.destination){\n var folder = DriveApp.getFolderById(p.destination);\n } else {\n var folder = DriveApp.getRootFolder();\n }\n var template = DriveApp.getFileById( p.template )\n var newfile = template.makeCopy( p.filename , folder );\n\n switch(newfile.getMimeType()){\n case MimeType.GOOGLE_DOCS:\n var body = DocumentApp.openById(newfile.getId()).getBody();\n replaceVars(body,p.vars);\n break;\n case MimeType.GOOGLE_SHEETS:\n //TBD\n case MimeType.GOOGLE_SLIDES:\n //TBD\n }\n return newfile.getUrl()\n}\n\n</code>",
"options": "<pre>// ERPNEXT GSuite integration\n//\n\nfunction doGet(e){\n return ContentService.createTextOutput('ok');\n}\n\nfunction doPost(e) {\n var p = JSON.parse(e.postData.contents);\n\n switch(p.exec){\n case 'new':\n var url = createDoc(p);\n result = { 'url': url };\n break;\n case 'test':\n result = { 'test':'ping' , 'version':'1.0'}\n }\n return ContentService.createTextOutput(JSON.stringify(result)).setMimeType(ContentService.MimeType.JSON);\n}\n\nfunction replaceVars(body,p){\n for (key in p) {\n if (p.hasOwnProperty(key)) {\n if (p[key] != null) {\n body.replaceText('{{'+key+'}}', p[key]);\n }\n }\n } \n}\n\nfunction createDoc(p) {\n if(p.destination){\n var folder = DriveApp.getFolderById(p.destination);\n } else {\n var folder = DriveApp.getRootFolder();\n }\n var template = DriveApp.getFileById( p.template )\n var newfile = template.makeCopy( p.filename , folder );\n\n switch(newfile.getMimeType()){\n case MimeType.GOOGLE_DOCS:\n var body = DocumentApp.openById(newfile.getId()).getBody();\n replaceVars(body,p.vars);\n break;\n case MimeType.GOOGLE_SHEETS:\n //TBD\n case MimeType.GOOGLE_SLIDES:\n //TBD\n }\n return newfile.getUrl()\n}\n\n</pre>",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -365,7 +365,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-05-19 15:28:44.663715",
"modified": "2017-10-20 16:11:47.757030",
"modified_by": "Administrator",
"module": "Integrations",
"name": "GSuite Settings",


+ 6
- 3
frappe/integrations/doctype/paypal_settings/paypal_settings.py 查看文件

@@ -227,11 +227,14 @@ def confirm_payment(token):
}, "Completed")

if data.get("reference_doctype") and data.get("reference_docname"):
redirect_url = frappe.get_doc(data.get("reference_doctype"), data.get("reference_docname")).run_method("on_payment_authorized", "Completed")
custom_redirect_to = frappe.get_doc(data.get("reference_doctype"),
data.get("reference_docname")).run_method("on_payment_authorized", "Completed")
frappe.db.commit()

if not redirect_url:
redirect_url = '/integrations/payment-success'
if custom_redirect_to:
redirect_to = custom_redirect_to

redirect_url = '/integrations/payment-success'
else:
redirect_url = "/integrations/payment-failed"



+ 4
- 4
frappe/integrations/doctype/razorpay_settings/razorpay_settings.py 查看文件

@@ -108,10 +108,7 @@ class RazorpaySettings(Document):
until it is explicitly captured by merchant.
"""
data = json.loads(self.integration_request.data)

settings = self.get_settings(data)
redirect_to = data.get('notes', {}).get('redirect_to') or None
redirect_message = data.get('notes', {}).get('redirect_message') or None

try:
resp = make_get_request("https://api.razorpay.com/v1/payments/{0}"
@@ -119,7 +116,7 @@ class RazorpaySettings(Document):
settings.api_secret))

if resp.get("status") == "authorized":
self.integration_request.db_set('status', 'Authorized', update_modified=False)
self.integration_request.update_status(data, 'Authorized')
self.flags.status_changed_to = "Authorized"

else:
@@ -132,6 +129,9 @@ class RazorpaySettings(Document):

status = frappe.flags.integration_request.status_code

redirect_to = data.get('notes', {}).get('redirect_to') or None
redirect_message = data.get('notes', {}).get('redirect_message') or None

if self.flags.status_changed_to == "Authorized":
if self.data.reference_doctype and self.data.reference_docname:
custom_redirect_to = None


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

@@ -316,7 +316,6 @@ class BaseDocument(object):
raise
else:
raise

self.set("__islocal", False)

def db_update(self):


+ 67
- 9
frappe/model/db_query.py 查看文件

@@ -308,15 +308,7 @@ class DatabaseQuery(object):
if f.operator.lower() == 'between' and \
(f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):

from_date = None
to_date = None
if f.value and isinstance(f.value, (list, tuple)):
if len(f.value) >= 1: from_date = f.value[0]
if len(f.value) >= 2: to_date = f.value[1]

value = "'%s' AND '%s'" % (
add_to_date(get_datetime(from_date),days=-1).strftime("%Y-%m-%d %H:%M:%S.%f"),
get_datetime(to_date).strftime("%Y-%m-%d %H:%M:%S.%f"))
value = get_between_date_filter(f.value)
fallback = "'0000-00-00 00:00:00'"

elif df and df.fieldtype=="Date":
@@ -571,3 +563,69 @@ def get_order_by(doctype, meta):
order_by = "`tab{0}`.docstatus asc, {1}".format(doctype, order_by)

return order_by


@frappe.whitelist()
def get_list(doctype, *args, **kwargs):
'''wrapper for DatabaseQuery'''
kwargs.pop('cmd', None)
return DatabaseQuery(doctype).execute(None, *args, **kwargs)

@frappe.whitelist()
def get_count(doctype, filters=None):
if filters:
filters = json.loads(filters)

if is_parent_only_filter(doctype, filters):
if isinstance(filters, list):
filters = frappe.utils.make_filter_dict(filters)

return frappe.db.count(doctype, filters=filters)

else:
# If filters contain child table as well as parent doctype - Join
tables, conditions = ['`tab{0}`'.format(doctype)], []
for f in filters:
fieldname = '`tab{0}`.{1}'.format(f[0], f[1])
table = '`tab{0}`'.format(f[0])

if table not in tables:
tables.append(table)

conditions.append('{fieldname} {operator} "{value}"'.format(fieldname=fieldname,
operator=f[2], value=f[3]))

if doctype != f[0]:
join_condition = '`tab{child_doctype}`.parent =`tab{doctype}`.name'.format(child_doctype=f[0], doctype=doctype)
if join_condition not in conditions:
conditions.append(join_condition)

return frappe.db.sql_list("""select count(*) from {0}
where {1}""".format(','.join(tables), ' and '.join(conditions)), debug=0)

def is_parent_only_filter(doctype, filters):
#check if filters contains only parent doctype
only_parent_doctype = True

if isinstance(filters, list):
for flt in filters:
if doctype not in flt:
only_parent_doctype = False
if 'Between' in flt:
flt[3] = get_between_date_filter(flt[3])

return only_parent_doctype

def get_between_date_filter(value):
from_date = None
to_date = None

if value and isinstance(value, (list, tuple)):
if len(value) >= 1: from_date = value[0]
if len(value) >= 2: to_date = value[1]

data = "'%s' AND '%s'" % (
add_to_date(get_datetime(from_date),days=-1).strftime("%Y-%m-%d %H:%M:%S.%f"),
get_datetime(to_date).strftime("%Y-%m-%d %H:%M:%S.%f"))

return data

+ 16
- 12
frappe/model/db_schema.py 查看文件

@@ -13,7 +13,10 @@ import os
import frappe
from frappe import _
from frappe.utils import cstr, cint, flt
import MySQLdb

# imports - third-party imports
import pymysql
from pymysql.constants import ER

class InvalidColumnName(frappe.ValidationError): pass

@@ -26,25 +29,26 @@ type_map = {
,'Float': ('decimal', '18,6')
,'Percent': ('decimal', '18,6')
,'Check': ('int', '1')
,'Small Text': ('text', '')
,'Long Text': ('longtext', '')
,'Small Text': ('text', '')
,'Long Text': ('longtext', '')
,'Code': ('longtext', '')
,'Text Editor': ('longtext', '')
,'Text Editor': ('longtext', '')
,'Date': ('date', '')
,'Datetime': ('datetime', '6')
,'Datetime': ('datetime', '6')
,'Time': ('time', '6')
,'Text': ('text', '')
,'Data': ('varchar', varchar_len)
,'Link': ('varchar', varchar_len)
,'Dynamic Link':('varchar', varchar_len)
,'Password': ('varchar', varchar_len)
,'Dynamic Link': ('varchar', varchar_len)
,'Password': ('varchar', varchar_len)
,'Select': ('varchar', varchar_len)
,'Read Only': ('varchar', varchar_len)
,'Read Only': ('varchar', varchar_len)
,'Attach': ('text', '')
,'Attach Image':('text', '')
,'Signature': ('longtext', '')
,'Attach Image': ('text', '')
,'Signature': ('longtext', '')
,'Color': ('varchar', varchar_len)
,'Barcode': ('longtext', '')
,'Geolocation': ('longtext', '')
}

default_columns = ['name', 'creation', 'modified', 'modified_by', 'owner',
@@ -120,8 +124,8 @@ class DbTable:
max_length = frappe.db.sql("""select max(char_length(`{fieldname}`)) from `tab{doctype}`"""\
.format(fieldname=col.fieldname, doctype=self.doctype))

except MySQLdb.OperationalError as e:
if e.args[0]==1054:
except pymysql.InternalError as e:
if e.args[0] == ER.BAD_FIELD_ERROR:
# Unknown column 'column_name' in 'field list'
continue



+ 3
- 1
frappe/modules/import_file.py 查看文件

@@ -95,10 +95,10 @@ ignore_doctypes = [""]

def import_doc(docdict, force=False, data_import=False, pre_process=None,
ignore_version=None, reset_permissions=False):

frappe.flags.in_import = True
docdict["__islocal"] = 1
doc = frappe.get_doc(docdict)

doc.flags.ignore_version = ignore_version
if pre_process:
pre_process(doc)
@@ -128,5 +128,7 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None,
doc.flags.ignore_validate = True
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.insert()

frappe.flags.in_import = False

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

@@ -23,7 +23,7 @@ frappe.patches.v8_0.rename_page_role_to_has_role #2017-03-16
frappe.patches.v7_2.setup_custom_perms #2017-01-19
frappe.patches.v8_0.set_user_permission_for_page_and_report #2017-03-20
execute:frappe.reload_doc('core', 'doctype', 'role') #2017-05-23
execute:frappe.reload_doc('core', 'doctype', 'user')
execute:frappe.reload_doc('core', 'doctype', 'user') #2017-10-27
execute:frappe.reload_doc('custom', 'doctype', 'custom_field') #2015-10-19
execute:frappe.reload_doc('core', 'doctype', 'page') #2013-13-26
execute:frappe.reload_doc('core', 'doctype', 'report') #2014-06-03


+ 16
- 6
frappe/public/build.json 查看文件

@@ -53,7 +53,9 @@
"public/js/frappe/form/controls/html.js",
"public/js/frappe/form/controls/heading.js",
"public/js/frappe/form/controls/autocomplete.js",
"public/js/frappe/form/controls/barcode.js"
"public/js/frappe/form/controls/barcode.js",
"public/js/frappe/form/controls/geolocation.js",
"public/js/frappe/form/controls/multiselect.js"
],
"js/dialog.min.js": [
"public/js/frappe/dom.js",
@@ -94,12 +96,17 @@
"public/js/frappe/form/controls/read_only.js",
"public/js/frappe/form/controls/button.js",
"public/js/frappe/form/controls/html.js",
"public/js/frappe/form/controls/heading.js"
"public/js/frappe/form/controls/heading.js",
"public/js/frappe/form/controls/geolocation.js"
],
"css/desk.min.css": [
"public/js/lib/datepicker/datepicker.min.css",
"public/js/lib/awesomplete/awesomplete.css",
"public/js/lib/summernote/summernote.css",
"public/js/lib/leaflet/leaflet.css",
"public/js/lib/leaflet/leaflet.draw.css",
"public/js/lib/leaflet/L.Control.Locate.css",
"public/js/lib/leaflet/easy-button.css",
"public/css/bootstrap.css",
"public/css/font-awesome.css",
"public/css/octicons/octicons.css",
@@ -113,8 +120,7 @@
"public/css/desktop.css",
"public/css/form.css",
"public/css/mobile.css",
"public/css/kanban.css",
"public/css/charts.css"
"public/css/kanban.css"
],
"css/frappe-rtl.css": [
"public/css/bootstrap-rtl.css",
@@ -136,8 +142,13 @@
"public/js/frappe/translate.js",
"public/js/lib/datepicker/datepicker.min.js",
"public/js/lib/datepicker/locale-all.js",
"public/js/lib/frappe-charts/frappe-charts.min.js",
"public/js/lib/jquery.jrumble.min.js",
"public/js/lib/webcam.min.js"
"public/js/lib/webcam.min.js",
"public/js/lib/leaflet/leaflet.js",
"public/js/lib/leaflet/leaflet.draw.js",
"public/js/lib/leaflet/L.Control.Locate.js",
"public/js/lib/leaflet/easy-button.js"
],
"js/desk.min.js": [
"public/js/frappe/class.js",
@@ -230,7 +241,6 @@
"public/js/frappe/desk.js",
"public/js/frappe/query_string.js",

"public/js/frappe/ui/charts.js",
"public/js/frappe/ui/comment.js",
"public/js/frappe/misc/rating_icons.html",



+ 0
- 284
frappe/public/css/charts.css 查看文件

@@ -1,284 +0,0 @@
/* charts */
.chart-container .graph-focus-margin {
margin: 0px 5%;
}
.chart-container .graphics {
margin-top: 10px;
padding-top: 10px;
padding-bottom: 10px;
position: relative;
}
.chart-container .graph-stats-group {
display: flex;
justify-content: space-around;
flex: 1;
}
.chart-container .graph-stats-container {
display: flex;
justify-content: space-around;
padding-top: 10px;
}
.chart-container .graph-stats-container .stats {
padding-bottom: 15px;
}
.chart-container .graph-stats-container .stats-title {
color: #8D99A6;
}
.chart-container .graph-stats-container .stats-value {
font-size: 20px;
font-weight: 300;
}
.chart-container .graph-stats-container .stats-description {
font-size: 12px;
color: #8D99A6;
}
.chart-container .graph-stats-container .graph-data .stats-value {
color: #98d85b;
}
.chart-container .axis,
.chart-container .chart-label {
font-size: 10px;
fill: #555b51;
}
.chart-container .axis line,
.chart-container .chart-label line {
stroke: rgba(27, 31, 35, 0.2);
}
.chart-container .percentage-graph .progress {
margin-bottom: 0px;
}
.chart-container .data-points circle {
stroke: #fff;
stroke-width: 2;
}
.chart-container .data-points path {
fill: none;
stroke-opacity: 1;
stroke-width: 2px;
}
.chart-container line.dashed {
stroke-dasharray: 5,3;
}
.chart-container .tick.x-axis-label {
display: block;
}
.chart-container .tick .specific-value {
text-anchor: start;
}
.chart-container .tick .y-value-text {
text-anchor: end;
}
.chart-container .tick .x-value-text {
text-anchor: middle;
}
.graph-svg-tip {
position: absolute;
z-index: 99999;
padding: 10px;
font-size: 12px;
color: #959da5;
text-align: center;
background: rgba(0, 0, 0, 0.8);
border-radius: 3px;
}
.graph-svg-tip.comparison {
padding: 0;
text-align: left;
pointer-events: none;
}
.graph-svg-tip.comparison .title {
display: block;
padding: 10px;
margin: 0;
font-weight: 600;
line-height: 1;
pointer-events: none;
}
.graph-svg-tip.comparison ul {
margin: 0;
white-space: nowrap;
list-style: none;
}
.graph-svg-tip.comparison li {
display: inline-block;
padding: 5px 10px;
}
.graph-svg-tip ul,
.graph-svg-tip ol {
padding-left: 0;
display: flex;
}
.graph-svg-tip ul.data-point-list li {
min-width: 90px;
flex: 1;
}
.graph-svg-tip strong {
color: #dfe2e5;
}
.graph-svg-tip .svg-pointer {
position: absolute;
bottom: -10px;
left: 50%;
width: 5px;
height: 5px;
margin: 0 0 0 -5px;
content: " ";
border: 5px solid transparent;
border-top-color: rgba(0, 0, 0, 0.8);
}
.stroke.grey {
stroke: #F0F4F7;
}
.stroke.blue {
stroke: #5e64ff;
}
.stroke.red {
stroke: #ff5858;
}
.stroke.light-green {
stroke: #98d85b;
}
.stroke.lightgreen {
stroke: #98d85b;
}
.stroke.green {
stroke: #28a745;
}
.stroke.orange {
stroke: #ffa00a;
}
.stroke.purple {
stroke: #743ee2;
}
.stroke.darkgrey {
stroke: #b8c2cc;
}
.stroke.black {
stroke: #36414C;
}
.stroke.yellow {
stroke: #FEEF72;
}
.stroke.light-blue {
stroke: #7CD6FD;
}
.stroke.lightblue {
stroke: #7CD6FD;
}
.fill.grey {
fill: #F0F4F7;
}
.fill.blue {
fill: #5e64ff;
}
.fill.red {
fill: #ff5858;
}
.fill.light-green {
fill: #98d85b;
}
.fill.lightgreen {
fill: #98d85b;
}
.fill.green {
fill: #28a745;
}
.fill.orange {
fill: #ffa00a;
}
.fill.purple {
fill: #743ee2;
}
.fill.darkgrey {
fill: #b8c2cc;
}
.fill.black {
fill: #36414C;
}
.fill.yellow {
fill: #FEEF72;
}
.fill.light-blue {
fill: #7CD6FD;
}
.fill.lightblue {
fill: #7CD6FD;
}
.background.grey {
background: #F0F4F7;
}
.background.blue {
background: #5e64ff;
}
.background.red {
background: #ff5858;
}
.background.light-green {
background: #98d85b;
}
.background.lightgreen {
background: #98d85b;
}
.background.green {
background: #28a745;
}
.background.orange {
background: #ffa00a;
}
.background.purple {
background: #743ee2;
}
.background.darkgrey {
background: #b8c2cc;
}
.background.black {
background: #36414C;
}
.background.yellow {
background: #FEEF72;
}
.background.light-blue {
background: #7CD6FD;
}
.background.lightblue {
background: #7CD6FD;
}
.border-top.grey {
border-top: 3px solid #F0F4F7;
}
.border-top.blue {
border-top: 3px solid #5e64ff;
}
.border-top.red {
border-top: 3px solid #ff5858;
}
.border-top.light-green {
border-top: 3px solid #98d85b;
}
.border-top.lightgreen {
border-top: 3px solid #98d85b;
}
.border-top.green {
border-top: 3px solid #28a745;
}
.border-top.orange {
border-top: 3px solid #ffa00a;
}
.border-top.purple {
border-top: 3px solid #743ee2;
}
.border-top.darkgrey {
border-top: 3px solid #b8c2cc;
}
.border-top.black {
border-top: 3px solid #36414C;
}
.border-top.yellow {
border-top: 3px solid #FEEF72;
}
.border-top.light-blue {
border-top: 3px solid #7CD6FD;
}
.border-top.lightblue {
border-top: 3px solid #7CD6FD;
}

+ 19
- 0
frappe/public/css/list.css 查看文件

@@ -34,11 +34,14 @@
}
.set-filters .btn-group {
margin-right: 10px;
white-space: nowrap;
font-size: 0;
}
.set-filters .btn-group .btn-default {
background-color: transparent;
border: 1px solid #d1d8dd;
color: #8D99A6;
float: none;
}
.filter-box {
border-bottom: 1px solid #d1d8dd;
@@ -405,6 +408,22 @@
.pswp__bg {
background-color: #fff !important;
}
.pswp__more-items {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
}
.pswp__more-item {
display: inline-block;
margin: 5px;
height: 100px;
cursor: pointer;
border: 1px solid #d1d8dd;
}
.pswp__more-item img {
max-height: 100%;
}
.gantt .details-container .heading {
margin-bottom: 10px;
font-size: 12px;


二進制
frappe/public/images/leaflet/layers-2x.png 查看文件

Before After
Width: 52  |  Height: 52  |  Size: 1.2 KiB

二進制
frappe/public/images/leaflet/layers.png 查看文件

Before After
Width: 26  |  Height: 26  |  Size: 696 B

二進制
frappe/public/images/leaflet/leafletmarker-icon.png 查看文件

Before After
Width: 25  |  Height: 41  |  Size: 1.4 KiB

二進制
frappe/public/images/leaflet/leafletmarker-shadow.png 查看文件

Before After
Width: 41  |  Height: 41  |  Size: 618 B

二進制
frappe/public/images/leaflet/lego.png 查看文件

Before After
Width: 477  |  Height: 472  |  Size: 221 KiB

二進制
frappe/public/images/leaflet/marker-icon-2x.png 查看文件

Before After
Width: 50  |  Height: 82  |  Size: 2.5 KiB

二進制
frappe/public/images/leaflet/marker-icon.png 查看文件

Before After
Width: 25  |  Height: 41  |  Size: 1.4 KiB

二進制
frappe/public/images/leaflet/marker-shadow.png 查看文件

Before After
Width: 41  |  Height: 41  |  Size: 618 B

二進制
frappe/public/images/leaflet/spritesheet-2x.png 查看文件

Before After
Width: 600  |  Height: 60  |  Size: 3.5 KiB

二進制
frappe/public/images/leaflet/spritesheet.png 查看文件

Before After
Width: 300  |  Height: 30  |  Size: 1.9 KiB

+ 156
- 0
frappe/public/images/leaflet/spritesheet.svg 查看文件

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 600 60"
height="60"
width="600"
id="svg4225"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="spritesheet.svg"
inkscape:export-filename="/home/fpuga/development/upstream/icarto.Leaflet.draw/src/images/spritesheet-2x.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<metadata
id="metadata4258">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4256" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1056"
id="namedview4254"
showgrid="false"
inkscape:zoom="1.3101852"
inkscape:cx="237.56928"
inkscape:cy="7.2419621"
inkscape:window-x="1920"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="svg4225" />
<g
id="enabled"
style="fill:#464646;fill-opacity:1">
<g
id="polyline"
style="fill:#464646;fill-opacity:1">
<path
d="m 18,36 0,6 6,0 0,-6 -6,0 z m 4,4 -2,0 0,-2 2,0 0,2 z"
id="path4229"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 36,18 0,6 6,0 0,-6 -6,0 z m 4,4 -2,0 0,-2 2,0 0,2 z"
id="path4231"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 23.142,39.145 -2.285,-2.29 16,-15.998 2.285,2.285 z"
id="path4233"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
</g>
<path
id="polygon"
d="M 100,24.565 97.904,39.395 83.07,42 76,28.773 86.463,18 Z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
id="rectangle"
d="m 140,20 20,0 0,20 -20,0 z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
id="circle"
d="m 221,30 c 0,6.078 -4.926,11 -11,11 -6.074,0 -11,-4.922 -11,-11 0,-6.074 4.926,-11 11,-11 6.074,0 11,4.926 11,11 z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
id="marker"
d="m 270,19 c -4.971,0 -9,4.029 -9,9 0,4.971 5.001,12 9,14 4.001,-2 9,-9.029 9,-14 0,-4.971 -4.029,-9 -9,-9 z m 0,12.5 c -2.484,0 -4.5,-2.014 -4.5,-4.5 0,-2.484 2.016,-4.5 4.5,-4.5 2.485,0 4.5,2.016 4.5,4.5 0,2.486 -2.015,4.5 -4.5,4.5 z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<g
id="edit"
style="fill:#464646;fill-opacity:1">
<path
d="m 337,30.156 0,0.407 0,5.604 c 0,1.658 -1.344,3 -3,3 l -10,0 c -1.655,0 -3,-1.342 -3,-3 l 0,-10 c 0,-1.657 1.345,-3 3,-3 l 6.345,0 3.19,-3.17 -9.535,0 c -3.313,0 -6,2.687 -6,6 l 0,10 c 0,3.313 2.687,6 6,6 l 10,0 c 3.314,0 6,-2.687 6,-6 l 0,-8.809 -3,2.968"
id="path4240"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 338.72,24.637 -8.892,8.892 -2.828,0 0,-2.829 8.89,-8.89 z"
id="path4242"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 338.697,17.826 4,0 0,4 -4,0 z"
transform="matrix(-0.70698336,-0.70723018,0.70723018,-0.70698336,567.55917,274.78273)"
id="path4244"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
</g>
<g
id="remove"
style="fill:#464646;fill-opacity:1">
<path
d="m 381,42 18,0 0,-18 -18,0 0,18 z m 14,-16 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z"
id="path4247"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 395,20 0,-4 -10,0 0,4 -6,0 0,2 22,0 0,-2 -6,0 z m -2,0 -6,0 0,-2 6,0 0,2 z"
id="path4249"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
</g>
</g>
<g
id="disabled"
transform="translate(120,0)"
style="fill:#bbbbbb">
<use
xlink:href="#edit"
id="edit-disabled"
x="0"
y="0"
width="100%"
height="100%" />
<use
xlink:href="#remove"
id="remove-disabled"
x="0"
y="0"
width="100%"
height="100%" />
</g>
<path
style="fill:none;stroke:#464646;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle-3"
d="m 581.65725,30 c 0,6.078 -4.926,11 -11,11 -6.074,0 -11,-4.922 -11,-11 0,-6.074 4.926,-11 11,-11 6.074,0 11,4.926 11,11 z"
inkscape:connector-curvature="0" />
</svg>

+ 21
- 0
frappe/public/js/frappe/db.js 查看文件

@@ -2,6 +2,27 @@
// MIT License. See license.txt

frappe.db = {
get_list: function(doctype, args) {
if (!args) {
args = {};
}
args.doctype = doctype;
if (!args.fields) {
args.fields = ['name'];
}
if (!args.limit) {
args.limit = 20;
}
return new Promise ((resolve) => {
frappe.call({
method: 'frappe.model.db_query.get_list',
args: args,
callback: function(r) {
resolve(r.message);
}
});
});
},
exists: function(doctype, name) {
return new Promise ((resolve) => {
frappe.db.get_value(doctype, {name: name}, 'name').then((r) => {


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

@@ -10,6 +10,11 @@ frappe.dom = {
by_id: function(id) {
return document.getElementById(id);
},
get_unique_id: function() {
const id = 'unique-' + frappe.dom.id_count;
frappe.dom.id_count++;
return id;
},
set_unique_id: function(ele) {
var $ele = $(ele);
if($ele.attr('id')) {


+ 20
- 18
frappe/public/js/frappe/form/controls/autocomplete.js 查看文件

@@ -2,30 +2,32 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
make_input() {
this._super();
this.setup_awesomplete();
this.set_options();
},

setup_awesomplete() {
var me = this;
set_options() {
if (this.df.options) {
let options = this.df.options || [];
if(typeof options === 'string') {
options = options.split('\n');
}
this._data = options;
}
},

this.awesomplete = new Awesomplete(this.input, {
get_awesomplete_settings() {
return {
minChars: 0,
maxItems: 99,
autoFirst: true,
list: this.get_data(),
data: function (item) {
if (typeof item === 'string') {
item = {
label: item,
value: item
};
}

return {
label: item.label || item.value,
value: item.value
};
}
});
list: this.get_data()
};
},

setup_awesomplete() {
var me = this;

this.awesomplete = new Awesomplete(this.input, this.get_awesomplete_settings());

$(this.input_area).find('.awesomplete ul').css('min-width', '100%');



+ 184
- 0
frappe/public/js/frappe/form/controls/geolocation.js 查看文件

@@ -0,0 +1,184 @@
frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlCode.extend({
make_wrapper() {
// Create the elements for map area
this._super();

let $input_wrapper = this.$wrapper.find('.control-input-wrapper');
this.map_id = frappe.dom.get_unique_id();
this.map_area = $(
`<div class="map-wrapper border">
<div id="` + this.map_id + `" style="min-height: 400px; z-index: 1; max-width:100%"></div>
</div>`
);
this.map_area.prependTo($input_wrapper);
this.$wrapper.find('.control-input').addClass("hidden");
this.bind_leaflet_map();
this.bind_leaflet_draw_control();
this.bind_leaflet_locate_control();
this.bind_leaflet_refresh_button();
},

format_for_input(value) {
// render raw value from db into map
this.clear_editable_layers();
if(value) {
var data_layers = new L.FeatureGroup()
.addLayer(L.geoJson(JSON.parse(value),{
pointToLayer: function(geoJsonPoint, latlng) {
if (geoJsonPoint.properties.point_type == "circle"){
return L.circle(latlng, {radius: geoJsonPoint.properties.radius});
} else if (geoJsonPoint.properties.point_type == "circlemarker") {
return L.circleMarker(latlng, {radius: geoJsonPoint.properties.radius});
}
else {
return L.marker(latlng);
}
}
}));
this.add_non_group_layers(data_layers, this.editableLayers);
try {
this.map.flyToBounds(this.editableLayers.getBounds(), {
padding: [50,50]
});
}
catch(err) {
// suppress error if layer has a point.
}
this.editableLayers.addTo(this.map);
this.map._onResize();
} else if ((value===undefined) || (value == JSON.stringify(new L.FeatureGroup().toGeoJSON()))) {
this.locate_control.start();
}
},

bind_leaflet_map() {

var circleToGeoJSON = L.Circle.prototype.toGeoJSON;
L.Circle.include({
toGeoJSON: function() {
var feature = circleToGeoJSON.call(this);
feature.properties = {
point_type: 'circle',
radius: this.getRadius()
};
return feature;
}
});

L.CircleMarker.include({
toGeoJSON: function() {
var feature = circleToGeoJSON.call(this);
feature.properties = {
point_type: 'circlemarker',
radius: this.getRadius()
};
return feature;
}
});

L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
this.map = L.map(this.map_id).setView([19.0800, 72.8961], 13);

L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(this.map);
},

bind_leaflet_locate_control() {
// To request location update and set location, sets current geolocation on load
this.locate_control = L.control.locate({position:'topright'});
this.locate_control.addTo(this.map);
},

bind_leaflet_draw_control() {
this.editableLayers = new L.FeatureGroup();

var options = {
position: 'topleft',
draw: {
polyline: {
shapeOptions: {
color: frappe.ui.color.get('blue'),
weight: 10
}
},
polygon: {
allowIntersection: false, // Restricts shapes to simple polygons
drawError: {
color: frappe.ui.color.get('orange'), // Color the shape will turn when intersects
message: '<strong>Oh snap!<strong> you can\'t draw that!' // Message that will show when intersect
},
shapeOptions: {
color: frappe.ui.color.get('blue')
}
},
circle: true,
rectangle: {
shapeOptions: {
clickable: false
}
}
},
edit: {
featureGroup: this.editableLayers, //REQUIRED!!
remove: true
}
};

// create control and add to map
var drawControl = new L.Control.Draw(options);

this.map.addControl(drawControl);

this.map.on('draw:created', (e) => {
var type = e.layerType,
layer = e.layer;
if (type === 'marker') {
layer.bindPopup('Marker');
}
this.editableLayers.addLayer(layer);
this.set_value(JSON.stringify(this.editableLayers.toGeoJSON()));
});

this.map.on('draw:deleted draw:edited', (e) => {
var layer = e.layer;
this.editableLayers.removeLayer(layer);
this.set_value(JSON.stringify(this.editableLayers.toGeoJSON()));
});
},

bind_leaflet_refresh_button() {
L.easyButton({
id: 'refresh-map-'+this.df.fieldname,
position: 'topright',
type: 'replace',
leafletClasses: true,
states:[{
stateName: 'refresh-map',
onClick: function(button, map){
map._onResize();
},
title: 'Refresh map',
icon: 'fa fa-refresh'
}]
}).addTo(this.map);
},

add_non_group_layers(source_layer, target_group) {
// https://gis.stackexchange.com/a/203773
// Would benefit from https://github.com/Leaflet/Leaflet/issues/4461
if (source_layer instanceof L.LayerGroup) {
source_layer.eachLayer((layer)=>{
this.add_non_group_layers(layer, target_group);
});
} else {
target_group.addLayer(source_layer);
}
},

clear_editable_layers() {
this.editableLayers.eachLayer((l)=>{
this.editableLayers.removeLayer(l);
});
}
});

+ 29
- 0
frappe/public/js/frappe/form/controls/multiselect.js 查看文件

@@ -0,0 +1,29 @@
frappe.ui.form.ControlMultiSelect = frappe.ui.form.ControlAutocomplete.extend({
get_awesomplete_settings() {
const settings = this._super();

return Object.assign(settings, {
filter: function(text, input) {
return Awesomplete.FILTER_CONTAINS(text, input.match(/[^,]*$/)[0]);
},

item: function(text, input) {
return Awesomplete.ITEM(text, input.match(/[^,]*$/)[0]);
},

replace: function(text) {
const before = this.input.value.match(/^.+,\s*|/)[0];
this.input.value = before + text + ", ";
}
});
},

get_data() {
const value = this.get_value() || '';
const values = value.split(', ').filter(d => d);
const data = this._super();

// return values which are not already selected
return data.filter(d => !values.includes(d));
}
});

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

@@ -334,7 +334,7 @@ frappe.ui.form.Dashboard = Class.extend({
// heatmap
render_heatmap: function() {
if(!this.heatmap) {
this.heatmap = new frappe.chart.FrappeChart({
this.heatmap = new Chart({
parent: "#heatmap-" + frappe.model.scrub(this.frm.doctype),
type: 'heatmap',
height: 100,
@@ -412,7 +412,7 @@ frappe.ui.form.Dashboard = Class.extend({
});
this.show();

this.chart = new frappe.chart.FrappeChart(args);
this.chart = new Chart(args);
if(!this.chart) {
this.hide();
}


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

@@ -611,7 +611,7 @@ frappe.ui.form.Grid = Class.extend({

me.frm.clear_table(me.df.fieldname);
$.each(data, function(i, row) {
if(i > 4) {
if(i > 6) {
var blank_row = true;
$.each(row, function(ci, value) {
if(value) {
@@ -659,6 +659,8 @@ frappe.ui.form.Grid = Class.extend({
data.push([]);
data.push([]);
data.push([]);
data.push([__("The CSV format is case sensitive")]);
data.push([__("Do not edit headers which are preset in the template")]);
data.push(["------"]);
$.each(frappe.get_meta(me.df.options).fields, function(i, df) {
if(frappe.model.is_value_type(df.fieldtype)) {


+ 0
- 2
frappe/public/js/frappe/form/quick_entry.js 查看文件

@@ -91,8 +91,6 @@ frappe.ui.form.QuickEntryForm = Class.extend({
fields: this.mandatory,
});
this.dialog.doc = this.doc;
// refresh dependencies etc
this.dialog.refresh();

this.register_primary_action();
this.render_edit_in_full_page_link();


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

@@ -264,7 +264,10 @@ frappe.ui.form.Toolbar = Class.extend({
status = "Submit";
} else if (this.can_save()) {
if (!this.frm.save_disabled) {
status = "Save";
//Show the save button if there is no workflow or if there is a workflow and there are changes
if (this.has_workflow() ? this.frm.doc.__unsaved : true) {
status = "Save";
}
}
} else if (this.can_update()) {
status = "Update";


+ 31
- 0
frappe/public/js/frappe/list/list_renderer.js 查看文件

@@ -74,6 +74,13 @@ frappe.views.ListRenderer = Class.extend({
should_refresh: function() {
return this.list_view.current_view !== this.list_view.last_view;
},
load_last_view: function() {
// this function should handle loading the last view of your list_renderer,
// If you have a last view (for e.g last kanban board in Kanban View),
// load it using frappe.set_route and return true
// else return false
return false;
},
set_wrapper: function () {
this.wrapper = this.list_view.wrapper && this.list_view.wrapper.find('.result-list');
},
@@ -348,6 +355,30 @@ frappe.views.ListRenderer = Class.extend({
this.render_tags($item_container, value);
});

this.render_count();
},

render_count: function() {
const $header_right = this.list_view.list_header.find('.list-item__content--activity');
const current_count = this.list_view.data.length;

frappe.call({
method: 'frappe.model.db_query.get_count',
args: {
doctype: this.doctype,
filters: this.list_view.get_filters_args()
}
}).then(r => {
const count = r.message || current_count;
const str = __('{0} of {1}', [current_count, count]);
const $html = $(`<span>${str}</span>`);

$html.css({
marginRight: '10px'
})
$header_right.addClass('text-muted');
$header_right.html($html);
})
},

// returns html for a data item,


+ 40
- 0
frappe/public/js/frappe/list/list_sidebar.js 查看文件

@@ -27,6 +27,7 @@ frappe.views.ListSidebar = Class.extend({
this.setup_assigned_to_me();
this.setup_views();
this.setup_kanban_boards();
this.setup_calendar_view();
this.setup_email_inbox();

let limits = frappe.boot.limits;
@@ -271,6 +272,45 @@ frappe.views.ListSidebar = Class.extend({
}
});
},
setup_calendar_view: function() {
const doctype = this.doctype;

frappe.db.get_list('Calendar View', {
filters: {
reference_doctype: doctype
}
}).then(result => {
if (!result) return;
const calendar_views = result;
const $link_calendar = this.sidebar.find('.list-link[data-view="Calendar"]');

let default_link = '';
if (frappe.views.calendar[this.doctype]) {
// has standard calendar view
default_link = `<li><a href="#List/${doctype}/Calendar/Default">
${ __("Default") }</a></li>`;
}
const other_links = calendar_views.map(
calendar_view => `<li><a href="#List/${doctype}/Calendar/${calendar_view.name}">
${ __(calendar_view.name) }</a>
</li>`
).join('');

const dropdown_html = `
<div class="btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
${ __("Calendar") } <span class="caret"></span>
</a>
<ul class="dropdown-menu calendar-dropdown" style="max-height: 300px; overflow-y: auto;">
${default_link}
${other_links}
</ul>
</div>
`;
$link_calendar.removeClass('hide');
$link_calendar.html(dropdown_html);
});
},
setup_email_inbox: function() {
// get active email account for the user and add in dropdown
if(this.doctype != "Communication")


+ 7
- 0
frappe/public/js/frappe/list/list_view.js 查看文件

@@ -399,6 +399,13 @@ frappe.views.ListView = frappe.ui.BaseList.extend({

if (this.list_renderer.should_refresh()) {
this.setup_list_renderer();

if (this.list_renderer.load_last_view && this.list_renderer.load_last_view()) {
// let the list_renderer load the last view for the current view
// for e.g last kanban board for kanban view
return;
}

this.refresh_surroundings();
this.dirty = true;
}


+ 26
- 22
frappe/public/js/frappe/roles_editor.js 查看文件

@@ -1,8 +1,9 @@
frappe.RoleEditor = Class.extend({
init: function(wrapper, frm) {
init: function(wrapper, frm, disable) {
var me = this;
this.frm = frm;
this.wrapper = wrapper;
this.disable = disable;
$(wrapper).html('<div class="help">' + __("Loading") + '...</div>')
return frappe.call({
method: 'frappe.core.doctype.user.user.get_all_roles',
@@ -21,33 +22,35 @@ frappe.RoleEditor = Class.extend({
show_roles: function() {
var me = this;
$(this.wrapper).empty();
var role_toolbar = $('<p><button class="btn btn-default btn-add btn-sm" style="margin-right: 5px;"></button>\
<button class="btn btn-sm btn-default btn-remove"></button></p>').appendTo($(this.wrapper));

role_toolbar.find(".btn-add")
.html(__('Add all roles'))
.on("click", function () {
$(me.wrapper).find('input[type="checkbox"]').each(function (i, check) {
if (!$(check).is(":checked")) {
check.checked = true;
}
if(me.frm.doctype != 'User') {
var role_toolbar = $('<p><button class="btn btn-default btn-add btn-sm" style="margin-right: 5px;"></button>\
<button class="btn btn-sm btn-default btn-remove"></button></p>').appendTo($(this.wrapper));

role_toolbar.find(".btn-add")
.html(__('Add all roles'))
.on("click", function () {
$(me.wrapper).find('input[type="checkbox"]').each(function (i, check) {
if (!$(check).is(":checked")) {
check.checked = true;
}
});
});
});
role_toolbar.find(".btn-remove")
.html(__('Clear all roles'))
.on("click", function() {
$(me.wrapper).find('input[type="checkbox"]').each(function(i, check) {
if($(check).is(":checked")) {
check.checked = false;
}
role_toolbar.find(".btn-remove")
.html(__('Clear all roles'))
.on("click", function() {
$(me.wrapper).find('input[type="checkbox"]').each(function(i, check) {
if($(check).is(":checked")) {
check.checked = false;
}
});
});
});
}

$.each(this.roles, function(i, role) {
$(me.wrapper).append(repl('<div class="user-role" \
data-user-role="%(role_value)s">\
<input type="checkbox" style="margin-top:0px;"> \
<input type="checkbox" style="margin-top:0px;" class="box"> \
<a href="#" class="grey role">%(role_display)s</a>\
</div>', {role_value: role,role_display:__(role)}));
});
@@ -63,6 +66,7 @@ frappe.RoleEditor = Class.extend({
},
show: function() {
var me = this;
$('.box').attr('disabled', this.disable);

// uncheck all roles
$(this.wrapper).find('input[type="checkbox"]')


+ 0
- 1556
frappe/public/js/frappe/ui/charts.js
文件差異過大導致無法顯示
查看文件


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

@@ -15,7 +15,9 @@ frappe.ui.color_map = {
skyblue: ["#d2f1ff", "#a6e4ff", "#78d6ff", "#4f8ea8"],
blue: ["#d2d2ff", "#a3a3ff", "#7575ff", "#4d4da8"],
purple: ["#dac7ff", "#b592ff", "#8e58ff", "#5e3aa8"],
pink: ["#f8d4f8", "#f3aaf0", "#ec7dea", "#934f92"]
pink: ["#f8d4f8", "#f3aaf0", "#ec7dea", "#934f92"],
white: ["#d1d8dd", "#fafbfc", "#ffffff", ""],
black: ["#8D99A6", "#6c7680", "#36414c", "#212a33"]
};

frappe.ui.color = {


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

@@ -8,14 +8,25 @@ frappe.views.CalendarView = frappe.views.ListRenderer.extend({
name: 'Calendar',
render_view: function() {
var me = this;
var options = {
doctype: this.doctype,
parent: this.wrapper,
page: this.list_view.page,
list_view: this.list_view
this.get_calendar_options()
.then(options => {
this.calendar = new frappe.views.Calendar(options);
});
},
load_last_view: function() {
const route = frappe.get_route();

if (!route[3]) {
// routed to Calendar view, check last calendar_view
let calendar_view = this.user_settings.last_calendar_view;

if (calendar_view) {
frappe.set_route('List', this.doctype, 'Calendar', calendar_view);
return true;
}
}
$.extend(options, frappe.views.calendar[this.doctype]);
this.calendar = new frappe.views.Calendar(options);
return false;
},
set_defaults: function() {
this._super();
@@ -27,6 +38,62 @@ frappe.views.CalendarView = frappe.views.ListRenderer.extend({
get_header_html: function() {
return null;
},
should_refresh: function() {
var should_refresh = this._super();
if(!should_refresh) {
this.last_calendar_view = this.current_calendar_view || '';
this.current_calendar_view = this.get_calendar_view();

if (this.current_calendar_view !== 'Default') {
this.page_title = __(this.current_calendar_view);
} else {
this.page_title = this.doctype + ' ' + __('Calendar');
}

should_refresh = this.current_calendar_view !== this.last_calendar_view;
}
return should_refresh;
},
get_calendar_view: function() {
return frappe.get_route()[3];
},
get_calendar_options: function() {
const calendar_view = frappe.get_route()[3] || 'Default';

// save in user_settings
frappe.model.user_settings.save(this.doctype, 'Calendar', {
last_calendar_view: calendar_view
});

const options = {
doctype: this.doctype,
parent: this.wrapper,
page: this.list_view.page,
list_view: this.list_view
}

return new Promise(resolve => {
if (calendar_view === 'Default') {
Object.assign(options, frappe.views.calendar[this.doctype]);
resolve(options);
} else {

frappe.model.with_doc('Calendar View', calendar_view, () => {
const doc = frappe.get_doc('Calendar View', calendar_view);
Object.assign(options, {
field_map: {
id: "name",
start: doc.start_date_field,
end: doc.end_date_field,
title: doc.subject_field
}
});

resolve(options);
});
}
})
},
required_libs: [
'assets/frappe/js/lib/fullcalendar/fullcalendar.min.css',
'assets/frappe/js/lib/fullcalendar/fullcalendar.min.js',
@@ -202,7 +269,8 @@ frappe.views.Calendar = Class.extend({
doctype: this.doctype,
start: this.get_system_datetime(start),
end: this.get_system_datetime(end),
filters: this.list_view.filter_list.get_filters()
filters: this.list_view.filter_list.get_filters(),
field_map: this.field_map
};
return args;
},
@@ -232,6 +300,15 @@ frappe.views.Calendar = Class.extend({
d.start = frappe.datetime.convert_to_user_tz(d.start);
d.end = frappe.datetime.convert_to_user_tz(d.end);

// show event on single day if start or end date is invalid
if (!frappe.datetime.validate(d.start) && d.end) {
d.start = frappe.datetime.add_days(d.end, -1);
}

if (d.start && !frappe.datetime.validate(d.end)) {
d.end = frappe.datetime.add_days(d.start, 1);
}

me.fix_end_date_for_event_render(d);
me.prepare_colors(d);
return d;
@@ -241,10 +318,8 @@ frappe.views.Calendar = Class.extend({
let color, color_name;
if(this.get_css_class) {
color_name = this.color_map[this.get_css_class(d)];
color_name =
frappe.ui.color.validate_hex(color_name) ?
color_name :
'blue';
color_name = frappe.ui.color.validate_hex(color_name) ?
color_name : 'blue';
d.backgroundColor = frappe.ui.color.get(color_name, 'extra-light');
d.textColor = frappe.ui.color.get(color_name, 'dark');
} else {


+ 8
- 2
frappe/public/js/frappe/views/communication.js 查看文件

@@ -48,8 +48,9 @@ frappe.views.CommunicationComposer = Class.extend({
get_fields: function() {
var fields= [
{label:__("To"), fieldtype:"Data", reqd: 0, fieldname:"recipients",length:524288},
{fieldtype: "Section Break", collapsible: 1, label: __("CC & Standard Reply")},
{fieldtype: "Section Break", collapsible: 1, label: __("CC, BCC & Standard Reply")},
{label:__("CC"), fieldtype:"Data", fieldname:"cc", length:524288},
{label:__("BCC"), fieldtype:"Data", fieldname:"bcc", length:524288},
{label:__("Standard Reply"), fieldtype:"Link", options:"Standard Reply",
fieldname:"standard_reply"},
{fieldtype: "Section Break"},
@@ -109,6 +110,7 @@ frappe.views.CommunicationComposer = Class.extend({

this.dialog.fields_dict.recipients.set_value(this.recipients || '');
this.dialog.fields_dict.cc.set_value(this.cc || '');
this.dialog.fields_dict.bcc.set_value(this.bcc || '');

if(this.dialog.fields_dict.sender) {
this.dialog.fields_dict.sender.set_value(this.sender || '');
@@ -123,6 +125,7 @@ frappe.views.CommunicationComposer = Class.extend({
if(!this.forward && !this.recipients && this.last_email) {
this.recipients = this.last_email.sender;
this.cc = this.last_email.cc;
this.bcc = this.last_email.bcc;
}

if(!this.forward && !this.recipients) {
@@ -446,6 +449,7 @@ frappe.views.CommunicationComposer = Class.extend({
// concat in cc
if ( form_values[df.fieldname] ) {
form_values.cc = ( form_values.cc ? (form_values.cc + ", ") : "" ) + df.fieldname;
form_values.bcc = ( form_values.bcc ? (form_values.bcc + ", ") : "" ) + df.fieldname;
}

delete form_values[df.fieldname];
@@ -484,6 +488,7 @@ frappe.views.CommunicationComposer = Class.extend({
args: {
recipients: form_values.recipients,
cc: form_values.cc,
bcc: form_values.bcc,
subject: form_values.subject,
content: form_values.content,
doctype: me.doc.doctype,
@@ -594,7 +599,8 @@ frappe.views.CommunicationComposer = Class.extend({
var me = this;
[
this.dialog.fields_dict.recipients.input,
this.dialog.fields_dict.cc.input
this.dialog.fields_dict.cc.input,
this.dialog.fields_dict.bcc.input
].map(function(input) {
me.setup_awesomplete_for_input(input);
});


+ 157
- 82
frappe/public/js/frappe/views/image/image_view.js 查看文件

@@ -7,8 +7,17 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({
name: 'Image',
render_view: function (values) {
this.items = values;
this.render_image_view();
this.setup_gallery();

this.get_attached_images()
.then(() => {
this.render_image_view();

if (!this.gallery) {
this.setup_gallery();
} else {
this.gallery.prepare_pswp_items(this.items, this.images_map);
}
});
},
set_defaults: function() {
this._super();
@@ -22,9 +31,14 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({
},
render_image_view: function () {
var html = this.items.map(this.render_item.bind(this)).join("");
this.container = $('<div>')
.addClass('image-view-container')
.appendTo(this.wrapper);

this.container = this.wrapper.find('.image-view-container');
if (this.container.length === 0) {
this.container = $('<div>')
.addClass('image-view-container')
.appendTo(this.wrapper);
}

this.container.append(html);
},
render_item: function (item) {
@@ -50,6 +64,14 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({
}
return null;
},
get_attached_images: function () {
return frappe.call({
method: 'frappe.core.doctype.file.file.get_attached_images',
args: { doctype: this.doctype, names: this.items.map(i => i.name) }
}).then(r => {
this.images_map = Object.assign(this.images_map || {}, r.message);
});
},
get_header_html: function () {
var main = frappe.render_template('list_item_main_head', {
col: { type: "Subject" },
@@ -60,16 +82,17 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({
},
setup_gallery: function() {
var me = this;
var gallery = new frappe.views.GalleryView({
this.gallery = new frappe.views.GalleryView({
doctype: this.doctype,
items: this.items,
wrapper: this.container
wrapper: this.container,
images_map: this.images_map
});
this.container.on('click', '.btn.zoom-view', function(e) {
e.preventDefault();
e.stopPropagation();
var name = $(this).data().name;
gallery.show(name);
me.gallery.show(name);
return false;
});
}
@@ -80,10 +103,9 @@ frappe.views.GalleryView = Class.extend({
$.extend(this, opts);
var me = this;

this.ready = false;
this.load_lib(function() {
this.lib_ready = this.load_lib();
this.lib_ready.then(function() {
me.prepare();
me.ready = true;
});
},
prepare: function() {
@@ -94,101 +116,154 @@ frappe.views.GalleryView = Class.extend({
this.pswp_root = $(pswp).appendTo('body');
}
},
show: function(docname) {
prepare_pswp_items: function(_items, _images_map) {
var me = this;
if(!this.ready) {
setTimeout(this.show.bind(this), 200);
return;

if (_items) {
// passed when more button clicked
this.items = this.items.concat(_items);
this.images_map = _images_map;
}
var items = this.items.map(function(i) {
var query = 'img[data-name="'+i.name+'"]';
var el = me.wrapper.find(query).get(0);

if(el) {
var width = el.naturalWidth;
var height = el.naturalHeight;
}
return new Promise(resolve => {
const items = this.items.map(function(i) {
const query = 'img[data-name="'+i.name+'"]';
let el = me.wrapper.find(query).get(0);

if(!el) {
el = me.wrapper.find('.image-field[data-name="'+i.name+'"]').get(0);
width = el.getBoundingClientRect().width;
height = el.getBoundingClientRect().height;
}
let width, height;
if(el) {
width = el.naturalWidth;
height = el.naturalHeight;
}

return {
src: i._image_url,
msrc: i._image_url,
name: i.name,
w: width,
h: height,
el: el
}
});
if(!el) {
el = me.wrapper.find('.image-field[data-name="'+i.name+'"]').get(0);
width = el.getBoundingClientRect().width;
height = el.getBoundingClientRect().height;
}

var index;
items.map(function(item, i) {
if(item.name === docname)
index = i;
return {
src: i._image_url,
msrc: i._image_url,
name: i.name,
w: width,
h: height,
el: el
}
});
this.pswp_items = items;
resolve();
});
},
show: function(docname) {
this.lib_ready
.then(() => this.prepare_pswp_items())
.then(() => this._show(docname));
},
_show: function(docname) {
const me = this;
const items = this.pswp_items;
const item_index = items.findIndex(item => item.name === docname);

var options = {
index: index,
index: item_index,
getThumbBoundsFn: function(index) {
var thumbnail = items[index].el, // find thumbnail
pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
const query = 'img[data-name="' + items[index].name + '"]';
let thumbnail = me.wrapper.find(query).get(0);

if (!thumbnail) {
return;
}

var pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
rect = thumbnail.getBoundingClientRect();

return {x:rect.left, y:rect.top + pageYScroll, w:rect.width};
},
history: false,
shareEl: false,
showHideOpacity: true
}
var pswp = new PhotoSwipe(

// init
this.pswp = new PhotoSwipe(
this.pswp_root.get(0),
PhotoSwipeUI_Default,
items,
options
);
pswp.init();
this.browse_images();
this.pswp.init();
},
get_image_urls: function() {
// not implemented yet
return frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "File",
order_by: "attached_to_name",
fields: [
"'image/*' as type", "ifnull(thumbnail_url, file_url) as thumbnail",
"concat(attached_to_name, ' - ', file_name) as title", "file_url as src",
"attached_to_name as name"
],
filters: [
["File", "attached_to_doctype", "=", this.doctype],
["File", "attached_to_name", "in", this.docnames],
["File", "is_folder", "!=", 1]
]
},
freeze: true,
freeze_message: __("Fetching Images..")
}).then(function(r) {
if (!r.message) {
frappe.msgprint(__("No Images found"))
} else {
// filter image files from other
var images = r.message.filter(function(image) {
return frappe.utils.is_image_file(image.title || image.href);
});
browse_images: function() {
const $more_items = this.pswp_root.find('.pswp__more-items');
const images_map = this.images_map;
let last_hide_timeout = null;

this.pswp.listen('afterChange', function() {
const images = images_map[this.currItem.name];
if (!images || images.length === 1) {
$more_items.html('');
return;
}

hide_more_items_after_2s();
const html = images.map(img_html).join("");
$more_items.html(html);
});

this.pswp.listen('beforeChange', hide_more_items);
this.pswp.listen('initialZoomOut', hide_more_items);
this.pswp.listen('destroy', $(document).off('mousemove', hide_more_items_after_2s));

// Replace current image on click
$more_items.on('click', '.pswp__more-item', (e) => {
const img_el = e.target;
const index = this.pswp.items.findIndex(i => i.name === this.pswp.currItem.name);

this.pswp.goTo(index);
this.pswp.items.splice(index, 1, {
src: img_el.src,
w: img_el.naturalWidth,
h: img_el.naturalHeight,
name: this.pswp.currItem.name
});
this.pswp.invalidateCurrItems();
this.pswp.updateSize(true);
});

// hide more-images 2s after mousemove
$(document).on('mousemove', hide_more_items_after_2s);

function hide_more_items_after_2s() {
clearTimeout(last_hide_timeout);
show_more_items();
last_hide_timeout = setTimeout(hide_more_items, 2000);
}

function show_more_items() {
$more_items.show();
}

function hide_more_items() {
$more_items.hide();
}

function img_html(src) {
return `<div class="pswp__more-item">
<img src="${src}">
</div>`;
}
},
load_lib: function(callback) {
var asset_dir = 'assets/frappe/js/lib/photoswipe/';
frappe.require([
asset_dir + 'photoswipe.css',
asset_dir + 'default-skin.css',
asset_dir + 'photoswipe.js',
asset_dir + 'photoswipe-ui-default.js'
], callback);
load_lib: function() {
return new Promise(resolve => {
var asset_dir = 'assets/frappe/js/lib/photoswipe/';
frappe.require([
asset_dir + 'photoswipe.css',
asset_dir + 'default-skin.css',
asset_dir + 'photoswipe.js',
asset_dir + 'photoswipe-ui-default.js'
], resolve);
});
}
});
});

+ 48
- 44
frappe/public/js/frappe/views/image/photoswipe_dom.html 查看文件

@@ -4,66 +4,70 @@
<!-- Root element of PhotoSwipe. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

<!-- Background of PhotoSwipe.
It's a separate element as animating opacity is faster than rgba(). -->
<div class="pswp__bg"></div>
<!-- Background of PhotoSwipe.
It's a separate element as animating opacity is faster than rgba(). -->
<div class="pswp__bg"></div>

<!-- Slides wrapper with overflow:hidden. -->
<div class="pswp__scroll-wrap">
<!-- Slides wrapper with overflow:hidden. -->
<div class="pswp__scroll-wrap">

<!-- Container that holds slides.
PhotoSwipe keeps only 3 of them in the DOM to save memory.
Don't modify these 3 pswp__item elements, data is added later on. -->
<div class="pswp__container">
<div class="pswp__item"></div>
<div class="pswp__item"></div>
<div class="pswp__item"></div>
</div>
<!-- Container that holds slides.
PhotoSwipe keeps only 3 of them in the DOM to save memory.
Don't modify these 3 pswp__item elements, data is added later on. -->
<div class="pswp__container">
<div class="pswp__item"></div>
<div class="pswp__item"></div>
<div class="pswp__item"></div>
</div>

<!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
<div class="pswp__ui pswp__ui--hidden">
<div class="pswp__more-items">

<div class="pswp__top-bar">
</div>

<!-- Controls are self-explanatory. Order can be changed. -->
<!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
<div class="pswp__ui pswp__ui--hidden">

<div class="pswp__counter"></div>
<div class="pswp__top-bar">

<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
<!-- Controls are self-explanatory. Order can be changed. -->

<button class="pswp__button pswp__button--share" title="Share"></button>
<div class="pswp__counter"></div>

<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>

<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
<button class="pswp__button pswp__button--share" title="Share"></button>

<!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
<!-- element will get class pswp__preloader--active when preloader is running -->
<div class="pswp__preloader">
<div class="pswp__preloader__icn">
<div class="pswp__preloader__cut">
<div class="pswp__preloader__donut"></div>
</div>
</div>
</div>
</div>
<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>

<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
<div class="pswp__share-tooltip"></div>
</div>
<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

<button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
</button>
<!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
<!-- element will get class pswp__preloader--active when preloader is running -->
<div class="pswp__preloader">
<div class="pswp__preloader__icn">
<div class="pswp__preloader__cut">
<div class="pswp__preloader__donut"></div>
</div>
</div>
</div>
</div>

<button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
</button>
<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
<div class="pswp__share-tooltip"></div>
</div>

<div class="pswp__caption">
<div class="pswp__caption__center"></div>
</div>
<button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
</button>

</div>
<button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
</button>

</div>
<div class="pswp__caption">
<div class="pswp__caption__center"></div>
</div>

</div>

</div>

</div>

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

@@ -672,7 +672,7 @@ frappe.views.GridReportWithPlot = frappe.views.GridReport.extend({
}
var chart_data = this.get_chart_data ? this.get_chart_data() : null;

this.chart = new frappe.chart.FrappeChart({
this.chart = new Chart({
parent: ".chart",
height: 200,
data: chart_data,


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

@@ -945,7 +945,7 @@ frappe.views.QueryReport = Class.extend({

if(opts.data && opts.data.labels && opts.data.labels.length) {
this.chart_area.toggle(true);
this.chart = new frappe.chart.FrappeChart(opts);
this.chart = new Chart(opts);
}
},



+ 12
- 8
frappe/public/js/frappe/views/treeview.js 查看文件

@@ -93,16 +93,20 @@ frappe.views.TreeView = Class.extend({
filter.default = frappe.route_options[filter.fieldname]
}

me.page.add_field(filter).$input
.on('change', function() {
var val = $(this).val();
if(val) {
me.args[$(this).attr("data-fieldname")] = val;
frappe.treeview_settings.filters = me.args;
filter.change = function() {
var val = this.get_value();
if(!val && me.set_root){
val = me.opts.root_label;
}
if(val){
me.args[filter.fieldname] = val;
frappe.treeview_setting
me.make_tree();
me.page.set_title(val);
}
})
}
}

me.page.add_field(filter);

if (filter.default) {
$("[data-fieldname='"+filter.fieldname+"']").trigger("change");


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

@@ -955,8 +955,8 @@ _f.Frm.prototype.validate_form_action = function(action, resolve) {
// Allow submit, write, cancel and create permissions for read only documents that are assigned by
// workflows if the user already have those permissions. This is to allow for users to
// continue through the workflow states and to allow execution of functions like Duplicate.
if (!frappe.workflow.is_read_only(this.doctype, this.docname) && (perms["write"] ||
perms["create"] || perms["submit"] || perms["cancel"])) {
if ((frappe.workflow.is_read_only(this.doctype, this.docname) && (perms["write"] ||
perms["create"] || perms["submit"] || perms["cancel"])) || !frappe.workflow.is_read_only(this.doctype, this.docname)) {
var allowed_for_workflow = true;
}



+ 0
- 39
frappe/public/js/lib/Chart.min.js 查看文件

@@ -1,39 +0,0 @@
var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a=
Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);d<b||d>c;)a=d<b?a/2:2*a,d=Math.round(e/a);c=[];z(f,c,d,h,a);return{steps:d,stepValue:a,graphMin:h,labels:c}}function z(a,c,b,e,h){if(a)for(var f=1;f<b+1;f++)c.push(E(a,{value:(e+h*f).toFixed(0!=h%1?h.toString().split(".")[1].length:0)}))}function A(a,c,b){return!isNaN(parseFloat(c))&&isFinite(c)&&a>c?c:!isNaN(parseFloat(b))&&
isFinite(b)&&a<b?b:a}function y(a,c){var b={},e;for(e in a)b[e]=a[e];for(e in c)b[e]=c[e];return b}function E(a,c){var b=!/\W/.test(a)?F[a]=F[a]||E(document.getElementById(a).innerHTML):new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+a.replace(/[\r\t\n]/g," ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c?
b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)?
0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1==
a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*
Math.PI)*Math.asin(1/e);return-(e*Math.pow(2,10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b))},easeOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return e*Math.pow(2,-10*a)*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(2==(a/=0.5))return 1;b||(b=1*0.3*1.5);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return 1>a?-0.5*e*Math.pow(2,10*
(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)*
a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0,
scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",
animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",
scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a,
c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,
onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,
pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",
scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]);
d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.length;f++)a[f].value>e&&(e=a[f].value),a[f].value<h&&(h=a[f].value);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,
m);k=g/j.steps;x(c,function(){for(var a=0;a<j.steps;a++)if(c.scaleShowLine&&(b.beginPath(),b.arc(q/2,u/2,k*(a+1),0,2*Math.PI,!0),b.strokeStyle=c.scaleLineColor,b.lineWidth=c.scaleLineWidth,b.stroke()),c.scaleShowLabels){b.textAlign="center";b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;var e=j.labels[a];if(c.scaleShowLabelBackdrop){var d=b.measureText(e).width;b.fillStyle=c.scaleBackdropColor;b.beginPath();b.rect(Math.round(q/2-d/2-c.scaleBackdropPaddingX),Math.round(u/2-k*(a+
1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(d+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY));b.fill()}b.textBaseline="middle";b.fillStyle=c.scaleFontColor;b.fillText(e,q/2,u/2-k*(a+1))}},function(e){var d=-Math.PI/2,g=2*Math.PI/a.length,f=1,h=1;c.animation&&(c.animateScale&&(f=e),c.animateRotate&&(h=e));for(e=0;e<a.length;e++)b.beginPath(),b.arc(q/2,u/2,f*v(a[e].value,j,k),d,d+h*g,!1),b.lineTo(q/2,u/2),b.closePath(),b.fillStyle=a[e].color,b.fill(),
c.segmentShowStroke&&(b.strokeStyle=c.segmentStrokeColor,b.lineWidth=c.segmentStrokeWidth,b.stroke()),d+=h*g},b)},H=function(a,c,b){var e,h,f,d,g,k,j,l,m;a.labels||(a.labels=[]);g=Math.min.apply(Math,[q,u])/2;d=2*c.scaleFontSize;for(e=l=0;e<a.labels.length;e++)b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily,h=b.measureText(a.labels[e]).width,h>l&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE;
h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(m=0;m<a.datasets[f].data.length;m++)a.datasets[f].data[m]>e&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]<h&&(h=a.datasets[f].data[m]);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,m);k=g/j.steps;x(c,function(){var e=2*Math.PI/
a.datasets[0].data.length;b.save();b.translate(q/2,u/2);if(c.angleShowLineOut){b.strokeStyle=c.angleLineColor;b.lineWidth=c.angleLineWidth;for(var d=0;d<a.datasets[0].data.length;d++)b.rotate(e),b.beginPath(),b.moveTo(0,0),b.lineTo(0,-g),b.stroke()}for(d=0;d<j.steps;d++){b.beginPath();if(c.scaleShowLine){b.strokeStyle=c.scaleLineColor;b.lineWidth=c.scaleLineWidth;b.moveTo(0,-k*(d+1));for(var f=0;f<a.datasets[0].data.length;f++)b.rotate(e),b.lineTo(0,-k*(d+1));b.closePath();b.stroke()}c.scaleShowLabels&&
(b.textAlign="center",b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily,b.textBaseline="middle",c.scaleShowLabelBackdrop&&(f=b.measureText(j.labels[d]).width,b.fillStyle=c.scaleBackdropColor,b.beginPath(),b.rect(Math.round(-f/2-c.scaleBackdropPaddingX),Math.round(-k*(d+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(f+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY)),b.fill()),b.fillStyle=c.scaleFontColor,b.fillText(j.labels[d],0,-k*(d+
1)))}for(d=0;d<a.labels.length;d++){b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily;b.fillStyle=c.pointLabelFontColor;var f=Math.sin(e*d)*(g+c.pointLabelFontSize),h=Math.cos(e*d)*(g+c.pointLabelFontSize);b.textAlign=e*d==Math.PI||0==e*d?"center":e*d>Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;g<a.datasets.length;g++){b.beginPath();
b.moveTo(0,d*-1*v(a.datasets[g].data[0],j,k));for(var f=1;f<a.datasets[g].data.length;f++)b.rotate(e),b.lineTo(0,d*-1*v(a.datasets[g].data[f],j,k));b.closePath();b.fillStyle=a.datasets[g].fillColor;b.strokeStyle=a.datasets[g].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.fill();b.stroke();if(c.pointDot){b.fillStyle=a.datasets[g].pointColor;b.strokeStyle=a.datasets[g].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(f=0;f<a.datasets[g].data.length;f++)b.rotate(e),b.beginPath(),b.arc(0,d*-1*
v(a.datasets[g].data[f],j,k),c.pointDotRadius,2*Math.PI,!1),b.fill(),b.stroke()}b.rotate(e)}b.restore()},b)},I=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=0;f<a.length;f++)e+=a[f].value;x(c,null,function(d){var g=-Math.PI/2,f=1,j=1;c.animation&&(c.animateScale&&(f=d),c.animateRotate&&(j=d));for(d=0;d<a.length;d++){var l=j*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,f*h,g,g+l);b.lineTo(q/2,u/2);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&(b.lineWidth=
c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());g+=l}},b)},J=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=h*(c.percentageInnerCutout/100),d=0;d<a.length;d++)e+=a[d].value;x(c,null,function(d){var k=-Math.PI/2,j=1,l=1;c.animation&&(c.animateScale&&(j=d),c.animateRotate&&(l=d));for(d=0;d<a.length;d++){var m=l*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,j*h,k,k+m,!1);b.arc(q/2,u/2,j*f,k+m,k,!0);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&
(b.lineWidth=c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());k+=m}},b)},K=function(a,c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(s=45,q/a.labels.length<Math.cos(s)*t?(s=90,g-=t):g-=Math.sin(s)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=
0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;
for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<s?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<s?(b.translate(n+d*m,p+c.scaleFontSize),b.rotate(-(s*(Math.PI/180))),b.fillText(a.labels[d],
0,0),b.restore()):b.fillText(a.labels[d],n+d*m,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+d*m,p+3),c.scaleShowGridLines&&0<d?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+d*m,5)):b.lineTo(n+d*m,p+3),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,
b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){function e(b,c){return p-d*v(a.datasets[b].data[c],j,k)}for(var f=0;f<a.datasets.length;f++){b.strokeStyle=a.datasets[f].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.beginPath();b.moveTo(n,p-d*v(a.datasets[f].data[0],j,k));for(var g=1;g<a.datasets[f].data.length;g++)c.bezierCurve?b.bezierCurveTo(n+m*(g-0.5),e(f,g-1),n+m*(g-0.5),
e(f,g),n+m*g,e(f,g)):b.lineTo(n+m*g,e(f,g));b.stroke();c.datasetFill?(b.lineTo(n+m*(a.datasets[f].data.length-1),p),b.lineTo(n,p),b.closePath(),b.fillStyle=a.datasets[f].fillColor,b.fill()):b.closePath();if(c.pointDot){b.fillStyle=a.datasets[f].pointColor;b.strokeStyle=a.datasets[f].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(g=0;g<a.datasets[f].data.length;g++)b.beginPath(),b.arc(n+m*g,p-d*v(a.datasets[f].data[g],j,k),c.pointDotRadius,0,2*Math.PI,!0),b.fill(),b.stroke()}}},b)},L=function(a,
c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s,w=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(w=45,q/a.labels.length<Math.cos(w)*t?(w=90,g-=t):g-=Math.sin(w)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<
h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=
Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<w?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<w?(b.translate(n+
d*m,p+c.scaleFontSize),b.rotate(-(w*(Math.PI/180))),b.fillText(a.labels[d],0,0),b.restore()):b.fillText(a.labels[d],n+d*m+m/2,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+(d+1)*m,p+3),b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+(d+1)*m,5),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*
k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){b.lineWidth=c.barStrokeWidth;for(var e=0;e<a.datasets.length;e++){b.fillStyle=a.datasets[e].fillColor;b.strokeStyle=a.datasets[e].strokeColor;for(var f=0;f<a.datasets[e].data.length;f++){var g=n+c.barValueSpacing+m*f+s*e+c.barDatasetSpacing*e+c.barStrokeWidth*e;b.beginPath();
b.moveTo(g,p);b.lineTo(g,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p);c.barShowStroke&&b.stroke();b.closePath();b.fill()}}},b)},D=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)},F={}};

+ 2
- 0
frappe/public/js/lib/frappe-charts/frappe-charts.min.js
文件差異過大導致無法顯示
查看文件


+ 12
- 0
frappe/public/js/lib/leaflet/L.Control.Locate.css 查看文件

@@ -0,0 +1,12 @@
/* Compatible with Leaflet 0.7 */
.leaflet-control-locate a {
font-size: 1.4em;
color: #444;
cursor: pointer;
}
.leaflet-control-locate.active a {
color: #2074B6;
}
.leaflet-control-locate.active.following a {
color: #FC8428;
}

+ 591
- 0
frappe/public/js/lib/leaflet/L.Control.Locate.js 查看文件

@@ -0,0 +1,591 @@
/*!
Copyright (c) 2016 Dominik Moritz

This file is part of the leaflet locate control. It is licensed under the MIT license.
You can find the project at: https://github.com/domoritz/leaflet-locatecontrol
*/
(function (factory, window) {
// see https://github.com/Leaflet/Leaflet/blob/master/PLUGIN-GUIDE.md#module-loaders
// for details on how to structure a leaflet plugin.

// define an AMD module that relies on 'leaflet'
if (typeof define === 'function' && define.amd) {
define(['leaflet'], factory);

// define a Common JS module that relies on 'leaflet'
} else if (typeof exports === 'object') {
if (typeof window !== 'undefined' && window.L) {
module.exports = factory(L);
} else {
module.exports = factory(require('leaflet'));
}
}

// attach your plugin to the global 'L' variable
if (typeof window !== 'undefined' && window.L){
window.L.Control.Locate = factory(L);
}
} (function (L) {
var LDomUtilApplyClassesMethod = function(method, element, classNames) {
classNames = classNames.split(' ');
classNames.forEach(function(className) {
L.DomUtil[method].call(this, element, className);
});
};

var addClasses = function(el, names) { LDomUtilApplyClassesMethod('addClass', el, names); };
var removeClasses = function(el, names) { LDomUtilApplyClassesMethod('removeClass', el, names); };

var LocateControl = L.Control.extend({
options: {
/** Position of the control */
position: 'topleft',
/** The layer that the user's location should be drawn on. By default creates a new layer. */
layer: undefined,
/**
* Automatically sets the map view (zoom and pan) to the user's location as it updates.
* While the map is following the user's location, the control is in the `following` state,
* which changes the style of the control and the circle marker.
*
* Possible values:
* - false: never updates the map view when location changes.
* - 'once': set the view when the location is first determined
* - 'always': always updates the map view when location changes.
* The map view follows the users location.
* - 'untilPan': (default) like 'always', except stops updating the
* view if the user has manually panned the map.
* The map view follows the users location until she pans.
*/
setView: 'untilPan',
/** Keep the current map zoom level when setting the view and only pan. */
keepCurrentZoomLevel: false,
/** Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. */
flyTo: false,
/**
* The user location can be inside and outside the current view when the user clicks on the
* control that is already active. Both cases can be configures separately.
* Possible values are:
* - 'setView': zoom and pan to the current location
* - 'stop': stop locating and remove the location marker
*/
clickBehavior: {
/** What should happen if the user clicks on the control while the location is within the current view. */
inView: 'stop',
/** What should happen if the user clicks on the control while the location is outside the current view. */
outOfView: 'setView',
},
/**
* If set, save the map bounds just before centering to the user's
* location. When control is disabled, set the view back to the
* bounds that were saved.
*/
returnToPrevBounds: false,
/**
* Keep a cache of the location after the user deactivates the control. If set to false, the user has to wait
* until the locate API returns a new location before they see where they are again.
*/
cacheLocation: true,
/** If set, a circle that shows the location accuracy is drawn. */
drawCircle: true,
/** If set, the marker at the users' location is drawn. */
drawMarker: true,
/** The class to be used to create the marker. For example L.CircleMarker or L.Marker */
markerClass: L.CircleMarker,
/** Accuracy circle style properties. */
circleStyle: {
color: '#136AEC',
fillColor: '#136AEC',
fillOpacity: 0.15,
weight: 2,
opacity: 0.5
},
/** Inner marker style properties. Only works if your marker class supports `setStyle`. */
markerStyle: {
color: '#136AEC',
fillColor: '#2A93EE',
fillOpacity: 0.7,
weight: 2,
opacity: 0.9,
radius: 5
},
/**
* Changes to accuracy circle and inner marker while following.
* It is only necessary to provide the properties that should change.
*/
followCircleStyle: {},
followMarkerStyle: {
// color: '#FFA500',
// fillColor: '#FFB000'
},
/** The CSS class for the icon. For example fa-location-arrow or fa-map-marker */
icon: 'fa fa-map-marker',
iconLoading: 'fa fa-spinner fa-spin',
/** The element to be created for icons. For example span or i */
iconElementTag: 'span',
/** Padding around the accuracy circle. */
circlePadding: [0, 0],
/** Use metric units. */
metric: true,
/**
* This callback can be used in case you would like to override button creation behavior.
* This is useful for DOM manipulation frameworks such as angular etc.
* This function should return an object with HtmlElement for the button (link property) and the icon (icon property).
*/
createButtonCallback: function (container, options) {
var link = L.DomUtil.create('a', 'leaflet-bar-part leaflet-bar-part-single', container);
link.title = options.strings.title;
var icon = L.DomUtil.create(options.iconElementTag, options.icon, link);
return { link: link, icon: icon };
},
/** This event is called in case of any location error that is not a time out error. */
onLocationError: function(err, control) {
alert(err.message);
},
/**
* This even is called when the user's location is outside the bounds set on the map.
* The event is called repeatedly when the location changes.
*/
onLocationOutsideMapBounds: function(control) {
control.stop();
alert(control.options.strings.outsideMapBoundsMsg);
},
/** Display a pop-up when the user click on the inner marker. */
showPopup: true,
strings: {
title: "Show me where I am",
metersUnit: "meters",
feetUnit: "feet",
popup: "You are within {distance} {unit} from this point",
outsideMapBoundsMsg: "You seem located outside the boundaries of the map"
},
/** The default options passed to leaflets locate method. */
locateOptions: {
maxZoom: Infinity,
watch: true, // if you overwrite this, visualization cannot be updated
setView: false // have to set this to false because we have to
// do setView manually
}
},

initialize: function (options) {
// set default options if nothing is set (merge one step deep)
for (var i in options) {
if (typeof this.options[i] === 'object') {
L.extend(this.options[i], options[i]);
} else {
this.options[i] = options[i];
}
}

// extend the follow marker style and circle from the normal style
this.options.followMarkerStyle = L.extend({}, this.options.markerStyle, this.options.followMarkerStyle);
this.options.followCircleStyle = L.extend({}, this.options.circleStyle, this.options.followCircleStyle);
},

/**
* Add control to map. Returns the container for the control.
*/
onAdd: function (map) {
var container = L.DomUtil.create('div',
'leaflet-control-locate leaflet-bar leaflet-control');

this._layer = this.options.layer || new L.LayerGroup();
this._layer.addTo(map);
this._event = undefined;
this._prevBounds = null;

var linkAndIcon = this.options.createButtonCallback(container, this.options);
this._link = linkAndIcon.link;
this._icon = linkAndIcon.icon;

L.DomEvent
.on(this._link, 'click', L.DomEvent.stopPropagation)
.on(this._link, 'click', L.DomEvent.preventDefault)
.on(this._link, 'click', this._onClick, this)
.on(this._link, 'dblclick', L.DomEvent.stopPropagation);

this._resetVariables();

this._map.on('unload', this._unload, this);

return container;
},

/**
* This method is called when the user clicks on the control.
*/
_onClick: function() {
this._justClicked = true;
this._userPanned = false;

if (this._active && !this._event) {
// click while requesting
this.stop();
} else if (this._active && this._event !== undefined) {
var behavior = this._map.getBounds().contains(this._event.latlng) ?
this.options.clickBehavior.inView : this.options.clickBehavior.outOfView;
switch (behavior) {
case 'setView':
this.setView();
break;
case 'stop':
this.stop();
if (this.options.returnToPrevBounds) {
var f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
f.bind(this._map)(this._prevBounds);
}
break;
}
} else {
if (this.options.returnToPrevBounds) {
this._prevBounds = this._map.getBounds();
}
this.start();
}

this._updateContainerStyle();
},

/**
* Starts the plugin:
* - activates the engine
* - draws the marker (if coordinates available)
*/
start: function() {
this._activate();

if (this._event) {
this._drawMarker(this._map);

// if we already have a location but the user clicked on the control
if (this.options.setView) {
this.setView();
}
}
this._updateContainerStyle();
},

/**
* Stops the plugin:
* - deactivates the engine
* - reinitializes the button
* - removes the marker
*/
stop: function() {
this._deactivate();

this._cleanClasses();
this._resetVariables();

this._removeMarker();
},

/**
* This method launches the location engine.
* It is called before the marker is updated,
* event if it does not mean that the event will be ready.
*
* Override it if you want to add more functionalities.
* It should set the this._active to true and do nothing if
* this._active is true.
*/
_activate: function() {
if (!this._active) {
this._map.locate(this.options.locateOptions);
this._active = true;

// bind event listeners
this._map.on('locationfound', this._onLocationFound, this);
this._map.on('locationerror', this._onLocationError, this);
this._map.on('dragstart', this._onDrag, this);
}
},

/**
* Called to stop the location engine.
*
* Override it to shutdown any functionalities you added on start.
*/
_deactivate: function() {
this._map.stopLocate();
this._active = false;

if (!this.options.cacheLocation) {
this._event = undefined;
}

// unbind event listeners
this._map.off('locationfound', this._onLocationFound, this);
this._map.off('locationerror', this._onLocationError, this);
this._map.off('dragstart', this._onDrag, this);
},

/**
* Zoom (unless we should keep the zoom level) and an to the current view.
*/
setView: function() {
this._drawMarker();
if (this._isOutsideMapBounds()) {
this._event = undefined; // clear the current location so we can get back into the bounds
this.options.onLocationOutsideMapBounds(this);
} else {
if (this.options.keepCurrentZoomLevel) {
var f = this.options.flyTo ? this._map.flyTo : this._map.panTo;
f.bind(this._map)([this._event.latitude, this._event.longitude]);
} else {
var f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
f.bind(this._map)(this._event.bounds, {
padding: this.options.circlePadding,
maxZoom: this.options.locateOptions.maxZoom
});
}
}
},

/**
* Draw the marker and accuracy circle on the map.
*
* Uses the event retrieved from onLocationFound from the map.
*/
_drawMarker: function() {
if (this._event.accuracy === undefined) {
this._event.accuracy = 0;
}

var radius = this._event.accuracy;
var latlng = this._event.latlng;

// circle with the radius of the location's accuracy
if (this.options.drawCircle) {
var style = this._isFollowing() ? this.options.followCircleStyle : this.options.circleStyle;

if (!this._circle) {
this._circle = L.circle(latlng, radius, style).addTo(this._layer);
} else {
this._circle.setLatLng(latlng).setRadius(radius).setStyle(style);
}
}

var distance, unit;
if (this.options.metric) {
distance = radius.toFixed(0);
unit = this.options.strings.metersUnit;
} else {
distance = (radius * 3.2808399).toFixed(0);
unit = this.options.strings.feetUnit;
}

// small inner marker
if (this.options.drawMarker) {
var mStyle = this._isFollowing() ? this.options.followMarkerStyle : this.options.markerStyle;
if (!this._marker) {
this._marker = new this.options.markerClass(latlng, mStyle).addTo(this._layer);
} else {
this._marker.setLatLng(latlng);
// If the markerClass can be updated with setStyle, update it.
if (this._marker.setStyle) {
this._marker.setStyle(mStyle);
}
}
}

var t = this.options.strings.popup;
if (this.options.showPopup && t && this._marker) {
this._marker
.bindPopup(L.Util.template(t, {distance: distance, unit: unit}))
._popup.setLatLng(latlng);
}
},

/**
* Remove the marker from map.
*/
_removeMarker: function() {
this._layer.clearLayers();
this._marker = undefined;
this._circle = undefined;
},

/**
* Unload the plugin and all event listeners.
* Kind of the opposite of onAdd.
*/
_unload: function() {
this.stop();
this._map.off('unload', this._unload, this);
},

/**
* Calls deactivate and dispatches an error.
*/
_onLocationError: function(err) {
// ignore time out error if the location is watched
if (err.code == 3 && this.options.locateOptions.watch) {
return;
}

this.stop();
this.options.onLocationError(err, this);
},

/**
* Stores the received event and updates the marker.
*/
_onLocationFound: function(e) {
// no need to do anything if the location has not changed
if (this._event &&
(this._event.latlng.lat === e.latlng.lat &&
this._event.latlng.lng === e.latlng.lng &&
this._event.accuracy === e.accuracy)) {
return;
}

if (!this._active) {
// we may have a stray event
return;
}

this._event = e;

this._drawMarker();
this._updateContainerStyle();

switch (this.options.setView) {
case 'once':
if (this._justClicked) {
this.setView();
}
break;
case 'untilPan':
if (!this._userPanned) {
this.setView();
}
break;
case 'always':
this.setView();
break;
case false:
// don't set the view
break;
}

this._justClicked = false;
},

/**
* When the user drags. Need a separate even so we can bind and unbind even listeners.
*/
_onDrag: function() {
// only react to drags once we have a location
if (this._event) {
this._userPanned = true;
this._updateContainerStyle();
this._drawMarker();
}
},

/**
* Compute whether the map is following the user location with pan and zoom.
*/
_isFollowing: function() {
if (!this._active) {
return false;
}

if (this.options.setView === 'always') {
return true;
} else if (this.options.setView === 'untilPan') {
return !this._userPanned;
}
},

/**
* Check if location is in map bounds
*/
_isOutsideMapBounds: function() {
if (this._event === undefined) {
return false;
}
return this._map.options.maxBounds &&
!this._map.options.maxBounds.contains(this._event.latlng);
},

/**
* Toggles button class between following and active.
*/
_updateContainerStyle: function() {
if (!this._container) {
return;
}

if (this._active && !this._event) {
// active but don't have a location yet
this._setClasses('requesting');
} else if (this._isFollowing()) {
this._setClasses('following');
} else if (this._active) {
this._setClasses('active');
} else {
this._cleanClasses();
}
},

/**
* Sets the CSS classes for the state.
*/
_setClasses: function(state) {
if (state == 'requesting') {
removeClasses(this._container, "active following");
addClasses(this._container, "requesting");

removeClasses(this._icon, this.options.icon);
addClasses(this._icon, this.options.iconLoading);
} else if (state == 'active') {
removeClasses(this._container, "requesting following");
addClasses(this._container, "active");

removeClasses(this._icon, this.options.iconLoading);
addClasses(this._icon, this.options.icon);
} else if (state == 'following') {
removeClasses(this._container, "requesting");
addClasses(this._container, "active following");

removeClasses(this._icon, this.options.iconLoading);
addClasses(this._icon, this.options.icon);
}
},

/**
* Removes all classes from button.
*/
_cleanClasses: function() {
L.DomUtil.removeClass(this._container, "requesting");
L.DomUtil.removeClass(this._container, "active");
L.DomUtil.removeClass(this._container, "following");

removeClasses(this._icon, this.options.iconLoading);
addClasses(this._icon, this.options.icon);
},

/**
* Reinitializes state variables.
*/
_resetVariables: function() {
// whether locate is active or not
this._active = false;

// true if the control was clicked for the first time
// we need this so we can pan and zoom once we have the location
this._justClicked = false;

// true if the user has panned the map after clicking the control
this._userPanned = false;
}
});

L.control.locate = function (options) {
return new L.Control.Locate(options);
};

return LocateControl;
}, window));

+ 56
- 0
frappe/public/js/lib/leaflet/easy-button.css 查看文件

@@ -0,0 +1,56 @@
.leaflet-bar button,
.leaflet-bar button:hover {
background-color: #fff;
border: none;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}

.leaflet-bar button {
background-position: 50% 50%;
background-repeat: no-repeat;
overflow: hidden;
display: block;
}

.leaflet-bar button:hover {
background-color: #f4f4f4;
}

.leaflet-bar button:first-of-type {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}

.leaflet-bar button:last-of-type {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}

.leaflet-bar.disabled,
.leaflet-bar button.disabled {
cursor: default;
pointer-events: none;
opacity: .4;
}

.easy-button-button .button-state{
display: block;
width: 100%;
height: 100%;
position: relative;
}


.leaflet-touch .leaflet-bar button {
width: 30px;
height: 30px;
line-height: 30px;
}

+ 370
- 0
frappe/public/js/lib/leaflet/easy-button.js 查看文件

@@ -0,0 +1,370 @@
(function(){

// This is for grouping buttons into a bar
// takes an array of `L.easyButton`s and
// then the usual `.addTo(map)`
L.Control.EasyBar = L.Control.extend({

options: {
position: 'topleft', // part of leaflet's defaults
id: null, // an id to tag the Bar with
leafletClasses: true // use leaflet classes?
},


initialize: function(buttons, options){

if(options){
L.Util.setOptions( this, options );
}

this._buildContainer();
this._buttons = [];

for(var i = 0; i < buttons.length; i++){
buttons[i]._bar = this;
buttons[i]._container = buttons[i].button;
this._buttons.push(buttons[i]);
this.container.appendChild(buttons[i].button);
}

},


_buildContainer: function(){
this._container = this.container = L.DomUtil.create('div', '');
this.options.leafletClasses && L.DomUtil.addClass(this.container, 'leaflet-bar easy-button-container leaflet-control');
this.options.id && (this.container.id = this.options.id);
},


enable: function(){
L.DomUtil.addClass(this.container, 'enabled');
L.DomUtil.removeClass(this.container, 'disabled');
this.container.setAttribute('aria-hidden', 'false');
return this;
},


disable: function(){
L.DomUtil.addClass(this.container, 'disabled');
L.DomUtil.removeClass(this.container, 'enabled');
this.container.setAttribute('aria-hidden', 'true');
return this;
},


onAdd: function () {
return this.container;
},

addTo: function (map) {
this._map = map;

for(var i = 0; i < this._buttons.length; i++){
this._buttons[i]._map = map;
}

var container = this._container = this.onAdd(map),
pos = this.getPosition(),
corner = map._controlCorners[pos];

L.DomUtil.addClass(container, 'leaflet-control');

if (pos.indexOf('bottom') !== -1) {
corner.insertBefore(container, corner.firstChild);
} else {
corner.appendChild(container);
}

return this;
}

});

L.easyBar = function(){
var args = [L.Control.EasyBar];
for(var i = 0; i < arguments.length; i++){
args.push( arguments[i] );
}
return new (Function.prototype.bind.apply(L.Control.EasyBar, args));
};

// L.EasyButton is the actual buttons
// can be called without being grouped into a bar
L.Control.EasyButton = L.Control.extend({

options: {
position: 'topleft', // part of leaflet's defaults

id: null, // an id to tag the button with

type: 'replace', // [(replace|animate)]
// replace swaps out elements
// animate changes classes with all elements inserted

states: [], // state names look like this
// {
// stateName: 'untracked',
// onClick: function(){ handle_nav_manually(); };
// title: 'click to make inactive',
// icon: 'fa-circle', // wrapped with <a>
// }

leafletClasses: true, // use leaflet styles for the button
tagName: 'button',
},



initialize: function(icon, onClick, title, id){

// clear the states manually
this.options.states = [];

// add id to options
if(id != null){
this.options.id = id;
}

// storage between state functions
this.storage = {};

// is the last item an object?
if( typeof arguments[arguments.length-1] === 'object' ){

// if so, it should be the options
L.Util.setOptions( this, arguments[arguments.length-1] );
}

// if there aren't any states in options
// use the early params
if( this.options.states.length === 0 &&
typeof icon === 'string' &&
typeof onClick === 'function'){

// turn the options object into a state
this.options.states.push({
icon: icon,
onClick: onClick,
title: typeof title === 'string' ? title : ''
});
}

// curate and move user's states into
// the _states for internal use
this._states = [];

for(var i = 0; i < this.options.states.length; i++){
this._states.push( new State(this.options.states[i], this) );
}

this._buildButton();

this._activateState(this._states[0]);

},

_buildButton: function(){

this.button = L.DomUtil.create(this.options.tagName, '');

if (this.options.tagName === 'button') {
this.button.setAttribute('type', 'button');
}

if (this.options.id ){
this.button.id = this.options.id;
}

if (this.options.leafletClasses){
L.DomUtil.addClass(this.button, 'easy-button-button leaflet-bar-part leaflet-interactive');
}

// don't let double clicks and mousedown get to the map
L.DomEvent.addListener(this.button, 'dblclick', L.DomEvent.stop);
L.DomEvent.addListener(this.button, 'mousedown', L.DomEvent.stop);

// take care of normal clicks
L.DomEvent.addListener(this.button,'click', function(e){
L.DomEvent.stop(e);
this._currentState.onClick(this, this._map ? this._map : null );
this._map && this._map.getContainer().focus();
}, this);

// prep the contents of the control
if(this.options.type == 'replace'){
this.button.appendChild(this._currentState.icon);
} else {
for(var i=0;i<this._states.length;i++){
this.button.appendChild(this._states[i].icon);
}
}
},


_currentState: {
// placeholder content
stateName: 'unnamed',
icon: (function(){ return document.createElement('span'); })()
},



_states: null, // populated on init



state: function(newState){

// activate by name
if(typeof newState == 'string'){

this._activateStateNamed(newState);

// activate by index
} else if (typeof newState == 'number'){

this._activateState(this._states[newState]);
}

return this;
},


_activateStateNamed: function(stateName){
for(var i = 0; i < this._states.length; i++){
if( this._states[i].stateName == stateName ){
this._activateState( this._states[i] );
}
}
},

_activateState: function(newState){

if( newState === this._currentState ){

// don't touch the dom if it'll just be the same after
return;

} else {

// swap out elements... if you're into that kind of thing
if( this.options.type == 'replace' ){
this.button.appendChild(newState.icon);
this.button.removeChild(this._currentState.icon);
}

if( newState.title ){
this.button.title = newState.title;
} else {
this.button.removeAttribute('title');
}

// update classes for animations
for(var i=0;i<this._states.length;i++){
L.DomUtil.removeClass(this._states[i].icon, this._currentState.stateName + '-active');
L.DomUtil.addClass(this._states[i].icon, newState.stateName + '-active');
}

// update classes for animations
L.DomUtil.removeClass(this.button, this._currentState.stateName + '-active');
L.DomUtil.addClass(this.button, newState.stateName + '-active');

// update the record
this._currentState = newState;

}
},

enable: function(){
L.DomUtil.addClass(this.button, 'enabled');
L.DomUtil.removeClass(this.button, 'disabled');
this.button.setAttribute('aria-hidden', 'false');
return this;
},

disable: function(){
L.DomUtil.addClass(this.button, 'disabled');
L.DomUtil.removeClass(this.button, 'enabled');
this.button.setAttribute('aria-hidden', 'true');
return this;
},

onAdd: function(map){
var bar = L.easyBar([this], {
position: this.options.position,
leafletClasses: this.options.leafletClasses
});
this._anonymousBar = bar;
this._container = bar.container;
return this._anonymousBar.container;
},

removeFrom: function (map) {
if (this._map === map)
this.remove();
return this;
},

});

L.easyButton = function(/* args will pass automatically */){
var args = Array.prototype.concat.apply([L.Control.EasyButton],arguments);
return new (Function.prototype.bind.apply(L.Control.EasyButton, args));
};

/*************************
*
* util functions
*
*************************/

// constructor for states so only curated
// states end up getting called
function State(template, easyButton){

this.title = template.title;
this.stateName = template.stateName ? template.stateName : 'unnamed-state';

// build the wrapper
this.icon = L.DomUtil.create('span', '');

L.DomUtil.addClass(this.icon, 'button-state state-' + this.stateName.replace(/(^\s*|\s*$)/g,''));
this.icon.innerHTML = buildIcon(template.icon);
this.onClick = L.Util.bind(template.onClick?template.onClick:function(){}, easyButton);
}

function buildIcon(ambiguousIconString) {

var tmpIcon;

// does this look like html? (i.e. not a class)
if( ambiguousIconString.match(/[&;=<>"']/) ){

// if so, the user should have put in html
// so move forward as such
tmpIcon = ambiguousIconString;

// then it wasn't html, so
// it's a class list, figure out what kind
} else {
ambiguousIconString = ambiguousIconString.replace(/(^\s*|\s*$)/g,'');
tmpIcon = L.DomUtil.create('span', '');

if( ambiguousIconString.indexOf('fa-') === 0 ){
L.DomUtil.addClass(tmpIcon, 'fa ' + ambiguousIconString)
} else if ( ambiguousIconString.indexOf('glyphicon-') === 0 ) {
L.DomUtil.addClass(tmpIcon, 'glyphicon ' + ambiguousIconString)
} else {
L.DomUtil.addClass(tmpIcon, /*rollwithit*/ ambiguousIconString)
}

// make this a string so that it's easy to set innerHTML below
tmpIcon = tmpIcon.outerHTML;
}

return tmpIcon;
}

})();

+ 632
- 0
frappe/public/js/lib/leaflet/leaflet.css 查看文件

@@ -0,0 +1,632 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg,
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer {
max-width: none !important; /* csslint allow: important */
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-tile {
will-change: opacity;
}
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
-o-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
-o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
-o-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline: 0;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a,
.leaflet-bar a:hover {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(/assets/frappe/images/leaflet/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(/assets/frappe/images/leaflet/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path {
background-image: url(/assets/frappe/images/leaflet/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.7);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover {
text-decoration: underline;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font-size: 11px;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 19px;
line-height: 1.4;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
border: none;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #999;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-clickable {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}

+ 10
- 0
frappe/public/js/lib/leaflet/leaflet.draw.css 查看文件

@@ -0,0 +1,10 @@
.leaflet-draw-section{position:relative}.leaflet-draw-toolbar{margin-top:12px}.leaflet-draw-toolbar-top{margin-top:0}.leaflet-draw-toolbar-notop a:first-child{border-top-right-radius:0}.leaflet-draw-toolbar-nobottom a:last-child{border-bottom-right-radius:0}.leaflet-draw-toolbar a{background-image:url('/assets/frappe/images/leaflet/spritesheet.png');background-image:linear-gradient(transparent,transparent),url('/assets/frappe/images/leaflet/spritesheet.svg');background-repeat:no-repeat;background-size:300px 30px;background-clip:padding-box}.leaflet-retina .leaflet-draw-toolbar a{background-image:url('/assets/frappe/images/leaflet/spritesheet-2x.png');background-image:linear-gradient(transparent,transparent),url('/assets/frappe/images/leaflet/spritesheet.svg')}
.leaflet-draw a{display:block;text-align:center;text-decoration:none}.leaflet-draw a .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.leaflet-draw-actions{display:none;list-style:none;margin:0;padding:0;position:absolute;left:26px;top:0;white-space:nowrap}.leaflet-touch .leaflet-draw-actions{left:32px}.leaflet-right .leaflet-draw-actions{right:26px;left:auto}.leaflet-touch .leaflet-right .leaflet-draw-actions{right:32px;left:auto}.leaflet-draw-actions li{display:inline-block}
.leaflet-draw-actions li:first-child a{border-left:0}.leaflet-draw-actions li:last-child a{-webkit-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.leaflet-right .leaflet-draw-actions li:last-child a{-webkit-border-radius:0;border-radius:0}.leaflet-right .leaflet-draw-actions li:first-child a{-webkit-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.leaflet-draw-actions a{background-color:#919187;border-left:1px solid #AAA;color:#FFF;font:11px/19px "Helvetica Neue",Arial,Helvetica,sans-serif;line-height:28px;text-decoration:none;padding-left:10px;padding-right:10px;height:28px}
.leaflet-touch .leaflet-draw-actions a{font-size:12px;line-height:30px;height:30px}.leaflet-draw-actions-bottom{margin-top:0}.leaflet-draw-actions-top{margin-top:1px}.leaflet-draw-actions-top a,.leaflet-draw-actions-bottom a{height:27px;line-height:27px}.leaflet-draw-actions a:hover{background-color:#a0a098}.leaflet-draw-actions-top.leaflet-draw-actions-bottom a{height:26px;line-height:26px}.leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:-2px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:0 -1px}
.leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-31px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-29px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-62px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-60px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-92px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-90px -1px}
.leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-122px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-120px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-273px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-271px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-152px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-150px -1px}
.leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-182px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-180px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-212px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-210px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-242px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-240px -2px}
.leaflet-mouse-marker{background-color:#fff;cursor:crosshair}.leaflet-draw-tooltip{background:#363636;background:rgba(0,0,0,0.5);border:1px solid transparent;-webkit-border-radius:4px;border-radius:4px;color:#fff;font:12px/18px "Helvetica Neue",Arial,Helvetica,sans-serif;margin-left:20px;margin-top:-21px;padding:4px 8px;position:absolute;visibility:hidden;white-space:nowrap;z-index:6}.leaflet-draw-tooltip:before{border-right:6px solid black;border-right-color:rgba(0,0,0,0.5);border-top:6px solid transparent;border-bottom:6px solid transparent;content:"";position:absolute;top:7px;left:-7px}
.leaflet-error-draw-tooltip{background-color:#f2dede;border:1px solid #e6b6bd;color:#b94a48}.leaflet-error-draw-tooltip:before{border-right-color:#e6b6bd}.leaflet-draw-tooltip-single{margin-top:-12px}.leaflet-draw-tooltip-subtext{color:#f8d5e4}.leaflet-draw-guide-dash{font-size:1%;opacity:.6;position:absolute;width:5px;height:5px}.leaflet-edit-marker-selected{background-color:rgba(254,87,161,0.1);border:4px dashed rgba(254,87,161,0.6);-webkit-border-radius:4px;border-radius:4px;box-sizing:content-box}
.leaflet-edit-move{cursor:move}.leaflet-edit-resize{cursor:pointer}.leaflet-oldie .leaflet-draw-toolbar{border:1px solid #999}

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

Loading…
取消
儲存