Merge branch 'develop' into log-webhook-error

This commit is contained in:
Shariq Ansari 2023-06-28 11:20:00 +05:30 committed by GitHub
commit 7ba35ffc45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 1661 additions and 2260 deletions

View file

@ -54,6 +54,8 @@ fi
echo "Starting Bench..."
export FRAPPE_TUNE_GC=True
bench start &> ~/frappe-bench/bench_start.log &
if [ "$TYPE" == "server" ]

View file

@ -25,7 +25,7 @@ jobs:
fetch-depth: 200
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
check-latest: true
- name: Check commit titles

View file

@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- uses: actions/setup-python@v4
with:

View file

@ -71,7 +71,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
check-latest: true
- name: Add to Hosts

View file

@ -16,7 +16,7 @@ jobs:
path: 'frappe'
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- uses: actions/setup-python@v4
with:
python-version: '3.11'

View file

@ -90,7 +90,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
check-latest: true
- name: Add to Hosts

View file

@ -78,7 +78,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
check-latest: true
- name: Add to Hosts

View file

@ -59,11 +59,13 @@ context("Form", () => {
.blur();
cy.click_listview_row_item_with_text("Test Form Contact 3");
cy.scrollTo(0);
cy.get("#page-Contact .page-head").findByTitle("Test Form Contact 3").should("exist");
cy.get(".prev-doc").should("be.visible").click();
cy.get(".msgprint-dialog .modal-body").contains("No further records").should("be.visible");
cy.hide_dialog();
cy.scrollTo(0);
cy.get("#page-Contact .page-head").findByTitle("Test Form Contact 3").should("exist");
cy.get(".next-doc").should("be.visible").click();
cy.get(".msgprint-dialog .modal-body").contains("No further records").should("be.visible");

View file

@ -13,15 +13,8 @@ context("List View", () => {
it("Keep checkbox checked after Refresh", { scrollBehavior: false }, () => {
cy.go_to_list("ToDo");
cy.clear_filters();
cy.get(".list-row-container .list-row-checkbox").click({
multiple: true,
force: true,
});
cy.get(".actions-btn-group button").contains("Actions").should("be.visible");
cy.intercept("/api/method/frappe.desk.reportview.get").as("list-refresh");
cy.wait(3000); // wait before you hit another refresh
cy.get('button[data-original-title="Refresh"]').click();
cy.wait("@list-refresh");
cy.get(".list-header-subject > .list-subject > .list-check-all").click();
cy.get("button[data-original-title='Refresh']").click();
cy.get(".list-row-container .list-row-checkbox:checked").should("be.visible");
});
@ -39,11 +32,8 @@ context("List View", () => {
];
cy.go_to_list("ToDo");
cy.clear_filters();
cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({
multiple: true,
force: true,
});
cy.get(".actions-btn-group button").contains("Actions").should("be.visible").click();
cy.get(".list-header-subject > .list-subject > .list-check-all").click();
cy.findByRole("button", { name: "Actions" }).click();
cy.get(".dropdown-menu li:visible .dropdown-item")
.should("have.length", 9)
.each((el, index) => {
@ -56,8 +46,7 @@ context("List View", () => {
}).as("bulk-approval");
cy.wrap(elements).contains("Approve").click();
cy.wait("@bulk-approval");
cy.wait(300);
cy.get_open_dialog().find(".btn-modal-close").click();
cy.hide_dialog();
cy.reload();
cy.clear_filters();
cy.get(".list-row-container:visible").should("contain", "Approved");

View file

@ -60,6 +60,11 @@ const argv = yargs
type: "boolean",
description: "Run build command for apps",
})
.option("save-metafiles", {
type: "boolean",
description:
"Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
})
.example("node esbuild --apps frappe,erpnext", "Run build only for frappe and erpnext")
.example(
"node esbuild --files frappe/website.bundle.js,frappe/desk.bundle.js",
@ -401,6 +406,13 @@ async function write_assets_json(metafile) {
await fs.promises.writeFile(assets_json_path, JSON.stringify(new_assets_json, null, 4));
await update_assets_json_in_cache();
if (argv["save-metafiles"]) {
// use current timestamp in readable formate as a suffix for filename
let current_timestamp = new Date().getTime();
const metafile_name = `meta-${current_timestamp}.json`;
await fs.promises.writeFile(`${metafile_name}`, JSON.stringify(metafile));
log(`Saved metafile as ${metafile_name}`);
}
return {
new_assets_json,
prev_assets_json,

View file

@ -11,6 +11,7 @@ be used to build database driven apps.
Read the documentation: https://frappeframework.com/docs
"""
import functools
import gc
import importlib
import inspect
import json
@ -57,6 +58,7 @@ re._MAXCACHE = (
50 # reduced from default 512 given we are already maintaining this on parent worker
)
_tune_gc = bool(os.environ.get("FRAPPE_TUNE_GC", False))
if _dev_server:
warnings.simplefilter("always", DeprecationWarning)
@ -2211,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>'
@ -2435,3 +2402,15 @@ def validate_and_sanitize_search_inputs(fn):
return fn(**kwargs)
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+
# objects easily.
# Bump this number higher, this will make GC less aggressive but that improves performance of
# everything else.
g0, g1, g2 = gc.get_threshold() # defaults are 700, 10, 10.
gc.set_threshold(g0 * 10, g1 * 2, g2 * 2)

View file

@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import gc
import logging
import os
@ -21,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)
@ -30,6 +31,30 @@ _site = None
_sites_path = os.environ.get("SITES_PATH", ".")
# If gc.freeze is done then importing modules before forking allows us to share the memory
if frappe._tune_gc:
import frappe.boot
import frappe.client
import frappe.core.doctype.user.user
import frappe.database.mariadb.database # Load database related utils
import frappe.database.query
import frappe.desk.desktop # workspace
import frappe.model.db_query
import frappe.query_builder
import frappe.utils.background_jobs # Enqueue is very common
import frappe.utils.data # common utils
import frappe.utils.jinja # web page rendering
import frappe.utils.jinja_globals
import frappe.utils.redis_wrapper # Exact redis_wrapper
import frappe.utils.safe_exec
import frappe.utils.typing_validations # any whitelisted method uses this
import frappe.website.path_resolver # all the page types and resolver
import frappe.website.router # Website router
import frappe.website.website_generator # web page doctypes
# end: module pre-loading
@local_manager.middleware
@Request.application
def application(request: Request):
@ -321,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)
@ -394,3 +419,17 @@ def serve(
use_evalex=not in_test_env,
threaded=not no_threading,
)
# Both Gunicorn and RQ use forking to spawn workers. In an ideal world, the fork should be sharing
# most of the memory if there are no writes made to data because of Copy on Write, however,
# python's GC is not CoW friendly and writes to data even if user-code doesn't. Specifically, the
# generational GC which stores and mutates every python object: `PyGC_Head`
#
# Calling gc.freeze() moves all the objects imported so far into permanant generation and hence
# doesn't mutate `PyGC_Head`
#
# Refer to issue for more info: https://github.com/frappe/frappe/issues/18927
if frappe._tune_gc:
gc.collect() # clean up any garbage created so far before freeze
gc.freeze()

View file

@ -9,7 +9,6 @@ from tempfile import mkdtemp, mktemp
from urllib.parse import urlparse
import click
import psutil
from semantic_version import Version
import frappe
@ -230,6 +229,7 @@ def bundle(
verbose=False,
skip_frappe=False,
files=None,
save_metafiles=False,
):
"""concat / minify js files"""
setup()
@ -249,6 +249,9 @@ def bundle(
command += " --run-build-command"
if save_metafiles:
command += " --save-metafiles"
check_node_executable()
frappe_app_path = frappe.get_app_path("frappe", "..")
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env(), raise_err=True)
@ -275,8 +278,8 @@ def watch(apps=None):
def check_node_executable():
node_version = Version(subprocess.getoutput("node -v")[1:])
warn = "⚠️ "
if node_version.major < 14:
click.echo(f"{warn} Please update your node version to 14")
if node_version.major < 18:
click.echo(f"{warn} Please update your node version to 18")
if not shutil.which("yarn"):
click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn")
click.echo()
@ -288,6 +291,8 @@ def get_node_env():
def get_safe_max_old_space_size():
import psutil
safe_max_old_space_size = 0
try:
total_memory = psutil.virtual_memory().total / (1024 * 1024)

View file

@ -215,6 +215,27 @@ 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
@ -251,5 +272,6 @@ commands = [
show_pending_jobs,
start_scheduler,
start_worker,
start_worker_pool,
trigger_scheduler_event,
]

View file

@ -31,6 +31,12 @@ EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
@click.option(
"--force", is_flag=True, default=False, help="Force build assets instead of downloading available"
)
@click.option(
"--save-metafiles",
is_flag=True,
default=False,
help="Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
)
def build(
app=None,
apps=None,
@ -38,6 +44,7 @@ def build(
production=False,
verbose=False,
force=False,
save_metafiles=False,
):
"Compile JS and CSS source files"
from frappe.build import bundle, download_frappe_assets
@ -62,7 +69,14 @@ def build(
if production:
mode = "production"
bundle(mode, apps=apps, hard_link=hard_link, verbose=verbose, skip_frappe=skip_frappe)
bundle(
mode,
apps=apps,
hard_link=hard_link,
verbose=verbose,
skip_frappe=skip_frappe,
save_metafiles=save_metafiles,
)
@click.command("watch")

View file

@ -1,7 +1,10 @@
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
from ldap3.core.exceptions import LDAPException, LDAPInappropriateAuthenticationResult
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils.error import _is_ldap_exception
# test_records = frappe.get_test_records('Error Log')
@ -12,3 +15,9 @@ class TestErrorLog(FrappeTestCase):
doc = frappe.new_doc("Error Log")
error = doc.log_error("This is an error")
self.assertEqual(error.doctype, "Error Log")
def test_ldap_exceptions(self):
exc = [LDAPException, LDAPInappropriateAuthenticationResult]
for e in exc:
self.assertTrue(_is_ldap_exception(e()))

View file

@ -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 }}
{% } %}

View file

@ -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>
{% } %}

View file

@ -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");
});
}
});

View file

@ -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"
}

View file

@ -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))))

View file

@ -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);
});
},
};

View file

@ -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")

View file

@ -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
}
],

View file

@ -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

View file

@ -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")

View file

@ -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",
]

View file

@ -62,7 +62,6 @@ class TestLogSettings(FrappeTestCase):
"Activity Log",
"Email Queue",
"Route History",
"Error Snapshot",
"Scheduled Job Log",
]

View file

@ -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)

View file

@ -96,6 +96,17 @@ class TestRQJob(FrappeTestCase):
_, stderr = execute_in_shell("bench worker --queue short,default --burst", check_exit_code=True)
self.assertIn("quitting", cstr(stderr))
@timeout(20)
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_dedup(self):
job_id = "test_dedup"

View 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} &rarr; </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);
}
}

View file

@ -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"},
},
}

View file

@ -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",

View file

@ -4,8 +4,6 @@ FrappeClient is a library that helps you connect with other frappe systems
import base64
import json
import requests
import frappe
from frappe.utils.data import cstr
@ -37,6 +35,8 @@ class FrappeClient:
api_secret=None,
frappe_authorization_source=None,
):
import requests
self.headers = {
"Accept": "application/json",
"content-type": "application/x-www-form-urlencoded",
@ -390,42 +390,13 @@ class FrappeClient:
class FrappeOAuth2Client(FrappeClient):
def __init__(self, url, access_token, verify=True):
import requests
self.access_token = access_token
self.headers = {
"Authorization": "Bearer " + access_token,
"content-type": "application/x-www-form-urlencoded",
}
self.verify = verify
self.session = OAuth2Session(self.headers)
self.session = requests.session()
self.url = url
def get_request(self, params):
res = requests.get(
self.url, params=self.preprocess(params), headers=self.headers, verify=self.verify
)
res = self.post_process(res)
return res
def post_request(self, data):
res = requests.post(
self.url, data=self.preprocess(data), headers=self.headers, verify=self.verify
)
res = self.post_process(res)
return res
class OAuth2Session:
def __init__(self, headers):
self.headers = headers
def get(self, url, params, verify):
res = requests.get(url, params=params, headers=self.headers, verify=verify)
return res
def post(self, url, data, verify):
res = requests.post(url, data=data, headers=self.headers, verify=verify)
return res
def put(self, url, data, verify):
res = requests.put(url, data=data, headers=self.headers, verify=verify)
return res

View file

@ -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",

View file

@ -287,6 +287,9 @@ def install_app(name, verbose=False, set_as_patched=True, force=False):
if out is False:
return
for fn in frappe.get_hooks("before_app_install"):
frappe.get_attr(fn)(name)
if name != "frappe":
add_module_defs(name, ignore_if_duplicate=force)
@ -302,6 +305,9 @@ def install_app(name, verbose=False, set_as_patched=True, force=False):
for after_install in app_hooks.after_install or []:
frappe.get_attr(after_install)()
for fn in frappe.get_hooks("after_app_install"):
frappe.get_attr(fn)(name)
sync_jobs()
sync_fixtures(name)
sync_customizations(name)
@ -369,6 +375,9 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
for before_uninstall in app_hooks.before_uninstall or []:
frappe.get_attr(before_uninstall)()
for fn in frappe.get_hooks("before_app_uninstall"):
frappe.get_attr(fn)(app_name)
modules = frappe.get_all("Module Def", filters={"app_name": app_name}, pluck="name")
drop_doctypes = _delete_modules(modules, dry_run=dry_run)
@ -382,6 +391,9 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
for after_uninstall in app_hooks.after_uninstall or []:
frappe.get_attr(after_uninstall)()
for fn in frappe.get_hooks("after_app_uninstall"):
frappe.get_attr(fn)(app_name)
click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
frappe.flags.in_uninstall = False
@ -605,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",
]:

View file

@ -34,6 +34,7 @@ ignore_values = {
"Print Style": ["disabled"],
"Module Onboarding": ["is_complete"],
"Onboarding Step": ["is_complete", "is_skipped"],
"Workspace": ["is_hidden"],
}
ignore_doctypes = [""]

View file

@ -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")

View file

@ -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,
}

View file

@ -6,7 +6,7 @@ import { ref } from "vue";
import { useStore } from "../store";
import { move_children_to_parent, confirm_dialog } from "../utils";
let props = defineProps(["section", "column"]);
const props = defineProps(["section", "column"]);
let store = useStore();
let hovered = ref(false);

View file

@ -3,7 +3,7 @@ import { ref, nextTick, computed } from "vue";
import { useStore } from "../store";
let store = useStore();
let props = defineProps({
const props = defineProps({
text: {
type: String
},
@ -46,7 +46,7 @@ function focus_on_label() {
:disabled="store.read_only"
type="text"
:placeholder="placeholder"
v-model="text"
:value="text"
:style="{ width: hidden_span_width }"
@input="event => $emit('update:modelValue', event.target.value)"
@keydown.enter="editing = false"

View file

@ -4,7 +4,7 @@ import { ref, computed } from "vue";
import { useStore } from "../store";
import { move_children_to_parent, clone_field } from "../utils";
let props = defineProps(["column", "field"]);
const props = defineProps(["column", "field"]);
let store = useStore();
let hovered = ref(false);

View file

@ -6,7 +6,7 @@ import { ref } from "vue";
import { useStore } from "../store";
import { section_boilerplate, move_children_to_parent, confirm_dialog } from "../utils";
let props = defineProps(["tab", "section"]);
const props = defineProps(["tab", "section"]);
let store = useStore();
let hovered = ref(false);

View file

@ -1,6 +1,6 @@
<!-- Used as Attach & Attach Image Control -->
<script setup>
let props = defineProps(["df"]);
const props = defineProps(["df"]);
</script>
<template>

View file

@ -1,6 +1,6 @@
<!-- Used as Button & Heading Control -->
<script setup>
let props = defineProps(["df", "value"]);
const props = defineProps(["df", "value"]);
</script>
<template>

View file

@ -3,7 +3,7 @@ import { useStore } from "../../store";
import { useSlots } from "vue";
let store = useStore();
let props = defineProps(["df", "value"]);
const props = defineProps(["df", "value"]);
let slots = useSlots();
</script>

View file

@ -4,7 +4,7 @@ import { computed, onMounted, ref, useSlots, watch } from "vue";
import { useStore } from "../../store";
let store = useStore();
let props = defineProps(["df", "modelValue"]);
const props = defineProps(["df", "modelValue"]);
let emit = defineEmits(["update:modelValue"]);
let slots = useSlots();

View file

@ -4,7 +4,7 @@ import { useStore } from "../../store";
import { ref, useSlots } from "vue";
let store = useStore();
let props = defineProps(["df", "value"]);
const props = defineProps(["df", "value"]);
let slots = useSlots();
let time_zone = ref("");
let placeholder = ref("");

View file

@ -1,7 +1,7 @@
<script setup>
import { onMounted, ref } from "vue";
let props = defineProps(["df"]);
const props = defineProps(["df"]);
let map = ref(null);
let map_control = ref(null);

View file

@ -1,5 +1,5 @@
<script setup>
let props = defineProps(["df"]);
const props = defineProps(["df"]);
</script>
<template>

View file

@ -4,7 +4,7 @@ import { onMounted, ref, useSlots, computed, watch } from "vue";
import { useStore } from "../../store";
let store = useStore();
let props = defineProps(["args", "df", "modelValue"]);
const props = defineProps(["args", "df", "modelValue"]);
let emit = defineEmits(["update:modelValue"]);
let slots = useSlots();

View file

@ -1,7 +1,7 @@
<script setup>
import { onMounted, ref, watch } from "vue";
let props = defineProps(["df"]);
const props = defineProps(["df"]);
let rating = ref(null);
let rating_control = ref(null);

View file

@ -3,7 +3,7 @@ import { useStore } from "../../store";
import { useSlots, onMounted, ref, computed, watch } from "vue";
let store = useStore();
let props = defineProps(["df", "modelValue", "no_label"]);
const props = defineProps(["df", "modelValue", "no_label"]);
let emit = defineEmits(["update:modelValue"]);
let slots = useSlots();

View file

@ -1,5 +1,5 @@
<script setup>
let props = defineProps(["df"]);
const props = defineProps(["df"]);
</script>
<template>

View file

@ -2,7 +2,7 @@
import { get_table_columns } from "../../utils";
import { computedAsync } from "@vueuse/core";
let props = defineProps(["df"]);
const props = defineProps(["df"]);
let table_columns = computedAsync(async () => {
let doctype = props.df.options;

View file

@ -5,7 +5,7 @@ import { useSlots, ref, computed, watch } from "vue";
import { computedAsync } from "@vueuse/core";
let store = useStore();
let props = defineProps(["df", "value", "modelValue"]);
const props = defineProps(["df", "value", "modelValue"]);
let emit = defineEmits(["update:modelValue"]);
let slots = useSlots();

View file

@ -1,7 +1,7 @@
<script setup>
import { onMounted, ref } from "vue";
let props = defineProps(["df"]);
const props = defineProps(["df"]);
let quill = ref(null);
let quill_control = ref(null);

View file

@ -60,7 +60,7 @@ import ProgressRing from "./ProgressRing.vue";
let emit = defineEmits(["toggle_optimize", "toggle_private", "toggle_image_cropper", "remove"]);
// props
let props = defineProps({
const props = defineProps({
file: Object,
});

View file

@ -608,7 +608,8 @@ defineExpose({
add_files,
upload_files,
toggle_all_private,
wrapper_ready
wrapper_ready,
close_dialog,
});
</script>

View file

@ -43,7 +43,7 @@ import { computed, onMounted, ref, watch } from "vue";
import Cropper from "cropperjs";
// props
let props = defineProps({
const props = defineProps({
file: Object,
fixed_aspect_ratio: Number,
});

View file

@ -44,7 +44,7 @@
import { computed, ref } from "vue";
// props
let props = defineProps({
const props = defineProps({
primary: String,
secondary: String,
radius: Number,

View file

@ -35,7 +35,7 @@ import TreeNode from "./TreeNode.vue";
import { computed } from "vue";
// props
let props = defineProps({
const props = defineProps({
node: Object,
selected_node: Object,
});

View file

@ -1,5 +1,6 @@
import { createApp } from "vue";
import FileUploaderComponent from "./FileUploader.vue";
import { watch } from "vue";
class FileUploader {
constructor({
@ -52,8 +53,8 @@ class FileUploader {
this.uploader.wrapper_ready = true;
}
this.uploader.$watch(
"files",
watch(
() => this.uploader.files,
(files) => {
let all_private = files.every((file) => file.private);
if (this.dialog) {
@ -65,27 +66,36 @@ class FileUploader {
{ deep: true }
);
this.uploader.$watch("trigger_upload", (trigger_upload) => {
if (trigger_upload) {
this.upload_files();
watch(
() => this.uploader.trigger_upload,
(trigger_upload) => {
if (trigger_upload) {
this.upload_files();
}
}
});
);
this.uploader.$watch("close_dialog", (close_dialog) => {
if (close_dialog) {
this.dialog && this.dialog.hide();
watch(
() => this.uploader.close_dialog,
(close_dialog) => {
if (close_dialog) {
this.dialog && this.dialog.hide();
}
}
});
);
this.uploader.$watch("hide_dialog_footer", (hide_dialog_footer) => {
if (hide_dialog_footer) {
this.dialog && this.dialog.footer.addClass("hide");
this.dialog.$wrapper.data("bs.modal")._config.backdrop = "static";
} else {
this.dialog && this.dialog.footer.removeClass("hide");
this.dialog.$wrapper.data("bs.modal")._config.backdrop = true;
watch(
() => this.uploader.hide_dialog_footer,
(hide_dialog_footer) => {
if (hide_dialog_footer) {
this.dialog && this.dialog.footer.addClass("hide");
this.dialog.$wrapper.data("bs.modal")._config.backdrop = "static";
} else {
this.dialog && this.dialog.footer.removeClass("hide");
this.dialog.$wrapper.data("bs.modal")._config.backdrop = true;
}
}
});
);
if (files && files.length) {
this.uploader.add_files(files);

View file

@ -111,14 +111,14 @@ export default class KanbanSettings {
fields_html.html(`
<div class="form-group">
<div class="clearfix">
<label class="control-label" style="padding-right: 0px;">Fields</label>
<label class="control-label" style="padding-right: 0px;">${__("Fields")}</label>
</div>
<div class="control-input-wrapper">
${fields}
</div>
<p class="help-box small text-muted">
<a class="add-new-fields text-muted">
+ Add / Remove Fields
${__("+ Add / Remove Fields")}
</a>
</p>
</div>

View file

@ -1,5 +1,5 @@
// This file is used to make sure that `moment` is bound to the window
// before the bundle finishes loading, due to imports (datetime.js) in the bundle
// that depend on `moment`.
import momentTimezone from "moment-timezone/builds/moment-timezone-with-data.js";
import momentTimezone from "moment-timezone/builds/moment-timezone-with-data-10-year-range.min.js";
window.moment = momentTimezone;

View file

@ -70,7 +70,7 @@ import { computed } from "vue";
import draggable from "vuedraggable";
// props
let props = defineProps(["df"]);
const props = defineProps(["df"]);
// methods
function remove_column(column) {

View file

@ -79,7 +79,7 @@ import ConfigureColumnsVue from "./ConfigureColumns.vue";
import { createApp, ref, nextTick, watch } from "vue";
// props
let props = defineProps(["df"]);
const props = defineProps(["df"]);
// variables
let editing = ref(false);

View file

@ -17,7 +17,7 @@
import { ref } from "vue";
// props
let props = defineProps(["value", "button-label"]);
const props = defineProps(["value", "button-label"]);
// emits
let emit = defineEmits(["change"]);

View file

@ -20,7 +20,7 @@ import { getStore } from "./store";
import { computed, ref, onMounted, provide } from "vue";
// props
let props = defineProps(["print_format_name"]);
const props = defineProps(["print_format_name"]);
// variables
let show_preview = ref(false);

View file

@ -83,7 +83,7 @@ import Field from "./Field.vue";
import { computed } from "vue";
// props
let props = defineProps(["section"]);
const props = defineProps(["section"]);
// emits
let emit = defineEmits(["add_section_above"]);

View file

@ -1,4 +1,4 @@
import { createApp } from "vue";
import { createApp, watch } from "vue";
import PrintFormatBuilderComponent from "./PrintFormatBuilder.vue";
class PrintFormatBuilder {
@ -32,8 +32,8 @@ class PrintFormatBuilder {
SetVueGlobals(app);
this.$component = app.mount(this.$wrapper.get(0));
this.$component.$watch(
"$store.dirty",
watch(
() => this.$component.$store.dirty,
(dirty) => {
if (dirty.value) {
this.page.set_indicator("Not Saved", "orange");
@ -48,9 +48,12 @@ class PrintFormatBuilder {
{ deep: true }
);
this.$component.$watch("show_preview", (value) => {
$toggle_preview_btn.text(value ? __("Hide Preview") : __("Show Preview"));
});
watch(
() => this.$component.show_preview,
(value) => {
$toggle_preview_btn.text(value ? __("Hide Preview") : __("Show Preview"));
}
);
}
}

View file

@ -4,7 +4,6 @@
from typing import TYPE_CHECKING
from urllib.parse import parse_qs, urljoin, urlparse
import jwt
import requests
from werkzeug.test import TestResponse
@ -362,6 +361,8 @@ class TestOAuth20(FrappeRequestTestCase):
self.assertTrue(payload.get("nonce") == nonce)
def decode_id_token(self, id_token):
import jwt
return jwt.decode(
id_token,
audience=self.client_id,

View file

@ -5,7 +5,6 @@ from base64 import b32encode, b64encode
from io import BytesIO
import pyotp
from pyqrcode import create as qrcreate
import frappe
import frappe.defaults
@ -387,6 +386,8 @@ def send_token_via_email(user, token, otp_secret, otp_issuer, subject=None, mess
def get_qr_svg_code(totp_uri):
"""Get SVG code to display Qrcode for OTP."""
from pyqrcode import create as qrcreate
url = qrcreate(totp_uri)
svg = ""
stream = BytesIO()

View file

@ -1,18 +1,20 @@
import gc
import os
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
@ -229,10 +231,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 not strategy:
strategy = DequeueStrategy.DEFAULT
_freeze_gc()
with frappe.init_site():
# empty init is required to get redis_queue from common_site_config.json
@ -246,19 +252,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):

View file

@ -371,6 +371,22 @@ app_license = "{app_license}"
# before_uninstall = "{app_name}.uninstall.before_uninstall"
# after_uninstall = "{app_name}.uninstall.after_uninstall"
# Integration Setup
# ------------------
# To set up dependencies/integrations with other apps
# Name of the app being installed is passed as an argument
# before_app_install = "{app_name}.utils.before_app_install"
# after_app_install = "{app_name}.utils.after_app_install"
# Integration Cleanup
# -------------------
# To clean up dependencies/integrations with other apps
# Name of the app being uninstalled is passed as an argument
# before_app_uninstall = "{app_name}.utils.before_app_uninstall"
# after_app_uninstall = "{app_name}.utils.after_app_uninstall"
# Desk Notifications
# ------------------
# See frappe.core.notifications.get_notification_config
@ -577,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

View file

@ -15,6 +15,9 @@ from typing import Any, Literal, Optional, TypeVar, Union
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlparse, urlunparse
from click import secho
from dateutil import parser
from dateutil.parser import ParserError
from dateutil.relativedelta import relativedelta
import frappe
from frappe.desk.utils import slug
@ -80,9 +83,6 @@ def getdate(
Converts string date (yyyy-mm-dd) to datetime.date object.
If no input is provided, current date is returned.
"""
from dateutil import parser
from dateutil.parser._parser import ParserError
if not string_date:
return get_datetime().date()
if isinstance(string_date, datetime.datetime):
@ -105,7 +105,6 @@ def getdate(
def get_datetime(
datetime_str: Optional["DateTimeLikeObject"] = None,
) -> datetime.datetime | None:
from dateutil import parser
if datetime_str is None:
return now_datetime()
@ -141,9 +140,6 @@ def get_timedelta(time: str | None = None) -> datetime.timedelta | None:
Returns:
datetime.timedelta: Timedelta object equivalent of the passed `time` string
"""
from dateutil import parser
from dateutil.parser import ParserError
time = time or "0:0:0"
try:
@ -161,8 +157,6 @@ def get_timedelta(time: str | None = None) -> datetime.timedelta | None:
def to_timedelta(time_str: str | datetime.time) -> datetime.timedelta:
from dateutil import parser
if isinstance(time_str, datetime.time):
time_str = str(time_str)
@ -237,9 +231,6 @@ def add_to_date(
as_datetime=False,
) -> DateTimeLikeObject:
"""Adds `days` to the given date"""
from dateutil import parser
from dateutil.parser._parser import ParserError
from dateutil.relativedelta import relativedelta
if date is None:
date = now_datetime()
@ -500,9 +491,6 @@ def get_year_ending(date) -> datetime.date:
def get_time(time_str: str) -> datetime.time:
from dateutil import parser
from dateutil.parser import ParserError
if isinstance(time_str, datetime.datetime):
return time_str.time()
elif isinstance(time_str, datetime.time):

View file

@ -1,216 +1,86 @@
# Copyright (c) 2015, Maxwell Morais and contributors
# License: MIT. See LICENSE
import datetime
import functools
import inspect
import json
import linecache
import os
import pydoc
import sys
import traceback
from ldap3.core.exceptions import LDAPException
import frappe
from frappe.utils import cstr, encode
EXCLUDE_EXCEPTIONS = (
frappe.AuthenticationError,
frappe.CSRFTokenError, # CSRF covers OAuth too
frappe.SecurityException,
LDAPException,
frappe.InReadOnlyMode,
)
LDAP_BASE_EXCEPTION = "LDAPException"
def make_error_snapshot(exception):
if frappe.conf.disable_error_snapshot:
def _is_ldap_exception(e):
"""Check if exception is from LDAP library.
This is a hack but ensures that LDAP is not imported unless it's required. This is tested in
unittests in case the exception changes in future.
"""
for t in type(e).__mro__:
if t.__name__ == LDAP_BASE_EXCEPTION:
return True
return False
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
if isinstance(exception, EXCLUDE_EXCEPTIONS):
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):
"""
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)
@ -256,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

View file

@ -5,8 +5,6 @@ import base64
import json
from typing import TYPE_CHECKING, Callable
import jwt
import frappe
import frappe.utils
from frappe import _
@ -126,6 +124,9 @@ def login_via_oauth2_id_token(
def get_info_via_oauth(
provider: str, code: str, decoder: Callable | None = None, id_token: bool = False
):
import jwt
flow = get_oauth2_flow(provider)
oauth2_providers = get_oauth2_providers()

View file

@ -8,7 +8,8 @@ from contextlib import suppress
import frappe
from frappe.utils import getdate
from frappe.utils.caching import site_cache
from posthog import Posthog
from posthog import Posthog # isort: skip
POSTHOG_PROJECT_FIELD = "posthog_project_id"
POSTHOG_HOST_FIELD = "posthog_host"

View file

@ -5,7 +5,6 @@ from urllib.parse import quote
import frappe
from frappe import _
from frappe.integrations.google_oauth import GoogleOAuth
from frappe.model.document import Document
from frappe.utils import encode, get_request_site_address
from frappe.website.utils import get_boot_data
@ -100,6 +99,8 @@ class WebsiteSettings(Document):
frappe.clear_cache()
def get_access_token(self):
from frappe.integrations.google_oauth import GoogleOAuth
if not self.indexing_refresh_token:
button_label = frappe.bold(_("Allow API Indexing Access"))
raise frappe.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label))

View file

@ -5,7 +5,6 @@ import frappe
import frappe.utils
from frappe import _
from frappe.auth import LoginManager
from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings
from frappe.rate_limiter import rate_limit
from frappe.utils import cint, get_url
from frappe.utils.data import escape_html
@ -85,7 +84,10 @@ def get_context(context):
)
context["social_login"] = True
context["ldap_settings"] = LDAPSettings.get_ldap_client_settings()
if cint(frappe.db.get_value("LDAP Settings", "LDAP Settings", "enabled")):
from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings
context["ldap_settings"] = LDAPSettings.get_ldap_client_settings()
login_label = [_("Email")]

View file

@ -16,7 +16,7 @@
"url": "https://github.com/frappe/frappe/issues"
},
"engines": {
"node": ">=14"
"node": ">=18"
},
"homepage": "https://frappeframework.com",
"dependencies": {
@ -56,7 +56,7 @@
"moment": "^2.29.4",
"moment-timezone": "^0.5.35",
"pinia": "^2.0.23",
"plyr": "^3.7.2",
"plyr": "^3.7.8",
"popper.js": "^1.16.0",
"postcss": "8",
"quill": "2.0.0-dev.4",
@ -72,7 +72,7 @@
"sortablejs": "1.9.0",
"superagent": "^3.8.2",
"touch": "^3.1.0",
"vue": "3.2.39",
"vue": "^3.3.0",
"vue-router": "^4.1.5",
"vuedraggable": "^4.1.0",
"vuex": "4.0.2",

View file

@ -62,7 +62,7 @@ dependencies = [
"hiredis~=2.2.3",
"requests-oauthlib~=1.3.1",
"requests~=2.31.0",
"rq~=1.15.0",
"rq~=1.15.1",
"rsa>=4.1",
"semantic-version~=2.10.0",
"sqlparse~=0.4.4",

2440
yarn.lock

File diff suppressed because it is too large Load diff