@@ -119,6 +119,8 @@ | |||
"getCookies": true, | |||
"get_url_arg": true, | |||
"QUnit": true, | |||
"JsBarcode": true | |||
"JsBarcode": true, | |||
"L": true, | |||
"Chart": true | |||
} | |||
} |
@@ -56,4 +56,4 @@ script: | |||
- set -e | |||
- bench run-tests | |||
- sleep 5 | |||
- bench run-ui-tests --app frappe | |||
- bench run-ui-tests --app frappe |
@@ -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 | |||
@@ -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) | |||
@@ -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: | |||
@@ -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; | |||
} | |||
@@ -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: | |||
@@ -17,6 +17,11 @@ def get_data(): | |||
"type": "doctype", | |||
"name": "Role", | |||
"description": _("User Roles") | |||
}, | |||
{ | |||
"type": "doctype", | |||
"name": "Role Profile", | |||
"description": _("Role Profile") | |||
} | |||
] | |||
}, | |||
@@ -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, | |||
@@ -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': | |||
@@ -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) | |||
@@ -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)) | |||
@@ -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", | |||
@@ -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 | |||
@@ -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", | |||
@@ -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 +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(); | |||
} | |||
} | |||
}); |
@@ -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 | |||
} |
@@ -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) |
@@ -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() | |||
]); | |||
}); |
@@ -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, []) |
@@ -20,4 +20,4 @@ QUnit.test("test: User", function (assert) { | |||
() => done() | |||
]); | |||
}); | |||
}); |
@@ -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() | |||
]); | |||
}); |
@@ -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) { | |||
@@ -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", | |||
@@ -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) |
@@ -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", | |||
@@ -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() | |||
]); | |||
}); |
@@ -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); | |||
} | |||
} | |||
@@ -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() |
@@ -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", | |||
@@ -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 | |||
@@ -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 +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(); | |||
}); | |||
} | |||
}); |
@@ -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 | |||
} |
@@ -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 |
@@ -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", | |||
@@ -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' | |||
@@ -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 | |||
@@ -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", | |||
@@ -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 | |||
@@ -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(), | |||
} | |||
@@ -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() | |||
@@ -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 | |||
@@ -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.") | |||
@@ -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", | |||
@@ -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" | |||
@@ -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 | |||
@@ -316,7 +316,6 @@ class BaseDocument(object): | |||
raise | |||
else: | |||
raise | |||
self.set("__islocal", False) | |||
def db_update(self): | |||
@@ -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 |
@@ -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 | |||
@@ -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 |
@@ -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 | |||
@@ -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", | |||
@@ -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; | |||
} |
@@ -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; | |||
@@ -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> |
@@ -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) => { | |||
@@ -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')) { | |||
@@ -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%'); | |||
@@ -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: '© <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); | |||
}); | |||
} | |||
}); |
@@ -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)); | |||
} | |||
}); |
@@ -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(); | |||
} | |||
@@ -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)) { | |||
@@ -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(); | |||
@@ -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"; | |||
@@ -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, | |||
@@ -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") | |||
@@ -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; | |||
} | |||
@@ -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"]') | |||
@@ -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 = { | |||
@@ -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 { | |||
@@ -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); | |||
}); | |||
@@ -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); | |||
}); | |||
} | |||
}); | |||
}); |
@@ -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> |
@@ -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, | |||
@@ -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); | |||
} | |||
}, | |||
@@ -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"); | |||
@@ -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; | |||
} | |||
@@ -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={}}; |
@@ -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; | |||
} |
@@ -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)); |
@@ -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; | |||
} |
@@ -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; | |||
} | |||
})(); |
@@ -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; | |||
} |
@@ -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} |