+ ${make_card ? "card-section" : ""}" data-fieldname="${this.df.fieldname}">
`).appendTo(this.parent);
- this.layout && this.layout.sections.push(this);
if (this.df) {
if (this.df.label) {
@@ -82,6 +82,12 @@ export default class Section {
}
}
+ add_field(fieldobj) {
+ this.fields_list.push(fieldobj);
+ this.fields_dict[fieldobj.fieldname] = fieldobj;
+ fieldobj.section = this.section;
+ }
+
refresh(hide) {
if (!this.df) return;
// hide if explicitly hidden
diff --git a/frappe/public/js/frappe/form/tab.js b/frappe/public/js/frappe/form/tab.js
index e5fab2d982..0a0eb8c80e 100644
--- a/frappe/public/js/frappe/form/tab.js
+++ b/frappe/public/js/frappe/form/tab.js
@@ -1,14 +1,12 @@
export default class Tab {
- constructor(parent, df, frm, tabs_list, tabs_content) {
- this.parent = parent;
+ constructor(layout, df, frm, tab_link_container, tabs_content) {
+ this.layout = layout;
this.df = df || {};
this.frm = frm;
this.doctype = this.frm.doctype;
this.label = this.df && this.df.label;
- this.tabs_list = tabs_list;
+ this.tab_link_container = tab_link_container;
this.tabs_content = tabs_content;
- this.fields_list = [];
- this.fields_dict = {};
this.make();
this.setup_listeners();
this.refresh();
@@ -16,17 +14,18 @@ export default class Tab {
make() {
const id = `${frappe.scrub(this.doctype, "-")}-${this.df.fieldname}`;
- this.parent = $(`
+ this.tab_link = $(`
${__(this.label)}
- `).appendTo(this.tabs_list);
+ `).appendTo(this.tab_link_container);
this.wrapper = $(`
`).appendTo(this.tabs_content);
@@ -38,11 +37,6 @@ export default class Tab {
// hide if explicitly hidden
let hide = this.df.hidden || this.df.hidden_due_to_dependency;
- // hide if dashboard and not saved
- if (!hide && this.df.show_dashboard && this.frm.is_new() && !this.fields_list.length) {
- hide = true;
- }
-
// hide if no read permission
if (!hide && this.frm && !this.frm.get_perm(this.df.permlevel || 0, "read")) {
hide = true;
@@ -60,29 +54,38 @@ export default class Tab {
}
}
+ // hide if dashboard and not saved
+ if (!hide && this.df.show_dashboard && this.frm.is_new()) {
+ hide = true;
+ }
+
this.toggle(!hide);
}
toggle(show) {
- this.parent.toggleClass("hide", !show);
+ this.tab_link.toggleClass("hide", !show);
this.wrapper.toggleClass("hide", !show);
- this.parent.toggleClass("show", show);
+ this.tab_link.toggleClass("show", show);
this.wrapper.toggleClass("show", show);
this.hidden = !show;
}
show() {
- this.parent.show();
+ this.tab_link.show();
}
hide() {
- this.parent.hide();
+ this.tab_link.hide();
+ }
+
+ add_field(fieldobj) {
+ fieldobj.tab = this;
}
set_active() {
- this.parent.find(".nav-link").tab("show");
- this.wrapper.addClass("active");
- this.frm?.set_active_tab?.(this);
+ this.tab_link.find(".nav-link").tab("show");
+ this.wrapper.addClass("show");
+ this.frm.active_tab = this;
}
is_active() {
@@ -90,12 +93,26 @@ export default class Tab {
}
is_hidden() {
- return this.wrapper.hasClass("hide");
+ return this.wrapper.hasClass("hide") && this.tab_link.hasClass("hide");
}
setup_listeners() {
- this.parent.find(".nav-link").on("shown.bs.tab", () => {
+ this.tab_link.find(".nav-link").on("shown.bs.tab", () => {
this?.frm.set_active_tab?.(this);
});
}
+
+ setup_switch_on_hover() {
+ this.tab_link.on("dragenter", () => {
+ this.action = setTimeout(() => {
+ this.set_active();
+ }, 2000);
+ });
+ this.tab_link.on("dragout", () => {
+ if (this.action) {
+ clearTimeout(this.action);
+ this.action = null;
+ }
+ });
+ }
}
diff --git a/frappe/test_runner.py b/frappe/test_runner.py
index e8c1656ca8..1e3573336a 100644
--- a/frappe/test_runner.py
+++ b/frappe/test_runner.py
@@ -143,7 +143,6 @@ def set_test_email_config():
"mail_server": "smtp.example.com",
"mail_login": "test@example.com",
"mail_password": "test",
- "admin_password": "admin",
}
)
diff --git a/frappe/tests/test_frappe_client.py b/frappe/tests/test_frappe_client.py
index facb0b3b72..e80e43f49c 100644
--- a/frappe/tests/test_frappe_client.py
+++ b/frappe/tests/test_frappe_client.py
@@ -2,31 +2,19 @@
# License: MIT. See LICENSE
import base64
-import unittest
import requests
import frappe
from frappe.core.doctype.user.user import generate_keys
-from frappe.frappeclient import AuthError, FrappeClient, FrappeException
+from frappe.frappeclient import FrappeClient, FrappeException
+from frappe.tests.utils import FrappeTestCase
from frappe.utils.data import get_url
-class TestFrappeClient(unittest.TestCase):
+class TestFrappeClient(FrappeTestCase):
PASSWORD = frappe.conf.admin_password or "admin"
- @classmethod
- def setUpClass(cls) -> None:
- site_url = get_url()
- try:
- FrappeClient(site_url, "Administrator", cls.PASSWORD, verify=False)
- except AuthError:
- raise unittest.SkipTest(
- f"AuthError raised for {site_url} [usr=Administrator, pwd={cls.PASSWORD}]"
- )
-
- return super().setUpClass()
-
def test_insert_many(self):
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Note", {"title": ("in", ("Sing", "a", "song", "of", "sixpence"))})
diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py
index 42094e145f..9dd8661fc4 100644
--- a/frappe/tests/test_utils.py
+++ b/frappe/tests/test_utils.py
@@ -19,30 +19,48 @@ from PIL import Image
import frappe
from frappe.installer import parse_app_name
from frappe.model.document import Document
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import (
ceil,
+ dict_to_str,
evaluate_filters,
+ execute_in_shell,
floor,
format_timedelta,
get_bench_path,
+ get_file_timestamp,
get_gravatar,
+ get_site_info,
+ get_sites,
get_url,
money_in_words,
parse_timedelta,
random_string,
+ remove_blanks,
+ safe_json_loads,
scrub_urls,
validate_email_address,
+ validate_name,
+ validate_phone_number_with_country_code,
validate_url,
)
from frappe.utils.data import (
add_to_date,
+ add_years,
cast,
+ cstr,
+ duration_to_seconds,
+ get_datetime,
get_first_day_of_week,
get_time,
get_timedelta,
+ get_timespan_date_range,
+ get_year_ending,
getdate,
now_datetime,
nowtime,
+ pretty_date,
+ to_timedelta,
validate_python_code,
)
from frappe.utils.dateutils import get_dates_from_timegrain
@@ -322,11 +340,36 @@ class TestValidationUtils(unittest.TestCase):
self.assertFalse(validate_email_address("someone"))
self.assertFalse(validate_email_address("someone@----.com"))
+ self.assertFalse(
+ validate_email_address("test@example.com test2@example.com,undisclosed-recipient")
+ )
+
# Invalid with throw
self.assertRaises(
frappe.InvalidEmailAddressError, validate_email_address, "someone.com", throw=True
)
+ def test_valid_phone(self):
+ valid_phones = ["+91 1234567890", ""]
+
+ for phone in valid_phones:
+ validate_phone_number_with_country_code(phone, "field")
+ self.assertRaises(
+ frappe.InvalidPhoneNumberError,
+ validate_phone_number_with_country_code,
+ "+420 1234567890",
+ "field",
+ )
+
+ def test_validate_name(self):
+ valid_names = ["", "abc", "asd a13", "asd-asd"]
+ for name in valid_names:
+ validate_name(name, True)
+
+ invalid_names = ["asd$wat", "asasd/ads"]
+ for name in invalid_names:
+ self.assertRaises(frappe.InvalidNameError, validate_name, name, True)
+
class TestImage(unittest.TestCase):
def test_strip_exif_data(self):
@@ -476,6 +519,79 @@ class TestDateUtils(unittest.TestCase):
self.assertIsInstance(get_timedelta(str(timedelta_input)), timedelta)
self.assertIsInstance(get_timedelta(str(time_input)), timedelta)
+ def test_to_timedelta(self):
+ self.assertEqual(to_timedelta("00:00:01"), timedelta(seconds=1))
+ self.assertEqual(to_timedelta("10:00:01"), timedelta(seconds=1, hours=10))
+ self.assertEqual(to_timedelta(time(hour=2)), timedelta(hours=2))
+
+ def test_add_date_utils(self):
+ self.assertEqual(add_years(datetime(2020, 1, 1), 1), datetime(2021, 1, 1))
+
+ def test_duration_to_sec(self):
+ self.assertEqual(duration_to_seconds("3h 34m 45s"), 12885)
+ self.assertEqual(duration_to_seconds("1h"), 3600)
+ self.assertEqual(duration_to_seconds("110m"), 110 * 60)
+ self.assertEqual(duration_to_seconds("110m"), 110 * 60)
+
+ def test_get_timespan_date_range(self):
+
+ supported_timespans = [
+ "last week",
+ "last month",
+ "last quarter",
+ "last 6 months",
+ "last year",
+ "yesterday",
+ "today",
+ "tomorrow",
+ "this week",
+ "this month",
+ "this quarter",
+ "this year",
+ "next week",
+ "next month",
+ "next quarter",
+ "next 6 months",
+ "next year",
+ ]
+
+ for ts in supported_timespans:
+ res = get_timespan_date_range(ts)
+ self.assertEqual(len(res), 2)
+
+ # Manual type checking eh?
+ self.assertIsInstance(res[0], date)
+ self.assertIsInstance(res[1], date)
+
+ def test_timesmap_utils(self):
+ self.assertEqual(get_year_ending(date(2021, 1, 1)), date(2021, 12, 31))
+ self.assertEqual(get_year_ending(date(2021, 1, 31)), date(2021, 12, 31))
+
+ def test_pretty_date(self):
+ from frappe import _
+
+ # differnt cases
+ now = get_datetime()
+
+ test_cases = {
+ now: _("just now"),
+ add_to_date(now, minutes=-1): _("1 minute ago"),
+ add_to_date(now, minutes=-3): _("3 minutes ago"),
+ add_to_date(now, hours=-1): _("1 hour ago"),
+ add_to_date(now, hours=-2): _("2 hours ago"),
+ add_to_date(now, days=-1): _("Yesterday"),
+ add_to_date(now, days=-5): _("5 days ago"),
+ add_to_date(now, days=-8): _("1 week ago"),
+ add_to_date(now, days=-14): _("2 weeks ago"),
+ add_to_date(now, days=-32): _("1 month ago"),
+ add_to_date(now, days=-32 * 2): _("2 months ago"),
+ add_to_date(now, years=-1, days=-5): _("1 year ago"),
+ add_to_date(now, years=-2, days=-10): _("2 years ago"),
+ }
+
+ for dt, exp_message in test_cases.items():
+ self.assertEqual(pretty_date(dt), exp_message)
+
def test_date_from_timegrain(self):
start_date = getdate("2021-01-01")
@@ -724,7 +840,7 @@ class TestLazyLoader(unittest.TestCase):
self.assertEqual(["Module `frappe.tests.data.load_sleep` loaded"], output)
-class TestIdenticon(unittest.TestCase):
+class TestIdenticon(FrappeTestCase):
def test_get_gravatar(self):
# developers@frappe.io has a gravatar linked so str URL will be returned
frappe.flags.in_test = False
@@ -747,3 +863,38 @@ class TestIdenticon(unittest.TestCase):
identicon_bs64 = identicon.base64()
self.assertIsInstance(identicon_bs64, str)
self.assertTrue(identicon_bs64.startswith("data:image/png;base64,"))
+
+
+class TestContainerUtils(FrappeTestCase):
+ def test_dict_to_str(self):
+ self.assertEqual(dict_to_str({"a": "b"}), "a=b")
+
+ def test_remove_blanks(self):
+ a = {"asd": "", "b": None, "c": "d"}
+ remove_blanks(a)
+ self.assertEqual(len(a), 1)
+ self.assertEqual(a["c"], "d")
+
+
+class TestMiscUtils(FrappeTestCase):
+ def test_get_file_timestamp(self):
+ self.assertIsInstance(get_file_timestamp(__file__), str)
+
+ def test_execute_in_shell(self):
+ err, out = execute_in_shell("ls")
+ self.assertIn("apps", cstr(out))
+
+ def test_get_all_sites(self):
+ self.assertIn(frappe.local.site, get_sites())
+
+ def test_get_site_info(self):
+ info = get_site_info()
+
+ installed_apps = [app["app_name"] for app in info["installed_apps"]]
+ self.assertIn("frappe", installed_apps)
+ self.assertGreaterEqual(len(info["users"]), 1)
+
+ def test_safe_json_load(self):
+ self.assertEqual(safe_json_loads("{}"), {})
+ self.assertEqual(safe_json_loads("{ /}"), "{ /}")
+ self.assertEqual(safe_json_loads("12"), 12) # this is a quirk
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index 9f25b33266..f84ad5a0da 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -9,10 +9,18 @@ import os
import re
import sys
import traceback
-from collections.abc import Generator, Iterable, MutableMapping, MutableSequence, Sequence
+from collections.abc import (
+ Container,
+ Generator,
+ Iterable,
+ MutableMapping,
+ MutableSequence,
+ Sequence,
+)
from email.header import decode_header, make_header
from email.utils import formataddr, parseaddr
from gzip import GzipFile
+from typing import Any, Literal
from urllib.parse import quote, urlparse
from redis.exceptions import ConnectionError
@@ -85,13 +93,10 @@ def get_formatted_email(user, mail=None):
def extract_email_id(email):
"""fetch only the email part of the Email Address"""
- email_id = parse_addr(email)[1]
- if email_id and isinstance(email_id, str) and not isinstance(email_id, str):
- email_id = email_id.decode("utf-8", "ignore")
- return email_id
+ return cstr(parse_addr(email)[1])
-def validate_phone_number_with_country_code(phone_number, fieldname):
+def validate_phone_number_with_country_code(phone_number: str, fieldname: str) -> None:
from phonenumbers import NumberParseException, is_valid_number, parse
from frappe import _
@@ -138,6 +143,8 @@ def validate_name(name, throw=False):
"""Returns True if the name is valid
valid names may have unicode and ascii characters, dash, quotes, numbers
anything else is considered invalid
+
+ Note: "Name" here is name of a person, not the primary key in Frappe doctypes.
"""
if not name:
return False
@@ -218,7 +225,11 @@ def split_emails(txt):
return email_list
-def validate_url(txt, throw=False, valid_schemes=None):
+def validate_url(
+ txt: str,
+ throw: bool = False,
+ valid_schemes: str | Container[str] | None = None,
+) -> bool:
"""
Checks whether `txt` has a valid URL string
@@ -244,7 +255,7 @@ def validate_url(txt, throw=False, valid_schemes=None):
return is_valid
-def random_string(length):
+def random_string(length: int) -> str:
"""generate a random string"""
import string
from random import choice
@@ -252,7 +263,7 @@ def random_string(length):
return "".join(choice(string.ascii_letters + string.digits) for i in range(length))
-def has_gravatar(email):
+def has_gravatar(email: str) -> str:
"""Returns gravatar url if user has set an avatar at gravatar.com"""
import requests
@@ -261,9 +272,7 @@ def has_gravatar(email):
# since querying gravatar for every item will be slow
return ""
- hexdigest = hashlib.md5(frappe.as_unicode(email).encode("utf-8")).hexdigest()
-
- gravatar_url = f"https://secure.gravatar.com/avatar/{hexdigest}?d=404&s=200"
+ gravatar_url = get_gravatar_url(email, "404")
try:
res = requests.get(gravatar_url)
if res.status_code == 200:
@@ -274,13 +283,12 @@ def has_gravatar(email):
return ""
-def get_gravatar_url(email):
- return "https://secure.gravatar.com/avatar/{hash}?d=mm&s=200".format(
- hash=hashlib.md5(email.encode("utf-8")).hexdigest()
- )
+def get_gravatar_url(email: str, default: Literal["mm", "404"] = "mm") -> str:
+ hexdigest = hashlib.md5(frappe.as_unicode(email).encode("utf-8")).hexdigest()
+ return f"https://secure.gravatar.com/avatar/{hexdigest}?d={default}&s=200"
-def get_gravatar(email):
+def get_gravatar(email: str) -> str:
from frappe.utils.identicon import Identicon
return has_gravatar(email) or Identicon(email).base64()
@@ -310,7 +318,7 @@ def log(event, details):
frappe.logger(event).info(details)
-def dict_to_str(args, sep="&"):
+def dict_to_str(args: dict[str, Any], sep: str = "&") -> str:
"""
Converts a dictionary to URL
"""
@@ -346,18 +354,13 @@ def set_default(key, val):
return frappe.db.set_default(key, val)
-def remove_blanks(d):
+def remove_blanks(d: dict) -> dict:
"""
- Returns d with empty ('' or None) values stripped
+ Returns d with empty ('' or None) values stripped. Mutates inplace.
"""
- empty_keys = []
- for key in d:
- if d[key] == "" or d[key] is None:
- # del d[key] raises runtime exception, using a workaround
- empty_keys.append(key)
- for key in empty_keys:
- del d[key]
-
+ for k, v in tuple(d.items()):
+ if v == "" or v == None:
+ del d[k]
return d
@@ -417,21 +420,20 @@ def execute_in_shell(cmd, verbose=0, low_priority=False):
import tempfile
from subprocess import Popen
- with tempfile.TemporaryFile() as stdout:
- with tempfile.TemporaryFile() as stderr:
- kwargs = {"shell": True, "stdout": stdout, "stderr": stderr}
+ with (tempfile.TemporaryFile() as stdout, tempfile.TemporaryFile() as stderr):
+ kwargs = {"shell": True, "stdout": stdout, "stderr": stderr}
- if low_priority:
- kwargs["preexec_fn"] = lambda: os.nice(10)
+ if low_priority:
+ kwargs["preexec_fn"] = lambda: os.nice(10)
- p = Popen(cmd, **kwargs)
- p.wait()
+ p = Popen(cmd, **kwargs)
+ p.wait()
- stdout.seek(0)
- out = stdout.read()
+ stdout.seek(0)
+ out = stdout.read()
- stderr.seek(0)
- err = stderr.read()
+ stderr.seek(0)
+ err = stderr.read()
if verbose:
if err:
@@ -563,7 +565,7 @@ def update_progress_bar(txt, i, l, absolute=False):
sys.stdout.flush()
return
- if not getattr(frappe.local, "request", None) or is_cli():
+ if not getattr(frappe.local, "request", None) or is_cli(): # pragma: no cover
lt = len(txt)
try:
col = 40 if os.get_terminal_size().columns > 80 else 20
@@ -746,7 +748,7 @@ def get_site_info():
kwargs = {
"fields": ["user", "creation", "full_name"],
- "filters": {"Operation": "Login", "Status": "Success"},
+ "filters": {"operation": "Login", "status": "Success"},
"limit": "10",
}
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index bdef60b930..6d4a96ce5f 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -483,13 +483,11 @@ def get_quarter_ending(date):
return date
-def get_year_ending(date):
+def get_year_ending(date) -> datetime.date:
"""returns year ending of the given date"""
date = getdate(date)
- # first day of next year (note year starts from 1)
- date = add_to_date(f"{date.year}-01-01", months=12)
- # last day of this month
- return add_to_date(date, days=-1)
+ next_year_start = datetime.date(date.year + 1, 1, 1)
+ return add_to_date(next_year_start, days=-1)
def get_time(time_str: str) -> datetime.time:
@@ -724,60 +722,77 @@ def get_weekday(datetime: datetime.datetime | None = None) -> str:
return weekdays[datetime.weekday()]
-def get_timespan_date_range(timespan: str) -> tuple[datetime.datetime, datetime.datetime]:
- today = nowdate()
- date_range_map = {
- "last week": lambda: (
- get_first_day_of_week(add_to_date(today, days=-7)),
- get_last_day_of_week(add_to_date(today, days=-7)),
- ),
- "last month": lambda: (
- get_first_day(add_to_date(today, months=-1)),
- get_last_day(add_to_date(today, months=-1)),
- ),
- "last quarter": lambda: (
- get_quarter_start(add_to_date(today, months=-3)),
- get_quarter_ending(add_to_date(today, months=-3)),
- ),
- "last 6 months": lambda: (
- get_quarter_start(add_to_date(today, months=-6)),
- get_quarter_ending(add_to_date(today, months=-3)),
- ),
- "last year": lambda: (
- get_year_start(add_to_date(today, years=-1)),
- get_year_ending(add_to_date(today, years=-1)),
- ),
- "yesterday": lambda: (add_to_date(today, days=-1),) * 2,
- "today": lambda: (today, today),
- "tomorrow": lambda: (add_to_date(today, days=1),) * 2,
- "this week": lambda: (get_first_day_of_week(today), get_last_day_of_week(today)),
- "this month": lambda: (get_first_day(today), get_last_day(today)),
- "this quarter": lambda: (get_quarter_start(today), get_quarter_ending(today)),
- "this year": lambda: (get_year_start(today), get_year_ending(today)),
- "next week": lambda: (
- get_first_day_of_week(add_to_date(today, days=7)),
- get_last_day_of_week(add_to_date(today, days=7)),
- ),
- "next month": lambda: (
- get_first_day(add_to_date(today, months=1)),
- get_last_day(add_to_date(today, months=1)),
- ),
- "next quarter": lambda: (
- get_quarter_start(add_to_date(today, months=3)),
- get_quarter_ending(add_to_date(today, months=3)),
- ),
- "next 6 months": lambda: (
- get_quarter_start(add_to_date(today, months=3)),
- get_quarter_ending(add_to_date(today, months=6)),
- ),
- "next year": lambda: (
- get_year_start(add_to_date(today, years=1)),
- get_year_ending(add_to_date(today, years=1)),
- ),
- }
+def get_timespan_date_range(timespan: str) -> tuple[datetime.datetime, datetime.datetime] | None:
+ today = getdate()
- if timespan in date_range_map:
- return date_range_map[timespan]()
+ match timespan:
+ case "last week":
+ return (
+ get_first_day_of_week(add_to_date(today, days=-7)),
+ get_last_day_of_week(add_to_date(today, days=-7)),
+ )
+ case "last month":
+ return (
+ get_first_day(add_to_date(today, months=-1)),
+ get_last_day(add_to_date(today, months=-1)),
+ )
+ case "last quarter":
+ return (
+ get_quarter_start(add_to_date(today, months=-3)),
+ get_quarter_ending(add_to_date(today, months=-3)),
+ )
+ case "last 6 months":
+ return (
+ get_quarter_start(add_to_date(today, months=-6)),
+ get_quarter_ending(add_to_date(today, months=-3)),
+ )
+ case "last year":
+ return (
+ get_year_start(add_to_date(today, years=-1)),
+ get_year_ending(add_to_date(today, years=-1)),
+ )
+
+ case "yesterday":
+ return (add_to_date(today, days=-1),) * 2
+ case "today":
+ return (today, today)
+ case "tomorrow":
+ return (add_to_date(today, days=1),) * 2
+ case "this week":
+ return (get_first_day_of_week(today), get_last_day_of_week(today))
+ case "this month":
+ return (get_first_day(today), get_last_day(today))
+ case "this quarter":
+ return (get_quarter_start(today), get_quarter_ending(today))
+ case "this year":
+ return (get_year_start(today), get_year_ending(today))
+ case "next week":
+ return (
+ get_first_day_of_week(add_to_date(today, days=7)),
+ get_last_day_of_week(add_to_date(today, days=7)),
+ )
+ case "next month":
+ return (
+ get_first_day(add_to_date(today, months=1)),
+ get_last_day(add_to_date(today, months=1)),
+ )
+ case "next quarter":
+ return (
+ get_quarter_start(add_to_date(today, months=3)),
+ get_quarter_ending(add_to_date(today, months=3)),
+ )
+ case "next 6 months":
+ return (
+ get_quarter_start(add_to_date(today, months=3)),
+ get_quarter_ending(add_to_date(today, months=6)),
+ )
+ case "next year":
+ return (
+ get_year_start(add_to_date(today, years=1)),
+ get_year_ending(add_to_date(today, years=1)),
+ )
+ case _:
+ return
def global_date_format(date, format="long"):
@@ -1460,15 +1475,15 @@ def pretty_date(iso_datetime: datetime.datetime | str) -> str:
elif dt_diff_days < 12:
return _("1 week ago")
elif dt_diff_days < 31.0:
- return _("{0} weeks ago").format(cint(math.ceil(dt_diff_days / 7.0)))
+ return _("{0} weeks ago").format(dt_diff_days // 7)
elif dt_diff_days < 46:
return _("1 month ago")
elif dt_diff_days < 365.0:
- return _("{0} months ago").format(cint(math.ceil(dt_diff_days / 30.0)))
+ return _("{0} months ago").format(dt_diff_days // 30)
elif dt_diff_days < 550.0:
return _("1 year ago")
else:
- return f"{cint(math.floor(dt_diff_days / 365.0))} years ago"
+ return _("{0} years ago").format(dt_diff_days // 365)
def comma_or(some_list, add_quotes=True):
@@ -1658,14 +1673,14 @@ operator_map = {
"in": lambda a, b: operator.contains(b, a),
"not in": lambda a, b: not operator.contains(b, a),
# comparison operators
- "=": lambda a, b: operator.eq(a, b),
- "!=": lambda a, b: operator.ne(a, b),
- ">": lambda a, b: operator.gt(a, b),
- "<": lambda a, b: operator.lt(a, b),
- ">=": lambda a, b: operator.ge(a, b),
- "<=": lambda a, b: operator.le(a, b),
- "not None": lambda a, b: a and True or False,
- "None": lambda a, b: (not a) and True or False,
+ "=": operator.eq,
+ "!=": operator.ne,
+ ">": operator.gt,
+ "<": operator.lt,
+ ">=": operator.ge,
+ "<=": operator.le,
+ "not None": lambda a, b: a is not None,
+ "None": lambda a, b: a is None,
}
@@ -1687,13 +1702,12 @@ def evaluate_filters(doc, filters: dict | list | tuple):
def compare(val1: Any, condition: str, val2: Any, fieldtype: str | None = None):
- ret = False
if fieldtype:
val2 = cast(fieldtype, val2)
if condition in operator_map:
- ret = operator_map[condition](val1, val2)
+ return operator_map[condition](val1, val2)
- return ret
+ return False
def get_filter(doctype: str, f: dict | list | tuple, filters_config=None) -> "frappe._dict":