diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml index fb6e56037c..57a7fa304d 100644 --- a/.github/workflows/server-mariadb-tests.yml +++ b/.github/workflows/server-mariadb-tests.yml @@ -120,49 +120,10 @@ jobs: CI_BUILD_ID: ${{ github.run_id }} ORCHESTRATOR_URL: http://test-orchestrator.frappe.io - - name: Upload Coverage Data - if: ${{ steps.check-build.outputs.build == 'strawberry' }} - id: upload-coverage-data - run: | - cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE} - cd ${GITHUB_WORKSPACE} - pip3 install coverage==5.5 - pip3 install coveralls==3.0.1 - coveralls - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} - COVERALLS_FLAG_NAME: run-${{ matrix.container }} - COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }} - COVERALLS_PARALLEL: true - - - run: echo ${{ steps.check-build.outputs.build }} > guess-the-fruit.txt - - uses: actions/upload-artifact@v1 - with: - name: fruit - path: guess-the-fruit.txt - - coveralls: - name: Coverage Wrap Up - needs: test - container: python:3-slim - runs-on: ubuntu-18.04 - steps: - - uses: actions/download-artifact@v1 + - name: Upload coverage data + uses: codecov/codecov-action@v2 with: - name: fruit - - run: echo "WILDCARD=$(cat fruit/guess-the-fruit.txt)" >> $GITHUB_ENV - - - name: Clone - if: ${{ env.WILDCARD == 'strawberry' }} - uses: actions/checkout@v2 - - - name: Coveralls Finished - if: ${{ env.WILDCARD == 'strawberry' }} - run: | - cd ${GITHUB_WORKSPACE} - pip3 install coverage==5.5 - pip3 install coveralls==3.0.1 - coveralls --finish - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + name: MariaDB + fail_ci_if_error: true + files: /home/runner/frappe-bench/sites/coverage.xml + verbose: true diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml index 1539e8c2d5..57ac9c6c60 100644 --- a/.github/workflows/server-postgres-tests.yml +++ b/.github/workflows/server-postgres-tests.yml @@ -3,6 +3,8 @@ name: Server on: pull_request: workflow_dispatch: + push: + branches: [ develop ] concurrency: group: server-postgres-develop-${{ github.event.number }} @@ -116,7 +118,15 @@ jobs: - name: Run Tests if: ${{ steps.check-build.outputs.build == 'strawberry' }} - run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator + run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage env: CI_BUILD_ID: ${{ github.run_id }} ORCHESTRATOR_URL: http://test-orchestrator.frappe.io + + - name: Upload coverage data + uses: codecov/codecov-action@v2 + with: + name: Postgres + fail_ci_if_error: true + files: /home/runner/frappe-bench/sites/coverage.xml + verbose: true diff --git a/.mergify.yml b/.mergify.yml index 8c7a7dc95d..0bd9641d5b 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -2,7 +2,9 @@ pull_request_rules: - name: Auto-close PRs on stable branch conditions: - and: - - author!=surajshetty3416 + - and: + - author!=surajshetty3416 + - author!=gavindsouza - or: - base=version-13 - base=version-12 diff --git a/README.md b/README.md index 11343a632a..6c2804d843 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ - - + + diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..eb81252b61 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,9 @@ +codecov: + require_ci_to_pass: yes + status: + project: + default: + threshold: 0.5% +comment: + layout: "diff, flags, files" + require_changes: true diff --git a/frappe/__init__.py b/frappe/__init__.py index 6d79cbd760..ea983e7f02 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -140,7 +140,11 @@ lang = local("lang") if typing.TYPE_CHECKING: from frappe.database.mariadb.database import MariaDBDatabase from frappe.database.postgres.database import PostgresDatabase + from pypika import Query + db: typing.Union[MariaDBDatabase, PostgresDatabase] + qb: Query + # end: static analysis hack def init(site, sites_path=None, new_site=False): diff --git a/frappe/coverage.py b/frappe/coverage.py index 33f945be40..1969cae141 100644 --- a/frappe/coverage.py +++ b/frappe/coverage.py @@ -58,4 +58,5 @@ class CodeCoverage(): def __exit__(self, exc_type, exc_value, traceback): if self.with_coverage: self.coverage.stop() - self.coverage.save() \ No newline at end of file + self.coverage.save() + self.coverage.xml_report() \ No newline at end of file diff --git a/frappe/database/database.py b/frappe/database/database.py index d6ecf0795d..9fab8e116f 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -14,7 +14,7 @@ import frappe.model.meta from frappe import _ from time import time -from frappe.utils import now, getdate, cast_fieldtype, get_datetime, get_table_name +from frappe.utils import now, getdate, cast, get_datetime, get_table_name from frappe.model.utils.link_count import flush_local_link_count @@ -516,7 +516,6 @@ class Database(object): FROM `tabSingles` WHERE doctype = %s """, doctype) - # result = _cast_result(doctype, result) dict_ = frappe._dict(result) @@ -557,7 +556,7 @@ class Database(object): if not df: frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName) - val = cast_fieldtype(df.fieldtype, val) + val = cast(df.fieldtype, val) self.value_cache[doctype][fieldname] = val @@ -1052,19 +1051,3 @@ def enqueue_jobs_after_commit(): q.enqueue_call(execute_job, timeout=job.get("timeout"), kwargs=job.get("queue_args")) frappe.flags.enqueue_after_commit = [] - -# Helpers -def _cast_result(doctype, result): - batch = [ ] - - try: - for field, value in result: - df = frappe.get_meta(doctype).get_field(field) - if df: - value = cast_fieldtype(df.fieldtype, value) - - batch.append(tuple([field, value])) - except frappe.exceptions.DoesNotExistError: - return result - - return tuple(batch) diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index 020f3153df..1e111b8d12 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -242,6 +242,7 @@ "label": "Parent Page" }, { + "default": "[]", "fieldname": "content", "fieldtype": "Long Text", "hidden": 1, @@ -265,7 +266,7 @@ } ], "links": [], - "modified": "2021-08-19 12:51:00.233017", + "modified": "2021-08-30 18:47:18.227154", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 31bb551330..33c6adbd2f 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -62,7 +62,7 @@ class Workspace(Document): for link in self.links: link = link.as_dict() if link.type == "Card Break": - if card_links and (not current_card['only_for'] or current_card['only_for'] == frappe.get_system_settings('country')): + if card_links and (not current_card.get('only_for') or current_card.get('only_for') == frappe.get_system_settings('country')): current_card['links'] = card_links cards.append(current_card) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 5603b2daae..5a204caf70 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -969,7 +969,7 @@ class BaseDocument(object): return self.cast(val, df) def cast(self, value, df): - return cast_fieldtype(df.fieldtype, value) + return cast_fieldtype(df.fieldtype, value, show_warning=False) def _extract_images_from_text_editor(self): from frappe.core.doctype.file.file import extract_images_from_doc diff --git a/frappe/model/meta.py b/frappe/model/meta.py index de794ba77f..f89163e092 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -16,7 +16,7 @@ Example: ''' from datetime import datetime import frappe, json, os -from frappe.utils import cstr, cint, cast_fieldtype +from frappe.utils import cstr, cint, cast from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields from frappe.model.document import Document from frappe.model.base_document import BaseDocument @@ -322,24 +322,24 @@ class Meta(Document): for ps in property_setters: if ps.doctype_or_field=='DocType': - self.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + self.set(ps.property, cast(ps.property_type, ps.value)) elif ps.doctype_or_field=='DocField': for d in self.fields: if d.fieldname == ps.field_name: - d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + d.set(ps.property, cast(ps.property_type, ps.value)) break elif ps.doctype_or_field=='DocType Link': for d in self.links: if d.name == ps.row_name: - d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + d.set(ps.property, cast(ps.property_type, ps.value)) break elif ps.doctype_or_field=='DocType Action': for d in self.actions: if d.name == ps.row_name: - d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + d.set(ps.property, cast(ps.property_type, ps.value)) break def add_custom_links_and_actions(self): @@ -532,7 +532,7 @@ class Meta(Document): label = link.group, items = [link.parent_doctype or link.link_doctype] )) - + if not link.is_child_table: if link.link_fieldname != data.fieldname: if data.fieldname: diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 99fc4da182..a4dc1a6709 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -285,10 +285,6 @@ frappe.Application = class Application { frappe.modules[page.module]=page; frappe.workspaces[frappe.router.slug(page.title)] = page; } - if (!frappe.workspaces['home']) { - // default workspace is settings for Frappe - frappe.workspaces['home'] = frappe.workspaces[Object.keys(frappe.workspaces)[0]]; - } } load_user_permissions() { diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 05c70b214d..dd1d622bab 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -786,6 +786,7 @@ export default class Grid { doctype: link_field.options, fieldname: link, qty_fieldname: qty, + get_query: link_field.get_query, target: this, txt: "" }); diff --git a/frappe/public/js/frappe/form/link_selector.js b/frappe/public/js/frappe/form/link_selector.js index b8f1b3a842..3ae03dbe91 100644 --- a/frappe/public/js/frappe/form/link_selector.js +++ b/frappe/public/js/frappe/form/link_selector.js @@ -96,10 +96,10 @@ frappe.ui.form.LinkSelector = class LinkSelector { .attr('data-value', v[0]) .click(function () { var value = $(this).attr("data-value"); - var $link = this; if (me.target.is_grid) { // set in grid - me.set_in_grid(value); + // call search after value is set to get latest filtered results + me.set_in_grid(value).then(() => me.search()); } else { if (me.target.doctype) me.target.parse_validate_and_set_in_model(value); @@ -110,8 +110,8 @@ frappe.ui.form.LinkSelector = class LinkSelector { me.dialog.hide(); } return false; - }) - }) + }); + }); } else { $('


' + __("No Results") + '' + (frappe.model.can_create(me.doctype) ? @@ -130,49 +130,56 @@ frappe.ui.form.LinkSelector = class LinkSelector { }, this.dialog.get_primary_btn()); } - set_in_grid (value) { - var me = this, updated = false; - var d = null; - if (this.qty_fieldname) { - frappe.prompt({ - fieldname: "qty", fieldtype: "Float", label: "Qty", - "default": 1, reqd: 1 - }, function (data) { - $.each(me.target.frm.doc[me.target.df.fieldname] || [], function (i, d) { - if (d[me.fieldname] === value) { - frappe.model.set_value(d.doctype, d.name, me.qty_fieldname, data.qty); - frappe.show_alert(__("Added {0} ({1})", [value, d[me.qty_fieldname]])); - updated = true; - return false; + set_in_grid(value) { + return new Promise((resolve) => { + if (this.qty_fieldname) { + frappe.prompt({ + fieldname: "qty", + fieldtype: "Float", + label: "Qty", + default: 1, + reqd: 1 + }, (data) => { + let updated = (this.target.frm.doc[this.target.df.fieldname] || []).some(d => { + if (d[this.fieldname] === value) { + frappe.model.set_value(d.doctype, d.name, this.qty_fieldname, data.qty).then(() => { + frappe.show_alert(__("Added {0} ({1})", [value, d[this.qty_fieldname]])); + resolve(); + }); + return true; + } + }); + if (!updated) { + let d = null; + frappe.run_serially([ + () => d = this.target.add_new_row(), + () => frappe.timeout(0.1), + () => { + let args = {}; + args[this.fieldname] = value; + args[this.qty_fieldname] = data.qty; + return frappe.model.set_value(d.doctype, d.name, args); + }, + () => frappe.show_alert(__("Added {0} ({1})", [value, data.qty])), + () => resolve() + ]); } + }, __("Set Quantity"), __("Set")); + } else if (this.dynamic_link_field) { + let d = this.target.add_new_row(); + frappe.model.set_value(d.doctype, d.name, this.dynamic_link_field, this.dynamic_link_reference); + frappe.model.set_value(d.doctype, d.name, this.fieldname, value).then(() => { + frappe.show_alert(__("{0} {1} added", [this.dynamic_link_reference, value])); + resolve(); }); - if (!updated) { - frappe.run_serially([ - () => { - d = me.target.add_new_row(); - }, - () => frappe.timeout(0.1), - () => { - let args = {}; - args[me.fieldname] = value; - args[me.qty_fieldname] = data.qty; - - return frappe.model.set_value(d.doctype, d.name, args); - }, - () => frappe.show_alert(__("Added {0} ({1})", [value, data.qty])) - ]); - } - }, __("Set Quantity"), __("Set")); - } else if (me.dynamic_link_field) { - var d = me.target.add_new_row(); - frappe.model.set_value(d.doctype, d.name, me.dynamic_link_field, me.dynamic_link_reference); - frappe.model.set_value(d.doctype, d.name, me.fieldname, value); - frappe.show_alert(__("{0} {1} added", [me.dynamic_link_reference, value])); - } else { - var d = me.target.add_new_row(); - frappe.model.set_value(d.doctype, d.name, me.fieldname, value); - frappe.show_alert(__("{0} added", [value])); - } + } else { + let d = this.target.add_new_row(); + frappe.model.set_value(d.doctype, d.name, this.fieldname, value).then(() => { + frappe.show_alert(__("{0} added", [value])); + resolve(); + }); + } + }); } }; diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 81b2c4ff8d..c5db7df88c 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -952,9 +952,16 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { get_indicator_html(doc) { const indicator = frappe.get_indicator(doc, this.doctype); + // sequence is important + const docstatus_description = [ + __('Document is in draft state'), + __('Document has been submitted'), + __('Document has been cancelled') + ]; + const title = docstatus_description[doc.docstatus || 0]; if (indicator) { return ` + data-filter='${indicator[2]}' title='${title}'> ${__(indicator[0])} `; } diff --git a/frappe/public/scss/common/indicator.scss b/frappe/public/scss/common/indicator.scss index 62d7cacc9d..13162ab6b1 100644 --- a/frappe/public/scss/common/indicator.scss +++ b/frappe/public/scss/common/indicator.scss @@ -175,4 +175,4 @@ @keyframes blink { 50% { opacity: 0.5; } -} +} \ No newline at end of file diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 95ba763482..3033673224 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -6,12 +6,13 @@ import frappe from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url from frappe.utils import validate_url, validate_email_address from frappe.utils import ceil, floor -from frappe.utils.data import validate_python_code +from frappe.utils.data import cast, validate_python_code from PIL import Image from frappe.utils.image import strip_exif_data, optimize_image import io from mimetypes import guess_type +from datetime import datetime, timedelta, date class TestFilters(unittest.TestCase): def test_simple_dict(self): @@ -93,6 +94,45 @@ class TestDataManipulation(unittest.TestCase): self.assertTrue('style="background-image: url(\'{0}/assets/frappe/bg.jpg\') !important"'.format(url) in html) self.assertTrue('email' in html) +class TestFieldCasting(unittest.TestCase): + def test_str_types(self): + STR_TYPES = ( + "Data", "Text", "Small Text", "Long Text", "Text Editor", "Select", "Link", "Dynamic Link" + ) + for fieldtype in STR_TYPES: + self.assertIsInstance(cast(fieldtype, value=None), str) + self.assertIsInstance(cast(fieldtype, value="12-12-2021"), str) + self.assertIsInstance(cast(fieldtype, value=""), str) + self.assertIsInstance(cast(fieldtype, value=[]), str) + self.assertIsInstance(cast(fieldtype, value=set()), str) + + def test_float_types(self): + FLOAT_TYPES = ("Currency", "Float", "Percent") + for fieldtype in FLOAT_TYPES: + self.assertIsInstance(cast(fieldtype, value=None), float) + self.assertIsInstance(cast(fieldtype, value=1.12), float) + self.assertIsInstance(cast(fieldtype, value=112), float) + + def test_int_types(self): + INT_TYPES = ("Int", "Check") + + for fieldtype in INT_TYPES: + self.assertIsInstance(cast(fieldtype, value=None), int) + self.assertIsInstance(cast(fieldtype, value=1.12), int) + self.assertIsInstance(cast(fieldtype, value=112), int) + + def test_datetime_types(self): + self.assertIsInstance(cast("Datetime", value=None), datetime) + self.assertIsInstance(cast("Datetime", value="12-2-22"), datetime) + + def test_date_types(self): + self.assertIsInstance(cast("Date", value=None), date) + self.assertIsInstance(cast("Date", value="12-12-2021"), date) + + def test_time_types(self): + self.assertIsInstance(cast("Time", value=None), timedelta) + self.assertIsInstance(cast("Time", value="12:03:34"), timedelta) + class TestMathUtils(unittest.TestCase): def test_floor(self): from decimal import Decimal @@ -205,7 +245,6 @@ class TestImage(unittest.TestCase): self.assertLess(len(optimized_content), len(original_content)) class TestPythonExpressions(unittest.TestCase): - def test_validation_for_good_python_expression(self): valid_expressions = [ "foo == bar", @@ -229,4 +268,4 @@ class TestPythonExpressions(unittest.TestCase): "oops = forgot_equals", ] for expr in invalid_expressions: - self.assertRaises(frappe.ValidationError, validate_python_code, expr) \ No newline at end of file + self.assertRaises(frappe.ValidationError, validate_python_code, expr) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index f2c553211d..5a7328b07e 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt +from typing import Optional import frappe import operator import json @@ -8,6 +9,7 @@ import re, datetime, math, time from code import compile_command from urllib.parse import quote, urljoin from frappe.desk.utils import slug +from click import secho DATE_FORMAT = "%Y-%m-%d" TIME_FORMAT = "%H:%M:%S.%f" @@ -16,10 +18,10 @@ DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT def is_invalid_date_string(date_string): # dateutil parser does not agree with dates like "0001-01-01" or "0000-00-00" - return (not date_string) or (date_string or "").startswith(("0001-01-01", "0000-00-00")) + return not isinstance(date_string, str) or ((not date_string) or (date_string or "").startswith(("0001-01-01", "0000-00-00"))) # datetime functions -def getdate(string_date=None): +def getdate(string_date: Optional[str] = None): """ Converts string date (yyyy-mm-dd) to datetime.date object. If no input is provided, current date is returned. @@ -67,6 +69,31 @@ def get_datetime(datetime_str=None): except ValueError: return parser.parse(datetime_str) +def get_timedelta(time: Optional[str] = None) -> Optional[datetime.timedelta]: + """Return `datetime.timedelta` object from string value of a + valid time format. Returns None if `time` is not a valid format + + Args: + time (str): A valid time representation. This string is parsed + using `dateutil.parser.parse`. Examples of valid inputs are: + '0:0:0', '17:21:00', '2012-01-19 17:21:00'. Checkout + https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.parse + + Returns: + datetime.timedelta: Timedelta object equivalent of the passed `time` string + """ + from dateutil import parser + + time = time or "0:0:0" + + try: + t = parser.parse(time) + return datetime.timedelta( + hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond + ) + except Exception: + return None + def to_timedelta(time_str): from dateutil import parser @@ -505,7 +532,14 @@ def has_common(l1, l2): """Returns truthy value if there are common elements in lists l1 and l2""" return set(l1) & set(l2) -def cast_fieldtype(fieldtype, value): +def cast_fieldtype(fieldtype, value, show_warning=True): + if show_warning: + message = ( + "Function `frappe.utils.data.cast` has been deprecated in favour" + " of `frappe.utils.data.cast`. Use the newer util for safer type casting." + ) + secho(message, fg="yellow") + if fieldtype in ("Currency", "Float", "Percent"): value = flt(value) @@ -527,6 +561,46 @@ def cast_fieldtype(fieldtype, value): return value +def cast(fieldtype, value=None): + """Cast the value to the Python native object of the Frappe fieldtype provided. + If value is None, the first/lowest value of the `fieldtype` will be returned. + If value can't be cast as fieldtype due to an invalid input, None will be returned. + + Mapping of Python types => Frappe types: + * str => ("Data", "Text", "Small Text", "Long Text", "Text Editor", "Select", "Link", "Dynamic Link") + * float => ("Currency", "Float", "Percent") + * int => ("Int", "Check") + * datetime.datetime => ("Datetime",) + * datetime.date => ("Date",) + * datetime.time => ("Time",) + """ + if fieldtype in ("Currency", "Float", "Percent"): + value = flt(value) + + elif fieldtype in ("Int", "Check"): + value = cint(value) + + elif fieldtype in ("Data", "Text", "Small Text", "Long Text", + "Text Editor", "Select", "Link", "Dynamic Link"): + value = cstr(value) + + elif fieldtype == "Date": + if value: + value = getdate(value) + else: + value = datetime.datetime(1, 1, 1).date() + + elif fieldtype == "Datetime": + if value: + value = get_datetime(value) + else: + value = datetime.datetime(1, 1, 1) + + elif fieldtype == "Time": + value = get_timedelta(value) + + return value + def flt(s, precision=None): """Convert to float (ignoring commas in string) @@ -1202,7 +1276,7 @@ def evaluate_filters(doc, filters): def compare(val1, condition, val2, fieldtype=None): ret = False if fieldtype: - val2 = cast_fieldtype(fieldtype, val2) + val2 = cast(fieldtype, val2) if condition in operator_map: ret = operator_map[condition](val1, val2) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 2e27859faa..7ccd80e346 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -30,8 +30,14 @@ class NamespaceDict(frappe._dict): def safe_exec(script, _globals=None, _locals=None): - # script reports must be enabled via site_config.json - if not frappe.conf.server_script_enabled: + # server scripts can be disabled via site_config.json + # they are enabled by default + if 'server_script_enabled' in frappe.conf: + enabled = frappe.conf.server_script_enabled + else: + enabled = True + + if not enabled: frappe.throw(_('Please Enable Server Scripts'), ServerScriptNotEnabled) # build globals @@ -228,6 +234,7 @@ VALID_UTILS = ( "getdate", "get_datetime", "to_timedelta", +"get_timedelta", "add_to_date", "add_days", "add_months",