Merge branch 'develop' into log-webhook-error
This commit is contained in:
commit
7ba35ffc45
85 changed files with 1661 additions and 2260 deletions
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
|
|
@ -54,6 +54,8 @@ fi
|
|||
|
||||
echo "Starting Bench..."
|
||||
|
||||
export FRAPPE_TUNE_GC=True
|
||||
|
||||
bench start &> ~/frappe-bench/bench_start.log &
|
||||
|
||||
if [ "$TYPE" == "server" ]
|
||||
|
|
|
|||
2
.github/workflows/linters.yml
vendored
2
.github/workflows/linters.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/on_release.yml
vendored
2
.github/workflows/on_release.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/patch-mariadb-tests.yml
vendored
2
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/publish-assets-develop.yml
vendored
2
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -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'
|
||||
|
|
|
|||
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
|
|
@ -90,7 +90,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
|
|
|
|||
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
|
|
@ -78,7 +78,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]:
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ ignore_values = {
|
|||
"Print Style": ["disabled"],
|
||||
"Module Onboarding": ["is_complete"],
|
||||
"Onboarding Step": ["is_complete", "is_skipped"],
|
||||
"Workspace": ["is_hidden"],
|
||||
}
|
||||
|
||||
ignore_doctypes = [""]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!-- Used as Attach & Attach Image Control -->
|
||||
<script setup>
|
||||
let props = defineProps(["df"]);
|
||||
const props = defineProps(["df"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!-- Used as Button & Heading Control -->
|
||||
<script setup>
|
||||
let props = defineProps(["df", "value"]);
|
||||
const props = defineProps(["df", "value"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
let props = defineProps(["df"]);
|
||||
const props = defineProps(["df"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
let props = defineProps(["df"]);
|
||||
const props = defineProps(["df"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -608,7 +608,8 @@ defineExpose({
|
|||
add_files,
|
||||
upload_files,
|
||||
toggle_all_private,
|
||||
wrapper_ready
|
||||
wrapper_ready,
|
||||
close_dialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
import { computed, ref } from "vue";
|
||||
|
||||
// props
|
||||
let props = defineProps({
|
||||
const props = defineProps({
|
||||
primary: String,
|
||||
secondary: String,
|
||||
radius: Number,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue