Merge branch 'develop' of github.com:frappe/frappe into skip-backup-tables

This commit is contained in:
Gavin D'souza 2020-08-28 12:19:31 +05:30
commit bf11c70b46
260 changed files with 6656 additions and 3610 deletions

View file

@ -9,7 +9,7 @@ Welcome to the Frappe Framework issue tracker! Before creating an issue, please
1. This tracker should only be used to report bugs and request features / enhancements to Frappe
- For questions and general support, use https://stackoverflow.com/questions/tagged/frappe
- For documentation issues, refer to https://frappe.io/docs/user/en or the developer cheetsheet https://github.com/frappe/frappe/wiki/Developer-Cheatsheet
- For documentation issues, refer to https://frappeframework.com/docs/user/en or the developer cheetsheet https://github.com/frappe/frappe/wiki/Developer-Cheatsheet
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for

View file

@ -9,7 +9,7 @@ Welcome to the Frappe Framework issue tracker! Before creating an issue, please
1. This tracker should only be used to report bugs and request features / enhancements to Frappe
- For questions and general support, refer to https://stackoverflow.com/questions/tagged/frappe
- For documentation issues, use https://frappe.io/docs/user/en or the developer cheetsheet https://github.com/frappe/frappe/wiki/Developer-Cheatsheet
- For documentation issues, use https://frappeframework.com/docs/user/en or the developer cheetsheet https://frappeframework.com/docs/user/en/bench/resources/bench-commands-cheatsheet
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.

View file

@ -12,7 +12,7 @@ for questions about using `ERPNext`: https://discuss.erpnext.com
for questions about using `bench`, probably the best place to start is the [bench repo](https://github.com/frappe/bench)
For documentation issues, use the [Frappe Framework Documentation](https://frappe.io/docs/user/en) or the [developer cheetsheet](https://github.com/frappe/frappe/wiki/Developer-Cheatsheet)
For documentation issues, use the [Frappe Framework Documentation](https://frappeframework.com/docs) or the [developer cheetsheet](https://github.com/frappe/frappe/wiki/Developer-Cheatsheet)
For a slightly outdated yet informative developer guide: https://www.youtube.com/playlist?list=PL3lFfCEoMxvzHtsZHFJ4T3n5yMM3nGJ1W

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View file

@ -22,7 +22,7 @@ for _file in files:
print(f'A syntax error has been discovered at line number: {num}')
print(f'Syntax error occurred with: {line}')
if errors_encounter > 0:
print('You can visit "https://frappe.io/docs/user/en/translations" to resolve this error.')
print('You can visit "https://frappeframework.com/docs/user/en/translations" to resolve this error.')
assert 1+1 == 3
else:
print('Good To Go!')

5
.gitignore vendored
View file

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

4
.snyk
View file

@ -1,5 +1,5 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.14.1
version: v1.19.0
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
SNYK-JS-AWESOMPLETE-174474:
@ -63,3 +63,5 @@ patch:
patched: '2020-04-30T23:02:32.330Z'
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash:
patched: '2020-04-30T23:02:32.330Z'
- quill-image-resize > lodash:
patched: '2020-08-24T23:06:37.710Z'

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

@ -17,7 +17,7 @@
<a href="https://travis-ci.com/frappe/frappe">
<img src="https://travis-ci.com/frappe/frappe.svg?branch=develop">
</a>
<a href='https://frappe.io/docs'>
<a href='https://frappeframework.com/docs'>
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/>
</a>
<a href='https://www.codetriage.com/frappe/frappe'>

View file

@ -61,7 +61,7 @@ context('Recorder', () => {
cy.visit('/desk#recorder');
cy.contains('.list-row-container span', 'frappe.desk.reportview.get').click();
cy.get('.list-row-container span').contains('frappe.desk.reportview.get').click();
cy.location('hash').should('contain', '#recorder/request/');
cy.get('form').should('contain', 'frappe.desk.reportview.get');

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

@ -73,19 +73,32 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
frappe.throw(_("No permission for {0}").format(doctype), frappe.PermissionError)
filters = get_safe_filters(filters)
if isinstance(filters, string_types):
filters = {"name": filters}
try:
fieldname = json.loads(fieldname)
fields = json.loads(fieldname)
except (TypeError, ValueError):
# name passed, not json
pass
fields = [fieldname]
# check whether the used filters were really parseable and usable
# and did not just result in an empty string or dict
if not filters:
filters = None
return frappe.db.get_value(doctype, filters, fieldname, as_dict=as_dict, debug=debug)
if frappe.get_meta(doctype).issingle:
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug)
else:
value = frappe.get_list(doctype, filters=filters, fields=fields, debug=debug, limit=1)
if as_dict:
value = value[0] if value else {}
else:
value = value[0].fieldname
return value
@frappe.whitelist()
def get_single_value(doctype, field):

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

@ -27,6 +27,11 @@ def get_data():
"name": "Stripe Settings",
"description": _("Stripe payment gateway settings"),
},
{
"type": "doctype",
"name": "Paytm Settings",
"description": _("Paytm payment gateway settings"),
},
]
},
{

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,11 +183,12 @@ 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
if not frappe.get_meta("Contact").get_field(searchfield)\
or searchfield not in frappe.db.DEFAULT_COLUMNS:
and searchfield not in frappe.db.DEFAULT_COLUMNS:
return []
link_doctype = filters.pop('link_doctype')

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"))
@ -465,6 +465,8 @@ class ImportFile:
if doctype != self.doctype and table_df:
child_doc = row.parse_doc(doctype, parent_doc, table_df)
if child_doc is None:
continue
parent_doc[table_df.fieldname] = parent_doc.get(table_df.fieldname, [])
parent_doc[table_df.fieldname].append(child_doc)
@ -570,6 +572,11 @@ class Row:
def parse_doc(self, doctype, parent_doc=None, table_df=None):
col_indexes = self.header.get_column_indexes(doctype, table_df)
values = self.get_values(col_indexes)
if all(v in INVALID_VALUES for v in values):
# if all values are invalid, no need to parse it
return None
columns = self.header.get_columns(col_indexes)
doc = self._parse_doc(doctype, columns, values, parent_doc, table_df)
return doc
@ -602,12 +609,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 +630,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 +646,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 +675,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 +696,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 +793,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 +923,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 +967,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 +1032,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 +1112,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 +1206,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.369095",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -989,7 +989,8 @@ def clear_permissions_cache(doctype):
`tabHas Role`,
`tabDocPerm`
WHERE `tabDocPerm`.`parent` = %s
AND `tabDocPerm`.`role` = `tabHas Role`.`role`
AND `tabDocPerm`.`role` = `tabHas Role`.`role`
AND `tabHas Role`.`parenttype` = 'User'
""", doctype):
frappe.clear_cache(user=user)

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

@ -922,3 +922,40 @@ def update_existing_file_docs(doc):
content_hash=doc.content_hash,
file_name=doc.name
))
def attach_files_to_document(doc, event):
""" Runs on on_update hook of all documents.
Goes through every Attach and Attach Image field and attaches
the file url to the document if it is not already attached.
"""
attach_fields = doc.meta.get(
"fields", {"fieldtype": ["in", ["Attach", "Attach Image"]]}
)
for df in attach_fields:
# this method runs in on_update hook of all documents
# we dont want the update to fail if file cannot be attached for some reason
try:
value = doc.get(df.fieldname)
if not value.startswith(("/files", "/private/files")):
return
if frappe.db.exists("File", {
"file_url": value,
"attached_to_name": doc.name,
"attached_to_doctype": doc.doctype,
"attached_to_field": df.fieldname,
}):
return
frappe.get_doc(
doctype="File",
file_url=value,
attached_to_name=doc.name,
attached_to_doctype=doc.doctype,
attached_to_field=df.fieldname,
folder="Home/Attachments",
).insert()
except Exception:
frappe.log_error(title=_("Error Attaching File"))

View file

@ -328,3 +328,49 @@ class TestFile(unittest.TestCase):
self.assertTrue(os.path.exists(file2.get_full_path()))
class TestAttachment(unittest.TestCase):
test_doctype = 'Test For Attachment'
def setUp(self):
if frappe.db.exists('DocType', self.test_doctype):
return
frappe.get_doc(
doctype='DocType',
name=self.test_doctype,
module='Custom',
custom=1,
fields=[
{'label': 'Title', 'fieldname': 'title', 'fieldtype': 'Data'},
{'label': 'Attachment', 'fieldname': 'attachment', 'fieldtype': 'Attach'},
]
).insert()
def tearDown(self):
frappe.delete_doc('DocType', self.test_doctype)
def test_file_attachment_on_update(self):
doc = frappe.get_doc(
doctype=self.test_doctype,
title='test for attachment on update'
).insert()
file = frappe.get_doc({
'doctype': 'File',
'file_name': 'test_attach.txt',
'content': 'Test Content'
})
file.save()
doc.attachment = file.file_url
doc.save()
exists = frappe.db.exists('File', {
'file_name': 'test_attach.txt',
'file_url': file.file_url,
'attached_to_doctype': self.test_doctype,
'attached_to_name': doc.name,
'attached_to_field': 'attachment'
})
self.assertTrue(exists)

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

@ -69,6 +69,40 @@ def run_background(prepared_report):
user=frappe.session.user
)
@frappe.whitelist()
def get_reports_in_queued_state(report_name, filters):
reports = frappe.get_all('Prepared Report',
filters = {
'report_name': report_name,
'filters': json.dumps(json.loads(filters)),
'status': 'Queued'
})
return reports
def delete_expired_prepared_reports():
system_settings = frappe.get_single('System Settings')
enable_auto_deletion = system_settings.enable_prepared_report_auto_deletion
if enable_auto_deletion:
expiry_period = system_settings.prepared_report_expiry_period
prepared_reports_to_delete = frappe.get_all('Prepared Report',
filters = {
'creation': ['<', frappe.utils.add_days(frappe.utils.now(), -expiry_period)]
})
args = {
'reports': prepared_reports_to_delete,
'limit': 50
}
enqueue(method=delete_prepared_reports, job_name="delete_prepared_reports", **args)
@frappe.whitelist()
def delete_prepared_reports(reports, limit=None):
reports = frappe.parse_json(reports)
for index, doc in enumerate(reports):
if limit and index == limit:
return
frappe.delete_doc('Prepared Report', doc['name'], ignore_permissions=True)
def create_json_gz_file(data, dt, dn):
# Storing data in CSV file causes information loss

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();
}
@ -42,26 +42,6 @@ frappe.ui.form.on('Report', {
}
},
report_type: function(frm) {
frm.set_intro("");
switch(frm.doc.report_type) {
case "Report Builder":
frm.set_intro(__("Report Builder reports are managed directly by the report builder. Nothing to do."));
break;
case "Query Report":
frm.set_intro(__("Write a SELECT query. Note result is not paged (all data is sent in one go).")
+ __("To format columns, give column labels in the query.") + "<br>"
+ __("[Label]:[Field Type]/[Options]:[Width]") + "<br><br>"
+ __("Example:") + "<br>"
+ "Employee:Link/Employee:200" + "<br>"
+ "Rate:Currency:120" + "<br>")
break;
case "Script Report":
frm.set_intro(__("Write a Python file in the same folder where this is saved and return column and result."));
break;
}
},
set_doctype_roles: function(frm) {
return frm.call('set_doctype_roles').then(() => {
frm.refresh_field('roles');

View file

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "field:report_name",
"creation": "2013-03-09 15:45:57",
"doctype": "DocType",
@ -17,10 +18,15 @@
"disabled",
"disable_prepared_report",
"prepared_report",
"filters_section",
"filters",
"columns_section",
"columns",
"section_break_6",
"query",
"javascript",
"report_script",
"client_code_section",
"javascript",
"json",
"permission_rules",
"roles"
@ -94,7 +100,8 @@
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Query / Script"
},
{
"depends_on": "eval:doc.report_type==\"Query Report\"",
@ -142,15 +149,50 @@
"read_only": 1
},
{
"depends_on": "eval:doc.report_type==\"Script Report\" && doc.is_standard===\"No\"",
"description": "output in the form of `data = [columns, result]`",
"depends_on": "eval:(doc.report_type===\"Script Report\" \n|| doc.report_type==\"Query Report\") \n&& doc.is_standard===\"No\"",
"description": "Filters will be accessible via <code>filters</code>. <br><br>Send output as <code>result = [result]</code>, or for old style <code>data = [columns], [result]</code>",
"fieldname": "report_script",
"fieldtype": "Code",
"label": "Script"
},
{
"collapsible": 1,
"collapsible_depends_on": "filters",
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"fieldname": "filters",
"fieldtype": "Table",
"label": "Filters",
"options": "Report Filter"
},
{
"collapsible": 1,
"collapsible_depends_on": "columns",
"fieldname": "columns_section",
"fieldtype": "Section Break",
"label": "Columns"
},
{
"fieldname": "columns",
"fieldtype": "Table",
"label": "Columns",
"options": "Report Column"
},
{
"collapsible": 1,
"collapsible_depends_on": "javascript",
"fieldname": "client_code_section",
"fieldtype": "Section Break",
"label": "Client Code"
}
],
"idx": 1,
"modified": "2019-10-09 15:43:08.577610",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-17 16:49:28.474274",
"modified_by": "Administrator",
"module": "Core",
"name": "Report",

View file

@ -51,6 +51,9 @@ class Report(Document):
def on_trash(self):
delete_custom_role('report', self.name)
def get_columns(self):
return [d.as_dict(no_default_fields = True) for d in self.columns]
def set_doctype_roles(self):
if not self.get('roles') and self.is_standard == 'No':
meta = frappe.get_meta(self.ref_doctype)
@ -99,8 +102,8 @@ class Report(Document):
if not self.query.lower().startswith("select"):
frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error'))
result = [list(t) for t in frappe.db.sql(self.query, filters)]
columns = [cstr(c[0]) for c in frappe.db.get_description()]
result = [list(t) for t in frappe.db.sql(self.query, filters, debug=True)]
columns = self.get_columns() or [cstr(c[0]) for c in frappe.db.get_description()]
return [columns, result]
@ -134,121 +137,167 @@ class Report(Document):
def execute_script(self, filters):
# server script
loc = {"filters": frappe._dict(filters), 'data':[]}
loc = {"filters": frappe._dict(filters), 'data':None, 'result':None}
safe_exec(self.report_script, None, loc)
return loc['data']
if loc['data']:
return loc['data']
else:
return self.get_columns(), loc['result']
def get_data(self, filters=None, limit=None, user=None, as_dict=False, ignore_prepared_report=False):
columns = []
out = []
if self.report_type in ('Query Report', 'Script Report', 'Custom Report'):
# query and script reports
data = frappe.desk.query_report.run(self.name,
filters=filters, user=user, ignore_prepared_report=ignore_prepared_report)
for d in data.get('columns'):
if isinstance(d, dict):
col = frappe._dict(d)
if not col.fieldname:
col.fieldname = col.label
columns.append(col)
else:
fieldtype, options = "Data", None
parts = d.split(':')
if len(parts) > 1:
if parts[1]:
fieldtype, options = parts[1], None
if fieldtype and '/' in fieldtype:
fieldtype, options = fieldtype.split('/')
columns.append(frappe._dict(label=parts[0], fieldtype=fieldtype, fieldname=parts[0], options=options))
out += data.get('result')
columns, result = self.run_query_report(filters, user, ignore_prepared_report)
else:
# standard report
params = json.loads(self.json)
if params.get('fields'):
columns = params.get('fields')
elif params.get('columns'):
columns = params.get('columns')
elif params.get('fields'):
columns = params.get('fields')
else:
columns = [['name', self.ref_doctype]]
for df in frappe.get_meta(self.ref_doctype).fields:
if df.in_list_view:
columns.append([df.fieldname, self.ref_doctype])
_filters = params.get('filters') or []
if filters:
for key, value in iteritems(filters):
condition, _value = '=', value
if isinstance(value, (list, tuple)):
condition, _value = value
_filters.append([key, condition, _value])
def _format(parts):
# sort by is saved as DocType.fieldname, covert it to sql
return '`tab{0}`.`{1}`'.format(*parts)
if params.get('sort_by'):
order_by = _format(params.get('sort_by').split('.')) + ' ' + params.get('sort_order')
elif params.get('order_by'):
order_by = params.get('order_by')
else:
order_by = _format([self.ref_doctype, 'modified']) + ' desc'
if params.get('sort_by_next'):
order_by += ', ' + _format(params.get('sort_by_next').split('.')) + ' ' + params.get('sort_order_next')
result = frappe.get_list(self.ref_doctype,
fields = [_format([c[1], c[0]]) for c in columns],
filters=_filters,
order_by = order_by,
as_list=True,
limit=limit,
user=user)
_columns = []
for (fieldname, doctype) in columns:
meta = frappe.get_meta(doctype)
if meta.get_field(fieldname):
field = meta.get_field(fieldname)
else:
field = frappe._dict(fieldname=fieldname, label=meta.get_label(fieldname))
# since name is the primary key for a document, it will always be a Link datatype
if fieldname == "name":
field.fieldtype = "Link"
field.options = doctype
_columns.append(field)
columns = _columns
out = out + [list(d) for d in result]
if params.get('add_totals_row'):
out = append_totals_row(out)
columns, result = self.run_standard_report(filters, limit, user)
if as_dict:
data = []
for row in out:
if isinstance(row, (list, tuple)):
_row = frappe._dict()
for i, val in enumerate(row):
_row[columns[i].get('fieldname')] = val
elif isinstance(row, dict):
# no need to convert from dict to dict
_row = frappe._dict(row)
data.append(_row)
else:
data = out
return columns, data
result = self.build_data_dict(result, columns)
return columns, result
def run_query_report(self, filters, user, ignore_prepared_report=False):
columns, result = [], []
data = frappe.desk.query_report.run(self.name,
filters=filters, user=user, ignore_prepared_report=ignore_prepared_report)
for d in data.get('columns'):
if isinstance(d, dict):
col = frappe._dict(d)
if not col.fieldname:
col.fieldname = col.label
columns.append(col)
else:
fieldtype, options = "Data", None
parts = d.split(':')
if len(parts) > 1:
if parts[1]:
fieldtype, options = parts[1], None
if fieldtype and '/' in fieldtype:
fieldtype, options = fieldtype.split('/')
columns.append(frappe._dict(label=parts[0], fieldtype=fieldtype, fieldname=parts[0], options=options))
result += data.get('result')
return columns, result
def run_standard_report(self, filters, limit, user):
params = json.loads(self.json)
columns = self.get_standard_report_columns(params)
result = []
order_by, group_by, group_by_args = self.get_standard_report_order_by(params)
_result = frappe.get_list(self.ref_doctype,
fields = [
get_group_by_field(group_by_args, c[1]) if c[0] == '_aggregate_column' and group_by_args
else Report._format([c[1], c[0]]) for c in columns
],
filters = self.get_standard_report_filters(params, filters),
order_by = order_by,
group_by = group_by,
as_list = True,
limit = limit,
user = user)
columns = self.build_standard_report_columns(columns, group_by_args)
result = result + [list(d) for d in _result]
if params.get('add_totals_row'):
result = append_totals_row(result)
return columns, result
@staticmethod
def _format(parts):
# sort by is saved as DocType.fieldname, covert it to sql
return '`tab{0}`.`{1}`'.format(*parts)
def get_standard_report_columns(self, params):
if params.get('fields'):
columns = params.get('fields')
elif params.get('columns'):
columns = params.get('columns')
elif params.get('fields'):
columns = params.get('fields')
else:
columns = [['name', self.ref_doctype]]
for df in frappe.get_meta(self.ref_doctype).fields:
if df.in_list_view:
columns.append([df.fieldname, self.ref_doctype])
return columns
def get_standard_report_filters(self, params, filters):
_filters = params.get('filters') or []
if filters:
for key, value in iteritems(filters):
condition, _value = '=', value
if isinstance(value, (list, tuple)):
condition, _value = value
_filters.append([key, condition, _value])
return _filters
def get_standard_report_order_by(self, params):
group_by_args = None
if params.get('sort_by'):
order_by = Report._format(params.get('sort_by').split('.')) + ' ' + params.get('sort_order')
elif params.get('order_by'):
order_by = params.get('order_by')
else:
order_by = Report._format([self.ref_doctype, 'modified']) + ' desc'
if params.get('sort_by_next'):
order_by += ', ' + Report._format(params.get('sort_by_next').split('.')) + ' ' + params.get('sort_order_next')
group_by = None
if params.get('group_by'):
group_by_args = frappe._dict(params['group_by'])
group_by = group_by_args['group_by']
order_by = '_aggregate_column desc'
return order_by, group_by, group_by_args
def build_standard_report_columns(self, columns, group_by_args):
_columns = []
for (fieldname, doctype) in columns:
meta = frappe.get_meta(doctype)
if meta.get_field(fieldname):
field = meta.get_field(fieldname)
else:
if fieldname == '_aggregate_column':
label = get_group_by_column_label(group_by_args, meta)
else:
label = meta.get_label(fieldname)
field = frappe._dict(fieldname=fieldname, label=label)
# since name is the primary key for a document, it will always be a Link datatype
if fieldname == "name":
field.fieldtype = "Link"
field.options = doctype
_columns.append(field)
return _columns
def build_data_dict(self, result, columns):
data = []
for row in result:
if isinstance(row, (list, tuple)):
_row = frappe._dict()
for i, val in enumerate(row):
_row[columns[i].get('fieldname')] = val
elif isinstance(row, dict):
# no need to convert from dict to dict
_row = frappe._dict(row)
data.append(_row)
return data
@Document.whitelist
def toggle_disable(self, disable):
@ -262,3 +311,30 @@ def is_prepared_report_disabled(report):
def get_report_module_dotted_path(module, report_name):
return frappe.local.module_app[scrub(module)] + "." + scrub(module) \
+ ".report." + scrub(report_name) + "." + scrub(report_name)
def get_group_by_field(args, doctype):
if args['aggregate_function'] == 'count':
group_by_field = 'count(*) as _aggregate_column'
else:
group_by_field = '{0}(`tab{1}`.{2}) as _aggregate_column'.format(
args.aggregate_function,
doctype,
args.aggregate_on
)
return group_by_field
def get_group_by_column_label(args, meta):
if args['aggregate_function'] == 'count':
label = 'Count'
else:
sql_fn_map = {
'avg': 'Average',
'sum': 'Sum'
}
aggregate_on_label = meta.get_label(args.aggregate_on)
label = _('{function} of {fieldlabel}').format(
function=sql_fn_map[args.aggregate_function],
fieldlabel = aggregate_on_label
)
return label

View file

@ -111,3 +111,41 @@ data = [
# check values
self.assertTrue('System User' in [d.get('type') for d in data[1]])
def test_script_report_with_columns(self):
report_name = 'Test Script Report With Columns'
if frappe.db.exists("Report", report_name):
frappe.delete_doc('Report', report_name)
report = frappe.get_doc({
'doctype': 'Report',
'ref_doctype': 'User',
'report_name': report_name,
'report_type': 'Script Report',
'is_standard': 'No',
'columns': [
dict(fieldname='type', label='Type', fieldtype='Data'),
dict(fieldname='value', label='Value', fieldtype='Int'),
]
}).insert(ignore_permissions=True)
report.report_script = '''
totals = {}
for user in frappe.get_all('User', fields = ['name', 'user_type', 'creation']):
if not user.user_type in totals:
totals[user.user_type] = 0
totals[user.user_type] = totals[user.user_type] + 1
result = [
{"type":key, "value": value} for key, value in totals.items()
]
'''
report.save()
data = report.get_data()
# check columns
self.assertEqual(data[0][0]['label'], 'Type')
# check values
self.assertTrue('System User' in [d.get('type') for d in data[1]])

View file

@ -0,0 +1,61 @@
{
"actions": [],
"creation": "2020-01-14 11:28:37.583656",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"fieldname",
"label",
"fieldtype",
"options",
"width"
],
"fields": [
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"reqd": 1
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"reqd": 1
},
{
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldtype",
"options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime",
"reqd": 1
},
{
"fieldname": "options",
"fieldtype": "Data",
"label": "Options"
},
{
"fieldname": "width",
"fieldtype": "Int",
"label": "Width"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-17 14:32:17.174796",
"modified_by": "Administrator",
"module": "Core",
"name": "Report Column",
"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 ReportColumn(Document):
pass

View file

@ -0,0 +1,71 @@
{
"actions": [],
"creation": "2020-01-14 11:38:58.016498",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"fieldname",
"label",
"fieldtype",
"mandatory",
"options",
"wildcard_filter"
],
"fields": [
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"reqd": 1
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"reqd": 1
},
{
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldtype",
"options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime",
"reqd": 1
},
{
"default": "0",
"fieldname": "mandatory",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Mandatory"
},
{
"fieldname": "options",
"fieldtype": "Data",
"label": "Options"
},
{
"default": "0",
"description": "Will add \"%\" before and after the query",
"fieldname": "wildcard_filter",
"fieldtype": "Check",
"label": "Wildcard Filter"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-17 16:15:46.937267",
"modified_by": "Administrator",
"module": "Core",
"name": "Report Filter",
"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 ReportFilter(Document):
pass

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

@ -27,3 +27,11 @@ frappe.ui.form.on("System Settings", "enable_two_factor_auth", function(frm) {
frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0);
}
});
frappe.ui.form.on("System Settings", "enable_prepared_report_auto_deletion", function(frm) {
if (frm.doc.enable_prepared_report_auto_deletion) {
if (!frm.doc.prepared_report_expiry_period) {
frm.set_value('prepared_report_expiry_period', 7);
}
}
});

View file

@ -60,6 +60,9 @@
"disable_standard_email_footer",
"hide_footer_in_auto_email_reports",
"attach_view_link",
"prepared_report_section",
"enable_prepared_report_auto_deletion",
"prepared_report_expiry_period",
"chat",
"enable_chat",
"use_socketio_to_upload_file"
@ -429,12 +432,32 @@
"fieldname": "attach_view_link",
"fieldtype": "Check",
"label": "Send document Web View link in email"
},
{
"default": "30",
"depends_on": "enable_prepared_report_auto_deletion",
"description": "System will automatically delete Prepared Reports after these many days since creation",
"fieldname": "prepared_report_expiry_period",
"fieldtype": "Int",
"label": "Prepared Report Expiry Period (Days)"
},
{
"default": "1",
"fieldname": "enable_prepared_report_auto_deletion",
"fieldtype": "Check",
"label": "Enable Auto-deletion of Prepared Reports"
},
{
"collapsible": 1,
"fieldname": "prepared_report_section",
"fieldtype": "Section Break",
"label": "Prepared Report"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2020-07-02 16:13:00.166382",
"modified": "2020-08-12 14:35:45.214327",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

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

@ -1,5 +0,0 @@
.restricted-button {
cursor: default;
position: relative;
right: -5px;
}

View file

@ -26,13 +26,6 @@ class Dashboard {
</div>`).appendTo(this.wrapper.find(".page-content").empty());
this.container = this.wrapper.find(".dashboard-graph");
this.page = wrapper.page;
this.page.set_title_sub(
$(`<button class="restricted-button">
<span class="octicon octicon-lock"></span>
<span>${__('Restricted')}</span>
</button>`)
);
}
show() {

View file

@ -334,6 +334,7 @@ frappe.PermissionEngine = Class.extend({
});
this.body.on("click", "input[type='checkbox']", function() {
frappe.dom.freeze();
var chk = $(this);
var args = {
role: chk.attr("data-role"),
@ -348,6 +349,7 @@ frappe.PermissionEngine = Class.extend({
method: "update",
args: args,
callback: function(r) {
frappe.dom.unfreeze();
if(r.exc) {
// exception: reverse
chk.prop("checked", !chk.prop("checked"));
@ -374,8 +376,7 @@ frappe.PermissionEngine = Class.extend({
options:me.options.roles, reqd:1,fieldname:"role"},
{fieldtype:"Select", label:__("Permission Level"),
options:[0,1,2,3,4,5,6,7,8,9], reqd:1, fieldname: "permlevel",
description: __("Level 0 is for document level permissions, \
higher levels for field level permissions.")}
description: __("Level 0 is for document level permissions, higher levels for field level permissions.")}
]
});
if(me.get_doctype()) {

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,18 +660,17 @@ class Database(object):
if dn and dt!=dn:
# with table
conditions, values = self.build_conditions(dn)
values.update(to_update)
set_values = []
for key in to_update:
set_values.append('`{0}`=%({0})s'.format(key))
self.sql("""update `tab{0}`
set {1} where {2}""".format(dt, ', '.join(set_values), conditions),
values, debug=debug)
for name in self.get_values(dt, dn, 'name', for_update=for_update):
values = dict(name=name[0])
values.update(to_update)
self.sql("""update `tab{0}`
set {1} where name=%(name)s""".format(dt, ', '.join(set_values)),
values, debug=debug)
else:
# for singles
keys = list(to_update)

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

@ -47,11 +47,11 @@ class Workspace:
self.allowed_pages = get_allowed_pages(cache=True)
self.allowed_reports = get_allowed_reports(cache=True)
if not minimal:
self.onboarding_doc = self.get_onboarding_doc()
self.onboarding = None
self.table_counts = get_table_with_counts()
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
@ -59,7 +59,7 @@ class Workspace:
def is_page_allowed(self):
cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module) + self.extended_cards
shortcuts = self.doc.shortcuts + self.extended_shortcuts
for section in cards:
links = loads(section.links) if isinstance(section.links, string_types) else section.links
for item in links:
@ -195,7 +195,7 @@ class Workspace:
'docs_url': self.onboarding_doc.documentation_url,
'items': self.get_onboarding_steps()
}
@handle_not_exist
def get_cards(self):
cards = self.doc.cards
@ -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
@ -301,7 +303,7 @@ class Workspace:
if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
if item.type == "Report":
report = self.allowed_reports.get(item.link_to, {})
if report.get("report_type") in ["Query Report", "Script Report"]:
if report.get("report_type") in ["Query Report", "Script Report", "Custom Report"]:
new_item['is_query_report'] = 1
else:
new_item['ref_doctype'] = report.get('ref_doctype')
@ -356,7 +358,7 @@ def get_desk_sidebar_items(flatten=False, cache=True):
_cache = frappe.cache()
if cache:
pages = _cache.get_value("desk_sidebar_items", user=frappe.session.user)
if not pages or not cache:
# don't get domain restricted pages
blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
@ -375,7 +377,7 @@ def get_desk_sidebar_items(flatten=False, cache=True):
order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
all_pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True)
pages = []
# Filter Page based on Permission
for page in all_pages:
try:

View file

@ -5,7 +5,7 @@ frappe.ui.form.on('Dashboard', {
refresh: function(frm) {
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
if (!frappe.boot.developer_mode) {
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frm.disable_form();
}

View file

@ -54,7 +54,8 @@
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard"
"label": "Is Standard",
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
},
{
"depends_on": "eval: doc.is_standard",
@ -66,7 +67,7 @@
}
],
"links": [],
"modified": "2020-07-10 17:48:19.468813",
"modified": "2020-07-23 11:05:41.890459",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",

View file

@ -23,43 +23,20 @@ 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();
}
frm.add_custom_button('Add Chart to Dashboard', () => {
const d = new frappe.ui.Dialog({
title: __('Add to Dashboard'),
fields: [
{
label: __('Select Dashboard'),
fieldtype: 'Link',
fieldname: 'dashboard',
options: 'Dashboard',
}
],
primary_action: (values) => {
values.chart_name = frm.doc.chart_name;
frappe.xcall(
'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard',
{args: values}
).then(()=> {
let dashboard_route_html =
`<a href = "#dashboard/${values.dashboard}">${values.dashboard}</a>`;
let message =
__(`Dashboard Chart ${values.chart_name} add to Dashboard ` + dashboard_route_html);
frappe.msgprint(message);
});
d.hide();
}
});
const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog(
frm.doc.name,
'Dashboard Chart',
'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard'
);
if (!frm.doc.chart_name) {
frappe.msgprint(__('Please create chart first'));
} else {
d.show();
dialog.show();
}
});
@ -79,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) {
@ -313,7 +285,7 @@ frappe.ui.form.on('Dashboard Chart', {
set_filters && frm.set_value('filters_json', JSON.stringify(filters));
}
let fields;
let fields = [];
if (is_document_type) {
fields = [
{
@ -338,7 +310,7 @@ frappe.ui.form.on('Dashboard Chart', {
} else if (frm.chart_filters.length) {
fields = frm.chart_filters.filter(f => f.fieldname);
fields.map( f => {
fields.map(f => {
if (filters[f.fieldname]) {
let condition = '=';
const filter_row =

View file

@ -240,7 +240,8 @@
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard"
"label": "Is Standard",
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
},
{
"depends_on": "eval: doc.is_standard",
@ -270,7 +271,7 @@
}
],
"links": [],
"modified": "2020-07-21 16:37:07.763482",
"modified": "2020-07-23 11:10:33.509497",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",

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
)
@ -128,7 +141,13 @@ def add_chart_to_dashboard(args):
dashboard = frappe.get_doc('Dashboard', args.dashboard)
dashboard_link = frappe.new_doc('Dashboard Chart Link')
dashboard_link.chart = args.chart_name
dashboard_link.chart = args.chart_name or args.name
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
chart.save()
dashboard.append('charts', dashboard_link)
dashboard.save()
@ -338,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

@ -7,6 +7,7 @@
"field_order": [
"type",
"link_to",
"doc_view",
"column_break_4",
"label",
"icon",
@ -34,6 +35,15 @@
"options": "type",
"reqd": 1
},
{
"depends_on": "eval:doc.type == \"DocType\"",
"description": "Which view of the associated DocType should this shortcut take you to?",
"fieldname": "doc_view",
"fieldtype": "Select",
"in_list_view": 1,
"label": "DocType View",
"options": "\nList\nReport Builder\nDashboard\nTree\nNew\nCalendar"
},
{
"fieldname": "stats_filter",
"fieldtype": "Code",
@ -86,9 +96,10 @@
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-05-14 16:02:15.420993",
"modified": "2020-08-12 14:11:55.080390",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Shortcut",

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,40 +32,16 @@ frappe.ui.form.on('Number Card', {
create_add_to_dashboard_button: function(frm) {
frm.add_custom_button('Add Card to Dashboard', () => {
const d = new frappe.ui.Dialog({
title: __('Add to Dashboard'),
fields: [
{
label: __('Select Dashboard'),
fieldtype: 'Link',
fieldname: 'dashboard',
options: 'Dashboard',
}
],
primary_action: (values) => {
values.name = frm.doc.name;
frappe.xcall(
'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard',
{
args: values
}
).then(()=> {
let dashboard_route_html =
`<a href = "#dashboard/${values.dashboard}">${values.dashboard}</a>`;
let message =
__(`Number Card ${values.name} add to Dashboard ` + dashboard_route_html);
frappe.msgprint(message);
});
d.hide();
}
});
const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog(
frm.doc.name,
'Number Card',
'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard'
);
if (!frm.doc.name) {
frappe.msgprint(__('Please create Card first'));
} else {
d.show();
dialog.show();
}
});
},
@ -140,6 +116,7 @@ frappe.ui.form.on('Number Card', {
},
report_name: function(frm) {
frm.filters = [];
frm.set_value('filters_json', '{}');
frm.set_value('dynamic_filters_json', '{}');
frm.set_df_property('report_field', 'options', []);
@ -271,7 +248,7 @@ frappe.ui.form.on('Number Card', {
set_filters && frm.set_value('filters_json', JSON.stringify(filters));
}
let fields;
let fields = [];
if (is_document_type) {
fields = [
{

View file

@ -115,7 +115,8 @@
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard"
"label": "Is Standard",
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
},
{
"depends_on": "eval: doc.is_standard",
@ -190,7 +191,7 @@
}
],
"links": [],
"modified": "2020-07-18 17:08:22.882538",
"modified": "2020-07-23 11:11:03.391719",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",

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,5 +181,11 @@ def add_card_to_dashboard(args):
dashboard_link = frappe.new_doc('Number Card Link')
dashboard_link.card = args.name
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
card.save()
dashboard.append('cards', dashboard_link)
dashboard.save()

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

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