Merge branch 'develop' into numbercard_fix

This commit is contained in:
fadilsid 2023-11-21 12:21:14 +00:00
commit 197faee02c
47 changed files with 385 additions and 97 deletions

View file

@ -14,7 +14,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: 14

View file

@ -429,7 +429,7 @@ def print_sql(enable: bool = True) -> None:
def log(msg: str) -> None:
"""Add to `debug_log`.
"""Add to `debug_log`
:param msg: Message."""
if not request:

View file

@ -647,7 +647,7 @@ def validate_auth_via_api_keys(authorization_header):
frappe.InvalidAuthorizationToken,
)
except (AttributeError, TypeError, ValueError):
raise frappe.AuthenticationError
pass
def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None):

View file

@ -51,12 +51,14 @@
"fieldname": "address_line1",
"fieldtype": "Data",
"label": "Address Line 1",
"length": 240,
"reqd": 1
},
{
"fieldname": "address_line2",
"fieldtype": "Data",
"label": "Address Line 2"
"label": "Address Line 2",
"length": 240
},
{
"fieldname": "city",
@ -148,7 +150,7 @@
"icon": "fa fa-map-marker",
"idx": 5,
"links": [],
"modified": "2023-10-30 05:50:23.912366",
"modified": "2023-11-20 17:28:41.698356",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Address",

View file

@ -478,7 +478,7 @@ class Communication(Document, CommunicationEmailMixin):
return self.timeline_links
def remove_link(self, link_doctype, link_name, autosave=False, ignore_permissions=True):
for l in self.timeline_links:
for l in list(self.timeline_links):
if l.link_doctype == link_doctype and l.link_name == link_name:
self.timeline_links.remove(l)

View file

@ -19,6 +19,7 @@
"reqd",
"is_virtual",
"search_index",
"not_nullable",
"column_break_18",
"options",
"sort_options",
@ -566,13 +567,20 @@
"fieldname": "link_filters",
"fieldtype": "JSON",
"label": "Link Filters"
},
{
"default": "0",
"depends_on": "eval:!in_list([\"Check\", \"Currency\", \"Float\", \"Int\", \"Percent\", \"Rating\", \"Select\", \"Table\", \"Table MultiSelect\"], doc.fieldtype)",
"fieldname": "not_nullable",
"fieldtype": "Check",
"label": "Not Nullable"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-13 11:48:51.502812",
"modified": "2023-11-16 11:26:56.364594",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -92,6 +92,7 @@ class DocField(Document):
max_height: DF.Data | None
no_copy: DF.Check
non_negative: DF.Check
not_nullable: DF.Check
oldfieldname: DF.Data | None
oldfieldtype: DF.Data | None
options: DF.SmallText | None

View file

@ -365,16 +365,23 @@ class DocType(Document):
SET `{fieldname}` = source.`{source_fieldname}`
FROM `tab{link_doctype}` as source
WHERE `{link_fieldname}` = source.name
AND ifnull(`{fieldname}`, '')=''
"""
if df.not_nullable:
update_query += "AND `{fieldname}`=''"
else:
update_query += "AND ifnull(`{fieldname}`, '')=''"
else:
update_query = """
UPDATE `tab{doctype}` as target
INNER JOIN `tab{link_doctype}` as source
ON `target`.`{link_fieldname}` = `source`.`name`
SET `target`.`{fieldname}` = `source`.`{source_fieldname}`
WHERE ifnull(`target`.`{fieldname}`, '')=""
"""
if df.not_nullable:
update_query += "WHERE `target`.`{fieldname}`=''"
else:
update_query += "WHERE ifnull(`target`.`{fieldname}`, '')=''"
self.flags.update_fields_to_fetch_queries.append(
update_query.format(

View file

@ -7,7 +7,8 @@
"field_order": [
"status",
"scheduled_job_type",
"details"
"details",
"debug_log"
],
"fields": [
{
@ -35,10 +36,16 @@
"options": "Scheduled Job Type",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "debug_log",
"fieldtype": "Code",
"label": "Debug Log",
"read_only": 1
}
],
"links": [],
"modified": "2022-06-13 05:41:21.090972",
"modified": "2023-11-09 12:06:41.781270",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Log",

View file

@ -16,6 +16,7 @@ class ScheduledJobLog(Document):
if TYPE_CHECKING:
from frappe.types import DF
debug_log: DF.Code | None
details: DF.Code | None
scheduled_job_type: DF.Link
status: DF.Literal["Scheduled", "Complete", "Failed"]

View file

@ -145,6 +145,8 @@ class ScheduledJobType(Document):
dict(doctype="Scheduled Job Log", scheduled_job_type=self.name)
).insert(ignore_permissions=True)
self.scheduler_log.db_set("status", status)
if frappe.debug_log:
self.scheduler_log.db_set("debug_log", "\n".join(frappe.debug_log))
if status == "Failed":
self.scheduler_log.db_set("details", frappe.get_traceback())
if status == "Start":

View file

@ -1,7 +1,6 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import datetime
import itertools
import json
import random
@ -14,7 +13,6 @@ from time import time
from typing import Any
from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder
from pypika.terms import Criterion, NullValue
import frappe
import frappe.defaults
@ -163,6 +161,8 @@ class Database:
:param auto_commit: Commit after executing the query.
:param update: Update this dict to all rows (if returned `as_dict`).
:param run: Returns query without executing it if False.
:param pluck: Get the plucked field only.
:param explain: Print `EXPLAIN` in error log.
Examples:
# return customer names as dicts
@ -369,7 +369,7 @@ class Database:
self.commit()
self.sql(query, debug=debug)
def check_transaction_status(self, query):
def check_transaction_status(self, query: str):
"""Raises exception if more than 200,000 `INSERT`, `UPDATE` queries are
executed in one transaction. This is to ensure that writes are always flushed otherwise this
could cause the system to hang."""
@ -388,13 +388,13 @@ class Database:
msg += _("The changes have been reverted.") + "<br>"
raise frappe.TooManyWritesError(msg)
def check_implicit_commit(self, query):
def check_implicit_commit(self, query: str):
if (
self.transaction_writes
and query
and is_query_type(query, ("start", "alter", "drop", "create", "begin", "truncate"))
):
raise ImplicitCommitError("This statement can cause implicit commit")
raise ImplicitCommitError("This statement can cause implicit commit", query)
def fetch_as_dict(self) -> list[frappe._dict]:
"""Internal. Converts results to dict."""

View file

@ -310,7 +310,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
)
@staticmethod
def get_on_duplicate_update(key=None):
def get_on_duplicate_update():
return "ON DUPLICATE key UPDATE "
def get_table_columns_description(self, table_name):
@ -329,7 +329,8 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
and Seq_in_index = 1
limit 1
), 0) as 'index',
column_key = 'UNI' as 'unique'
column_key = 'UNI' as 'unique',
(is_nullable = 'NO') AS 'not_nullable'
from information_schema.columns as columns
where table_name = '{table_name}' """.format(
table_name=table_name

View file

@ -3,6 +3,7 @@ from pymysql.constants.ER import DUP_ENTRY
import frappe
from frappe import _
from frappe.database.schema import DBTable
from frappe.utils.defaults import get_not_null_defaults
class MariaDBTable(DBTable):
@ -69,7 +70,7 @@ class MariaDBTable(DBTable):
add_column_query = [
f"ADD COLUMN `{col.fieldname}` {col.get_definition()}" for col in self.add_column
]
columns_to_modify = set(self.change_type + self.set_default)
columns_to_modify = set(self.change_type + self.set_default + self.change_nullability)
modify_column_query = [
f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}"
for col in columns_to_modify
@ -102,12 +103,23 @@ class MariaDBTable(DBTable):
if index_record := frappe.db.get_column_index(self.table_name, col.fieldname, unique=False):
drop_index_query.append(f"DROP INDEX `{index_record.Key_name}`")
for col in self.change_nullability:
if col.not_nullable:
try:
table = frappe.qb.DocType(self.doctype)
frappe.qb.update(table).set(
col.fieldname, col.default or get_not_null_defaults(col.fieldtype)
).where(table[col.fieldname].isnull()).run()
except Exception:
print(f"Failed to update data in {self.table_name} for {col.fieldname}")
raise
try:
for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]:
if query_parts:
query_body = ", ".join(query_parts)
query = f"ALTER TABLE `{self.table_name}` {query_body}"
frappe.db.sql(query)
# nosemgrep
frappe.db.sql_ddl(query)
except Exception as e:
if query := locals().get("query"): # this weirdness is to avoid potentially unbounded vars

View file

@ -392,7 +392,8 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
END AS type,
BOOL_OR(b.index) AS index,
SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default,
BOOL_OR(b.unique) AS unique
BOOL_OR(b.unique) AS unique,
COALESCE(a.is_nullable = 'NO', false) AS not_nullable
FROM information_schema.columns a
LEFT JOIN
(SELECT indexdef, tablename,
@ -402,7 +403,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
WHERE tablename='{table_name}') b
ON SUBSTRING(b.indexdef, '(.*)') LIKE CONCAT('%', a.column_name, '%')
WHERE a.table_name = '{table_name}'
GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;
GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length, a.is_nullable;
""".format(
table_name=table_name
),

View file

@ -2,6 +2,7 @@ import frappe
from frappe import _
from frappe.database.schema import DBTable, get_definition
from frappe.utils import cint, flt
from frappe.utils.defaults import get_not_null_defaults
class PostgresTable(DBTable):
@ -45,7 +46,7 @@ class PostgresTable(DBTable):
docstatus smallint not null default '0',
idx bigint not null default '0',
{additional_definitions}
)"""
)""",
)
self.create_indexes()
@ -139,11 +140,34 @@ class PostgresTable(DBTable):
if col.fieldname != "name":
# if index key exists
drop_contraint_query += f'DROP INDEX IF EXISTS "unique_{col.fieldname}" ;'
change_nullability = []
for col in self.change_nullability:
default = col.default or get_not_null_defaults(col.fieldtype)
if isinstance(default, str):
default = frappe.db.escape(default)
change_nullability.append(
f"ALTER COLUMN \"{col.fieldname}\" {'SET' if col.not_nullable else 'DROP'} NOT NULL"
)
change_nullability.append(f'ALTER COLUMN "{col.fieldname}" SET DEFAULT {default}')
if col.not_nullable:
try:
table = frappe.qb.DocType(self.doctype)
frappe.qb.update(table).set(
col.fieldname, col.default or get_not_null_defaults(col.fieldtype)
).where(table[col.fieldname].isnull()).run()
except Exception:
print(f"Failed to update data in {self.table_name} for {col.fieldname}")
raise
try:
if query:
final_alter_query = "ALTER TABLE `{}` {}".format(self.table_name, ", ".join(query))
# nosemgrep
frappe.db.sql(final_alter_query)
if change_nullability:
# nosemgrep
frappe.db.sql(f"ALTER TABLE `{self.table_name}` {','.join(change_nullability)}")
if create_contraint_query:
# nosemgrep
frappe.db.sql(create_contraint_query)

View file

@ -3,6 +3,7 @@ import re
import frappe
from frappe import _
from frappe.utils import cint, cstr, flt
from frappe.utils.defaults import get_not_null_defaults
SPECIAL_CHAR_PATTERN = re.compile(r"[\W]", flags=re.UNICODE)
VARCHAR_CAST_PATTERN = re.compile(r"varchar\(([\d]+)\)")
@ -24,6 +25,7 @@ class DBTable:
self.add_column: list[DbColumn] = []
self.change_type: list[DbColumn] = []
self.change_name: list[DbColumn] = []
self.change_nullability: list[DbColumn] = []
self.add_unique: list[DbColumn] = []
self.add_index: list[DbColumn] = []
self.drop_unique: list[DbColumn] = []
@ -89,15 +91,16 @@ class DBTable:
continue
self.columns[field.get("fieldname")] = DbColumn(
self,
field.get("fieldname"),
field.get("fieldtype"),
field.get("length"),
field.get("default"),
field.get("search_index"),
field.get("options"),
field.get("unique"),
field.get("precision"),
table=self,
fieldname=field.get("fieldname"),
fieldtype=field.get("fieldtype"),
length=field.get("length"),
default=field.get("default"),
set_index=field.get("search_index"),
options=field.get("options"),
unique=field.get("unique"),
precision=field.get("precision"),
not_nullable=field.get("not_nullable"),
)
def validate(self):
@ -175,7 +178,18 @@ class DBTable:
class DbColumn:
def __init__(
self, table, fieldname, fieldtype, length, default, set_index, options, unique, precision
self,
*,
table,
fieldname,
fieldtype,
length,
default,
set_index,
options,
unique,
precision,
not_nullable,
):
self.table = table
self.fieldname = fieldname
@ -186,6 +200,7 @@ class DbColumn:
self.options = options
self.unique = unique
self.precision = precision
self.not_nullable = not_nullable
def get_definition(self, for_modification=False):
column_def = get_definition(self.fieldtype, precision=self.precision, length=self.length)
@ -193,24 +208,43 @@ class DbColumn:
if not column_def:
return column_def
null = True
default = None
unique = False
if self.fieldtype in ("Check", "Int"):
default_value = cint(self.default) or 0
column_def += f" not null default {default_value}"
default = cint(self.default) or 0
null = False
elif self.fieldtype in ("Currency", "Float", "Percent"):
default_value = flt(self.default) or 0
column_def += f" not null default {default_value}"
default = flt(self.default) or 0
null = False
elif (
self.default
and (self.default not in frappe.db.DEFAULT_SHORTCUTS)
and not cstr(self.default).startswith(":")
):
column_def += f" default {frappe.db.escape(self.default)}"
default = frappe.db.escape(self.default)
if self.not_nullable and null:
if default is None:
default = get_not_null_defaults(self.fieldtype)
if isinstance(default, str):
default = frappe.db.escape(default)
null = False
if self.unique and not for_modification and (column_def not in ("text", "longtext")):
column_def += " unique"
unique = True
if not null:
column_def += " NOT NULL"
if default is not None:
column_def += f" DEFAULT {default}"
if unique:
column_def += " UNIQUE"
return column_def
def build_for_alter_table(self, current_def):
@ -250,6 +284,10 @@ class DbColumn:
):
self.table.set_default.append(self)
# nullability
if self.not_nullable is not None and (self.not_nullable != current_def["not_nullable"]):
self.table.change_nullability.append(self)
# index should be applied or dropped irrespective of type change
if (current_def["index"] and not self.set_index) and column_type not in ("text", "longtext"):
self.table.drop_index.append(self)

View file

@ -1,7 +1,6 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import typing
from functools import cached_property
from types import NoneType
@ -9,9 +8,6 @@ import frappe
from frappe.query_builder.builder import MariaDB, Postgres
from frappe.query_builder.functions import Function
if typing.TYPE_CHECKING:
from frappe.query_builder import DocType
Query = str | MariaDB | Postgres
QueryValues = tuple | list | dict | NoneType
@ -27,7 +23,7 @@ NestedSetHierarchy = (
)
def is_query_type(query: str, query_type: str | tuple[str]) -> bool:
def is_query_type(query: str, query_type: str | tuple[str, ...]) -> bool:
return query.lstrip().split(maxsplit=1)[0].lower().startswith(query_type)

View file

@ -121,9 +121,7 @@ class Event(Document):
["Communication Link", "link_doctype", "=", participant.reference_doctype],
["Communication Link", "link_name", "=", participant.reference_docname],
]
comms = frappe.get_all("Communication", filters=filters, fields=["name"])
if comms:
if comms := frappe.get_all("Communication", filters=filters, fields=["name"], distinct=True):
for comm in comms:
communication = frappe.get_doc("Communication", comm.name)
self.update_communication(participant, communication)

View file

@ -11,6 +11,10 @@ frappe.ui.form.on("Notification Log", {
},
open_reference_document: function (frm) {
if (frm.doc?.link) {
frappe.set_route(frm.doc.link);
return;
}
const dt = frm.doc.document_type;
const dn = frm.doc.document_name;
frappe.set_route("Form", dt, dn);

View file

@ -15,7 +15,8 @@
"attached_file",
"attachment_link",
"open_reference_document",
"from_user"
"from_user",
"link"
],
"fields": [
{
@ -91,12 +92,18 @@
"fieldname": "attachment_link",
"fieldtype": "HTML",
"label": "Attachment Link"
},
{
"fieldname": "link",
"fieldtype": "Data",
"hidden": 1,
"label": "Link"
}
],
"hide_toolbar": 1,
"in_create": 1,
"links": [],
"modified": "2023-06-14 21:20:51.197943",
"modified": "2023-11-18 22:40:12.145940",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",

View file

@ -25,6 +25,7 @@ class NotificationLog(Document):
email_content: DF.TextEditor | None
for_user: DF.Link | None
from_user: DF.Link | None
link: DF.Data | None
read: DF.Check
subject: DF.Text | None
type: DF.Literal["Mention", "Energy Point", "Assignment", "Share", "Alert"]
@ -125,21 +126,24 @@ def send_notification_email(doc):
if not email:
return
doc_link = get_url_to_form(doc.document_type, doc.document_name)
header = get_email_header(doc)
email_subject = strip_html(doc.subject)
args = {
"body_content": doc.subject,
"description": doc.email_content,
}
if doc.link:
args["doc_link"] = doc.link
else:
args["document_type"] = doc.document_type
args["document_name"] = doc.document_name
args["doc_link"] = get_url_to_form(doc.document_type, doc.document_name)
frappe.sendmail(
recipients=email,
subject=email_subject,
template="new_notification",
args={
"body_content": doc.subject,
"description": doc.email_content,
"document_type": doc.document_type,
"document_name": doc.document_name,
"doc_link": doc_link,
},
args=args,
header=[header, "orange"],
now=frappe.flags.in_test,
)

View file

@ -29,7 +29,7 @@
],
"fields": [
{
"description": "To print output use <code>log(text)</code>",
"description": "To print output use <code>print(text)</code>",
"fieldname": "console",
"fieldtype": "Code",
"label": "Console",
@ -86,7 +86,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-04-15 14:15:58.398590",
"modified": "2023-11-03 13:02:00.706806",
"modified_by": "Administrator",
"module": "Desk",
"name": "System Console",

View file

@ -44,7 +44,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "DocType View",
"options": "\nList\nReport Builder\nDashboard\nTree\nNew\nCalendar\nKanban"
"options": "\nList\nReport Builder\nDashboard\nTree\nNew\nCalendar\nKanban\nImage"
},
{
"fieldname": "column_break_4",

View file

@ -287,7 +287,6 @@ def get_attr(cmd):
f"Calling shorthand for {cmd} is deprecated, please specify full path in RPC call."
)
method = globals()[cmd]
frappe.log("method:" + cmd)
return method

View file

@ -31,6 +31,7 @@ from frappe.utils import (
sanitize_html,
strip_html,
)
from frappe.utils.defaults import get_not_null_defaults
from frappe.utils.html_utils import unescape_html
if TYPE_CHECKING:
@ -384,6 +385,13 @@ class BaseDocument:
if ignore_nulls and not is_virtual_field and value is None:
continue
# If the docfield is not nullable - set a default non-null value
if value is None and getattr(df, "not_nullable", False):
if df.default:
value = df.default
else:
value = get_not_null_defaults(df.fieldtype)
d[fieldname] = value
return d

View file

@ -109,7 +109,6 @@ class DatabaseQuery:
save_user_settings=False,
save_user_settings_fields=False,
update=None,
add_total_row=None,
user_settings=None,
reference_doctype=None,
run=True,
@ -734,12 +733,15 @@ class DatabaseQuery:
f.update(get_additional_filter_field(additional_filters_config, f, f.value))
meta = frappe.get_meta(f.doctype)
df = meta.get("fields", {"fieldname": f.fieldname})
df = df[0] if df else None
can_be_null = True
value = None
# prepare in condition
if f.operator.lower() in NestedSetHierarchy:
values = f.value or ""
# TODO: handle list and tuple
# if not isinstance(values, (list, tuple)):
# values = values.split(",")
@ -785,30 +787,33 @@ class DatabaseQuery:
"not in" if f.operator.lower() in ("not ancestors of", "not descendants of") else "in"
)
elif f.operator.lower() in ("in", "not in"):
if f.operator.lower() in ("in", "not in"):
# if values contain '' or falsy values then only coalesce column
# for `in` query this is only required if values contain '' or values are empty.
# for `not in` queries we can't be sure as column values might contain null.
can_be_null = not getattr(df, "not_nullable", False)
if f.operator.lower() == "in":
can_be_null = not f.value or any(v is None or v == "" for v in f.value)
can_be_null &= not f.value or any(v is None or v == "" for v in f.value)
values = f.value or ""
if isinstance(values, str):
values = values.split(",")
if value is None:
values = f.value or ""
if isinstance(values, str):
values = values.split(",")
fallback = "''"
value = [frappe.db.escape((cstr(v) or "").strip(), percent=False) for v in values]
if len(value):
value = f"({', '.join(value)})"
else:
value = "('')"
fallback = "''"
value = [frappe.db.escape((cstr(v) or "").strip(), percent=False) for v in values]
if len(value):
value = f"({', '.join(value)})"
else:
value = "('')"
else:
escape = True
df = meta.get("fields", {"fieldname": f.fieldname})
df = df[0] if df else None
if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
if df and (
df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent")
or getattr(df, "not_nullable", False)
):
can_be_null = False
if f.operator.lower() in ("previous", "next", "timespan"):
@ -841,7 +846,7 @@ class DatabaseQuery:
elif f.value == "not set":
f.operator = "="
fallback = "''"
can_be_null = True
can_be_null = not getattr(df, "not_nullable", False)
value = ""
@ -981,7 +986,6 @@ class DatabaseQuery:
)
def add_user_permissions(self, user_permissions):
doctype_link_fields = []
doctype_link_fields = self.doctype_meta.get_link_fields()
# append current doctype with fieldname as 'name' as first link field

View file

@ -343,7 +343,7 @@ class Document(BaseDocument):
:param ignore_permissions: Do not check permissions if True.
:param ignore_version: Do not save version if True."""
if self.flags.in_print:
return
return self
self.flags.notifications_executed = []

View file

@ -464,11 +464,12 @@ def get_link_fields(doctype: str) -> list[dict]:
cf = frappe.qb.DocType("Custom Field")
ps = frappe.qb.DocType("Property Setter")
st_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle")
standard_fields = (
frappe.qb.from_(df)
.select(df.parent, df.fieldname, st_issingle)
.where((df.options == doctype) & (df.fieldtype == "Link"))
.inner_join(dt)
.on(df.parent == dt.name)
.select(df.parent, df.fieldname, dt.issingle.as_("issingle"))
.where((df.options == doctype) & (df.fieldtype == "Link") & (dt.is_virtual == 0))
.run(as_dict=True)
)

View file

@ -8,6 +8,7 @@
src: url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2-variations'),
url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2');
src: url('/assets/frappe/css/fonts/inter/Inter.var.woff2?v=3.19') format('woff2') tech('variations');
unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}
@font-face {
font-family: 'Inter V';
@ -17,6 +18,7 @@
src: url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2-variations'),
url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2');
src: url('/assets/frappe/css/fonts/inter/Inter-Italic.var.woff2?v=3.19') format('woff2') tech('variations');
unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}
@font-face {
font-family: 'Inter';

View file

@ -36,11 +36,18 @@ frappe.get_indicator = function (doc, doctype, show_workflow_state) {
var settings = frappe.listview_settings[doctype] || {};
var is_submittable = frappe.model.is_submittable(doctype),
workflow_fieldname = frappe.workflow.get_state_fieldname(doctype);
var is_submittable = frappe.model.is_submittable(doctype);
let workflow_fieldname = frappe.workflow.get_state_fieldname(doctype);
let avoid_status_override = (frappe.workflow.avoid_status_override[doctype] || []).includes(
doc[workflow_fieldname]
);
// workflow
if (workflow_fieldname && (!without_workflow || show_workflow_state)) {
if (
workflow_fieldname &&
(!without_workflow || show_workflow_state) &&
!avoid_status_override
) {
var value = doc[workflow_fieldname];
if (value) {
let colour = "";

View file

@ -6,11 +6,15 @@ frappe.provide("frappe.workflow");
frappe.workflow = {
state_fields: {},
workflows: {},
avoid_status_override: {},
setup: function (doctype) {
var wf = frappe.get_list("Workflow", { document_type: doctype });
if (wf.length) {
frappe.workflow.workflows[doctype] = wf[0];
frappe.workflow.state_fields[doctype] = wf[0].workflow_state_field;
frappe.workflow.avoid_status_override[doctype] = wf[0].states
.filter((row) => row.avoid_status_override)
.map((d) => d.state);
} else {
frappe.workflow.state_fields[doctype] = null;
}

View file

@ -483,8 +483,8 @@ frappe.request.cleanup = function (opts, r) {
if (opts.args) {
console.log("======== arguments ========");
console.log(opts.args);
console.log("========");
}
console.log("======== debug messages ========");
$.each(JSON.parse(r._debug_messages), function (i, v) {
console.log(v);
});

View file

@ -325,6 +325,9 @@ class NotificationsView extends BaseNotificationsView {
}
get_item_link(notification_doc) {
if (notification_doc.link) {
return notification_doc.link;
}
const link_doctype = notification_doc.document_type
? notification_doc.document_type
: "Notification Log";

View file

@ -1300,6 +1300,9 @@ Object.assign(frappe.utils, {
route += `/${item.kanban_board}`;
}
break;
case "Image":
route = `${doctype_slug}/view/image`;
break;
default:
route = doctype_slug;
}

View file

@ -110,7 +110,7 @@ export default class WebFormList {
fetch_data() {
if (this.condition_json && JSON.parse(this.condition_json)) {
let filter = frappe.utils.get_filter_from_json(this.condition_json,this.doctype);
let filter = frappe.utils.get_filter_from_json(this.condition_json, this.doctype);
filter = frappe.utils.get_filter_as_json(filter);
this.filters = Object.assign(this.filters, JSON.parse(filter));
}

View file

@ -396,6 +396,7 @@ class ShortcutDialog extends WidgetDialog {
const views = ["List", "Report Builder", "Dashboard", "New"];
if (meta.is_tree === 1) views.push("Tree");
if (meta.image_field) views.push("Image");
if (frappe.boot.calendars.includes(doctype)) views.push("Calendar");
const response = await frappe.db.get_value(
@ -427,7 +428,7 @@ class ShortcutDialog extends WidgetDialog {
fieldtype: "Select",
fieldname: "doc_view",
label: "DocType View",
options: "List\nReport Builder\nDashboard\nTree\nNew\nCalendar\nKanban",
options: "List\nReport Builder\nDashboard\nTree\nNew\nCalendar\nKanban\nImage",
description: __(
"Which view of the associated DocType should this shortcut take you to?"
),

View file

@ -282,7 +282,7 @@ class TestMethodAPI(FrappeAPITestCase):
response = self.get(self.method_path("frappe.auth.get_logged_user"))
self.assertEqual(response.status_code, 401)
authorization_token = f"NonExistentKey:INCORRECT"
authorization_token = "NonExistentKey:INCORRECT"
response = self.get(self.method_path("frappe.auth.get_logged_user"))
self.assertEqual(response.status_code, 401)
@ -382,7 +382,7 @@ def after_request(*args, **kwargs):
class TestResponse(FrappeAPITestCase):
def test_generate_pdf(self):
response = self.get(
f"/api/method/frappe.utils.print_format.download_pdf",
"/api/method/frappe.utils.print_format.download_pdf",
{"sid": self.sid, "doctype": "User", "name": "Guest"},
)
self.assertEqual(response.status_code, 200)

View file

@ -0,0 +1,62 @@
import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.database.schema import DBTable
from frappe.tests.utils import FrappeTestCase
class TestNonNullableDocfield(FrappeTestCase):
def setUp(self):
doc = new_doctype(
fields=[
{
"fieldname": "test_field",
"fieldtype": "Data",
"label": "test_field",
"not_nullable": 1,
},
],
)
doc.insert()
self.doctype_name = doc.name
nullable_doc = new_doctype(
fields=[
{
"fieldname": "test_field",
"fieldtype": "Data",
"label": "test_field",
}
]
)
nullable_doc.insert()
self.nullable_doctype_name = nullable_doc.name
def test_non_nullable_field(self):
doc = frappe.new_doc(doctype=self.doctype_name)
doc.insert()
inserted_doc = frappe.db.get(self.doctype_name, {"name": doc.name})
self.assertEqual(inserted_doc.test_field, "")
def test_edit_field_nullable_status(self):
doc = frappe.new_doc(doctype=self.nullable_doctype_name)
doc.insert()
inserted_doc = frappe.db.get(self.nullable_doctype_name, {"name": doc.name})
self.assertEqual(inserted_doc.test_field, None)
table = DBTable(self.nullable_doctype_name)
query = "SELECT column_name AS name, column_default is NULL AS default_null,is_nullable = 'NO' AS not_nullable FROM information_schema.columns WHERE table_name=%s"
for column in frappe.db.sql(query, table.table_name, as_dict=True):
if column.name == "test_field":
self.assertFalse(column.not_nullable)
doctype_doc = frappe.get_doc("DocType", self.nullable_doctype_name)
for field in doctype_doc.fields:
if field.fieldname == "test_field":
field.not_nullable = 1
break
doctype_doc.save()
for column in frappe.db.sql(query, table.table_name, as_dict=True):
if column.name == "test_field":
self.assertFalse(column.default_null)
self.assertTrue(column.not_nullable)
inserted_doc = frappe.db.get(self.nullable_doctype_name, {"name": doc.name})
self.assertEqual(inserted_doc.test_field, "")

View file

@ -121,6 +121,11 @@ class TestSafeExec(FrappeTestCase):
# dont Allow modifying _dict class
self.assertRaises(Exception, safe_exec, "_dict.x = 1")
def test_print(self):
test_str = frappe.generate_hash()
safe_exec(f"print('{test_str}')")
self.assertEqual(frappe.local.debug_log[-1], test_str)
class TestNoSafeExec(FrappeTestCase):
def test_safe_exec_disabled_by_default(self):

View file

@ -162,6 +162,9 @@ class TypeExporter:
if field.fieldtype in non_nullable_types:
return False
if field.not_nullable:
return False
return not bool(field.reqd)
def _generic_parameters(self, field) -> str | None:

50
frappe/utils/defaults.py Normal file
View file

@ -0,0 +1,50 @@
from typing import Literal
def get_not_null_defaults(column_type: str) -> Literal["", 0] | None:
"""
Method to return a default value for a column type that is not NoneType
:param column_type: The type of column
:return: The value to be set
"""
column_type_map = {
"Data": str,
"Text": str,
"Autocomplete": str,
"Attach": str,
"AttachImage": str,
"Barcode": str,
"Check": int,
"Code": str,
"Color": str,
"Currency": float,
"Date": str,
"Datetime": str,
"Duration": int,
"DynamicLink": str,
"Float": float,
"HTMLEditor": str,
"Int": int,
"JSON": str,
"Link": str,
"LongText": str,
"MarkdownEditor": str,
"Password": str,
"Percent": float,
"Phone": str,
"ReadOnly": str,
"Rating": float,
"Select": str,
"SmallText": str,
"TextEditor": str,
"Time": str,
"Table": list,
"Table MultiSelect": list,
}
data_type = column_type_map.get(column_type.replace(" ", ""), str)
# data_type = eval(f"frappe.types.DF.{column_type.replace(' ', '')}")
if data_type == str:
return ""
if data_type in (int, float):
return 0
return None

View file

@ -175,7 +175,7 @@ def _make_logs_v1():
if frappe.local.message_log:
response["_server_messages"] = json.dumps([json.dumps(d) for d in frappe.local.message_log])
if frappe.debug_log and frappe.conf.get("logging"):
if frappe.debug_log:
response["_debug_messages"] = json.dumps(frappe.local.debug_log)
if frappe.flags.error_message:
@ -188,7 +188,7 @@ def _make_logs_v2():
if frappe.local.message_log:
response["messages"] = frappe.local.message_log
if frappe.debug_log and frappe.conf.get("logging"):
if frappe.debug_log:
response["debug"] = [{"message": m} for m in frappe.local.debug_log]

View file

@ -1,6 +1,7 @@
import ast
import copy
import inspect
import io
import json
import mimetypes
import types
@ -8,7 +9,7 @@ from contextlib import contextmanager
from functools import lru_cache
import RestrictedPython.Guards
from RestrictedPython import compile_restricted, safe_globals
from RestrictedPython import PrintCollector, compile_restricted, safe_globals
from RestrictedPython.transformer import RestrictingNodeTransformer
import frappe
@ -60,6 +61,16 @@ class FrappeTransformer(RestrictingNodeTransformer):
return super().check_name(node, name, *args, **kwargs)
class FrappePrintCollector(PrintCollector):
"""Collect written text, and return it when called."""
def _call_print(self, *objects, **kwargs):
output = io.StringIO()
print(*objects, file=output, **kwargs)
frappe.log(output.getvalue().strip())
output.close()
def is_safe_exec_enabled() -> bool:
# server scripts can only be enabled via common_site_config.json
return bool(frappe.get_common_site_config().get(SAFE_EXEC_CONFIG_KEY))
@ -261,6 +272,9 @@ def get_safe_globals():
out._getitem_ = _getitem
out._getattr_ = _getattr_for_safe_exec
# Allow using `print()` calls with `safe_exec()`
out._print_ = FrappePrintCollector
# allow iterators and list comprehension
out._getiter_ = iter
out._iter_unpack_sequence_ = RestrictedPython.Guards.guarded_iter_unpack_sequence

View file

@ -684,7 +684,7 @@ def get_link_options(web_form_name, doctype, allow_read_on_all_link_options=Fals
frappe.get_cached_value("DocType", doctype, "show_title_field_in_link") == 1
)
if not show_title_field_in_link:
value = frappe.get_cached_value(
value = frappe.db.get_value(
"Property Setter",
fieldname="value",
filters={"property": "show_title_field_in_link", "doc_type": doctype},

View file

@ -13,6 +13,7 @@
"update_value",
"column_break_4",
"is_optional_state",
"avoid_status_override",
"next_action_email_template",
"allow_edit",
"section_break_9",
@ -97,12 +98,19 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Workflow Builder ID"
},
{
"default": "0",
"description": "If Checked workflow status will not override status in list view",
"fieldname": "avoid_status_override",
"fieldtype": "Check",
"label": "Don't Override Status"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-16 12:10:15.472947",
"modified": "2023-11-13 18:27:08.633239",
"modified_by": "Administrator",
"module": "Workflow",
"name": "Workflow Document State",

View file

@ -15,6 +15,7 @@ class WorkflowDocumentState(Document):
from frappe.types import DF
allow_edit: DF.Link
avoid_status_override: DF.Check
doc_status: DF.Literal["0", "1", "2"]
is_optional_state: DF.Check
message: DF.Text | None