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