Merge branch 'develop' of https://github.com/frappe/frappe into develop

This commit is contained in:
barredterra 2020-08-17 13:27:47 +02:00
commit 58927e58eb
175 changed files with 3449 additions and 2024 deletions

5
.gitignore vendored
View file

@ -188,4 +188,7 @@ typings/
# cypress
cypress/screenshots
cypress/videos
cypress/videos
# JetBrains IDEs
.idea/

View file

@ -31,12 +31,12 @@ matrix:
- name: "Python 3.7 MariaDB"
python: 3.7
env: DB=mariadb TYPE=server
script: bench --site test_site run-tests --coverage
script: bench --verbose --site test_site run-tests --coverage
- name: "Python 3.7 PostgreSQL"
python: 3.7
env: DB=postgres TYPE=server
script: bench --site test_site run-tests --coverage
script: bench --verbose --site test_site run-tests --coverage
- name: "Cypress"
python: 3.7

View file

@ -2,6 +2,7 @@
"db_host": "localhost",
"db_name": "test_frappe_consumer",
"db_password": "test_frappe",
"allow_tests": true,
"db_type": "mariadb",
"auto_email_id": "test@example.com",
"mail_server": "smtp.example.com",

View file

@ -3,6 +3,7 @@
"db_name": "test_frappe_consumer",
"db_password": "test_frappe",
"db_type": "postgres",
"allow_tests": true,
"auto_email_id": "test@example.com",
"mail_server": "smtp.example.com",
"mail_login": "test@example.com",

View file

@ -2,6 +2,7 @@
"db_host": "localhost",
"db_name": "test_frappe_producer",
"db_password": "test_frappe",
"allow_tests": true,
"db_type": "mariadb",
"auto_email_id": "test@example.com",
"mail_server": "smtp.example.com",

View file

@ -3,6 +3,7 @@
"db_name": "test_frappe_producer",
"db_password": "test_frappe",
"db_type": "postgres",
"allow_tests": true,
"auto_email_id": "test@example.com",
"mail_server": "smtp.example.com",
"mail_login": "test@example.com",

View file

@ -153,6 +153,7 @@ def init(site, sites_path=None, new_site=False):
local.site = site
local.sites_path = sites_path
local.site_path = os.path.join(sites_path, site)
local.all_apps = None
local.request_ip = None
local.response = _dict({"docs":[]})
@ -231,8 +232,7 @@ def get_site_config(sites_path=None, site_path=None):
if os.path.exists(site_config):
config.update(get_file_json(site_config))
elif local.site and not local.flags.new_site:
print("Site {0} does not exist".format(local.site))
sys.exit(1)
raise IncorrectSitePath("{0} does not exist".format(local.site))
return _dict(config)
@ -267,7 +267,7 @@ def destroy():
# memcache
redis_server = None
def cache():
"""Returns memcache connection."""
"""Returns redis connection."""
global redis_server
if not redis_server:
from frappe.utils.redis_wrapper import RedisWrapper
@ -290,6 +290,9 @@ def errprint(msg):
error_log.append({"exc": msg})
def print_sql(enable=True):
return cache().set_value('flag_print_sql', enable)
def log(msg):
"""Add to `debug_log`.
@ -300,7 +303,7 @@ def log(msg):
debug_log.append(as_unicode(msg))
def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None):
def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None):
"""Print a message to the user (via HTTP response).
Messages are sent in the `__server_messages` property in the
response JSON and shown in a pop-up / modal.
@ -310,6 +313,8 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
:param raise_exception: [optional] Raise given exception and show message.
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
:param primary_action: [optional] Bind a primary server/client side action.
:param is_minimizable: [optional] Allow users to minimize the modal
:param wide: [optional] Show wide modal
"""
from frappe.utils import encode
@ -367,6 +372,9 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
if primary_action:
out.primary_action = primary_action
if wide:
out.wide = wide
message_log.append(json.dumps(out))
if raise_exception and hasattr(raise_exception, '__name__'):
@ -388,12 +396,12 @@ def clear_last_message():
if len(local.message_log) > 0:
local.message_log = local.message_log[:-1]
def throw(msg, exc=ValidationError, title=None, is_minimizable=None):
def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None):
"""Throw execption and show message (`msgprint`).
:param msg: Message.
:param exc: Exception class. Default `frappe.ValidationError`"""
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable)
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide)
def emit_js(js, user=False, **kwargs):
if user == False:
@ -436,12 +444,8 @@ def get_roles(username=None):
"""Returns roles of current user."""
if not local.session:
return ["Guest"]
if username:
import frappe.permissions
return frappe.permissions.get_roles(username)
else:
return get_user().get_roles()
import frappe.permissions
return frappe.permissions.get_roles(username or local.session.user)
def get_request_header(key, default=None):
"""Return HTTP request header.
@ -762,7 +766,7 @@ def get_doc(*args, **kwargs):
# insert a new document
todo = frappe.get_doc({"doctype":"ToDo", "description": "test"})
tood.insert()
todo.insert()
# open an existing document
todo = frappe.get_doc("ToDo", "TD0001")
@ -921,10 +925,13 @@ def get_installed_apps(sort=False, frappe_last=False):
if not db:
connect()
if not local.all_apps:
local.all_apps = get_all_apps(True)
installed = json.loads(db.get_global("installed_apps") or "[]")
if sort:
installed = [app for app in get_all_apps(True) if app in installed]
installed = [app for app in local.all_apps if app in installed]
if frappe_last:
if 'frappe' in installed:
@ -1559,10 +1566,10 @@ def get_doctype_app(doctype):
loggers = {}
log_level = None
def logger(module=None, with_more_info=False):
def logger(module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20):
'''Returns a python logger that uses StreamHandler'''
from frappe.utils.logger import get_logger
return get_logger(module=module, with_more_info=with_more_info)
return get_logger(module=module, with_more_info=with_more_info, allow_site=allow_site, filter=filter, max_size=max_size, file_count=file_count)
def log_error(message=None, title=_("Error")):
'''Log error to Error Log'''
@ -1707,3 +1714,7 @@ def mock(type, size=1, locale='en'):
from frappe.chat.util import squashify
return squashify(results)
def validate_and_sanitize_search_inputs(fn):
from frappe.desk.search import validate_and_sanitize_search_inputs as func
return func(fn)

View file

@ -99,15 +99,16 @@ def application(request):
frappe.monitor.stop(response)
frappe.recorder.dump()
frappe.logger("frappe.web").info({
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),
"scheme": getattr(request, "scheme", "NOTFOUND"),
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger:
frappe.logger("frappe.web", allow_site=frappe.local.site).info({
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),
"scheme": getattr(request, "scheme", "NOTFOUND"),
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
if response and hasattr(frappe.local, 'rate_limiter'):
response.headers.extend(frappe.local.rate_limiter.headers())
@ -256,9 +257,11 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
'SERVER_NAME': 'localhost:8000'
}
log = logging.getLogger('werkzeug')
log.propagate = False
in_test_env = os.environ.get('CI')
if in_test_env:
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
run_simple('0.0.0.0', int(port), application,

View file

@ -18,6 +18,7 @@ from frappe.utils.password import check_password, delete_login_failed_cache
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,
confirm_otp_token, get_cached_user_pass)
from frappe.website.utils import get_home_page
from six.moves.urllib.parse import quote
@ -167,7 +168,7 @@ class LoginManager:
frappe.local.cookie_manager.set_cookie("system_user", "no")
if not resume:
frappe.local.response["message"] = "No App"
frappe.local.response["home_page"] = get_website_user_home_page(self.user)
frappe.local.response["home_page"] = '/' + get_home_page()
else:
frappe.local.cookie_manager.set_cookie("system_user", "yes")
if not resume:
@ -338,8 +339,13 @@ class CookieManager:
self.set_cookie("country", frappe.session.session_country)
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
if not secure:
if not secure and hasattr(frappe.local, 'request'):
secure = frappe.local.request.scheme == "https"
# Cordova does not work with Lax
if frappe.local.session.data.device == "mobile":
samesite = None
self.cookies[key] = {
"value": value,
"expires": expires,
@ -377,16 +383,6 @@ def clear_cookies():
frappe.session.sid = ""
frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"])
def get_website_user_home_page(user):
home_page_method = frappe.get_hooks('get_website_user_home_page')
if home_page_method:
home_page = frappe.get_attr(home_page_method[-1])(user)
return '/' + home_page.strip('/')
elif frappe.get_hooks('website_user_home_page'):
return '/' + frappe.get_hooks('website_user_home_page')[-1].strip('/')
else:
return '/me'
def get_last_tried_login_data(user, get_last_login=False):
locked_account_time = frappe.cache().hget('locked_account_time', user)
if get_last_login and locked_account_time:

View file

@ -3,7 +3,7 @@
{
"hidden": 0,
"label": "Tools",
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
},
{
"hidden": 0,
@ -29,10 +29,11 @@
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
"hide_custom": 0,
"idx": 0,
"is_standard": 1,
"label": "Tools",
"modified": "2020-04-20 18:21:14.152537",
"modified": "2020-07-21 19:32:18.480700",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",

View file

@ -9,6 +9,7 @@ from frappe.model.document import Document
from frappe.desk.form import assign_to
import frappe.cache_manager
from frappe import _
from frappe.model import log_types
class AssignmentRule(Document):
@ -19,10 +20,13 @@ class AssignmentRule(Document):
frappe.throw(_("Assignment Day {0} has been repeated.").format(frappe.bold(repeated_days)))
def on_update(self): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name)
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def after_rename(self, old, new, merge): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name)
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def on_trash(self): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def apply_unassign(self, doc, assignments):
if (self.unassign_condition and
@ -165,7 +169,13 @@ def reopen_closed_assignment(doc):
return True
def apply(doc, method=None, doctype=None, name=None):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard:
if not doctype:
doctype = doc.doctype
if (frappe.flags.in_patch
or frappe.flags.in_install
or frappe.flags.in_setup_wizard
or doctype in log_types):
return
if not doc and doctype and name:

View file

@ -374,6 +374,7 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e
# method for reference_doctype filter
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
res = frappe.db.get_all('Property Setter', {
'property': 'allow_auto_repeat',

View file

@ -7,13 +7,14 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import frappe.cache_manager
from frappe.model import log_types
class MilestoneTracker(Document):
def on_update(self):
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.name)
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type)
def on_trash(self):
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.name)
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.document_type)
def apply(self, doc):
before_save = doc.get_doc_before_save()
@ -32,8 +33,15 @@ class MilestoneTracker(Document):
def evaluate_milestone(doc, event):
if (frappe.flags.in_install
or frappe.flags.in_migrate
or frappe.flags.in_setup_wizard):
or frappe.flags.in_setup_wizard
or doc.doctype in log_types):
return
for d in frappe.cache_manager.get_doctype_map('Milestone Tracker', doc.doctype,
dict(document_type = doc.doctype, disabled=0)):
frappe.get_doc('Milestone Tracker', d.name).apply(doc)
# track milestones related to this doctype
for d in get_milestone_trackers(doc.doctype):
frappe.get_doc('Milestone Tracker', d.get('name')).apply(doc)
def get_milestone_trackers(doctype):
return frappe.cache_manager.get_doctype_map('Milestone Tracker', doctype,
dict(document_type = doctype, disabled=0))

View file

@ -4,12 +4,16 @@
from __future__ import unicode_literals
import frappe
import frappe.cache_manager
import unittest
class TestMilestoneTracker(unittest.TestCase):
def test_milestone(self):
frappe.db.sql('delete from `tabMilestone Tracker`')
frappe.get_doc(dict(
frappe.cache().delete_key('milestone_tracker_map')
milestone_tracker = frappe.get_doc(dict(
doctype = 'Milestone Tracker',
document_type = 'ToDo',
track_field = 'status'
@ -17,7 +21,8 @@ class TestMilestoneTracker(unittest.TestCase):
todo = frappe.get_doc(dict(
doctype = 'ToDo',
description = 'test milestone'
description = 'test milestone',
status = 'Open'
)).insert()
milestones = frappe.get_all('Milestone',
@ -40,3 +45,6 @@ class TestMilestoneTracker(unittest.TestCase):
self.assertEqual(milestones[0].track_field, 'status')
self.assertEqual(milestones[0].value, 'Closed')
# cleanup
frappe.db.sql('delete from tabMilestone')
milestone_tracker.delete()

View file

@ -21,6 +21,7 @@ from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabl
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.model.base_document import get_controller
from frappe.social.doctype.post.post import frequently_visited_links
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings
def get_bootinfo():
"""build and return boot info"""
@ -59,6 +60,7 @@ def get_bootinfo():
load_print(bootinfo, doclist)
doclist.extend(get_meta_bundle("Page"))
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
bootinfo.navbar_settings = get_navbar_settings()
# ipinfo
if frappe.session.data.get('ipinfo'):

View file

@ -11,12 +11,15 @@ from frappe.desk.notifications import (delete_notification_count_for,
common_default_keys = ["__default", "__global"]
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map',
'milestone_tracker_map', 'event_consumer_document_type_map')
global_cache_keys = ("app_hooks", "installed_apps",
"app_modules", "module_app", "system_settings",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
'sitemap_routes', 'db_tables')
'sitemap_routes', 'db_tables') + doctype_map_keys
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",
@ -24,8 +27,8 @@ user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"has_role:Page", "has_role:Report", "desk_sidebar_items")
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
"linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map', 'data_import_column_header_map')
"linked_doctypes", 'notifications', 'workflow' ,
'data_import_column_header_map') + doctype_map_keys
def clear_user_cache(user=None):
cache = frappe.cache()
@ -102,19 +105,19 @@ def clear_doctype_cache(doctype=None):
# Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured
clear_document_cache()
def get_doctype_map(doctype, name, filters, order_by=None):
def get_doctype_map(doctype, name, filters=None, order_by=None):
cache = frappe.cache()
cache_key = frappe.scrub(doctype) + '_map'
doctype_map = cache.hget(cache_key, name)
if doctype_map:
if doctype_map is not None:
# cached, return
items = json.loads(doctype_map)
else:
# non cached, build cache
try:
items = frappe.get_all(doctype, filters=filters, order_by = order_by)
cache.hset(cache_key, doctype, json.dumps(items))
cache.hset(cache_key, name, json.dumps(items))
except frappe.db.TableMissingError:
# executed from inside patch, ignore
items = []
@ -122,8 +125,7 @@ def get_doctype_map(doctype, name, filters, order_by=None):
return items
def clear_doctype_map(doctype, name):
cache_key = frappe.scrub(doctype) + '_map'
frappe.cache().hdel(cache_key, name)
frappe.cache().hdel(frappe.scrub(doctype) + '_map', name)
def build_table_count_cache():
if (frappe.flags.in_patch

View file

@ -274,8 +274,9 @@ def disable_user(context, email):
@click.command('migrate')
@click.option('--rebuild-website', help="Rebuild webpages after migration")
@click.option('--skip-failing', is_flag=True, help="Skip patches that fail to run")
@click.option('--skip-search-index', is_flag=True, help="Skip search indexing for web documents")
@pass_context
def migrate(context, rebuild_website=False, skip_failing=False):
def migrate(context, rebuild_website=False, skip_failing=False, skip_search_index=False):
"Run patches, sync schema and rebuild files/translations"
from frappe.migrate import migrate
@ -284,13 +285,18 @@ def migrate(context, rebuild_website=False, skip_failing=False):
frappe.init(site=site)
frappe.connect()
try:
migrate(context.verbose, rebuild_website=rebuild_website, skip_failing=skip_failing)
migrate(
context.verbose,
rebuild_website=rebuild_website,
skip_failing=skip_failing,
skip_search_index=skip_search_index
)
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
print("Compiling Python Files...")
print("Compiling Python files...")
compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
@click.command('migrate-to')
@ -655,6 +661,22 @@ def start_ngrok(context):
frappe.destroy()
ngrok.kill()
@click.command('build-search-index')
@pass_context
def build_search_index(context):
from frappe.search.website_search import build_index_for_all_routes
site = get_site(context)
if not site:
raise SiteNotSpecifiedError
print('Building search index for {}'.format(site))
frappe.init(site=site)
frappe.connect()
try:
build_index_for_all_routes()
finally:
frappe.destroy()
commands = [
add_system_manager,
backup,
@ -680,5 +702,6 @@ commands = [
start_recording,
stop_recording,
add_to_hosts,
start_ngrok
start_ngrok,
build_search_index
]

View file

@ -528,7 +528,6 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
@pass_context
def run_ui_tests(context, app, headless=False):
"Run UI tests"
site = get_site(context)
app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..'))
site_url = frappe.utils.get_site_url(site)

View file

@ -147,6 +147,7 @@ def delete_contact_and_address(doctype, docname):
doc.delete()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters):
if not txt: txt = ""

View file

@ -231,6 +231,7 @@ def get_company_address(company):
return ret
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def address_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond

View file

@ -183,6 +183,7 @@ def update_contact(doc, method):
contact.save(ignore_permissions=True)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def contact_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond

View file

@ -317,6 +317,7 @@ frappe.ui.form.on('Data Import', {
},
show_import_warnings(frm, preview_data) {
let columns = preview_data.columns;
let warnings = JSON.parse(frm.doc.template_warnings || '[]');
warnings = warnings.concat(preview_data.warnings || []);
@ -367,11 +368,13 @@ frappe.ui.form.on('Data Import', {
.map(warning => {
let header = '';
if (warning.col) {
header = __('Column {0}', [warning.col]);
let column_number = `<span class="text-uppercase">${__('Column {0}', [warning.col])}</span>`;
let column_header = columns[warning.col].header_title;
header = `${column_number} (${column_header})`;
}
return `
<div class="warning" data-col="${warning.col}">
<h5 class="text-uppercase">${header}</h5>
<h5>${header}</h5>
<div class="body">${warning.message}</div>
</div>
`;

View file

@ -17,6 +17,7 @@ frappe.listview_settings['Data Import'] = {
get_indicator: function(doc) {
var colors = {
'Pending': 'orange',
'Not Started': 'orange',
'Partial Success': 'orange',
'Success': 'green',
'In Progress': 'orange',
@ -26,6 +27,9 @@ frappe.listview_settings['Data Import'] = {
if (imports_in_progress.includes(doc.name)) {
status = 'In Progress';
}
if (status == 'Pending') {
status = 'Not Started';
}
return [__(status), colors[status], 'status,=,' + doc.status];
},
formatters: {

View file

@ -7,7 +7,7 @@ import io
import frappe
import timeit
import json
from datetime import datetime
from datetime import datetime, date
from frappe import _
from frappe.utils import cint, flt, update_progress_bar, cstr
from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets
@ -233,7 +233,7 @@ class Importer:
return updated_doc
else:
# throw if no changes
frappe.throw('No changes to update')
frappe.throw("No changes to update")
def get_eta(self, current, total, processing_time):
self.last_eta = getattr(self, "last_eta", 0)
@ -322,7 +322,7 @@ class ImportFile:
if isinstance(file, frappe.string_types):
if frappe.db.exists("File", {"file_url": file}):
self.file_doc = frappe.get_doc("File", {"file_url": file})
elif 'docs.google.com/spreadsheets' in file:
elif "docs.google.com/spreadsheets" in file:
self.google_sheets_url = file
elif os.path.exists(file):
self.file_path = file
@ -348,7 +348,7 @@ class ImportFile:
elif self.google_sheets_url:
content = get_csv_content_from_google_sheets(self.google_sheets_url)
extension = 'csv'
extension = "csv"
if not content:
frappe.throw(_("Invalid or corrupted content for import"))
@ -602,12 +602,20 @@ class Row:
is_table = frappe.get_meta(doctype).istable
is_update = self.import_type == UPDATE
if is_table and is_update and doc.get("name") in INVALID_VALUES:
# for table rows being inserted in update
# create a new doc with defaults set
new_doc = frappe.new_doc(doctype, as_dict=True)
new_doc.update(doc)
doc = new_doc
if is_table and is_update:
# check if the row already exists
# if yes, fetch the original doc so that it is not updated
# if no, create a new doc
id_field = get_id_field(doctype)
id_value = doc.get(id_field.fieldname)
if id_value and frappe.db.exists(doctype, id_value):
doc = frappe.get_doc(doctype, id_value)
else:
# for table rows being inserted in update
# create a new doc with defaults set
new_doc = frappe.new_doc(doctype, as_dict=True)
new_doc.update(doc)
doc = new_doc
self.check_mandatory_fields(doctype, doc, table_df)
return doc
@ -615,16 +623,12 @@ class Row:
def validate_value(self, value, col):
df = col.df
if df.fieldtype == "Select":
select_options = [d for d in (df.options or '').split('\n') if d]
select_options = get_select_options(df)
if select_options and value not in select_options:
options_string = ", ".join([frappe.bold(d) for d in select_options])
msg = _("Value must be one of {0}").format(options_string)
self.warnings.append(
{
"row": self.row_number,
"field": df_as_json(df),
"message": msg,
}
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
)
return
@ -635,11 +639,7 @@ class Row:
frappe.bold(value), frappe.bold(df.options)
)
self.warnings.append(
{
"row": self.row_number,
"field": df_as_json(df),
"message": msg,
}
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
)
return
elif df.fieldtype in ["Date", "Datetime"]:
@ -668,7 +668,7 @@ class Row:
def parse_value(self, value, col):
df = col.df
if isinstance(value, datetime) and df.fieldtype in ["Date", "Datetime"]:
if isinstance(value, (datetime, date)) and df.fieldtype in ["Date", "Datetime"]:
return value
value = cstr(value)
@ -689,7 +689,7 @@ class Row:
return value
def get_date(self, value, column):
if isinstance(value, datetime):
if isinstance(value, (datetime, date)):
return value
date_format = column.date_format
@ -786,9 +786,7 @@ class Header(Row):
for j, header in enumerate(row):
column_values = [get_item_at_index(r, j) for r in raw_data]
map_to_field = column_to_field_map.get(str(j))
column = Column(
j, header, self.doctype, column_values, map_to_field, self.seen
)
column = Column(j, header, self.doctype, column_values, map_to_field, self.seen)
self.seen.append(header)
self.columns.append(column)
@ -918,13 +916,20 @@ class Column:
self.skip_import = skip_import
def guess_date_format_for_column(self):
""" Guesses date format for a column by parsing all the values in the column,
"""Guesses date format for a column by parsing all the values in the column,
getting the date format and then returning the one which has the maximum frequency
"""
date_formats = [
frappe.utils.guess_date_format(d) for d in self.column_values if isinstance(d, str)
]
def guess_date_format(d):
if isinstance(d, (datetime, date)):
if self.df.fieldtype == "Date":
return "%Y-%m-%d"
if self.df.fieldtype == "Datetime":
return "%Y-%m-%d %H:%M:%S"
if isinstance(d, str):
return frappe.utils.guess_date_format(d)
date_formats = [guess_date_format(d) for d in self.column_values]
date_formats = [d for d in date_formats if d]
if not date_formats:
return
@ -955,28 +960,61 @@ class Column:
if not self.df:
return
if self.df.fieldtype == 'Link':
if self.skip_import:
return
if self.df.fieldtype == "Link":
# find all values that dont exist
values = list(set([cstr(v) for v in self.column_values[1:] if v]))
exists = [d.name for d in frappe.db.get_all(self.df.options, filters={'name': ('in', values)})]
exists = [
d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})
]
not_exists = list(set(values) - set(exists))
if not_exists:
missing_values = ', '.join(not_exists)
self.warnings.append({
'col': self.column_number,
'message': "The following values do not exist for {}: {}".format(self.df.options, missing_values),
'type': 'warning'
})
missing_values = ", ".join(not_exists)
self.warnings.append(
{
"col": self.column_number,
"message": (
"The following values do not exist for {}: {}".format(
self.df.options, missing_values
)
),
"type": "warning",
}
)
elif self.df.fieldtype in ("Date", "Time", "Datetime"):
# guess date format
self.date_format = self.guess_date_format_for_column()
if not self.date_format:
self.date_format = '%Y-%m-%d'
self.warnings.append({
'col': self.column_number,
'message': _("Date format could not determined from the values in this column. Defaulting to yyyy-mm-dd."),
'type': 'info'
})
self.date_format = "%Y-%m-%d"
self.warnings.append(
{
"col": self.column_number,
"message": _(
"Date format could not be determined from the values in"
" this column. Defaulting to yyyy-mm-dd."
),
"type": "info",
}
)
elif self.df.fieldtype == "Select":
options = get_select_options(self.df)
if options:
values = list(set([cstr(v) for v in self.column_values[1:] if v]))
invalid = list(set(values) - set(options))
if invalid:
valid_values = ", ".join([frappe.bold(o) for o in options])
invalid_values = ", ".join([frappe.bold(i) for i in invalid])
self.warnings.append(
{
"col": self.column_number,
"message": (
"The following values are invalid: {0}. Values must be"
" one of {1}".format(invalid_values, valid_values)
),
}
)
def as_dict(self):
d = frappe._dict()
@ -987,7 +1025,7 @@ class Column:
d.map_to_field = self.map_to_field
d.date_format = self.date_format
d.df = self.df
if hasattr(self.df, 'is_child_table_field'):
if hasattr(self.df, "is_child_table_field"):
d.is_child_table_field = self.df.is_child_table_field
d.child_table_df = self.df.child_table_df
d.skip_import = self.skip_import
@ -1067,7 +1105,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
# other fields
fields = get_standard_fields(doctype) + frappe.get_meta(doctype).fields
for df in fields:
label = (df.label or '').strip()
label = (df.label or "").strip()
fieldtype = df.fieldtype or "Data"
parent = df.parent or parent_doctype
if fieldtype not in no_value_fields:
@ -1161,12 +1199,17 @@ def get_user_format(date_format):
.replace("%d", "dd")
)
def df_as_json(df):
return {
'fieldname': df.fieldname,
'fieldtype': df.fieldtype,
'label': df.label,
'options': df.options,
'parent': df.parent,
'default': df.default
"fieldname": df.fieldname,
"fieldtype": df.fieldtype,
"label": df.label,
"options": df.options,
"parent": df.parent,
"default": df.default,
}
def get_select_options(df):
return [d for d in (df.options or "").split("\n") if d]

View file

@ -3,17 +3,26 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, json
import frappe
import json
from frappe.desk.doctype.bulk_update.bulk_update import show_progress
from frappe.model.document import Document
from frappe import _
class DeletedDocument(Document):
pass
@frappe.whitelist()
def restore(name):
def restore(name, alert=True):
deleted = frappe.get_doc('Deleted Document', name)
if deleted.restored:
frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored)
doc = frappe.get_doc(json.loads(deleted.data))
try:
doc.insert()
except frappe.DocstatusTransitionError:
@ -27,4 +36,34 @@ def restore(name):
deleted.restored = 1
deleted.db_update()
frappe.msgprint(_('Document Restored'))
if alert:
frappe.msgprint(_('Document Restored'))
@frappe.whitelist()
def bulk_restore(docnames):
docnames = frappe.parse_json(docnames)
message = _('Restoring Deleted Document')
restored, invalid, failed = [], [], []
for i, d in enumerate(docnames):
try:
show_progress(docnames, message, i + 1, d)
restore(d, alert=False)
frappe.db.commit()
restored.append(d)
except frappe.DocumentAlreadyRestored:
frappe.message_log.pop()
invalid.append(d)
except Exception:
frappe.message_log.pop()
failed.append(d)
frappe.db.rollback()
return {
"restored": restored,
"invalid": invalid,
"failed": failed
}

View file

@ -0,0 +1,40 @@
frappe.listview_settings["Deleted Document"] = {
onload: function (doclist) {
const action = () => {
const selected_docs = doclist.get_checked_items();
if (selected_docs.length > 0) {
let docnames = selected_docs.map(doc => doc.name);
frappe.call({
method: "frappe.core.doctype.deleted_document.deleted_document.bulk_restore",
args: { docnames },
callback: function (r) {
if (r.message) {
function body(docnames) {
const html = docnames.map(docname => {
return `<li><a href='/desk#Form/Deleted Document/${docname}'>${docname}</a></li>`;
});
return "<br><ul>" + html.join("");
}
function message(title, docnames) {
return (docnames.length > 0) ? title + body(docnames) + "</ul>": "";
}
const { restored, invalid, failed } = r.message;
const restored_summary = message(__("Documents restored successfully"), restored);
const invalid_summary = message(__("Documents that were already restored"), invalid);
const failed_summary = message(__("Documents that failed to restore"), failed);
const summary = restored_summary + invalid_summary + failed_summary;
frappe.msgprint(summary, __("Document Restoration Summary"), true);
if (restored.length > 0) {
doclist.refresh();
}
}
},
});
}
};
doclist.page.add_actions_menu_item(__("Restore"), action, false);
},
};

View file

@ -70,6 +70,7 @@
"web_view",
"has_web_view",
"allow_guest_to_view",
"index_web_pages_for_search",
"route",
"is_published_field",
"advanced",
@ -472,6 +473,8 @@
"label": "Documentation Link"
},
{
"collapsible": 1,
"collapsible_depends_on": "actions",
"fieldname": "actions_section",
"fieldtype": "Section Break",
"label": "Actions"
@ -483,6 +486,8 @@
"options": "DocType Action"
},
{
"collapsible": 1,
"collapsible_depends_on": "links",
"fieldname": "links_section",
"fieldtype": "Section Break",
"label": "Links Section"
@ -517,12 +522,94 @@
"fieldname": "email_settings_sb",
"fieldtype": "Section Break",
"label": "Email Settings"
},
{
"default": "1",
"fieldname": "index_web_pages_for_search",
"fieldtype": "Check",
"label": "Index Web Pages for Search"
}
],
"icon": "fa fa-bolt",
"idx": 6,
"links": [],
"modified": "2020-03-27 14:51:44.581128",
"links": [
{
"group": "Views",
"link_doctype": "Report",
"link_fieldname": "ref_doctype"
},
{
"group": "Workflow",
"link_doctype": "Workflow",
"link_fieldname": "document_type"
},
{
"group": "Workflow",
"link_doctype": "Notification",
"link_fieldname": "document_type"
},
{
"group": "Customization",
"link_doctype": "Custom Field",
"link_fieldname": "dt"
},
{
"group": "Customization",
"link_doctype": "Custom Script",
"link_fieldname": "dt"
},
{
"group": "Customization",
"link_doctype": "Server Script",
"link_fieldname": "reference_doctype"
},
{
"group": "Workflow",
"link_doctype": "Webhook",
"link_fieldname": "webhook_doctype"
},
{
"group": "Views",
"link_doctype": "Print Format",
"link_fieldname": "doc_type"
},
{
"group": "Views",
"link_doctype": "Web Form",
"link_fieldname": "doc_type"
},
{
"group": "Views",
"link_doctype": "Calendar View",
"link_fieldname": "reference_doctype"
},
{
"group": "Views",
"link_doctype": "Kanban Board",
"link_fieldname": "reference_doctype"
},
{
"group": "Workflow",
"link_doctype": "Onboarding Step",
"link_fieldname": "reference_document"
},
{
"group": "Rules",
"link_doctype": "Auto Repeat",
"link_fieldname": "reference_doctype"
},
{
"group": "Rules",
"link_doctype": "Assignment Rule",
"link_fieldname": "document_type"
},
{
"group": "Rules",
"link_doctype": "Energy Point Rule",
"link_fieldname": "reference_doctype"
}
],
"modified": "2020-08-06 12:59:32.369093",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -376,3 +376,96 @@ class TestDocType(unittest.TestCase):
link_doc.delete()
doc.delete()
frappe.db.commit()
def test_ignore_cancelation_of_linked_doctype_during_cancell(self):
import json
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
#create linked doctype
link_doc = self.new_doctype('Test Linked Doctype 1')
link_doc.is_submittable = 1
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
link_doc.insert()
#create first parent doctype
test_doc_1 = self.new_doctype('Test Doctype 1')
test_doc_1.is_submittable = 1
field_2 = test_doc_1.append('fields', {})
field_2.label = 'Test Linked Doctype 1'
field_2.fieldname = 'test_linked_doctype_a'
field_2.fieldtype = 'Link'
field_2.options = 'Test Linked Doctype 1'
for data in test_doc_1.get('permissions'):
data.submit = 1
data.cancel = 1
test_doc_1.insert()
#crete second parent doctype
doc = self.new_doctype('Test Doctype 2')
doc.is_submittable = 1
field_2 = doc.append('fields', {})
field_2.label = 'Test Linked Doctype 1'
field_2.fieldname = 'test_linked_doctype_a'
field_2.fieldtype = 'Link'
field_2.options = 'Test Linked Doctype 1'
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
doc.insert()
# create doctype data
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1')
data_link_doc_1.some_fieldname = 'Data1'
data_link_doc_1.insert()
data_link_doc_1.save()
data_link_doc_1.submit()
data_doc_2 = frappe.new_doc('Test Doctype 1')
data_doc_2.some_fieldname = 'Data1'
data_doc_2.test_linked_doctype_a = data_link_doc_1.name
data_doc_2.insert()
data_doc_2.save()
data_doc_2.submit()
data_doc = frappe.new_doc('Test Doctype 2')
data_doc.some_fieldname = 'Data1'
data_doc.test_linked_doctype_a = data_link_doc_1.name
data_doc.insert()
data_doc.save()
data_doc.submit()
docs = get_submitted_linked_docs(link_doc.name, data_link_doc_1.name)
dump_docs = json.dumps(docs.get('docs'))
cancel_all_linked_docs(dump_docs, ignore_doctypes_on_cancel_all=["Test Doctype 2"])
# checking that doc for Test Doctype 2 is not canceled
self.assertRaises(frappe.LinkExistsError, data_link_doc_1.cancel)
data_doc.load_from_db()
data_doc_2.load_from_db()
self.assertEqual(data_link_doc_1.docstatus, 2)
#linked doc is canceled
self.assertEqual(data_doc_2.docstatus, 2)
#ignored doctype 2 during cancel
self.assertEqual(data_doc.docstatus, 1)
# delete doctype record
data_doc.cancel()
data_doc.delete()
data_doc_2.delete()
data_link_doc_1.delete()
# delete doctype
link_doc.delete()
doc.delete()
test_doc_1.delete()
frappe.db.commit()

View file

@ -3,6 +3,8 @@
frappe.ui.form.on('Module Def', {
refresh: function(frm) {
frappe.xcall('frappe.core.doctype.module_def.module_def.get_installed_apps').then(r => {
frm.set_df_property('app_name', 'options', JSON.parse(r));
});
}
});

View file

@ -1,172 +1,88 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "field:module_name",
"beta": 0,
"creation": "2013-01-10 16:34:03",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 0,
"engine": "InnoDB",
"actions": [],
"allow_rename": 1,
"autoname": "field:module_name",
"creation": "2013-01-10 16:34:03",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"module_name",
"custom",
"app_name",
"restrict_to_domain"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "module_name",
"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": "Module Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "module_name",
"oldfieldtype": "Data",
"permlevel": 0,
"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
},
"fieldname": "module_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Module Name",
"oldfieldname": "module_name",
"oldfieldtype": "Data",
"reqd": 1,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "app_name",
"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": 1,
"label": "App Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
"fieldname": "app_name",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "App Name",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "restrict_to_domain",
"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": "Restrict To Domain",
"length": 0,
"no_copy": 0,
"options": "Domain",
"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
"fieldname": "restrict_to_domain",
"fieldtype": "Link",
"label": "Restrict To Domain",
"options": "Domain"
},
{
"default": "0",
"fieldname": "custom",
"fieldtype": "Check",
"label": "Custom"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-sitemap",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-13 03:05:28.213656",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Def",
"owner": "Administrator",
],
"icon": "fa fa-sitemap",
"idx": 1,
"links": [
{
"link_doctype": "DocType",
"link_fieldname": "module"
},
{
"link_doctype": "Desk Page",
"link_fieldname": "module"
}
],
"modified": "2020-08-06 12:39:30.740379",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Def",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
"create": 1,
"delete": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 1,
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0
],
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}

View file

@ -2,7 +2,7 @@
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, os
import frappe, os, json
from frappe.model.document import Document
@ -11,7 +11,7 @@ class ModuleDef(Document):
"""If in `developer_mode`, create folder for module and
add in `modules.txt` of app if missing."""
frappe.clear_cache()
if frappe.conf.get("developer_mode"):
if not self.custom and frappe.conf.get("developer_mode"):
self.create_modules_folder()
self.add_to_modules_txt()
@ -43,7 +43,7 @@ class ModuleDef(Document):
def on_trash(self):
"""Delete module name from modules.txt"""
if frappe.flags.in_uninstall:
if frappe.flags.in_uninstall or self.custom:
return
modules = None
@ -60,3 +60,7 @@ class ModuleDef(Document):
frappe.clear_cache()
frappe.setup_module_map()
@frappe.whitelist()
def get_installed_apps():
return json.dumps(frappe.get_installed_apps())

View file

@ -1,7 +1,7 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Video', {
frappe.ui.form.on('Navbar Item', {
// refresh: function(frm) {
// }

View file

@ -0,0 +1,87 @@
{
"actions": [],
"creation": "2020-08-01 23:38:41.783206",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_label",
"item_type",
"route",
"action",
"hidden",
"is_standard"
],
"fields": [
{
"columns": 2,
"fieldname": "item_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Item Label",
"mandatory_depends_on": "eval:doc.item_type == 'Route' || doc.item_type == 'Action'",
"show_days": 1,
"show_seconds": 1
},
{
"columns": 2,
"fieldname": "item_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Item Type",
"options": "Route\nAction\nSeparator",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Hidden",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"columns": 4,
"depends_on": "eval:doc.item_type == 'Route'",
"fieldname": "route",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Route",
"mandatory_depends_on": "eval:doc.item_type == 'Route'",
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval:doc.item_type == 'Action'",
"fieldname": "action",
"fieldtype": "Data",
"label": "Action",
"mandatory_depends_on": "eval:doc.item_type == 'Action'",
"show_days": 1,
"show_seconds": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-08-06 16:32:49.597060",
"modified_by": "Administrator",
"module": "Core",
"name": "Navbar Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class Video(Document):
class NavbarItem(Document):
pass

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
class TestVideo(unittest.TestCase):
class TestNavbarItem(unittest.TestCase):
pass

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Navbar Settings', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,91 @@
{
"actions": [],
"creation": "2020-08-01 23:41:12.577160",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"logo_section",
"app_logo",
"column_break_3",
"logo_width",
"section_break_2",
"settings_dropdown",
"help_dropdown"
],
"fields": [
{
"fieldname": "app_logo",
"fieldtype": "Attach Image",
"label": "Application Logo",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "settings_dropdown",
"fieldtype": "Table",
"label": "Settings Dropdown",
"options": "Navbar Item",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "help_dropdown",
"fieldtype": "Table",
"label": "Help Dropdown",
"options": "Navbar Item",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"label": "Dropdowns",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "logo_section",
"fieldtype": "Section Break",
"label": "Application Logo",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "logo_width",
"fieldtype": "Int",
"label": "Logo Width",
"show_days": 1,
"show_seconds": 1
}
],
"issingle": 1,
"links": [],
"modified": "2020-08-06 18:11:29.955835",
"modified_by": "Administrator",
"module": "Core",
"name": "Navbar Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
class NavbarSettings(Document):
def validate(self):
self.validate_standard_navbar_items()
def validate_standard_navbar_items(self):
doc_before_save = self.get_doc_before_save()
before_save_items = [item for item in \
doc_before_save.help_dropdown + doc_before_save.settings_dropdown if item.is_standard]
after_save_items = [item for item in \
self.help_dropdown + self.settings_dropdown if item.is_standard]
if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)):
frappe.throw(_("Please hide the standard navbar items instead of deleting them"))
@frappe.whitelist()
def get_app_logo():
app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo')
if not app_logo:
app_logo = frappe.get_hooks('app_logo_url')[-1]
return app_logo
def get_navbar_settings():
navbar_settings = frappe.get_single('Navbar Settings')
return navbar_settings

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestNavbarSettings(unittest.TestCase):
pass

View file

@ -1,6 +1,6 @@
frappe.ui.form.on('Report', {
refresh: function(frm) {
if (frm.doc.is_standard && !frappe.boot.developer_mode) {
if (frm.doc.is_standard === "Yes" && !frappe.boot.developer_mode) {
// make the document read-only
frm.set_read_only();
}

View file

@ -1,216 +1,90 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "field:role_name",
"beta": 0,
"creation": "2013-01-08 15:50:01",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 0,
"actions": [],
"allow_rename": 1,
"autoname": "field:role_name",
"creation": "2013-01-08 15:50:01",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"role_name",
"home_page",
"restrict_to_domain",
"column_break_4",
"disabled",
"desk_access",
"two_factor_auth"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "role_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Role Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "role_name",
"oldfieldtype": "Data",
"permlevel": 0,
"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
},
"fieldname": "role_name",
"fieldtype": "Data",
"label": "Role Name",
"oldfieldname": "role_name",
"oldfieldtype": "Data",
"reqd": 1,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "If disabled, this role will be removed from all users.",
"fieldname": "disabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disabled",
"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
},
"default": "0",
"description": "If disabled, this role will be removed from all users.",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "desk_access",
"fieldtype": "Check",
"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": "Desk Access",
"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
},
"default": "1",
"fieldname": "desk_access",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Desk Access"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "two_factor_auth",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Two Factor Authentication",
"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
},
"default": "0",
"fieldname": "two_factor_auth",
"fieldtype": "Check",
"label": "Two Factor Authentication"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "restrict_to_domain",
"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": "Restrict To Domain",
"length": 0,
"no_copy": 0,
"options": "Domain",
"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
"fieldname": "restrict_to_domain",
"fieldtype": "Link",
"label": "Restrict To Domain",
"options": "Domain"
},
{
"description": "Route: Example \"/desk\"",
"fieldname": "home_page",
"fieldtype": "Data",
"label": "Home Page"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-bookmark",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-06 12:42:57.097914",
"modified_by": "Administrator",
"module": "Core",
"name": "Role",
"owner": "Administrator",
],
"icon": "fa fa-bookmark",
"idx": 1,
"links": [],
"modified": "2020-08-06 15:42:59.036960",
"modified_by": "Administrator",
"module": "Core",
"name": "Role",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}

View file

@ -33,16 +33,22 @@ class Role(Document):
if user_type != user.user_type:
user.save()
# Get email addresses of all users that have been assigned this role
def get_emails_from_role(role):
emails = []
for user in get_users(role):
user_email, enabled = frappe.db.get_value("User", user, ["email", "enabled"])
if enabled and user_email not in ["admin@example.com", "guest@example.com"]:
emails.append(user_email)
def get_info_based_on_role(role, field='email'):
''' Get information of all users that have been assigned this role '''
users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"},
fields=["parent"])
return emails
return get_user_info(users, field)
def get_user_info(users, field='email'):
''' Fetch details about users for the specified field '''
info_list = []
for user in users:
user_info, enabled = frappe.db.get_value("User", user.parent, [field, "enabled"])
if enabled and user_info not in ["admin@example.com", "guest@example.com"]:
info_list.append(user_info)
return info_list
def get_users(role):
return [d.parent for d in frappe.get_all("Has Role", filters={"role": role, "parenttype": "User"},

View file

@ -2,8 +2,11 @@
// For license information, please see license.txt
frappe.ui.form.on('Server Script', {
setup: function(frm) {
frm.trigger('setup_help');
},
refresh: function(frm) {
if(frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled){
if (frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled) {
frm.add_custom_button('Schedule Script', function() {
var d = new frappe.ui.Dialog({
title: "Schedule Script Execution",
@ -33,14 +36,50 @@ frappe.ui.form.on('Server Script', {
}
},
schedule_script(frm, data){
schedule_script(frm, data) {
frm.call({
method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events",
args: {
'script_name': frm.doc.name,
'frequency': data.event_type
}
})
});
},
setup_help(frm) {
frm.get_field('help_html').html(`
<h3>Examples</h3>
<h4>DocType Event</h4>
<pre><code>
# set property
if "test" in doc.description:
doc.status = 'Closed'
# validate
if "validate" in doc.description:
raise frappe.ValidationError
# auto create another document
if doc.allocted_to:
frappe.get_doc(dict(
doctype = 'ToDo'
owner = doc.allocated_to,
description = doc.subject
)).insert()
</code></pre>
<hr>
<h4>API Call</h4>
<pre><code>
# respond to API
if frappe.form_dict.message == "ping":
frappe.response['message'] = "pong"
else:
frappe.response['message'] = "ok"
</code></pre>
`);
}
});

View file

@ -14,7 +14,9 @@
"api_method",
"allow_guest",
"section_break_8",
"script"
"script",
"help_section",
"help_html"
],
"fields": [
{
@ -72,10 +74,19 @@
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"fieldname": "help_section",
"fieldtype": "Section Break",
"label": "Help"
},
{
"fieldname": "help_html",
"fieldtype": "HTML"
}
],
"links": [],
"modified": "2020-04-06 11:24:38.161555",
"modified": "2020-08-07 13:13:02.483963",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",

View file

@ -52,9 +52,10 @@ class TestServerScript(unittest.TestCase):
frappe.db.commit()
# @classmethod
# def tearDownClass(cls):
# frappe.db.sql('truncate `tabServer Script`')
@classmethod
def tearDownClass(cls):
frappe.db.commit()
frappe.db.sql('truncate `tabServer Script`')
def setUp(self):
frappe.cache().delete_value('server_script_map')

View file

@ -18,6 +18,9 @@ class SMSSettings(Document):
def validate_receiver_nos(receiver_list):
validated_receiver_list = []
for d in receiver_list:
if not d:
break
# remove invalid character
for x in [' ','-', '(', ')']:
d = d.replace(x, '')

View file

@ -588,9 +588,70 @@
"icon": "fa fa-user",
"idx": 413,
"image_field": "user_image",
"links": [],
"links": [
{
"group": "Profile",
"link_doctype": "Contact",
"link_fieldname": "user"
},
{
"group": "Profile",
"link_doctype": "Chat Profile",
"link_fieldname": "user"
},
{
"group": "Profile",
"link_doctype": "Blogger",
"link_fieldname": "user"
},
{
"group": "Logs",
"link_doctype": "Access Log",
"link_fieldname": "user"
},
{
"group": "Logs",
"link_doctype": "Activity Log",
"link_fieldname": "user"
},
{
"group": "Logs",
"link_doctype": "Energy Point Log",
"link_fieldname": "user"
},
{
"group": "Logs",
"link_doctype": "Route History",
"link_fieldname": "user"
},
{
"group": "Settings",
"link_doctype": "User Permission",
"link_fieldname": "user"
},
{
"group": "Settings",
"link_doctype": "Assignment Rule",
"link_fieldname": "user"
},
{
"group": "Settings",
"link_doctype": "Document Follow",
"link_fieldname": "user"
},
{
"group": "Activity",
"link_doctype": "Communication",
"link_fieldname": "user"
},
{
"group": "Activity",
"link_doctype": "ToDo",
"link_fieldname": "owner"
}
],
"max_attachments": 5,
"modified": "2020-04-08 12:27:36.716490",
"modified": "2020-08-06 19:48:49.677800",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@ -606,13 +667,13 @@
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
},
{
"permlevel": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"write": 1
}

View file

@ -812,6 +812,7 @@ def reset_password(user):
return 'not found'
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def user_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond

View file

@ -119,6 +119,8 @@ def user_permission_exists(user, allow, for_value, applicable_for=None):
return has_same_user_permission
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len, filters):
linked_doctypes_map = get_linked_doctypes(doctype, True)

View file

@ -1,106 +0,0 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
"creation": "2018-10-17 05:47:13.087395",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"provider",
"url",
"column_break_4",
"publish_date",
"duration",
"section_break_7",
"description"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1,
"unique": 1
},
{
"fieldname": "provider",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Provider",
"options": "YouTube\nVimeo",
"reqd": 1
},
{
"fieldname": "url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "URL",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "publish_date",
"fieldtype": "Date",
"label": "Publish Date"
},
{
"fieldname": "duration",
"fieldtype": "Data",
"label": "Duration"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Description",
"reqd": 1
}
],
"links": [],
"modified": "2020-04-22 12:09:49.057403",
"modified_by": "Administrator",
"module": "Core",
"name": "Video",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -41,6 +41,8 @@ def get_columns_and_fields(doctype):
return columns, fields
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def query_doctypes(doctype, txt, searchfield, start, page_len, filters):
user = filters.get("user")
user_perms = frappe.utils.user.UserPermissions(user)

View file

@ -108,7 +108,7 @@ def create_plan():
'connector_name': 'Local Connector',
'connector_type': 'Frappe',
# connect to same host.
'hostname': frappe.conf.host_name,
'hostname': frappe.conf.host_name or frappe.utils.get_site_url(frappe.local.site),
'username': 'Administrator',
'password': 'admin'
'password': frappe.conf.get("admin_password") or 'admin'
}).insert(ignore_if_duplicate=True)

View file

@ -134,6 +134,8 @@ class Database(object):
if debug:
time_start = time()
self.log_query(query, values, debug, explain)
if values!=():
if isinstance(values, dict):
values = dict(values)
@ -142,41 +144,18 @@ class Database(object):
if not isinstance(values, (dict, tuple, list)):
values = (values,)
if debug and query.strip().lower().startswith('select'):
try:
if explain:
self.explain_query(query, values)
frappe.errprint(query % values)
except TypeError:
frappe.errprint([query, values])
if (frappe.conf.get("logging") or False)==2:
frappe.log("<<<< query")
frappe.log(query)
frappe.log("with values:")
frappe.log(values)
frappe.log(">>>>")
self._cursor.execute(query, values)
if frappe.flags.in_migrate:
self.log_touched_tables(query, values)
else:
if debug:
if explain:
self.explain_query(query)
frappe.errprint(query)
if (frappe.conf.get("logging") or False)==2:
frappe.log("<<<< query")
frappe.log(query)
frappe.log(">>>>")
self._cursor.execute(query)
if frappe.flags.in_migrate:
self.log_touched_tables(query)
if debug:
frappe.errprint(self._cursor.mogrify(query, values))
time_end = time()
frappe.errprint(("Execution time: {0} sec").format(round(time_end - time_start, 2)))
@ -213,6 +192,33 @@ class Database(object):
else:
return self._cursor.fetchall()
def log_query(self, query, values, debug, explain):
# for debugging in tests
if frappe.conf.get('allow_tests') and frappe.cache().get_value('flag_print_sql'):
print(self.mogrify(query, values))
# debug
if debug:
if explain and query.strip().lower().startswith('select'):
self.explain_query(query, values)
frappe.errprint(self.mogrify(query, values))
# info
if (frappe.conf.get("logging") or False)==2:
frappe.log("<<<< query")
frappe.log(self.mogrify(query, values))
frappe.log(">>>>")
def mogrify(self, query, values):
'''build the query string with values'''
if not values:
return query
else:
try:
return self._cursor.mogrify(query, values)
except: # noqa: E722
return (query, values)
def explain_query(self, query, values=None):
"""Print `EXPLAIN` in error log."""
try:
@ -378,7 +384,7 @@ class Database(object):
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
def get_value(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
debug=False, order_by=None, cache=False):
debug=False, order_by=None, cache=False, for_update=False):
"""Returns a document property or list of properties.
:param doctype: DocType name.
@ -405,12 +411,12 @@ class Database(object):
"""
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
order_by, cache=cache)
order_by, cache=cache, for_update=for_update)
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
debug=False, order_by=None, update=None, cache=False):
debug=False, order_by=None, update=None, cache=False, for_update=False):
"""Returns multiple document properties.
:param doctype: DocType name.
@ -449,7 +455,7 @@ class Database(object):
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
try:
out = self._get_values_from_table(fields, filters, doctype, as_dict, debug, order_by, update)
out = self._get_values_from_table(fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update)
except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
# table or column not found, return None
@ -576,7 +582,7 @@ class Database(object):
"""Alias for get_single_value"""
return self.get_single_value(*args, **kwargs)
def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None, update=None):
def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None, update=None, for_update=False):
fl = []
if isinstance(fields, (list, tuple)):
for f in fields:
@ -594,9 +600,15 @@ class Database(object):
order_by = ("order by " + order_by) if order_by else ""
r = self.sql("select {0} from `tab{1}` {2} {3} {4}"
.format(fl, doctype, "where" if conditions else "", conditions, order_by), values,
as_dict=as_dict, debug=debug, update=update)
r = self.sql("select {fields} from `tab{doctype}` {where} {conditions} {order_by} {for_update}"
.format(
for_update = 'for update' if for_update else '',
fields = fl,
doctype = doctype,
where = "where" if conditions else "",
conditions = conditions,
order_by = order_by),
values, as_dict=as_dict, debug=debug, update=update)
return r
@ -616,7 +628,7 @@ class Database(object):
return self.set_value(*args, **kwargs)
def set_value(self, dt, dn, field, val=None, modified=None, modified_by=None,
update_modified=True, debug=False):
update_modified=True, debug=False, for_update=True):
"""Set a single value in the database, do not call the ORM triggers
but update the modified timestamp (unless specified not to).
@ -630,6 +642,7 @@ class Database(object):
:param modified_by: Set this user as `modified_by`.
:param update_modified: default True. Set as false, if you don't want to update the timestamp.
:param debug: Print the query in the developer / js console.
:param for_update: Will add a row-level lock to the value that is being set so that it can be released on commit.
"""
if not modified:
modified = now()
@ -647,7 +660,9 @@ class Database(object):
if dn and dt!=dn:
# with table
conditions, values = self.build_conditions(dn)
values = dict(
name=self.get_value(dt, dn, 'name', for_update=for_update)
)
values.update(to_update)
@ -656,7 +671,7 @@ class Database(object):
set_values.append('`{0}`=%({0})s'.format(key))
self.sql("""update `tab{0}`
set {1} where {2}""".format(dt, ', '.join(set_values), conditions),
set {1} where name=%(name)s""".format(dt, ', '.join(set_values)),
values, debug=debug)
else:

View file

@ -46,7 +46,7 @@ class MariaDBDatabase(Database):
'Data': ('varchar', self.VARCHAR_LEN),
'Link': ('varchar', self.VARCHAR_LEN),
'Dynamic Link': ('varchar', self.VARCHAR_LEN),
'Password': ('varchar', self.VARCHAR_LEN),
'Password': ('text', ''),
'Select': ('varchar', self.VARCHAR_LEN),
'Rating': ('int', '1'),
'Read Only': ('varchar', self.VARCHAR_LEN),
@ -186,7 +186,7 @@ class MariaDBDatabase(Database):
`doctype` VARCHAR(140) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`fieldname` VARCHAR(140) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")

View file

@ -277,7 +277,7 @@ CREATE TABLE `__Auth` (
`doctype` VARCHAR(140) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`fieldname` VARCHAR(140) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View file

@ -51,7 +51,7 @@ class PostgresDatabase(Database):
'Data': ('varchar', self.VARCHAR_LEN),
'Link': ('varchar', self.VARCHAR_LEN),
'Dynamic Link': ('varchar', self.VARCHAR_LEN),
'Password': ('varchar', self.VARCHAR_LEN),
'Password': ('text', ''),
'Select': ('varchar', self.VARCHAR_LEN),
'Rating': ('smallint', None),
'Read Only': ('varchar', self.VARCHAR_LEN),
@ -179,7 +179,7 @@ class PostgresDatabase(Database):
"doctype" VARCHAR(140) NOT NULL,
"name" VARCHAR(255) NOT NULL,
"fieldname" VARCHAR(140) NOT NULL,
"password" VARCHAR(255) NOT NULL,
"password" TEXT NOT NULL,
"encrypted" INT NOT NULL DEFAULT 0,
PRIMARY KEY ("doctype", "name", "fieldname")
)""")

View file

@ -281,7 +281,7 @@ CREATE TABLE "__Auth" (
"doctype" VARCHAR(140) NOT NULL,
"name" VARCHAR(255) NOT NULL,
"fieldname" VARCHAR(140) NOT NULL,
"password" VARCHAR(255) NOT NULL,
"password" TEXT NOT NULL,
"encrypted" int NOT NULL DEFAULT 0,
PRIMARY KEY ("doctype", "name", "fieldname")
);

View file

@ -221,6 +221,8 @@ class Workspace:
incomplete_dependencies = [d for d in item.dependencies if not _doctype_contains_a_record(d)]
if len(incomplete_dependencies):
item.incomplete_dependencies = incomplete_dependencies
else:
item.incomplete_dependencies = ""
if item.onboard:
# Mark Spotlights for initial

View file

@ -23,7 +23,6 @@ frappe.ui.form.on('Dashboard Chart', {
frm.chart_filters = null;
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frm.set_df_property('chart_options_section', 'hidden', 1);
frm.disable_form();
}
@ -57,11 +56,6 @@ frappe.ui.form.on('Dashboard Chart', {
if (frm.doc.report_name) {
frm.trigger('set_chart_report_filters');
}
if (!frappe.boot.developer_mode) {
frm.set_df_property("custom_options", "hidden", 1);
}
},
is_standard: function(frm) {

View file

@ -28,15 +28,28 @@ def get_permission_query_conditions(user):
if "System Manager" in roles:
return None
allowed_doctypes = ['"%s"' % doctype for doctype in frappe.permissions.get_doctypes_with_read()]
allowed_reports = ['"%s"' % key if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
doctype_condition = False
report_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
if allowed_doctypes:
doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format(
allowed_doctypes=','.join(allowed_doctypes))
if allowed_reports:
report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format(
allowed_reports=','.join(allowed_reports))
return '''
`tabDashboard Chart`.`document_type` in ({allowed_doctypes})
or `tabDashboard Chart`.`report_name` in ({allowed_reports})
(`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
and {doctype_condition})
or
(`tabDashboard Chart`.`chart_type` = 'Report'
and {report_condition})
'''.format(
allowed_doctypes=','.join(allowed_doctypes),
allowed_reports=','.join(allowed_reports)
doctype_condition=doctype_condition,
report_condition=report_condition
)
@ -130,7 +143,7 @@ def add_chart_to_dashboard(args):
dashboard_link = frappe.new_doc('Dashboard Chart Link')
dashboard_link.chart = args.chart_name or args.name
if args.set_standard:
if args.set_standard and dashboard.is_standard:
chart = frappe.get_doc('Dashboard Chart', dashboard_link.chart)
chart.is_standard = 1
chart.module = dashboard.module
@ -344,6 +357,8 @@ def get_year_ending(date):
# last day of this month
return add_to_date(date, days=-1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
or_filters = {'owner': frappe.session.user, 'is_public': 1}
return frappe.db.get_list('Dashboard Chart',

View file

@ -5,14 +5,23 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils.data import validate_json_string
from frappe.modules.export_file import export_to_files
from frappe.model.document import Document
class DeskPage(Document):
def validate(self):
self.validate_cards_json()
if (self.is_standard and not frappe.conf.developer_mode and not disable_saving_as_standard()):
frappe.throw(_("You need to be in developer mode to edit this document"))
def validate_cards_json(self):
for card in self.cards:
try:
validate_json_string(card.links)
except frappe.ValidationError:
frappe.throw(_("Invalid JSON in card links for {0}").format(frappe.bold(card.label)))
def on_update(self):
if disable_saving_as_standard():
return

View file

@ -82,6 +82,27 @@ class Event(Document):
communication.add_link(participant.reference_doctype, participant.reference_docname)
communication.save(ignore_permissions=True)
def add_participant(self, doctype, docname):
"""Add a single participant to event participants
Args:
doctype (string): Reference Doctype
docname (string): Reference Docname
"""
self.append("event_participants", {
"reference_doctype": doctype,
"reference_docname": docname,
})
def add_participants(self, participants):
"""Add participant entry
Args:
participants ([Array]): Array of a dict with doctype and docname
"""
for participant in participants:
self.add_participant(participant["doctype"], participant["docname"])
@frappe.whitelist()
def delete_communication(event, reference_doctype, reference_docname):
deleted_participant = frappe.get_doc(reference_doctype, reference_docname)

View file

@ -32,13 +32,17 @@ def get_permission_query_conditions(user=None):
if "System Manager" in roles:
return None
allowed_doctypes = ['"%s"' % doctype for doctype in frappe.permissions.get_doctypes_with_read()]
doctype_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
if allowed_doctypes:
doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format(
allowed_doctypes=','.join(allowed_doctypes))
return '''
`tabNumber Card`.`document_type` in ({allowed_doctypes})
'''.format(
allowed_doctypes=','.join(allowed_doctypes)
)
{doctype_condition}
'''.format(doctype_condition=doctype_condition)
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
@ -124,11 +128,16 @@ def create_number_card(args):
doc.insert(ignore_permissions=True)
return doc
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
meta = frappe.get_meta(doctype)
searchfields = meta.get_search_fields()
search_conditions = []
if not frappe.db.exists('DocType', doctype):
return
if txt:
for field in searchfields:
search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt))
@ -172,7 +181,7 @@ def add_card_to_dashboard(args):
dashboard_link = frappe.new_doc('Number Card Link')
dashboard_link.card = args.name
if args.set_standard:
if args.set_standard and dashboard.is_standard:
card = frappe.get_doc('Number Card', dashboard_link.card)
card.is_standard = 1
card.module = dashboard.module

View file

@ -184,7 +184,7 @@
}
],
"links": [],
"modified": "2020-05-18 19:42:30.435604",
"modified": "2020-08-06 12:55:20.377679",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Step",

View file

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe
import frappe.utils
from frappe.utils import get_url_to_form
from frappe.model import log_types
from frappe import _
from itertools import groupby
@ -20,22 +21,26 @@ def follow_document(doctype, doc_name, user, force=False):
avoided for some doctype
follow only if track changes are set to 1
'''
avoid_follow = ["Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log",
"File", "Version", "View Log", "Document Follow", "Comment"]
if (doctype in ("Communication", "ToDo", "Email Unsubscribe", "File", "Comment")
or doctype in log_types):
return
track_changes = frappe.get_meta(doctype).track_changes
exists = is_document_followed(doctype, doc_name, user)
if exists == 0:
user_can_follow = frappe.db.get_value("User", user, "document_follow_notify", ignore=True)
if user != "Administrator" and user_can_follow and track_changes and (doctype not in avoid_follow or force):
doc = frappe.new_doc("Document Follow")
doc.update({
"ref_doctype": doctype,
"ref_docname": doc_name,
"user": user
})
doc.save()
return doc
if ((not frappe.get_meta(doctype).track_changes)
or user == "Administrator"):
return
if not frappe.db.get_value("User", user, "document_follow_notify", ignore=True, cache=True):
return
if not is_document_followed(doctype, doc_name, user):
doc = frappe.new_doc("Document Follow")
doc.update({
"ref_doctype": doctype,
"ref_docname": doc_name,
"user": user
})
doc.save()
return doc
@frappe.whitelist()
def unfollow_document(doctype, doc_name, user):

View file

@ -11,7 +11,6 @@ from frappe import _
from frappe.model.meta import is_single
from frappe.modules import load_doctype_module
@frappe.whitelist()
def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
"""
@ -78,7 +77,7 @@ def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
@frappe.whitelist()
def cancel_all_linked_docs(docs):
def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
"""
Cancel all linked doctype
@ -87,14 +86,16 @@ def cancel_all_linked_docs(docs):
"""
docs = json.loads(docs)
if isinstance(ignore_doctypes_on_cancel_all, string_types):
ignore_doctypes_on_cancel_all = json.loads(ignore_doctypes_on_cancel_all)
for i, doc in enumerate(docs, 1):
if validate_linked_doc(doc) is True:
frappe.publish_progress(percent=i * 100 / len(docs), title=_("Cancelling documents"))
if validate_linked_doc(doc, ignore_doctypes_on_cancel_all) is True:
frappe.publish_progress(percent=i * 100 / ((len(docs) - len(ignore_doctypes_on_cancel_all))), title=_("Cancelling documents"))
linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name"))
linked_doc.cancel()
def validate_linked_doc(docinfo):
def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
"""
Validate a document to be submitted and non-exempted from auto-cancel.
@ -105,6 +106,10 @@ def validate_linked_doc(docinfo):
bool: True if linked document passes all validations, else False
"""
#ignore doctype to cancel
if docinfo.get("doctype") in ignore_doctypes_on_cancel_all:
return False
# skip non-submittable doctypes since they don't need to be cancelled
if not frappe.get_meta(docinfo.get('doctype')).is_submittable:
return False

View file

@ -67,8 +67,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
# Reordered columns
columns = json.loads(report.custom_columns)
if report.report_type == 'Query Report':
result = reorder_data_for_custom_columns(columns, query_columns, result)
result = reorder_data_for_custom_columns(columns, query_columns, result, report.report_type)
result = add_data_to_custom_columns(columns, result)
@ -216,15 +215,21 @@ def add_data_to_custom_columns(columns, result):
return data
def reorder_data_for_custom_columns(custom_columns, columns, result):
def reorder_data_for_custom_columns(custom_columns, columns, result, report_type):
custom_column_labels = [col["label"] for col in custom_columns]
if report_type == 'Query Report':
original_column_labels = [col.split(":")[0] for col in columns]
else:
original_column_labels = [col["label"] for col in columns]
reordered_result = []
columns = [col.split(":")[0] for col in columns]
for res in result:
r = []
for col in custom_columns:
for col_name in custom_column_labels:
try:
idx = columns.index(col.get("label"))
idx = original_column_labels.index(col_name)
r.append(res[idx])
except ValueError:
pass

View file

@ -10,6 +10,7 @@ from frappe.handler import is_whitelisted
from frappe import _
from six import string_types
import re
import wrapt
UNTRANSLATED_DOCTYPES = ["DocType", "Role"]
@ -206,3 +207,15 @@ def scrub_custom_query(query, key, txt):
if '%s' in query:
query = query.replace('%s', ((txt or '') + '%'))
return query
@wrapt.decorator
def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
kwargs.update(dict(zip(fn.__code__.co_varnames, args)))
sanitize_searchfield(kwargs['searchfield'])
kwargs['start'] = cint(kwargs['start'])
kwargs['page_len'] = cint(kwargs['page_len'])
if kwargs['doctype'] and not frappe.db.exists('DocType', kwargs['doctype']):
return []
return fn(**kwargs)

View file

@ -57,6 +57,8 @@ def relink(name, reference_doctype=None, reference_name=None):
communication_type = "Communication" and
name = %s""", (reference_doctype, reference_name, name))
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_communication_doctype(doctype, txt, searchfield, start, page_len, filters):
user_perms = frappe.utils.user.UserPermissions(frappe.session.user)
user_perms.build_permissions()

View file

@ -251,7 +251,7 @@ class EmailAccount(Document):
email_server = None
if frappe.local.flags.in_test:
incoming_mails = test_mails
incoming_mails = test_mails or []
else:
email_sync_rule = self.build_email_sync_rule()

View file

@ -5,7 +5,10 @@ from __future__ import unicode_literals
import frappe, os
import unittest, email
test_records = frappe.get_test_records('Email Account')
from frappe.test_runner import make_test_records
make_test_records("User")
make_test_records("Email Account")
from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments

View file

@ -132,10 +132,11 @@
"has_web_view": 1,
"icon": "fa fa-envelope",
"idx": 1,
"index_web_pages_for_search": 1,
"is_published_field": "published",
"links": [],
"max_attachments": 3,
"modified": "2020-05-12 18:09:40.137138",
"modified": "2020-07-21 16:25:17.687476",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",

View file

@ -3,104 +3,175 @@
this.frm.add_fetch('sender', 'email_id', 'sender_email');
this.frm.fields_dict.sender.get_query = function(){
this.frm.fields_dict.sender.get_query = function() {
return {
filters: {
'enable_outgoing': 1
enable_outgoing: 1
}
}
};
};
frappe.notification = {
setup_fieldname_select: function(frm) {
// get the doctype to update fields
if(!frm.doc.document_type) {
if (!frm.doc.document_type) {
return;
}
frappe.model.with_doctype(frm.doc.document_type, function() {
let get_select_options = function(df) {
return {value: df.fieldname, label: df.fieldname + " (" + __(df.label) + ")"};
}
return {
value: df.fieldname,
label: df.fieldname + ' (' + __(df.label) + ')'
};
};
let get_date_change_options = function() {
let date_options = $.map(fields, function(d) {
return (d.fieldtype=="Date" || d.fieldtype=="Datetime")?
get_select_options(d) : null;
return d.fieldtype == 'Date' || d.fieldtype == 'Datetime'
? get_select_options(d)
: null;
});
// append creation and modified date to Date Change field
return date_options.concat([
{ value: "creation", label: `creation (${__('Created On')})` },
{ value: "modified", label: `modified (${__('Last Modified Date')})` }
{ value: 'creation', label: `creation (${__('Created On')})` },
{ value: 'modified', label: `modified (${__('Last Modified Date')})` }
]);
}
};
let fields = frappe.get_doc("DocType", frm.doc.document_type).fields;
let options = $.map(fields,
function(d) { return in_list(frappe.model.no_value_type, d.fieldtype) ?
null : get_select_options(d); });
let fields = frappe.get_doc('DocType', frm.doc.document_type).fields;
let options = $.map(fields, function(d) {
return in_list(frappe.model.no_value_type, d.fieldtype)
? null : get_select_options(d);
});
// set value changed options
frm.set_df_property("value_changed", "options", [""].concat(options));
frm.set_df_property("set_property_after_alert", "options", [""].concat(options));
frm.set_df_property('value_changed', 'options', [''].concat(options));
frm.set_df_property(
'set_property_after_alert',
'options',
[''].concat(options)
);
// set date changed options
frm.set_df_property("date_changed", "options", get_date_change_options());
frm.set_df_property('date_changed', 'options', get_date_change_options());
let email_fields = $.map(fields,
function(d) { return (d.options == "Email" ||
(d.options=='User' && d.fieldtype=='Link')) ?
get_select_options(d) : null; });
let receiver_fields = [];
if (frm.doc.channel === 'Email') {
receiver_fields = $.map(fields, function(d) {
return d.options == 'Email' ||
(d.options == 'User' && d.fieldtype == 'Link')
? get_select_options(d) : null;
});
} else if (in_list(['WhatsApp', 'SMS'], frm.doc.channel)) {
receiver_fields = $.map(fields, function(d) {
return d.options == 'Phone' ? get_select_options(d) : null;
});
}
// set email recipient options
frappe.meta.get_docfield("Notification Recipient", "email_by_document_field",
frappe.meta.get_docfield(
'Notification Recipient',
'receiver_by_document_field',
// set first option as blank to allow notification not to be defaulted to the owner
frm.doc.name).options = [""].concat(["owner"].concat(email_fields));
frm.doc.name
).options = [''].concat(["owner"]).concat(receiver_fields);
frm.fields_dict.recipients.grid.refresh();
});
}
}
},
setup_example_message: function(frm) {
let template = '';
if (frm.doc.channel === 'WhatsApp') {
template = `<h5 style='display: inline-block'>Warning:</h5> Only Use Pre-Approved WhatsApp for Business Template
<h5>Message Example</h5>
frappe.ui.form.on("Notification", {
<pre>
Your {{ doc.name }} order of {{ doc.total }} has shipped and should be delivered on {{ doc.date }}. Details : {{doc.customer}}
</pre>`;
} else if (frm.doc.channel === 'Email') {
template = `<h5>Message Example</h5>
<pre>&lt;h3&gt;Order Overdue&lt;/h3&gt;
&lt;p&gt;Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.&lt;/p&gt;
&lt;!-- show last comment --&gt;
{% if comments %}
Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
{% endif %}
&lt;h4&gt;Details&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Customer: {{ doc.customer }}
&lt;li&gt;Amount: {{ doc.grand_total }}
&lt;/ul&gt;
</pre>
`;
} else {
template = `<h5>Message Example</h5>
<pre>*Order Overdue*
Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.
<!-- show last comment -->
{% if comments %}
Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
{% endif %}
*Details*
Customer: {{ doc.customer }}
Amount: {{ doc.grand_total }}
</pre>`;
}
frm.set_df_property('message_examples', 'options', template);
}
};
frappe.ui.form.on('Notification', {
onload: function(frm) {
frm.set_query("document_type", function() {
frm.set_query('document_type', function() {
return {
"filters": {
"istable": 0
filters: {
istable: 0
}
}
};
});
frm.set_query("print_format", function() {
frm.set_query('print_format', function() {
return {
"filters": {
"doc_type": frm.doc.document_type
filters: {
doc_type: frm.doc.document_type
}
}
};
});
},
refresh: function(frm) {
frappe.notification.setup_fieldname_select(frm);
frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
frm.get_field('is_standard').toggle(frappe.boot.developer_mode);
frm.trigger('event');
},
document_type: function(frm) {
frappe.notification.setup_fieldname_select(frm);
},
view_properties: function(frm) {
frappe.route_options = {doc_type:frm.doc.document_type};
frappe.set_route("Form", "Customize Form");
frappe.route_options = { doc_type: frm.doc.document_type };
frappe.set_route('Form', 'Customize Form');
},
event: function(frm) {
if(in_list(['Days Before', 'Days After'], frm.doc.event)) {
if (in_list(['Days Before', 'Days After'], frm.doc.event)) {
frm.add_custom_button(__('Get Alerts for Today'), function() {
frappe.call({
method: 'frappe.email.doctype.notification.notification.get_documents_for_today',
method:
'frappe.email.doctype.notification.notification.get_documents_for_today',
args: {
notification: frm.doc.name
},
callback: function(r) {
if(r.message) {
if (r.message) {
frappe.msgprint(r.message);
} else {
frappe.msgprint(__('No alerts for today'));
@ -111,6 +182,14 @@ frappe.ui.form.on("Notification", {
}
},
channel: function(frm) {
frm.toggle_reqd("recipients", frm.doc.channel=="Email");
frm.toggle_reqd('recipients', frm.doc.channel == 'Email');
frappe.notification.setup_fieldname_select(frm);
frappe.notification.setup_example_message(frm);
if (frm.doc.channel === 'SMS' && frm.doc.__islocal) {
frm.set_df_property('channel',
'description', `To use SMS Channel, initialize <a href="#Form/SMS Settings">SMS Settings</a>.`);
} else {
frm.set_df_property('channel', 'description', ` `);
}
}
});

View file

@ -10,6 +10,7 @@
"enabled",
"column_break_2",
"channel",
"twilio_number",
"slack_webhook_url",
"filters",
"subject",
@ -37,7 +38,6 @@
"message_sb",
"message",
"message_examples",
"slack_message_examples",
"view_properties",
"column_break_25",
"attach_print",
@ -60,11 +60,13 @@
"fieldname": "channel",
"fieldtype": "Select",
"label": "Channel",
"options": "Email\nSlack\nSystem Notification",
"reqd": 1
"options": "Email\nSlack\nSystem Notification\nWhatsApp\nSMS",
"reqd": 1,
"set_only_once": 1
},
{
"depends_on": "eval:doc.channel=='Slack'",
"description": "To use Slack Channel, add a <a href=\"\\#Form/Slack Webhook URL\">Slack Webhook URL</a>.",
"fieldname": "slack_webhook_url",
"fieldtype": "Link",
"label": "Slack Channel",
@ -77,13 +79,14 @@
"label": "Filters"
},
{
"depends_on": "eval: !in_list(['SMS', 'WhatsApp'], doc.channel)",
"description": "To add dynamic subject, use jinja tags like\n\n<div><pre><code>{{ doc.name }} Delivered</code></pre></div>",
"fieldname": "subject",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"in_list_view": 1,
"label": "Subject",
"reqd": 1
"mandatory_depends_on": "eval:!in_list(['SMS', 'WhatsApp'], doc.channel)"
},
{
"fieldname": "document_type",
@ -153,6 +156,7 @@
"label": "Value Changed"
},
{
"depends_on": "eval: doc.channel == 'Email'",
"fieldname": "sender",
"fieldtype": "Link",
"label": "Sender",
@ -203,7 +207,7 @@
"label": "Value To Be Set"
},
{
"depends_on": "eval:doc.channel!=='Slack'",
"depends_on": "eval:in_list(['Email', 'SMS', 'WhatsApp'], doc.channel)",
"fieldname": "column_break_5",
"fieldtype": "Section Break",
"label": "Recipients"
@ -228,19 +232,11 @@
"label": "Message"
},
{
"depends_on": "eval:doc.channel=='Email'",
"fieldname": "message_examples",
"fieldtype": "HTML",
"label": "Message Examples",
"options": "<h5>Message Example</h5>\n\n<pre>&lt;h3&gt;Order Overdue&lt;/h3&gt;\n\n&lt;p&gt;Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.&lt;/p&gt;\n\n&lt;!-- show last comment --&gt;\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n&lt;h4&gt;Details&lt;/h4&gt;\n\n&lt;ul&gt;\n&lt;li&gt;Customer: {{ doc.customer }}\n&lt;li&gt;Amount: {{ doc.grand_total }}\n&lt;/ul&gt;\n</pre>"
},
{
"depends_on": "eval:doc.channel=='Slack'",
"fieldname": "slack_message_examples",
"fieldtype": "HTML",
"label": "Message Examples",
"options": "<h5>Message Example</h5>\n\n<pre>*Order Overdue*\n\nTransaction {{ doc.name }} has exceeded Due Date. Please take necessary action.\n\n<!-- show last comment -->\n{% if comments %}\nLast comment: {{ comments[-1].comment }} by {{ comments[-1].by }}\n{% endif %}\n\n*Details*\n\n\u2022 Customer: {{ doc.customer }}\n\u2022 Amount: {{ doc.grand_total }}\n</pre>"
},
{
"fieldname": "view_properties",
"fieldtype": "Button",
@ -266,6 +262,14 @@
"label": "Print Format",
"options": "Print Format"
},
{
"depends_on": "eval: doc.channel==='WhatsApp'",
"description": "To use WhatsApp for Business, initialize <a href=\"#Form/Twilio Settings\">Twilio Settings</a>.",
"fieldname": "twilio_number",
"fieldtype": "Link",
"label": "Twilio Number",
"options": "Twilio Number Group"
},
{
"default": "0",
"depends_on": "eval: doc.channel !== 'System Notification'",
@ -277,7 +281,7 @@
],
"icon": "fa fa-envelope",
"links": [],
"modified": "2020-06-23 14:01:25.462544",
"modified": "2020-08-11 19:24:35.479373",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification",

View file

@ -7,12 +7,14 @@ import frappe
import json, os
from frappe import _
from frappe.model.document import Document
from frappe.core.doctype.role.role import get_emails_from_role
from frappe.core.doctype.role.role import get_info_based_on_role, get_user_info
from frappe.utils import validate_email_address, nowdate, parse_val, is_html, add_to_date
from frappe.utils.jinja import validate_template
from frappe.modules.utils import export_module_json, get_doc_module
from six import string_types
from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message
from frappe.integrations.doctype.twilio_settings.twilio_settings import send_whatsapp_message
from frappe.core.doctype.sms_settings.sms_settings import send_sms
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification
class Notification(Document):
@ -26,7 +28,9 @@ class Notification(Document):
self.name = self.subject
def validate(self):
validate_template(self.subject)
if self.channel not in ('WhatsApp', 'SMS'):
validate_template(self.subject)
validate_template(self.message)
if self.event in ("Days Before", "Days After") and not self.date_changed:
@ -126,8 +130,15 @@ def get_context(context):
if self.channel == 'Slack':
self.send_a_slack_msg(doc, context)
if self.channel == 'WhatsApp':
self.send_whatsapp_msg(doc, context)
if self.channel == 'SMS':
self.send_sms(doc, context)
if self.channel == 'System Notification' or self.send_system_notification:
self.create_system_notification(doc, context)
except:
frappe.log_error(title='Failed to send notification', message=frappe.get_traceback())
@ -195,11 +206,24 @@ def get_context(context):
and attachments[0].get('print_letterhead')) or False))
def send_a_slack_msg(self, doc, context):
send_slack_message(
webhook_url=self.slack_webhook_url,
message=frappe.render_template(self.message, context),
reference_doctype = doc.doctype,
reference_name = doc.name)
send_slack_message(
webhook_url=self.slack_webhook_url,
message=frappe.render_template(self.message, context),
reference_doctype=doc.doctype,
reference_name=doc.name)
def send_whatsapp_msg(self, doc, context):
send_whatsapp_message(
sender=self.twilio_number,
receiver_list=self.get_receiver_list(doc, context),
message=frappe.render_template(self.message, context),
)
def send_sms(self, doc, context):
send_sms(
receiver_list=self.get_receiver_list(doc, context),
msg=frappe.render_template(self.message, context)
)
def get_list_of_recipients(self, doc, context):
recipients = []
@ -209,8 +233,8 @@ def get_context(context):
if recipient.condition:
if not frappe.safe_eval(recipient.condition, None, context):
continue
if recipient.email_by_document_field:
email_ids_value = doc.get(recipient.email_by_document_field)
if recipient.receiver_by_document_field:
email_ids_value = doc.get(recipient.receiver_by_document_field)
if validate_email_address(email_ids_value):
email_ids = email_ids_value.replace(",", "\n")
recipients = recipients + email_ids.split("\n")
@ -232,8 +256,8 @@ def get_context(context):
bcc = bcc + recipient.bcc.split("\n")
#For sending emails to specified role
if recipient.email_by_role:
emails = get_emails_from_role(recipient.email_by_role)
if recipient.receiver_by_role:
emails = get_info_based_on_role(recipient.receiver_by_role, 'email')
for email in emails:
recipients = recipients + email.split("\n")
@ -242,6 +266,27 @@ def get_context(context):
return None, None, None
return list(set(recipients)), list(set(cc)), list(set(bcc))
def get_receiver_list(self, doc, context):
''' return receiver list based on the doc field and role specified '''
receiver_list = []
for recipient in self.recipients:
if recipient.condition:
if not frappe.safe_eval(recipient.condition, None, context):
continue
# For sending messages to the owner's mobile phone number
if recipient.receiver_by_document_field == 'owner':
receiver_list.append(get_user_info(doc.get('owner'), 'mobile_no'))
# For sending messages to the number specified in the receiver field
elif recipient.receiver_by_document_field:
receiver_list.append(doc.get(recipient.receiver_by_document_field))
#For sending messages to specified role
if recipient.receiver_by_role:
receiver_list += get_info_based_on_role(recipient.receiver_by_role, 'mobile_no')
return receiver_list
def get_attachment(self, doc):
""" check print settings are attach the pdf """
if not self.attach_print:

View file

@ -63,7 +63,7 @@ class TestNotification(unittest.TestCase):
notification.message = "test"
recipent = frappe.new_doc("Notification Recipient")
recipent.email_by_document_field = "owner"
recipent.receiver_by_document_field = "owner"
notification.recipents = recipent
notification.condition = "test"
@ -105,7 +105,7 @@ class TestNotification(unittest.TestCase):
"value_changed": "description1",
"message": "Description changed",
"recipients": [
{ "email_by_document_field": "owner" }
{ "receiver_by_document_field": "owner" }
]
}).insert()
frappe.db.commit()

View file

@ -8,7 +8,7 @@
"message": "New comment {{ doc.content }} created",
"condition": "doc.communication_type=='Comment'",
"recipients": [
{ "email_by_document_field": "owner" }
{ "receiver_by_document_field": "owner" }
]
},
{
@ -20,7 +20,7 @@
"message": "New comment {{ doc.content }} saved",
"condition": "doc.communication_type=='Comment'",
"recipients": [
{ "email_by_document_field": "owner" }
{ "receiver_by_document_field": "owner" }
],
"set_property_after_alert": "subject",
"property_value": "__testing__"
@ -34,7 +34,7 @@
"condition": "doc.event_type=='Public'",
"message": "A new public event {{ doc.subject }} on {{ doc.starts_on }} is created",
"recipients": [
{ "email_by_document_field": "owner" }
{ "receiver_by_document_field": "owner" }
]
},
{
@ -46,7 +46,7 @@
"value_changed": "description",
"message": "Description changed",
"recipients": [
{ "email_by_document_field": "owner" }
{ "receiver_by_document_field": "owner" }
]
},
{
@ -59,7 +59,7 @@
"days_in_advance": 2,
"message": "Description changed",
"recipients": [
{ "email_by_document_field": "owner" }
{ "receiver_by_document_field": "owner" }
]
},
{
@ -70,7 +70,7 @@
"attach_print": 0,
"message": "New user {{ doc.name }} created",
"recipients": [
{ "email_by_document_field": "owner", "cc": "{{ doc.email }}" }
{ "receiver_by_document_field": "owner", "cc": "{{ doc.email }}" }
]
}
]

View file

@ -1,204 +1,60 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2014-07-11 17:19:37.037109",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2014-07-11 17:19:37.037109",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"receiver_by_document_field",
"receiver_by_role",
"cc",
"bcc",
"condition"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "email_by_document_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": "Email By Document Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
},
"depends_on": "eval:parent.channel=='Email'",
"description": "Optional: Always send to these ids. Each Email Address on a new row",
"fieldname": "cc",
"fieldtype": "Code",
"label": "CC"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "email_by_role",
"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": "Email By Role",
"length": 0,
"no_copy": 0,
"options": "Role",
"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,
"translatable": 0,
"unique": 0
},
"depends_on": "eval:parent.channel=='Email'",
"fieldname": "bcc",
"fieldtype": "Code",
"label": "BCC"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Optional: Always send to these ids. Each Email Address on a new row",
"fieldname": "cc",
"fieldtype": "Code",
"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": "CC",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
},
"description": "Expression, Optional",
"fieldname": "condition",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Condition"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"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,
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "receiver_by_document_field",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Receiver By Document Field"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Expression, Optional",
"fieldname": "condition",
"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": "Condition",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
"fieldname": "receiver_by_role",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Receiver By Role",
"options": "Role"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-09-03 18:37:57.043251",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification Recipient",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"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,
"track_views": 0
],
"istable": 1,
"links": [],
"modified": "2020-02-21 11:18:40.125233",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification Recipient",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View file

@ -25,6 +25,11 @@ class EventConsumer(Document):
else:
frappe.db.set_value(self.doctype, self.name, 'incoming_change', 0)
frappe.cache().delete_value('event_consumer_document_type_map')
def on_trash(self):
frappe.cache().delete_value('event_consumer_document_type_map')
def update_consumer_status(self):
consumer_site = get_consumer_site(self.callback_url)
event_producer = consumer_site.get_doc('Event Producer', get_url())

View file

@ -1,14 +1,17 @@
{
"actions": [],
"creation": "2019-10-03 21:10:54.754651",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"ref_doctype",
"status"
"status",
"unsubscribed"
],
"fields": [
{
"columns": 4,
"fieldname": "ref_doctype",
"fieldtype": "Link",
"in_list_view": 1,
@ -18,16 +21,27 @@
"reqd": 1
},
{
"columns": 4,
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Approval Status",
"options": "Pending\nApproved\nRejected"
},
{
"columns": 2,
"default": "0",
"fieldname": "unsubscribed",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Unsubscribed",
"read_only": 1
}
],
"istable": 1,
"modified": "2019-10-29 15:26:32.436528",
"links": [],
"modified": "2020-08-14 12:38:40.918620",
"modified_by": "Administrator",
"module": "Event Streaming",
"name": "Event Consumer Document Type",

View file

@ -107,7 +107,8 @@ class EventProducer(Document):
event_consumer.consumer_doctypes.append({
'ref_doctype': ref_doctype,
'status': get_approval_status(config, ref_doctype)
'status': get_approval_status(config, ref_doctype),
'unsubscribed': entry.unsubscribe
})
if frappe.flags.in_test:
event_consumer.in_test = True
@ -213,11 +214,12 @@ def sync(update, producer_site, event_producer, in_retry=False):
except Exception:
if in_retry:
if frappe.flags.in_test:
print(frappe.get_traceback())
return 'Failed'
log_event_sync(update, event_producer.name, 'Failed', frappe.get_traceback())
frappe.db.set_value('Event Producer', event_producer.name, 'last_update', update.creation)
event_producer.reload()
event_producer.db_set('last_update', update.creation)
frappe.db.commit()

View file

@ -12,9 +12,20 @@ from frappe.event_streaming.doctype.event_producer.event_producer import pull_fr
producer_url = 'http://test_site_producer:8000'
class TestEventProducer(unittest.TestCase):
# @classmethod
# def setUpClass(cls):
# frappe.print_sql(True)
# @classmethod
# def tearDownClass(cls):
# frappe.print_sql(False)
def setUp(self):
create_event_producer(producer_url)
def tearDown(self):
unsubscribe_doctypes(producer_url)
def test_insert(self):
producer = get_remote_site()
producer_doc = insert_into_producer(producer, 'test creation 1 sync')
@ -98,7 +109,7 @@ class TestEventProducer(unittest.TestCase):
def test_dynamic_link_dependencies_synced(self):
producer = get_remote_site()
#unsubscribe for Note to check whether dependency is fulfilled
event_producer = frappe.get_doc('Event Producer', producer_url)
event_producer = frappe.get_doc('Event Producer', producer_url, for_update=True)
event_producer.producer_doctypes = []
event_producer.append('producer_doctypes', {
'ref_doctype': 'ToDo',
@ -126,7 +137,7 @@ class TestEventProducer(unittest.TestCase):
def test_naming_configuration(self):
#test with use_same_name = 0
producer = get_remote_site()
event_producer = frappe.get_doc('Event Producer', producer_url)
event_producer = frappe.get_doc('Event Producer', producer_url, for_update=True)
event_producer.producer_doctypes = []
event_producer.append('producer_doctypes', {
'ref_doctype': 'ToDo',
@ -167,7 +178,7 @@ class TestEventProducer(unittest.TestCase):
def test_mapping(self):
producer = get_remote_site()
event_producer = frappe.get_doc('Event Producer', producer_url)
event_producer = frappe.get_doc('Event Producer', producer_url, for_update=True)
event_producer.producer_doctypes = []
mapping = [{
'local_fieldname': 'description',
@ -205,36 +216,8 @@ class TestEventProducer(unittest.TestCase):
def test_inner_mapping(self):
producer = get_remote_site()
event_producer = frappe.get_doc('Event Producer', producer_url)
event_producer.producer_doctypes = []
inner_mapping = [
{
'local_fieldname':'role_name',
'remote_fieldname':'title'
}
]
inner_map = get_mapping('Role to Note Dependency Creation', 'Role', 'Note', inner_mapping)
mapping = [
{
'local_fieldname':'description',
'remote_fieldname':'content',
},
{
'local_fieldname': 'role',
'remote_fieldname': 'title',
'mapping_type': 'Document',
'mapping': inner_map,
'remote_value_filters': json.dumps({'title': 'title'})
}
]
event_producer.append('producer_doctypes', {
'ref_doctype': 'ToDo',
'use_same_name': 1,
'has_mapping': 1,
'mapping': get_mapping('ToDo to Note Mapping', 'ToDo', 'Note', mapping)
})
event_producer.save()
setup_event_producer_for_inner_mapping()
producer_note = frappe._dict(doctype='Note', title='Inner Mapping Tester', content='Test Inner Mapping')
delete_on_remote_if_exists(producer, 'Note', {'title': producer_note.title})
producer_note = producer.insert(producer_note)
@ -248,6 +231,39 @@ class TestEventProducer(unittest.TestCase):
reset_configuration(producer_url)
def setup_event_producer_for_inner_mapping():
event_producer = frappe.get_doc('Event Producer', producer_url, for_update=True)
event_producer.producer_doctypes = []
inner_mapping = [
{
'local_fieldname':'role_name',
'remote_fieldname':'title'
}
]
inner_map = get_mapping('Role to Note Dependency Creation', 'Role', 'Note', inner_mapping)
mapping = [
{
'local_fieldname':'description',
'remote_fieldname':'content',
},
{
'local_fieldname': 'role',
'remote_fieldname': 'title',
'mapping_type': 'Document',
'mapping': inner_map,
'remote_value_filters': json.dumps({'title': 'title'})
}
]
event_producer.append('producer_doctypes', {
'ref_doctype': 'ToDo',
'use_same_name': 1,
'has_mapping': 1,
'mapping': get_mapping('ToDo to Note Mapping', 'ToDo', 'Note', mapping)
})
event_producer.save()
return event_producer
def insert_into_producer(producer, description):
#create and insert todo on remote site
todo = dict(doctype='ToDo', description=description, assigned_by='Administrator')
@ -276,7 +292,12 @@ def get_mapping(mapping_name, local, remote, field_map):
def create_event_producer(producer_url):
if frappe.db.exists('Event Producer', producer_url):
event_producer = frappe.get_doc('Event Producer', producer_url)
for entry in event_producer.producer_doctypes:
entry.unsubscribe = 0
event_producer.save()
return
event_producer = frappe.new_doc('Event Producer')
event_producer.producer_doctypes = []
event_producer.producer_url = producer_url
@ -292,7 +313,7 @@ def create_event_producer(producer_url):
event_producer.save()
def reset_configuration(producer_url):
event_producer = frappe.get_doc('Event Producer', producer_url)
event_producer = frappe.get_doc('Event Producer', producer_url, for_update=True)
event_producer.producer_doctypes = []
event_producer.producer_url = producer_url
event_producer.append('producer_doctypes', {
@ -315,3 +336,9 @@ def get_remote_site():
frappe_authorization_source='Event Consumer'
)
return producer_site
def unsubscribe_doctypes(producer_url):
event_producer = frappe.get_doc('Event Producer', producer_url)
for entry in event_producer.producer_doctypes:
entry.unsubscribe = 1
event_producer.save()

View file

@ -8,11 +8,13 @@
"ref_doctype",
"status",
"use_same_name",
"unsubscribe",
"has_mapping",
"mapping"
],
"fields": [
{
"columns": 3,
"fieldname": "ref_doctype",
"fieldtype": "Link",
"in_list_view": 1,
@ -36,6 +38,7 @@
"options": "Document Type Mapping"
},
{
"columns": 2,
"default": "0",
"description": "If this is checked the documents will have the same name as they have on the Event Producer's site",
"fieldname": "use_same_name",
@ -44,6 +47,7 @@
"label": "Use Same Name"
},
{
"columns": 3,
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
@ -51,11 +55,19 @@
"label": "Approval Status",
"options": "Pending\nApproved\nRejected",
"read_only": 1
},
{
"columns": 2,
"default": "0",
"fieldname": "unsubscribe",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Unsubscribe"
}
],
"istable": 1,
"links": [],
"modified": "2019-12-30 11:42:36.032351",
"modified": "2020-08-14 11:38:01.278996",
"modified_by": "Administrator",
"module": "Event Streaming",
"name": "Event Producer Document Type",

View file

@ -9,17 +9,40 @@ from frappe.utils.background_jobs import get_jobs
from frappe.model import no_value_fields, table_fields
class EventUpdateLog(Document):
pass
def after_insert(self):
"""Send update notification updates to event consumers
whenever update log is generated"""
enqueued_method = 'frappe.event_streaming.doctype.event_consumer.event_consumer.notify_event_consumers'
jobs = get_jobs()
if not jobs or enqueued_method not in jobs[frappe.local.site]:
frappe.enqueue(enqueued_method, doctype=self.ref_doctype, queue='long',
enqueue_after_commit=True)
def notify_consumers(doc, event):
'''called via hooks'''
# make event update log for doctypes having event consumers
if frappe.flags.in_install or frappe.flags.in_migrate:
return
def notify_consumers(doc, _method=None):
"""Send update notification updates to event consumers
whenever update log is generated"""
enqueued_method = 'frappe.event_streaming.doctype.event_consumer.event_consumer.notify_event_consumers'
jobs = get_jobs()
if not jobs or enqueued_method not in jobs[frappe.local.site]:
frappe.enqueue(enqueued_method, doctype=doc.ref_doctype, queue='long', enqueue_after_commit=True)
consumers = check_doctype_has_consumers(doc.doctype)
if consumers:
if event=='after_insert':
doc.flags.event_update_log = make_event_update_log(doc, update_type='Create')
elif event=='on_trash':
make_event_update_log(doc, update_type='Delete')
else:
# on_update
# called after saving
if not doc.flags.event_update_log: # if not already inserted
diff = get_update(doc.get_doc_before_save(), doc)
if diff:
doc.diff = diff
make_event_update_log(doc, update_type='Update')
def check_doctype_has_consumers(doctype):
"""Check if doctype has event consumers for event streaming"""
return frappe.cache_manager.get_doctype_map('Event Consumer Document Type', doctype,
dict(ref_doctype=doctype, status='Approved', unsubscribed=0))
def get_update(old, new, for_child=False):
"""
@ -60,6 +83,20 @@ def get_update(old, new, for_child=False):
return out
return None
def make_event_update_log(doc, update_type):
"""Save update info for doctypes that have event consumers"""
if update_type != 'Delete':
# diff for update type, doc for create type
data = frappe.as_json(doc) if not doc.get('diff') else frappe.as_json(doc.diff)
else:
data = None
return frappe.get_doc({
'doctype': 'Event Update Log',
'update_type': update_type,
'ref_doctype': doc.doctype,
'docname': doc.name,
'data': data
}).insert(ignore_permissions=True)
def make_maps(old_value, new_value):
"""make maps"""

View file

@ -104,6 +104,7 @@ class IncompatibleApp(ValidationError): pass
class InvalidDates(ValidationError): pass
class DataTooLongException(ValidationError): pass
class FileAlreadyAttachedException(Exception): pass
class DocumentAlreadyRestored(Exception): pass
# OAuth exceptions
class InvalidAuthorizationHeader(CSRFTokenError): pass
class InvalidAuthorizationPrefix(CSRFTokenError): pass

View file

@ -620,7 +620,12 @@
},
"Congo, The Democratic Republic of the": {
"code": "cd",
"number_format": "#,###.##"
"number_format": "#,###.##",
"currency": "CDF",
"currency_name": "Congolese franc",
"currency_symbol": "FC",
"currency_fraction": "Centime",
"currency_fraction_units": 100
},
"Cook Islands": {
"code": "ck",

View file

@ -127,12 +127,16 @@ standard_queries = {
doc_events = {
"*": {
"after_insert": [
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
],
"on_update": [
"frappe.desk.notifications.clear_doctype_notifications",
"frappe.core.doctype.activity_log.feed.update_feed",
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
"frappe.automation.doctype.assignment_rule.assignment_rule.apply",
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone"
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone",
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
],
"after_rename": "frappe.desk.notifications.clear_doctype_notifications",
"on_cancel": [
@ -141,7 +145,8 @@ doc_events = {
],
"on_trash": [
"frappe.desk.notifications.clear_doctype_notifications",
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions"
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
],
"on_change": [
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points"
@ -163,9 +168,6 @@ doc_events = {
"Page": {
"after_insert": "frappe.cache_manager.build_domain_restriced_page_cache",
"after_save": "frappe.cache_manager.build_domain_restriced_page_cache",
},
"Event Update Log": {
"after_insert": "frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
}
}
@ -272,9 +274,6 @@ setup_wizard_exception = [
]
before_migrate = ['frappe.patches.v11_0.sync_user_permission_doctype_before_migrate.execute']
after_migrate = [
'frappe.modules.full_text_search.build_index_for_all_routes'
]
otp_methods = ['OTP App','Email','SMS']
user_privacy_documents = [

View file

@ -74,11 +74,11 @@
},
{
"default": "us-east-1",
"description": "See https://docs.aws.amazon.com/de_de/general/latest/gr/rande.html#s3_region for details.",
"description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.",
"fieldname": "region",
"fieldtype": "Select",
"label": "Region",
"options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-north-1\nsa-east-1"
"options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1"
},
{
"fieldname": "endpoint_url",
@ -151,7 +151,7 @@
"hide_toolbar": 1,
"issingle": 1,
"links": [],
"modified": "2020-04-13 20:57:24.432183",
"modified": "2020-07-27 17:27:21.400000",
"modified_by": "Administrator",
"module": "Integrations",
"name": "S3 Backup Settings",
@ -172,4 +172,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View file

@ -0,0 +1,32 @@
{
"actions": [],
"autoname": "field:phone_number",
"creation": "2020-02-24 13:58:58.036914",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"phone_number"
],
"fields": [
{
"fieldname": "phone_number",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Phone Number",
"unique": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-03-02 14:54:34.396254",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Twilio Number Group",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class TwilioNumberGroup(Document):
pass

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestTwilioSettings(unittest.TestCase):
pass

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Twilio Settings', {
refresh: function(frm) {
frm.dashboard.set_headline(__("For more information, {0}.", [`<a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/notifications'>${__('Click here')}</a>`]));
}
});

View file

@ -0,0 +1,57 @@
{
"actions": [],
"creation": "2020-01-28 15:21:44.457163",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account_sid",
"auth_token",
"column_break_2",
"twilio_number"
],
"fields": [
{
"fieldname": "account_sid",
"fieldtype": "Data",
"label": "Account SID"
},
{
"fieldname": "auth_token",
"fieldtype": "Password",
"label": "Auth Token"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "twilio_number",
"fieldtype": "Table",
"label": "Twilio Number",
"options": "Twilio Number Group"
}
],
"issingle": 1,
"links": [],
"modified": "2020-08-11 15:28:57.860554",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Twilio Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from twilio.rest import Client
from frappe import _
from frappe.utils.password import get_decrypted_password
from six import string_types
class TwilioSettings(Document):
pass
def send_whatsapp_message(sender, receiver_list, message):
import json
if isinstance(receiver_list, string_types):
receiver_list = json.loads(receiver_list)
if not isinstance(receiver_list, list):
receiver_list = [receiver_list]
twilio_settings = frappe.get_doc("Twilio Settings")
auth_token = get_decrypted_password("Twilio Settings", "Twilio Settings", 'auth_token')
client = Client(twilio_settings.account_sid, auth_token)
args = {
"from_": 'whatsapp:+{}'.format(sender),
"body": message
}
failed_delivery = []
for rec in receiver_list:
args.update({"to": 'whatsapp:{}'.format(rec)})
resp = _send_whatsapp(args, client)
if not resp or resp.error_message:
failed_delivery.append(rec)
if failed_delivery:
frappe.log_error(_("The message wasn't correctly delivered to: {}".format(", ".join(failed_delivery))), _('Delivery Failed'))
def _send_whatsapp(message_dict, client):
response = frappe._dict()
try:
response = client.messages.create(**message_dict)
except Exception as e:
frappe.log_error(e, title = _('Twilio WhatsApp Message Error'))
return response

View file

@ -12,8 +12,10 @@ from frappe.utils import split_emails, get_backups_path
def send_email(success, service_name, doctype, email_field, error_status=None):
recipients = get_recipients(doctype, email_field)
if not recipients:
frappe.log_error("No Email Recipient found for {0}".format(service_name),
"{0}: Failed to send backup status email".format(service_name))
frappe.log_error(
"No Email Recipient found for {0}".format(service_name),
"{0}: Failed to send backup status email".format(service_name),
)
return
if success:
@ -23,7 +25,9 @@ def send_email(success, service_name, doctype, email_field, error_status=None):
subject = "Backup Upload Successful"
message = """
<h3>Backup Uploaded Successfully!</h3>
<p>Hi there, this is just to inform you that your backup was successfully uploaded to your {0} bucket. So relax!</p>""".format(service_name)
<p>Hi there, this is just to inform you that your backup was successfully uploaded to your {0} bucket. So relax!</p>""".format(
service_name
)
else:
subject = "[Warning] Backup Upload Failed"
@ -31,7 +35,9 @@ def send_email(success, service_name, doctype, email_field, error_status=None):
<h3>Backup Upload Failed!</h3>
<p>Oops, your automated backup to {0} failed.</p>
<p>Error message: {1}</p>
<p>Please contact your system manager for more information.</p>""".format(service_name, error_status)
<p>Please contact your system manager for more information.</p>""".format(
service_name, error_status
)
frappe.sendmail(recipients=recipients, subject=subject, message=message)
@ -44,29 +50,31 @@ def get_recipients(doctype, email_field):
def get_latest_backup_file(with_files=False):
from frappe.utils.backups import BackupGenerator
def get_latest(file_ext):
file_list = glob.glob(os.path.join(get_backups_path(), file_ext))
return max(file_list, key=os.path.getctime) if file_list else None
latest_file = get_latest('*.sql.gz')
latest_site_config = get_latest('*.json')
odb = BackupGenerator(
frappe.conf.db_name,
frappe.conf.db_name,
frappe.conf.db_password,
db_host=frappe.db.host,
db_type=frappe.conf.db_type,
db_port=frappe.conf.db_port,
)
database, public, private, config = odb.get_recent_backup(older_than=24 * 30)
if with_files:
latest_public_file_bak = get_latest('*-files.tar')
latest_private_file_bak = get_latest('*-private-files.tar')
return latest_file, latest_site_config, latest_public_file_bak, latest_private_file_bak
return database, config, public, private
return latest_file, latest_site_config
return database, config
def get_file_size(file_path, unit):
if not unit:
unit = 'MB'
unit = "MB"
file_size = os.path.getsize(file_path)
memory_size_unit_mapper = {'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4}
memory_size_unit_mapper = {"KB": 1, "MB": 2, "GB": 3, "TB": 4}
i = 0
while i < memory_size_unit_mapper[unit]:
file_size = file_size / 1000.0
@ -78,7 +86,7 @@ def get_file_size(file_path, unit):
def validate_file_size():
frappe.flags.create_new_backup = True
latest_file, site_config = get_latest_backup_file()
file_size = get_file_size(latest_file, unit='GB')
file_size = get_file_size(latest_file, unit="GB")
if file_size > 1:
frappe.flags.create_new_backup = False

View file

@ -19,10 +19,10 @@ from frappe.website import render
from frappe.core.doctype.language.language import sync_languages
from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.utils import global_search
from frappe.search.website_search import build_index_for_all_routes
def migrate(verbose=True, rebuild_website=False, skip_failing=False):
def migrate(verbose=True, rebuild_website=False, skip_failing=False, skip_search_index=False):
'''Migrate all apps to the latest version, will:
- run before migrate hooks
- run patches
@ -80,9 +80,6 @@ Otherwise, check the server logs and ensure that all the required services are r
# syncs statics
render.clear_cache()
# add static pages to global search
global_search.update_global_search_for_all_web_pages()
# updating installed applications data
frappe.get_single('Installed Applications').update_versions()
@ -91,6 +88,12 @@ Otherwise, check the server logs and ensure that all the required services are r
for fn in frappe.get_hooks('after_migrate', app_name=app):
frappe.get_attr(fn)()
# build web_routes index
if not skip_search_index:
# Run this last as it updates the current session
print('Building search index for {}'.format(frappe.local.site))
build_index_for_all_routes()
frappe.db.commit()
clear_notifications()

Some files were not shown because too many files have changed in this diff Show more