Selaa lähdekoodia

Revert "fix(doc)!: Always cast datetime, date and time fields"

Revert "fix(doc)!: Always cast datetime, date and time fields (#15891)"

This reverts commit d7789ab6ff.
version-14
Ankush Menat 2 vuotta sitten
committed by GitHub
vanhempi
commit
261fbfcd11
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
12 muutettua tiedostoa jossa 133 lisäystä ja 368 poistoa
  1. +4
    -12
      frappe/automation/doctype/assignment_rule/test_assignment_rule.py
  2. +4
    -4
      frappe/automation/doctype/auto_repeat/test_auto_repeat.py
  3. +1
    -20
      frappe/database/mariadb/database.py
  4. +2
    -12
      frappe/database/postgres/database.py
  5. +3
    -1
      frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py
  6. +21
    -116
      frappe/model/base_document.py
  7. +6
    -0
      frappe/model/create_new.py
  8. +51
    -70
      frappe/model/document.py
  9. +0
    -26
      frappe/model/meta.py
  10. +4
    -66
      frappe/tests/test_document.py
  11. +31
    -39
      frappe/utils/data.py
  12. +6
    -2
      frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py

+ 4
- 12
frappe/automation/doctype/assignment_rule/test_assignment_rule.py Näytä tiedosto

@@ -257,17 +257,13 @@ class TestAutoAssign(unittest.TestCase):
)[0]

note1_todo_doc = frappe.get_doc("ToDo", note1_todo.name)
self.assertEqual(
frappe.utils.get_date_str(note1_todo_doc.date), frappe.utils.get_date_str(expiry_date)
)
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date)

# due date should be updated if the reference doc's date is updated.
note1.expiry_date = frappe.utils.add_days(expiry_date, 2)
note1.save()
note1_todo_doc.reload()
self.assertEqual(
frappe.utils.get_date_str(note1_todo_doc.date), frappe.utils.get_date_str(note1.expiry_date)
)
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date)

# saving one note's expiry should not update other note todo's due date
note2_todo = frappe.get_all(
@@ -275,12 +271,8 @@ class TestAutoAssign(unittest.TestCase):
filters=dict(reference_type="Note", reference_name=note2.name, status="Open"),
fields=["name", "date"],
)[0]
self.assertNotEqual(
frappe.utils.get_date_str(note2_todo.date), frappe.utils.get_date_str(note1.expiry_date)
)
self.assertEqual(
frappe.utils.get_date_str(note2_todo.date), frappe.utils.get_date_str(expiry_date)
)
self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date)
self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date)
assignment_rule.delete()




+ 4
- 4
frappe/automation/doctype/auto_repeat/test_auto_repeat.py Näytä tiedosto

@@ -9,7 +9,7 @@ from frappe.automation.doctype.auto_repeat.auto_repeat import (
week_map,
)
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.utils import add_days, add_months, cast, getdate, today
from frappe.utils import add_days, add_months, getdate, today


def add_custom_fields():
@@ -39,7 +39,7 @@ class TestAutoRepeat(unittest.TestCase):
).insert()

doc = make_auto_repeat(reference_document=todo.name)
self.assertEqual(doc.next_schedule_date, cast("Date", today()))
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
@@ -67,7 +67,7 @@ class TestAutoRepeat(unittest.TestCase):
start_date=add_days(today(), -7),
)

self.assertEqual(doc.next_schedule_date, cast("Date", today()))
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
@@ -99,7 +99,7 @@ class TestAutoRepeat(unittest.TestCase):
days=days,
)

self.assertEqual(doc.next_schedule_date, cast("Date", today()))
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()


+ 1
- 20
frappe/database/mariadb/database.py Näytä tiedosto

@@ -1,5 +1,3 @@
import datetime

import pymysql
from pymysql.constants import ER, FIELD_TYPE
from pymysql.converters import conversions, escape_string
@@ -7,15 +5,7 @@ from pymysql.converters import conversions, escape_string
import frappe
from frappe.database.database import Database
from frappe.database.mariadb.schema import MariaDBTable
from frappe.utils import (
UnicodeWithAttrs,
cstr,
get_date_str,
get_datetime,
get_datetime_str,
get_table_name,
get_time_str,
)
from frappe.utils import UnicodeWithAttrs, cstr, get_datetime, get_table_name


class MariaDBDatabase(Database):
@@ -133,15 +123,6 @@ class MariaDBDatabase(Database):
# pymysql expects unicode argument to escape_string with Python 3
s = frappe.as_unicode(escape_string(frappe.as_unicode(s)), "utf-8").replace("`", "\\`")

if isinstance(s, datetime.datetime):
s = get_datetime_str(s)

elif isinstance(s, datetime.date):
s = get_date_str(s)

elif isinstance(s, datetime.timedelta):
s = get_time_str(s)

# NOTE separating % escape, because % escape should only be done when using LIKE operator
# or when you use python format string to generate query that already has a %s
# for example: sql("select name from `tabUser` where name=%s and {0}".format(conditions), something)


+ 2
- 12
frappe/database/postgres/database.py Näytä tiedosto

@@ -1,4 +1,3 @@
import datetime
import re

import psycopg2
@@ -9,7 +8,7 @@ from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
import frappe
from frappe.database.database import Database
from frappe.database.postgres.schema import PostgresTable
from frappe.utils import cstr, get_date_str, get_datetime_str, get_table_name, get_time_str
from frappe.utils import cstr, get_table_name

# cast decimals as floats
DEC2FLOAT = psycopg2.extensions.new_type(
@@ -96,15 +95,6 @@ class PostgresDatabase(Database):
if isinstance(s, bytes):
s = s.decode("utf-8")

if isinstance(s, datetime.datetime):
s = get_datetime_str(s)

elif isinstance(s, datetime.date):
s = get_date_str(s)

elif isinstance(s, datetime.timedelta):
s = get_time_str(s)

# MariaDB's driver treats None as an empty string
# So Postgres should do the same

@@ -402,7 +392,7 @@ def modify_query(query):
# only find int (with/without signs), ignore decimals (with/without signs), ignore hashes (which start with numbers),
# drop .0 from decimals and add quotes around them
#
# >>> query = "c='abcd' , a >= 45, b = -45.0, c = 40, d=4500.0, e=3500.53, f=40psdfsd, g=9092094312, h=12.00023"
# >>> query = "c='abcd' , a >= 45, b = -45.0, c = 40, d=4500.0, e=3500.53, f=40psdfsd, g=9092094312, h=12.00023"
# >>> re.sub(r"([=><]+)\s*([+-]?\d+)(\.0)?(?![a-zA-Z\.\d])", r"\1 '\2'", query)
# "c='abcd' , a >= '45', b = '-45', c = '40', d= '4500', e=3500.53, f=40psdfsd, g= '9092094312', h=12.00023



+ 3
- 1
frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py Näytä tiedosto

@@ -8,4 +8,6 @@ from frappe.model.document import Document
class OAuthBearerToken(Document):
def validate(self):
if not self.expiration_time:
self.expiration_time = self.creation + frappe.utils.datetime.timedelta(seconds=self.expires_in)
self.expiration_time = frappe.utils.datetime.datetime.strptime(
self.creation, "%Y-%m-%d %H:%M:%S.%f"
) + frappe.utils.datetime.timedelta(seconds=self.expires_in)

+ 21
- 116
frappe/model/base_document.py Näytä tiedosto

@@ -2,7 +2,6 @@
# License: MIT. See LICENSE
import datetime
import json
from typing import Any

import frappe
from frappe import _, _dict
@@ -18,18 +17,7 @@ from frappe.model.docstatus import DocStatus
from frappe.model.naming import set_new_name
from frappe.model.utils.link_count import notify_link_count
from frappe.modules import load_doctype_module
from frappe.utils import (
DEFAULT_DATETIME_SHORTCUTS,
cast,
cast_fieldtype,
cint,
cstr,
flt,
now,
now_datetime,
sanitize_html,
strip_html,
)
from frappe.utils import cast_fieldtype, cint, cstr, flt, now, sanitize_html, strip_html
from frappe.utils.html_utils import unescape_html

max_positive_value = {"smallint": 2**15, "int": 2**31, "bigint": 2**63}
@@ -106,40 +94,19 @@ class BaseDocument:
"_table_fieldnames",
"_reserved_keywords",
"dont_update_if_missing",
"_datetime_fieldnames",
"_date_fieldnames",
"_time_fieldnames",
}

def __init__(self, doc):
if doc.get("doctype"):
self.doctype = doc["doctype"]
def __init__(self, d):
if d.get("doctype"):
self.doctype = d["doctype"]

self._table_fieldnames = (
doc["_table_fieldnames"]
if "_table_fieldnames" in doc
d["_table_fieldnames"] # from cache
if "_table_fieldnames" in d
else {df.fieldname for df in self._get_table_fields()}
)

self._datetime_fieldnames = (
doc["_datetime_fieldnames"]
if "_datetime_fieldnames" in doc
else {df.fieldname for df in self._get_datetime_fields()}
)

self._date_fieldnames = (
doc["_date_fieldnames"]
if "_date_fieldnames" in doc
else {df.fieldname for df in self._get_date_fields()}
)

self._time_fieldnames = (
doc["_time_fieldnames"]
if "_time_fieldnames" in doc
else {df.fieldname for df in self._get_time_fields()}
)

self.update(doc)
self.update(d)
self.dont_update_if_missing = []

if hasattr(self, "__setup__"):
@@ -171,30 +138,14 @@ class BaseDocument:

state.pop("_meta", None)

def __setattr__(self, __name: str, __value: Any) -> None:
"""
Cast datetime/date/time/string value to datetime, date and time respectively for Datetime, Date and Time fields only.
"""
if __value in DEFAULT_DATETIME_SHORTCUTS:
__value = now_datetime()

if hasattr(self, "_datetime_fieldnames") and __value and __name in self._datetime_fieldnames:
__value = cast("Datetime", __value)
elif hasattr(self, "_date_fieldnames") and __value and __name in self._date_fieldnames:
__value = cast("Date", __value)
elif hasattr(self, "_time_fieldnames") and __value and __name in self._time_fieldnames:
__value = cast("Time", __value)

self.__dict__[__name] = __value

def update(self, d):
"""Update multiple fields of a doctype using a dictionary of key-value pairs.

Example:
doc.update({
doc.update({
"user": "admin",
"balance": 42000
})
})
"""

# set name first, as it is used a reference in child document
@@ -249,9 +200,6 @@ class BaseDocument:
if key in self._reserved_keywords:
return

if value in DEFAULT_DATETIME_SHORTCUTS:
value = now_datetime()

if not as_value and key in self._table_fieldnames:
self.__dict__[key] = []

@@ -260,12 +208,6 @@ class BaseDocument:
self.extend(key, value)

return
elif value and key in self._datetime_fieldnames:
value = cast("Datetime", value)
elif value and key in self._date_fieldnames:
value = cast("Date", value)
elif value and key in self._time_fieldnames:
value = cast("Time", value)

self.__dict__[key] = value

@@ -277,11 +219,11 @@ class BaseDocument:
"""Append an item to a child table.

Example:
doc.append("childtable", {
doc.append("childtable", {
"child_table_field": "value",
"child_table_int_field": 0,
...
})
})
"""
if value is None:
value = {}
@@ -353,45 +295,6 @@ class BaseDocument:

return self.meta.get_table_fields()

def _get_datetime_fields(self):
"""
To get datetime fields during Document init
"""
if (
self.doctype == "DocType"
or self.doctype in DOCTYPES_FOR_DOCTYPE
or getattr(self, "parentfield", None)
):
return ()

return self.meta.get_datetime_fields(with_standard_datetime_fields=True)

def _get_date_fields(self):
"""
To get date fields during Document init
"""
if (
self.doctype == "DocType"
or self.doctype in DOCTYPES_FOR_DOCTYPE
or getattr(self, "parentfield", None)
):
return ()

return self.meta.get_date_fields()

def _get_time_fields(self):
"""
To get time fields during Document init
"""
if (
self.doctype == "DocType"
or self.doctype in DOCTYPES_FOR_DOCTYPE
or getattr(self, "parentfield", None)
):
return ()

return self.meta.get_time_fields()

def get_valid_dict(
self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False
) -> dict:
@@ -565,7 +468,9 @@ class BaseDocument:
"""INSERT the document (with valid columns) in the database.

args:
ignore_if_duplicate: ignore primary key collision at database level (postgres) in python (mariadb)
ignore_if_duplicate: ignore primary key collision
at database level (postgres)
in python (mariadb)
"""
if not self.name:
# name will be set by document class in most cases
@@ -688,13 +593,13 @@ class BaseDocument:
This function returns the `column_name` associated with the `key_name` passed

Args:
key_name (str): The name of the database index.
key_name (str): The name of the database index.

Raises:
IndexError: If the key is not found in the table.
IndexError: If the key is not found in the table.

Returns:
str: The column name associated with the key.
str: The column name associated with the key.
"""
return frappe.db.sql(
f"""
@@ -715,10 +620,10 @@ class BaseDocument:
"""Returns the associated label for fieldname

Args:
fieldname (str): The fieldname in the DocType to use to pull the label.
fieldname (str): The fieldname in the DocType to use to pull the label.

Returns:
str: The label associated with the fieldname, if found, otherwise `None`.
str: The label associated with the fieldname, if found, otherwise `None`.
"""
df = self.meta.get_field(fieldname)
if df:
@@ -1205,9 +1110,9 @@ class BaseDocument:
Print Hide can be set via the Print Format Builder or in the controller as a list
of hidden fields. Example

class MyDoc(Document):
class MyDoc(Document):
def __setup__(self):
self.print_hide = ["field1", "field2"]
self.print_hide = ["field1", "field2"]

:param fieldname: Fieldname to be checked if hidden.
"""


+ 6
- 0
frappe/model/create_new.py Näytä tiedosto

@@ -145,6 +145,12 @@ def set_dynamic_default_values(doc, parent_doc, parentfield):
if default_value is not None and not doc.get(df.fieldname):
doc[df.fieldname] = default_value

elif df.fieldtype == "Datetime" and df.default.lower() == "now":
doc[df.fieldname] = now_datetime()

if df.fieldtype == "Time":
doc[df.fieldname] = nowtime()

if parent_doc:
doc["parent"] = parent_doc.name
doc["parenttype"] = parent_doc.doctype


+ 51
- 70
frappe/model/document.py Näytä tiedosto

@@ -16,7 +16,7 @@ from frappe.model.base_document import BaseDocument, get_controller
from frappe.model.docstatus import DocStatus
from frappe.model.naming import set_new_name, validate_name
from frappe.model.workflow import set_workflow_state_on_action, validate_workflow
from frappe.utils import cast, cstr, date_diff, file_lock, flt, get_datetime_str, now
from frappe.utils import cstr, date_diff, file_lock, flt, get_datetime_str, now
from frappe.utils.data import get_absolute_url
from frappe.utils.global_search import update_global_search

@@ -30,23 +30,23 @@ def get_doc(*args, **kwargs):

There are multiple ways to call `get_doc`

1. will fetch the latest user object (with child table) from the database
user = get_doc("User", "test@example.com")
# will fetch the latest user object (with child table) from the database
user = get_doc("User", "test@example.com")

2. create a new object
user = get_doc({
# create a new object
user = get_doc({
"doctype":"User"
"email_id": "test@example.com",
"roles: [
{"role": "System Manager"}
{"role": "System Manager"}
]
})
})

3. create new object with keyword arguments
user = get_doc(doctype='User', email_id='test@example.com')
# create new object with keyword arguments
user = get_doc(doctype='User', email_id='test@example.com')

4. select a document for update
user = get_doc("User", "test@example.com", for_update=True)
# select a document for update
user = get_doc("User", "test@example.com", for_update=True)
"""
if args:
if isinstance(args[0], BaseDocument):
@@ -145,8 +145,7 @@ class Document(BaseDocument):
)
if not d:
frappe.throw(
_("{0} {1} not found").format(_(self.doctype), self.name),
frappe.DoesNotExistError,
_("{0} {1} not found").format(_(self.doctype), self.name), frappe.DoesNotExistError
)

super().__init__(d)
@@ -400,29 +399,22 @@ class Document(BaseDocument):

if rows:
# select rows that do not match the ones in the document
Table = frappe.qb.DocType(df.options)

deleted_rows = (
frappe.qb.from_(Table)
.select(Table.name)
.where(
(Table.parent == self.name)
& (Table.parenttype == self.doctype)
& (Table.parentfield == fieldname)
& (Table.name.notin(rows))
)
.run(pluck=True)
deleted_rows = frappe.db.sql(
"""select name from `tab{}` where parent=%s
and parenttype=%s and parentfield=%s
and name not in ({})""".format(
df.options, ",".join(["%s"] * len(rows))
),
[self.name, self.doctype, fieldname] + rows,
)

if deleted_rows:
if len(deleted_rows) > 0:
# delete rows that do not match the ones in the document
frappe.db.delete(df.options, {"name": ("in", deleted_rows)})
frappe.db.delete(df.options, {"name": ("in", tuple(row[0] for row in deleted_rows))})

else:
# no rows found, delete all rows
frappe.db.delete(
df.options,
{"parent": self.name, "parenttype": self.doctype, "parentfield": fieldname},
df.options, {"parent": self.name, "parenttype": self.doctype, "parentfield": fieldname}
)

def get_doc_before_save(self):
@@ -486,10 +478,11 @@ class Document(BaseDocument):
frappe.db.delete("Singles", {"doctype": self.doctype})
for field, value in d.items():
if field != "doctype":
Singles = frappe.qb.DocType("Singles")
frappe.qb.into(Singles).columns(Singles.doctype, Singles.field, Singles.value).insert(
self.doctype, field, value
).run()
frappe.db.sql(
"""insert into `tabSingles` (doctype, field, value)
values (%s, %s, %s)""",
(self.doctype, field, value),
)

if self.doctype in frappe.db.value_cache:
del frappe.db.value_cache[self.doctype]
@@ -547,7 +540,6 @@ class Document(BaseDocument):
d._extract_images_from_text_editor()
d._sanitize_content()
d._save_passwords()

if self.is_new():
# don't set fields like _assign, _comments for new doc
for fieldname in optional_fields:
@@ -664,12 +656,12 @@ class Document(BaseDocument):
has_access_to = self.get_permlevel_access("read")

for df in self.meta.fields:
if df.permlevel and df.permlevel not in has_access_to:
if df.permlevel and not df.permlevel in has_access_to:
self.set(df.fieldname, None)

for table_field in self.meta.get_table_fields():
for df in frappe.get_meta(table_field.options).fields or []:
if df.permlevel and df.permlevel not in has_access_to:
if df.permlevel and not df.permlevel in has_access_to:
for child in self.get(table_field.fieldname) or []:
child.set(df.fieldname, None)

@@ -753,35 +745,32 @@ class Document(BaseDocument):
self._action = "save"
if not self.get("__islocal") and not self.meta.get("is_virtual"):
if self.meta.issingle:
Singles = frappe.qb.DocType("Singles")
modified = (
frappe.qb.from_(Singles)
.select(Singles.value)
.where((Singles.doctype == self.doctype) & (Singles.field == "modified"))
.for_update()
.run(pluck=True)
modified = frappe.db.sql(
"""select value from tabSingles
where doctype=%s and field='modified' for update""",
self.doctype,
)
modified = modified and modified[0]

if modified and cast("Datetime", modified) != cast("Datetime", self._original_modified):
modified = modified and modified[0][0]
if modified and modified != cstr(self._original_modified):
conflict = True
else:
Table = frappe.qb.DocType(self.doctype)
tmp = (
frappe.qb.from_(Table)
.select(Table.modified, Table.docstatus)
.where(Table.name == self.name)
.for_update()
.run(as_dict=True)
tmp = frappe.db.sql(
"""select modified, docstatus from `tab{}`
where name = %s for update""".format(
self.doctype
),
self.name,
as_dict=True,
)

if not tmp:
frappe.throw(_("Record does not exist"))
else:
tmp = tmp[0]

tmp = tmp[0]
modified = tmp.modified
modified = cstr(tmp.modified)

if modified and cast("Datetime", modified) != cast("Datetime", self._original_modified):
if modified and modified != cstr(self._original_modified):
conflict = True

self.check_docstatus_transition(tmp.docstatus)
@@ -789,7 +778,7 @@ class Document(BaseDocument):
if conflict:
frappe.msgprint(
_("Error: Document has been modified after you have opened it")
+ (f" ({cstr(modified)}, {cstr(self.modified)}). ")
+ (f" ({modified}, {self.modified}). ")
+ _("Please refresh to get the latest document."),
raise_exception=frappe.TimestampMismatchError,
)
@@ -973,7 +962,7 @@ class Document(BaseDocument):
return

def _evaluate_alert(alert):
if alert.name not in self.flags.notifications_executed:
if not alert.name in self.flags.notifications_executed:
evaluate_alert(self, alert.name, alert.event)
self.flags.notifications_executed.append(alert.name)

@@ -1123,11 +1112,7 @@ class Document(BaseDocument):
"""Clear _seen property and set current user as seen"""
if getattr(self.meta, "track_seen", False):
frappe.db.set_value(
self.doctype,
self.name,
"_seen",
json.dumps([frappe.session.user]),
update_modified=False,
self.doctype, self.name, "_seen", json.dumps([frappe.session.user]), update_modified=False
)

def notify_update(self):
@@ -1321,8 +1306,7 @@ class Document(BaseDocument):
if not (isinstance(self.get(parentfield), list) and len(self.get(parentfield)) > 0):
label = self.meta.get_label(parentfield)
frappe.throw(
_("Table {0} cannot be empty").format(label),
raise_exception or frappe.EmptyTableError,
_("Table {0} cannot be empty").format(label), raise_exception or frappe.EmptyTableError
)

def round_floats_in(self, doc, fieldnames=None):
@@ -1337,10 +1321,7 @@ class Document(BaseDocument):
)

for fieldname in fieldnames:
doc.set(
fieldname,
flt(doc.get(fieldname), self.precision(fieldname, doc.get("parentfield"))),
)
doc.set(fieldname, flt(doc.get(fieldname), self.precision(fieldname, doc.get("parentfield"))))

def get_url(self):
"""Returns Desk URL for this document."""


+ 0
- 26
frappe/model/meta.py Näytä tiedosto

@@ -17,7 +17,6 @@ Example:
import json
import os
from datetime import datetime
from typing import Dict, List

import click

@@ -100,10 +99,6 @@ class Meta(Document):
frappe._dict(fieldname="creation", fieldtype="Datetime"),
frappe._dict(fieldname="owner", fieldtype="Data"),
]
standard_datetime_fields = [
frappe._dict(fieldname="creation", fieldtype="Datetime"),
frappe._dict(fieldname="modified", fieldtype="Datetime"),
]

def __init__(self, doctype):
self._fields = {}
@@ -659,27 +654,6 @@ class Meta(Document):
def is_nested_set(self):
return self.has_field("lft") and self.has_field("rgt")

def get_date_fields(self) -> list[dict[str, str]]:
if not hasattr(self, "_date_fields"):
self._date_fields = self.get("fields", {"fieldtype": "Date"})

return self._date_fields

def get_time_fields(self) -> list[dict[str, str]]:
if not hasattr(self, "_time_fields"):
self._time_fields = self.get("fields", {"fieldtype": "Time"})

return self._time_fields

def get_datetime_fields(self, with_standard_datetime_fields=False) -> list[dict[str, str]]:
if not hasattr(self, "_datetime_fields"):
self._datetime_fields = self.get("fields", {"fieldtype": "Datetime"}, default=[])

if with_standard_datetime_fields:
self._datetime_fields += self.standard_datetime_fields

return self._datetime_fields


#######



+ 4
- 66
frappe/tests/test_document.py Näytä tiedosto

@@ -2,14 +2,14 @@
# License: MIT. See LICENSE
import unittest
from contextlib import contextmanager
from datetime import datetime, timedelta
from datetime import timedelta
from unittest.mock import patch

import frappe
from frappe.app import make_form_dict
from frappe.desk.doctype.note.note import Note
from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last
from frappe.utils import cast, cint, get_datetime, now_datetime, set_request
from frappe.utils import cint, now_datetime, set_request
from frappe.website.serve import get_response

from . import update_system_settings
@@ -228,11 +228,7 @@ class TestDocument(unittest.TestCase):
frappe.delete_doc_if_exists("Currency", "Frappe Coin", 1)

d = frappe.get_doc(
{
"doctype": "Currency",
"currency_name": "Frappe Coin",
"smallest_currency_fraction_value": -1,
}
{"doctype": "Currency", "currency_name": "Frappe Coin", "smallest_currency_fraction_value": -1}
)

self.assertRaises(frappe.NonNegativeError, d.insert)
@@ -251,12 +247,7 @@ class TestDocument(unittest.TestCase):
"module": "Custom",
"custom": 1,
"fields": [
{
"label": "Currency",
"fieldname": "currency",
"reqd": 1,
"fieldtype": "Currency",
},
{"label": "Currency", "fieldname": "currency", "reqd": 1, "fieldtype": "Currency"},
],
}
).insert(ignore_if_duplicate=True)
@@ -382,59 +373,6 @@ class TestDocument(unittest.TestCase):
except Exception as e:
self.fail(f"Invalid doc hook: {doctype}:{hook}\n{e}")

def test_date_casting(self):
create_time_custom_field()

_datetime_str = "2022-02-13 12:02:33.713418"
_datetime = get_datetime(_datetime_str)

# Check if the system parses the string for date and time
todo = frappe.get_doc(
{
"doctype": "ToDo",
"description": "test_date_and_time_casting",
"date": _datetime_str,
"time": _datetime_str,
}
).insert()

self.assertEqual(todo.date, cast("Date", _datetime_str))
self.assertEqual(todo.time, cast("Time", _datetime_str))

# Check if the system parses the datetime object for date and time
todo.date = _datetime
todo.time = _datetime
todo.save()

self.assertEqual(todo.date, cast("Date", _datetime))
self.assertEqual(todo.time, cast("Time", _datetime))

# Check if the system parses the datetime object for date and time
todo.date = None
todo.time = None
todo.save()

self.assertEqual(todo.date, None)
self.assertEqual(todo.time, None)

# Check for standard datetime fields
self.assertIsInstance(todo.creation, datetime)
self.assertIsInstance(todo.modified, datetime)


def create_time_custom_field():
if not frappe.db.exists({"doctype": "Custom Field", "dt": "ToDo", "fieldname": "time"}):
frappe.get_doc(
{
"doctype": "Custom Field",
"label": "Time",
"dt": "ToDo",
"fieldname": "time",
"fieldtype": "Time",
"insert_after": "date",
}
).insert()


class TestDocumentWebView(unittest.TestCase):
def get(self, path, user="Guest"):


+ 31
- 39
frappe/utils/data.py Näytä tiedosto

@@ -130,13 +130,13 @@ def get_timedelta(time: str | None = None) -> datetime.timedelta | None:
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
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
datetime.timedelta: Timedelta object equivalent of the passed `time` string
"""
from dateutil import parser
from dateutil.parser import ParserError
@@ -173,15 +173,6 @@ def to_timedelta(time_str: str | datetime.time) -> datetime.timedelta:
return time_str


def to_datetime(
datetime_str: str | datetime.datetime, format: str | None = None
) -> datetime.datetime:
if isinstance(datetime_str, datetime.datetime):
return datetime_str
elif isinstance(datetime_str, str):
return datetime.datetime.strptime(datetime_str, format or DATETIME_FORMAT)


@typing.overload
def add_to_date(
date,
@@ -849,12 +840,12 @@ def cast(fieldtype, value=None):
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",)
* 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)
@@ -962,11 +953,13 @@ def floor(s):

Parameters
----------
s : int or str or Decimal object. The mathematical value to be floored.
s : int or str or Decimal object
The mathematical value to be floored

Returns
-------
int: number representing the largest integer less than or equal to the specified number
int
number representing the largest integer less than or equal to the specified number

"""
try:
@@ -982,11 +975,13 @@ def ceil(s):

Parameters
----------
s : int or str or Decimal object. The mathematical value to be ceiled
s : int or str or Decimal object
The mathematical value to be ceiled

Returns
-------
int: smallest integer greater than or equal to the given number
int
smallest integer greater than or equal to the given number

"""
try:
@@ -1003,15 +998,15 @@ def cstr(s, encoding="utf-8"):
def sbool(x: str) -> bool | Any:
"""Converts str object to Boolean if possible.
Example:
"true" becomes True
"1" becomes True
"{}" remains "{}"
"true" becomes True
"1" becomes True
"{}" remains "{}"

Args:
x (str): String to be converted to Bool
x (str): String to be converted to Bool

Returns:
object: Returns Boolean or x
object: Returns Boolean or x
"""
try:
val = x.lower()
@@ -1705,11 +1700,11 @@ def get_filter(doctype: str, f: dict | list | tuple, filters_config=None) -> "fr
"""Returns a _dict like

{
"doctype":
"fieldname":
"operator":
"value":
"fieldtype":
"doctype":
"fieldname":
"operator":
"value":
"fieldtype":
}
"""
from frappe.model import child_table_fields, default_fields, optional_fields
@@ -2061,8 +2056,8 @@ def validate_python_code(
"""Validate python code fields by using compile_command to ensure that expression is valid python.

args:
fieldname: name of field being validated.
is_expression: true for validating simple single line python expression, else validated as script.
fieldname: name of field being validated.
is_expression: true for validating simple single line python expression, else validated as script.
"""

if not string:
@@ -2119,6 +2114,3 @@ def parse_timedelta(s: str) -> datetime.timedelta:
m = TIMEDELTA_BASE_PATTERN.match(s)

return datetime.timedelta(**{key: float(val) for key, val in m.groupdict().items()})


DEFAULT_DATETIME_SHORTCUTS = ["Today", "__today", "now", "Now"]

+ 6
- 2
frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py Näytä tiedosto

@@ -49,7 +49,9 @@ class TestPersonalDataDeletionRequest(unittest.TestCase):
self.assertEqual(self.delete_request.status, "Deleted")

def test_unverified_record_removal(self):
date_time_obj = self.delete_request.creation + timedelta(days=-7)
date_time_obj = datetime.strptime(
self.delete_request.creation, "%Y-%m-%d %H:%M:%S.%f"
) + timedelta(days=-7)
self.delete_request.db_set("creation", date_time_obj)
self.delete_request.db_set("status", "Pending Verification")

@@ -58,7 +60,9 @@ class TestPersonalDataDeletionRequest(unittest.TestCase):

def test_process_auto_request(self):
frappe.db.set_value("Website Settings", None, "auto_account_deletion", "1")
date_time_obj = self.delete_request.creation + timedelta(hours=-2)
date_time_obj = datetime.strptime(
self.delete_request.creation, "%Y-%m-%d %H:%M:%S.%f"
) + timedelta(hours=-2)
self.delete_request.db_set("creation", date_time_obj)
self.delete_request.db_set("status", "Pending Approval")



Ladataan…
Peruuta
Tallenna