Merge branch 'develop' into bump-pydantic-v2
This commit is contained in:
commit
03fbdde007
59 changed files with 689 additions and 972 deletions
|
|
@ -2213,41 +2213,6 @@ def logger(
|
|||
)
|
||||
|
||||
|
||||
def log_error(title=None, message=None, reference_doctype=None, reference_name=None):
|
||||
"""Log error to Error Log"""
|
||||
# Parameter ALERT:
|
||||
# the title and message may be swapped
|
||||
# the better API for this is log_error(title, message), and used in many cases this way
|
||||
# this hack tries to be smart about whats a title (single line ;-)) and fixes it
|
||||
|
||||
traceback = None
|
||||
if message:
|
||||
if "\n" in title: # traceback sent as title
|
||||
traceback, title = title, message
|
||||
else:
|
||||
traceback = message
|
||||
|
||||
title = title or "Error"
|
||||
traceback = as_unicode(traceback or get_traceback(with_context=True))
|
||||
|
||||
if not db:
|
||||
print(f"Failed to log error in db: {title}")
|
||||
return
|
||||
|
||||
error_log = get_doc(
|
||||
doctype="Error Log",
|
||||
error=traceback,
|
||||
method=title,
|
||||
reference_doctype=reference_doctype,
|
||||
reference_name=reference_name,
|
||||
)
|
||||
|
||||
if flags.read_only:
|
||||
error_log.deferred_insert()
|
||||
else:
|
||||
return error_log.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def get_desk_link(doctype, name):
|
||||
html = (
|
||||
'<a href="/app/Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
|
||||
|
|
@ -2439,6 +2404,8 @@ def validate_and_sanitize_search_inputs(fn):
|
|||
return wrapper
|
||||
|
||||
|
||||
from frappe.utils.error import log_error # noqa: backward compatibility
|
||||
|
||||
if _tune_gc:
|
||||
# generational GC gets triggered after certain allocs (g0) which is 700 by default.
|
||||
# This number is quite small for frappe where a single query can potentially create 700+
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from frappe import _
|
|||
from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest
|
||||
from frappe.middlewares import StaticDataMiddleware
|
||||
from frappe.utils import cint, get_site_name, sanitize_html
|
||||
from frappe.utils.error import make_error_snapshot
|
||||
from frappe.utils.error import log_error_snapshot
|
||||
from frappe.website.serve import get_response
|
||||
|
||||
local_manager = LocalManager(frappe.local)
|
||||
|
|
@ -346,7 +346,7 @@ def handle_exception(e):
|
|||
frappe.local.login_manager.clear_cookies()
|
||||
|
||||
if http_status_code >= 500:
|
||||
make_error_snapshot(e)
|
||||
log_error_snapshot(e)
|
||||
|
||||
if return_as_message:
|
||||
response = get_response("message", http_status_code=http_status_code)
|
||||
|
|
|
|||
|
|
@ -215,18 +215,39 @@ def start_worker(
|
|||
)
|
||||
|
||||
|
||||
@click.command("worker-pool")
|
||||
@click.option(
|
||||
"--queue",
|
||||
type=str,
|
||||
help="Queue to consume from. Multiple queues can be specified using comma-separated string. If not specified all queues are consumed.",
|
||||
)
|
||||
@click.option("--num-workers", type=int, default=2, help="Number of workers to spawn in pool.")
|
||||
@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs")
|
||||
@click.option("--burst", is_flag=True, default=False, help="Run Worker in Burst mode.")
|
||||
def start_worker_pool(queue, quiet=False, num_workers=2, burst=False):
|
||||
"""Start a backgrond worker"""
|
||||
from frappe.utils.background_jobs import start_worker_pool
|
||||
|
||||
start_worker_pool(
|
||||
queue=queue,
|
||||
quiet=quiet,
|
||||
burst=burst,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
|
||||
|
||||
@click.command("ready-for-migration")
|
||||
@click.option("--site", help="site name")
|
||||
@pass_context
|
||||
def ready_for_migration(context, site=None):
|
||||
from frappe.utils.doctor import get_pending_jobs
|
||||
from frappe.utils.doctor import any_job_pending
|
||||
|
||||
if not site:
|
||||
site = get_site(context)
|
||||
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
pending_jobs = get_pending_jobs(site=site)
|
||||
pending_jobs = any_job_pending(site=site)
|
||||
|
||||
if pending_jobs:
|
||||
print(f"NOT READY for migration: site {site} has pending background jobs")
|
||||
|
|
@ -251,5 +272,6 @@ commands = [
|
|||
show_pending_jobs,
|
||||
start_scheduler,
|
||||
start_worker,
|
||||
start_worker_pool,
|
||||
trigger_scheduler_event,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -700,9 +700,12 @@ def _use(site, sites_path="."):
|
|||
|
||||
|
||||
def use(site, sites_path="."):
|
||||
from frappe.installer import update_site_config
|
||||
|
||||
if os.path.exists(os.path.join(sites_path, site)):
|
||||
with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile:
|
||||
sitefile.write(site)
|
||||
sites_path = os.getcwd()
|
||||
conifg = os.path.join(sites_path, "common_site_config.json")
|
||||
update_site_config("default_site", site, validate=False, site_config_path=conifg)
|
||||
print(f"Current Site set to {site}")
|
||||
else:
|
||||
print(f"Site {site} does not exist")
|
||||
|
|
|
|||
|
|
@ -33,13 +33,28 @@ class TestDocShare(FrappeTestCase):
|
|||
|
||||
def test_doc_permission(self):
|
||||
frappe.set_user(self.user)
|
||||
|
||||
self.assertFalse(self.event.has_permission())
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
frappe.share.add("Event", self.event.name, self.user)
|
||||
|
||||
frappe.set_user(self.user)
|
||||
self.assertTrue(self.event.has_permission())
|
||||
# PERF: All share permission check should happen with maximum 1 query.
|
||||
with self.assertRowsRead(1):
|
||||
self.assertTrue(self.event.has_permission())
|
||||
|
||||
second_event = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Event",
|
||||
"subject": "test share event 2",
|
||||
"starts_on": "2015-01-01 10:00:00",
|
||||
"event_type": "Private",
|
||||
}
|
||||
).insert()
|
||||
frappe.share.add("Event", second_event.name, self.user)
|
||||
with self.assertRowsRead(1):
|
||||
self.assertTrue(self.event.has_permission())
|
||||
|
||||
def test_share_permission(self):
|
||||
frappe.share.add("Event", self.event.name, self.user, write=1, share=1)
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
{% if (Object.prototype.toString.call(x) === "[object Object]") { %}
|
||||
<table class="table">
|
||||
{% for (var key in x) { %}
|
||||
<tr>
|
||||
<td><code>{{ key }}</code></td>
|
||||
<td>{{ x[key] }}</td>
|
||||
</tr>
|
||||
{% } %}
|
||||
</table>
|
||||
{% } else { %}
|
||||
{{ x }}
|
||||
{% } %}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
<style>
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.codebox {
|
||||
font-family: monospace;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
.codebox .line.current {
|
||||
background: rgba(0,0,255, 0.1);
|
||||
}
|
||||
|
||||
.codebox .lineno {
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.codebox .code {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.object-link {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
{% function id(){ return id._old_id++; }; id._old_id = 0; %}
|
||||
<h3>{{ __("Error Report") }}</h3>
|
||||
<p class="text-muted">{{ doc.pyver }}</p>
|
||||
<dl>
|
||||
<dt>{{ __("Timestamp") }}: </dt>
|
||||
<dd>{{ doc.timestamp }}</dd>
|
||||
<dt>{{ __("Relapsed") }}</dt>
|
||||
<dd><code>{{ doc.relapses }}</code></dd>
|
||||
</dl>
|
||||
|
||||
<h3>{{ __("Exception") }}</h3>
|
||||
{{ frappe.render_template("error_object", {x: JSON.parse(doc.exception)}) }}
|
||||
|
||||
<h3>{{ __("Locals") }}</h3>
|
||||
{{ frappe.render_template("error_object", {x: JSON.parse(doc.locals)} )}}
|
||||
|
||||
<h3>{{ __("Traceback") }}</h3>
|
||||
{% var frames = JSON.parse(doc.frames); %}
|
||||
{% for (var i in frames) { %}
|
||||
{% var frameid = id(), frame = frames[i] %}
|
||||
<p><i class="octicon octicon-file-text"></i> <code>{{ frame.file }}: {{ frame.lnum }}</code>
|
||||
<div class="row">
|
||||
<div class="codebox">
|
||||
<div class="col-lg-11">
|
||||
{% for (var index in frame.lines) { %}
|
||||
{% var line = frame.lines[index] %}
|
||||
<div class="line {{ index == frame.lnum ? "current": "" }}">
|
||||
<span class="lineno text-muted">{{ index }}</span>
|
||||
<span class="code">{{ line }}</span>
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="col-lg-1">
|
||||
<span class="btn btn-xs btn-default" data-toggle="collapse" data-target="#frame-{{ frameid }}-locals">
|
||||
<i class="fa fa-list-ul"> {{ __("Locals") }}</i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 collapse" id="frame-{{ frameid }}-locals">
|
||||
<h4>{{ __("Locals") }}</h4>
|
||||
{{ frappe.render_template("error_object", {x: frame.dump }) }}
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
{% } %}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
frappe.ui.form.on("Error Snapshot", "load", function (frm) {
|
||||
frm.set_read_only(true);
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Error Snapshot", "refresh", function (frm) {
|
||||
frm.set_df_property(
|
||||
"view",
|
||||
"options",
|
||||
frappe.render_template("error_snapshot", { doc: frm.doc })
|
||||
);
|
||||
|
||||
if (frm.doc.relapses) {
|
||||
frm.add_custom_button(__("Show Relapses"), function () {
|
||||
frappe.route_options = {
|
||||
parent_error_snapshot: frm.doc.name,
|
||||
};
|
||||
frappe.set_route("List", "Error Snapshot");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2015-11-28 00:57:39.766888",
|
||||
"doctype": "DocType",
|
||||
"document_type": "System",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"view",
|
||||
"seen",
|
||||
"evalue",
|
||||
"timestamp",
|
||||
"relapses",
|
||||
"etype",
|
||||
"traceback",
|
||||
"parent_error_snapshot",
|
||||
"pyver",
|
||||
"exception",
|
||||
"locals",
|
||||
"frames"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "view",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Snapshot View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "seen",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"in_filter": 1,
|
||||
"label": "Seen"
|
||||
},
|
||||
{
|
||||
"fieldname": "evalue",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Friendly Title",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "timestamp",
|
||||
"fieldtype": "Datetime",
|
||||
"hidden": 1,
|
||||
"label": "Timestamp",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "relapses",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Relapses",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "etype",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Exception Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "traceback",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Traceback",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "parent_error_snapshot",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Parent Error Snapshot"
|
||||
},
|
||||
{
|
||||
"fieldname": "pyver",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Pyver",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "exception",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Exception"
|
||||
},
|
||||
{
|
||||
"fieldname": "locals",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Locals"
|
||||
},
|
||||
{
|
||||
"fieldname": "frames",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Frames"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-03 12:20:53.504160",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Error Snapshot",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "timestamp",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "evalue"
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
|
||||
|
||||
class ErrorSnapshot(Document):
|
||||
no_feed_on_delete = True
|
||||
|
||||
def onload(self):
|
||||
if not self.parent_error_snapshot:
|
||||
self.db_set("seen", 1, update_modified=False)
|
||||
|
||||
for relapsed in frappe.get_all("Error Snapshot", filters={"parent_error_snapshot": self.name}):
|
||||
frappe.db.set_value("Error Snapshot", relapsed.name, "seen", 1, update_modified=False)
|
||||
|
||||
frappe.local.flags.commit = True
|
||||
|
||||
def validate(self):
|
||||
parent = frappe.get_all(
|
||||
"Error Snapshot",
|
||||
filters={"evalue": self.evalue, "parent_error_snapshot": ""},
|
||||
fields=["name", "relapses", "seen"],
|
||||
limit_page_length=1,
|
||||
)
|
||||
|
||||
if parent:
|
||||
parent = parent[0]
|
||||
self.update({"parent_error_snapshot": parent["name"]})
|
||||
frappe.db.set_value("Error Snapshot", parent["name"], "relapses", parent["relapses"] + 1)
|
||||
if parent["seen"]:
|
||||
frappe.db.set_value("Error Snapshot", parent["name"], "seen", 0)
|
||||
|
||||
@staticmethod
|
||||
def clear_old_logs(days=30):
|
||||
table = frappe.qb.DocType("Error Snapshot")
|
||||
frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
frappe.listview_settings["Error Snapshot"] = {
|
||||
add_fields: ["parent_error_snapshot", "relapses", "seen"],
|
||||
filters: [
|
||||
["parent_error_snapshot", "=", null],
|
||||
["seen", "=", false],
|
||||
],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.parent_error_snapshot && doc.parent_error_snapshot.length) {
|
||||
return [__("Relapsed"), !doc.seen ? "orange" : "blue", "parent_error_snapshot,!=,"];
|
||||
} else {
|
||||
return [__("First Level"), !doc.seen ? "red" : "green", "parent_error_snapshot,=,"];
|
||||
}
|
||||
},
|
||||
onload: function (listview) {
|
||||
frappe.require("logtypes.bundle.js", () => {
|
||||
frappe.utils.logtypes.show_log_retention_message(cur_list.doctype);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils.logger import sanitized_dict
|
||||
|
||||
# test_records = frappe.get_test_records('Error Snapshot')
|
||||
|
||||
|
||||
class TestErrorSnapshot(FrappeTestCase):
|
||||
def test_form_dict_sanitization(self):
|
||||
self.assertNotEqual(sanitized_dict({"pwd": "SECRET", "usr": "WHAT"}).get("pwd"), "SECRET")
|
||||
|
|
@ -174,7 +174,7 @@
|
|||
"icon": "fa fa-file",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-13 15:50:15.508251",
|
||||
"modified": "2023-05-02 15:42:14.274901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "File",
|
||||
|
|
@ -196,14 +196,8 @@
|
|||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.database.schema import SPECIAL_CHAR_PATTERN
|
||||
from frappe.model.document import Document
|
||||
from frappe.permissions import get_doctypes_with_read
|
||||
from frappe.utils import call_hook_method, cint, get_files_path, get_hook_method, get_url
|
||||
from frappe.utils.file_manager import is_safe_path
|
||||
from frappe.utils.image import optimize_image, strip_exif_data
|
||||
|
|
@ -718,40 +719,39 @@ def on_doctype_update():
|
|||
|
||||
|
||||
def has_permission(doc, ptype=None, user=None):
|
||||
has_access = False
|
||||
user = user or frappe.session.user
|
||||
|
||||
if ptype == "create":
|
||||
has_access = frappe.has_permission("File", "create", user=user)
|
||||
return frappe.has_permission("File", "create", user=user)
|
||||
|
||||
if not doc.is_private or doc.owner in [user, "Guest"] or user == "Administrator":
|
||||
has_access = True
|
||||
if not doc.is_private or doc.owner == user or user == "Administrator":
|
||||
return True
|
||||
|
||||
if doc.attached_to_doctype and doc.attached_to_name:
|
||||
attached_to_doctype = doc.attached_to_doctype
|
||||
attached_to_name = doc.attached_to_name
|
||||
|
||||
try:
|
||||
ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name)
|
||||
ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name)
|
||||
|
||||
if ptype in ["write", "create", "delete"]:
|
||||
has_access = ref_doc.has_permission("write")
|
||||
if ptype in ["write", "create", "delete"]:
|
||||
return ref_doc.has_permission("write")
|
||||
else:
|
||||
return ref_doc.has_permission("read")
|
||||
|
||||
if ptype == "delete" and not has_access:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot delete file as it belongs to {0} {1} for which you do not have permissions"
|
||||
).format(doc.attached_to_doctype, doc.attached_to_name),
|
||||
frappe.PermissionError,
|
||||
)
|
||||
else:
|
||||
has_access = ref_doc.has_permission("read")
|
||||
except frappe.DoesNotExistError:
|
||||
# if parent doc is not created before file is created
|
||||
# we cannot check its permission so we will use file's permission
|
||||
pass
|
||||
return False
|
||||
|
||||
return has_access
|
||||
|
||||
def get_permission_query_conditions(user: str = None) -> str:
|
||||
user = user or frappe.session.user
|
||||
if user == "Administrator":
|
||||
return ""
|
||||
|
||||
readable_doctypes = ", ".join(repr(dt) for dt in get_doctypes_with_read())
|
||||
return f"""
|
||||
(`tabFile`.`is_private` = 0)
|
||||
OR (`tabFile`.`attached_to_doctype` IS NULL AND `tabFile`.`owner` = {user !r})
|
||||
OR (`tabFile`.`attached_to_doctype` IN ({readable_doctypes}))
|
||||
"""
|
||||
|
||||
|
||||
# Note: kept at the end to not cause circular, partial imports & maintain backwards compatibility
|
||||
|
|
|
|||
|
|
@ -611,46 +611,42 @@ class TestAttachmentsAccess(FrappeTestCase):
|
|||
def setUp(self) -> None:
|
||||
frappe.db.delete("File", {"is_folder": 0})
|
||||
|
||||
def test_attachments_access(self):
|
||||
def test_list_private_attachments(self):
|
||||
frappe.set_user("test4@example.com")
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": "test_user.txt",
|
||||
"attached_to_doctype": self.attached_to_doctype,
|
||||
"attached_to_name": self.attached_to_docname,
|
||||
"content": "Testing User",
|
||||
}
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_user_attachment.txt",
|
||||
attached_to_doctype=self.attached_to_doctype,
|
||||
attached_to_name=self.attached_to_docname,
|
||||
content="Testing User",
|
||||
is_private=1,
|
||||
).insert()
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": "test_user_home.txt",
|
||||
"content": "User Home",
|
||||
}
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_user_standalone.txt",
|
||||
content="User Home",
|
||||
is_private=1,
|
||||
).insert()
|
||||
|
||||
frappe.set_user("test@example.com")
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": "test_system_manager.txt",
|
||||
"attached_to_doctype": self.attached_to_doctype,
|
||||
"attached_to_name": self.attached_to_docname,
|
||||
"content": "Testing System Manager",
|
||||
}
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_sm_attachment.txt",
|
||||
attached_to_doctype=self.attached_to_doctype,
|
||||
attached_to_name=self.attached_to_docname,
|
||||
content="Testing System Manager",
|
||||
is_private=1,
|
||||
).insert()
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": "test_sm_home.txt",
|
||||
"content": "System Manager Home",
|
||||
}
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_sm_standalone.txt",
|
||||
content="System Manager Home",
|
||||
is_private=1,
|
||||
).insert()
|
||||
|
||||
system_manager_files = [file.file_name for file in get_files_in_folder("Home")["files"]]
|
||||
|
|
@ -664,15 +660,47 @@ class TestAttachmentsAccess(FrappeTestCase):
|
|||
file.file_name for file in get_files_in_folder("Home/Attachments")["files"]
|
||||
]
|
||||
|
||||
self.assertIn("test_sm_home.txt", system_manager_files)
|
||||
self.assertNotIn("test_sm_home.txt", user_files)
|
||||
self.assertIn("test_user_home.txt", system_manager_files)
|
||||
self.assertIn("test_user_home.txt", user_files)
|
||||
self.assertIn("test_sm_standalone.txt", system_manager_files)
|
||||
self.assertNotIn("test_sm_standalone.txt", user_files)
|
||||
|
||||
self.assertIn("test_system_manager.txt", system_manager_attachments_files)
|
||||
self.assertNotIn("test_system_manager.txt", user_attachments_files)
|
||||
self.assertIn("test_user.txt", system_manager_attachments_files)
|
||||
self.assertIn("test_user.txt", user_attachments_files)
|
||||
self.assertIn("test_user_standalone.txt", user_files)
|
||||
self.assertNotIn("test_user_standalone.txt", system_manager_files)
|
||||
|
||||
self.assertIn("test_sm_attachment.txt", system_manager_attachments_files)
|
||||
self.assertIn("test_sm_attachment.txt", user_attachments_files)
|
||||
self.assertIn("test_user_attachment.txt", system_manager_attachments_files)
|
||||
self.assertIn("test_user_attachment.txt", user_attachments_files)
|
||||
|
||||
def test_list_public_single_file(self):
|
||||
"""Ensure that users are able to list public standalone files."""
|
||||
frappe.set_user("test@example.com")
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_public_single.txt",
|
||||
content="Public single File",
|
||||
is_private=0,
|
||||
).insert()
|
||||
|
||||
frappe.set_user("test4@example.com")
|
||||
files = [file.file_name for file in get_files_in_folder("Home")["files"]]
|
||||
self.assertIn("test_public_single.txt", files)
|
||||
|
||||
def test_list_public_attachment(self):
|
||||
"""Ensure that users are able to list public attachments."""
|
||||
frappe.set_user("test@example.com")
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_public_attachment.txt",
|
||||
attached_to_doctype=self.attached_to_doctype,
|
||||
attached_to_name=self.attached_to_docname,
|
||||
content="Public Attachment",
|
||||
is_private=0,
|
||||
).insert()
|
||||
|
||||
frappe.set_user("test4@example.com")
|
||||
files = [file.file_name for file in get_files_in_folder("Home/Attachments")["files"]]
|
||||
self.assertIn("test_public_attachment.txt", files)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
frappe.set_user("Administrator")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ DEFAULT_LOGTYPES_RETENTION = {
|
|||
"Error Log": 30,
|
||||
"Activity Log": 90,
|
||||
"Email Queue": 30,
|
||||
"Error Snapshot": 30,
|
||||
"Scheduled Job Log": 90,
|
||||
"Route History": 90,
|
||||
"Submission Queue": 30,
|
||||
|
|
@ -45,11 +44,11 @@ def _supports_log_clearing(doctype: str) -> bool:
|
|||
|
||||
class LogSettings(Document):
|
||||
def validate(self):
|
||||
self._remove_unsupported_doctypes()
|
||||
self.remove_unsupported_doctypes()
|
||||
self._deduplicate_entries()
|
||||
self.add_default_logtypes()
|
||||
|
||||
def _remove_unsupported_doctypes(self):
|
||||
def remove_unsupported_doctypes(self):
|
||||
for entry in list(self.logs_to_clear):
|
||||
if _supports_log_clearing(entry.ref_doctype):
|
||||
continue
|
||||
|
|
@ -114,6 +113,7 @@ class LogSettings(Document):
|
|||
|
||||
def run_log_clean_up():
|
||||
doc = frappe.get_doc("Log Settings")
|
||||
doc.remove_unsupported_doctypes()
|
||||
doc.add_default_logtypes()
|
||||
doc.save()
|
||||
doc.clear_logs()
|
||||
|
|
@ -156,7 +156,6 @@ LOG_DOCTYPES = [
|
|||
"Route History",
|
||||
"Email Queue",
|
||||
"Email Queue Recipient",
|
||||
"Error Snapshot",
|
||||
"Error Log",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ class TestLogSettings(FrappeTestCase):
|
|||
"Activity Log",
|
||||
"Email Queue",
|
||||
"Route History",
|
||||
"Error Snapshot",
|
||||
"Scheduled Job Log",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ class Report(Document):
|
|||
# automatically set as prepared
|
||||
execution_time = (datetime.datetime.now() - start_time).total_seconds()
|
||||
if execution_time > threshold and not self.prepared_report:
|
||||
self.db_set("prepared_report", 1)
|
||||
frappe.enqueue(enable_prepared_report, report=self.name)
|
||||
|
||||
frappe.cache.hset("report_execution_time", self.name, execution_time)
|
||||
|
||||
|
|
@ -382,3 +382,7 @@ def get_group_by_column_label(args, meta):
|
|||
function=sql_fn_map[args.aggregate_function], fieldlabel=aggregate_on_label
|
||||
)
|
||||
return label
|
||||
|
||||
|
||||
def enable_prepared_report(report: str):
|
||||
frappe.db.set_value("Report", report, "prepared_report", 1)
|
||||
|
|
|
|||
|
|
@ -63,23 +63,15 @@ class RQJob(Document):
|
|||
|
||||
order_desc = "desc" in args.get("order_by", "")
|
||||
|
||||
matched_job_ids = RQJob.get_matching_job_ids(args)
|
||||
matched_job_ids = RQJob.get_matching_job_ids(args)[start : start + page_length]
|
||||
|
||||
jobs = []
|
||||
for job_ids in create_batch(matched_job_ids, 100):
|
||||
jobs.extend(
|
||||
serialize_job(job)
|
||||
for job in Job.fetch_many(job_ids=job_ids, connection=get_redis_conn())
|
||||
if job and for_current_site(job)
|
||||
)
|
||||
if len(jobs) > start + page_length:
|
||||
# we have fetched enough. This is inefficient but because of site filtering TINA
|
||||
break
|
||||
conn = get_redis_conn()
|
||||
jobs = [serialize_job(job) for job in Job.fetch_many(job_ids=matched_job_ids, connection=conn)]
|
||||
|
||||
return sorted(jobs, key=lambda j: j.modified, reverse=order_desc)[start : start + page_length]
|
||||
return sorted(jobs, key=lambda j: j.modified, reverse=order_desc)
|
||||
|
||||
@staticmethod
|
||||
def get_matching_job_ids(args):
|
||||
def get_matching_job_ids(args) -> list[str]:
|
||||
filters = make_filter_dict(args.get("filters"))
|
||||
|
||||
queues = _eval_filters(filters.get("queue"), QUEUES)
|
||||
|
|
@ -92,7 +84,7 @@ class RQJob(Document):
|
|||
for status in statuses:
|
||||
matched_job_ids.extend(fetch_job_ids(queue, status))
|
||||
|
||||
return matched_job_ids
|
||||
return filter_current_site_jobs(matched_job_ids)
|
||||
|
||||
@check_permissions
|
||||
def delete(self):
|
||||
|
|
@ -107,8 +99,7 @@ class RQJob(Document):
|
|||
|
||||
@staticmethod
|
||||
def get_count(args) -> int:
|
||||
# Can not be implemented efficiently due to site filtering hence ignored.
|
||||
return 0
|
||||
return len(RQJob.get_matching_job_ids(args))
|
||||
|
||||
# None of these methods apply to virtual job doctype, overriden for sanity.
|
||||
@staticmethod
|
||||
|
|
@ -155,6 +146,12 @@ def for_current_site(job: Job) -> bool:
|
|||
return job.kwargs.get("site") == frappe.local.site
|
||||
|
||||
|
||||
def filter_current_site_jobs(job_ids: list[str]) -> list[str]:
|
||||
site = frappe.local.site
|
||||
|
||||
return [j for j in job_ids if j.startswith(site)]
|
||||
|
||||
|
||||
def _eval_filters(filter, values: list[str]) -> list[str]:
|
||||
if filter:
|
||||
operator, operand = filter
|
||||
|
|
@ -186,10 +183,13 @@ def remove_failed_jobs():
|
|||
frappe.only_for("System Manager")
|
||||
for queue in get_queues():
|
||||
fail_registry = queue.failed_job_registry
|
||||
for job_ids in create_batch(fail_registry.get_job_ids(), 100):
|
||||
for job in Job.fetch_many(job_ids=job_ids, connection=get_redis_conn()):
|
||||
if job and for_current_site(job):
|
||||
fail_registry.remove(job, delete_job=True)
|
||||
failed_jobs = filter_current_site_jobs(fail_registry.get_job_ids())
|
||||
|
||||
# Delete in batches to avoid loading too many things in memory
|
||||
conn = get_redis_conn()
|
||||
for job_ids in create_batch(failed_jobs, 100):
|
||||
for job in Job.fetch_many(job_ids=job_ids, connection=conn):
|
||||
job and fail_registry.remove(job, delete_job=True)
|
||||
|
||||
|
||||
def get_all_queued_jobs():
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ frappe.listview_settings["RQ Job"] = {
|
|||
);
|
||||
|
||||
if (listview.list_view_settings) {
|
||||
listview.list_view_settings.disable_count = 1;
|
||||
listview.list_view_settings.disable_sidebar_stats = 1;
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +56,6 @@ frappe.listview_settings["RQ Job"] = {
|
|||
}
|
||||
|
||||
listview.refresh();
|
||||
}, 5000);
|
||||
}, 15000);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -97,13 +97,38 @@ class TestRQJob(FrappeTestCase):
|
|||
self.assertIn("quitting", cstr(stderr))
|
||||
|
||||
@timeout(20)
|
||||
def test_job_id_dedup(self):
|
||||
def test_multi_queue_burst_consumption_worker_pool(self):
|
||||
for _ in range(3):
|
||||
for q in ["default", "short"]:
|
||||
frappe.enqueue(self.BG_JOB, sleep=1, queue=q)
|
||||
|
||||
_, stderr = execute_in_shell(
|
||||
"bench worker-pool --queue short,default --burst --num-workers=4", check_exit_code=True
|
||||
)
|
||||
self.assertIn("quitting", cstr(stderr))
|
||||
|
||||
@timeout(20)
|
||||
def test_job_id_manual_dedup(self):
|
||||
job_id = "test_dedup"
|
||||
job = frappe.enqueue(self.BG_JOB, sleep=5, job_id=job_id)
|
||||
self.assertTrue(is_job_enqueued(job_id))
|
||||
self.check_status(job, "finished")
|
||||
self.assertFalse(is_job_enqueued(job_id))
|
||||
|
||||
@timeout(20)
|
||||
def test_auto_job_dedup(self):
|
||||
job_id = "test_dedup"
|
||||
job1 = frappe.enqueue(self.BG_JOB, sleep=2, job_id=job_id, deduplicate=True)
|
||||
job2 = frappe.enqueue(self.BG_JOB, sleep=5, job_id=job_id, deduplicate=True)
|
||||
self.assertIsNone(job2)
|
||||
self.check_status(job1, "finished") # wait
|
||||
|
||||
# Failed jobs last longer, subsequent job should still pass with same ID.
|
||||
job3 = frappe.enqueue(self.BG_JOB, fail=True, job_id=job_id, deduplicate=True)
|
||||
self.check_status(job3, "failed")
|
||||
job4 = frappe.enqueue(self.BG_JOB, sleep=1, job_id=job_id, deduplicate=True)
|
||||
self.check_status(job4, "finished")
|
||||
|
||||
@timeout(20)
|
||||
def test_enqueue_after_commit(self):
|
||||
job_id = frappe.generate_hash()
|
||||
|
|
|
|||
38
frappe/core/doctype/server_script/server_script_list.js
Normal file
38
frappe/core/doctype/server_script/server_script_list.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
frappe.listview_settings["Server Script"] = {
|
||||
onload: function (listview) {
|
||||
add_github_star_cta(listview);
|
||||
},
|
||||
};
|
||||
|
||||
function add_github_star_cta(listview) {
|
||||
try {
|
||||
const key = "show_github_star_banner";
|
||||
if (localStorage.getItem(key) == "false") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listview.github_star_banner) {
|
||||
listview.github_star_banner.remove();
|
||||
}
|
||||
|
||||
const message = "Loving Frappe Framework?";
|
||||
const link = "https://github.com/frappe/frappe";
|
||||
const cta = "Star us on GitHub";
|
||||
|
||||
listview.github_star_banner = $(`
|
||||
<div style="position: relative;">
|
||||
<div class="pr-3">
|
||||
${message} <br><a href="${link}" target="_blank" style="color: var(--primary-color)">${cta} → </a>
|
||||
</div>
|
||||
<div style="position: absolute; top: -1px; right: -4px; cursor: pointer;" title="Dismiss"
|
||||
onclick="localStorage.setItem('${key}', 'false') || this.parentElement.remove()">
|
||||
<svg class="icon icon-sm" style="">
|
||||
<use class="" href="#icon-close"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`).appendTo(listview.page.sidebar);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ def get_notification_config():
|
|||
"Communication": {"status": "Open", "communication_type": "Communication"},
|
||||
"ToDo": "frappe.core.notifications.get_things_todo",
|
||||
"Event": "frappe.core.notifications.get_todays_events",
|
||||
"Error Snapshot": {"seen": 0, "parent_error_snapshot": None},
|
||||
"Workflow Action": {"status": "Open"},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,74 +155,6 @@
|
|||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "System Logs",
|
||||
"link_count": 6,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Background Jobs",
|
||||
"link_count": 0,
|
||||
"link_to": "RQ Job",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Scheduled Jobs Logs",
|
||||
"link_count": 0,
|
||||
"link_to": "Scheduled Job Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Error Logs",
|
||||
"link_count": 0,
|
||||
"link_to": "Error Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Error Snapshot",
|
||||
"link_count": 0,
|
||||
"link_to": "Error Snapshot",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Communication Logs",
|
||||
"link_count": 0,
|
||||
"link_to": "Communication",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Activity Log",
|
||||
"link_count": 0,
|
||||
"link_to": "Activity Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
|
|
@ -331,9 +263,67 @@
|
|||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "System Logs",
|
||||
"link_count": 5,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Background Jobs",
|
||||
"link_count": 0,
|
||||
"link_to": "RQ Job",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Scheduled Jobs Logs",
|
||||
"link_count": 0,
|
||||
"link_to": "Scheduled Job Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Error Logs",
|
||||
"link_count": 0,
|
||||
"link_to": "Error Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Communication Logs",
|
||||
"link_count": 0,
|
||||
"link_to": "Communication",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Activity Log",
|
||||
"link_count": 0,
|
||||
"link_to": "Activity Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2023-05-24 14:47:24.395259",
|
||||
"modified": "2023-06-28 10:30:17.228167",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Build",
|
||||
|
|
|
|||
|
|
@ -132,7 +132,6 @@ def return_unsubscribed_page(email, doctype, name):
|
|||
def flush(from_test=False):
|
||||
"""flush email queue, every time: called from scheduler"""
|
||||
from frappe.email.doctype.email_queue.email_queue import send_mail
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
|
||||
# To avoid running jobs inside unit tests
|
||||
if frappe.are_emails_muted():
|
||||
|
|
@ -142,24 +141,16 @@ def flush(from_test=False):
|
|||
if cint(frappe.db.get_default("suspend_email_queue")) == 1:
|
||||
return
|
||||
|
||||
try:
|
||||
queued_jobs = set(get_jobs(site=frappe.local.site, key="job_name")[frappe.local.site])
|
||||
except Exception:
|
||||
queued_jobs = set()
|
||||
|
||||
for row in get_queue():
|
||||
try:
|
||||
job_name = f"email_queue_sendmail_{row.name}"
|
||||
if job_name not in queued_jobs:
|
||||
frappe.enqueue(
|
||||
method=send_mail,
|
||||
email_queue_name=row.name,
|
||||
now=from_test,
|
||||
job_name=job_name,
|
||||
queue="short",
|
||||
)
|
||||
else:
|
||||
frappe.logger().debug(f"Not queueing job {job_name} because it is in queue already")
|
||||
frappe.enqueue(
|
||||
method=send_mail,
|
||||
email_queue_name=row.name,
|
||||
now=from_test,
|
||||
job_id=f"email_queue_sendmail_{row.name}",
|
||||
queue="short",
|
||||
dedupicate=True,
|
||||
)
|
||||
except Exception:
|
||||
frappe.get_doc("Email Queue", row.name).log_error()
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ permission_query_conditions = {
|
|||
"Communication": "frappe.core.doctype.communication.communication.get_permission_query_conditions_for_communication",
|
||||
"Workflow Action": "frappe.workflow.doctype.workflow_action.workflow_action.get_permission_query_conditions",
|
||||
"Prepared Report": "frappe.core.doctype.prepared_report.prepared_report.get_permission_query_condition",
|
||||
"File": "frappe.core.doctype.file.file.get_permission_query_conditions",
|
||||
}
|
||||
|
||||
has_permission = {
|
||||
|
|
@ -213,7 +214,6 @@ scheduler_events = {
|
|||
"hourly": [
|
||||
"frappe.model.utils.link_count.update_link_count",
|
||||
"frappe.model.utils.user_settings.sync_user_settings",
|
||||
"frappe.utils.error.collect_error_snapshots",
|
||||
"frappe.desk.page.backups.backups.delete_downloadable_backups",
|
||||
"frappe.deferred_insert.save_to_db",
|
||||
"frappe.desk.form.document_follow.send_hourly_updates",
|
||||
|
|
|
|||
|
|
@ -617,7 +617,6 @@ def make_site_dirs():
|
|||
os.path.join("public", "files"),
|
||||
os.path.join("private", "backups"),
|
||||
os.path.join("private", "files"),
|
||||
"error-snapshots",
|
||||
"locks",
|
||||
"logs",
|
||||
]:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class Webhook(Document):
|
|||
self.validate_request_url()
|
||||
self.validate_request_body()
|
||||
self.validate_repeating_fields()
|
||||
self.validate_secret()
|
||||
self.preview_document = None
|
||||
|
||||
def on_update(self):
|
||||
|
|
@ -74,6 +75,13 @@ class Webhook(Document):
|
|||
if len(webhook_data) != len(set(webhook_data)):
|
||||
frappe.throw(_("Same Field is entered more than once"))
|
||||
|
||||
def validate_secret(self):
|
||||
if self.enable_security:
|
||||
try:
|
||||
self.get_password("webhook_secret", False).encode("utf8")
|
||||
except Exception:
|
||||
frappe.throw(_("Invalid Webhook Secret"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_preview(self):
|
||||
# This function doesn't need to do anything specific as virtual fields
|
||||
|
|
@ -112,16 +120,21 @@ def get_context(doc):
|
|||
|
||||
|
||||
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)
|
||||
try:
|
||||
webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name"))
|
||||
headers = get_webhook_headers(doc, webhook)
|
||||
data = get_webhook_data(doc, webhook)
|
||||
|
||||
if webhook.is_dynamic_url:
|
||||
request_url = frappe.render_template(webhook.request_url, get_context(doc))
|
||||
else:
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
frappe.logger().debug({"enqueue_webhook_error": e})
|
||||
log_request(webhook.name, doc.name, request_url, headers, data)
|
||||
return
|
||||
|
||||
r = None
|
||||
for i in range(3):
|
||||
try:
|
||||
r = requests.request(
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ execute:frappe.reload_doc('core', 'doctype', 'user') #2017-10-27
|
|||
execute:frappe.reload_doc('core', 'doctype', 'report_column')
|
||||
execute:frappe.reload_doc('core', 'doctype', 'report_filter')
|
||||
execute:frappe.reload_doc('core', 'doctype', 'report') #2020-08-25
|
||||
execute:frappe.reload_doc('core', 'doctype', 'error_snapshot')
|
||||
execute:frappe.get_doc("User", "Guest").save()
|
||||
execute:frappe.delete_doc("DocType", "Control Panel", force=1)
|
||||
execute:frappe.delete_doc("DocType", "Tag")
|
||||
|
|
@ -42,7 +41,6 @@ execute:frappe.db.sql("delete from `tabProperty Setter` where `property` = 'idx'
|
|||
execute:frappe.db.sql("delete from tabSessions where user is null")
|
||||
execute:frappe.delete_doc("DocType", "Backup Manager")
|
||||
execute:frappe.permissions.reset_perms("Web Page")
|
||||
execute:frappe.permissions.reset_perms("Error Snapshot")
|
||||
execute:frappe.db.sql("delete from `tabWeb Page` where ifnull(template_path, '')!=''")
|
||||
execute:frappe.core.doctype.language.language.update_language_names() # 2017-04-12
|
||||
execute:frappe.db.set_value("Print Settings", "Print Settings", "add_draft_heading", 1)
|
||||
|
|
@ -227,3 +225,4 @@ frappe.patches.v15_0.remove_background_jobs_from_dropdown
|
|||
frappe.desk.doctype.form_tour.patches.introduce_ui_tours
|
||||
execute:frappe.delete_doc_if_exists("Workspace", "Customization")
|
||||
execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter")
|
||||
execute:frappe.delete_doc_if_exists("DocType", "Error Snapshot")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ def execute():
|
|||
"Email Queue": get_current_setting("clear_email_queue_after") or 30,
|
||||
# child table on email queue
|
||||
"Email Queue Recipient": get_current_setting("clear_email_queue_after") or 30,
|
||||
"Error Snapshot": get_current_setting("clear_error_log_after") or 90,
|
||||
# newly added
|
||||
"Scheduled Job Log": 90,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,17 +118,24 @@ def has_permission(
|
|||
|
||||
def false_if_not_shared():
|
||||
if ptype in ("read", "write", "share", "submit", "email", "print"):
|
||||
shared = frappe.share.get_shared(
|
||||
doctype, user, ["read" if ptype in ("email", "print") else ptype]
|
||||
)
|
||||
|
||||
rights = ["read" if ptype in ("email", "print") else ptype]
|
||||
|
||||
if doc:
|
||||
doc_name = get_doc_name(doc)
|
||||
if doc_name in shared:
|
||||
shared = frappe.share.get_shared(
|
||||
doctype,
|
||||
user,
|
||||
rights=rights,
|
||||
filters=[["share_name", "=", doc_name]],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if shared:
|
||||
if ptype in ("read", "write", "share", "submit") or meta.permissions[0].get(ptype):
|
||||
return True
|
||||
|
||||
elif shared:
|
||||
elif frappe.share.get_shared(doctype, user, rights=rights, limit=1):
|
||||
# if atleast one shared doc of that type, then return True
|
||||
# this is used in db_query to check if permission on DocType
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -174,6 +174,15 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
|
|||
if (typeof options[0] === "string") {
|
||||
options = options.map((o) => ({ label: o, value: o }));
|
||||
}
|
||||
|
||||
options = options.map((o) => {
|
||||
if (typeof o !== "string") {
|
||||
o.label = o.label.toString();
|
||||
o.value = o.value.toString();
|
||||
}
|
||||
return o;
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -711,7 +711,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
get_column_html(col, doc) {
|
||||
if (col.type === "Status") {
|
||||
if (col.type === "Status" || col.df?.options == "Workflow State") {
|
||||
return `
|
||||
<div class="list-row-col hidden-xs ellipsis">
|
||||
${this.get_indicator_html(doc)}
|
||||
|
|
|
|||
|
|
@ -202,14 +202,16 @@ frappe.dashboard_utils = {
|
|||
},
|
||||
|
||||
get_all_filters(doc) {
|
||||
let filters = JSON.parse(doc.filters_json || "null");
|
||||
let dynamic_filters = JSON.parse(doc.dynamic_filters_json || "null");
|
||||
let filters = doc.filters_json ? JSON.parse(doc.filters_json) : null;
|
||||
let dynamic_filters = doc.dynamic_filters_json
|
||||
? JSON.parse(doc.dynamic_filters_json)
|
||||
: null;
|
||||
|
||||
if (!dynamic_filters) {
|
||||
if (!dynamic_filters || !Object.keys(dynamic_filters).length) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
if ($.isArray(dynamic_filters)) {
|
||||
if (Array.isArray(dynamic_filters)) {
|
||||
dynamic_filters.forEach((f) => {
|
||||
try {
|
||||
f[3] = eval(f[3]);
|
||||
|
|
|
|||
|
|
@ -45,42 +45,5 @@
|
|||
$background-color: var(--bg-color),
|
||||
$border-radius: var(--border-radius),
|
||||
) {
|
||||
|
||||
@if $content {
|
||||
img:after {
|
||||
content: url($content);
|
||||
}
|
||||
} @else {
|
||||
img:after {
|
||||
content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='lightgrey' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-image'><rect x='3' y='3' width='18' height='18' rx='2' ry='2'/><circle cx='8.5' cy='8.5' r='1.5'/><polyline points='21 15 16 10 5 21'/></svg>");
|
||||
}
|
||||
}
|
||||
|
||||
img[alt]:after {
|
||||
height: $height;
|
||||
top: $top;
|
||||
left: $left;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
@include flex();
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// @mixin img-foreground() {
|
||||
// content: "\f1c5";
|
||||
// display: block;
|
||||
// font-style: normal;
|
||||
// font-family: FontAwesome;
|
||||
// font-size: 32px;
|
||||
// color: var(--text-muted);
|
||||
|
||||
// position: absolute;
|
||||
// top: 50%;
|
||||
// transform: translateY(-50%);
|
||||
// left: 0;
|
||||
// width: 100%;
|
||||
// text-align: center;
|
||||
// }
|
||||
// Deprecated: Does not work as expected anymore. Also, this never worked in Safari.
|
||||
}
|
||||
|
|
@ -97,11 +97,6 @@
|
|||
color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@include broken-img(
|
||||
$height: 70px,
|
||||
$top: -15px,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -153,11 +153,6 @@
|
|||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include broken-img(
|
||||
$height: 175px,
|
||||
$border-radius: 0
|
||||
);
|
||||
}
|
||||
|
||||
.image-title {
|
||||
|
|
|
|||
|
|
@ -181,11 +181,6 @@
|
|||
color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@include broken-img(
|
||||
$height: 125px,
|
||||
$top: -4px,
|
||||
)
|
||||
}
|
||||
|
||||
.kanban-card-body {
|
||||
|
|
|
|||
|
|
@ -100,11 +100,11 @@
|
|||
|
||||
.group-by-button {
|
||||
margin: 5px;
|
||||
max-width: 125px;
|
||||
}
|
||||
|
||||
.group-by-button.btn-primary-light {
|
||||
color: var(--text-on-blue);
|
||||
outline: 1px solid var(--bg-dark-blue);
|
||||
}
|
||||
|
||||
.group-by-icon.active {
|
||||
|
|
|
|||
|
|
@ -100,10 +100,10 @@ def emit_via_redis(event, message, room):
|
|||
:param event: Event name, like `task_progress` etc.
|
||||
:param message: JSON message object. For async must contain `task_id`
|
||||
:param room: name of the room"""
|
||||
from frappe.utils.background_jobs import get_redis_conn
|
||||
from frappe.utils.background_jobs import get_redis_connection_without_auth
|
||||
|
||||
with suppress(redis.exceptions.ConnectionError):
|
||||
r = get_redis_conn()
|
||||
r = get_redis_connection_without_auth()
|
||||
r.publish("events", frappe.as_json({"event": event, "message": message, "room": room}))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,9 @@ import frappe.translate
|
|||
import frappe.utils
|
||||
from frappe import _
|
||||
from frappe.cache_manager import clear_user_cache
|
||||
from frappe.query_builder import DocType, Order
|
||||
from frappe.query_builder.functions import Now
|
||||
from frappe.query_builder.utils import PseudoColumn
|
||||
from frappe.query_builder import Order
|
||||
from frappe.utils import cint, cstr, get_assets_json
|
||||
from frappe.utils.data import add_to_date
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -62,7 +61,7 @@ def get_sessions_to_clear(user=None, keep_current=False):
|
|||
simultaneous_sessions = frappe.db.get_value("User", user, "simultaneous_sessions") or 1
|
||||
offset = simultaneous_sessions - 1
|
||||
|
||||
session = DocType("Sessions")
|
||||
session = frappe.qb.DocType("Sessions")
|
||||
session_id = frappe.qb.from_(session).where(session.user == user)
|
||||
if keep_current:
|
||||
session_id = session_id.where(session.sid != frappe.session.sid)
|
||||
|
|
@ -88,7 +87,7 @@ def delete_session(sid=None, user=None, reason="Session Expired"):
|
|||
frappe.cache.hdel("session", sid)
|
||||
frappe.cache.hdel("last_db_session_update", sid)
|
||||
if sid and not user:
|
||||
table = DocType("Sessions")
|
||||
table = frappe.qb.DocType("Sessions")
|
||||
user_details = (
|
||||
frappe.qb.from_(table).where(table.sid == sid).select(table.user).run(as_dict=True)
|
||||
)
|
||||
|
|
@ -112,17 +111,12 @@ def clear_all_sessions(reason=None):
|
|||
def get_expired_sessions():
|
||||
"""Returns list of expired sessions"""
|
||||
|
||||
sessions = DocType("Sessions")
|
||||
|
||||
return frappe.db.get_values(
|
||||
sessions,
|
||||
filters=(
|
||||
PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})") > get_expiry_period_for_query()
|
||||
),
|
||||
fieldname="sid",
|
||||
order_by=None,
|
||||
pluck=True,
|
||||
)
|
||||
sessions = frappe.qb.DocType("Sessions")
|
||||
return (
|
||||
frappe.qb.from_(sessions)
|
||||
.select(sessions.sid)
|
||||
.where(sessions.lastupdate < get_expired_threshold())
|
||||
).run(pluck=True)
|
||||
|
||||
|
||||
def clear_expired_sessions():
|
||||
|
|
@ -232,7 +226,7 @@ class Session:
|
|||
sid = frappe.generate_hash()
|
||||
|
||||
self.data.user = self.user
|
||||
self.data.sid = sid
|
||||
self.sid = self.data.sid = sid
|
||||
self.data.data.user = self.user
|
||||
self.data.data.session_ip = frappe.local.request_ip
|
||||
if self.user != "Guest":
|
||||
|
|
@ -268,14 +262,17 @@ class Session:
|
|||
frappe.db.commit()
|
||||
|
||||
def insert_session_record(self):
|
||||
frappe.db.sql(
|
||||
"""insert into `tabSessions`
|
||||
(`sessiondata`, `user`, `lastupdate`, `sid`, `status`)
|
||||
values (%s , %s, NOW(), %s, 'Active')""",
|
||||
(str(self.data["data"]), self.data["user"], self.data["sid"]),
|
||||
)
|
||||
|
||||
# also add to memcache
|
||||
Sessions = frappe.qb.DocType("Sessions")
|
||||
now = frappe.utils.now()
|
||||
|
||||
(
|
||||
frappe.qb.into(Sessions)
|
||||
.columns(
|
||||
Sessions.sessiondata, Sessions.user, Sessions.lastupdate, Sessions.sid, Sessions.status
|
||||
)
|
||||
.insert((str(self.data["data"]), self.data["user"], now, self.data["sid"], "Active"))
|
||||
).run()
|
||||
frappe.cache.hset("session", self.data.sid, self.data)
|
||||
|
||||
def resume(self):
|
||||
|
|
@ -338,20 +335,18 @@ class Session:
|
|||
return data and data.data
|
||||
|
||||
def get_session_data_from_db(self):
|
||||
sessions = DocType("Sessions")
|
||||
rec = frappe.db.get_values(
|
||||
sessions,
|
||||
filters=(sessions.sid == self.sid)
|
||||
& (
|
||||
PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})") < get_expiry_period_for_query()
|
||||
),
|
||||
fieldname=["user", "sessiondata"],
|
||||
order_by=None,
|
||||
)
|
||||
sessions = frappe.qb.DocType("Sessions")
|
||||
|
||||
if rec:
|
||||
data = frappe._dict(frappe.safe_eval(rec and rec[0][1] or "{}"))
|
||||
data.user = rec[0][0]
|
||||
record = (
|
||||
frappe.qb.from_(sessions)
|
||||
.select(sessions.user, sessions.sessiondata)
|
||||
.where(sessions.sid == self.sid)
|
||||
.where(sessions.lastupdate > get_expired_threshold())
|
||||
).run()
|
||||
|
||||
if record:
|
||||
data = frappe._dict(frappe.safe_eval(record and record[0][1] or "{}"))
|
||||
data.user = record[0][0]
|
||||
else:
|
||||
self._delete_session()
|
||||
data = None
|
||||
|
|
@ -373,6 +368,8 @@ class Session:
|
|||
|
||||
now = frappe.utils.now()
|
||||
|
||||
Sessions = frappe.qb.DocType("Sessions")
|
||||
|
||||
self.data["data"]["last_updated"] = now
|
||||
self.data["data"]["lang"] = str(frappe.lang)
|
||||
|
||||
|
|
@ -384,17 +381,14 @@ class Session:
|
|||
updated_in_db = False
|
||||
if (force or (time_diff is None) or (time_diff > 600)) and not frappe.flags.read_only:
|
||||
# update sessions table
|
||||
frappe.db.sql(
|
||||
"""update `tabSessions` set sessiondata=%s,
|
||||
lastupdate=NOW() where sid=%s""",
|
||||
(str(self.data["data"]), self.data["sid"]),
|
||||
)
|
||||
(
|
||||
frappe.qb.update(Sessions)
|
||||
.where(Sessions.sid == self.data["sid"])
|
||||
.set(Sessions.sessiondata, str(self.data["data"]))
|
||||
.set(Sessions.lastupdate, now)
|
||||
).run()
|
||||
|
||||
# update last active in user table
|
||||
frappe.db.sql(
|
||||
"""update `tabUser` set last_active=%(now)s where name=%(name)s""",
|
||||
{"now": now, "name": frappe.session.user},
|
||||
)
|
||||
frappe.db.set_value("User", frappe.session.user, "last_active", now, update_modified=False)
|
||||
|
||||
frappe.db.commit()
|
||||
frappe.cache.hset("last_db_session_update", self.sid, now)
|
||||
|
|
@ -421,6 +415,15 @@ def get_expiry_in_seconds(expiry=None):
|
|||
return (cint(parts[0]) * 3600) + (cint(parts[1]) * 60) + cint(parts[2])
|
||||
|
||||
|
||||
def get_expired_threshold():
|
||||
"""Get cutoff time before which all sessions are considered expired."""
|
||||
|
||||
now = frappe.utils.now()
|
||||
expiry_in_seconds = get_expiry_in_seconds()
|
||||
|
||||
return add_to_date(now, seconds=-expiry_in_seconds, as_string=True)
|
||||
|
||||
|
||||
def get_expiry_period():
|
||||
exp_sec = frappe.defaults.get_global_default("session_expiry") or "06:00:00"
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ def get_users(doctype, name):
|
|||
)
|
||||
|
||||
|
||||
def get_shared(doctype, user=None, rights=None):
|
||||
def get_shared(doctype, user=None, rights=None, *, filters=None, limit=None):
|
||||
"""Get list of shared document names for given user and DocType.
|
||||
|
||||
:param doctype: DocType of which shared names are queried.
|
||||
|
|
@ -154,14 +154,22 @@ def get_shared(doctype, user=None, rights=None):
|
|||
if not rights:
|
||||
rights = ["read"]
|
||||
|
||||
filters = [[right, "=", 1] for right in rights]
|
||||
filters += [["share_doctype", "=", doctype]]
|
||||
share_filters = [[right, "=", 1] for right in rights]
|
||||
share_filters += [["share_doctype", "=", doctype]]
|
||||
if filters:
|
||||
share_filters += filters
|
||||
|
||||
or_filters = [["user", "=", user]]
|
||||
if user != "Guest":
|
||||
or_filters += [["everyone", "=", 1]]
|
||||
|
||||
shared_docs = frappe.get_all(
|
||||
"DocShare", fields=["share_name"], filters=filters, or_filters=or_filters, order_by=None
|
||||
"DocShare",
|
||||
fields=["share_name"],
|
||||
filters=share_filters,
|
||||
or_filters=or_filters,
|
||||
order_by=None,
|
||||
limit_page_length=limit,
|
||||
)
|
||||
|
||||
return [doc.share_name for doc in shared_docs]
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe.auth import LoginAttemptTracker
|
||||
from frappe.frappeclient import AuthError, FrappeClient
|
||||
from frappe.sessions import Session, get_expired_sessions, get_expiry_in_seconds
|
||||
from frappe.tests.test_api import FrappeAPITestCase
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import get_site_url, now
|
||||
from frappe.utils.data import add_to_date
|
||||
from frappe.www.login import _generate_temporary_login_link
|
||||
|
||||
|
||||
|
|
@ -26,9 +30,7 @@ class TestAuth(FrappeTestCase):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.HOST_NAME = frappe.get_site_config().host_name or frappe.utils.get_site_url(
|
||||
frappe.local.site
|
||||
)
|
||||
cls.HOST_NAME = frappe.get_site_config().host_name or get_site_url(frappe.local.site)
|
||||
cls.test_user_email = "test_auth@test.com"
|
||||
cls.test_user_name = "test_auth_user"
|
||||
cls.test_user_mobile = "+911234567890"
|
||||
|
|
@ -197,3 +199,27 @@ class TestLoginAttemptTracker(FrappeTestCase):
|
|||
|
||||
tracker.add_failure_attempt()
|
||||
self.assertTrue(tracker.is_user_allowed())
|
||||
|
||||
|
||||
class TestSessionExpirty(FrappeAPITestCase):
|
||||
def test_session_expires(self):
|
||||
sid = self.sid # triggers login for test case login
|
||||
s: Session = frappe.local.session_obj
|
||||
|
||||
expiry_in = get_expiry_in_seconds()
|
||||
session_created = now()
|
||||
|
||||
# Try with 1% increments of times, it should always work
|
||||
for step in range(0, 100, 1):
|
||||
seconds_elapsed = expiry_in * step / 100
|
||||
|
||||
time_now = add_to_date(session_created, seconds=seconds_elapsed, as_string=True)
|
||||
with patch("frappe.utils.now", return_value=time_now):
|
||||
data = s.get_session_data_from_db()
|
||||
self.assertEqual(data.user, "Administrator")
|
||||
|
||||
# 1% higher should immediately expire
|
||||
time_now = add_to_date(session_created, seconds=expiry_in * 1.01, as_string=True)
|
||||
with patch("frappe.utils.now", return_value=time_now):
|
||||
self.assertIn(sid, get_expired_sessions())
|
||||
self.assertFalse(s.get_session_data_from_db())
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from frappe.tests.utils import FrappeTestCase
|
|||
from frappe.utils.background_jobs import (
|
||||
RQ_JOB_FAILURE_TTL,
|
||||
RQ_RESULTS_TTL,
|
||||
create_job_id,
|
||||
execute_job,
|
||||
generate_qname,
|
||||
get_redis_conn,
|
||||
|
|
@ -54,11 +55,12 @@ class TestBackgroundJobs(FrappeTestCase):
|
|||
|
||||
def test_enqueue_call(self):
|
||||
with patch.object(Queue, "enqueue_call") as mock_enqueue_call:
|
||||
frappe.enqueue(
|
||||
job = frappe.enqueue(
|
||||
"frappe.handler.ping",
|
||||
queue="short",
|
||||
timeout=300,
|
||||
kwargs={"site": frappe.local.site},
|
||||
job_id="test",
|
||||
)
|
||||
|
||||
mock_enqueue_call.assert_called_once_with(
|
||||
|
|
@ -78,7 +80,7 @@ class TestBackgroundJobs(FrappeTestCase):
|
|||
at_front=False,
|
||||
failure_ttl=RQ_JOB_FAILURE_TTL,
|
||||
result_ttl=RQ_RESULTS_TTL,
|
||||
job_id=None,
|
||||
job_id=create_job_id("test"),
|
||||
)
|
||||
|
||||
def test_job_hooks(self):
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ from unittest.mock import patch
|
|||
import click
|
||||
from click import Command
|
||||
from click.testing import CliRunner, Result
|
||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
import frappe.commands.scheduler
|
||||
import frappe.commands.site
|
||||
import frappe.commands.utils
|
||||
import frappe.recorder
|
||||
|
|
@ -760,6 +762,19 @@ class TestBenchBuild(BaseTestCommands):
|
|||
)
|
||||
|
||||
|
||||
class TestSchedulerUtils(BaseTestCommands):
|
||||
# Retry just in case there are stuck queued jobs
|
||||
@retry(
|
||||
retry=retry_if_exception_type(AssertionError),
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_fixed(3),
|
||||
reraise=True,
|
||||
)
|
||||
def test_ready_for_migrate(self):
|
||||
with cli(frappe.commands.scheduler.ready_for_migration) as result:
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
|
||||
|
||||
class TestCommandUtils(FrappeTestCase):
|
||||
def test_bench_helper(self):
|
||||
from frappe.utils.bench_helper import get_app_groups
|
||||
|
|
|
|||
|
|
@ -140,27 +140,6 @@ class TestDB(FrappeTestCase):
|
|||
frappe.db.get_value("DocType", "DocField", order_by="creation desc, modified asc, name", run=0),
|
||||
)
|
||||
|
||||
def test_get_value_limits(self):
|
||||
# check both dict and list style filters
|
||||
filters = [{"enabled": 1}, [["enabled", "=", 1]]]
|
||||
for filter in filters:
|
||||
self.assertEqual(1, len(frappe.db.get_values("User", filters=filter, limit=1)))
|
||||
# count of last touched rows as per DB-API 2.0 https://peps.python.org/pep-0249/#rowcount
|
||||
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount))
|
||||
self.assertEqual(2, len(frappe.db.get_values("User", filters=filter, limit=2)))
|
||||
self.assertGreaterEqual(2, cint(frappe.db._cursor.rowcount))
|
||||
|
||||
# without limits length == count
|
||||
self.assertEqual(
|
||||
len(frappe.db.get_values("User", filters=filter)), frappe.db.count("User", filter)
|
||||
)
|
||||
|
||||
frappe.db.get_value("User", filters=filter)
|
||||
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount))
|
||||
|
||||
frappe.db.exists("User", filter)
|
||||
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount))
|
||||
|
||||
def test_escape(self):
|
||||
frappe.db.escape("香港濟生堂製藥有限公司 - IT".encode())
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from frappe.model.base_document import get_controller
|
|||
from frappe.query_builder.utils import db_type_is
|
||||
from frappe.tests.test_query_builder import run_only_if
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cint
|
||||
from frappe.website.path_resolver import PathResolver
|
||||
|
||||
|
||||
|
|
@ -70,6 +71,25 @@ class TestPerformance(FrappeTestCase):
|
|||
with self.assertQueryCount(0):
|
||||
get_controller("User")
|
||||
|
||||
def test_get_value_limits(self):
|
||||
# check both dict and list style filters
|
||||
filters = [{"enabled": 1}, [["enabled", "=", 1]]]
|
||||
for filter in filters:
|
||||
with self.assertRowsRead(1):
|
||||
self.assertEqual(1, len(frappe.db.get_values("User", filters=filter, limit=1)))
|
||||
with self.assertRowsRead(2):
|
||||
self.assertEqual(2, len(frappe.db.get_values("User", filters=filter, limit=2)))
|
||||
|
||||
self.assertEqual(
|
||||
len(frappe.db.get_values("User", filters=filter)), frappe.db.count("User", filter)
|
||||
)
|
||||
|
||||
with self.assertRowsRead(1):
|
||||
frappe.db.get_value("User", filters=filter)
|
||||
|
||||
with self.assertRowsRead(1):
|
||||
frappe.db.exists("User", filter)
|
||||
|
||||
def test_db_value_cache(self):
|
||||
"""Link validation if repeated should just use db.value_cache, hence no extra queries"""
|
||||
doc = frappe.get_last_doc("User")
|
||||
|
|
|
|||
|
|
@ -11,10 +11,16 @@ from frappe.website.utils import build_response, clear_website_cache, get_home_p
|
|||
class TestWebsite(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.set_user("Guest")
|
||||
self._clearRequest()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.delete("Access Log")
|
||||
frappe.set_user("Administrator")
|
||||
self._clearRequest()
|
||||
|
||||
def _clearRequest(self):
|
||||
if hasattr(frappe.local, "request"):
|
||||
delattr(frappe.local, "request")
|
||||
|
||||
def test_home_page(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
|
@ -340,8 +346,9 @@ class TestWebsite(FrappeTestCase):
|
|||
FILES_TO_SKIP = choices(list(WWW.glob("**/*.py*")), k=10)
|
||||
|
||||
for suffix in FILES_TO_SKIP:
|
||||
content = get_response_content(suffix.relative_to(WWW))
|
||||
self.assertIn("404", content)
|
||||
path: str = suffix.relative_to(WWW).as_posix()
|
||||
content = get_response_content(path)
|
||||
self.assertIn("<title>Not Found</title>", content)
|
||||
|
||||
def test_metatags(self):
|
||||
content = get_response_content("/_test/_test_metatags")
|
||||
|
|
|
|||
|
|
@ -92,6 +92,26 @@ class FrappeTestCase(unittest.TestCase):
|
|||
finally:
|
||||
frappe.db.sql = orig_sql
|
||||
|
||||
@contextmanager
|
||||
def assertRowsRead(self, count):
|
||||
rows_read = 0
|
||||
|
||||
def _sql_with_count(*args, **kwargs):
|
||||
nonlocal rows_read
|
||||
|
||||
ret = orig_sql(*args, **kwargs)
|
||||
# count of last touched rows as per DB-API 2.0 https://peps.python.org/pep-0249/#rowcount
|
||||
rows_read += cint(frappe.db._cursor.rowcount)
|
||||
return ret
|
||||
|
||||
try:
|
||||
orig_sql = frappe.db.sql
|
||||
frappe.db.sql = _sql_with_count
|
||||
yield
|
||||
self.assertLessEqual(rows_read, count, msg="Queries read more rows than expected")
|
||||
finally:
|
||||
frappe.db.sql = orig_sql
|
||||
|
||||
|
||||
class MockedRequestTestCase(FrappeTestCase):
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@ import socket
|
|||
import time
|
||||
from collections import defaultdict
|
||||
from functools import lru_cache
|
||||
from typing import Any, Callable, Literal, NoReturn
|
||||
from typing import Any, Callable, NoReturn
|
||||
from uuid import uuid4
|
||||
|
||||
import redis
|
||||
from redis.exceptions import BusyLoadingError, ConnectionError
|
||||
from rq import Connection, Queue, Worker
|
||||
from rq import Queue, Worker
|
||||
from rq.exceptions import NoSuchJobError
|
||||
from rq.job import Job, JobStatus
|
||||
from rq.logutils import setup_loghandlers
|
||||
from rq.worker import RandomWorker, RoundRobinWorker
|
||||
from rq.worker import DequeueStrategy
|
||||
from rq.worker_pool import WorkerPool
|
||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
||||
|
||||
import frappe
|
||||
|
|
@ -65,6 +66,7 @@ def enqueue(
|
|||
on_failure: Callable = None,
|
||||
at_front: bool = False,
|
||||
job_id: str = None,
|
||||
deduplicate=False,
|
||||
**kwargs,
|
||||
) -> Job | Any:
|
||||
"""
|
||||
|
|
@ -78,14 +80,28 @@ def enqueue(
|
|||
:param job_name: [DEPRECATED] can be used to name an enqueue call, which can be used to prevent duplicate calls
|
||||
:param now: if now=True, the method is executed via frappe.call
|
||||
:param kwargs: keyword arguments to be passed to the method
|
||||
:param deduplicate: do not re-queue job if it's already queued, requires job_id.
|
||||
:param job_id: Assigning unique job id, which can be checked using `is_job_enqueued`
|
||||
"""
|
||||
# To handle older implementations
|
||||
is_async = kwargs.pop("async", is_async)
|
||||
|
||||
if job_id:
|
||||
# namespace job ids to sites
|
||||
job_id = create_job_id(job_id)
|
||||
if deduplicate:
|
||||
if not job_id:
|
||||
frappe.throw(_("`job_id` paramater is required for deduplication."))
|
||||
job = get_job(job_id)
|
||||
if job and job.get_status() in (JobStatus.QUEUED, JobStatus.STARTED):
|
||||
frappe.logger().debug(f"Not queueing job {job.id} because it is in queue already")
|
||||
return
|
||||
elif job:
|
||||
# delete job to avoid argument issues related to job args
|
||||
# https://github.com/rq/rq/issues/793
|
||||
job.delete()
|
||||
|
||||
# If job exists and is completed then delete it before re-queue
|
||||
|
||||
# namespace job ids to sites
|
||||
job_id = create_job_id(job_id)
|
||||
|
||||
if job_name:
|
||||
deprecation_warning("Using enqueue with `job_name` is deprecated, use `job_id` instead.")
|
||||
|
|
@ -230,14 +246,14 @@ def start_worker(
|
|||
rq_username: str | None = None,
|
||||
rq_password: str | None = None,
|
||||
burst: bool = False,
|
||||
strategy: Literal["round_robin", "random"] | None = None,
|
||||
strategy: DequeueStrategy | None = DequeueStrategy.DEFAULT,
|
||||
) -> NoReturn | None: # pragma: no cover
|
||||
"""Wrapper to start rq worker. Connects to redis and monitors these queues."""
|
||||
DEQUEUE_STRATEGIES = {"round_robin": RoundRobinWorker, "random": RandomWorker}
|
||||
|
||||
if frappe._tune_gc:
|
||||
gc.collect()
|
||||
gc.freeze()
|
||||
if not strategy:
|
||||
strategy = DequeueStrategy.DEFAULT
|
||||
|
||||
_freeze_gc()
|
||||
|
||||
with frappe.init_site():
|
||||
# empty init is required to get redis_queue from common_site_config.json
|
||||
|
|
@ -251,19 +267,59 @@ def start_worker(
|
|||
if os.environ.get("CI"):
|
||||
setup_loghandlers("ERROR")
|
||||
|
||||
WorkerKlass = DEQUEUE_STRATEGIES.get(strategy, Worker)
|
||||
logging_level = "INFO"
|
||||
if quiet:
|
||||
logging_level = "WARNING"
|
||||
|
||||
with Connection(redis_connection):
|
||||
logging_level = "INFO"
|
||||
if quiet:
|
||||
logging_level = "WARNING"
|
||||
worker = WorkerKlass(queues, name=get_worker_name(queue_name))
|
||||
worker.work(
|
||||
logging_level=logging_level,
|
||||
burst=burst,
|
||||
date_format="%Y-%m-%d %H:%M:%S",
|
||||
log_format="%(asctime)s,%(msecs)03d %(message)s",
|
||||
)
|
||||
worker = Worker(queues, name=get_worker_name(queue_name), connection=redis_connection)
|
||||
worker.work(
|
||||
logging_level=logging_level,
|
||||
burst=burst,
|
||||
date_format="%Y-%m-%d %H:%M:%S",
|
||||
log_format="%(asctime)s,%(msecs)03d %(message)s",
|
||||
dequeue_strategy=strategy,
|
||||
)
|
||||
|
||||
|
||||
def start_worker_pool(
|
||||
queue: str | None = None,
|
||||
num_workers: int = 1,
|
||||
quiet: bool = False,
|
||||
burst: bool = False,
|
||||
) -> NoReturn:
|
||||
"""Start worker pool with specified number of workers.
|
||||
|
||||
WARNING: This feature is considered "EXPERIMENTAL".
|
||||
"""
|
||||
|
||||
_freeze_gc()
|
||||
|
||||
with frappe.init_site():
|
||||
redis_connection = get_redis_conn()
|
||||
|
||||
if queue:
|
||||
queue = [q.strip() for q in queue.split(",")]
|
||||
queues = get_queue_list(queue, build_queue_name=True)
|
||||
|
||||
if os.environ.get("CI"):
|
||||
setup_loghandlers("ERROR")
|
||||
|
||||
logging_level = "INFO"
|
||||
if quiet:
|
||||
logging_level = "WARNING"
|
||||
|
||||
pool = WorkerPool(
|
||||
queues=queues,
|
||||
connection=redis_connection,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
pool.start(logging_level=logging_level, burst=burst)
|
||||
|
||||
|
||||
def _freeze_gc():
|
||||
if frappe._tune_gc:
|
||||
gc.collect()
|
||||
gc.freeze()
|
||||
|
||||
|
||||
def get_worker_name(queue):
|
||||
|
|
@ -353,8 +409,8 @@ def validate_queue(queue, default_queue_list=None):
|
|||
|
||||
|
||||
@retry(
|
||||
retry=retry_if_exception_type(BusyLoadingError) | retry_if_exception_type(ConnectionError),
|
||||
stop=stop_after_attempt(10),
|
||||
retry=retry_if_exception_type((BusyLoadingError, ConnectionError)),
|
||||
stop=stop_after_attempt(5),
|
||||
wait=wait_fixed(1),
|
||||
reraise=True,
|
||||
)
|
||||
|
|
@ -382,9 +438,7 @@ def get_redis_conn(username=None, password=None):
|
|||
|
||||
try:
|
||||
if not cred:
|
||||
if not _redis_queue_conn:
|
||||
_redis_queue_conn = RedisQueue.get_connection()
|
||||
return _redis_queue_conn
|
||||
return get_redis_connection_without_auth()
|
||||
else:
|
||||
return RedisQueue.get_connection(**cred)
|
||||
except (redis.exceptions.AuthenticationError, redis.exceptions.ResponseError):
|
||||
|
|
@ -399,6 +453,14 @@ def get_redis_conn(username=None, password=None):
|
|||
raise
|
||||
|
||||
|
||||
def get_redis_connection_without_auth():
|
||||
global _redis_queue_conn
|
||||
|
||||
if not _redis_queue_conn:
|
||||
_redis_queue_conn = RedisQueue.get_connection()
|
||||
return _redis_queue_conn
|
||||
|
||||
|
||||
def get_queues() -> list[Queue]:
|
||||
"""Get all the queues linked to the current bench."""
|
||||
queues = Queue.all(connection=get_redis_conn())
|
||||
|
|
@ -434,6 +496,9 @@ def test_job(s):
|
|||
|
||||
def create_job_id(job_id: str) -> str:
|
||||
"""Generate unique job id for deduplication"""
|
||||
|
||||
if not job_id:
|
||||
job_id = str(uuid4())
|
||||
return f"{frappe.local.site}::{job_id}"
|
||||
|
||||
|
||||
|
|
@ -443,9 +508,13 @@ def is_job_enqueued(job_id: str) -> bool:
|
|||
|
||||
def get_job_status(job_id: str) -> JobStatus | None:
|
||||
"""Get RQ job status, returns None if job is not found."""
|
||||
job = get_job(job_id)
|
||||
if job:
|
||||
return job.get_status()
|
||||
|
||||
|
||||
def get_job(job_id: str) -> Job:
|
||||
try:
|
||||
job = Job.fetch(create_job_id(job_id), connection=get_redis_conn())
|
||||
return Job.fetch(create_job_id(job_id), connection=get_redis_conn())
|
||||
except NoSuchJobError:
|
||||
return None
|
||||
|
||||
return job.get_status()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import os
|
|||
import traceback
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
import click
|
||||
|
||||
|
|
@ -52,10 +53,19 @@ def get_sites(site_arg: str) -> list[str]:
|
|||
return [site_arg]
|
||||
elif os.environ.get("FRAPPE_SITE"):
|
||||
return [os.environ.get("FRAPPE_SITE")]
|
||||
elif os.path.exists("currentsite.txt"):
|
||||
with open("currentsite.txt") as f:
|
||||
if site := f.read().strip():
|
||||
return [site]
|
||||
elif default_site := frappe.get_conf().default_site:
|
||||
return [default_site]
|
||||
# This is not supported, just added here for warning.
|
||||
elif (site := frappe.read_file("currentsite.txt")) and site.strip():
|
||||
click.secho(
|
||||
dedent(
|
||||
f"""
|
||||
WARNING: currentsite.txt is not supported anymore for setting default site. Use following command to set it as default site.
|
||||
$ bench use {site}"""
|
||||
),
|
||||
fg="red",
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -593,7 +593,7 @@ jobs:
|
|||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Cache pip
|
||||
|
|
|
|||
|
|
@ -79,6 +79,15 @@ def get_pending_jobs(site=None):
|
|||
return jobs_per_queue
|
||||
|
||||
|
||||
def any_job_pending(site: str) -> bool:
|
||||
for queue in get_queue_list():
|
||||
q = get_queue(queue)
|
||||
for job_id in q.get_job_ids():
|
||||
if job_id.startswith(site):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_number_of_workers():
|
||||
return len(get_workers())
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
# Copyright (c) 2015, Maxwell Morais and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import datetime
|
||||
import functools
|
||||
import inspect
|
||||
import json
|
||||
import linecache
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cstr, encode
|
||||
|
||||
EXCLUDE_EXCEPTIONS = (
|
||||
frappe.AuthenticationError,
|
||||
|
|
@ -37,194 +30,57 @@ def _is_ldap_exception(e):
|
|||
return False
|
||||
|
||||
|
||||
def make_error_snapshot(exception):
|
||||
if frappe.conf.disable_error_snapshot:
|
||||
def log_error(
|
||||
title=None, message=None, reference_doctype=None, reference_name=None, *, defer_insert=False
|
||||
):
|
||||
"""Log error to Error Log"""
|
||||
# Parameter ALERT:
|
||||
# the title and message may be swapped
|
||||
# the better API for this is log_error(title, message), and used in many cases this way
|
||||
# this hack tries to be smart about whats a title (single line ;-)) and fixes it
|
||||
|
||||
traceback = None
|
||||
if message:
|
||||
if "\n" in title: # traceback sent as title
|
||||
traceback, title = title, message
|
||||
else:
|
||||
traceback = message
|
||||
|
||||
title = title or "Error"
|
||||
traceback = frappe.as_unicode(traceback or frappe.get_traceback(with_context=True))
|
||||
|
||||
if not frappe.db:
|
||||
print(f"Failed to log error in db: {title}")
|
||||
return
|
||||
|
||||
error_log = frappe.get_doc(
|
||||
doctype="Error Log",
|
||||
error=traceback,
|
||||
method=title,
|
||||
reference_doctype=reference_doctype,
|
||||
reference_name=reference_name,
|
||||
)
|
||||
|
||||
if frappe.flags.read_only or defer_insert:
|
||||
error_log.deferred_insert()
|
||||
else:
|
||||
return error_log.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def log_error_snapshot(exception: Exception):
|
||||
|
||||
if isinstance(exception, EXCLUDE_EXCEPTIONS) or _is_ldap_exception(exception):
|
||||
return
|
||||
|
||||
logger = frappe.logger(with_more_info=True)
|
||||
|
||||
try:
|
||||
error_id = "{timestamp:s}-{ip:s}-{hash:s}".format(
|
||||
timestamp=cstr(datetime.datetime.now()),
|
||||
ip=frappe.local.request_ip or "127.0.0.1",
|
||||
hash=frappe.generate_hash(length=3),
|
||||
)
|
||||
snapshot_folder = get_error_snapshot_path()
|
||||
frappe.create_folder(snapshot_folder)
|
||||
|
||||
snapshot_file_path = os.path.join(snapshot_folder, f"{error_id}.json")
|
||||
snapshot = get_snapshot(exception)
|
||||
|
||||
with open(encode(snapshot_file_path), "wb") as error_file:
|
||||
error_file.write(encode(frappe.as_json(snapshot)))
|
||||
|
||||
logger.error(f"New Exception collected with id: {error_id}")
|
||||
|
||||
log_error(title=str(exception), defer_insert=True)
|
||||
logger.error("New Exception collected in error log")
|
||||
except Exception as e:
|
||||
logger.error(f"Could not take error snapshot: {e}", exc_info=True)
|
||||
|
||||
|
||||
def get_snapshot(exception, context=10):
|
||||
import pydoc
|
||||
|
||||
"""
|
||||
Return a dict describing a given traceback (based on cgitb.text)
|
||||
"""
|
||||
|
||||
etype, evalue, etb = sys.exc_info()
|
||||
if isinstance(etype, type):
|
||||
etype = etype.__name__
|
||||
|
||||
# creates a snapshot dict with some basic information
|
||||
|
||||
s = {
|
||||
"pyver": "Python {version:s}: {executable:s} (prefix: {prefix:s})".format(
|
||||
version=sys.version.split(maxsplit=1)[0], executable=sys.executable, prefix=sys.prefix
|
||||
),
|
||||
"timestamp": cstr(datetime.datetime.now()),
|
||||
"traceback": traceback.format_exc(),
|
||||
"frames": [],
|
||||
"etype": cstr(etype),
|
||||
"evalue": cstr(repr(evalue)),
|
||||
"exception": {},
|
||||
"locals": {},
|
||||
}
|
||||
|
||||
# start to process frames
|
||||
records = inspect.getinnerframes(etb, 5)
|
||||
|
||||
for frame, file, lnum, func, lines, index in records:
|
||||
file = file and os.path.abspath(file) or "?"
|
||||
args, varargs, varkw, locals = inspect.getargvalues(frame)
|
||||
call = ""
|
||||
|
||||
if func != "?":
|
||||
call = inspect.formatargvalues(
|
||||
args, varargs, varkw, locals, formatvalue=lambda value: f"={pydoc.text.repr(value)}"
|
||||
)
|
||||
|
||||
# basic frame information
|
||||
f = {"file": file, "func": func, "call": call, "lines": {}, "lnum": lnum}
|
||||
|
||||
def reader(lnum=[lnum]): # noqa
|
||||
try:
|
||||
# B023: function is evaluated immediately, binding not necessary
|
||||
return linecache.getline(file, lnum[0]) # noqa: B023
|
||||
finally:
|
||||
lnum[0] += 1
|
||||
|
||||
vars = _scanvars(reader, frame, locals)
|
||||
|
||||
# if it is a view, replace with generated code
|
||||
# if file.endswith('html'):
|
||||
# lmin = lnum > context and (lnum - context) or 0
|
||||
# lmax = lnum + context
|
||||
# lines = code.split("\n")[lmin:lmax]
|
||||
# index = min(context, lnum) - 1
|
||||
|
||||
if index is not None:
|
||||
i = lnum - index
|
||||
for line in lines:
|
||||
f["lines"][i] = line.rstrip()
|
||||
i += 1
|
||||
|
||||
# dump local variable (referenced in current line only)
|
||||
f["dump"] = {}
|
||||
for name, where, value in vars:
|
||||
if name in f["dump"]:
|
||||
continue
|
||||
if value is not __UNDEF__:
|
||||
if where == "global":
|
||||
name = f"global {name:s}"
|
||||
elif where != "local":
|
||||
name = where + " " + name.split(".")[-1]
|
||||
f["dump"][name] = pydoc.text.repr(value)
|
||||
else:
|
||||
f["dump"][name] = "undefined"
|
||||
|
||||
s["frames"].append(f)
|
||||
|
||||
# add exception type, value and attributes
|
||||
if isinstance(evalue, BaseException):
|
||||
for name in dir(evalue):
|
||||
if name != "messages" and not name.startswith("__"):
|
||||
value = pydoc.text.repr(getattr(evalue, name))
|
||||
s["exception"][name] = encode(value)
|
||||
|
||||
# add all local values (of last frame) to the snapshot
|
||||
for name, value in locals.items():
|
||||
s["locals"][name] = value if isinstance(value, str) else pydoc.text.repr(value)
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def collect_error_snapshots():
|
||||
"""Scheduled task to collect error snapshots from files and push into Error Snapshot table"""
|
||||
if frappe.conf.disable_error_snapshot:
|
||||
return
|
||||
|
||||
try:
|
||||
path = get_error_snapshot_path()
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
|
||||
for fname in os.listdir(path):
|
||||
fullpath = os.path.join(path, fname)
|
||||
|
||||
try:
|
||||
with open(fullpath) as filedata:
|
||||
data = json.load(filedata)
|
||||
|
||||
except ValueError:
|
||||
# empty file
|
||||
os.remove(fullpath)
|
||||
continue
|
||||
|
||||
for field in ["locals", "exception", "frames"]:
|
||||
data[field] = frappe.as_json(data[field])
|
||||
|
||||
doc = frappe.new_doc("Error Snapshot")
|
||||
doc.update(data)
|
||||
doc.save()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
os.remove(fullpath)
|
||||
|
||||
clear_old_snapshots()
|
||||
|
||||
except Exception as e:
|
||||
make_error_snapshot(e)
|
||||
|
||||
# prevent creation of unlimited error snapshots
|
||||
raise
|
||||
|
||||
|
||||
def clear_old_snapshots():
|
||||
"""Clear snapshots that are older than a month"""
|
||||
from frappe.query_builder import DocType, Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
|
||||
ErrorSnapshot = DocType("Error Snapshot")
|
||||
frappe.db.delete(ErrorSnapshot, filters=(ErrorSnapshot.creation < (Now() - Interval(months=1))))
|
||||
|
||||
path = get_error_snapshot_path()
|
||||
today = datetime.datetime.now()
|
||||
|
||||
for file in os.listdir(path):
|
||||
p = os.path.join(path, file)
|
||||
ctime = datetime.datetime.fromtimestamp(os.path.getctime(p))
|
||||
if (today - ctime).days > 31:
|
||||
os.remove(os.path.join(path, p))
|
||||
|
||||
|
||||
def get_error_snapshot_path():
|
||||
return frappe.get_site_path("error-snapshots")
|
||||
|
||||
|
||||
def get_default_args(func):
|
||||
"""Get default arguments of a function from its signature."""
|
||||
signature = inspect.signature(func)
|
||||
|
|
@ -270,56 +126,3 @@ def raise_error_on_no_output(error_message, error_type=None, keep_quiet=None):
|
|||
return wrapper_raise_error_on_no_output
|
||||
|
||||
return decorator_raise_error_on_no_output
|
||||
|
||||
|
||||
# Vendored from cgitb standard library reused under PSF License:
|
||||
# https://github.com/python/cpython/blob/main/LICENSE
|
||||
|
||||
|
||||
import keyword
|
||||
import tokenize
|
||||
|
||||
__UNDEF__ = [] # a special sentinel object
|
||||
|
||||
|
||||
def _scanvars(reader, frame, locals):
|
||||
"""Scan one logical line of Python and look up values of variables used."""
|
||||
vars, lasttoken, parent, prefix, value = [], None, None, "", __UNDEF__
|
||||
for ttype, token, start, end, line in tokenize.generate_tokens(reader):
|
||||
if ttype == tokenize.NEWLINE:
|
||||
break
|
||||
if ttype == tokenize.NAME and token not in keyword.kwlist:
|
||||
if lasttoken == ".":
|
||||
if parent is not __UNDEF__:
|
||||
value = getattr(parent, token, __UNDEF__)
|
||||
vars.append((prefix + token, prefix, value))
|
||||
else:
|
||||
where, value = _lookup(token, frame, locals)
|
||||
vars.append((token, where, value))
|
||||
elif token == ".":
|
||||
prefix += lasttoken + "."
|
||||
parent = value
|
||||
else:
|
||||
parent, prefix = None, ""
|
||||
lasttoken = token
|
||||
return vars
|
||||
|
||||
|
||||
def _lookup(name, frame, locals):
|
||||
"""Find the value for a given name in the given environment."""
|
||||
if name in locals:
|
||||
return "local", locals[name]
|
||||
if name in frame.f_globals:
|
||||
return "global", frame.f_globals[name]
|
||||
if "__builtins__" in frame.f_globals:
|
||||
builtins = frame.f_globals["__builtins__"]
|
||||
if type(builtins) is type({}): # noqa
|
||||
if name in builtins:
|
||||
return "builtin", builtins[name]
|
||||
else:
|
||||
if hasattr(builtins, name):
|
||||
return "builtin", getattr(builtins, name)
|
||||
return None, __UNDEF__
|
||||
|
||||
|
||||
# end: vendored code
|
||||
|
|
|
|||
|
|
@ -631,7 +631,7 @@ def get_link_options(web_form_name, doctype, allow_read_on_all_link_options=Fals
|
|||
if title_field and show_title_field_in_link:
|
||||
return json.dumps(link_options, default=str)
|
||||
else:
|
||||
return "\n".join([doc.value for doc in link_options])
|
||||
return "\n".join([str(doc.value) for doc in link_options])
|
||||
|
||||
else:
|
||||
raise frappe.PermissionError(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,18 @@ import frappe
|
|||
from frappe.website.page_renderers.base_renderer import BaseRenderer
|
||||
from frappe.website.utils import is_binary_file
|
||||
|
||||
UNSUPPORTED_STATIC_PAGE_TYPES = ("html", "md", "js", "xml", "css", "txt", "py", "json")
|
||||
UNSUPPORTED_STATIC_PAGE_TYPES = (
|
||||
"css",
|
||||
"html",
|
||||
"js",
|
||||
"json",
|
||||
"md",
|
||||
"py",
|
||||
"pyc",
|
||||
"pyo",
|
||||
"txt",
|
||||
"xml",
|
||||
)
|
||||
|
||||
|
||||
class StaticPage(BaseRenderer):
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ class PathResolver:
|
|||
TemplatePage,
|
||||
ListPage,
|
||||
PrintPage,
|
||||
NotFoundPage,
|
||||
]
|
||||
|
||||
for renderer in renderers:
|
||||
|
|
|
|||
|
|
@ -31,10 +31,6 @@ function get_conf() {
|
|||
if (process.env.FRAPPE_SITE) {
|
||||
conf.default_site = process.env.FRAPPE_SITE;
|
||||
}
|
||||
if (fs.existsSync("sites/currentsite.txt")) {
|
||||
conf.default_site = fs.readFileSync("sites/currentsite.txt").toString().trim();
|
||||
}
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue