Merge branch 'develop' into map-read-only

This commit is contained in:
Raffael Meyer 2023-06-03 14:40:04 +02:00 committed by GitHub
commit 64d1e6492b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 439 additions and 93 deletions

View file

@ -34,3 +34,6 @@ c0c5b2ebdddbe8898ce2d5e5365f4931ff73b6bf
# db.get_all -> get_all
2eec621e95564c359ad22da79501a855c1f32b03
# minor formatting fix in `user.py`
f223bc02490902dfcc32892058f13f343d51fbaf

View file

@ -1049,18 +1049,26 @@ def reset_metadata_version():
def new_doc(
doctype: str,
*,
parent_doc: Optional["Document"] = None,
parentfield: str | None = None,
as_dict: bool = False,
**kwargs,
) -> "Document":
"""Returns a new document of the given DocType with defaults set.
:param doctype: DocType of the new document.
:param parent_doc: [optional] add to parent document.
:param parentfield: [optional] add against this `parentfield`."""
:param parentfield: [optional] add against this `parentfield`.
:param as_dict: [optional] return as dictionary instead of Document.
:param kwargs: [optional] You can specify fields as field=value pairs in function call.
"""
from frappe.model.create_new import get_new_doc
return get_new_doc(doctype, parent_doc, parentfield, as_dict=as_dict)
new_doc = get_new_doc(doctype, parent_doc, parentfield, as_dict=as_dict)
return new_doc.update(kwargs)
def set_value(doctype, docname, fieldname, value=None):

View file

@ -19,7 +19,6 @@ import frappe.recorder
import frappe.utils.response
from frappe import _
from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe.middlewares import StaticDataMiddleware
from frappe.utils import cint, get_site_name, sanitize_html
from frappe.utils.error import make_error_snapshot
@ -351,8 +350,6 @@ def sync_database(rollback: bool) -> bool:
frappe.db.commit()
rollback = False
update_comments_in_parent_after_request()
return rollback

View file

@ -152,14 +152,9 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
except Exception as e:
if frappe.db.is_column_missing(e) and getattr(frappe.local, "request", None):
# missing column and in request, add column and update after commit
frappe.local._comments = getattr(frappe.local, "_comments", []) + [
(reference_doctype, reference_name, _comments)
]
pass
elif frappe.db.is_data_too_long(e):
raise frappe.DataTooLongException
else:
raise
else:
@ -169,13 +164,3 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
# Clear route cache
if route := frappe.get_cached_value(reference_doctype, reference_name, "route"):
clear_cache(route)
def update_comments_in_parent_after_request():
"""update _comments in parent if _comments column is missing"""
if hasattr(frappe.local, "_comments"):
for (reference_doctype, reference_name, _comments) in frappe.local._comments:
add_column(reference_doctype, "_comments", "Text")
update_comments_in_parent(reference_doctype, reference_name, _comments)
frappe.db.commit()

View file

@ -33,7 +33,7 @@ from frappe.model.meta import Meta
from frappe.modules import get_doc_path, make_boilerplate
from frappe.modules.import_file import get_file_path
from frappe.query_builder.functions import Concat
from frappe.utils import cint, random_string
from frappe.utils import cint, flt, random_string
from frappe.website.utils import clear_cache
if TYPE_CHECKING:
@ -1751,3 +1751,14 @@ def get_field(doc, fieldname):
for field in doc.fields:
if field.fieldname == fieldname:
return field
@frappe.whitelist()
def get_row_size_utilization(doctype: str) -> float:
"""Get row size utilization in percentage"""
frappe.has_permission("DocType", throw=True)
try:
return flt(frappe.db.get_row_size(doctype) / frappe.db.MAX_ROW_SIZE_LIMIT * 100, 2)
except Exception:
return 0.0

View file

@ -4,5 +4,9 @@
frappe.ui.form.on("Patch Log", {
refresh: function (frm) {
frm.disable_save();
frm.add_custom_button(__("Re-Run Patch"), () => {
frm.call("rerun_patch");
});
},
});

View file

@ -4,11 +4,20 @@
# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.model.document import Document
class PatchLog(Document):
pass
@frappe.whitelist()
def rerun_patch(self):
from frappe.modules.patch_handler import run_single
if not frappe.conf.developer_mode:
frappe.throw(_("Re-running patch is only allowed in developer mode."))
run_single(self.patch, force=True)
frappe.msgprint(_("Successfully re-ran patch: {0}").format(self.patch), alert=True)
def before_migrate():

View file

@ -72,6 +72,8 @@
"disable_standard_email_footer",
"hide_footer_in_auto_email_reports",
"attach_view_link",
"welcome_email_template",
"reset_password_template",
"prepared_report_section",
"max_auto_email_report_per_user",
"system_updates_section",
@ -549,12 +551,24 @@
"fieldname": "enable_telemetry",
"fieldtype": "Check",
"label": "Allow Sending Usage Data for Improving Applications"
},
{
"fieldname": "welcome_email_template",
"fieldtype": "Link",
"label": "Welcome Email Template",
"options": "Email Template"
},
{
"fieldname": "reset_password_template",
"fieldtype": "Link",
"label": "Reset Password Template",
"options": "Email Template"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2023-04-23 11:14:59.302851",
"modified": "2023-05-25 13:02:54.808773",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -329,7 +329,16 @@ class User(Document):
return (self.first_name or "") + (self.first_name and " " or "") + (self.last_name or "")
def password_reset_mail(self, link):
self.send_login_mail(_("Password Reset"), "password_reset", {"link": link}, now=True)
reset_password_template = frappe.db.get_system_setting("reset_password_template")
self.send_login_mail(
_("Password Reset"),
"password_reset",
{"link": link},
now=True,
custom_template=reset_password_template,
)
def send_welcome_mail_to_user(self):
from frappe.utils import get_url
@ -346,6 +355,8 @@ class User(Document):
else:
subject = _("Complete Registration")
welcome_email_template = frappe.db.get_system_setting("welcome_email_template")
self.send_login_mail(
subject,
"new_user",
@ -353,9 +364,10 @@ class User(Document):
link=link,
site_url=get_url(),
),
custom_template=welcome_email_template,
)
def send_login_mail(self, subject, template, add_args, now=None):
def send_login_mail(self, subject, template, add_args, now=None, custom_template=None):
"""send mail with login details"""
from frappe.utils import get_url
from frappe.utils.user import get_user_fullname
@ -378,11 +390,19 @@ class User(Document):
frappe.session.user not in STANDARD_USERS and get_formatted_email(frappe.session.user) or None
)
if custom_template:
from frappe.email.doctype.email_template.email_template import get_email_template
email_template = get_email_template(custom_template, args)
subject = email_template.get("subject")
content = email_template.get("message")
frappe.sendmail(
recipients=self.email,
sender=sender,
subject=subject,
template=template,
template=template if not custom_template else None,
content=content if custom_template else None,
args=args,
header=[subject, "green"],
delayed=(not now) if now is not None else self.flags.delay_emails,

View file

@ -1,8 +1,15 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
class ViewLog(Document):
pass
@staticmethod
def clear_old_logs(days=180):
from frappe.query_builder import Interval
from frappe.query_builder.functions import Now
table = frappe.qb.DocType("View Log")
frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))

View file

@ -172,7 +172,18 @@ class CustomizeForm(Document):
check_email_append_to(self)
if self.flags.update_db:
frappe.db.updatedb(self.doc_type)
try:
frappe.db.updatedb(self.doc_type)
except Exception as e:
if frappe.db.is_db_table_size_limit(e):
frappe.throw(
_("You have hit the row size limit on database table: {0}").format(
"<a href='https://docs.erpnext.com/docs/v14/user/manual/en/customize-erpnext/articles/maximum-number-of-fields-in-a-form'>"
"Maximum Number of Fields in a Form</a>"
),
title=_("Database Table Row Size Limit"),
)
raise
if not hasattr(self, "hide_success") or not self.hide_success:
frappe.msgprint(_("{0} updated").format(_(self.doc_type)), alert=True)

View file

@ -105,6 +105,8 @@ class Database:
self.password = password or frappe.conf.db_password
self.value_cache = {}
self.logger = frappe.logger("database")
self.logger.setLevel("WARNING")
# self.db_type: str
# self.last_query (lazy) attribute of last sql query executed
@ -122,7 +124,7 @@ class Database:
if execution_timeout := get_query_execution_timeout():
self.set_execution_timeout(execution_timeout)
except Exception as e:
frappe.logger("database").warning(f"Couldn't set execution timeout {e}")
self.logger.warning(f"Couldn't set execution timeout {e}")
def set_execution_timeout(self, seconds: int):
"""Set session speicifc timeout on exeuction of statements.
@ -285,7 +287,13 @@ class Database:
return self.convert_to_lists(self.last_result)
return self.last_result
def _log_query(self, mogrified_query: str, debug: bool = False, explain: bool = False) -> None:
def _log_query(
self,
mogrified_query: str,
debug: bool = False,
explain: bool = False,
unmogrified_query: str = "",
) -> None:
"""Takes the query and logs it to various interfaces according to the settings."""
_query = None
@ -303,6 +311,12 @@ class Database:
_query = _query or str(mogrified_query)
frappe.log(f"<<<< query\n{_query}\n>>>>")
if unmogrified_query and is_query_type(
unmogrified_query, ("alter", "drop", "create", "truncate", "rename")
):
_query = _query or str(mogrified_query)
self.logger.warning("DDL Query made to DB:\n" + _query)
if frappe.flags.in_migrate:
_query = _query or str(mogrified_query)
self.log_touched_tables(_query)
@ -314,7 +328,7 @@ class Database:
# like cursor._transformed_statement from the cursor object. We can also avoid setting
# mogrified_query if we don't need to log it.
mogrified_query = self.lazy_mogrify(query, values)
self._log_query(mogrified_query, debug, explain)
self._log_query(mogrified_query, debug, explain, unmogrified_query=query)
return mogrified_query
def mogrify(self, query: Query, values: QueryValues):
@ -812,6 +826,7 @@ class Database:
fields=fields,
distinct=distinct,
limit=limit,
validate_filters=True,
)
if isinstance(fields, str) and fields == "*":
as_dict = True
@ -840,6 +855,7 @@ class Database:
order_by=order_by,
distinct=distinct,
limit=limit,
validate_filters=True,
).run(debug=debug, run=run, as_dict=as_dict, pluck=pluck)
return {}
@ -889,7 +905,12 @@ class Database:
field, val, modified=modified, modified_by=modified_by, update_modified=update_modified
)
query = frappe.qb.get_query(table=dt, filters=dn, update=True)
query = frappe.qb.get_query(
table=dt,
filters=dn,
update=True,
validate_filters=True,
)
if isinstance(dn, str):
frappe.clear_document_cache(dt, dn)
@ -1057,9 +1078,13 @@ class Database:
cache_count = frappe.cache().get_value(f"doctype:count:{dt}")
if cache_count is not None:
return cache_count
count = frappe.qb.get_query(table=dt, filters=filters, fields=Count("*"), distinct=distinct).run(
debug=debug
)[0][0]
count = frappe.qb.get_query(
table=dt,
filters=filters,
fields=Count("*"),
distinct=distinct,
validate_filters=True,
).run(debug=debug)[0][0]
if not filters and cache:
frappe.cache().set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
return count
@ -1179,7 +1204,12 @@ class Database:
Doctype name can be passed directly, it will be pre-pended with `tab`.
"""
filters = filters or kwargs.get("conditions")
query = frappe.qb.get_query(table=doctype, filters=filters, delete=True)
query = frappe.qb.get_query(
table=doctype,
filters=filters,
delete=True,
validate_filters=True,
)
if "debug" not in kwargs:
kwargs["debug"] = debug
return query.run(**kwargs)
@ -1269,6 +1299,10 @@ class Database:
return get_next_val(*args, **kwargs)
def get_row_size(self, doctype: str) -> int:
"""Get estimated max row size of any table in bytes."""
raise NotImplementedError
def enqueue_jobs_after_commit():
from frappe.utils.background_jobs import (

View file

@ -76,6 +76,10 @@ class MariaDBExceptionUtil:
def is_data_too_long(e: pymysql.Error) -> bool:
return e.args[0] == ER.DATA_TOO_LONG
@staticmethod
def is_db_table_size_limit(e: pymysql.Error) -> bool:
return e.args[0] == ER.TOO_BIG_ROWSIZE
@staticmethod
def is_primary_key_violation(e: pymysql.Error) -> bool:
return (
@ -145,6 +149,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
UnicodeWithAttrs: escape_string,
}
default_port = "3306"
MAX_ROW_SIZE_LIMIT = 65_535 # bytes
def setup_type_map(self):
self.db_type = "mariadb"
@ -200,8 +205,8 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
return db_size[0].get("database_size")
def log_query(self, query, values, debug, explain):
self.last_query = query = self._cursor._executed
self._log_query(query, debug, explain)
self.last_query = self._cursor._executed
self._log_query(self.last_query, debug, explain, query)
return self.last_query
@staticmethod
@ -445,3 +450,56 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
frappe.cache().set_value("db_tables", tables)
return tables
def get_row_size(self, doctype: str) -> int:
"""Get estimated max row size of any table in bytes."""
# Query reused from this answer: https://dba.stackexchange.com/a/313889/274503
# Modification: get values for particular table instead of full summary.
# Reference: https://mariadb.com/kb/en/data-type-storage-requirements/
est_row_size = frappe.db.sql(
"""
SELECT SUM(col_sizes.col_size) AS EST_MAX_ROW_SIZE
FROM (
SELECT
cols.COLUMN_NAME,
CASE cols.DATA_TYPE
WHEN 'tinyint' THEN 1
WHEN 'smallint' THEN 2
WHEN 'mediumint' THEN 3
WHEN 'int' THEN 4
WHEN 'bigint' THEN 8
WHEN 'float' THEN IF(cols.NUMERIC_PRECISION > 24, 8, 4)
WHEN 'double' THEN 8
WHEN 'decimal' THEN ((cols.NUMERIC_PRECISION - cols.NUMERIC_SCALE) DIV 9)*4 + (cols.NUMERIC_SCALE DIV 9)*4 + CEIL(MOD(cols.NUMERIC_PRECISION - cols.NUMERIC_SCALE,9)/2) + CEIL(MOD(cols.NUMERIC_SCALE,9)/2)
WHEN 'bit' THEN (cols.NUMERIC_PRECISION + 7) DIV 8
WHEN 'year' THEN 1
WHEN 'date' THEN 3
WHEN 'time' THEN 3 + CEIL(cols.DATETIME_PRECISION /2)
WHEN 'datetime' THEN 5 + CEIL(cols.DATETIME_PRECISION /2)
WHEN 'timestamp' THEN 4 + CEIL(cols.DATETIME_PRECISION /2)
WHEN 'char' THEN cols.CHARACTER_OCTET_LENGTH
WHEN 'binary' THEN cols.CHARACTER_OCTET_LENGTH
WHEN 'varchar' THEN IF(cols.CHARACTER_OCTET_LENGTH > 255, 2, 1) + cols.CHARACTER_OCTET_LENGTH
WHEN 'varbinary' THEN IF(cols.CHARACTER_OCTET_LENGTH > 255, 2, 1) + cols.CHARACTER_OCTET_LENGTH
WHEN 'tinyblob' THEN 9
WHEN 'tinytext' THEN 9
WHEN 'blob' THEN 10
WHEN 'text' THEN 10
WHEN 'mediumblob' THEN 11
WHEN 'mediumtext' THEN 11
WHEN 'longblob' THEN 12
WHEN 'longtext' THEN 12
WHEN 'enum' THEN 2
WHEN 'set' THEN 8
ELSE 0
END AS col_size
FROM INFORMATION_SCHEMA.COLUMNS cols
WHERE cols.TABLE_NAME = %s
) AS col_sizes;""",
(get_table_name(doctype),),
)
if est_row_size:
return int(est_row_size[0][0])

View file

@ -107,6 +107,10 @@ class PostgresExceptionUtil:
def is_data_too_long(e):
return getattr(e, "pgcode", None) == STRING_DATA_RIGHT_TRUNCATION
@staticmethod
def is_db_table_size_limit(e) -> bool:
return False
class PostgresDatabase(PostgresExceptionUtil, Database):
REGEX_CHARACTER = "~"

View file

@ -9,6 +9,7 @@ from pypika.queries import QueryBuilder, Table
import frappe
from frappe import _
from frappe.database.operator_map import OPERATOR_MAP
from frappe.database.schema import SPECIAL_CHAR_PATTERN
from frappe.database.utils import DefaultOrderBy, get_doctype_name
from frappe.query_builder import Criterion, Field, Order, functions
from frappe.query_builder.functions import Function, SqlFunctions
@ -44,9 +45,12 @@ class Engine:
update: bool = False,
into: bool = False,
delete: bool = False,
*,
validate_filters: bool = False,
) -> QueryBuilder:
self.is_mariadb = frappe.db.db_type == "mariadb"
self.is_postgres = frappe.db.db_type == "postgres"
self.validate_filters = validate_filters
if isinstance(table, Table):
self.table = table
@ -157,14 +161,16 @@ class Engine:
_value = value
_operator = operator
if isinstance(_field, Field):
if not isinstance(_field, str):
pass
elif dynamic_field := DynamicTableField.parse(field, self.doctype):
elif not self.validate_filters and (
dynamic_field := DynamicTableField.parse(field, self.doctype)
):
# apply implicit join if link field's field is referenced
self.query = dynamic_field.apply_join(self.query)
_field = dynamic_field.field
elif has_function(field):
_field = self.get_function_object(field)
elif self.validate_filters and SPECIAL_CHAR_PATTERN.search(_field):
frappe.throw(_("Invalid filter: {0}").format(_field))
elif not doctype or doctype == self.doctype:
_field = self.table[field]
elif doctype:

View file

@ -202,7 +202,11 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
if txt:
search_conditions = [numberCard[field].like(f"%{txt}%") for field in searchfields]
condition_query = frappe.qb.get_query(doctype, filters=filters)
condition_query = frappe.qb.get_query(
doctype,
filters=filters,
validate_filters=True,
)
return (
condition_query.select(numberCard.name, numberCard.label, numberCard.document_type)

View file

@ -36,7 +36,12 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d
ToDo = DocType("ToDo")
User = DocType("User")
count = Count("*").as_("count")
filtered_records = frappe.qb.get_query(doctype, filters=current_filters, fields=["name"])
filtered_records = frappe.qb.get_query(
doctype,
filters=current_filters,
fields=["name"],
validate_filters=True,
)
return (
frappe.qb.from_(ToDo)

View file

@ -206,3 +206,59 @@ class TestWebhook(FrappeTestCase):
enqueue_webhook(doc, wh)
log = frappe.get_last_doc("Webhook Request Log")
self.assertEqual(len(json.loads(log.response)["json"]), 3)
def test_webhook_with_dynamic_url_enabled(self):
wh_config = {
"doctype": "Webhook",
"webhook_doctype": "Note",
"webhook_docevent": "after_insert",
"enabled": 1,
"request_url": "https://httpbin.org/anything/{{ doc.doctype }}",
"is_dynamic_url": 1,
"request_method": "POST",
"request_structure": "JSON",
"webhook_json": "{}",
"meets_condition": "Yes",
"webhook_headers": [
{
"key": "Content-Type",
"value": "application/json",
}
],
}
with get_test_webhook(wh_config) as wh:
doc = frappe.new_doc("Note")
doc.title = "Test Webhook Note"
enqueue_webhook(doc, wh)
log = frappe.get_last_doc("Webhook Request Log")
self.assertEqual(json.loads(log.response)["url"], "https://httpbin.org/anything/Note")
def test_webhook_with_dynamic_url_disabled(self):
wh_config = {
"doctype": "Webhook",
"webhook_doctype": "Note",
"webhook_docevent": "after_insert",
"enabled": 1,
"request_url": "https://httpbin.org/anything/{{doc.doctype}}",
"is_dynamic_url": 0,
"request_method": "POST",
"request_structure": "JSON",
"webhook_json": "{}",
"meets_condition": "Yes",
"webhook_headers": [
{
"key": "Content-Type",
"value": "application/json",
}
],
}
with get_test_webhook(wh_config) as wh:
doc = frappe.new_doc("Note")
doc.title = "Test Webhook Note"
enqueue_webhook(doc, wh)
log = frappe.get_last_doc("Webhook Request Log")
self.assertEqual(
json.loads(log.response)["url"], "https://httpbin.org/anything/{{doc.doctype}}"
)

View file

@ -18,8 +18,9 @@
"html_condition",
"sb_webhook",
"request_url",
"request_method",
"is_dynamic_url",
"cb_webhook",
"request_method",
"request_structure",
"sb_security",
"enable_security",
@ -202,6 +203,13 @@
{
"fieldname": "section_break_28",
"fieldtype": "Section Break"
},
{
"default": "0",
"description": "On checking this option, URL will be treated like a jinja template string",
"fieldname": "is_dynamic_url",
"fieldtype": "Check",
"label": "Is Dynamic URL?"
}
],
"links": [
@ -210,7 +218,7 @@
"link_fieldname": "webhook"
}
],
"modified": "2023-05-21 15:42:58.844826",
"modified": "2023-05-22 16:30:10.740512",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",

View file

@ -115,29 +115,34 @@ def enqueue_webhook(doc, webhook) -> None:
webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name"))
headers = get_webhook_headers(doc, webhook)
data = get_webhook_data(doc, webhook)
r = None
if webhook.is_dynamic_url:
request_url = frappe.render_template(webhook.request_url, get_context(doc))
else:
request_url = webhook.request_url
r = None
for i in range(3):
try:
r = requests.request(
method=webhook.request_method,
url=webhook.request_url,
url=request_url,
data=json.dumps(data, default=str),
headers=headers,
timeout=5,
)
r.raise_for_status()
frappe.logger().debug({"webhook_success": r.text})
log_request(webhook.name, doc.name, webhook.request_url, headers, data, r)
log_request(webhook.name, doc.name, request_url, headers, data, r)
break
except requests.exceptions.ReadTimeout as e:
frappe.logger().debug({"webhook_error": e, "try": i + 1})
log_request(webhook.name, doc.name, webhook.request_url, headers, data)
log_request(webhook.name, doc.name, request_url, headers, data)
except Exception as e:
frappe.logger().debug({"webhook_error": e, "try": i + 1})
log_request(webhook.name, doc.name, webhook.request_url, headers, data, r)
log_request(webhook.name, doc.name, request_url, headers, data, r)
sleep(3 * i + 1)
if i != 2:
continue

View file

@ -530,7 +530,7 @@ class BaseDocument:
if not ignore_if_duplicate:
frappe.msgprint(
_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)),
_("{0} {1} already exists").format(_(self.doctype), frappe.bold(self.name)),
title=_("Duplicate Name"),
indicator="red",
)

View file

@ -62,6 +62,7 @@ def get_mapped_doc(
postprocess=None,
ignore_permissions=False,
ignore_child_tables=False,
cached=False,
):
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
@ -79,7 +80,10 @@ def get_mapped_doc(
):
target_doc.raise_no_permission_to("create")
source_doc = frappe.get_doc(from_doctype, from_docname)
if cached:
source_doc = frappe.get_cached_doc(from_doctype, from_docname)
else:
source_doc = frappe.get_doc(from_doctype, from_docname)
if not ignore_permissions:
if not source_doc.has_permission("read"):
@ -255,7 +259,9 @@ def map_fetch_fields(target_doc, df, no_copy_fields):
def map_child_doc(source_d, target_parent, table_map, source_parent=None):
target_child_doctype = table_map["doctype"]
target_parentfield = target_parent.get_parentfield_of_doctype(target_child_doctype)
target_d = frappe.new_doc(target_child_doctype, target_parent, target_parentfield)
target_d = frappe.new_doc(
target_child_doctype, parent_doc=target_parent, parentfield=target_parentfield
)
map_doc(source_d, target_d, table_map, source_parent)

View file

@ -44,7 +44,7 @@ def execute():
if field:
field.update(cf)
else:
df = frappe.new_doc("DocField", meta, "fields")
df = frappe.new_doc("DocField", parent_doc=meta, parentfield="fields")
df.update(cf)
meta.fields.append(df)
frappe.db.delete("Custom Field", {"name": cf.name})

View file

@ -245,6 +245,7 @@
"default": "14",
"fieldname": "font_size",
"fieldtype": "Int",
"hidden": 1,
"label": "Font Size"
},
{
@ -258,7 +259,7 @@
"icon": "fa fa-print",
"idx": 1,
"links": [],
"modified": "2022-11-09 15:29:46.709305",
"modified": "2023-05-31 15:40:52.919029",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",

View file

@ -47,7 +47,7 @@
"default": "1",
"fieldname": "repeat_header_footer",
"fieldtype": "Check",
"label": "Repeat Header and Footer in PDF"
"label": "Repeat Header and Footer"
},
{
"fieldname": "column_break_4",
@ -176,7 +176,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-09-17 12:59:14.783694",
"modified": "2023-05-30 14:55:25.740691",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Settings",
@ -193,5 +193,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -91,7 +91,7 @@ frappe.ui.form.PrintView = class {
fieldtype: "Link",
fieldname: "print_format",
options: "Print Format",
placeholder: __("Print Format"),
label: __("Print Format"),
get_query: () => {
return { filters: { doc_type: this.frm.doctype } };
},
@ -101,7 +101,7 @@ frappe.ui.form.PrintView = class {
this.language_selector = this.add_sidebar_item({
fieldtype: "Link",
fieldname: "language",
placeholder: __("Language"),
label: __("Language"),
options: "Language",
change: () => {
this.set_user_lang();
@ -109,12 +109,27 @@ frappe.ui.form.PrintView = class {
},
}).$input;
let description = "";
if (!cint(this.print_settings.repeat_header_footer)) {
description =
"<div class='form-message yellow p-3 mt-3'>" +
__("Footer might not be visible as {0} option is disabled</div>", [
`<a href="/app/print-settings/Print Settings">${__(
"Repeat Header and Footer"
)}</a>`,
]);
}
const print_view = this;
this.letterhead_selector = this.add_sidebar_item({
fieldtype: "Link",
fieldname: "letterhead",
options: "Letter Head",
placeholder: __("Letter Head"),
change: () => this.preview(),
label: __("Letter Head"),
description: description,
change: function () {
this.set_description(this.get_value() ? description : "");
print_view.preview();
},
}).$input;
this.sidebar_dynamic_section = $(`<div class="dynamic-settings"></div>`).appendTo(
this.sidebar

View file

@ -22,6 +22,29 @@ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form.
};
}
refresh() {
this.show_db_utilization();
}
show_db_utilization() {
const doctype = this.frm.doc.doc_type || this.frm.doc.name;
frappe
.xcall("frappe.core.doctype.doctype.doctype.get_row_size_utilization", {
doctype,
})
.then((r) => {
if (r < 50.0) return;
this.frm.dashboard.show_progress(
__("Database Row Size Utilization"),
r,
__(
"Database Table Row Size Utilization: {0}%, this limits number of fields you can add.",
[r]
)
);
});
}
max_attachments() {
if (!this.frm.doc.max_attachments) {
return;

View file

@ -18,13 +18,11 @@ frappe.ui.form.ControlMultiCheck = class ControlMultiCheck extends frappe.ui.for
this.$checkbox_area = $(`<div class="checkbox-options ${row}"></div>`).appendTo(
this.wrapper
);
this.refresh();
}
refresh() {
this.set_options();
this.bind_checkboxes();
this.refresh_input();
super.refresh();
}

View file

@ -365,8 +365,14 @@ frappe.form.formatters = {
</div>`
: "";
},
Attach: format_attachment_url,
AttachImage: format_attachment_url,
};
function format_attachment_url(url) {
return url ? `<a href="${url}" target="_blank">${url}</a>` : "";
}
frappe.form.get_formatter = function (fieldtype) {
if (!fieldtype) fieldtype = "Data";
return frappe.form.formatters[fieldtype.replace(/ /g, "")] || frappe.form.formatters.Data;

View file

@ -5,7 +5,7 @@
{% } %}
<div class="col-md-4">
<div class="form-link-title">
<span>{{ __(transactions[i].label) }}<span>
<span>{{ __(transactions[i].label) }}</span>
</div>
{% for (let j=0; j < transactions[i].items.length; j++) { %}
{% let doctype = transactions[i].items[j]; %}

View file

@ -1029,7 +1029,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return `<span class="indicator-pill ${indicator[1]} filterable ellipsis"
data-filter='${indicator[2]}' title='${title}'>
<span class="ellipsis"> ${__(indicator[0])}</span>
<span>`;
</span>`;
}
return "";
}

View file

@ -747,7 +747,10 @@ frappe.views.CommunicationComposer = class {
this.content_set = true;
}
message += await this.get_signature(sender_email || null);
const signature = await this.get_signature(sender_email || "");
if (!this.content_set || !strip_html(message).includes(strip_html(signature))) {
message += signature;
}
if (this.is_a_reply && !this.reply_set) {
message += this.get_earlier_reply();

View file

@ -353,7 +353,7 @@ frappe.views.Workspace = class Workspace {
let current_page = pages.filter((p) => p.title == page.name)[0];
this.content = current_page && JSON.parse(current_page.content);
this.add_custom_cards_in_content();
this.content && this.add_custom_cards_in_content();
$(".item-anchor").addClass("disable-click");

View file

@ -29,6 +29,13 @@ frappe.ui.OnboardingTour = class OnboardingTour {
step.popover.node.offsetTop + step.options.step_info.offset_y
}px`;
}
if (step.popover.node.offsetLeft < 0) {
step.popover.node.style.minWidth = "200px";
step.popover.node.style.maxWidth = `${
350 + step.popover.node.offsetLeft
}px`;
step.popover.node.style.left = "0px";
}
if (step.popover.closeBtnNode) {
step.popover.closeBtnNode.onclick = () => {
this.on_finish && this.on_finish();

View file

@ -32,9 +32,9 @@ class TelemetryManager {
}
}
capture(event, app) {
capture(event, app, props) {
if (!this.enabled) return;
posthog.capture(`${app}_${event}`);
posthog.capture(`${app}_${event}`, props);
}
disable() {
@ -49,7 +49,7 @@ class TelemetryManager {
if (!last || moment(now).diff(moment(last), "hours") > 12) {
localStorage.setItem(KEY, now.toISOString());
this.capture("heartbeat", "frappe");
this.capture("heartbeat", "frappe", { frappe_version: frappe.boot?.versions?.frappe });
}
}

View file

@ -45,9 +45,6 @@
.layout-side-section.print-preview-sidebar {
padding-right: var(--padding-md);
.clearfix {
display: none;
}
.label-area {
white-space: nowrap;

View file

@ -169,6 +169,10 @@ class TestDocument(FrappeTestCase):
with self.assertQueryCount(0):
user.db_set("user_type", "Magical Wizard")
def test_new_doc_with_fields(self):
user = frappe.new_doc("User", first_name="wizard")
self.assertEqual(user.first_name, "wizard")
def test_update_after_submit(self):
d = self.test_insert()
d.starts_on = "2014-09-09"

View file

@ -218,13 +218,6 @@ class TestQuery(FrappeTestCase):
@run_only_if(db_type_is.MARIADB)
def test_filters(self):
self.assertEqual(
frappe.qb.get_query(
"User", filters={"IfNull(name, " ")": ("<", Now())}, fields=["Max(name)"]
).run(),
frappe.qb.from_("User").select(Max(Field("name"))).where(Ifnull("name", "") < Now()).run(),
)
self.assertEqual(
frappe.qb.get_query(
"DocType",
@ -258,6 +251,17 @@ class TestQuery(FrappeTestCase):
),
)
self.assertRaisesRegex(
frappe.ValidationError,
"Invalid filter",
lambda: frappe.qb.get_query(
"DocType",
fields=["name"],
filters={"permissions.role": "System Manager"},
validate_filters=True,
),
)
self.assertEqual(
frappe.qb.get_query(
"DocType",

View file

@ -31,6 +31,7 @@ def get_monthly_results(
Function(aggregation, goal_field),
],
filters=filters,
validate_filters=True,
)
.groupby("month_year")
.run()

View file

@ -31,6 +31,10 @@
"allow_incomplete",
"section_break_2",
"max_attachment_size",
"section_break_xzqr",
"condition",
"column_break_tjgl",
"condition_description",
"section_break_3",
"list_setting_message",
"show_list",
@ -279,10 +283,6 @@
"fieldtype": "Tab Break",
"label": "Form"
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_1",
"fieldtype": "Section Break"
@ -297,7 +297,6 @@
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
@ -374,13 +373,33 @@
"fieldname": "anonymous",
"fieldtype": "Check",
"label": "Anonymous"
},
{
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition",
"max_height": "150px"
},
{
"fieldname": "section_break_xzqr",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_tjgl",
"fieldtype": "Column Break"
},
{
"fieldname": "condition_description",
"fieldtype": "HTML",
"label": "Condition Description",
"options": "<p>Multiple webforms can be created for a single doctype. Write a condition specific to this webform to display correct record after submission.</p><p>For Example:</p>\n<p>If you create a separate webform every year to capture feedback from employees add a \n field named year in doctype and add a condition <b>doc.year==\"2023\"</b></p>\n"
}
],
"has_web_view": 1,
"icon": "icon-edit",
"is_published_field": "published",
"links": [],
"modified": "2023-04-20 17:24:42.657731",
"modified": "2023-06-03 19:18:56.760479",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form",

View file

@ -153,10 +153,16 @@ def get_context(context):
and not frappe.form_dict.name
and not frappe.form_dict.is_list
):
name = frappe.db.get_value(self.doc_type, {"owner": frappe.session.user}, "name")
if name:
context.in_view_mode = True
frappe.redirect(f"/{self.route}/{name}")
names = frappe.db.get_values(self.doc_type, {"owner": frappe.session.user}, pluck="name")
for name in names:
if self.condition:
doc = frappe.get_doc(self.doc_type, name)
if frappe.safe_eval(self.condition, None, {"doc": doc.as_dict()}):
context.in_view_mode = True
frappe.redirect(f"/{self.route}/{name}")
else:
context.in_view_mode = True
frappe.redirect(f"/{self.route}/{name}")
# Show new form when
# - User is Guest

View file

@ -9,7 +9,13 @@ from frappe.model.document import Document
class WebPageView(Document):
pass
@staticmethod
def clear_old_logs(days=180):
from frappe.query_builder import Interval
from frappe.query_builder.functions import Now
table = frappe.qb.DocType("Web Page View")
frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
@frappe.whitelist(allow_guest=True)