Merge branch 'develop' into dib-fixes-2020-07-22

This commit is contained in:
Suraj Shetty 2020-08-10 12:14:58 +05:30 committed by GitHub
commit 48634e131a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 288 additions and 251 deletions

View file

@ -231,8 +231,7 @@ def get_site_config(sites_path=None, site_path=None):
if os.path.exists(site_config):
config.update(get_file_json(site_config))
elif local.site and not local.flags.new_site:
print("Site {0} does not exist".format(local.site))
sys.exit(1)
raise IncorrectSitePath("{0} does not exist".format(local.site))
return _dict(config)
@ -436,12 +435,8 @@ def get_roles(username=None):
"""Returns roles of current user."""
if not local.session:
return ["Guest"]
if username:
import frappe.permissions
return frappe.permissions.get_roles(username)
else:
return get_user().get_roles()
import frappe.permissions
return frappe.permissions.get_roles(username or local.session.user)
def get_request_header(key, default=None):
"""Return HTTP request header.
@ -1559,10 +1554,10 @@ def get_doctype_app(doctype):
loggers = {}
log_level = None
def logger(module=None, with_more_info=False):
def logger(module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20):
'''Returns a python logger that uses StreamHandler'''
from frappe.utils.logger import get_logger
return get_logger(module=module, with_more_info=with_more_info)
return get_logger(module=module, with_more_info=with_more_info, allow_site=allow_site, filter=filter, max_size=max_size, file_count=file_count)
def log_error(message=None, title=_("Error")):
'''Log error to Error Log'''
@ -1707,3 +1702,7 @@ def mock(type, size=1, locale='en'):
from frappe.chat.util import squashify
return squashify(results)
def validate_and_sanitize_search_inputs(fn):
from frappe.desk.search import validate_and_sanitize_search_inputs as func
return func(fn)

View file

@ -99,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

@ -338,7 +338,7 @@ class CookieManager:
self.set_cookie("country", frappe.session.session_country)
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
if not secure:
if not secure and hasattr(frappe.local, 'request'):
secure = frappe.local.request.scheme == "https"
self.cookies[key] = {
"value": value,

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

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

View file

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

View file

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

View file

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

View file

@ -960,6 +960,9 @@ class Column:
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]))

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1307,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

@ -296,3 +296,4 @@ frappe.patches.v13_0.update_duration_options
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

@ -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

@ -3,7 +3,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({
let template = `
<div class="multiselect-list dropdown">
<div class="form-control cursor-pointer dropdown-toggle input-sm" data-toggle="dropdown" tabindex=0>
<span class="status-text ellipsis"></span>
<div class="status-text ellipsis"></div>
</div>
<ul class="dropdown-menu">
<li class="dropdown-input-wrapper">

View file

@ -19,7 +19,7 @@ frappe.ui.form.on = frappe.ui.form.on_change = function(doctype, fieldname, hand
let _handler = (...args) => {
try {
handler(...args);
return handler(...args);
} catch (error) {
console.error(handler);
throw error;

View file

@ -6,7 +6,6 @@ frappe.breadcrumbs = {
preferred: {
"File": "",
"Video": "",
"Dashboard": "Customization",
"Dashboard Chart": "Customization",
"Dashboard Chart Source": "Customization"

View file

@ -122,7 +122,8 @@ function shorten_number(number, country) {
const number_system = get_number_system(country);
let x = Math.abs(Math.round(number));
for (const map of number_system) {
if (x >= map.divisor) {
const condition = map.condition ? map.condition(x) : x >= map.divisor;
if (condition) {
return Math.round(number/map.divisor) + ' ' + map.symbol;
}
}
@ -152,9 +153,14 @@ function get_number_system(country) {
{
divisor: 1.0e+6,
symbol: 'M'
},
{
divisor: 1.0e+3,
symbol: 'K',
condition: (num) => num.toFixed().length > 5
}]
};
return number_system_map[country];
}
export { generate_route, generate_grid, build_summary_item, shorten_number };
export { generate_route, generate_grid, build_summary_item, shorten_number };

View file

@ -174,18 +174,12 @@ class ShortcutDialog extends WidgetDialog {
onchange: () => {
if (this.dialog.get_value("type") == "DocType") {
let doctype = this.dialog.get_value("link_to");
doctype &&
frappe.db
.get_value("DocType", doctype, "issingle")
.then((res) => {
if (res.message && res.message.issingle) {
this.hide_filters();
} else {
this.setup_filter(doctype);
this.show_filters();
}
});
if (doctype && frappe.boot.single_types.includes(doctype)) {
this.hide_filters();
} else if (doctype) {
this.setup_filter(doctype);
this.show_filters();
}
} else {
this.hide_filters();
}

View file

@ -1,9 +1,7 @@
{%- set res = frappe.utils.get_thumbnail_base64_for_image(src) if src else false -%}
{%- if res and res['base64'].startswith('data:') -%}
<img src="{{ res['base64'] }}" class="image-with-blur {{ resolve_class(class) }}"
data-src="{{ src or '' }}" alt="{{ alt or '' }}"
width="{{ res['width'] }}" height="{{ res['height'] }}"
style="width: {{ res['width'] }}px; height: {{ res['height'] }}px;" />
alt="{{ alt or '' }}" width="{{ res['width'] }}" height="{{ res['height'] }}" data-src="{{ src or '' }}" />
{%- else -%}
<img src="{{ src or '' }}" class="{{ resolve_class(class) }}" alt="{{ alt or '' }}" />
{%- endif -%}

View file

@ -49,7 +49,7 @@ class TestScheduler(TestCase):
# 2nd job not loaded
self.assertFalse(job.enqueue())
job.delete()
frappe.db.sql('DELETE FROM `tabScheduled Job Log` WHERE `scheduled_job_type`=%s', job.name)
def test_is_dormant(self):
self.assertTrue(is_dormant(check_time= get_datetime('2100-01-01 00:00:00')))

View file

@ -50,3 +50,31 @@ class TestSearch(unittest.TestCase):
def tearDown(self):
frappe.local.lang = 'en'
def test_validate_and_sanitize_search_inputs(self):
# should raise error if searchfield is injectable
self.assertRaises(frappe.DataError,
get_data, *('User', 'Random', 'select * from tabSessions) --', '1', '10', dict()))
# page_len and start should be converted to int
self.assertListEqual(get_data('User', 'Random', 'email', 'name or (select * from tabSessions)', '10', dict()),
['User', 'Random', 'email', 0, 10, {}])
self.assertListEqual(get_data('User', 'Random', 'email', page_len='2', start='10', filters=dict()),
['User', 'Random', 'email', 10, 2, {}])
# DocType can be passed as None which should be accepted
self.assertListEqual(get_data(None, 'Random', 'email', '2', '10', dict()),
[None, 'Random', 'email', 2, 10, {}])
# return empty string if passed doctype is invalid
self.assertListEqual(get_data("Random DocType", 'Random', 'email', '2', '10', dict()), [])
# should not fail if function is called via frappe.call with extra arguments
args = ("Random DocType", 'Random', 'email', '2', '10', dict())
kwargs = {'as_dict': False}
self.assertListEqual(frappe.call('frappe.tests.test_search.get_data', *args, **kwargs), [])
@frappe.validate_and_sanitize_search_inputs
def get_data(doctype, txt, searchfield, start, page_len, filters):
return [doctype, txt, searchfield, start, page_len, filters]

View file

@ -119,7 +119,7 @@ def get_dict(fortype, name=None):
messages += frappe.db.sql("select 'Role:', name from tabRole")
messages += frappe.db.sql("select 'Module:', name from `tabModule Def`")
message_dict = make_dict_from_messages(messages)
message_dict = make_dict_from_messages(messages, load_user_translation=False)
message_dict.update(get_dict_from_hooks(fortype, name))
# remove untranslated
message_dict = {k:v for k, v in iteritems(message_dict) if k!=v}
@ -127,6 +127,7 @@ def get_dict(fortype, name=None):
cache.hset("translation_assets", frappe.local.lang, translation_assets, shared=True)
translation_map = translation_assets[asset_key]
if fortype == "boot":
translation_map.update(get_user_translations(frappe.local.lang))
@ -144,14 +145,17 @@ def get_dict_from_hooks(fortype, name):
return translated_dict
def make_dict_from_messages(messages, full_dict=None):
def make_dict_from_messages(messages, full_dict=None, load_user_translation=True):
"""Returns translated messages as a dict in Language specified in `frappe.local.lang`
:param messages: List of untranslated messages
"""
out = {}
if full_dict==None:
full_dict = get_full_dict(frappe.local.lang)
if load_user_translation:
full_dict = get_full_dict(frappe.local.lang)
else:
full_dict = load_lang(frappe.local.lang)
for m in messages:
if m[1] in full_dict:
@ -189,11 +193,9 @@ def get_full_dict(lang):
try:
# get user specific transaltion data
user_translations = get_user_translations(lang)
except Exception:
user_translations = None
if user_translations:
frappe.local.lang_full_dict.update(user_translations)
except Exception:
pass
return frappe.local.lang_full_dict

View file

@ -118,8 +118,9 @@ def get_versions():
def get_app_branch(app):
'''Returns branch of an app'''
try:
null_stream = open(os.devnull, 'wb')
result = subprocess.check_output('cd ../apps/{0} && git rev-parse --abbrev-ref HEAD'.format(app),
shell=True)
shell=True, stdin=null_stream, stderr=null_stream)
result = safe_decode(result)
result = result.strip()
return result
@ -128,8 +129,9 @@ def get_app_branch(app):
def get_app_last_commit_ref(app):
try:
null_stream = open(os.devnull, 'wb')
result = subprocess.check_output('cd ../apps/{0} && git rev-parse HEAD --short 7'.format(app),
shell=True)
shell=True, stdin=null_stream, stderr=null_stream)
result = safe_decode(result)
result = result.strip()
return result

View file

@ -8,6 +8,7 @@ import frappe
from dateutil.parser._parser import ParserError
import subprocess
import operator
import json
import re, datetime, math, time
import babel.dates
from babel.core import UnknownLocaleError
@ -1236,8 +1237,6 @@ def is_subset(list_a, list_b):
def generate_hash(*args, **kwargs):
return frappe.generate_hash(*args, **kwargs)
def guess_date_format(date_string):
DATE_FORMATS = [
r"%d-%m-%Y",
@ -1310,3 +1309,9 @@ def guess_date_format(date_string):
if date_format and time_format:
return (date_format + ' ' + time_format).strip()
def validate_json_string(string):
try:
json.loads(string)
except (TypeError, ValueError):
raise frappe.ValidationError

View file

@ -11,56 +11,86 @@ from six import text_type
# imports - module imports
import frappe
from frappe.utils import get_sites
default_log_level = logging.DEBUG
site = getattr(frappe.local, 'site', None)
def get_logger(module, with_more_info=False):
global site
if module in frappe.loggers:
return frappe.loggers[module]
def get_logger(module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20):
"""Application Logger for your given module
Args:
module (str, optional): Name of your logger and consequently your log file. Defaults to None.
with_more_info (bool, optional): Will log the form dict using the SiteContextFilter. Defaults to False.
allow_site ((str, bool), optional): Pass site name to explicitly log under it's logs. If True and unspecified, guesses which site the logs would be saved under. Defaults to True.
filter (function, optional): Add a filter function for your logger. Defaults to None.
max_size (int, optional): Max file size of each log file in bytes. Defaults to 100_000.
file_count (int, optional): Max count of log files to be retained via Log Rotation. Defaults to 20.
Returns:
<class 'logging.Logger'>: Returns a Python logger object with Site and Bench level logging capabilities.
"""
if allow_site is True:
site = getattr(frappe.local, "site", None)
elif allow_site in get_sites():
site = allow_site
else:
site = False
logger_name = "{0}-{1}".format(module, site or "all")
try:
return frappe.loggers[logger_name]
except KeyError:
pass
if not module:
module = "frappe"
with_more_info = True
logfile = module + '.log'
site = getattr(frappe.local, 'site', None)
LOG_FILENAME = os.path.join('..', 'logs', logfile)
logfile = module + ".log"
log_filename = os.path.join("..", "logs", logfile)
logger = logging.getLogger(module)
logger = logging.getLogger(logger_name)
logger.setLevel(frappe.log_level or default_log_level)
logger.propagate = False
formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s')
handler = RotatingFileHandler(LOG_FILENAME, maxBytes=100_000, backupCount=20)
formatter = logging.Formatter("%(asctime)s %(levelname)s {0} %(message)s".format(module))
handler = RotatingFileHandler(log_filename, maxBytes=max_size, backupCount=file_count)
handler.setFormatter(formatter)
logger.addHandler(handler)
#
if site:
SITELOG_FILENAME = os.path.join(site, 'logs', logfile)
site_handler = RotatingFileHandler(SITELOG_FILENAME, maxBytes=100_000, backupCount=20)
sitelog_filename = os.path.join(site, "logs", logfile)
site_handler = RotatingFileHandler(sitelog_filename, maxBytes=max_size, backupCount=file_count)
site_handler.setFormatter(formatter)
logger.addHandler(site_handler)
if with_more_info:
handler.addFilter(SiteContextFilter())
handler.setFormatter(formatter)
if filter:
logger.addFilter(filter)
frappe.loggers[module] = logger
frappe.loggers[logger_name] = logger
return logger
class SiteContextFilter(logging.Filter):
"""This is a filter which injects request information (if available) into the log."""
def filter(self, record):
if "Form Dict" not in text_type(record.msg):
record.msg = text_type(record.msg) + "\nSite: {0}\nForm Dict: {1}".format(site, getattr(frappe.local, 'form_dict', None))
site = getattr(frappe.local, "site", None)
form_dict = getattr(frappe.local, "form_dict", None)
record.msg = text_type(record.msg) + "\nSite: {0}\nForm Dict: {1}".format(site, form_dict)
return True
def set_log_level(level):
'''Use this method to set log level to something other than the default DEBUG'''
frappe.log_level = getattr(logging, (level or '').upper(), None) or default_log_level
"""Use this method to set log level to something other than the default DEBUG"""
frappe.log_level = getattr(logging, (level or "").upper(), None) or default_log_level
frappe.loggers = {}

View file

@ -70,6 +70,7 @@ def get_safe_globals():
render_template=frappe.render_template,
msgprint=frappe.msgprint,
throw=frappe.throw,
sendmail = frappe.sendmail,
user=user,
get_fullname=frappe.utils.get_fullname,

View file

@ -16,6 +16,7 @@
"route_to_success_link",
"allow_edit",
"allow_multiple",
"apply_document_permissions",
"show_in_grid",
"allow_delete",
"allow_print",
@ -346,14 +347,20 @@
"fieldname": "custom_css_section",
"fieldtype": "Section Break",
"label": "Custom CSS"
},
{
"default": "0",
"fieldname": "apply_document_permissions",
"fieldtype": "Check",
"label": "Apply Document Permissions"
}
],
"has_web_view": 1,
"icon": "icon-edit",
"is_published_field": "published",
"links": [],
"modified": "2019-12-24 14:15:43.497431",
"modified_by": "faris@erpnext.com",
"modified": "2020-06-30 21:49:18.237443",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form",
"owner": "Administrator",

View file

@ -130,7 +130,7 @@ def get_context(context):
if frappe.session.user == "Guest" and frappe.form_dict.name:
frappe.throw(_("You need to be logged in to access this {0}.").format(self.doc_type), frappe.PermissionError)
if frappe.form_dict.name and not has_web_form_permission(self.doc_type, frappe.form_dict.name):
if frappe.form_dict.name and not self.has_web_form_permission(self.doc_type, frappe.form_dict.name):
frappe.throw(_("You don't have the permissions to access this document"), frappe.PermissionError)
self.reset_field_parent()
@ -343,6 +343,27 @@ def get_context(context):
frappe.throw(_('Mandatory Information missing:') + '<br><br>'
+ '<br>'.join(['{0} ({1})'.format(d.label, d.fieldtype) for d in missing]))
def has_web_form_permission(self, doctype, name, ptype='read'):
if frappe.session.user=="Guest":
return False
if self.apply_document_permissions:
return frappe.get_doc(doctype, name).has_permission()
# owner matches
elif frappe.db.get_value(doctype, name, "owner")==frappe.session.user:
return True
elif frappe.has_website_permission(name, ptype=ptype, doctype=doctype):
return True
elif check_webform_perm(doctype, name):
return True
else:
return False
@frappe.whitelist(allow_guest=True)
def accept(web_form, data, docname=None, for_payment=False):
@ -391,7 +412,7 @@ def accept(web_form, data, docname=None, for_payment=False):
doc.run_method('validate_payment')
if doc.name:
if has_web_form_permission(doc.doctype, doc.name, "write"):
if web_form.has_web_form_permission(doc.doctype, doc.name, "write"):
doc.save(ignore_permissions=True)
else:
# only if permissions are present
@ -478,24 +499,6 @@ def delete_multiple(web_form_name, docnames):
raise frappe.PermissionError("You do not have permisssion to delete " + ", ".join(restricted_docnames))
def has_web_form_permission(doctype, name, ptype='read'):
if frappe.session.user=="Guest":
return False
# owner matches
elif frappe.db.get_value(doctype, name, "owner")==frappe.session.user:
return True
elif frappe.has_website_permission(name, ptype=ptype, doctype=doctype):
return True
elif check_webform_perm(doctype, name):
return True
else:
return False
def check_webform_perm(doctype, name):
doc = frappe.get_doc(doctype, name)
if hasattr(doc, "has_webform_permission"):
@ -532,7 +535,7 @@ def get_form_data(doctype, docname=None, web_form_name=None):
if docname:
doc = frappe.get_doc(doctype, docname)
if has_web_form_permission(doctype, docname, ptype='read'):
if web_form.has_web_form_permission(doctype, docname, ptype='read'):
out.doc = doc
else:
frappe.throw(_("Not permitted"), frappe.PermissionError)

View file

@ -6,6 +6,9 @@ import frappe
import unittest
from frappe.utils import random_string
from frappe.model.workflow import apply_workflow, WorkflowTransitionError, WorkflowPermissionError, get_common_transition_actions
from frappe.test_runner import make_test_records
make_test_records("User")
class TestWorkflow(unittest.TestCase):
def setUp(self):
@ -78,7 +81,7 @@ class TestWorkflow(unittest.TestCase):
frappe.set_user('test2@example.com')
doc = self.test_default_condition()
workflow_actions = frappe.get_all('Workflow Action', fields=['status'])
workflow_actions = frappe.get_all('Workflow Action', fields=['status', 'reference_name'])
self.assertEqual(len(workflow_actions), 1)
# test if status of workflow actions are updated on approval
@ -102,6 +105,9 @@ class TestWorkflow(unittest.TestCase):
todo.reload()
self.assertEqual(todo.docstatus, 1)
self.workflow.states[1].doc_status = 0
self.workflow.save()
def test_if_workflow_set_on_action(self):
self.workflow.states[1].doc_status = 1
self.workflow.save()
@ -111,12 +117,17 @@ class TestWorkflow(unittest.TestCase):
self.assertEqual(todo.docstatus, 1)
self.assertEqual(todo.workflow_state, 'Approved')
self.workflow.states[1].doc_status = 0
self.workflow.save()
def create_todo_workflow():
if frappe.db.exists('Workflow', 'Test ToDo'):
return frappe.get_doc('Workflow', 'Test ToDo').save(ignore_permissions=True)
else:
frappe.get_doc(dict(doctype='Role',
role_name='Test Approver')).insert(ignore_if_duplicate=True)
frappe.db.commit()
frappe.cache().hdel('roles', frappe.session.user)
workflow = frappe.new_doc('Workflow')
workflow.workflow_name = 'Test ToDo'
workflow.document_type = 'ToDo'

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import get_controller
from frappe.utils import get_request_site_address, get_datetime, nowdate
from frappe.utils import get_datetime, nowdate, get_url
from frappe.website.router import get_pages, get_all_page_context_from_doctypes
from six import iteritems
from six.moves.urllib.parse import quote, urljoin
@ -25,13 +25,13 @@ def get_context(context):
for route, page in iteritems(get_pages()):
if page.sitemap:
links.append({
"loc": urljoin(host, quote(page.name.encode("utf-8"))),
"loc": get_url(quote(page.name.encode("utf-8"))),
"lastmod": nowdate()
})
for route, data in iteritems(get_public_pages_from_doctypes()):
links.append({
"loc": urljoin(host, quote((route or "").encode("utf-8"))),
"loc": get_url(quote((route or "").encode("utf-8"))),
"lastmod": get_datetime(data.get("modified")).strftime("%Y-%m-%d")
})

View file

@ -69,4 +69,5 @@ Whoosh==2.7.4
xlrd==1.2.0
zxcvbn-python==4.4.24
pycryptodome==3.9.8
paytmchecksum==1.7.0
paytmchecksum==1.7.0
wrapt==1.10.11