@@ -120,49 +120,10 @@ jobs: | |||||
CI_BUILD_ID: ${{ github.run_id }} | CI_BUILD_ID: ${{ github.run_id }} | ||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io | 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: | 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 |
@@ -3,6 +3,8 @@ name: Server | |||||
on: | on: | ||||
pull_request: | pull_request: | ||||
workflow_dispatch: | workflow_dispatch: | ||||
push: | |||||
branches: [ develop ] | |||||
concurrency: | concurrency: | ||||
group: server-postgres-develop-${{ github.event.number }} | group: server-postgres-develop-${{ github.event.number }} | ||||
@@ -116,7 +118,15 @@ jobs: | |||||
- name: Run Tests | - name: Run Tests | ||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }} | 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: | env: | ||||
CI_BUILD_ID: ${{ github.run_id }} | CI_BUILD_ID: ${{ github.run_id }} | ||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io | 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 |
@@ -2,7 +2,9 @@ pull_request_rules: | |||||
- name: Auto-close PRs on stable branch | - name: Auto-close PRs on stable branch | ||||
conditions: | conditions: | ||||
- and: | - and: | ||||
- author!=surajshetty3416 | |||||
- and: | |||||
- author!=surajshetty3416 | |||||
- author!=gavindsouza | |||||
- or: | - or: | ||||
- base=version-13 | - base=version-13 | ||||
- base=version-12 | - base=version-12 | ||||
@@ -26,8 +26,8 @@ | |||||
<a href='https://www.codetriage.com/frappe/frappe'> | <a href='https://www.codetriage.com/frappe/frappe'> | ||||
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'> | <img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'> | ||||
</a> | </a> | ||||
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'> | |||||
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'> | |||||
<a href="https://codecov.io/gh/frappe/frappe"> | |||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/> | |||||
</a> | </a> | ||||
</div> | </div> | ||||
@@ -0,0 +1,9 @@ | |||||
codecov: | |||||
require_ci_to_pass: yes | |||||
status: | |||||
project: | |||||
default: | |||||
threshold: 0.5% | |||||
comment: | |||||
layout: "diff, flags, files" | |||||
require_changes: true |
@@ -140,7 +140,11 @@ lang = local("lang") | |||||
if typing.TYPE_CHECKING: | if typing.TYPE_CHECKING: | ||||
from frappe.database.mariadb.database import MariaDBDatabase | from frappe.database.mariadb.database import MariaDBDatabase | ||||
from frappe.database.postgres.database import PostgresDatabase | from frappe.database.postgres.database import PostgresDatabase | ||||
from pypika import Query | |||||
db: typing.Union[MariaDBDatabase, PostgresDatabase] | db: typing.Union[MariaDBDatabase, PostgresDatabase] | ||||
qb: Query | |||||
# end: static analysis hack | # end: static analysis hack | ||||
def init(site, sites_path=None, new_site=False): | def init(site, sites_path=None, new_site=False): | ||||
@@ -58,4 +58,5 @@ class CodeCoverage(): | |||||
def __exit__(self, exc_type, exc_value, traceback): | def __exit__(self, exc_type, exc_value, traceback): | ||||
if self.with_coverage: | if self.with_coverage: | ||||
self.coverage.stop() | self.coverage.stop() | ||||
self.coverage.save() | |||||
self.coverage.save() | |||||
self.coverage.xml_report() |
@@ -14,7 +14,7 @@ import frappe.model.meta | |||||
from frappe import _ | from frappe import _ | ||||
from time import time | 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 | from frappe.model.utils.link_count import flush_local_link_count | ||||
@@ -516,7 +516,6 @@ class Database(object): | |||||
FROM `tabSingles` | FROM `tabSingles` | ||||
WHERE doctype = %s | WHERE doctype = %s | ||||
""", doctype) | """, doctype) | ||||
# result = _cast_result(doctype, result) | |||||
dict_ = frappe._dict(result) | dict_ = frappe._dict(result) | ||||
@@ -557,7 +556,7 @@ class Database(object): | |||||
if not df: | if not df: | ||||
frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName) | 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 | self.value_cache[doctype][fieldname] = val | ||||
@@ -1052,19 +1051,3 @@ def enqueue_jobs_after_commit(): | |||||
q.enqueue_call(execute_job, timeout=job.get("timeout"), | q.enqueue_call(execute_job, timeout=job.get("timeout"), | ||||
kwargs=job.get("queue_args")) | kwargs=job.get("queue_args")) | ||||
frappe.flags.enqueue_after_commit = [] | 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) |
@@ -242,6 +242,7 @@ | |||||
"label": "Parent Page" | "label": "Parent Page" | ||||
}, | }, | ||||
{ | { | ||||
"default": "[]", | |||||
"fieldname": "content", | "fieldname": "content", | ||||
"fieldtype": "Long Text", | "fieldtype": "Long Text", | ||||
"hidden": 1, | "hidden": 1, | ||||
@@ -265,7 +266,7 @@ | |||||
} | } | ||||
], | ], | ||||
"links": [], | "links": [], | ||||
"modified": "2021-08-19 12:51:00.233017", | |||||
"modified": "2021-08-30 18:47:18.227154", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Desk", | "module": "Desk", | ||||
"name": "Workspace", | "name": "Workspace", | ||||
@@ -62,7 +62,7 @@ class Workspace(Document): | |||||
for link in self.links: | for link in self.links: | ||||
link = link.as_dict() | link = link.as_dict() | ||||
if link.type == "Card Break": | 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 | current_card['links'] = card_links | ||||
cards.append(current_card) | cards.append(current_card) | ||||
@@ -969,7 +969,7 @@ class BaseDocument(object): | |||||
return self.cast(val, df) | return self.cast(val, df) | ||||
def cast(self, value, 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): | def _extract_images_from_text_editor(self): | ||||
from frappe.core.doctype.file.file import extract_images_from_doc | from frappe.core.doctype.file.file import extract_images_from_doc | ||||
@@ -16,7 +16,7 @@ Example: | |||||
''' | ''' | ||||
from datetime import datetime | from datetime import datetime | ||||
import frappe, json, os | 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 import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.model.base_document import BaseDocument | from frappe.model.base_document import BaseDocument | ||||
@@ -322,24 +322,24 @@ class Meta(Document): | |||||
for ps in property_setters: | for ps in property_setters: | ||||
if ps.doctype_or_field=='DocType': | 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': | elif ps.doctype_or_field=='DocField': | ||||
for d in self.fields: | for d in self.fields: | ||||
if d.fieldname == ps.field_name: | 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 | break | ||||
elif ps.doctype_or_field=='DocType Link': | elif ps.doctype_or_field=='DocType Link': | ||||
for d in self.links: | for d in self.links: | ||||
if d.name == ps.row_name: | 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 | break | ||||
elif ps.doctype_or_field=='DocType Action': | elif ps.doctype_or_field=='DocType Action': | ||||
for d in self.actions: | for d in self.actions: | ||||
if d.name == ps.row_name: | 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 | break | ||||
def add_custom_links_and_actions(self): | def add_custom_links_and_actions(self): | ||||
@@ -532,7 +532,7 @@ class Meta(Document): | |||||
label = link.group, | label = link.group, | ||||
items = [link.parent_doctype or link.link_doctype] | items = [link.parent_doctype or link.link_doctype] | ||||
)) | )) | ||||
if not link.is_child_table: | if not link.is_child_table: | ||||
if link.link_fieldname != data.fieldname: | if link.link_fieldname != data.fieldname: | ||||
if data.fieldname: | if data.fieldname: | ||||
@@ -285,10 +285,6 @@ frappe.Application = class Application { | |||||
frappe.modules[page.module]=page; | frappe.modules[page.module]=page; | ||||
frappe.workspaces[frappe.router.slug(page.title)] = 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() { | load_user_permissions() { | ||||
@@ -786,6 +786,7 @@ export default class Grid { | |||||
doctype: link_field.options, | doctype: link_field.options, | ||||
fieldname: link, | fieldname: link, | ||||
qty_fieldname: qty, | qty_fieldname: qty, | ||||
get_query: link_field.get_query, | |||||
target: this, | target: this, | ||||
txt: "" | txt: "" | ||||
}); | }); | ||||
@@ -96,10 +96,10 @@ frappe.ui.form.LinkSelector = class LinkSelector { | |||||
.attr('data-value', v[0]) | .attr('data-value', v[0]) | ||||
.click(function () { | .click(function () { | ||||
var value = $(this).attr("data-value"); | var value = $(this).attr("data-value"); | ||||
var $link = this; | |||||
if (me.target.is_grid) { | if (me.target.is_grid) { | ||||
// set in 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 { | } else { | ||||
if (me.target.doctype) | if (me.target.doctype) | ||||
me.target.parse_validate_and_set_in_model(value); | me.target.parse_validate_and_set_in_model(value); | ||||
@@ -110,8 +110,8 @@ frappe.ui.form.LinkSelector = class LinkSelector { | |||||
me.dialog.hide(); | me.dialog.hide(); | ||||
} | } | ||||
return false; | return false; | ||||
}) | |||||
}) | |||||
}); | |||||
}); | |||||
} else { | } else { | ||||
$('<p><br><span class="text-muted">' + __("No Results") + '</span>' | $('<p><br><span class="text-muted">' + __("No Results") + '</span>' | ||||
+ (frappe.model.can_create(me.doctype) ? | + (frappe.model.can_create(me.doctype) ? | ||||
@@ -130,49 +130,56 @@ frappe.ui.form.LinkSelector = class LinkSelector { | |||||
}, this.dialog.get_primary_btn()); | }, 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(); | |||||
}); | |||||
} | |||||
}); | |||||
} | } | ||||
}; | }; | ||||
@@ -952,9 +952,16 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
get_indicator_html(doc) { | get_indicator_html(doc) { | ||||
const indicator = frappe.get_indicator(doc, this.doctype); | 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) { | if (indicator) { | ||||
return `<span class="indicator-pill ${indicator[1]} filterable ellipsis" | return `<span class="indicator-pill ${indicator[1]} filterable ellipsis" | ||||
data-filter='${indicator[2]}'> | |||||
data-filter='${indicator[2]}' title='${title}'> | |||||
<span class="ellipsis"> ${__(indicator[0])}</span> | <span class="ellipsis"> ${__(indicator[0])}</span> | ||||
<span>`; | <span>`; | ||||
} | } | ||||
@@ -175,4 +175,4 @@ | |||||
@keyframes blink { | @keyframes blink { | ||||
50% { opacity: 0.5; } | 50% { opacity: 0.5; } | ||||
} | |||||
} |
@@ -6,12 +6,13 @@ import frappe | |||||
from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url | 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 validate_url, validate_email_address | ||||
from frappe.utils import ceil, floor | 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 PIL import Image | ||||
from frappe.utils.image import strip_exif_data, optimize_image | from frappe.utils.image import strip_exif_data, optimize_image | ||||
import io | import io | ||||
from mimetypes import guess_type | from mimetypes import guess_type | ||||
from datetime import datetime, timedelta, date | |||||
class TestFilters(unittest.TestCase): | class TestFilters(unittest.TestCase): | ||||
def test_simple_dict(self): | 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('style="background-image: url(\'{0}/assets/frappe/bg.jpg\') !important"'.format(url) in html) | ||||
self.assertTrue('<a href="mailto:test@example.com">email</a>' in html) | self.assertTrue('<a href="mailto:test@example.com">email</a>' 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): | class TestMathUtils(unittest.TestCase): | ||||
def test_floor(self): | def test_floor(self): | ||||
from decimal import Decimal | from decimal import Decimal | ||||
@@ -205,7 +245,6 @@ class TestImage(unittest.TestCase): | |||||
self.assertLess(len(optimized_content), len(original_content)) | self.assertLess(len(optimized_content), len(original_content)) | ||||
class TestPythonExpressions(unittest.TestCase): | class TestPythonExpressions(unittest.TestCase): | ||||
def test_validation_for_good_python_expression(self): | def test_validation_for_good_python_expression(self): | ||||
valid_expressions = [ | valid_expressions = [ | ||||
"foo == bar", | "foo == bar", | ||||
@@ -229,4 +268,4 @@ class TestPythonExpressions(unittest.TestCase): | |||||
"oops = forgot_equals", | "oops = forgot_equals", | ||||
] | ] | ||||
for expr in invalid_expressions: | for expr in invalid_expressions: | ||||
self.assertRaises(frappe.ValidationError, validate_python_code, expr) | |||||
self.assertRaises(frappe.ValidationError, validate_python_code, expr) |
@@ -1,6 +1,7 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# MIT License. See license.txt | # MIT License. See license.txt | ||||
from typing import Optional | |||||
import frappe | import frappe | ||||
import operator | import operator | ||||
import json | import json | ||||
@@ -8,6 +9,7 @@ import re, datetime, math, time | |||||
from code import compile_command | from code import compile_command | ||||
from urllib.parse import quote, urljoin | from urllib.parse import quote, urljoin | ||||
from frappe.desk.utils import slug | from frappe.desk.utils import slug | ||||
from click import secho | |||||
DATE_FORMAT = "%Y-%m-%d" | DATE_FORMAT = "%Y-%m-%d" | ||||
TIME_FORMAT = "%H:%M:%S.%f" | TIME_FORMAT = "%H:%M:%S.%f" | ||||
@@ -16,10 +18,10 @@ DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT | |||||
def is_invalid_date_string(date_string): | def is_invalid_date_string(date_string): | ||||
# dateutil parser does not agree with dates like "0001-01-01" or "0000-00-00" | # 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 | # datetime functions | ||||
def getdate(string_date=None): | |||||
def getdate(string_date: Optional[str] = None): | |||||
""" | """ | ||||
Converts string date (yyyy-mm-dd) to datetime.date object. | Converts string date (yyyy-mm-dd) to datetime.date object. | ||||
If no input is provided, current date is returned. | If no input is provided, current date is returned. | ||||
@@ -67,6 +69,31 @@ def get_datetime(datetime_str=None): | |||||
except ValueError: | except ValueError: | ||||
return parser.parse(datetime_str) | 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): | def to_timedelta(time_str): | ||||
from dateutil import parser | 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""" | """Returns truthy value if there are common elements in lists l1 and l2""" | ||||
return set(l1) & set(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"): | if fieldtype in ("Currency", "Float", "Percent"): | ||||
value = flt(value) | value = flt(value) | ||||
@@ -527,6 +561,46 @@ def cast_fieldtype(fieldtype, value): | |||||
return 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): | def flt(s, precision=None): | ||||
"""Convert to float (ignoring commas in string) | """Convert to float (ignoring commas in string) | ||||
@@ -1202,7 +1276,7 @@ def evaluate_filters(doc, filters): | |||||
def compare(val1, condition, val2, fieldtype=None): | def compare(val1, condition, val2, fieldtype=None): | ||||
ret = False | ret = False | ||||
if fieldtype: | if fieldtype: | ||||
val2 = cast_fieldtype(fieldtype, val2) | |||||
val2 = cast(fieldtype, val2) | |||||
if condition in operator_map: | if condition in operator_map: | ||||
ret = operator_map[condition](val1, val2) | ret = operator_map[condition](val1, val2) | ||||
@@ -30,8 +30,14 @@ class NamespaceDict(frappe._dict): | |||||
def safe_exec(script, _globals=None, _locals=None): | 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) | frappe.throw(_('Please Enable Server Scripts'), ServerScriptNotEnabled) | ||||
# build globals | # build globals | ||||
@@ -228,6 +234,7 @@ VALID_UTILS = ( | |||||
"getdate", | "getdate", | ||||
"get_datetime", | "get_datetime", | ||||
"to_timedelta", | "to_timedelta", | ||||
"get_timedelta", | |||||
"add_to_date", | "add_to_date", | ||||
"add_days", | "add_days", | ||||
"add_months", | "add_months", | ||||