Merge branch 'develop' into resize-image-editor

This commit is contained in:
Suraj Shetty 2020-08-04 17:55:03 +05:30 committed by GitHub
commit 94a81f17c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
204 changed files with 3848 additions and 4699 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

4
.github/frappe-framework-logo.svg vendored Normal file
View file

@ -0,0 +1,4 @@
<svg viewBox="0 0 1082 125" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 56.284v67.508h26.904V79.368l47.902-.04V56.248L1 56.284zM82.994 1H1.034v23.043h81.96V1z" fill="#3C88F7"/>
<path d="M216.735 75.269V53.233h-52.897V22.505h61.849V.469H138v122.509h25.838v-47.71h52.897zM285.477 59.096c2.645 0 5.29.404 7.731 1.415l3.052-26.281c-2.441-.607-4.883-1.01-7.934-1.01-11.8.201-19.125 6.67-25.635 22.439.61-3.639.814-7.48.814-11.321v-9.3h-23.193v87.94h25.227V72.64c5.697-12.13 15.056-13.544 19.938-13.544zM380.301 125c4.476 0 9.359-1.011 13.428-4.043l-2.442-17.184c-.813.404-1.627.607-2.645.607-2.644 0-4.272-2.426-4.272-6.672v-32.75c0-23.652-16.276-31.739-39.266-31.739-23.193 0-38.655 10.715-41.3 30.728l22.583 1.213c1.22-8.49 7.12-12.533 17.09-12.533 9.358 0 15.869 4.65 15.869 13.949v5.458h-16.683c-26.245 0-41.097 8.895-41.097 27.898 0 18.397 14.648 25.068 31.128 25.068 15.462 0 24.007-6.671 28.89-15.364C363.211 121.563 372.163 125 380.301 125zm-20.752-39.22c0 14.759-10.579 21.228-19.735 21.228-7.324 0-12.41-3.033-12.41-9.704 0-8.086 6.917-11.523 18.107-11.523h14.038zM449.522 55.255c9.562 0 15.055 5.862 15.055 19.003v48.72h25.228V69.001c3.458-8.895 9.358-13.746 16.683-13.746 10.172 0 15.055 5.862 15.055 19.003v48.72h25.228v-54.38c0-24.462-11.8-35.379-32.145-35.379-11.8 0-22.38 5.054-29.297 15.97-4.476-11.118-13.021-15.97-26.856-15.97-13.02 0-22.379 5.66-28.076 17.184.204-2.022.204-3.841.204-5.863V35.04h-23.193v87.939h25.227V69.406c3.459-9.097 8.952-14.151 16.887-14.151zM606.579 125c22.583 0 39.062-8.491 44.148-29.111l-24.21-2.224c-2.442 7.48-8.749 11.725-18.311 11.725-12.614 0-19.531-7.277-20.548-21.429l64.087-.202c.203-2.224.203-4.245.203-6.469 0-28.505-15.462-44.07-43.131-44.07-29.297 0-47.201 18.8-47.201 46.496 0 28.909 17.09 45.284 44.963 45.284zm1.831-72.98c10.783 0 17.09 6.065 17.903 17.184h-37.841c2.441-11.928 9.765-17.184 19.938-17.184zM751.095 122.978h34.18l24.414-87.94H782.63l-15.056 63.479h-1.831l-17.089-63.478h-28.89l-17.497 63.478h-2.035l-15.055-63.478h-27.669l24.617 87.939h34.383l16.48-61.456h1.831l16.276 61.456zM814.935 79.918c0 28.303 16.276 45.082 45.98 45.082 31.535 0 48.828-17.992 48.828-46.093 0-27.898-16.073-45.688-45.573-45.688-31.738 0-49.235 18.599-49.235 46.7zm26.652-.606c0-16.173 8.545-26.079 21.566-26.079 12.614 0 19.938 9.906 19.938 26.079 0 15.768-8.341 25.876-20.955 25.876-12.818 0-20.549-10.108-20.549-25.876zM971.128 59.096c2.645 0 5.289.404 7.731 1.415l3.052-26.281c-2.442-.607-4.883-1.01-7.935-1.01-11.8.201-19.124 6.67-25.635 22.439.611-3.639.814-7.48.814-11.321v-9.3h-23.193v87.94h25.228V72.64c5.696-12.13 15.055-13.544 19.938-13.544zM1070.4 125c3.46 0 7.33-.404 10.79-1.819l-1.83-21.227c-.82.202-1.63.404-3.26.404-4.27 0-7.12-2.224-11.39-7.682l-16.48-21.833L1082 35.039h-27.67l-34.18 36.793h-.61V1.5h-25.227v121.478h25.227v-20.62l10.78-12.534 14.45 19.61c8.95 12.938 15.87 15.566 25.63 15.566z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

5
.gitignore vendored
View file

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

View file

@ -4,8 +4,7 @@ pull_request_rules:
- status-success=Sider
- status-success=Semantic Pull Request
- status-success=Travis CI - Pull Request
- status-success=security/snyk - package.json (frappe)
- status-success=security/snyk - requirements.txt (frappe)
- status-success=security/snyk (frappe)
- label!=don't-merge
- label!=squash
- "#approved-reviews-by>=1"
@ -17,8 +16,7 @@ pull_request_rules:
- status-success=Sider
- status-success=Semantic Pull Request
- status-success=Travis CI - Pull Request
- status-success=security/snyk - package.json (frappe)
- status-success=security/snyk - requirements.txt (frappe)
- status-success=security/snyk (frappe)
- label!=don't-merge
- label=squash
- "#approved-reviews-by>=1"

View file

@ -1,12 +1,12 @@
<div align="center">
<img src=".github/frappe-framework-logo.png" height="150">
<h1>
<br>
<a href="https://frappeframework.com">
frappe
<img src=".github/frappe-framework-logo.svg" height="50">
</a>
</h1>
<h3>
a web framework with <a href="https://www.youtube.com/watch?v=LOjk3m0wTwg">"batteries included"
a web framework with <a href="https://www.youtube.com/watch?v=LOjk3m0wTwg">"batteries included"</a>
</h3>
<h5>
it's pronounced - <em>fra-pay</em>

View file

@ -490,7 +490,8 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
message = content or message
if as_markdown:
message = frappe.utils.md_to_html(message)
from frappe.utils import md_to_html
message = md_to_html(message)
if not delayed:
now = True
@ -1558,10 +1559,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'''

View file

@ -99,7 +99,7 @@ def application(request):
frappe.monitor.stop(response)
frappe.recorder.dump()
frappe.logger("frappe.web").info({
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"),
@ -256,9 +256,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

@ -333,12 +333,20 @@ class CookieManager:
# sid expires in 3 days
expires = datetime.datetime.now() + datetime.timedelta(days=3)
if frappe.session.sid:
self.cookies["sid"] = {"value": frappe.session.sid, "expires": expires}
self.set_cookie("sid", frappe.session.sid, expires=expires, httponly=True)
if frappe.session.session_country:
self.cookies["country"] = {"value": frappe.session.get("session_country")}
self.set_cookie("country", frappe.session.session_country)
def set_cookie(self, key, value, expires=None):
self.cookies[key] = {"value": value, "expires": expires}
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
if not secure:
secure = frappe.local.request.scheme == "https"
self.cookies[key] = {
"value": value,
"expires": expires,
"secure": secure,
"httponly": httponly,
"samesite": samesite
}
def delete_cookie(self, to_delete):
if not isinstance(to_delete, (list, tuple)):
@ -349,7 +357,10 @@ class CookieManager:
def flush_cookies(self, response):
for key, opts in self.cookies.items():
response.set_cookie(key, quote((opts.get("value") or "").encode('utf-8')),
expires=opts.get("expires"))
expires=opts.get("expires"),
secure=opts.get("secure"),
httponly=opts.get("httponly"),
samesite=opts.get("samesite"))
# expires yesterday!
expires = datetime.datetime.now() + datetime.timedelta(days=-1)

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

@ -146,7 +146,7 @@ class AutoRepeat(Document):
def make_new_document(self):
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
new_doc = frappe.copy_doc(reference_doc)
self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)
@ -372,7 +372,8 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e
doc.save()
return doc
#method for reference_doctype filter
# method for reference_doctype filter
@frappe.whitelist()
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

@ -107,7 +107,7 @@ def load_desktop_data(bootinfo):
from frappe.config import get_modules_from_all_apps_for_user
from frappe.desk.desktop import get_desk_sidebar_items
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
bootinfo.allowed_workspaces = get_desk_sidebar_items(True)
bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False)
bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map()
bootinfo.dashboards = frappe.get_all("Dashboard")

View file

@ -21,7 +21,7 @@ global_cache_keys = ("app_hooks", "installed_apps",
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",
"desktop_icons", 'portal_menu_items', 'user_perm_can_read',
"has_role:Page", "has_role:Report")
"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')

View file

@ -201,16 +201,31 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro
def install_app(context, apps):
"Install a new app to site, supports multiple apps"
from frappe.installer import install_app as _install_app
exit_code = 0
if not context.sites:
raise SiteNotSpecifiedError
for site in context.sites:
frappe.init(site=site)
frappe.connect()
try:
for app in apps:
for app in apps:
try:
_install_app(app, verbose=context.verbose)
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
except frappe.IncompatibleApp as err:
err_msg = ":\n{}".format(err) if str(err) else ""
print("App {} is Incompatible with Site {}{}".format(app, site, err_msg))
exit_code = 1
except Exception as err:
err_msg = ":\n{}".format(err if str(err) else frappe.get_traceback())
print("An error occurred while installing {}{}".format(app, err_msg))
exit_code = 1
frappe.destroy()
sys.exit(exit_code)
@click.command('list-apps')
@pass_context
@ -422,15 +437,16 @@ def remove_from_installed_apps(context, app):
@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False, multiple=True)
@click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False)
@click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False)
@click.option('--force', help='Force remove app from site', is_flag=True, default=False)
@pass_context
def uninstall(context, app, dry_run=False, yes=False, no_backup=False):
def uninstall(context, app, dry_run, yes, no_backup, force):
"Remove app and linked modules from site"
from frappe.installer import remove_app
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
remove_app(app, dry_run, yes, no_backup)
remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force)
finally:
frappe.destroy()
if not context.sites:
@ -615,6 +631,29 @@ def stop_recording(context):
if not context.sites:
raise SiteNotSpecifiedError
@click.command('ngrok')
@pass_context
def start_ngrok(context):
from pyngrok import ngrok
site = get_site(context)
frappe.init(site=site)
port = frappe.conf.http_port or frappe.conf.webserver_port
public_url = ngrok.connect(port=port, options={
'host_header': site
})
print(f'Public URL: {public_url}')
print('Inspect logs at http://localhost:4040')
ngrok_process = ngrok.get_ngrok_process()
try:
# Block until CTRL-C or some other terminating event
ngrok_process.proc.wait()
except KeyboardInterrupt:
print("Shutting down server...")
frappe.destroy()
ngrok.kill()
commands = [
add_system_manager,
@ -640,5 +679,6 @@ commands = [
browse,
start_recording,
stop_recording,
add_to_hosts
add_to_hosts,
start_ngrok
]

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

@ -16,6 +16,13 @@ def get_data():
"description": _("Language, Date and Time settings"),
"hide_count": True
},
{
"type": "doctype",
"name": "Global Defaults",
"label": _("Global Defaults"),
"description": _("Company, Fiscal Year and Currency defaults"),
"hide_count": True
},
{
"type": "doctype",
"name": "Error Log",

View file

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

View file

@ -230,6 +230,7 @@ def get_company_address(company):
return ret
@frappe.whitelist()
def address_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond
@ -237,16 +238,17 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
link_name = filters.pop('link_name')
condition = ""
for fieldname, value in iteritems(filters):
condition += " and {field}={value}".format(
field=fieldname,
value=value
)
meta = frappe.get_meta("Address")
for fieldname, value in iteritems(filters):
if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS:
condition += " and {field}={value}".format(
field=fieldname,
value=frappe.db.escape(value))
searchfields = meta.get_search_fields()
if searchfield:
if searchfield and (meta.get_field(searchfield)\
or searchfield in frappe.db.DEFAULT_COLUMNS):
searchfields.append(searchfield)
search_condition = ''
@ -289,4 +291,4 @@ def get_condensed_address(doc):
return ", ".join([doc.get(d) for d in fields if doc.get(d)])
def update_preferred_address(address, field):
frappe.db.set_value('Address', address, field, 0)
frappe.db.set_value('Address', address, field, 0)

View file

@ -182,19 +182,17 @@ def update_contact(doc, method):
contact.flags.ignore_mandatory = True
contact.save(ignore_permissions=True)
@frappe.whitelist()
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:
return []
link_doctype = filters.pop('link_doctype')
link_name = filters.pop('link_name')
condition = ""
for fieldname, value in iteritems(filters):
condition += " and {field}={value}".format(
field=fieldname,
value=value
)
return frappe.db.sql("""select
`tabContact`.name, `tabContact`.first_name, `tabContact`.last_name
from
@ -209,9 +207,7 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
order by
if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999),
`tabContact`.idx desc, `tabContact`.name
limit %(start)s, %(page_len)s """.format(
mcond=get_match_cond(doctype),
key=searchfield), {
limit %(start)s, %(page_len)s """.format(mcond=get_match_cond(doctype), key=searchfield), {
'txt': '%' + txt + '%',
'_txt': txt.replace("%", ""),
'start': start,

View file

@ -18,7 +18,7 @@
{
"hidden": 0,
"label": "Core",
"links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Company, Fiscal Year and Currency defaults\",\n \"hide_count\": true,\n \"label\": \"Global Defaults\",\n \"name\": \"Global Defaults\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@ -39,10 +39,11 @@
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
"hide_custom": 0,
"idx": 0,
"is_standard": 1,
"label": "Settings",
"modified": "2020-04-01 11:24:40.636747",
"modified": "2020-07-14 10:09:09.520557",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",

View file

@ -221,7 +221,7 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
:param print_html: Send given value as HTML attachment.
:param print_format: Attach print format of parent document."""
view_link = frappe.utils.cint(frappe.db.get_value("Print Settings", "Print Settings", "attach_view_link"))
view_link = frappe.utils.cint(frappe.db.get_value("System Settings", "System Settings", "attach_view_link"))
if print_format and view_link:
doc.content += get_attach_link(doc, print_format)
@ -236,7 +236,7 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
if doc.sender:
# combine for sending to get the format 'Jane <jane@example.com>'
doc.sender = formataddr([doc.sender_full_name, doc.sender])
doc.sender = get_formatted_email(doc.sender_full_name, mail=doc.sender)
doc.attachments = []

View file

@ -102,6 +102,10 @@ frappe.ui.form.on('Data Import', {
},
update_primary_action(frm) {
if (frm.is_dirty()) {
frm.enable_save();
return;
}
frm.disable_save();
if (frm.doc.status !== 'Success') {
if (!frm.is_new() && (frm.has_import_file())) {
@ -199,20 +203,12 @@ frappe.ui.form.on('Data Import', {
},
download_template(frm) {
if (
frm.data_exporter &&
frm.data_exporter.doctype === frm.doc.reference_doctype
) {
frm.data_exporter.exporting_for = frm.doc.import_type;
frm.data_exporter.dialog.show();
} else {
frappe.require('/assets/js/data_import_tools.min.js', () => {
frm.data_exporter = new frappe.data_import.DataExporter(
frm.doc.reference_doctype,
frm.doc.import_type
);
});
}
frappe.require('/assets/js/data_import_tools.min.js', () => {
frm.data_exporter = new frappe.data_import.DataExporter(
frm.doc.reference_doctype,
frm.doc.import_type
);
});
},
reference_doctype(frm) {
@ -301,8 +297,8 @@ frappe.ui.form.on('Data Import', {
events: {
remap_column(changed_map) {
let template_options = JSON.parse(frm.doc.template_options || '{}');
template_options.remap_column = template_options.remap_column || {};
Object.assign(template_options.remap_column, changed_map);
template_options.column_to_field_map = template_options.column_to_field_map || {};
Object.assign(template_options.column_to_field_map, changed_map);
frm.set_value('template_options', JSON.stringify(template_options));
frm.save().then(() => frm.trigger('import_file'));
}
@ -435,10 +431,10 @@ frappe.ui.form.on('Data Import', {
.join('');
let id = frappe.dom.get_unique_id();
html = `${messages}
<button class="btn btn-default btn-xs margin-top" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}">
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
${__('Show Traceback')}
</button>
<div class="collapse margin-top" id="${id}">
<div class="collapse" id="${id}" style="margin-top: 15px;">
<div class="well">
<pre>${log.exception}</pre>
</div>

View file

@ -119,7 +119,7 @@
{
"fieldname": "import_warnings_section",
"fieldtype": "Section Break",
"label": "Warnings"
"label": "Import File Errors and Warnings"
},
{
"fieldname": "import_warnings",
@ -127,7 +127,7 @@
"label": "Import Warnings"
},
{
"depends_on": "reference_doctype",
"depends_on": "eval:!doc.__islocal",
"fieldname": "download_template",
"fieldtype": "Button",
"label": "Download Template"
@ -159,7 +159,7 @@
"label": "Import from Google Sheets"
},
{
"depends_on": "eval:doc.google_sheets_url",
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved",
"fieldname": "refresh_google_sheet",
"fieldtype": "Button",
"label": "Refresh Google Sheet"
@ -167,7 +167,7 @@
],
"hide_toolbar": 1,
"links": [],
"modified": "2020-06-18 16:05:54.211034",
"modified": "2020-06-24 14:33:03.173876",
"modified_by": "Administrator",
"module": "Core",
"name": "Data Import",

View file

@ -16,6 +16,7 @@ from frappe.utils.xlsxutils import (
read_xls_file_from_attached_file,
)
from frappe.model import no_value_fields, table_fields as table_fieldtypes
from frappe.core.doctype.version.version import get_diff
INVALID_VALUES = ("", None)
MAX_ROWS_IN_PREVIEW = 10
@ -58,6 +59,7 @@ class Importer:
frappe.flags.in_import = True
frappe.flags.mute_emails = self.data_import.mute_emails
self.data_import.db_set("status", "Pending")
self.data_import.db_set("template_warnings", "")
def import_data(self):
@ -216,14 +218,22 @@ class Importer:
def update_record(self, doc):
id_field = get_id_field(self.doctype)
existing_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname))
existing_doc.flags.updater_reference = {
"doctype": self.data_import.doctype,
"docname": self.data_import.name,
"label": _("via Data Import"),
}
existing_doc.update(doc)
existing_doc.save()
return existing_doc
updated_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname))
updated_doc.update(doc)
if get_diff(existing_doc, updated_doc):
# update doc if there are changes
updated_doc.flags.updater_reference = {
"doctype": self.data_import.doctype,
"docname": self.data_import.name,
"label": _("via Data Import"),
}
updated_doc.save()
return updated_doc
else:
# throw if no changes
frappe.throw('No changes to update')
def get_eta(self, current, total, processing_time):
self.last_eta = getattr(self, "last_eta", 0)
@ -306,8 +316,9 @@ class ImportFile:
)
self.column_to_field_map = self.template_options.column_to_field_map
self.import_type = import_type
self.warnings = []
self.file_doc = self.file_path = None
self.file_doc = self.file_path = self.google_sheets_url = None
if isinstance(file, frappe.string_types):
if frappe.db.exists("File", {"file_url": file}):
self.file_doc = frappe.get_doc("File", {"file_url": file})
@ -430,9 +441,8 @@ class ImportFile:
# if there are child doctypes, find the subsequent rows
if len(doctypes) > 1:
# subsequent rows either dont have any parent value set
# or have the same value as the parent row
# we include a row if either of conditions match
# subsequent rows that have blank values in parent columns
# are considered as child rows
parent_column_indexes = self.header.get_column_indexes(self.doctype)
parent_row_values = first_row.get_values(parent_column_indexes)
@ -443,11 +453,8 @@ class ImportFile:
if all([v in INVALID_VALUES for v in row_values]):
rows.append(row)
continue
# if the row has same values as parent row, it's a child row doc
if row_values == parent_row_values:
rows.append(row)
continue
# if any of those conditions dont match, it's the next doc
# if we encounter a row which has values in parent columns,
# then it is the next doc
break
parent_doc = None
@ -462,38 +469,46 @@ class ImportFile:
parent_doc[table_df.fieldname].append(child_doc)
doc = parent_doc
# check if there is atleast one row for mandatory table fields
meta = frappe.get_meta(self.doctype)
mandatory_table_fields = [
df
for df in meta.fields
if df.fieldtype in table_fieldtypes
and df.reqd
and len(doc.get(df.fieldname, [])) == 0
]
if len(mandatory_table_fields) == 1:
self.warnings.append(
{
"row": first_row.row_number,
"message": _("There should be atleast one row for {0} table").format(
mandatory_table_fields[0].label
),
}
)
elif mandatory_table_fields:
fields_string = ", ".join([df.label for df in mandatory_table_fields])
message = _("There should be atleast one row for the following tables: {0}").format(
fields_string
)
self.warnings.append({"row": first_row.row_number, "message": message})
if self.import_type == INSERT:
# check if there is atleast one row for mandatory table fields
meta = frappe.get_meta(self.doctype)
mandatory_table_fields = [
df
for df in meta.fields
if df.fieldtype in table_fieldtypes
and df.reqd
and len(doc.get(df.fieldname, [])) == 0
]
if len(mandatory_table_fields) == 1:
self.warnings.append(
{
"row": first_row.row_number,
"message": _("There should be atleast one row for {0} table").format(
frappe.bold(mandatory_table_fields[0].label)
),
}
)
elif mandatory_table_fields:
fields_string = ", ".join([df.label for df in mandatory_table_fields])
message = _("There should be atleast one row for the following tables: {0}").format(
fields_string
)
self.warnings.append({"row": first_row.row_number, "message": message})
return doc, rows, data[len(rows) :]
def get_warnings(self):
warnings = []
# ImportFile warnings
warnings += self.warnings
# Column warnings
for col in self.header.columns:
warnings += col.warnings
# Row warnings
for row in self.data:
warnings += row.warnings
@ -600,14 +615,14 @@ class Row:
def validate_value(self, value, col):
df = col.df
if df.fieldtype == "Select":
select_options = df.get_select_options()
select_options = [d for d in (df.options or '').split('\n') if d]
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_dict(convert_dates_to_str=True),
"field": df_as_json(df),
"message": msg,
}
)
@ -622,7 +637,7 @@ class Row:
self.warnings.append(
{
"row": self.row_number,
"field": df.as_dict(convert_dates_to_str=True),
"field": df_as_json(df),
"message": msg,
}
)
@ -635,7 +650,7 @@ class Row:
{
"row": self.row_number,
"col": col.column_number,
"field": df.as_dict(convert_dates_to_str=True),
"field": df_as_json(df),
"message": _("Value {0} must in {1} format").format(
frappe.bold(value), frappe.bold(get_user_format(col.date_format))
),
@ -646,7 +661,7 @@ class Row:
return value
def link_exists(self, value, df):
key = df.options + "::" + value
key = df.options + "::" + cstr(value)
if Row.link_values_exist_map.get(key) is None:
Row.link_values_exist_map[key] = frappe.db.exists(df.options, value)
return Row.link_values_exist_map.get(key)
@ -674,6 +689,9 @@ class Row:
return value
def get_date(self, value, column):
if isinstance(value, datetime):
return value
date_format = column.date_format
if date_format:
try:
@ -755,19 +773,21 @@ class Row:
class Header(Row):
def __init__(self, index, row, doctype, raw_data, column_to_field_map):
def __init__(self, index, row, doctype, raw_data, column_to_field_map=None):
self.index = index
self.row_number = index + 1
self.data = row
self.doctype = doctype
column_to_field_map = column_to_field_map or frappe._dict()
self.seen = []
self.columns = []
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, column_to_field_map.get(header), self.seen
j, header, self.doctype, column_values, map_to_field, self.seen
)
self.seen.append(header)
self.columns.append(column)
@ -824,7 +844,7 @@ class Column:
self.meta = frappe.get_meta(doctype)
self.parse()
self.parse_date_format()
self.validate_values()
def parse(self):
header_title = self.header_title
@ -897,10 +917,6 @@ class Column:
self.df = df
self.skip_import = skip_import
def parse_date_format(self):
if self.df and self.df.fieldtype in ("Date", "Time", "Datetime"):
self.date_format = self.guess_date_format_for_column()
def guess_date_format_for_column(self):
""" 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
@ -935,6 +951,36 @@ class Column:
return max_occurred_date_format
def validate_values(self):
if not self.df:
return
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)})]
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'
})
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'
})
def as_dict(self):
d = frappe._dict()
d.index = self.index
@ -944,6 +990,9 @@ 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'):
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
d.warnings = self.warnings
return d
@ -1021,6 +1070,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()
fieldtype = df.fieldtype or "Data"
parent = df.parent or parent_doctype
if fieldtype not in no_value_fields:
@ -1029,12 +1079,12 @@ def build_fields_dict_for_column_matching(parent_doctype):
# Label
# label
# Label (label)
if not out.get(df.label):
if not out.get(label):
# if Label is already set, don't set it again
# in case of duplicate column headers
out[df.label] = df
out[label] = df
out[df.fieldname] = df
label_with_fieldname = "{0} ({1})".format(df.label, df.fieldname)
label_with_fieldname = "{0} ({1})".format(label, df.fieldname)
out[label_with_fieldname] = df
else:
# in case there are multiple table fields with the same doctype
@ -1045,7 +1095,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
"fields", {"fieldtype": ["in", table_fieldtypes], "options": parent}
)
for table_field in table_fields:
by_label = "{0} ({1})".format(df.label, table_field.label)
by_label = "{0} ({1})".format(label, table_field.label)
by_fieldname = "{0}.{1}".format(table_field.fieldname, df.fieldname)
# create a new df object to avoid mutation problems
@ -1113,3 +1163,13 @@ def get_user_format(date_format):
.replace("%m", "mm")
.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
}

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"creation": "2012-12-12 11:19:22",
"doctype": "DocType",
@ -63,7 +64,8 @@
"fieldname": "is_home_folder",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Home Folder"
"label": "Is Home Folder",
"search_index": 1
},
{
"default": "0",
@ -172,7 +174,8 @@
],
"icon": "fa fa-file",
"idx": 1,
"modified": "2019-08-30 19:46:20.796453",
"links": [],
"modified": "2020-06-28 12:21:30.772386",
"modified_by": "Administrator",
"module": "Core",
"name": "File",

View file

@ -100,26 +100,26 @@ class File(Document):
self.validate_file()
self.generate_content_hash()
self.validate_url()
if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}):
old_file_url = self.file_url
if not self.is_folder and (self.is_private != self.db_get('is_private')):
private_files = frappe.get_site_path('private', 'files')
public_files = frappe.get_site_path('public', 'files')
file_name = self.file_url.split('/')[-1]
if not self.is_private:
shutil.move(os.path.join(private_files, self.file_name),
os.path.join(public_files, self.file_name))
shutil.move(os.path.join(private_files, file_name),
os.path.join(public_files, file_name))
self.file_url = "/files/{0}".format(self.file_name)
self.file_url = "/files/{0}".format(file_name)
else:
shutil.move(os.path.join(public_files, self.file_name),
os.path.join(private_files, self.file_name))
shutil.move(os.path.join(public_files, file_name),
os.path.join(private_files, file_name))
self.file_url = "/private/files/{0}".format(self.file_name)
self.file_url = "/private/files/{0}".format(file_name)
update_existing_file_docs(self)
# update documents image url with new file url
if self.attached_to_doctype and self.attached_to_name:
@ -135,6 +135,8 @@ class File(Document):
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
self.attached_to_field, self.file_url)
self.validate_url()
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
@ -182,13 +184,7 @@ class File(Document):
if duplicate_file:
duplicate_file_doc = frappe.get_cached_doc('File', duplicate_file.name)
if duplicate_file_doc.exists_on_disk():
# if it is attached to a document then throw FileAlreadyAttachedException
if self.attached_to_doctype and self.attached_to_name:
self.duplicate_entry = duplicate_file.name
frappe.throw(_("Same file has already been attached to the record"),
frappe.FileAlreadyAttachedException)
# else just use the url, to avoid uploading a duplicate
else:
# just use the url, to avoid uploading a duplicate
self.file_url = duplicate_file.file_url
def set_file_name(self):
@ -909,3 +905,20 @@ def get_files_in_folder(folder):
{ 'folder': folder },
['name', 'file_name', 'file_url', 'is_folder', 'modified']
)
def update_existing_file_docs(doc):
# Update is private and file url of all file docs that point to the same file
frappe.db.sql("""
UPDATE `tabFile`
SET
file_url = %(file_url)s,
is_private = %(is_private)s
WHERE
content_hash = %(content_hash)s
and name != %(file_name)s
""", dict(
file_url=doc.file_url,
is_private=doc.is_private,
content_hash=doc.content_hash,
file_name=doc.name
))

View file

@ -294,4 +294,37 @@ class TestFile(unittest.TestCase):
folder = frappe.get_doc("File", "Home/Test Folder 1/Test Folder 3")
self.assertRaises(frappe.ValidationError, folder.delete)
def test_same_file_url_update(self):
attached_to_doctype1, attached_to_docname1 = make_test_doc()
attached_to_doctype2, attached_to_docname2 = make_test_doc()
file1 = frappe.get_doc({
"doctype": "File",
"file_name": 'file1.txt',
"attached_to_doctype": attached_to_doctype1,
"attached_to_name": attached_to_docname1,
"is_private": 1,
"content": test_content1}).insert()
file2 = frappe.get_doc({
"doctype": "File",
"file_name": 'file2.txt',
"attached_to_doctype": attached_to_doctype2,
"attached_to_name": attached_to_docname2,
"is_private": 1,
"content": test_content1}).insert()
self.assertEqual(file1.is_private, file2.is_private, 1)
self.assertEqual(file1.file_url, file2.file_url)
self.assertTrue(os.path.exists(file1.get_full_path()))
file1.is_private = 0
file1.save()
file2 = frappe.get_doc('File', file2.name)
self.assertEqual(file1.is_private, file2.is_private, 0)
self.assertEqual(file1.file_url, file2.file_url)
self.assertTrue(os.path.exists(file2.get_full_path()))

View file

@ -42,6 +42,10 @@ class ModuleDef(Document):
def on_trash(self):
"""Delete module name from modules.txt"""
if frappe.flags.in_uninstall:
return
modules = None
if frappe.local.module_app.get(frappe.scrub(self.name)):
with open(frappe.get_app_path(self.app_name, "modules.txt"), "r") as f:

View file

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

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

@ -59,6 +59,7 @@
"column_break_18",
"disable_standard_email_footer",
"hide_footer_in_auto_email_reports",
"attach_view_link",
"chat",
"enable_chat",
"use_socketio_to_upload_file"
@ -422,12 +423,18 @@
"fieldname": "enable_onboarding",
"fieldtype": "Check",
"label": "Enable Onboarding"
},
{
"default": "1",
"fieldname": "attach_view_link",
"fieldtype": "Check",
"label": "Send document Web View link in email"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2020-05-01 19:21:15.496065",
"modified": "2020-07-02 16:13:00.166382",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -4,7 +4,7 @@
from __future__ import unicode_literals, print_function
import frappe
from frappe.model.document import Document
from frappe.utils import cint, flt, has_gravatar, format_datetime, now_datetime, get_formatted_email, today
from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password
from frappe.desk.notifications import clear_notifications
@ -770,7 +770,7 @@ def sign_up(email, full_name, redirect_to):
user = frappe.get_doc({
"doctype":"User",
"email": email,
"first_name": full_name,
"first_name": escape_html(full_name),
"enabled": 1,
"new_password": random_string(10),
"user_type": "Website User"
@ -811,6 +811,7 @@ def reset_password(user):
frappe.clear_messages()
return 'not found'
@frappe.whitelist()
def user_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond

View file

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

View file

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

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,10 +0,0 @@
# -*- 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 Video(Document):
pass

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() {
@ -172,19 +165,26 @@ class Dashboard {
set_dropdown() {
this.page.clear_menu();
this.page.add_menu_item('Edit...', () => {
this.page.add_menu_item(__('Edit'), () => {
frappe.set_route('Form', 'Dashboard', frappe.dashboard.dashboard_name);
}, 1);
});
this.page.add_menu_item('New...', () => {
this.page.add_menu_item(__('New'), () => {
frappe.new_doc('Dashboard');
}, 1);
});
frappe.db.get_list("Dashboard").then(dashboards => {
this.page.add_menu_item(__('Refresh All'), () => {
this.chart_group &&
this.chart_group.widgets_list.forEach(chart => chart.refresh());
this.number_card_group &&
this.number_card_group.widgets_list.forEach(card => card.render_card());
});
frappe.db.get_list('Dashboard').then(dashboards => {
dashboards.map(dashboard => {
let name = dashboard.name;
if(name != this.dashboard_name){
this.page.add_menu_item(name, () => frappe.set_route("dashboard", name));
this.page.add_menu_item(name, () => frappe.set_route("dashboard", name), 1);
}
});
});

View file

@ -5,23 +5,23 @@ from __future__ import unicode_literals
import frappe
from frappe import _, throw
import frappe.utils.user
from frappe.permissions import check_admin_or_system_manager
from frappe.permissions import check_admin_or_system_manager, rights
from frappe.model import data_fieldtypes
def execute(filters=None):
user, doctype, show_permissions = filters.get("user"), filters.get("doctype"), filters.get("show_permissions")
if not validate(user, doctype): return [], []
columns, fields = get_columns_and_fields(doctype)
data = frappe.get_list(doctype, fields=fields, as_list=True, user=user)
if show_permissions:
columns = columns + ["Read", "Write", "Create", "Delete", "Submit", "Cancel", "Amend", "Print", "Email",
"Report", "Import", "Export", "Share"]
columns = columns + [frappe.unscrub(right) + ':Check:80' for right in rights]
data = list(data)
for i,item in enumerate(data):
temp = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, item[0]), False,user)
data[i] = item+(temp.get("read"),temp.get("write"),temp.get("create"),temp.get("delete"),temp.get("submit"),temp.get("cancel"),temp.get("amend"),temp.get("print"),temp.get("email"),temp.get("report"),temp.get("import"),temp.get("export"),temp.get("share"),)
for i, doc in enumerate(data):
permission = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, doc[0]), user)
data[i] = doc + tuple(permission.get(right) for right in rights)
return columns, data
@ -41,6 +41,7 @@ def get_columns_and_fields(doctype):
return columns, fields
@frappe.whitelist()
def query_doctypes(doctype, txt, searchfield, start, page_len, filters):
user = filters.get("user")
user_perms = frappe.utils.user.UserPermissions(user)

View file

@ -49,7 +49,7 @@ class DbManager:
host = self.get_current_host()
if frappe.conf.get('rds_db', 0) == 1:
self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON `%s`.* TO '%s'@'%s';" % (target, user, host))
self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, LOCK TABLES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
else:
self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host))

View file

@ -82,5 +82,7 @@ class MariaDBTable(DBTable):
fieldname = str(e).split("'")[-2]
frappe.throw(_("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format(
fieldname, self.table_name))
elif e.args[0]==1067:
frappe.throw(str(e.args[1]))
else:
raise e

View file

@ -29,31 +29,56 @@ def handle_not_exist(fn):
class Workspace:
def __init__(self, page_name):
def __init__(self, page_name, minimal=False):
self.page_name = page_name
self.extended_cards = []
self.extended_charts = []
self.extended_shortcuts = []
self.user = frappe.get_user()
self.allowed_modules = self.get_cached_value('user_allowed_modules', self.get_allowed_modules)
self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules)
self.doc = self.get_page_for_user()
if self.doc.module not in self.allowed_modules:
raise frappe.PermissionError
self.can_read = self.get_cached_value('user_perm_can_read', self.get_can_read_items)
self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items)
self.allowed_pages = get_allowed_pages(cache=True)
self.allowed_reports = get_allowed_reports(cache=True)
self.onboarding_doc = self.get_onboarding_doc()
self.onboarding = None
self.table_counts = get_table_with_counts()
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()
def get_cached_value(self, cache_key, fallback_fn):
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:
if self.is_item_allowed(item.get('name'), item.get('type')):
return True
def _in_active_domains(item):
if not item.restrict_to_domain:
return True
else:
return item.restrict_to_domain in frappe.get_active_domains()
for item in shortcuts:
if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
return True
return False
def get_cached(self, cache_key, fallback_fn):
_cache = frappe.cache()
value = _cache.get_value(cache_key, user=frappe.session.user)
@ -83,12 +108,12 @@ class Workspace:
'extends': self.page_name,
'for_user': frappe.session.user
}
pages = frappe.get_list("Desk Page", filters=filters)
pages = frappe.get_all("Desk Page", filters=filters, limit=1)
if pages:
return frappe.get_doc("Desk Page", pages[0])
return frappe.get_cached_doc("Desk Page", pages[0])
self.get_pages_to_extend()
return frappe.get_doc("Desk Page", self.page_name)
return frappe.get_cached_doc("Desk Page", self.page_name)
def get_onboarding_doc(self):
# Check if onboarding is enabled
@ -123,7 +148,7 @@ class Workspace:
'module': ['in', self.allowed_modules]
})
pages = [frappe.get_doc("Desk Page", page['name']) for page in pages]
pages = [frappe.get_cached_doc("Desk Page", page['name']) for page in pages]
for page in pages:
self.extended_cards = self.extended_cards + page.cards
@ -170,6 +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
@ -195,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
@ -323,25 +351,44 @@ def get_desktop_page(page):
}
@frappe.whitelist()
def get_desk_sidebar_items(flatten=False):
def get_desk_sidebar_items(flatten=False, cache=True):
"""Get list of sidebar items for desk
"""
# don't get domain restricted pages
blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
pages = []
_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()
filters = {
'restrict_to_domain': ['in', frappe.get_active_domains()],
'extends_another_page': 0,
'for_user': '',
'module': ['not in', blocked_modules]
}
filters = {
'restrict_to_domain': ['in', frappe.get_active_domains()],
'extends_another_page': 0,
'for_user': '',
'module': ['not in', blocked_modules]
}
if not frappe.local.conf.developer_mode:
filters['developer_mode_only'] = '0'
if not frappe.local.conf.developer_mode:
filters['developer_mode_only'] = '0'
# pages sorted based on pinned to top and then by name
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:
wspace = Workspace(page.get('name'), True)
if wspace.is_page_allowed():
pages.append(page)
except frappe.PermissionError:
pass
_cache.set_value("desk_sidebar_items", pages, frappe.session.user)
# pages sorted based on pinned to top and then by name
order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True)
if flatten:
return pages
@ -375,7 +422,7 @@ def get_custom_reports_and_doctypes(module):
]
def get_custom_doctype_list(module):
doctypes = frappe.get_list("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name", ignore_permissions=True)
doctypes = frappe.get_all("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name")
out = []
for d in doctypes:
@ -390,9 +437,9 @@ def get_custom_doctype_list(module):
def get_custom_report_list(module):
"""Returns list on new style reports for modules."""
reports = frappe.get_list("Report", fields=["name", "ref_doctype", "report_type"], filters=
reports = frappe.get_all("Report", fields=["name", "ref_doctype", "report_type"], filters=
{"is_standard": "No", "disabled": 0, "module": module},
order_by="name", ignore_permissions=True)
order_by="name")
out = []
for r in reports:

View file

@ -5,10 +5,14 @@ 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 && frm.doc.is_standard) {
frm.disable_form();
}
frm.set_query("chart", "charts", function() {
return {
filters: {
is_public: 1
is_public: 1,
}
};
});
@ -16,7 +20,7 @@ frappe.ui.form.on('Dashboard', {
frm.set_query("card", "cards", function() {
return {
filters: {
is_public: 1
is_public: 1,
}
};
});

View file

@ -1,5 +1,6 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:dashboard_name",
"creation": "2019-01-10 12:54:40.938705",
"doctype": "DocType",
@ -8,6 +9,8 @@
"field_order": [
"dashboard_name",
"is_default",
"is_standard",
"module",
"charts",
"chart_options",
"cards"
@ -35,21 +38,36 @@
"reqd": 1
},
{
"description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
"fieldname": "chart_options",
"fieldtype": "Code",
"label": "Chart Options",
"options": "JSON"
"description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
"fieldname": "chart_options",
"fieldtype": "Code",
"label": "Chart Options",
"options": "JSON"
},
{
"fieldname": "cards",
"fieldtype": "Table",
"label": "Cards",
"options": "Number Card Link"
},
{
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard",
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
},
{
"depends_on": "eval: doc.is_standard",
"fieldname": "module",
"fieldtype": "Link",
"label": "Module",
"mandatory_depends_on": "eval: doc.is_standard",
"options": "Module Def"
}
],
"links": [],
"modified": "2020-04-29 13:26:37.362482",
"modified": "2020-07-23 11:05:41.890459",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",

View file

@ -4,6 +4,7 @@
from __future__ import unicode_literals
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
import frappe
from frappe import _
import json
@ -15,7 +16,23 @@ class Dashboard(Document):
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module)
def validate(self):
if not frappe.conf.developer_mode and self.is_standard:
frappe.throw('Cannot edit Standard Dashboards')
if self.is_standard:
non_standard_docs_map = {
'Dashboard Chart': get_non_standard_charts_in_dashboard(self),
'Number Card': get_non_standard_cards_in_dashboard(self)
}
if non_standard_docs_map['Dashboard Chart'] or non_standard_docs_map['Number Card']:
message = get_non_standard_warning_message(non_standard_docs_map)
frappe.throw(message, title=_("Standard Not Set"), is_minimizable=True)
self.validate_custom_options()
def validate_custom_options(self):
@ -48,3 +65,29 @@ def get_permitted_cards(dashboard_name):
if frappe.has_permission('Number Card', doc=card.card):
permitted_cards.append(card)
return permitted_cards
def get_non_standard_charts_in_dashboard(dashboard):
non_standard_charts = [doc.name for doc in frappe.get_list('Dashboard Chart', {'is_standard': 0})]
return [chart_link.chart for chart_link in dashboard.charts if chart_link.chart in non_standard_charts]
def get_non_standard_cards_in_dashboard(dashboard):
non_standard_cards = [doc.name for doc in frappe.get_list('Number Card', {'is_standard': 0})]
return [card_link.card for card_link in dashboard.cards if card_link.card in non_standard_cards]
def get_non_standard_warning_message(non_standard_docs_map):
message = _('''Please set the following documents in this Dashboard as standard first.''')
def get_html(docs, doctype):
html = '<p>{}</p>'.format(frappe.bold(doctype))
for doc in docs:
html += '<div><a href="#Form/{doctype}/{doc}">{doc}</a></div>'.format(doctype=doctype, doc=doc)
html += '<br>'
return html
html = message + '<br>'
for doctype in non_standard_docs_map:
if non_standard_docs_map[doctype]:
html += get_html(non_standard_docs_map[doctype], doctype)
return html

View file

@ -9,46 +9,41 @@ frappe.ui.form.on('Dashboard Chart', {
frm.add_fetch('source', 'timeseries', 'timeseries');
},
before_save: function(frm) {
let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null');
let static_filters = JSON.parse(frm.doc.filters_json || 'null');
static_filters =
frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters);
frm.set_value('filters_json', JSON.stringify(static_filters));
frm.trigger('show_filters');
},
refresh: function(frm) {
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();
}
});
frm.set_df_property("filters_section", "hidden", 1);
frm.set_df_property("dynamic_filters_section", "hidden", 1);
frm.trigger('set_time_series');
frm.set_query('document_type', function() {
return {
@ -66,6 +61,15 @@ frappe.ui.form.on('Dashboard Chart', {
if (!frappe.boot.developer_mode) {
frm.set_df_property("custom_options", "hidden", 1);
}
},
is_standard: function(frm) {
if (frappe.boot.developer_mode && frm.doc.is_standard) {
frm.trigger('render_dynamic_filters_table');
} else {
frm.set_df_property("dynamic_filters_section", "hidden", 1);
}
},
source: function(frm) {
@ -111,6 +115,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_value('based_on', '');
frm.set_value('value_based_on', '');
frm.set_value('filters_json', '[]');
frm.set_value('dynamic_filters_json', '[]');
frm.trigger('update_options');
},
@ -119,6 +124,8 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_value('y_axis', []);
frm.set_df_property('x_field', 'options', []);
frm.set_value('filters_json', '{}');
frm.set_value('dynamic_filters_json', '{}');
frm.set_value('use_report_chart', 0);
frm.trigger('set_chart_report_filters');
},
@ -146,7 +153,10 @@ frappe.ui.form.on('Dashboard Chart', {
},
set_chart_field_options: function(frm) {
let filters = frm.doc.filters_json.length > 2? JSON.parse(frm.doc.filters_json): null;
let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
filters = frappe.dashboard_utils.get_all_filters(frm.doc);
}
frappe.xcall(
'frappe.desk.query_report.run',
{
@ -156,16 +166,13 @@ frappe.ui.form.on('Dashboard Chart', {
}
).then(data => {
frm.report_data = data;
if (!data.chart) {
frm.set_value('is_custom', 0);
frm.set_df_property('is_custom', 'hidden', 1);
} else {
frm.set_df_property('is_custom', 'hidden', 0);
}
let report_has_chart = Boolean(data.chart);
if (!frm.doc.is_custom) {
frm.set_df_property('use_report_chart', 'hidden', !report_has_chart);
if (!frm.doc.use_report_chart) {
if (data.result.length) {
frm.field_options = frappe.report_utils.get_possible_chart_options(data.columns, data);
frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields);
if (!frm.field_options.numeric_fields.length) {
frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`));
@ -240,11 +247,14 @@ frappe.ui.form.on('Dashboard Chart', {
show_filters: function(frm) {
frm.chart_filters = [];
frappe.dashboard_utils.get_filters_for_chart_type(frm.doc).then(filters => {
if (filters) {
frm.chart_filters = filters;
}
if (filters) {
frm.chart_filters = filters;
}
frm.trigger('render_filters_table');
frm.trigger('render_filters_table');
if (frappe.boot.developer_mode && frm.doc.is_standard) {
frm.trigger('render_dynamic_filters_table');
}
});
},
@ -257,8 +267,8 @@ frappe.ui.form.on('Dashboard Chart', {
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
<thead>
<tr>
<th style="width: 33%">${__('Filter')}</th>
<th style="width: 33%">${__('Condition')}</th>
<th style="width: 20%">${__('Filter')}</th>
<th style="width: 20%">${__('Condition')}</th>
<th>${__('Value')}</th>
</tr>
</thead>
@ -281,7 +291,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 = [
{
@ -306,7 +316,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 =
@ -378,4 +388,102 @@ frappe.ui.form.on('Dashboard Chart', {
});
},
render_dynamic_filters_table(frm) {
frm.set_df_property("dynamic_filters_section", "hidden", 0);
let is_document_type = frm.doc.chart_type !== 'Report'
&& frm.doc.chart_type !== 'Custom';
let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty();
frm.dynamic_filter_table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
<thead>
<tr>
<th style="width: 20%">${__('Filter')}</th>
<th style="width: 20%">${__('Condition')}</th>
<th>${__('Value')}</th>
</tr>
</thead>
<tbody></tbody>
</table>`).appendTo(wrapper);
frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
? JSON.parse(frm.doc.dynamic_filters_json)
: null;
frm.trigger('set_dynamic_filters_in_table');
let filters = JSON.parse(frm.doc.filters_json || '[]');
let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog(
is_document_type, filters, frm.dynamic_filters
);
frm.dynamic_filter_table.on('click', () => {
let dialog = new frappe.ui.Dialog({
title: __('Set Dynamic Filters'),
fields: fields,
primary_action: () => {
let values = dialog.get_values();
dialog.hide();
let dynamic_filters = [];
for (let key of Object.keys(values)) {
if (is_document_type) {
let [doctype, fieldname] = key.split(':');
dynamic_filters.push([doctype, fieldname, '=', values[key]]);
}
}
if (is_document_type) {
frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters));
} else {
frm.set_value('dynamic_filters_json', JSON.stringify(values));
}
frm.trigger('set_dynamic_filters_in_table');
},
primary_action_label: "Set"
});
dialog.show();
dialog.set_values(frm.dynamic_filters);
});
},
set_dynamic_filters_in_table: function(frm) {
frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
? JSON.parse(frm.doc.dynamic_filters_json)
: null;
if (!frm.dynamic_filters) {
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
${__("Click to Set Dynamic Filters")}</td></tr>`);
frm.dynamic_filter_table.find('tbody').html(filter_row);
} else {
let filter_rows = '';
if ($.isArray(frm.dynamic_filters)) {
frm.dynamic_filters.forEach(filter => {
filter_rows +=
`<tr>
<td>${filter[1]}</td>
<td>${filter[2] || ""}</td>
<td>${filter[3]}</td>
</tr>`;
});
} else {
let condition = '=';
for (let [key, val] of Object.entries(frm.dynamic_filters)) {
filter_rows +=
`<tr>
<td>${key}</td>
<td>${condition}</td>
<td>${val || ""}</td>
</tr>`
;
}
}
frm.dynamic_filter_table.find('tbody').html(filter_rows);
}
}
});

View file

@ -7,10 +7,12 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"is_standard",
"module",
"chart_name",
"chart_type",
"report_name",
"is_custom",
"use_report_chart",
"x_field",
"y_axis",
"source",
@ -32,10 +34,12 @@
"type",
"filters_section",
"filters_json",
"dynamic_filters_section",
"dynamic_filters_json",
"chart_options_section",
"color",
"column_break_2",
"custom_options",
"column_break_2",
"color",
"section_break_10",
"last_synced_on"
],
@ -67,7 +71,8 @@
"fieldname": "document_type",
"fieldtype": "Link",
"label": "Document Type",
"options": "DocType"
"options": "DocType",
"set_only_once": 1
},
{
"depends_on": "eval: doc.timeseries && ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
@ -189,32 +194,27 @@
"label": "To Date"
},
{
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom",
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart",
"fieldname": "x_field",
"fieldtype": "Select",
"label": "X Field",
"mandatory_depends_on": "eval: doc.report_name && !doc.is_custom"
"mandatory_depends_on": "eval: doc.report_name && !doc.use_report_chart"
},
{
"depends_on": "eval:doc.chart_type === 'Report'",
"fieldname": "report_name",
"fieldtype": "Link",
"label": "Report Name",
"options": "Report"
"mandatory_depends_on": "eval:doc.chart_type === 'Report'",
"options": "Report",
"set_only_once": 1
},
{
"default": "0",
"depends_on": "eval: doc.report_name",
"fieldname": "is_custom",
"fieldtype": "Check",
"label": "Is Custom"
},
{
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom",
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart",
"fieldname": "y_axis",
"fieldtype": "Table",
"label": "Y Axis",
"mandatory_depends_on": "eval:doc.report_name && !doc.is_custom",
"mandatory_depends_on": "eval:doc.report_name && !doc.use_report_chart",
"options": "Dashboard Chart Field"
},
{
@ -235,10 +235,43 @@
"fieldname": "heatmap_year",
"fieldtype": "Select",
"label": "Year"
},
{
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard",
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
},
{
"depends_on": "eval: doc.is_standard",
"fieldname": "module",
"fieldtype": "Link",
"label": "Module",
"mandatory_depends_on": "eval: doc.is_standard",
"options": "Module Def"
},
{
"fieldname": "dynamic_filters_json",
"fieldtype": "Code",
"label": "Dynamic Filters JSON",
"options": "JSON"
},
{
"fieldname": "dynamic_filters_section",
"fieldtype": "Section Break",
"label": "Dynamic Filters"
},
{
"default": "0",
"depends_on": "eval: doc.report_name",
"fieldname": "use_report_chart",
"fieldtype": "Check",
"label": "Use Report Chart"
}
],
"links": [],
"modified": "2020-05-16 15:03:02.455395",
"modified": "2020-07-23 11:10:33.509497",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",

View file

@ -13,6 +13,7 @@ from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
def get_permission_query_conditions(user):
@ -27,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
)
@ -80,7 +94,9 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
to_date = get_datetime(chart.to_date)
timegrain = time_interval or chart.time_interval
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
if not filters:
filters = []
# don't include cancelled documents
filters.append([chart.document_type, 'docstatus', '<', 2, False])
@ -125,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:
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()
@ -260,9 +282,7 @@ def get_result(data, timegrain, from_date, to_date):
start_date = getdate(from_date)
end_date = getdate(to_date)
result = []
if timegrain == 'Daily':
result.append([start_date, 0.0])
result = [[start_date, 0.0]]
while start_date < end_date:
next_date = get_next_expected_date(start_date, timegrain)
@ -280,11 +300,8 @@ def get_result(data, timegrain, from_date, to_date):
def get_next_expected_date(date, timegrain):
next_date = None
if timegrain=='Daily':
next_date = add_to_date(date, days=1)
else:
# given date is always assumed to be the period ending date
next_date = get_period_ending(add_to_date(date, days=1), timegrain)
# given date is always assumed to be the period ending date
next_date = get_period_ending(add_to_date(date, days=1), timegrain)
return getdate(next_date)
def get_period_ending(date, timegrain):
@ -340,6 +357,7 @@ def get_year_ending(date):
# last day of this month
return add_to_date(date, days=-1)
@frappe.whitelist()
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',
@ -352,8 +370,13 @@ class DashboardChart(Document):
def on_update(self):
frappe.cache().delete_key('chart-data:{}'.format(self.name))
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[['Dashboard Chart', self.name]], record_module=self.module)
def validate(self):
if not frappe.conf.developer_mode and self.is_standard:
frappe.throw('Cannot edit Standard charts')
if self.chart_type != 'Custom' and self.chart_type != 'Report':
self.check_required_field()
self.check_document_type()

View file

@ -4,13 +4,12 @@
from __future__ import unicode_literals
import unittest, frappe
from frappe.utils import getdate, formatdate
from frappe.utils import getdate, formatdate, get_last_day
from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get,
get_period_ending)
from datetime import datetime
from dateutil.relativedelta import relativedelta
import calendar
class TestDashboardChart(unittest.TestCase):
def test_period_ending(self):
@ -53,16 +52,18 @@ class TestDashboardChart(unittest.TestCase):
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Dashboard Chart', refresh = 1)
for idx in range(13):
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
result = get(chart_name='Test Dashboard Chart', refresh=1)
self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
cur_date += relativedelta(months=1)
for idx in range(1, 13):
month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
# self.assertEqual(result.get('datasets')[0].get('values')[:-1],
# [44, 28, 8, 11, 2, 6, 18, 6, 4, 5, 15, 13])
frappe.db.rollback()
def test_empty_dashboard_chart(self):
@ -79,15 +80,20 @@ class TestDashboardChart(unittest.TestCase):
based_on = 'creation',
timespan = 'Last Year',
time_interval = 'Monthly',
filters_json = '{}',
filters_json = '[]',
timeseries = 1
)).insert()
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Empty Dashboard Chart', refresh = 1)
for idx in range(13):
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
result = get(chart_name ='Test Empty Dashboard Chart', refresh=1)
self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
cur_date += relativedelta(months=1)
for idx in range(1, 13):
month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
@ -111,15 +117,20 @@ class TestDashboardChart(unittest.TestCase):
based_on = 'creation',
timespan = 'Last Year',
time_interval = 'Monthly',
filters_json = '{}',
filters_json = '[]',
timeseries = 1
)).insert()
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1)
for idx in range(13):
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
cur_date += relativedelta(months=1)
for idx in range(1, 13):
month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
@ -141,7 +152,7 @@ class TestDashboardChart(unittest.TestCase):
chart_type = 'Group By',
document_type = 'ToDo',
group_by_based_on = 'status',
filters_json = '{}',
filters_json = '[]',
)).insert()
result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1)
@ -168,7 +179,7 @@ class TestDashboardChart(unittest.TestCase):
time_interval = 'Daily',
from_date = datetime(2019, 1, 6),
to_date = datetime(2019, 1, 11),
filters_json = '{}',
filters_json = '[]',
timeseries = 1
)).insert()
@ -200,22 +211,24 @@ class TestDashboardChart(unittest.TestCase):
time_interval = 'Weekly',
from_date = datetime(2018, 12, 30),
to_date = datetime(2019, 1, 15),
filters_json = '{}',
filters_json = '[]',
timeseries = 1
)).insert()
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 800.0, 0.0])
self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0])
self.assertEqual(result.get('labels'), [formatdate('2018-12-30'), formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
frappe.db.rollback()
def insert_test_records():
create_new_communication(datetime(2019, 1, 10), 100)
create_new_communication(datetime(2018, 12, 30), 50)
create_new_communication(datetime(2019, 1, 4), 100)
create_new_communication(datetime(2019, 1, 6), 200)
create_new_communication(datetime(2019, 1, 7), 400)
create_new_communication(datetime(2019, 1, 8), 300)
create_new_communication(datetime(2019, 1, 10), 100)
def create_new_communication(date, rating):
communication = {

View file

@ -1,162 +1,69 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"actions": [],
"autoname": "field:source_name",
"beta": 0,
"creation": "2019-02-06 07:55:29.579840",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"source_name",
"module",
"timeseries"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "source_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": "Source Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "module",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Module",
"length": 0,
"no_copy": 0,
"options": "Module Def",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"default": "0",
"fieldname": "timeseries",
"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": "Timeseries",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Timeseries"
}
],
"has_web_view": 0,
"hide_toolbar": 0,
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-09 14:20:51.548207",
"links": [],
"modified": "2020-06-26 18:00:37.421491",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart Source",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View file

@ -18,10 +18,6 @@ def get_config(name):
return f.read()
class DashboardChartSource(Document):
def validate(self):
if frappe.session.user != "Administrator":
frappe.throw(_("Only Administrator is allowed to create Dashboard Chart Sources"))
def on_update(self):
export_to_files(record_list=[[self.doctype, self.name]],
record_module=self.module, create_init=True)

View file

@ -69,7 +69,6 @@ def make_notification_logs(doc, users):
_doc = frappe.new_doc('Notification Log')
_doc.update(doc)
_doc.for_user = user
_doc.subject = _doc.subject.replace('<div>', '').replace('</div>', '')
if _doc.for_user != _doc.from_user or doc.type == 'Energy Point' or doc.type == 'Alert':
_doc.insert(ignore_permissions=True)

View file

@ -3,8 +3,130 @@
frappe.ui.form.on('Number Card', {
refresh: function(frm) {
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frm.disable_form();
}
frm.set_df_property("filters_section", "hidden", 1);
frm.set_df_property("dynamic_filters_section", "hidden", 1);
frm.trigger('set_options');
if (!frm.doc.type) {
frm.set_value('type', 'Document Type');
}
if (frm.doc.type == 'Report' && frm.doc.report_name) {
frm.trigger('set_report_filters');
}
if (frm.doc.type == 'Custom') {
if (!frappe.boot.developer_mode) {
frm.disable_form();
}
frm.filters = eval(frm.doc.filters_config);
frm.trigger('set_filters_description');
frm.trigger('set_method_description');
frm.trigger('render_filters_table');
}
frm.trigger('create_add_to_dashboard_button');
},
create_add_to_dashboard_button: function(frm) {
frm.add_custom_button('Add Card to Dashboard', () => {
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 {
dialog.show();
}
});
},
before_save: function(frm) {
let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null');
let static_filters = JSON.parse(frm.doc.filters_json || 'null');
static_filters =
frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters);
frm.set_value('filters_json', JSON.stringify(static_filters));
frm.trigger('render_filters_table');
frm.trigger('render_dynamic_filters_table');
},
is_standard: function(frm) {
frm.trigger('render_dynamic_filters_table');
frm.set_df_property("dynamic_filters_section", "hidden", 1);
},
set_filters_description: function(frm) {
if (frm.doc.type == 'Custom') {
frm.fields_dict.filters_config.set_description(`
Set the filters here. For example:
<pre class="small text-muted">
<code>
[{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
fieldname: "account",
label: __("Account"),
fieldtype: "Link",
options: "Account",
reqd: 1
}]
</code></pre>`);
}
},
set_method_description: function(frm) {
if (frm.doc.type == 'Custom') {
frm.fields_dict.method.set_description(`
Set the path to a whitelisted function that will return the number on the card in the format:
<pre class="small text-muted">
<code>
{
"value": value,
"fieldtype": "Currency"
}
</code></pre>`);
}
},
type: function(frm) {
frm.trigger('set_filters_description');
if (frm.doc.type == 'Report') {
frm.set_query('report_name', () => {
return {
filters: {
'report_type': ['!=', 'Report Builder']
}
};
});
}
},
report_name: function(frm) {
frm.filters = [];
frm.set_value('filters_json', '{}');
frm.set_value('dynamic_filters_json', '{}');
frm.set_df_property('report_field', 'options', []);
frm.trigger('set_report_filters');
},
filters_config: function(frm) {
frm.filters = eval(frm.doc.filters_config);
const filter_values = frappe.report_utils.get_filter_values(frm.filters);
frm.set_value('filters_json', JSON.stringify(filter_values));
frm.trigger('render_filters_table');
},
@ -17,11 +139,16 @@ frappe.ui.form.on('Number Card', {
};
});
frm.set_value('filters_json', '[]');
frm.set_value('dynamic_filters_json', '[]');
frm.set_value('aggregate_function_based_on', '');
frm.trigger('set_options');
},
set_options: function(frm) {
if (frm.doc.type !== 'Document Type') {
return;
}
let aggregate_based_on_fields = [];
const doctype = frm.doc.document_type;
@ -40,80 +167,275 @@ frappe.ui.form.on('Number Card', {
frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields);
});
frm.trigger('render_filters_table');
frm.trigger('render_dynamic_filters_table');
}
},
set_report_filters: function(frm) {
const report_name = frm.doc.report_name;
if (report_name) {
frappe.report_utils.get_report_filters(report_name).then(filters => {
if (filters) {
frm.filters = filters;
const filter_values = frappe.report_utils.get_filter_values(filters);
if (frm.doc.filters_json.length <= 2) {
frm.set_value('filters_json', JSON.stringify(filter_values));
}
}
frm.trigger('render_filters_table');
frm.trigger('set_report_field_options');
frm.trigger('render_dynamic_filters_table');
});
}
},
set_report_field_options: function(frm) {
let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
filters = frappe.dashboard_utils.get_all_filters(frm.doc);
}
frappe.xcall(
'frappe.desk.query_report.run',
{
report_name: frm.doc.report_name,
filters: filters,
ignore_prepared_report: 1
}
).then(data => {
if (data.result.length) {
frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
frm.set_df_property('report_field', 'options', frm.field_options.numeric_fields);
if (!frm.field_options.numeric_fields.length) {
frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`));
}
} else {
frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name'));
}
});
},
render_filters_table: function(frm) {
frm.set_df_property("filters_section", "hidden", 0);
let is_document_type = frm.doc.type == 'Document Type';
let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default;
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
frm.filter_table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
<thead>
<tr>
<th style="width: 33%">${__('Filter')}</th>
<th style="width: 33%">${__('Condition')}</th>
<th style="width: 20%">${__('Filter')}</th>
<th style="width: 20%">${__('Condition')}</th>
<th>${__('Value')}</th>
</tr>
</thead>
<tbody></tbody>
</table>`).appendTo(wrapper);
$(`<p class="text-muted small">${__("Click table to edit")}</p>`).appendTo(wrapper);
let filters = JSON.parse(frm.doc.filters_json || '[]');
let filters_set = false;
// Set dynamic filters for reports
if (frm.doc.type == 'Report') {
let set_filters = false;
frm.filters.forEach(f => {
if (is_dynamic_filter(f)) {
filters[f.fieldname] = f.default;
set_filters = true;
}
});
set_filters && frm.set_value('filters_json', JSON.stringify(filters));
}
let fields = [];
if (is_document_type) {
fields = [
{
fieldtype: 'HTML',
fieldname: 'filter_area',
}
];
if (filters.length) {
filters.forEach(filter => {
const filter_row =
$(`<tr>
<td>${filter[1]}</td>
<td>${filter[2] || ""}</td>
<td>${filter[3]}</td>
</tr>`);
table.find('tbody').append(filter_row);
});
filters_set = true;
}
} else if (frm.filters.length) {
fields = frm.filters.filter(f => f.fieldname);
fields.map(f => {
if (filters[f.fieldname]) {
let condition = '=';
const filter_row =
$(`<tr>
<td>${f.label}</td>
<td>${condition}</td>
<td>${filters[f.fieldname] || ""}</td>
</tr>`);
table.find('tbody').append(filter_row);
if (!filters_set) filters_set = true;
}
});
}
if (!filters_set) {
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
${__("Click to Set Filters")}</td></tr>`);
table.find('tbody').append(filter_row);
}
table.on('click', () => {
let dialog = new frappe.ui.Dialog({
title: __('Set Filters'),
fields: fields.filter(f => !is_dynamic_filter(f)),
primary_action: function() {
let values = this.get_values();
if (values) {
this.hide();
if (is_document_type) {
let filters = frm.filter_group.get_filters();
frm.set_value('filters_json', JSON.stringify(filters));
} else {
frm.set_value('filters_json', JSON.stringify(values));
}
frm.trigger('render_filters_table');
}
},
primary_action_label: "Set"
});
if (is_document_type) {
frm.filter_group = new frappe.ui.FilterGroup({
parent: dialog.get_field('filter_area').$wrapper,
doctype: frm.doc.document_type,
on_change: () => {},
});
filters && frm.filter_group.add_filters_to_filter_group(filters);
}
dialog.show();
if (frm.doc.type == 'Report') {
//Set query report object so that it can be used while fetching filter values in the report
frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
frappe.query_reports[frm.doc.report_name]
&& frappe.query_reports[frm.doc.report_name].onload
&& frappe.query_reports[frm.doc.report_name].onload(frappe.query_report);
}
dialog.set_values(filters);
});
},
render_dynamic_filters_table(frm) {
if (!frappe.boot.developer_mode || !frm.doc.is_standard || frm.doc.type == 'Custom') {
return;
}
frm.set_df_property("dynamic_filters_section", "hidden", 0);
let is_document_type = frm.doc.type == 'Document Type';
let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty();
frm.dynamic_filter_table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
<thead>
<tr>
<th style="width: 20%">${__('Filter')}</th>
<th style="width: 20%">${__('Condition')}</th>
<th>${__('Value')}</th>
</tr>
</thead>
<tbody></tbody>
</table>`).appendTo(wrapper);
frm.filters = JSON.parse(frm.doc.filters_json || '[]');
frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
? JSON.parse(frm.doc.dynamic_filters_json)
: null;
frm.trigger('set_filters_in_table');
frm.trigger('set_dynamic_filters_in_table');
frm.filter_table.on('click', () => {
let filters = JSON.parse(frm.doc.filters_json || '[]');
let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog(
is_document_type, filters, frm.dynamic_filters
);
frm.dynamic_filter_table.on('click', () => {
let dialog = new frappe.ui.Dialog({
title: __('Set Filters'),
fields: [{
fieldtype: 'HTML',
fieldname: 'filter_area',
}],
primary_action: function() {
let values = this.get_values();
if (values) {
this.hide();
frm.filters = frm.filter_group.get_filters();
frm.set_value('filters_json', JSON.stringify(frm.filters));
frm.trigger('set_filters_in_table');
title: __('Set Dynamic Filters'),
fields: fields,
primary_action: () => {
let values = dialog.get_values();
dialog.hide();
let dynamic_filters = [];
for (let key of Object.keys(values)) {
if (is_document_type) {
let [doctype, fieldname] = key.split(':');
dynamic_filters.push([doctype, fieldname, '=', values[key]]);
}
}
if (is_document_type) {
frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters));
} else {
frm.set_value('dynamic_filters_json', JSON.stringify(values));
}
frm.trigger('set_dynamic_filters_in_table');
},
primary_action_label: "Set"
});
frappe.dashboards.filters_dialog = dialog;
frm.filter_group = new frappe.ui.FilterGroup({
parent: dialog.get_field('filter_area').$wrapper,
doctype: frm.doc.document_type,
on_change: () => {},
});
frm.filter_group.add_filters_to_filter_group(frm.filters);
dialog.show();
dialog.set_values(frm.filters);
dialog.set_values(frm.dynamic_filters);
});
},
set_filters_in_table: function(frm) {
if (!frm.filters.length) {
set_dynamic_filters_in_table: function(frm) {
frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
? JSON.parse(frm.doc.dynamic_filters_json)
: null;
if (!frm.dynamic_filters) {
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
${__("Click to Set Filters")}</td></tr>`);
frm.filter_table.find('tbody').html(filter_row);
${__("Click to Set Dynamic Filters")}</td></tr>`);
frm.dynamic_filter_table.find('tbody').html(filter_row);
} else {
let filter_rows = '';
frm.filters.forEach(filter => {
filter_rows +=
`<tr>
<td>${filter[1]}</td>
<td>${filter[2] || ""}</td>
<td>${filter[3]}</td>
</tr>`;
if ($.isArray(frm.dynamic_filters)) {
frm.dynamic_filters.forEach(filter => {
filter_rows +=
`<tr>
<td>${filter[1]}</td>
<td>${filter[2] || ""}</td>
<td>${filter[3]}</td>
</tr>`;
});
} else {
let condition = '=';
for (let [key, val] of Object.entries(frm.dynamic_filters)) {
filter_rows +=
`<tr>
<td>${key}</td>
<td>${condition}</td>
<td>${val || ""}</td>
</tr>`
;
}
}
});
frm.filter_table.find('tbody').html(filter_rows);
frm.dynamic_filter_table.find('tbody').html(filter_rows);
}
}
});

View file

@ -1,39 +1,53 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2020-04-15 18:06:39.444683",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"is_standard",
"module",
"label",
"type",
"report_name",
"method",
"function",
"aggregate_function_based_on",
"column_break_2",
"document_type",
"report_field",
"report_function",
"is_public",
"custom_configuration_section",
"filters_config",
"stats_section",
"show_percentage_stats",
"stats_time_interval",
"filters_section",
"filters_json",
"dynamic_filters_section",
"dynamic_filters_json",
"section_break_16",
"color"
],
"fields": [
{
"depends_on": "eval: doc.type == 'Document Type'",
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"reqd": 1
"mandatory_depends_on": "eval: doc.type == 'Document Type'",
"options": "DocType"
},
{
"depends_on": "eval: doc.document_type",
"depends_on": "eval: doc.type == 'Document Type'",
"fieldname": "function",
"fieldtype": "Select",
"label": "Function",
"options": "Count\nSum\nAverage\nMinimum\nMaximum",
"reqd": 1
"mandatory_depends_on": "eval: doc.type == 'Document Type'",
"options": "Count\nSum\nAverage\nMinimum\nMaximum"
},
{
"depends_on": "eval: doc.function !== 'Count'",
@ -92,13 +106,92 @@
"options": "Daily\nWeekly\nMonthly\nYearly"
},
{
"depends_on": "eval: doc.type == 'Document Type'",
"fieldname": "stats_section",
"fieldtype": "Section Break",
"label": "Stats"
},
{
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard",
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
},
{
"depends_on": "eval: doc.is_standard",
"fieldname": "module",
"fieldtype": "Link",
"label": "Module",
"mandatory_depends_on": "eval: doc.is_standard",
"options": "Module Def"
},
{
"fieldname": "dynamic_filters_json",
"fieldtype": "Code",
"label": "Dynamic Filters JSON",
"options": "JSON"
},
{
"fieldname": "section_break_16",
"fieldtype": "Section Break"
},
{
"fieldname": "dynamic_filters_section",
"fieldtype": "Section Break",
"label": "Dynamic Filters Section"
},
{
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
"options": "Document Type\nReport\nCustom"
},
{
"depends_on": "eval: doc.type == 'Report'",
"fieldname": "report_name",
"fieldtype": "Link",
"label": "Report Name",
"mandatory_depends_on": "eval: doc.type == 'Report'",
"options": "Report"
},
{
"depends_on": "eval: doc.type == 'Report'",
"fieldname": "report_field",
"fieldtype": "Select",
"label": "Field",
"mandatory_depends_on": "eval: doc.type == 'Report'"
},
{
"depends_on": "eval: doc.type == 'Custom'",
"fieldname": "method",
"fieldtype": "Data",
"label": "Method",
"mandatory_depends_on": "eval: doc.type == 'Custom'"
},
{
"depends_on": "eval: doc.type == 'Custom'",
"fieldname": "custom_configuration_section",
"fieldtype": "Section Break",
"label": "Custom Configuration"
},
{
"fieldname": "filters_config",
"fieldtype": "Code",
"label": "Filters Configuration",
"options": "JSON"
},
{
"depends_on": "eval: doc.type == 'Report'",
"fieldname": "report_function",
"fieldtype": "Select",
"label": "Function",
"mandatory_depends_on": "eval: doc.type == 'Report'",
"options": "Sum\nAverage\nMinimum\nMaximum"
}
],
"links": [],
"modified": "2020-05-06 19:47:57.753574",
"modified": "2020-07-23 11:11:03.391719",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",

View file

@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils import cint
from frappe.model.naming import append_number_if_name_exists
from frappe.modules.export_file import export_to_files
class NumberCard(Document):
def autoname(self):
@ -16,6 +17,10 @@ class NumberCard(Document):
if frappe.db.exists("Number Card", self.name):
self.name = append_number_if_name_exists('Number Card', self.name)
def on_update(self):
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[['Number Card', self.name]], record_module=self.module)
def get_permission_query_conditions(user=None):
if not user:
user = frappe.session.user
@ -27,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)
@ -47,7 +56,7 @@ def has_permission(doc, ptype, user):
return False
@frappe.whitelist()
def get_result(doc, to_date=None):
def get_result(doc, filters, to_date=None):
doc = frappe.parse_json(doc)
fields = []
sql_function_map = {
@ -65,10 +74,13 @@ def get_result(doc, to_date=None):
else:
fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)]
filters = frappe.parse_json(doc.filters_json)
filters = frappe.parse_json(filters)
if not filters:
filters = []
if to_date:
filters.append([doc.document_type, 'creation', '<', to_date, False])
filters.append([doc.document_type, 'creation', '<', to_date])
res = frappe.db.get_list(doc.document_type, fields=fields, filters=filters)
number = res[0]['result'] if res else 0
@ -76,7 +88,7 @@ def get_result(doc, to_date=None):
return cint(number)
@frappe.whitelist()
def get_percentage_difference(doc, result):
def get_percentage_difference(doc, filters, result):
doc = frappe.parse_json(doc)
result = frappe.parse_json(result)
@ -85,13 +97,13 @@ def get_percentage_difference(doc, result):
if not doc.get('show_percentage_stats'):
return
previous_result = calculate_previous_result(doc)
previous_result = calculate_previous_result(doc, filters)
difference = (result - previous_result)/100.0
return difference
def calculate_previous_result(doc):
def calculate_previous_result(doc, filters):
from frappe.utils import add_to_date
current_date = frappe.utils.now()
@ -104,7 +116,7 @@ def calculate_previous_result(doc):
else:
previous_date = add_to_date(current_date, years=-1)
number = get_result(doc, previous_date)
number = get_result(doc, filters, previous_date)
return number
@frappe.whitelist()
@ -116,11 +128,15 @@ def create_number_card(args):
doc.insert(ignore_permissions=True)
return doc
@frappe.whitelist()
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))
@ -147,3 +163,28 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
search_conditions=search_conditions,
conditions=conditions
), values)
@frappe.whitelist()
def create_report_number_card(args):
card = create_number_card(args)
args = frappe.parse_json(args)
args.name = card.name
if args.dashboard:
add_card_to_dashboard(frappe.as_json(args))
@frappe.whitelist()
def add_card_to_dashboard(args):
args = frappe.parse_json(args)
dashboard = frappe.get_doc('Dashboard', args.dashboard)
dashboard_link = frappe.new_doc('Number Card Link')
dashboard_link.card = args.name
if args.set_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

@ -1,4 +1,5 @@
{
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2016-05-25 09:43:44.767581",
"doctype": "DocType",
@ -46,4 +47,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
}
}

View file

@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module
@frappe.whitelist()
def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=None):
def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
"""
Get all nested submitted linked doctype linkinfo
@ -31,34 +31,27 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=Non
if not docs:
docs = []
if not linked:
linked = {}
if not visited:
visited = []
visited = {}
if name in visited:
if doctype not in visited:
visited[doctype] = []
if name in visited[doctype]:
return
linkinfo = get_linked_doctypes(doctype)
linked_docs = get_linked_docs(doctype, name, linkinfo)
link_count = 0
visited.append(name)
visited[doctype].append(name)
for link_doctype, link_names in linked_docs.items():
if link_doctype not in linked:
linked[link_doctype] = []
for link in link_names:
if link['name'] == name:
continue
if linked and name in linked[link_doctype]:
continue
linked[link_doctype].append(link['name'])
docinfo = link.update({"doctype": link_doctype})
validated_doc = validate_linked_doc(docinfo)
@ -66,10 +59,8 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=Non
continue
link_count += 1
if link.name in [doc.get("name") for doc in docs]:
continue
links = get_submitted_linked_docs(link_doctype, link.name, docs, linked, visited)
links = get_submitted_linked_docs(link_doctype, link.name, docs, visited)
if links:
docs.append({
"doctype": link_doctype,

View file

@ -100,6 +100,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
"shared": frappe.share.get_users(doc.doctype, doc.name),
"views": get_view_logs(doc.doctype, doc.name),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
"milestones": get_milestones(doc.doctype, doc.name),
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
"tags": get_tags(doc.doctype, doc.name),
@ -277,3 +278,14 @@ def get_document_email(doctype, name):
def get_automatic_email_link():
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
def get_additional_timeline_content(doctype, docname):
contents = []
hooks = frappe.get_hooks().get('additional_timeline_content', {})
methods_for_all_doctype = hooks.get('*', [])
methods_for_current_doctype = hooks.get(doctype, [])
for method in methods_for_all_doctype + methods_for_current_doctype:
contents.extend(frappe.get_attr(method)(doctype, docname) or [])
return contents

View file

@ -18,12 +18,7 @@ def savedocs(doc, action):
if doc.docstatus==1:
doc.submit()
else:
try:
doc.save()
except frappe.NameError as e:
doctype, name, original_exception = e if isinstance(e, tuple) else (doc.doctype or "", doc.name or "", None)
frappe.msgprint(frappe._("{0} {1} already exists").format(doctype, name))
raise
doc.save()
# update recent documents
run_onload(doc)

View file

@ -14,13 +14,16 @@ def get_leaderboards():
return leaderboards
@frappe.whitelist()
def get_energy_point_leaderboard(from_date, company = None, field = None, limit = None):
def get_energy_point_leaderboard(date_range, company = None, field = None, limit = None):
filters = [
['type', '!=', 'Review'],
]
if date_range:
date_range = frappe.parse_json(date_range)
filters.append(['creation', 'between', [date_range[0], date_range[1]]])
energy_point_users = frappe.db.get_all('Energy Point Log',
fields = ['user as name', 'sum(points) as value'],
filters = [
['type', '!=', 'Review'],
['creation', '>', from_date]
],
filters = filters,
group_by = 'user',
order_by = 'value desc'
)

View file

@ -49,7 +49,7 @@ class Leaderboard {
this.timespans = [
"This Week", "This Month", "This Quarter", "This Year",
"Last Week", "Last Month", "Last Quarter", "Last Year",
"All Time", "Select From Date"
"All Time", "Select Date Range"
];
// for saving current selected filters
@ -113,7 +113,7 @@ class Leaderboard {
return {"label": __(d), value: d };
})
);
this.create_from_date_field();
this.create_date_range_field();
this.type_select = this.page.add_select(__("Field"),
this.options.selected_filter.map(d => {
@ -123,12 +123,12 @@ class Leaderboard {
this.timespan_select.on("change", (e) => {
this.options.selected_timespan = e.currentTarget.value;
if (this.options.selected_timespan === 'Select From Date') {
this.from_date_field.show();
if (this.options.selected_timespan === 'Select Date Range') {
this.date_range_field.show();
} else {
this.from_date_field.hide();
this.make_request();
this.date_range_field.hide();
}
this.make_request();
});
this.type_select.on("change", (e) => {
@ -137,21 +137,21 @@ class Leaderboard {
});
}
create_from_date_field() {
create_date_range_field() {
let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`);
this.from_date_field = $(`<div class="from-date-field"></div>`).insertAfter(timespan_field).hide();
this.date_range_field = $(`<div class="from-date-field"></div>`).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
df: {
fieldtype: 'Date',
fieldname: 'selected_from_date',
placeholder: frappe.datetime.month_start(),
default: frappe.datetime.month_start(),
fieldtype: 'DateRange',
fieldname: 'selected_date_range',
placeholder: "Date Range",
default: [frappe.datetime.month_start(), frappe.datetime.now_date()],
input_class: 'input-sm',
reqd: 1,
change: () => {
this.selected_from_date = date_field.get_value();
if (this.selected_from_date) this.make_request();
this.selected_date_range = date_field.get_value();
if (this.selected_date_range) this.make_request();
}
},
parent: $(this.parent).find('.from-date-field'),
@ -225,7 +225,7 @@ class Leaderboard {
frappe.call(
this.leaderboard_config[this.options.selected_doctype].method,
{
'from_date': this.get_from_date(),
'date_range': this.get_date_range(),
'company': this.options.selected_company,
'field': this.options.selected_filter_item,
'limit': this.leaderboard_limit,
@ -375,23 +375,22 @@ class Leaderboard {
</li>`);
}
get_from_date() {
get_date_range() {
let timespan = this.options.selected_timespan.toLowerCase();
let current_date = frappe.datetime.now_date();
let get_from_date = {
"this week": frappe.datetime.week_start(),
"this month": frappe.datetime.month_start(),
"this quarter": frappe.datetime.quarter_start(),
"this year": frappe.datetime.year_start(),
"last week": frappe.datetime.add_days(current_date, -7),
"last month": frappe.datetime.add_months(current_date, -1),
"last quarter": frappe.datetime.add_months(current_date, -3),
"last year": frappe.datetime.add_months(current_date, -12),
"all time": "",
"select from date": this.selected_from_date || frappe.datetime.month_start()
let date_range_map = {
"this week": [frappe.datetime.week_start(), current_date],
"this month": [frappe.datetime.month_start(), current_date],
"this quarter": [frappe.datetime.quarter_start(), current_date],
"this year": [frappe.datetime.year_start(), current_date],
"last week": [frappe.datetime.add_days(current_date, -7), current_date],
"last month": [frappe.datetime.add_months(current_date, -1), current_date],
"last quarter": [frappe.datetime.add_months(current_date, -3), current_date],
"last year": [frappe.datetime.add_months(current_date, -12), current_date],
"all time": null,
"select date range": this.selected_date_range || [frappe.datetime.month_start(), current_date]
}
return get_from_date[timespan];
return date_range_map[timespan];
}
}

View file

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

View file

@ -6,6 +6,7 @@ from __future__ import unicode_literals
import frappe, json
from frappe.utils import cstr, unique, cint
from frappe.permissions import has_permission
from frappe.handler import is_whitelisted
from frappe import _
from six import string_types
import re
@ -74,8 +75,17 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
if query and query.split()[0].lower()!="select":
# by method
frappe.response["values"] = frappe.call(query, doctype, txt,
searchfield, start, page_length, filters, as_dict=as_dict)
try:
is_whitelisted(frappe.get_attr(query))
frappe.response["values"] = frappe.call(query, doctype, txt,
searchfield, start, page_length, filters, as_dict=as_dict)
except Exception as e:
if frappe.local.conf.developer_mode:
raise e
else:
frappe.respond_as_web_page(title='Invalid Method', html='Method not found',
indicator_color='red', http_status_code=404)
return
elif not query and doctype in standard_queries:
# from standard queries
search_widget(doctype, txt, standard_queries[doctype][0],

View file

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

View file

@ -95,6 +95,11 @@ frappe.ui.form.on("Email Account", {
enable_incoming: function(frm) {
frm.doc.no_remaining = null; //perform full sync
//frm.set_df_property("append_to", "reqd", frm.doc.enable_incoming);
frm.trigger("warn_autoreply_on_incoming");
},
enable_auto_reply: function(frm) {
frm.trigger("warn_autoreply_on_incoming");
},
notify_if_unreplied: function(frm) {
@ -184,7 +189,18 @@ frappe.ui.form.on("Email Account", {
read as well as unread message from server. This may also cause the duplication\
of Communication (emails).");
frappe.confirm(msg, null, function() {
frm.set_value("email_sync_option", "ALL");
frm.set_value("email_sync_option", "UNSEEN");
});
}
},
warn_autoreply_on_incoming: function(frm) {
if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) {
var msg = __("Enabling auto reply on an incoming email account will send automated replies \
to all the synchronized emails. Do you wish to continue?");
frappe.confirm(msg, null, function() {
frm.set_value("enable_auto_reply", 0);
frappe.show_alert({message: __("Disabled Auto Reply"), indicator: "blue"});
});
}
}

View file

@ -478,26 +478,38 @@ class EmailAccount(Document):
if self.append_to and self.sender_field:
if self.subject_field:
# try and match by subject and sender
# if sent by same sender with same subject,
# append it to old coversation
subject = frappe.as_unicode(strip(re.sub(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*",
"", email.subject, 0, flags=re.IGNORECASE)))
if '#' in email.subject:
# try and match if ID is found
# document ID is appended to subject
# example "Re: Your email (#OPP-2020-2334343)"
parent_id = email.subject.rsplit('#', 1)[-1].strip(' ()')
if parent_id:
parent = frappe.db.get_all(self.append_to, filters = dict(name = parent_id),
fields = 'name')
parent = frappe.db.get_all(self.append_to, filters={
self.sender_field: email.from_email,
self.subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
}, fields="name")
if not parent:
# try and match by subject and sender
# if sent by same sender with same subject,
# append it to old coversation
subject = frappe.as_unicode(strip(re.sub(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*",
"", email.subject, 0, flags=re.IGNORECASE)))
parent = frappe.db.get_all(self.append_to, filters={
self.sender_field: email.from_email,
self.subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
}, fields = "name", limit = 1)
# match only subject field
# when the from_email is of a user in the system
# and subject is atleast 10 chars long
if not parent and len(subject) > 10 and is_system_user(email.from_email):
# match only subject field
# when the from_email is of a user in the system
# and subject is atleast 10 chars long
parent = frappe.db.get_all(self.append_to, filters={
self.subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
}, fields="name")
}, fields = "name", limit = 1)
if parent:
parent = frappe._dict(doctype=self.append_to, name=parent[0].name)

View file

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

View file

@ -1,640 +1,166 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"actions": [],
"autoname": "hash",
"beta": 0,
"creation": "2012-08-02 15:17:28",
"custom": 0,
"description": "Email Queue records.",
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"sender",
"recipients",
"show_as_cc",
"message",
"status",
"error",
"message_id",
"reference_doctype",
"reference_name",
"communication",
"send_after",
"priority",
"add_unsubscribe_link",
"unsubscribe_param",
"unsubscribe_method",
"expose_recipients",
"attachments",
"retry"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sender",
"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,
"ignore_xss_filter": 1,
"label": "Sender",
"length": 0,
"no_copy": 0,
"options": "Email",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Email"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "recipients",
"fieldtype": "Table",
"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": "Recipient",
"length": 0,
"no_copy": 0,
"options": "Email Queue Recipient",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Email Queue Recipient"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "show_as_cc",
"fieldtype": "Small Text",
"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": "Show as cc",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Show as cc"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Message"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Not Sent",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "\nNot Sent\nSending\nSent\nError\nExpired",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "\nNot Sent\nSending\nSent\nError\nExpired"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "error",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Error",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Error"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message_id",
"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": "Message ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference Document Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_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": "Reference DocName",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "communication",
"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": "Communication",
"length": 0,
"no_copy": 0,
"options": "Communication",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "send_after",
"fieldtype": "Datetime",
"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": "Send After",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "priority",
"fieldtype": "Int",
"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": "Priority",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "add_unsubscribe_link",
"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": "Add Unsubscribe Link",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Add Unsubscribe Link"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "unsubscribe_param",
"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": "Unsubscribe Param",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "unsubscribe_method",
"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": "Unsubscribe Method",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Unsubscribe Method"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "expose_recipients",
"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": "Expose Recipients",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Expose Recipients"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "attachments",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Attachments",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "retry",
"fieldtype": "Int",
"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": "Retry",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-envelope",
"idx": 1,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-09-05 14:22:27.664645",
"links": [],
"modified": "2020-07-17 15:58:15.369419",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Queue",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 0,
"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": 0,
"submit": 0,
"write": 0
"role": "System Manager"
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View file

@ -107,6 +107,9 @@ class Newsletter(WebsiteGenerator):
if self.get("__islocal"):
throw(_("Please save the Newsletter before sending"))
if not self.recipients:
frappe.throw(_("Newsletter should have at least one recipient"))
def get_context(self, context):
newsletters = get_newsletter_list("Newsletter", None, None, 0)
if newsletters:

View file

@ -11,6 +11,7 @@ import email.utils
from six import iteritems, text_type, string_types
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email import policy
def get_email(recipients, sender='', msg='', subject='[No Subject]',
@ -68,8 +69,8 @@ class EMail:
self.subject = subject
self.expose_recipients = expose_recipients
self.msg_root = MIMEMultipart('mixed')
self.msg_alternative = MIMEMultipart('alternative')
self.msg_root = MIMEMultipart('mixed', policy=policy.SMTPUTF8)
self.msg_alternative = MIMEMultipart('alternative', policy=policy.SMTPUTF8)
self.msg_root.attach(self.msg_alternative)
self.cc = cc or []
self.bcc = bcc or []
@ -100,7 +101,7 @@ class EMail:
Attach message in the text portion of multipart/alternative
"""
from email.mime.text import MIMEText
part = MIMEText(message, 'plain', 'utf-8')
part = MIMEText(message, 'plain', 'utf-8', policy=policy.SMTPUTF8)
self.msg_alternative.attach(part)
def set_part_html(self, message, inline_images):
@ -113,9 +114,9 @@ class EMail:
message, _inline_images = replace_filename_with_cid(message)
# prepare parts
msg_related = MIMEMultipart('related')
msg_related = MIMEMultipart('related', policy=policy.SMTPUTF8)
html_part = MIMEText(message, 'html', 'utf-8')
html_part = MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8)
msg_related.attach(html_part)
for image in _inline_images:
@ -124,7 +125,7 @@ class EMail:
self.msg_alternative.attach(msg_related)
else:
self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8'))
self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8))
def set_html_as_text(self, html):
"""Set plain text from HTML"""
@ -135,7 +136,7 @@ class EMail:
from email.mime.text import MIMEText
maintype, subtype = mime_type.split('/')
part = MIMEText(message, _subtype = subtype)
part = MIMEText(message, _subtype = subtype, policy=policy.SMTPUTF8)
if as_attachment:
part.add_header('Content-Disposition', 'attachment', filename=filename)
@ -222,7 +223,8 @@ class EMail:
# reset headers as values may be changed.
for key, val in iteritems(headers):
self.set_header(key, val)
if val:
self.set_header(key, val)
# call hook to enable apps to modify msg_root before sending
for hook in frappe.get_hooks("make_email_body_message"):
@ -238,7 +240,7 @@ class EMail:
"""validate, build message and convert to string"""
self.validate()
self.make()
return self.msg_root.as_string()
return self.msg_root.as_string(policy=policy.SMTPUTF8)
def get_formatted_html(subject, message, footer=None, print_html=None,
email_account=None, header=None, unsubscribe_link=None, sender=None):

View file

@ -347,7 +347,7 @@ def flush(from_test=False):
if not smtpserver:
smtpserver = SMTPServer()
smtpserver_dict[email.sender] = smtpserver
if from_test:
send_one(email.name, smtpserver, auto_commit)
else:
@ -390,12 +390,12 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False):
where
name=%s
for update''', email, as_dict=True)
if len(email):
email = email[0]
else:
return
recipients_list = frappe.db.sql('''select name, recipient, status from
`tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1)
@ -417,6 +417,8 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False):
if email.communication:
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
email_sent_to_any_recipient = None
try:
message = None

View file

@ -39,7 +39,7 @@ This is the text version of this email
subject='Test Subject',
content=email_html,
text_content=email_text
).as_string()
).as_string().replace("\r\n", "\n")
def test_prepare_message_returns_already_encoded_string(self):
@ -153,7 +153,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
subject='Test Subject',
content=email_html,
header=['Email Title', 'green']
).as_string()
).as_string().replace("\r\n", "\n")
self.assertTrue('''<span class=3D"indicator indicator-green" style=3D"background-color:#98=
d85b; border-radius:8px; display:inline-block; height:8px; margin-right:5px=

View file

@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/frappe"
app_license = "MIT"
app_logo_url = '/assets/frappe/images/frappe-framework-logo.png'
develop_version = '12.x.x-develop'
develop_version = '13.x.x-develop'
app_email = "info@frappe.io"

View file

@ -9,6 +9,7 @@ from __future__ import unicode_literals, print_function
from six.moves import input
import os, json, subprocess, shutil
import click
import frappe
import frappe.database
import importlib
@ -118,12 +119,20 @@ def remove_from_installed_apps(app_name):
if frappe.flags.in_install:
post_install()
def remove_app(app_name, dry_run=False, yes=False, no_backup=False):
def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False):
"""Remove app and all linked to the app's module with the app from a site."""
# dont allow uninstall app if not installed unless forced
if not force:
if app_name not in frappe.get_installed_apps():
click.secho("App {0} not installed on Site {1}".format(app_name, frappe.local.site), fg="yellow")
return
print("Uninstalling App {0} from Site {1}...".format(app_name, frappe.local.site))
if not dry_run and not yes:
confirm = input("All doctypes (including custom), modules related to this app will be deleted. Are you sure you want to continue (y/n) ? ")
if confirm!="y":
confirm = click.confirm("All doctypes (including custom), modules related to this app will be deleted. Are you sure you want to continue?")
if not confirm:
return
if not no_backup:
@ -146,8 +155,12 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False):
if not doctype.issingle:
drop_doctypes.append(doctype.name)
# remove reports, pages and web forms
for doctype in ("Report", "Page", "Web Form"):
linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent'])
ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"]
doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes]
for doctype in doctypes_with_linked_modules:
for record in frappe.get_list(doctype, filters={"module": module_name}):
print("removing {0} {1}...".format(doctype, record.name))
if not dry_run:
@ -166,6 +179,8 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False):
for doctype in set(drop_doctypes):
frappe.db.sql("drop table `tab{0}`".format(doctype))
click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green")
frappe.flags.in_uninstall = False
def post_install(rebuild_website=False):

View file

@ -12,6 +12,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import get_request_site_address
from googleapiclient.errors import HttpError
from frappe.utils.password import set_encrypted_password
from frappe.utils import add_days, get_datetime, get_weekdays, now_datetime, add_to_date, get_time_zone
from dateutil import parser
from datetime import datetime, timedelta
@ -198,7 +199,7 @@ def check_google_calendar(account, google_calendar):
except HttpError as err:
frappe.throw(_("Google Calendar - Could not create Calendar for {0}, error code {1}.").format(account.name, err.resp.status))
def sync_events_from_google_calendar(g_calendar, method=None, page_length=10):
def sync_events_from_google_calendar(g_calendar, method=None):
"""
Syncs Events from Google Calendar in Framework Calendar.
Google Calendar returns nextSyncToken when all the events in Google Calendar are fetched.
@ -210,23 +211,32 @@ def sync_events_from_google_calendar(g_calendar, method=None, page_length=10):
if not account.pull_from_google_calendar:
return
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
events = frappe._dict()
results = []
while True:
try:
# API Response listed at EOF
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
events = google_calendar.events().list(calendarId=account.google_calendar_id, maxResults=page_length,
singleEvents=False, showDeleted=True, syncToken=sync_token).execute()
events = google_calendar.events().list(calendarId=account.google_calendar_id, maxResults=2000,
pageToken=events.get("nextPageToken"), singleEvents=False, showDeleted=True, syncToken=sync_token).execute()
except HttpError as err:
frappe.throw(_("Google Calendar - Could not fetch event from Google Calendar, error code {0}.").format(err.resp.status))
msg = _("Google Calendar - Could not fetch event from Google Calendar, error code {0}.").format(err.resp.status)
if err.resp.status == 410:
set_encrypted_password("Google Calendar", account.name, "", "next_sync_token")
frappe.db.commit()
msg += ' ' + _('Sync token was invalid and has been resetted, Retry syncing.')
frappe.msgprint(msg, title='Invalid Sync Token', indicator='blue')
else:
frappe.throw(msg)
for event in events.get("items", []):
results.append(event)
if not events.get("nextPageToken"):
if events.get("nextSyncToken"):
frappe.db.set_value("Google Calendar", account.name, "next_sync_token", events.get("nextSyncToken"))
frappe.db.commit()
account.next_sync_token = events.get("nextSyncToken")
account.save()
break
for idx, event in enumerate(results):

View file

@ -189,14 +189,17 @@ def upload_system_backup_to_google_drive():
if frappe.flags.create_new_backup:
set_progress(1, "Backing up Data.")
backup = new_backup()
fileurl_backup = os.path.basename(backup.backup_path_db)
fileurl_site_config = os.path.basename(backup.site_config_backup_path)
fileurl_public_files = os.path.basename(backup.backup_path_files)
fileurl_private_files = os.path.basename(backup.backup_path_private_files)
else:
fileurl_backup, fileurl_site_config, fileurl_public_files, fileurl_private_files = get_latest_backup_file(with_files=True)
file_urls = []
file_urls.append(backup.backup_path_db)
file_urls.append(backup.site_config_backup_path)
for fileurl in [fileurl_backup, fileurl_site_config, fileurl_public_files, fileurl_private_files]:
if account.file_backup:
file_urls.append(backup.backup_path_files)
file_urls.append(backup.backup_path_private_files)
else:
file_urls = get_latest_backup_file(with_files=account.file_backup)
for fileurl in file_urls:
if not fileurl:
continue
@ -208,7 +211,7 @@ def upload_system_backup_to_google_drive():
try:
media = MediaFileUpload(get_absolute_path(filename=fileurl), mimetype="application/gzip", resumable=True)
except IOError as e:
frappe.throw(_("Google Drive - Could not locate locate - {0}").format(e))
frappe.throw(_("Google Drive - Could not locate - {0}").format(e))
try:
set_progress(2, "Uploading backup to Google Drive.")
@ -232,7 +235,7 @@ def weekly_backup():
upload_system_backup_to_google_drive()
def get_absolute_path(filename):
file_path = os.path.join(get_backups_path()[2:], filename)
file_path = os.path.join(get_backups_path()[2:], os.path.basename(filename))
return "{0}/sites/{1}".format(get_bench_path(), file_path)
def set_progress(progress, message):

View file

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

View file

@ -0,0 +1,89 @@
{
"actions": [],
"creation": "2020-04-02 00:11:22.846697",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"merchant_id",
"merchant_key",
"staging",
"column_break_4",
"industry_type_id",
"website"
],
"fields": [
{
"fieldname": "merchant_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Merchant ID",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "merchant_key",
"fieldtype": "Password",
"in_list_view": 1,
"label": "Merchant Key",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "staging",
"fieldtype": "Check",
"label": "Staging",
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval: !doc.staging",
"fieldname": "website",
"fieldtype": "Data",
"label": "Website",
"mandatory_depends_on": "eval: !doc.staging",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval: !doc.staging",
"fieldname": "industry_type_id",
"fieldtype": "Data",
"label": "Industry Type ID",
"mandatory_depends_on": "eval: !doc.staging",
"show_days": 1,
"show_seconds": 1
}
],
"issingle": 1,
"links": [],
"modified": "2020-06-08 13:36:09.703143",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Paytm Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import json
import requests
from six.moves.urllib.parse import urlencode
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import get_url, call_hook_method, cint, flt, cstr
from frappe.integrations.utils import create_request_log, create_payment_gateway
from frappe.utils import get_request_site_address
from paytmchecksum import generateSignature, verifySignature
from frappe.utils.password import get_decrypted_password
class PaytmSettings(Document):
supported_currencies = ["INR"]
def validate(self):
create_payment_gateway('Paytm')
call_hook_method('payment_gateway_enabled', gateway='Paytm')
def validate_transaction_currency(self, currency):
if currency not in self.supported_currencies:
frappe.throw(_("Please select another payment method. Paytm does not support transactions in currency '{0}'").format(currency))
def get_payment_url(self, **kwargs):
'''Return payment url with several params'''
# create unique order id by making it equal to the integration request
integration_request = create_request_log(kwargs, "Host", "Paytm")
kwargs.update(dict(order_id=integration_request.name))
return get_url("./integrations/paytm_checkout?{0}".format(urlencode(kwargs)))
def get_paytm_config():
''' Returns paytm config '''
paytm_config = frappe.db.get_singles_dict('Paytm Settings')
paytm_config.update(dict(merchant_key=get_decrypted_password('Paytm Settings', 'Paytm Settings', 'merchant_key')))
if cint(paytm_config.staging):
paytm_config.update(dict(
website="WEBSTAGING",
url='https://securegw-stage.paytm.in/order/process',
transaction_status_url='https://securegw-stage.paytm.in/order/status',
industry_type_id='RETAIL'
))
else:
paytm_config.update(dict(
url='https://securegw.paytm.in/order/process',
transaction_status_url='https://securegw.paytm.in/order/status',
))
return paytm_config
def get_paytm_params(payment_details, order_id, paytm_config):
# initialize a dictionary
paytm_params = dict()
redirect_uri = get_request_site_address(True) + "/api/method/frappe.integrations.doctype.paytm_settings.paytm_settings.verify_transaction"
paytm_params.update({
"MID" : paytm_config.merchant_id,
"WEBSITE" : paytm_config.website,
"INDUSTRY_TYPE_ID" : paytm_config.industry_type_id,
"CHANNEL_ID" : "WEB",
"ORDER_ID" : order_id,
"CUST_ID" : payment_details['payer_email'],
"EMAIL" : payment_details['payer_email'],
"TXN_AMOUNT" : cstr(flt(payment_details['amount'], 2)),
"CALLBACK_URL" : redirect_uri,
})
checksum = generateSignature(paytm_params, paytm_config.merchant_key)
paytm_params.update({
"CHECKSUMHASH" : checksum
})
return paytm_params
@frappe.whitelist(allow_guest=True)
def verify_transaction(**paytm_params):
'''Verify checksum for received data in the callback and then verify the transaction'''
paytm_config = get_paytm_config()
is_valid_checksum = False
paytm_params.pop('cmd', None)
paytm_checksum = paytm_params.pop('CHECKSUMHASH', None)
if paytm_params and paytm_config and paytm_checksum:
# Verify checksum
is_valid_checksum = verifySignature(paytm_params, paytm_config.merchant_key, paytm_checksum)
if is_valid_checksum and paytm_params.get('RESPCODE') == '01':
verify_transaction_status(paytm_config, paytm_params['ORDERID'])
else:
frappe.respond_as_web_page("Payment Failed",
"Transaction failed to complete. In case of any deductions, deducted amount will get refunded to your account.",
http_status_code=401, indicator_color='red')
frappe.log_error("Order unsuccessful. Failed Response:"+cstr(paytm_params), 'Paytm Payment Failed')
def verify_transaction_status(paytm_config, order_id):
'''Verify transaction completion after checksum has been verified'''
paytm_params=dict(
MID=paytm_config.merchant_id,
ORDERID= order_id
)
checksum = generateSignature(paytm_params, paytm_config.merchant_key)
paytm_params["CHECKSUMHASH"] = checksum
post_data = json.dumps(paytm_params)
url = paytm_config.transaction_status_url
response = requests.post(url, data = post_data, headers = {"Content-type": "application/json"}).json()
finalize_request(order_id, response)
def finalize_request(order_id, transaction_response):
request = frappe.get_doc('Integration Request', order_id)
transaction_data = frappe._dict(json.loads(request.data))
redirect_to = transaction_data.get('redirect_to') or None
redirect_message = transaction_data.get('redirect_message') or None
if transaction_response['STATUS'] == "TXN_SUCCESS":
if transaction_data.reference_doctype and transaction_data.reference_docname:
custom_redirect_to = None
try:
custom_redirect_to = frappe.get_doc(transaction_data.reference_doctype,
transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed')
request.db_set('status', 'Completed')
except Exception:
request.db_set('status', 'Failed')
frappe.log_error(frappe.get_traceback())
if custom_redirect_to:
redirect_to = custom_redirect_to
redirect_url = '/integrations/payment-success'
else:
request.db_set('status', 'Failed')
redirect_url = '/integrations/payment-failed'
if redirect_to:
redirect_url += '?' + urlencode({'redirect_to': redirect_to})
if redirect_message:
redirect_url += '&' + urlencode({'redirect_message': redirect_message})
frappe.local.response['type'] = 'redirect'
frappe.local.response['location'] = redirect_url
def get_gateway_controller(doctype, docname):
reference_doc = frappe.get_doc(doctype, docname)
gateway_controller = frappe.db.get_value("Payment Gateway", reference_doc.payment_gateway, "gateway_controller")
return gateway_controller

View file

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

View file

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

View file

@ -19,6 +19,9 @@ from botocore.exceptions import ClientError
class S3BackupSettings(Document):
def validate(self):
if not self.enabled:
return
if not self.endpoint_url:
self.endpoint_url = 'https://s3.amazonaws.com'
conn = boto3.client(

View file

@ -7,7 +7,7 @@ from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrato
def migrate_to(local_site, frappe_provider):
if frappe_provider in ("frappe.cloud", "frappecloud.com"):
return frappecloud_migrator(local_site, frappe_provider)
return frappecloud_migrator(local_site)
else:
print("{} is not supported yet".format(frappe_provider))
sys.exit(1)

View file

@ -1,412 +1,29 @@
# imports - standard imports
import getpass
import json
import os
import re
import sys
# imports - third party imports
import click
from html2text import html2text
import requests
from tenacity import retry, stop_after_attempt, wait_fixed
from html2text import html2text
# imports - module imports
import frappe
import frappe.utils.backups
from frappe.utils import get_installed_apps_info
from frappe.utils.commands import render_table, add_line_after, add_line_before
# TODO: check upgrade compatibility
def render_actions_table():
actions_table = [["#", "Action"]]
actions = []
for n, action in enumerate(migrator_actions):
actions_table.append([n+1, action["title"]])
actions.append(action["fn"])
render_table(actions_table)
return actions
def render_site_table(sites_info):
sites_table = [["#", "Site Name", "Status"]]
available_sites = []
for n, site_data in enumerate(sites_info):
name, status = site_data["name"], site_data["status"]
if status in ("Active", "Broken"):
sites_table.append([n + 1, name, status])
available_sites.append(name)
render_table(sites_table)
return available_sites
def render_teams_table(teams):
teams_table = [["#", "Team"]]
for n, team in enumerate(teams):
teams_table.append([n+1, team])
render_table(teams_table)
def render_plan_table(plans_list):
plans_table = [["Plan", "CPU Time"]]
visible_headers = ["name", "cpu_time_per_day"]
for plan in plans_list:
plan, cpu_time = [plan[header] for header in visible_headers]
plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
render_table(plans_table)
def render_group_table(app_groups):
# title row
app_groups_table = [["#", "App Group", "Apps"]]
# all rows
for idx, app_group in enumerate(app_groups):
apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
row = [idx + 1, app_group["name"], apps_list]
app_groups_table.append(row)
render_table(app_groups_table)
def handle_request_failure(request=None, message=None, traceback=True, exit_code=1):
message = message or "Request failed with error code {}".format(request.status_code)
response = html2text(request.text) if traceback else ""
print("{0}{1}".format(message, "\n" + response))
sys.exit(exit_code)
@add_line_after
def select_primary_action():
actions = render_actions_table()
idx = click.prompt("What do you want to do?", type=click.IntRange(1, len(actions))) - 1
return actions[idx]
@add_line_after
def select_site():
get_all_sites_request = session.post(all_site_url, headers={
"accept": "application/json",
"accept-encoding": "gzip, deflate, br",
"content-type": "application/json; charset=utf-8"
})
if get_all_sites_request.ok:
all_sites = get_all_sites_request.json()["message"]
available_sites = render_site_table(all_sites)
while True:
selected_site = click.prompt("Name of the site you want to restore to", type=str).strip()
if selected_site in available_sites:
return selected_site
else:
print("Site {} does not exist. Try again ❌".format(selected_site))
else:
print("Couldn't retrive sites list...Try again later")
sys.exit(1)
@add_line_before
def select_team(session):
# get team options
account_details_sc = session.post(account_details_url)
if account_details_sc.ok:
account_details = account_details_sc.json()["message"]
available_teams = account_details["teams"]
# ask if they want to select, go ahead with if only one exists
if len(available_teams) == 1:
team = available_teams[0]
else:
render_teams_table(available_teams)
idx = click.prompt("Select Team", type=click.IntRange(1, len(available_teams))) - 1
team = available_teams[idx]
print("Team '{}' set for current session".format(team))
return team
@retry(stop=stop_after_attempt(5))
def get_new_site_options():
site_options_sc = session.post(options_url)
if site_options_sc.ok:
site_options = site_options_sc.json()["message"]
return site_options
else:
print("Couldn't retrive New site information: {}".format(site_options_sc.status_code))
def is_valid_subdomain(subdomain):
if len(subdomain) < 5:
print("Subdomain too short. Use 5 or more characters")
return False
matched = re.match("^[a-z0-9][a-z0-9-]*[a-z0-9]$", subdomain)
if matched:
return True
print("Subdomain contains invalid characters. Use lowercase characters, numbers and hyphens")
@retry(stop=stop_after_attempt(5))
def is_subdomain_available(subdomain):
res = session.post(site_exists_url, {"subdomain": subdomain})
if res.ok:
available = not res.json()["message"]
if not available:
print("Subdomain already exists! Try another one")
return available
@add_line_after
def choose_plan(plans_list):
print("{} plans available".format(len(plans_list)))
available_plans = [plan["name"] for plan in plans_list]
render_plan_table(plans_list)
while True:
input_plan = click.prompt("Select Plan").strip()
if input_plan in available_plans:
print("{} Plan selected ✅".format(input_plan))
return input_plan
else:
print("Invalid Selection ❌")
@add_line_after
def check_app_compat(available_group):
is_compat = True
incompatible_apps, filtered_apps, branch_msgs = [], [], []
existing_group = [(app["app_name"], app["branch"]) for app in get_installed_apps_info()]
print("Checking availability of existing app group")
for (app, branch) in existing_group:
info = [ (a["name"], a["branch"]) for a in available_group["apps"] if a["scrubbed"] == app ]
if info:
app_title, available_branch = info[0]
if branch != available_branch:
print("⚠️ App {}:{} => {}".format(app, branch, available_branch))
branch_msgs.append([app, branch, available_branch])
filtered_apps.append(app_title)
is_compat = False
else:
print("✅ App {}:{}".format(app, branch))
filtered_apps.append(app_title)
else:
incompatible_apps.append(app)
print("❌ App {}:{}".format(app, branch))
is_compat = False
start_msg = "\nSelecting this group will "
incompatible_apps = ("\n\nDrop the following apps:\n" + "\n".join(incompatible_apps)) if incompatible_apps else ""
branch_change = ("\n\nUpgrade the following apps:\n" + "\n".join(["{}: {} => {}".format(*x) for x in branch_msgs])) if branch_msgs else ""
changes = (incompatible_apps + branch_change) or "be perfect for you :)"
warning_message = start_msg + changes
print(warning_message)
return is_compat, filtered_apps
@add_line_after
def filter_apps(app_groups):
render_group_table(app_groups)
while True:
app_group_index = click.prompt("Select App Group Number", type=int) - 1
try:
if app_group_index == -1:
raise IndexError
selected_group = app_groups[app_group_index]
except IndexError:
print("Invalid Selection ❌")
continue
is_compat, filtered_apps = check_app_compat(selected_group)
if is_compat or click.confirm("Continue anyway?"):
print("App Group {} selected! ✅".format(selected_group["name"]))
break
return selected_group["name"], filtered_apps
@add_line_after
def get_subdomain(domain):
while True:
subdomain = click.prompt("Enter subdomain").strip()
if is_valid_subdomain(subdomain) and is_subdomain_available(subdomain):
print("Site Domain: {}.{}".format(subdomain, domain))
return subdomain
@retry(stop=stop_after_attempt(2), wait=wait_fixed(5))
def upload_backup_file(file_type, file_path):
return session.post(files_url, data={}, files={
"file": open(file_path, "rb"),
"is_private": 1,
"folder": "Home",
"method": "press.api.site.upload_backup",
"type": file_type
})
@add_line_after
def upload_backup(local_site):
# take backup
files_session = {}
print("Taking backup for site {}".format(local_site))
odb = frappe.utils.backups.new_backup(ignore_files=False, force=True)
# upload files
for x, (file_type, file_path) in enumerate([
("database", odb.backup_path_db),
("public", odb.backup_path_files),
("private", odb.backup_path_private_files)
]):
file_name = file_path.split(os.sep)[-1]
print("Uploading {} file: {} ({}/3)".format(file_type, file_name, x+1))
file_upload_response = upload_backup_file(file_type, file_path)
if file_upload_response.ok:
files_session[file_type] = file_upload_response.json()["message"]
else:
print("Upload failed for: {}".format(file_path))
files_uploaded = { k: v["file_url"] for k, v in files_session.items() }
print("Uploaded backup files! ✅")
return files_uploaded
def new_site(local_site):
# get new site options
site_options = get_new_site_options()
# set preferences from site options
subdomain = get_subdomain(site_options["domain"])
plan = choose_plan(site_options["plans"])
app_groups = site_options["groups"]
selected_group, filtered_apps = filter_apps(app_groups)
files_uploaded = upload_backup(local_site)
# push to frappe_cloud
payload = json.dumps({
"site": {
"apps": filtered_apps,
"files": files_uploaded,
"group": selected_group,
"name": subdomain,
"plan": plan
}
})
session.headers.update({"Content-Type": "application/json; charset=utf-8"})
site_creation_request = session.post(upload_url, payload)
if site_creation_request.ok:
site_url = site_creation_request.json()["message"]
print("Your site {} is being migrated ✨".format(local_site))
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
print("Your site URL: {}".format(site_url))
else:
handle_request_failure(site_creation_request)
def restore_site(local_site):
# get list of existing sites they can restore
selected_site = select_site()
# TODO: check if they can restore it
click.confirm("This is an irreversible action. Are you sure you want to continue?", abort=True)
# backup site
files_uploaded = upload_backup(local_site)
# push to frappe_cloud
payload = json.dumps({
"name": selected_site,
"files": files_uploaded
})
headers = {"Content-Type": "application/json; charset=utf-8"}
site_restore_request = session.post(restore_site_url, payload, headers=headers)
if site_restore_request.ok:
print("Your site {0} is being restored on {1}".format(local_site, selected_site))
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, selected_site))
print("Your site URL: {}".format(selected_site))
else:
handle_request_failure(site_restore_request)
@add_line_after
def create_session():
print("Frappe Cloud credentials @ {}".format(remote_site))
# take user input from STDIN
username = click.prompt("Username").strip()
password = getpass.unix_getpass()
auth_credentials = {"usr": username, "pwd": password}
session = requests.Session()
login_sc = session.post(login_url, auth_credentials)
if login_sc.ok:
print("Authorization Successful! ✅")
team = select_team(session)
session.headers.update({
"X-Press-Team": team,
"Connection": "keep-alive"
})
return session
else:
handle_request_failure(message="Authorization Failed with Error Code {}".format(login_sc.status_code), traceback=False)
def frappecloud_migrator(local_site, frappecloud_site):
global login_url, upload_url, files_url, options_url, site_exists_url, restore_site_url, account_details_url, all_site_url
global session, migrator_actions, remote_site
def frappecloud_migrator(local_site):
print("Retreiving Site Migrator...")
remote_site = frappe.conf.frappecloud_url or "frappecloud.com"
request_url = "https://{}/api/method/press.api.script".format(remote_site)
request = requests.get(request_url)
login_url = "https://{}/api/method/login".format(remote_site)
upload_url = "https://{}/api/method/press.api.site.new".format(remote_site)
files_url = "https://{}/api/method/upload_file".format(remote_site)
options_url = "https://{}/api/method/press.api.site.options_for_new".format(remote_site)
site_exists_url = "https://{}/api/method/press.api.site.exists".format(remote_site)
account_details_url = "https://{}/api/method/press.api.account.get".format(remote_site)
all_site_url = "https://{}/api/method/press.api.site.all".format(remote_site)
restore_site_url = "https://{}/api/method/press.api.site.restore".format(remote_site)
if request.status_code / 100 != 2:
print("Request exitted with Status Code: {}\nPayload: {}".format(request.status_code, html2text(request.text)))
click.secho("Some errors occurred while recovering the migration script. Please contact us @ Frappe Cloud if this issue persists", fg="yellow")
return
migrator_actions = [
{ "title": "Create a new site", "fn": new_site },
{ "title": "Restore to an existing site", "fn": restore_site }
]
script_contents = request.json()["message"]
# get credentials + auth user + start session
session = create_session()
import tempfile
import os
import sys
# available actions defined in migrator_actions
primary_action = select_primary_action()
primary_action(local_site)
py = sys.executable
script = tempfile.NamedTemporaryFile(mode="w")
script.write(script_contents)
print("Site Migrator stored at {}".format(script.name))
os.execv(py, [py, script.name, local_site])

View file

@ -334,7 +334,7 @@ class BaseDocument(object):
self.db_insert()
return
frappe.msgprint(_("Duplicate name {0} {1}").format(self.doctype, self.name))
frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red")
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
elif frappe.db.is_unique_key_violation(e):
@ -702,16 +702,13 @@ class BaseDocument(object):
df = self.meta.get_field(fieldname)
sanitized_value = value
if df and df.get("fieldtype") in ("Data", "Code", "Small Text", "Text") and df.get("options")=="Email":
sanitized_value = sanitize_email(value)
if df and (df.get("ignore_xss_filter")
or (df.get("fieldtype") in ("Data", "Small Text", "Text") and df.get("options")=="Email")
or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode", "Code")
elif df and (df.get("ignore_xss_filter")
or (df.get("fieldtype")=="Code" and df.get("options")!="Email")
or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode")
# cancelled and submit but not update after submit should be ignored
or self.docstatus==2
or (self.docstatus==1 and not df.get("allow_on_submit"))):
# cancelled and submit but not update after submit should be ignored
or self.docstatus==2
or (self.docstatus==1 and not df.get("allow_on_submit"))):
continue
else:

View file

@ -45,7 +45,9 @@ def make_new_doc(doctype):
doc = doc.get_valid_dict(sanitize=False)
doc["doctype"] = doctype
doc["__islocal"] = 1
doc["__unsaved"] = 1
if not frappe.model.meta.is_single(doctype):
doc["__unsaved"] = 1
return doc

View file

@ -203,7 +203,7 @@ class DatabaseQuery(object):
def sanitize_fields(self):
'''
regex : ^.*[,();].*
purpose : The regex will look for malicious patterns like `,`, '(', ')', ';' in each
purpose : The regex will look for malicious patterns like `,`, '(', ')', '@', ;' in each
field which may leads to sql injection.
example :
field = "`DocType`.`issingle`, version()"
@ -211,11 +211,11 @@ class DatabaseQuery(object):
the system will filter out this field.
'''
sub_query_regex = re.compile("^.*[,();].*")
blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case']
sub_query_regex = re.compile("^.*[,();@].*")
blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case', 'show']
blacklisted_functions = ['concat', 'concat_ws', 'if', 'ifnull', 'nullif', 'coalesce',
'connection_id', 'current_user', 'database', 'last_insert_id', 'session_user',
'system_user', 'user', 'version']
'system_user', 'user', 'version', 'global']
def _raise_exception():
frappe.throw(_('Use of sub-query or function is restricted'), frappe.DataError)
@ -238,6 +238,10 @@ class DatabaseQuery(object):
if any("{0}(".format(keyword) in field.lower() for keyword in blacklisted_functions):
_raise_exception()
if '@' in field.lower():
# prevent access to global variables
_raise_exception()
if re.compile(r"[0-9a-zA-Z]+\s*'").match(field):
_raise_exception()
@ -854,4 +858,4 @@ def get_date_range(operator, value):
timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value
return get_timespan_date_range(timespan)
return get_timespan_date_range(timespan)

View file

@ -403,9 +403,16 @@ class Document(BaseDocument):
def set_new_name(self, force=False, set_name=None, set_child_names=True):
"""Calls `frappe.naming.set_new_name` for parent and child docs."""
if self.flags.name_set and not force:
return
# If autoname has set as Prompt (name)
if self.get("__newname"):
self.name = self.get("__newname")
self.flags.name_set = True
return
if set_name:
self.name = set_name
else:
@ -830,7 +837,7 @@ class Document(BaseDocument):
def run_notifications(self, method):
"""Run notifications for this method"""
if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install:
if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install:
return
if self.flags.notifications_executed==None:
@ -1300,6 +1307,16 @@ class Document(BaseDocument):
users = set([assignment.owner for assignment in assignments])
return users
def add_tag(self, tag):
"""Add a Tag to this document"""
from frappe.desk.doctype.tag.tag import DocTags
DocTags(self.doctype).add(self.name, tag)
def get_tags(self):
"""Return a list of Tags attached to this document"""
from frappe.desk.doctype.tag.tag import DocTags
return DocTags(self.doctype).get_tags(self.name).split(",")[1:]
def execute_action(doctype, name, action, **kwargs):
"""Execute an action on a document (called by background worker)"""
doc = frappe.get_doc(doctype, name)

View file

@ -307,4 +307,4 @@ def set_workflow_state_on_action(doc, workflow_name, action):
for state in workflow.states:
if state.doc_status == docstatus:
doc.set(workflow_state_field, state.state)
return
return

View file

@ -12,16 +12,17 @@ def export_doc(doc):
def export_to_files(record_list=None, record_module=None, verbose=0, create_init=None):
"""
Export record_list to files. record_list is a list of lists ([doctype],[docname] ) ,
Export record_list to files. record_list is a list of lists ([doctype, docname, folder name],) ,
"""
if frappe.flags.in_import:
return
if record_list:
for record in record_list:
write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init)
folder_name = record[2] if len(record) == 3 else None
write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init, folder_name=folder_name)
def write_document_file(doc, record_module=None, create_init=True):
def write_document_file(doc, record_module=None, create_init=True, folder_name=None):
newdoc = doc.as_dict(no_nulls=True)
doc.run_method("before_export", newdoc)
@ -35,7 +36,10 @@ def write_document_file(doc, record_module=None, create_init=True):
module = record_module or get_module_name(doc)
# create folder
folder = create_folder(module, doc.doctype, doc.name, create_init)
if folder_name:
folder = create_folder(module, folder_name, doc.name, create_init)
else:
folder = create_folder(module, doc.doctype, doc.name, create_init)
# write the data file
fname = scrub(doc.name)

View file

@ -4,6 +4,7 @@ import pytz
from frappe import _
from frappe.auth import LoginManager
from http import cookies
from oauthlib.oauth2.rfc6749.tokens import BearerToken
from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant
from oauthlib.oauth2 import RequestValidator
@ -130,15 +131,12 @@ class OAuthWebRequestValidator(RequestValidator):
oac.scopes = get_url_delimiter().join(request.scopes)
oac.redirect_uri_bound_to_authorization_code = request.redirect_uri
oac.client = client_id
oac.user = unquote(cookie_dict['user_id'])
oac.user = unquote(cookie_dict['user_id'].value)
oac.authorization_code = code['code']
oac.save(ignore_permissions=True)
frappe.db.commit()
def authenticate_client(self, request, *args, **kwargs):
cookie_dict = get_cookie_dict_from_headers(request)
#Get ClientID in URL
if request.client_id:
oc = frappe.get_doc("OAuth Client", request.client_id)
@ -155,7 +153,9 @@ class OAuthWebRequestValidator(RequestValidator):
except Exception as e:
print("Failed body authentication: Application %s does not exist".format(cid=request.client_id))
return frappe.session.user == unquote(cookie_dict.get('user_id', "Guest"))
cookie_dict = get_cookie_dict_from_headers(request)
user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest"
return frappe.session.user == user_id
def authenticate_client_id(self, client_id, request, *args, **kwargs):
cli_id = frappe.db.get_value('OAuth Client', client_id, 'name')
@ -400,13 +400,10 @@ class OAuthWebRequestValidator(RequestValidator):
return True
def get_cookie_dict_from_headers(r):
cookie = cookies.BaseCookie()
if r.headers.get('Cookie'):
cookie = r.headers.get('Cookie')
cookie = cookie.split("; ")
cookie_dict = {k:v for k,v in (x.split('=') for x in cookie)}
return cookie_dict
else:
return {}
cookie.load(r.headers.get('Cookie'))
return cookie
def calculate_at_hash(access_token, hash_alg):
"""Helper method for calculating an access token

View file

@ -19,6 +19,7 @@ execute:frappe.reload_doc('core', 'doctype', 'module_def') #2017-09-22
execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01
execute:frappe.reload_doc('email', 'doctype', 'document_follow')
execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02
execute:frappe.reload_doc('core', 'doctype', 'has_role')
execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02
frappe.patches.v11_0.replicate_old_user_permissions
frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03
@ -263,6 +264,7 @@ frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26
frappe.patches.v12_0.setup_email_linking
frappe.patches.v12_0.fix_home_settings_for_all_users
frappe.patches.v12_0.change_existing_dashboard_chart_filters
frappe.patches.v12_0.set_correct_assign_value_in_docs #2020-07-13
execute:frappe.delete_doc("Test Runner")
execute:frappe.delete_doc_if_exists('DocType', 'Google Maps Settings')
execute:frappe.db.set_default('desktop:home_page', 'workspace')
@ -271,7 +273,9 @@ execute:frappe.delete_doc_if_exists('DocType', 'GSuite Templates')
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
frappe.patches.v12_0.remove_example_email_thread_notify
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
frappe.patches.v12_0.set_correct_url_in_files
frappe.patches.v13_0.website_theme_custom_scss
frappe.patches.v13_0.set_existing_dashboard_charts_as_public
frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
@ -289,4 +293,7 @@ execute:frappe.delete_doc("DocType", "Onboarding Slide Field")
execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link")
frappe.patches.v13_0.update_date_filters_in_user_settings
frappe.patches.v13_0.update_duration_options
frappe.patches.v13_0.replace_old_data_import
frappe.patches.v13_0.replace_old_data_import # 2020-06-24
frappe.patches.v13_0.create_custom_dashboards_cards_and_charts
frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart
frappe.patches.v13_0.generate_theme_files_in_public_folder

View file

@ -2,7 +2,7 @@ from __future__ import unicode_literals
import frappe
def execute():
if frappe.db.exists('DocType', 'View log'):
if frappe.db.table_exists('View log'):
# for mac users direct renaming would not work since mysql for mac saves table name in lower case
# so while renaming `tabView log` to `tabView Log` we get "Table 'tabView Log' already exists" error
# more info https://stackoverflow.com/a/44753093/5955589 ,

View file

@ -0,0 +1,8 @@
import frappe
def execute():
# remove all example.com email user accounts from notifications
frappe.db.sql("""UPDATE `tabUser`
SET thread_notify=0, send_me_a_copy=0
WHERE email like '%@example.com'""")

View file

@ -0,0 +1,32 @@
import frappe
def execute():
frappe.reload_doc('desk', 'doctype', 'todo')
query = '''
SELECT
name, reference_type, reference_name, {} as assignees
FROM
`tabToDo`
WHERE
COALESCE(reference_type, '') != '' AND
COALESCE(reference_name, '') != '' AND
status != 'Cancelled'
GROUP BY
reference_type, reference_name
'''
assignments = frappe.db.multisql({
'mariadb': query.format('GROUP_CONCAT(DISTINCT `owner`)'),
'postgres': query.format('STRING_AGG(DISTINCT "owner", ",")')
}, as_dict=True)
for doc in assignments:
assignments = doc.assignees.split(',')
frappe.db.set_value(
doc.reference_type,
doc.reference_name,
'_assign',
frappe.as_json(assignments),
update_modified=False
)

View file

@ -0,0 +1,39 @@
import frappe
import os
def execute():
files = frappe.get_all('File',
fields = ['name', 'file_name', 'file_url'],
filters = {
'is_folder': 0,
'file_url': ['!=', ''],
})
private_file_path = frappe.get_site_path('private', 'files')
public_file_path = frappe.get_site_path('public', 'files')
for file in files:
file_path = file.file_url
file_name = file_path.split('/')[-1]
if not file_path.startswith(('/private/', '/files/')):
continue
file_is_private = file_path.startswith('/private/files/')
full_path = frappe.utils.get_files_path(file_name, is_private=file_is_private)
if not os.path.exists(full_path):
if file_is_private:
public_file_url = os.path.join(public_file_path, file_name)
if os.path.exists(public_file_url):
frappe.db.set_value('File', file.name, {
'file_url': '/files/{0}'.format(file_name),
'is_private': 0
})
else:
private_file_url = os.path.join(private_file_path, file_name)
if os.path.exists(private_file_url):
frappe.db.set_value('File', file.name, {
'file_url': '/private/files/{0}'.format(file_name),
'is_private': 1
})

View file

@ -0,0 +1,45 @@
import frappe
from frappe.model.naming import append_number_if_name_exists
from frappe.utils.dashboard import get_dashboards_with_link
def execute():
if not frappe.db.table_exists('Dashboard Chart')\
or not frappe.db.table_exists('Number Card')\
or not frappe.db.table_exists('Dashboard'):
return
frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
frappe.reload_doc('desk', 'doctype', 'number_card')
frappe.reload_doc('desk', 'doctype', 'dashboard')
modified_charts = get_modified_docs('Dashboard Chart')
modified_cards = get_modified_docs('Number Card')
modified_dashboards = [doc.name for doc in get_modified_docs('Dashboard')]
for chart in modified_charts:
modified_dashboards += get_dashboards_with_link(chart.name, 'Dashboard Chart')
rename_modified_doc(chart.name, 'Dashboard Chart')
for card in modified_cards:
modified_dashboards += get_dashboards_with_link(card.name, 'Number Card')
rename_modified_doc(card.name, 'Number Card')
modified_dashboards = list(set(modified_dashboards))
for dashboard in modified_dashboards:
rename_modified_doc(dashboard, 'Dashboard')
def get_modified_docs(doctype):
return frappe.get_all(doctype,
filters = {
'owner': 'Administrator',
'modified_by': ['!=', 'Administrator']
})
def rename_modified_doc(docname, doctype):
new_name = docname + ' Custom'
try:
frappe.rename_doc(doctype, docname, new_name)
except frappe.ValidationError:
new_name = append_number_if_name_exists(doctype, new_name)
frappe.rename_doc(doctype, docname, new_name)

View file

@ -0,0 +1,15 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
themes = frappe.db.get_all(
"Website Theme", filters={"theme_url": ("not like", "/files/website_theme/%")}
)
for theme in themes:
doc = frappe.get_doc("Website Theme", theme.name)
doc.generate_bootstrap_theme()
doc.save()

View file

@ -0,0 +1,11 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
if not frappe.db.table_exists('Dashboard Chart'):
return
frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
if frappe.db.has_column('Dashboard Chart', 'is_custom'):
rename_field('Dashboard Chart', 'is_custom', 'use_report_chart')

View file

@ -6,9 +6,15 @@ import frappe
def execute():
frappe.rename_doc('DocType', 'Data Import', 'Data Import Legacy')
if not frappe.db.table_exists("Data Import"): return
meta = frappe.get_meta("Data Import")
# if Data Import is the new one, return early
if meta.fields[1].fieldname == "import_type":
return
frappe.db.sql("DROP TABLE IF EXISTS `tabData Import Legacy`")
frappe.rename_doc("DocType", "Data Import", "Data Import Legacy")
frappe.db.commit()
frappe.db.sql("DROP TABLE IF EXISTS `tabData Import`")
frappe.reload_doc("core", "doctype", "data_import")
frappe.get_doc("DocType", "Data Import").on_update()
frappe.delete_doc_if_exists("DocType", "Data Import Beta")
frappe.rename_doc("DocType", "Data Import Beta", "Data Import")

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