Merge branch 'frappe:develop' into integrate_frappe_notification

This commit is contained in:
Tanmoy Sarkar 2024-01-08 16:52:27 +05:30 committed by GitHub
commit a084f91e18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 217 additions and 178 deletions

View file

@ -224,8 +224,8 @@ context("View", () => {
});
});
it("Route to Settings Workspace", () => {
cy.visit("/app/settings");
cy.get(".title-text").should("contain", "Settings");
it("Route to Website Workspace", () => {
cy.visit("/app/website");
cy.get(".title-text").should("contain", "Website");
});
});

View file

@ -7,8 +7,8 @@ context("Workspace 2.0", () => {
it("Navigate to page from sidebar", () => {
cy.visit("/app/build");
cy.get(".codex-editor__redactor .ce-block");
cy.get('.sidebar-item-container[item-name="Settings"]').first().click();
cy.location("pathname").should("eq", "/app/settings");
cy.get('.sidebar-item-container[item-name="Website"]').first().click();
cy.location("pathname").should("eq", "/app/website");
});
it("Create Private Page", () => {

View file

@ -1002,19 +1002,11 @@ def has_permission(
)
if throw and not out:
# mimics frappe.throw
document_label = (
f"{_(doctype)} {doc if isinstance(doc, str) else doc.name}" if doc else _(doctype)
)
msgprint(
_("No permission for {0}").format(document_label),
raise_exception=ValidationError,
title=None,
indicator="red",
is_minimizable=None,
wide=None,
as_list=False,
)
frappe.flags.error_message = _("No permission for {0}").format(document_label)
raise frappe.PermissionError
return out

View file

@ -270,9 +270,6 @@ def get_user_info():
user_info = frappe._dict()
add_user_info(frappe.session.user, user_info)
if frappe.session.user == "Administrator" and user_info.Administrator.email:
user_info[user_info.Administrator.email] = user_info.Administrator
return user_info

View file

@ -267,6 +267,7 @@ frappe.ui.form.on("Communication", {
$.extend(args, {
subject: __("Re: {0}", [frm.doc.subject]),
recipients: frm.doc.sender,
is_a_reply: true,
});
new frappe.views.CommunicationComposer(args);
@ -278,6 +279,7 @@ frappe.ui.form.on("Communication", {
subject: __("Res: {0}", [frm.doc.subject]),
recipients: frm.doc.sender,
cc: frm.doc.cc,
is_a_reply: true,
});
new frappe.views.CommunicationComposer(args);
},
@ -287,6 +289,7 @@ frappe.ui.form.on("Communication", {
$.extend(args, {
forward: true,
subject: __("Fw: {0}", [frm.doc.subject]),
is_a_reply: true,
});
new frappe.views.CommunicationComposer(args);

View file

@ -234,6 +234,7 @@ class DocType(Document):
"DocPerm",
"Custom Field",
"Customize Form Field",
"Web Form Field",
"DocField",
]

View file

@ -14,6 +14,7 @@
"cmd",
"time",
"duration",
"event_type",
"section_break_1skt",
"request_headers",
"section_break_sgro",
@ -30,6 +31,7 @@
"label": "Path"
},
{
"depends_on": "eval:doc.event_type==\"HTTP Request\"",
"fieldname": "cmd",
"fieldtype": "Data",
"in_standard_filter": 1,
@ -67,6 +69,7 @@
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.event_type==\"HTTP Request\"",
"fieldname": "request_headers",
"fieldtype": "Code",
"label": "Request Headers"
@ -76,11 +79,13 @@
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.event_type==\"HTTP Request\"",
"fieldname": "form_dict",
"fieldtype": "Code",
"label": "Form Dict"
},
{
"depends_on": "eval:doc.event_type==\"HTTP Request\"",
"fieldname": "method",
"fieldtype": "Select",
"in_standard_filter": 1,
@ -96,6 +101,12 @@
{
"fieldname": "section_break_9jhm",
"fieldtype": "Section Break"
},
{
"fieldname": "event_type",
"fieldtype": "Data",
"hidden": 1,
"label": "Event Type"
}
],
"hide_toolbar": 1,
@ -103,7 +114,7 @@
"index_web_pages_for_search": 1,
"is_virtual": 1,
"links": [],
"modified": "2023-08-10 12:01:03.456643",
"modified": "2024-01-03 16:45:47.110048",
"modified_by": "Administrator",
"module": "Core",
"name": "Recorder",

View file

@ -19,6 +19,7 @@ class Recorder(Document):
cmd: DF.Data | None
duration: DF.Float
event_type: DF.Data | None
form_dict: DF.Code | None
method: DF.Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
number_of_queries: DF.Int
@ -27,7 +28,6 @@ class Recorder(Document):
sql_queries: DF.Table[RecorderQuery]
time: DF.Datetime | None
time_in_queries: DF.Float
# end: auto-generated types
def load_from_db(self):

View file

@ -362,7 +362,8 @@ def rename_fieldname(custom_field: str, fieldname: str):
frappe.msgprint(_("Old and new fieldnames are same."), alert=True)
return
frappe.db.rename_column(parent_doctype, old_fieldname, new_fieldname)
if frappe.db.has_column(field.dt, old_fieldname):
frappe.db.rename_column(parent_doctype, old_fieldname, new_fieldname)
# Update in DB after alter column is successful, alter column will implicitly commit, so it's
# best to commit change on field too to avoid any possible mismatch between two.

View file

@ -10,7 +10,7 @@ from frappe.model.naming import append_number_if_name_exists
from frappe.modules.export_file import export_to_files
from frappe.query_builder import Criterion
from frappe.query_builder.utils import DocType
from frappe.utils import cint
from frappe.utils import cint, flt
class NumberCard(Document):
@ -165,7 +165,7 @@ def get_result(doc, filters, to_date=None):
)
number = res[0]["result"] if res else 0
return cint(number)
return flt(number)
@frappe.whitelist()

View file

@ -268,15 +268,15 @@ class EmailAccount(Document):
if not in_receive and self.use_imap:
email_server.imap.logout()
# reset failed attempts count
self.set_failed_attempts_count(0)
return email_server
def check_email_server_connection(self, email_server, in_receive):
# tries to connect to email server and handles failure
try:
email_server.connect()
# reset failed attempts count - do it after succesful connection
self.set_failed_attempts_count(0)
except (error_proto, imaplib.IMAP4.error) as e:
message = cstr(e).lower().replace(" ", "")
auth_error_codes = [
@ -294,6 +294,8 @@ class EmailAccount(Document):
error_message = _(
"Authentication failed while receiving emails from Email Account: {0}."
).format(self.name)
error_message = _("Email Account Disabled.") + " " + error_message
error_message += "<br>" + _("Message from server: {0}").format(cstr(e))
self.handle_incoming_connect_error(description=error_message)
return None
@ -489,31 +491,35 @@ class EmailAccount(Document):
state.pop("_smtp_server_instance", None)
def handle_incoming_connect_error(self, description):
if self.get_failed_attempts_count() > 2:
self.db_set("enable_incoming", 0)
for user in get_system_managers(only_name=True):
try:
assign_to.add(
{
"assign_to": user,
"doctype": self.doctype,
"name": self.name,
"description": description,
"priority": "High",
"notify": 1,
}
)
except assign_to.DuplicateToDoError:
frappe.clear_last_message()
if self.get_failed_attempts_count() > 5:
# This is done in background to avoid committing here.
frappe.enqueue(self._disable_broken_incoming_account, description=description)
else:
self.set_failed_attempts_count(self.get_failed_attempts_count() + 1)
def _disable_broken_incoming_account(self, description):
self.db_set("enable_incoming", 0)
for user in get_system_managers(only_name=True):
try:
assign_to.add(
{
"assign_to": [user],
"doctype": self.doctype,
"name": self.name,
"description": description,
"priority": "High",
"notify": 1,
}
)
except assign_to.DuplicateToDoError:
pass
def set_failed_attempts_count(self, value):
frappe.cache.set(f"{self.name}:email-account-failed-attempts", value)
frappe.cache.set_value(f"{self.name}:email-account-failed-attempts", value)
def get_failed_attempts_count(self):
return cint(frappe.cache.get(f"{self.name}:email-account-failed-attempts"))
return cint(frappe.cache.get_value(f"{self.name}:email-account-failed-attempts"))
def receive(self):
"""Called by scheduler to receive emails from this EMail account using POP3/IMAP."""

View file

@ -428,6 +428,7 @@ before_request = [
# Background Job Hooks
before_job = [
"frappe.recorder.record",
"frappe.monitor.start",
]
@ -438,6 +439,7 @@ if os.getenv("FRAPPE_SENTRY_DSN") and (
before_job.append("frappe.utils.sentry.set_sentry_context")
after_job = [
"frappe.recorder.dump",
"frappe.monitor.stop",
"frappe.utils.file_lock.release_document_locks",
"frappe.utils.telemetry.flush",

View file

@ -153,15 +153,14 @@ def get_context(doc):
def enqueue_webhook(doc, webhook) -> None:
request_url = headers = data = None
try:
webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name"))
headers = get_webhook_headers(doc, webhook)
data = get_webhook_data(doc, webhook)
request_url = webhook.request_url
if webhook.is_dynamic_url:
request_url = frappe.render_template(webhook.request_url, get_context(doc))
else:
request_url = webhook.request_url
headers = get_webhook_headers(doc, webhook)
data = get_webhook_data(doc, webhook)
except Exception as e:
frappe.logger().debug({"enqueue_webhook_error": e})

View file

@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import copy
import functools
import frappe
import frappe.share
@ -37,17 +38,23 @@ AUTOMATIC_ROLES = (GUEST_ROLE, ALL_USER_ROLE, SYSTEM_USER_ROLE, ADMIN_ROLE)
def print_has_permission_check_logs(func):
@functools.wraps(func)
def inner(*args, **kwargs):
frappe.flags["has_permission_check_logs"] = []
result = func(*args, **kwargs)
self_perm_check = True if not kwargs.get("user") else kwargs.get("user") == frappe.session.user
raise_exception = kwargs.get("raise_exception", True)
self_perm_check = True if not kwargs.get("user") else kwargs.get("user") == frappe.session.user
if raise_exception:
frappe.flags["has_permission_check_logs"] = []
result = func(*args, **kwargs)
# print only if access denied
# and if user is checking his own permission
if not result and self_perm_check and raise_exception:
msgprint(("<br>").join(frappe.flags.get("has_permission_check_logs", [])))
frappe.flags.pop("has_permission_check_logs", None)
if raise_exception:
frappe.flags.pop("has_permission_check_logs", None)
return result
return inner
@ -163,9 +170,6 @@ def get_doc_permissions(doc, user=None, ptype=None):
if not user:
user = frappe.session.user
if frappe.is_table(doc.doctype):
return {"read": 1, "write": 1}
meta = frappe.get_meta(doc.doctype)
def is_user_owner():

View file

@ -85,33 +85,15 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
};
}
init_option_cache() {
if (!this.$input.cache) {
this.$input.cache = {};
}
if (!this.$input.cache[this.doctype]) {
this.$input.cache[this.doctype] = {};
}
if (!this.$input.cache[this.doctype][this.df.fieldname]) {
this.$input.cache[this.doctype][this.df.fieldname] = {};
}
}
setup_awesomplete() {
this.awesomplete = new Awesomplete(this.input, this.get_awesomplete_settings());
$(this.input_area).find(".awesomplete ul").css("min-width", "100%");
this.init_option_cache();
this.$input.on(
"input",
frappe.utils.debounce((e) => {
const cached_options =
this.$input.cache[this.doctype][this.df.fieldname][e.target.value];
if (cached_options && cached_options.length) {
this.set_data(cached_options);
} else if (this.get_query || this.df.get_query) {
if (this.get_query || this.df.get_query) {
this.execute_query_if_exists(e.target.value);
} else {
this.awesomplete.list = this.get_data();
@ -245,7 +227,6 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
if (!this.$input.is(":focus")) {
return;
}
this.$input.cache[this.doctype][this.df.fieldname][term] = message;
this.set_data(message);
},
});

View file

@ -429,10 +429,10 @@ export default class GridRow {
$(`
<div class='form-group'>
<div class='row' style='margin:0px; margin-bottom:10px;'>
<div class='col-md-8'>
<div class='col-6 col-md-8'>
${__("Fieldname").bold()}
</div>
<div class='col-md-4' style='padding-left:5px;'>
<div class='col-6 col-md-4' style='padding-left:5px;'>
${__("Column Width").bold()}
</div>
</div>
@ -522,13 +522,13 @@ export default class GridRow {
data-label='${docfield.label}' data-type='${docfield.fieldtype}'>
<div class='row'>
<div class='col-md-1' style='padding-top: 4px;'>
<div class='col-1' style='padding-top: 4px;'>
<a style='cursor: grabbing;'>${frappe.utils.icon("drag", "xs")}</a>
</div>
<div class='col-md-8' style='padding-right:0px; padding-top: 5px;'>
<div class='col-6 col-md-8' style='padding-right:0px; padding-top: 5px;'>
${__(docfield.label)}
</div>
<div class='col-md-2' style='padding-left:0px; padding-top: 2px; margin-top:-2px;' title='${__(
<div class='col-3 col-md-2' style='padding-left:0px; padding-top: 2px; margin-top:-2px;' title='${__(
"Columns"
)}'>
<input class='form-control column-width my-1 input-xs text-right'
@ -536,7 +536,7 @@ export default class GridRow {
value='${docfield.columns || cint(d.columns)}'
data-fieldname='${docfield.fieldname}' style='background-color: var(--modal-bg); display: inline'>
</div>
<div class='col-md-1' style='padding-top: 3px;'>
<div class='col-1' style='padding-top: 3px;'>
<a class='text-muted remove-field' data-fieldname='${docfield.fieldname}'>
<i class='fa fa-trash-o' aria-hidden='true'></i>
</a>

View file

@ -114,13 +114,13 @@ export default class ListSettings {
data-label="${me.fields[idx].label}" data-type="${me.fields[idx].type}">
<div class="row">
<div class="col-md-1">
<div class="col-1">
${frappe.utils.icon("drag", "xs", "", "", "sortable-handle " + show_sortable_handle)}
</div>
<div class="col-md-10" style="padding-left:0px;">
<div class="col-10" style="padding-left:0px;">
${me.fields[idx].label}
</div>
<div class="col-md-1 ${can_remove}">
<div class="col-1 ${can_remove}">
<a class="text-muted remove-field" data-fieldname="${me.fields[idx].fieldname}">
${frappe.utils.icon("delete", "xs")}
</a>

View file

@ -37,6 +37,10 @@ $("body").on("click", "a", function (e) {
const href = target_element.getAttribute("href");
const is_on_same_host = target_element.hostname === window.location.hostname;
if (target_element.getAttribute("target") === "_blank") {
return;
}
const override = (route) => {
e.preventDefault();
frappe.set_route(route);

View file

@ -111,10 +111,9 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
//Setup groupby for reports
this.group_by_control = new frappe.ui.GroupBy(this);
if (this.report_doc && this.report_doc.json.group_by) {
if (this.report_doc?.json?.group_by) {
this.group_by_control.apply_settings(this.report_doc.json.group_by);
}
if (this.view_user_settings && this.view_user_settings.group_by) {
} else if (this.view_user_settings?.group_by) {
this.group_by_control.apply_settings(this.view_user_settings.group_by);
}
}

View file

@ -235,7 +235,6 @@ frappe.views.TreeView = class TreeView {
method: "frappe.utils.nestedset.rebuild_tree",
args: {
doctype: me.doctype,
parent_field: "parent_" + me.doctype.toLowerCase().replace(/ /g, "_"),
},
callback: function (r) {
if (!r.exc) {

View file

@ -32,7 +32,7 @@ export default class Card extends Block {
if (this.data && this.data.card_name) {
let has_data = this.make("card", this.data.card_name, "links");
if (!has_data) return;
if (!has_data) return this.wrapper;
}
if (!this.readOnly) {

View file

@ -33,7 +33,7 @@ export default class Chart extends Block {
if (this.data && this.data.chart_name) {
let has_data = this.make("chart", this.data.chart_name);
if (!has_data) return;
if (!has_data) return this.wrapper;
}
if (!this.readOnly) {

View file

@ -32,7 +32,7 @@ export default class CustomBlock extends Block {
if (this.data && this.data.custom_block_name) {
let has_data = this.make("custom_block", this.data.custom_block_name);
if (!has_data) return;
if (!has_data) return this.wrapper;
}
if (!this.readOnly) {

View file

@ -33,7 +33,7 @@ export default class NumberCard extends Block {
if (this.data && this.data.number_card_name) {
let has_data = this.make("number_card", this.data.number_card_name);
if (!has_data) return;
if (!has_data) return this.wrapper;
}
if (!this.readOnly) {

View file

@ -113,7 +113,7 @@ export default class Onboarding extends Block {
if (this.data && this.data.onboarding_name) {
let has_data = this.make("onboarding", this.data.onboarding_name);
if (!has_data) return;
if (!has_data) return this.wrapper;
}
if (!this.readOnly) {

View file

@ -33,7 +33,7 @@ export default class QuickList extends Block {
if (this.data && this.data.quick_list_name) {
let has_data = this.make("quick_list", this.data.quick_list_name);
if (!has_data) return;
if (!has_data) return this.wrapper;
}
if (!this.readOnly) {

View file

@ -52,7 +52,7 @@ export default class Shortcut extends Block {
if (this.data && this.data.shortcut_name) {
let has_data = this.make("shortcut", this.data.shortcut_name);
if (!has_data) return;
if (!has_data) return this.wrapper;
}
if (!this.readOnly) {

View file

@ -39,7 +39,6 @@ class TelemetryManager {
disable() {
this.enabled = false;
posthog.opt_out_capturing();
}
can_enable() {

View file

@ -4,6 +4,9 @@ body {
@include media-breakpoint-up(sm) {
background-color: var(--bg-light-gray);
}
.page-content-wrapper {
min-height: calc(100vh - 220px);
}
.web-footer {
display: none;
}

View file

@ -8,6 +8,7 @@ import re
import time
from collections import Counter
from collections.abc import Callable
from enum import Enum
import sqlparse
@ -21,6 +22,12 @@ RECORDER_REQUEST_HASH = "recorder-requests"
TRACEBACK_PATH_PATTERN = re.compile(".*/apps/")
class RecorderEvent(str, Enum):
HTTP_REQUEST = "HTTP Request"
BACKGROUND_JOB = "Background Job"
INVALID = "Invalid"
def sql(*args, **kwargs):
start_time = time.monotonic()
result = frappe.db._sql(*args, **kwargs)
@ -121,7 +128,10 @@ def normalize_query(query: str) -> str:
for token in q.flatten():
if "Token.Literal" in str(token.ttype):
token.value = "?"
return str(q)
# Transform IN parts like this: IN (?, ?, ?) -> IN (?)
q = re.sub(r"( IN )\(\?[\s\n\?\,]*\)", r"\1(?)", str(q), flags=re.IGNORECASE)
return q
except Exception as e:
print("Failed to normalize query ", e)
@ -151,7 +161,16 @@ class Recorder:
self.method = frappe.request.method
self.headers = dict(frappe.local.request.headers)
self.form_dict = frappe.local.form_dict
self.event_type = RecorderEvent.HTTP_REQUEST
elif frappe.job:
self.event_type = RecorderEvent.BACKGROUND_JOB
self.path = frappe.job.method
self.cmd = None
self.method = None
self.headers = None
self.form_dict = None
else:
self.event_type = RecorderEvent.INVALID
self.path = None
self.cmd = None
self.method = None
@ -173,6 +192,7 @@ class Recorder:
"time_queries": float("{:0.3f}".format(sum(call["duration"] for call in self.calls))),
"duration": float(f"{(datetime.datetime.now() - self.time).total_seconds() * 1000:0.3f}"),
"method": self.method,
"event_type": self.event_type,
}
frappe.cache.hset(RECORDER_REQUEST_SPARSE_HASH, self.uuid, request_data)
frappe.publish_realtime(

View file

@ -2,6 +2,7 @@
// don't remove this line (used in test)
window.disable_signup = {{ disable_signup and "true" or "false" }};
window.show_footer_on_login = {{ show_footer_on_login and "true" or "false" }};
window.login = {};
@ -305,6 +306,10 @@ frappe.ready(function () {
$(window).trigger("hashchange");
}
if (window.show_footer_on_login) {
$("body .web-footer").show();
}
$(".form-signup, .form-forgot, .form-login-with-email-link").removeClass("hide");
$(document).trigger('login_rendered');
});

View file

@ -361,8 +361,10 @@ class TestEmailIntegrationTest(FrappeTestCase):
subject = "checking if email works"
content = "is email working?"
frappe.sendmail(sender=sender, recipients=recipients, subject=subject, content=content, now=True)
email = frappe.get_last_doc("Email Queue")
email = frappe.sendmail(
sender=sender, recipients=recipients, subject=subject, content=content, now=True
)
email.reload()
self.assertEqual(email.sender, sender)
self.assertEqual(len(email.recipients), 2)
self.assertEqual(email.status, "Sent")

View file

@ -145,7 +145,7 @@ class TestNestedSet(FrappeTestCase):
leaf_node.reload()
def test_rebuild_tree(self):
rebuild_tree(TEST_DOCTYPE, "parent_test_tree_doctype")
rebuild_tree(TEST_DOCTYPE)
self.test_basic_tree()
def test_move_group_into_another(self):

View file

@ -122,7 +122,13 @@ class TestRecorder(FrappeTestCase):
frappe.recorder.post_process()
requests = frappe.recorder.get()
request = frappe.recorder.get(requests[0]["uuid"])
request = frappe.recorder.get(
next(
request
for request in requests
if request["event_type"] == frappe.recorder.RecorderEvent.HTTP_REQUEST
)["uuid"]
)
for query, call in zip(queries, request["calls"]):
self.assertEqual(call["exact_copies"], query[1])
@ -152,6 +158,7 @@ class TestQueryNormalization(FrappeTestCase):
"select * from `user` where a > 5": "select * from `user` where a > ?",
"select `name` from `user`": "select `name` from `user`",
"select `name` from `user` limit 10": "select `name` from `user` limit ?",
"select `name` from `user` where name in ('a', 'b', 'c')": "select `name` from `user` where name in (?)",
}
for query, normalized in test_cases.items():

View file

@ -199,7 +199,7 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True,
method_name = method
method = frappe.get_attr(method)
else:
method_name = cstr(method.__name__)
method_name = f"{method.__module__}.{method.__qualname__}"
actual_func_name = kwargs.get("job_type") if "run_scheduled_job" in method_name else method_name
setproctitle.setproctitle(f"rq: Started running {actual_func_name} at {time.time()}")

View file

@ -168,11 +168,8 @@ def update_move_node(doc: Document, parent_field: str):
@frappe.whitelist()
def rebuild_tree(doctype, parent_field):
"""
call rebuild_node for all root nodes
"""
def rebuild_tree(doctype: str) -> None:
"""Call rebuild_node for all root nodes."""
# Check for perm if called from client-side
if frappe.request and frappe.local.form_dict.cmd == "rebuild_tree":
frappe.only_for("System Manager")
@ -184,6 +181,8 @@ def rebuild_tree(doctype, parent_field):
title=_("Invalid Action"),
)
parent_field = meta.nsm_parent_field or f"parent_{frappe.scrub(doctype)}"
# get all roots
right = 1
table = DocType(doctype)
@ -327,7 +326,7 @@ class NestedSet(Document):
)
if merge:
rebuild_tree(self.doctype, parent_field)
rebuild_tree(self.doctype)
def validate_one_root(self):
if not self.get(self.nsm_parent_field):

View file

@ -39,6 +39,18 @@ class TestHelpArticle(FrappeTestCase):
self.assertEqual(self.help_article.helpful, 1)
self.assertEqual(self.help_article.not_helpful, 1)
def test_category_disable(self):
self.help_article.load_from_db()
self.help_article.published = 1
self.help_article.save()
self.help_category.load_from_db()
self.help_category.published = 0
self.help_category.save()
self.help_article.load_from_db()
self.assertEqual(self.help_article.published, 0)
@classmethod
def tearDownClass(cls) -> None:
frappe.delete_doc(cls.help_article.doctype, cls.help_article.name)

View file

@ -32,6 +32,11 @@ class HelpCategory(WebsiteGenerator):
def validate(self):
self.set_route()
# disable help articles of this category
if not self.published:
for d in frappe.get_all("Help Article", dict(category=self.name)):
frappe.db.set_value("Help Article", d.name, "published", 0)
def set_route(self):
if not self.route:
self.route = "kb/" + self.scrub(self.category_name)

View file

@ -11,8 +11,7 @@
"open_in_new_tab",
"right",
"column_break_5",
"parent_label",
"icon"
"parent_label"
],
"fields": [
{
@ -50,12 +49,6 @@
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"description": "If Icon is set, it will be shown instead of Label",
"fieldname": "icon",
"fieldtype": "Attach Image",
"label": "Icon"
},
{
"default": "0",
"depends_on": "eval:doc.url",
@ -68,12 +61,13 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-26 20:59:42.142208",
"modified": "2024-01-08 12:05:25.782635",
"modified_by": "Administrator",
"module": "Website",
"name": "Top Bar Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View file

@ -14,7 +14,6 @@ class TopBarItem(Document):
if TYPE_CHECKING:
from frappe.types import DF
icon: DF.AttachImage | None
label: DF.Data
open_in_new_tab: DF.Check
parent: DF.Data
@ -24,4 +23,5 @@ class TopBarItem(Document):
right: DF.Check
url: DF.Data | None
# end: auto-generated types
pass

View file

@ -17,7 +17,7 @@
{% block header_buttons %}
{% if allow_edit and in_view_mode %}
<!-- edit button -->
<a href="/{{ route }}/{{ doc_name }}/edit" class="edit-button btn btn-default btn-sm">{{ _("Edit Response", null, "Button in web form") }}</a>
<a href="/{{ route }}/{{ doc_name }}/edit" class="edit-button btn btn-default btn-sm">{{ _("Edit Response", context="Button in web form") }}</a>
{% endif %}
{% if allow_print and in_view_mode %}
@ -38,10 +38,10 @@
{% if not in_view_mode %}
<!-- discard button -->
<button class="discard-btn btn btn-default btn-sm">
{{ _("Discard", null, "Button in web form") }}
{{ _("Discard", context="Button in web form") }}
</button>
<!-- submit button -->
<button type="submit" class="submit-btn btn btn-primary btn-sm ml-2">{{ button_label or _("Submit", null, "Button in web form") }}</button>
<button type="submit" class="submit-btn btn btn-primary btn-sm ml-2">{{ _(button_label, context="Button in web form") or _("Submit", context="Button in web form") }}</button>
{% endif %}
</div>
{% endblock %}
@ -147,10 +147,10 @@
</div>
{% else %}
{% if show_list %}
<a href="/{{ route }}/list" class="show-list-button btn btn-default btn-md">{{ _("See previous responses", null, "Button in web form") }}</a>
<a href="/{{ route }}/list" class="show-list-button btn btn-default btn-md">{{ _("See previous responses", context="Button in web form") }}</a>
{% endif %}
{% if not login_required or allow_multiple %}
<a href="/{{ route }}/new" class="new-btn btn btn-default btn-md">{{ _("Submit another response", null, "Button in web form") }}</a>
<a href="/{{ route }}/new" class="new-btn btn btn-default btn-md">{{ _("Submit another response", context="Button in web form") }}</a>
{% endif %}
{% endif %}
</div>

View file

@ -107,6 +107,7 @@ frappe.ui.form.on("Web Form", {
reqd: df.reqd,
default: df.default,
read_only: df.read_only,
precision: df.precision,
depends_on: df.depends_on,
mandatory_depends_on: df.mandatory_depends_on,
read_only_depends_on: df.read_only_depends_on,

View file

@ -17,6 +17,7 @@
"options",
"max_length",
"max_value",
"precision",
"property_depends_on_section",
"depends_on",
"mandatory_depends_on",
@ -143,11 +144,19 @@
"fieldtype": "Code",
"label": "Read Only Depends On",
"options": "JS"
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
}
],
"istable": 1,
"links": [],
"modified": "2022-11-21 17:41:52.139191",
"modified": "2024-01-08 13:21:06.272248",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form Field",

View file

@ -56,6 +56,7 @@ class WebFormField(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
precision: DF.Literal["", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
read_only: DF.Check
read_only_depends_on: DF.Code | None
reqd: DF.Check

View file

@ -19,6 +19,7 @@
"misc_section",
"app_name",
"disable_signup",
"show_footer_on_login",
"column_break_9",
"app_logo",
"section_break_6",
@ -49,9 +50,9 @@
"footer",
"footer_items",
"footer_details_section",
"hide_footer_signup",
"copyright",
"footer_logo",
"hide_footer_signup",
"column_break_37",
"address",
"footer_powered",
@ -126,7 +127,7 @@
"fieldname": "website_theme_image_link",
"fieldtype": "Code",
"hidden": 1,
"label": "Website Theme Image Link"
"label": "Website Theme image link"
},
{
"collapsible": 1,
@ -211,7 +212,7 @@
"default": "0",
"fieldname": "hide_footer_signup",
"fieldtype": "Check",
"label": "Hide Footer Signup"
"label": "Hide footer signup"
},
{
"collapsible": 1,
@ -248,10 +249,10 @@
},
{
"default": "1",
"description": "Disable Signups on site. New users will have to be manually registered by system managers.",
"description": "New users will have to be manually registered by system managers.",
"fieldname": "disable_signup",
"fieldtype": "Check",
"label": "Disable Signup"
"label": "Disable signups"
},
{
"collapsible": 1,
@ -282,20 +283,20 @@
"description": "To use Google Indexing, enable <a href=\"/app/google-settings\">Google Settings</a>.",
"fieldname": "enable_google_indexing",
"fieldtype": "Check",
"label": "Enable Google Indexing"
"label": "Enable Google indexing"
},
{
"fieldname": "indexing_refresh_token",
"fieldtype": "Data",
"hidden": 1,
"label": "Indexing Refresh Token",
"label": "Indexing refresh token",
"read_only": 1
},
{
"fieldname": "indexing_authorization_code",
"fieldtype": "Data",
"hidden": 1,
"label": "Indexing Authorization Code",
"label": "Indexing authorization code",
"read_only": 1
},
{
@ -308,7 +309,7 @@
"default": "0",
"fieldname": "enable_view_tracking",
"fieldtype": "Check",
"label": "Enable In App Website Tracking"
"label": "Enable in-app website tracking"
},
{
"fieldname": "footer_logo",
@ -373,7 +374,7 @@
"default": "1",
"fieldname": "google_analytics_anonymize_ip",
"fieldtype": "Check",
"label": "Google Analytics Anonymize IP"
"label": "Google Analytics anonymise IP"
},
{
"default": "0",
@ -408,13 +409,13 @@
"default": "0",
"fieldname": "show_account_deletion_link",
"fieldtype": "Check",
"label": "Show Account Deletion Link in My Account Page"
"label": "Show account deletion link in My Account page"
},
{
"default": "72",
"fieldname": "auto_account_deletion",
"fieldtype": "Int",
"label": "Auto Account Deletion within (Hours)"
"label": "Automatically delete account within (hours)"
},
{
"fieldname": "footer_powered",
@ -469,6 +470,12 @@
"fieldname": "analytics_section",
"fieldtype": "Section Break",
"label": "Analytics"
},
{
"default": "0",
"fieldname": "show_footer_on_login",
"fieldtype": "Check",
"label": "Show footer on login"
}
],
"icon": "fa fa-cog",
@ -476,7 +483,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-12-08 15:52:37.525003",
"modified": "2024-01-08 11:50:34.750809",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Settings",

View file

@ -56,6 +56,7 @@ class WebsiteSettings(Document):
robots_txt: DF.Code | None
route_redirects: DF.Table[WebsiteRouteRedirect]
show_account_deletion_link: DF.Check
show_footer_on_login: DF.Check
show_language_picker: DF.Check
splash_image: DF.AttachImage | None
subdomain: DF.SmallText | None
@ -63,8 +64,8 @@ class WebsiteSettings(Document):
top_bar_items: DF.Table[TopBarItem]
website_theme: DF.Link | None
website_theme_image_link: DF.Code | None
# end: auto-generated types
def validate(self):
self.validate_top_bar_items()
self.validate_footer_items()

View file

@ -37,6 +37,7 @@ def get_context(context):
context["hide_login"] = True # dont show login link on login page again.
context["provider_logins"] = []
context["disable_signup"] = cint(frappe.get_website_settings("disable_signup"))
context["show_footer_on_login"] = cint(frappe.get_website_settings("show_footer_on_login"))
context["disable_user_pass_login"] = cint(frappe.get_system_settings("disable_user_pass_login"))
context["logo"] = frappe.get_website_settings("app_logo") or frappe.get_hooks("app_logo_url")[-1]
context["app_name"] = (

View file

@ -20,7 +20,7 @@
},
"homepage": "https://frappeframework.com",
"dependencies": {
"@editorjs/editorjs": "~2.26.3",
"@editorjs/editorjs": "^2.28.2",
"@frappe/esbuild-plugin-postcss2": "^0.1.3",
"@headlessui/vue": "^1.7.16",
"@popperjs/core": "^2.11.2",
@ -47,7 +47,7 @@
"fast-deep-equal": "^2.0.1",
"fast-glob": "^3.2.5",
"frappe-charts": "2.0.0-rc22",
"frappe-datatable": "1.17.13",
"frappe-datatable": "1.17.14",
"frappe-gantt": "^0.6.0",
"highlight.js": "^10.4.1",
"html5-qrcode": "^2.3.8",

View file

@ -31,21 +31,10 @@
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
"@codexteam/icons@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.1.0.tgz#a02885fe8699f69902d05b077b5f1cd48a2ca6b9"
integrity sha512-jW1fWnwtWzcP4FBGsaodbJY3s1ZaRU+IJy1pvJ7ygNQxkQinybJcwXoyt0a5mWwu/4w30A42EWhCrZn8lp4fdw==
"@editorjs/editorjs@~2.26.3":
version "2.26.5"
resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.26.5.tgz#ee0f1dbd3a3c6ba97d3ed30f13ab7d2e7b29dbe4"
integrity sha512-imwXZi9NmzxKjNosa1xQf286liJYsTe2J2DWCiV5TwKhvYZ1INg5Y+FietcM2v65QmeLqP7wgBUhoI7wiCB+yQ==
dependencies:
"@codexteam/icons" "0.1.0"
codex-notifier "^1.1.2"
codex-tooltip "^1.0.5"
html-janitor "^2.0.4"
nanoid "^3.1.22"
"@editorjs/editorjs@^2.28.2":
version "2.28.2"
resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.28.2.tgz#a265c7d10e83adef81813e4dc0f01fe3464dff50"
integrity sha512-g6V0Nd3W9IIWMpvxDNTssQ6e4kxBp1Y0W4GIf8cXRlmcBp3TUjrgCYJQmNy3l2a6ZzhyBAoVSe8krJEq4g7PQw==
"@esbuild/linux-loong64@0.14.54":
version "0.14.54"
@ -680,16 +669,6 @@ cluster-key-slot@1.1.2:
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
codex-notifier@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/codex-notifier/-/codex-notifier-1.1.2.tgz#a733079185f4c927fa296f1d71eb8753fe080895"
integrity sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==
codex-tooltip@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.5.tgz#ba25fd5b3a58ba2f73fd667c2b46987ffd1edef2"
integrity sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -1497,10 +1476,10 @@ frappe-charts@2.0.0-rc22:
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc22.tgz#9a5a747febdc381a1d4d7af96e89cf519dfba8c0"
integrity sha512-N7f/8979wJCKjusOinaUYfMxB80YnfuVLrSkjpj4LtyqS0BGS6SuJxUnb7Jl4RWUFEIs7zEhideIKnyLeFZF4Q==
frappe-datatable@1.17.13:
version "1.17.13"
resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.17.13.tgz#349a15fd102b8abe55ab903c65de074aa66a26d6"
integrity sha512-rHzLjuAbWdbqEZbU6RCcimyeTswdIKtl8WJLjctj5zze7w5DFSQX0iDTbKNBG7TehS7YqP4k+ySIhvkHRELq3w==
frappe-datatable@1.17.14:
version "1.17.14"
resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.17.14.tgz#5fe99fa45089d6f2a54d13215608ef777bc947ec"
integrity sha512-rrUyk+8ueX9ADDXwaHobBGmAWK86lF3P3yc3KYGHyhNiNTwKpUW08zQeuTUzJnWv0OSZ/zXYePzrjFKG7ZR4Wg==
dependencies:
hyperlist "^1.0.0-beta"
lodash "^4.17.5"
@ -1704,11 +1683,6 @@ homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1:
dependencies:
parse-passwd "^1.0.0"
html-janitor@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/html-janitor/-/html-janitor-2.0.4.tgz#ae5a115cdf3331cd5501edd7b5471b18ea44cdbb"
integrity sha512-92J5h9jNZRk30PMHapjHEJfkrBWKCOy0bq3oW2pBungky6lzYSoboBGPMvxl1XRKB2q+kniQmsLsPbdpY7RM2g==
html5-qrcode@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.3.8.tgz#0b0cdf7a9926cfd4be530e13a51db47592adfa0d"
@ -2263,7 +2237,7 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.1.22, nanoid@^3.3.6:
nanoid@^3.3.6:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==