Merge branch 'develop' into feature/force-web-capture-setting
This commit is contained in:
commit
c61608f1a7
132 changed files with 2688 additions and 3084 deletions
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
|
|
@ -54,8 +54,6 @@ 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");
|
||||
|
|
@ -109,22 +111,6 @@ context("Form", () => {
|
|||
cy.get("@email_input2").should("not.have.class", "invalid");
|
||||
});
|
||||
|
||||
it("Shows version conflict warning", { scrollBehavior: false }, () => {
|
||||
cy.visit("/app/todo");
|
||||
|
||||
cy.insert_doc("ToDo", { description: "old" }).then((doc) => {
|
||||
cy.visit(`/app/todo/${doc.name}`);
|
||||
// make form dirty
|
||||
cy.fill_field("status", "Cancelled", "Select");
|
||||
|
||||
// update doc using api - simulating parallel change by another user
|
||||
cy.update_doc("ToDo", doc.name, { status: "Closed" }).then(() => {
|
||||
cy.findByRole("button", { name: "Refresh" }).click();
|
||||
cy.get_field("status", "Select").should("have.value", "Closed");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Jump to field in collapsed section", { scrollBehavior: false }, () => {
|
||||
cy.new_form("User");
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
80
cypress/integration/socket_updates.js
Normal file
80
cypress/integration/socket_updates.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
context("Realtime updates", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit("/app/todo");
|
||||
// required because immediately after load socket is still connecting.
|
||||
// Not a huge deal breaker in prod.
|
||||
cy.wait(500);
|
||||
cy.clear_filters();
|
||||
});
|
||||
|
||||
it("Shows version conflict warning", { scrollBehavior: false }, () => {
|
||||
cy.insert_doc("ToDo", { description: "old" }).then((doc) => {
|
||||
cy.visit(`/app/todo/${doc.name}`);
|
||||
// make form dirty
|
||||
cy.fill_field("status", "Cancelled", "Select");
|
||||
|
||||
// update doc using api - simulating parallel change by another user
|
||||
cy.update_doc("ToDo", doc.name, { status: "Closed" }).then(() => {
|
||||
cy.findByRole("button", { name: "Refresh" }).click();
|
||||
cy.get_field("status", "Select").should("have.value", "Closed");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("List view updates in realtime on insert", { scrollBehavior: false }, () => {
|
||||
const original = "Added for realtime update";
|
||||
const updated = "Updated for realtime update";
|
||||
cy.insert_doc("ToDo", { description: original }).then((doc) => {
|
||||
cy.contains(original).should("be.visible");
|
||||
|
||||
// update doc using api - simulating parallel change by another user
|
||||
cy.update_doc("ToDo", doc.name, { description: updated }).then(() => {
|
||||
cy.contains(updated).should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Recieves msgprint from server", { scrollBehavior: false }, () => {
|
||||
// required because immediately after load socket is still connecting.
|
||||
// Not a deal breaker in prod
|
||||
const msg = "msgprint sent via realtime";
|
||||
publish_realtime({ event: "msgprint", message: msg }).then(() => {
|
||||
cy.contains(msg).should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
it("Recieves custom messages from server", { scrollBehavior: false }, () => {
|
||||
const event = "cypress_event";
|
||||
let handler = {
|
||||
handle() {
|
||||
console.log("clear");
|
||||
},
|
||||
};
|
||||
cy.spy(handler, "handle").as("callback");
|
||||
|
||||
cy.window()
|
||||
.its("frappe")
|
||||
.then((frappe) => {
|
||||
frappe.realtime.on(event, () => handler.handle());
|
||||
});
|
||||
|
||||
publish_realtime({ event }).then(() => {
|
||||
cy.get("@callback").should("be.called");
|
||||
});
|
||||
});
|
||||
|
||||
it("Progress bar", { scrollBehavior: false }, () => {
|
||||
const title = "RealTime Progress";
|
||||
cy.call("frappe.tests.ui_test_helpers.publish_progress", { title }).then(() => {
|
||||
cy.contains(title).should("be.visible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function publish_realtime(args) {
|
||||
return cy.call("frappe.tests.ui_test_helpers.publish_realtime", args);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -58,7 +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))
|
||||
_tune_gc = bool(sbool(os.environ.get("FRAPPE_TUNE_GC", True)))
|
||||
|
||||
if _dev_server:
|
||||
warnings.simplefilter("always", DeprecationWarning)
|
||||
|
|
@ -2213,41 +2213,6 @@ def logger(
|
|||
)
|
||||
|
||||
|
||||
def log_error(title=None, message=None, reference_doctype=None, reference_name=None):
|
||||
"""Log error to Error Log"""
|
||||
# Parameter ALERT:
|
||||
# the title and message may be swapped
|
||||
# the better API for this is log_error(title, message), and used in many cases this way
|
||||
# this hack tries to be smart about whats a title (single line ;-)) and fixes it
|
||||
|
||||
traceback = None
|
||||
if message:
|
||||
if "\n" in title: # traceback sent as title
|
||||
traceback, title = title, message
|
||||
else:
|
||||
traceback = message
|
||||
|
||||
title = title or "Error"
|
||||
traceback = as_unicode(traceback or get_traceback(with_context=True))
|
||||
|
||||
if not db:
|
||||
print(f"Failed to log error in db: {title}")
|
||||
return
|
||||
|
||||
error_log = get_doc(
|
||||
doctype="Error Log",
|
||||
error=traceback,
|
||||
method=title,
|
||||
reference_doctype=reference_doctype,
|
||||
reference_name=reference_name,
|
||||
)
|
||||
|
||||
if flags.read_only:
|
||||
error_log.deferred_insert()
|
||||
else:
|
||||
return error_log.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def get_desk_link(doctype, name):
|
||||
html = (
|
||||
'<a href="/app/Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
|
||||
|
|
@ -2439,6 +2404,8 @@ def validate_and_sanitize_search_inputs(fn):
|
|||
return wrapper
|
||||
|
||||
|
||||
from frappe.utils.error import log_error # noqa: backward compatibility
|
||||
|
||||
if _tune_gc:
|
||||
# generational GC gets triggered after certain allocs (g0) which is 700 by default.
|
||||
# This number is quite small for frappe where a single query can potentially create 700+
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from frappe import _
|
|||
from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest
|
||||
from frappe.middlewares import StaticDataMiddleware
|
||||
from frappe.utils import cint, get_site_name, sanitize_html
|
||||
from frappe.utils.error import make_error_snapshot
|
||||
from frappe.utils.error import log_error_snapshot
|
||||
from frappe.website.serve import get_response
|
||||
|
||||
local_manager = LocalManager(frappe.local)
|
||||
|
|
@ -346,7 +346,7 @@ def handle_exception(e):
|
|||
frappe.local.login_manager.clear_cookies()
|
||||
|
||||
if http_status_code >= 500:
|
||||
make_error_snapshot(e)
|
||||
log_error_snapshot(e)
|
||||
|
||||
if return_as_message:
|
||||
response = get_response("message", http_status_code=http_status_code)
|
||||
|
|
|
|||
|
|
@ -349,8 +349,6 @@ class CookieManager:
|
|||
expires = datetime.datetime.now() + datetime.timedelta(days=3)
|
||||
if frappe.session.sid:
|
||||
self.set_cookie("sid", frappe.session.sid, expires=expires, httponly=True)
|
||||
if frappe.session.session_country:
|
||||
self.set_cookie("country", frappe.session.session_country)
|
||||
|
||||
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
|
||||
if not secure and hasattr(frappe.local, "request"):
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ def bundle(
|
|||
verbose=False,
|
||||
skip_frappe=False,
|
||||
files=None,
|
||||
save_metafiles=False,
|
||||
):
|
||||
"""concat / minify js files"""
|
||||
setup()
|
||||
|
|
@ -248,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)
|
||||
|
|
@ -274,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()
|
||||
|
|
|
|||
|
|
@ -215,18 +215,39 @@ def start_worker(
|
|||
)
|
||||
|
||||
|
||||
@click.command("worker-pool")
|
||||
@click.option(
|
||||
"--queue",
|
||||
type=str,
|
||||
help="Queue to consume from. Multiple queues can be specified using comma-separated string. If not specified all queues are consumed.",
|
||||
)
|
||||
@click.option("--num-workers", type=int, default=2, help="Number of workers to spawn in pool.")
|
||||
@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs")
|
||||
@click.option("--burst", is_flag=True, default=False, help="Run Worker in Burst mode.")
|
||||
def start_worker_pool(queue, quiet=False, num_workers=2, burst=False):
|
||||
"""Start a backgrond worker"""
|
||||
from frappe.utils.background_jobs import start_worker_pool
|
||||
|
||||
start_worker_pool(
|
||||
queue=queue,
|
||||
quiet=quiet,
|
||||
burst=burst,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
|
||||
|
||||
@click.command("ready-for-migration")
|
||||
@click.option("--site", help="site name")
|
||||
@pass_context
|
||||
def ready_for_migration(context, site=None):
|
||||
from frappe.utils.doctor import get_pending_jobs
|
||||
from frappe.utils.doctor import any_job_pending
|
||||
|
||||
if not site:
|
||||
site = get_site(context)
|
||||
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
pending_jobs = get_pending_jobs(site=site)
|
||||
pending_jobs = any_job_pending(site=site)
|
||||
|
||||
if pending_jobs:
|
||||
print(f"NOT READY for migration: site {site} has pending background jobs")
|
||||
|
|
@ -251,5 +272,6 @@ commands = [
|
|||
show_pending_jobs,
|
||||
start_scheduler,
|
||||
start_worker,
|
||||
start_worker_pool,
|
||||
trigger_scheduler_event,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -700,9 +700,12 @@ def _use(site, sites_path="."):
|
|||
|
||||
|
||||
def use(site, sites_path="."):
|
||||
from frappe.installer import update_site_config
|
||||
|
||||
if os.path.exists(os.path.join(sites_path, site)):
|
||||
with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile:
|
||||
sitefile.write(site)
|
||||
sites_path = os.getcwd()
|
||||
conifg = os.path.join(sites_path, "common_site_config.json")
|
||||
update_site_config("default_site", site, validate=False, site_config_path=conifg)
|
||||
print(f"Current Site set to {site}")
|
||||
else:
|
||||
print(f"Site {site} does not exist")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -33,13 +33,28 @@ class TestDocShare(FrappeTestCase):
|
|||
|
||||
def test_doc_permission(self):
|
||||
frappe.set_user(self.user)
|
||||
|
||||
self.assertFalse(self.event.has_permission())
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
frappe.share.add("Event", self.event.name, self.user)
|
||||
|
||||
frappe.set_user(self.user)
|
||||
self.assertTrue(self.event.has_permission())
|
||||
# PERF: All share permission check should happen with maximum 1 query.
|
||||
with self.assertRowsRead(1):
|
||||
self.assertTrue(self.event.has_permission())
|
||||
|
||||
second_event = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Event",
|
||||
"subject": "test share event 2",
|
||||
"starts_on": "2015-01-01 10:00:00",
|
||||
"event_type": "Private",
|
||||
}
|
||||
).insert()
|
||||
frappe.share.add("Event", second_event.name, self.user)
|
||||
with self.assertRowsRead(1):
|
||||
self.assertTrue(self.event.has_permission())
|
||||
|
||||
def test_share_permission(self):
|
||||
frappe.share.add("Event", self.event.name, self.user, write=1, share=1)
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
{% if (Object.prototype.toString.call(x) === "[object Object]") { %}
|
||||
<table class="table">
|
||||
{% for (var key in x) { %}
|
||||
<tr>
|
||||
<td><code>{{ key }}</code></td>
|
||||
<td>{{ x[key] }}</td>
|
||||
</tr>
|
||||
{% } %}
|
||||
</table>
|
||||
{% } else { %}
|
||||
{{ x }}
|
||||
{% } %}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
<style>
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.codebox {
|
||||
font-family: monospace;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
.codebox .line.current {
|
||||
background: rgba(0,0,255, 0.1);
|
||||
}
|
||||
|
||||
.codebox .lineno {
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.codebox .code {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.object-link {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
{% function id(){ return id._old_id++; }; id._old_id = 0; %}
|
||||
<h3>{{ __("Error Report") }}</h3>
|
||||
<p class="text-muted">{{ doc.pyver }}</p>
|
||||
<dl>
|
||||
<dt>{{ __("Timestamp") }}: </dt>
|
||||
<dd>{{ doc.timestamp }}</dd>
|
||||
<dt>{{ __("Relapsed") }}</dt>
|
||||
<dd><code>{{ doc.relapses }}</code></dd>
|
||||
</dl>
|
||||
|
||||
<h3>{{ __("Exception") }}</h3>
|
||||
{{ frappe.render_template("error_object", {x: JSON.parse(doc.exception)}) }}
|
||||
|
||||
<h3>{{ __("Locals") }}</h3>
|
||||
{{ frappe.render_template("error_object", {x: JSON.parse(doc.locals)} )}}
|
||||
|
||||
<h3>{{ __("Traceback") }}</h3>
|
||||
{% var frames = JSON.parse(doc.frames); %}
|
||||
{% for (var i in frames) { %}
|
||||
{% var frameid = id(), frame = frames[i] %}
|
||||
<p><i class="octicon octicon-file-text"></i> <code>{{ frame.file }}: {{ frame.lnum }}</code>
|
||||
<div class="row">
|
||||
<div class="codebox">
|
||||
<div class="col-lg-11">
|
||||
{% for (var index in frame.lines) { %}
|
||||
{% var line = frame.lines[index] %}
|
||||
<div class="line {{ index == frame.lnum ? "current": "" }}">
|
||||
<span class="lineno text-muted">{{ index }}</span>
|
||||
<span class="code">{{ line }}</span>
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="col-lg-1">
|
||||
<span class="btn btn-xs btn-default" data-toggle="collapse" data-target="#frame-{{ frameid }}-locals">
|
||||
<i class="fa fa-list-ul"> {{ __("Locals") }}</i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 collapse" id="frame-{{ frameid }}-locals">
|
||||
<h4>{{ __("Locals") }}</h4>
|
||||
{{ frappe.render_template("error_object", {x: frame.dump }) }}
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
{% } %}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
frappe.ui.form.on("Error Snapshot", "load", function (frm) {
|
||||
frm.set_read_only(true);
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Error Snapshot", "refresh", function (frm) {
|
||||
frm.set_df_property(
|
||||
"view",
|
||||
"options",
|
||||
frappe.render_template("error_snapshot", { doc: frm.doc })
|
||||
);
|
||||
|
||||
if (frm.doc.relapses) {
|
||||
frm.add_custom_button(__("Show Relapses"), function () {
|
||||
frappe.route_options = {
|
||||
parent_error_snapshot: frm.doc.name,
|
||||
};
|
||||
frappe.set_route("List", "Error Snapshot");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2015-11-28 00:57:39.766888",
|
||||
"doctype": "DocType",
|
||||
"document_type": "System",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"view",
|
||||
"seen",
|
||||
"evalue",
|
||||
"timestamp",
|
||||
"relapses",
|
||||
"etype",
|
||||
"traceback",
|
||||
"parent_error_snapshot",
|
||||
"pyver",
|
||||
"exception",
|
||||
"locals",
|
||||
"frames"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "view",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Snapshot View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "seen",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"in_filter": 1,
|
||||
"label": "Seen"
|
||||
},
|
||||
{
|
||||
"fieldname": "evalue",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Friendly Title",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "timestamp",
|
||||
"fieldtype": "Datetime",
|
||||
"hidden": 1,
|
||||
"label": "Timestamp",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "relapses",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Relapses",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "etype",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Exception Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "traceback",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Traceback",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "parent_error_snapshot",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Parent Error Snapshot"
|
||||
},
|
||||
{
|
||||
"fieldname": "pyver",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Pyver",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "exception",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Exception"
|
||||
},
|
||||
{
|
||||
"fieldname": "locals",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Locals"
|
||||
},
|
||||
{
|
||||
"fieldname": "frames",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Frames"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-03 12:20:53.504160",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Error Snapshot",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "timestamp",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "evalue"
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
|
||||
|
||||
class ErrorSnapshot(Document):
|
||||
no_feed_on_delete = True
|
||||
|
||||
def onload(self):
|
||||
if not self.parent_error_snapshot:
|
||||
self.db_set("seen", 1, update_modified=False)
|
||||
|
||||
for relapsed in frappe.get_all("Error Snapshot", filters={"parent_error_snapshot": self.name}):
|
||||
frappe.db.set_value("Error Snapshot", relapsed.name, "seen", 1, update_modified=False)
|
||||
|
||||
frappe.local.flags.commit = True
|
||||
|
||||
def validate(self):
|
||||
parent = frappe.get_all(
|
||||
"Error Snapshot",
|
||||
filters={"evalue": self.evalue, "parent_error_snapshot": ""},
|
||||
fields=["name", "relapses", "seen"],
|
||||
limit_page_length=1,
|
||||
)
|
||||
|
||||
if parent:
|
||||
parent = parent[0]
|
||||
self.update({"parent_error_snapshot": parent["name"]})
|
||||
frappe.db.set_value("Error Snapshot", parent["name"], "relapses", parent["relapses"] + 1)
|
||||
if parent["seen"]:
|
||||
frappe.db.set_value("Error Snapshot", parent["name"], "seen", 0)
|
||||
|
||||
@staticmethod
|
||||
def clear_old_logs(days=30):
|
||||
table = frappe.qb.DocType("Error Snapshot")
|
||||
frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
frappe.listview_settings["Error Snapshot"] = {
|
||||
add_fields: ["parent_error_snapshot", "relapses", "seen"],
|
||||
filters: [
|
||||
["parent_error_snapshot", "=", null],
|
||||
["seen", "=", false],
|
||||
],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.parent_error_snapshot && doc.parent_error_snapshot.length) {
|
||||
return [__("Relapsed"), !doc.seen ? "orange" : "blue", "parent_error_snapshot,!=,"];
|
||||
} else {
|
||||
return [__("First Level"), !doc.seen ? "red" : "green", "parent_error_snapshot,=,"];
|
||||
}
|
||||
},
|
||||
onload: function (listview) {
|
||||
frappe.require("logtypes.bundle.js", () => {
|
||||
frappe.utils.logtypes.show_log_retention_message(cur_list.doctype);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils.logger import sanitized_dict
|
||||
|
||||
# test_records = frappe.get_test_records('Error Snapshot')
|
||||
|
||||
|
||||
class TestErrorSnapshot(FrappeTestCase):
|
||||
def test_form_dict_sanitization(self):
|
||||
self.assertNotEqual(sanitized_dict({"pwd": "SECRET", "usr": "WHAT"}).get("pwd"), "SECRET")
|
||||
|
|
@ -174,7 +174,7 @@
|
|||
"icon": "fa fa-file",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-13 15:50:15.508251",
|
||||
"modified": "2023-05-02 15:42:14.274901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "File",
|
||||
|
|
@ -196,14 +196,8 @@
|
|||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.database.schema import SPECIAL_CHAR_PATTERN
|
||||
from frappe.model.document import Document
|
||||
from frappe.permissions import get_doctypes_with_read
|
||||
from frappe.utils import call_hook_method, cint, get_files_path, get_hook_method, get_url
|
||||
from frappe.utils.file_manager import is_safe_path
|
||||
from frappe.utils.image import optimize_image, strip_exif_data
|
||||
|
|
@ -718,40 +719,39 @@ def on_doctype_update():
|
|||
|
||||
|
||||
def has_permission(doc, ptype=None, user=None):
|
||||
has_access = False
|
||||
user = user or frappe.session.user
|
||||
|
||||
if ptype == "create":
|
||||
has_access = frappe.has_permission("File", "create", user=user)
|
||||
return frappe.has_permission("File", "create", user=user)
|
||||
|
||||
if not doc.is_private or doc.owner in [user, "Guest"] or user == "Administrator":
|
||||
has_access = True
|
||||
if not doc.is_private or doc.owner == user or user == "Administrator":
|
||||
return True
|
||||
|
||||
if doc.attached_to_doctype and doc.attached_to_name:
|
||||
attached_to_doctype = doc.attached_to_doctype
|
||||
attached_to_name = doc.attached_to_name
|
||||
|
||||
try:
|
||||
ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name)
|
||||
ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name)
|
||||
|
||||
if ptype in ["write", "create", "delete"]:
|
||||
has_access = ref_doc.has_permission("write")
|
||||
if ptype in ["write", "create", "delete"]:
|
||||
return ref_doc.has_permission("write")
|
||||
else:
|
||||
return ref_doc.has_permission("read")
|
||||
|
||||
if ptype == "delete" and not has_access:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot delete file as it belongs to {0} {1} for which you do not have permissions"
|
||||
).format(doc.attached_to_doctype, doc.attached_to_name),
|
||||
frappe.PermissionError,
|
||||
)
|
||||
else:
|
||||
has_access = ref_doc.has_permission("read")
|
||||
except frappe.DoesNotExistError:
|
||||
# if parent doc is not created before file is created
|
||||
# we cannot check its permission so we will use file's permission
|
||||
pass
|
||||
return False
|
||||
|
||||
return has_access
|
||||
|
||||
def get_permission_query_conditions(user: str = None) -> str:
|
||||
user = user or frappe.session.user
|
||||
if user == "Administrator":
|
||||
return ""
|
||||
|
||||
readable_doctypes = ", ".join(repr(dt) for dt in get_doctypes_with_read())
|
||||
return f"""
|
||||
(`tabFile`.`is_private` = 0)
|
||||
OR (`tabFile`.`attached_to_doctype` IS NULL AND `tabFile`.`owner` = {user !r})
|
||||
OR (`tabFile`.`attached_to_doctype` IN ({readable_doctypes}))
|
||||
"""
|
||||
|
||||
|
||||
# Note: kept at the end to not cause circular, partial imports & maintain backwards compatibility
|
||||
|
|
|
|||
|
|
@ -611,46 +611,42 @@ class TestAttachmentsAccess(FrappeTestCase):
|
|||
def setUp(self) -> None:
|
||||
frappe.db.delete("File", {"is_folder": 0})
|
||||
|
||||
def test_attachments_access(self):
|
||||
def test_list_private_attachments(self):
|
||||
frappe.set_user("test4@example.com")
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": "test_user.txt",
|
||||
"attached_to_doctype": self.attached_to_doctype,
|
||||
"attached_to_name": self.attached_to_docname,
|
||||
"content": "Testing User",
|
||||
}
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_user_attachment.txt",
|
||||
attached_to_doctype=self.attached_to_doctype,
|
||||
attached_to_name=self.attached_to_docname,
|
||||
content="Testing User",
|
||||
is_private=1,
|
||||
).insert()
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": "test_user_home.txt",
|
||||
"content": "User Home",
|
||||
}
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_user_standalone.txt",
|
||||
content="User Home",
|
||||
is_private=1,
|
||||
).insert()
|
||||
|
||||
frappe.set_user("test@example.com")
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": "test_system_manager.txt",
|
||||
"attached_to_doctype": self.attached_to_doctype,
|
||||
"attached_to_name": self.attached_to_docname,
|
||||
"content": "Testing System Manager",
|
||||
}
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_sm_attachment.txt",
|
||||
attached_to_doctype=self.attached_to_doctype,
|
||||
attached_to_name=self.attached_to_docname,
|
||||
content="Testing System Manager",
|
||||
is_private=1,
|
||||
).insert()
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": "test_sm_home.txt",
|
||||
"content": "System Manager Home",
|
||||
}
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_sm_standalone.txt",
|
||||
content="System Manager Home",
|
||||
is_private=1,
|
||||
).insert()
|
||||
|
||||
system_manager_files = [file.file_name for file in get_files_in_folder("Home")["files"]]
|
||||
|
|
@ -664,15 +660,47 @@ class TestAttachmentsAccess(FrappeTestCase):
|
|||
file.file_name for file in get_files_in_folder("Home/Attachments")["files"]
|
||||
]
|
||||
|
||||
self.assertIn("test_sm_home.txt", system_manager_files)
|
||||
self.assertNotIn("test_sm_home.txt", user_files)
|
||||
self.assertIn("test_user_home.txt", system_manager_files)
|
||||
self.assertIn("test_user_home.txt", user_files)
|
||||
self.assertIn("test_sm_standalone.txt", system_manager_files)
|
||||
self.assertNotIn("test_sm_standalone.txt", user_files)
|
||||
|
||||
self.assertIn("test_system_manager.txt", system_manager_attachments_files)
|
||||
self.assertNotIn("test_system_manager.txt", user_attachments_files)
|
||||
self.assertIn("test_user.txt", system_manager_attachments_files)
|
||||
self.assertIn("test_user.txt", user_attachments_files)
|
||||
self.assertIn("test_user_standalone.txt", user_files)
|
||||
self.assertNotIn("test_user_standalone.txt", system_manager_files)
|
||||
|
||||
self.assertIn("test_sm_attachment.txt", system_manager_attachments_files)
|
||||
self.assertIn("test_sm_attachment.txt", user_attachments_files)
|
||||
self.assertIn("test_user_attachment.txt", system_manager_attachments_files)
|
||||
self.assertIn("test_user_attachment.txt", user_attachments_files)
|
||||
|
||||
def test_list_public_single_file(self):
|
||||
"""Ensure that users are able to list public standalone files."""
|
||||
frappe.set_user("test@example.com")
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_public_single.txt",
|
||||
content="Public single File",
|
||||
is_private=0,
|
||||
).insert()
|
||||
|
||||
frappe.set_user("test4@example.com")
|
||||
files = [file.file_name for file in get_files_in_folder("Home")["files"]]
|
||||
self.assertIn("test_public_single.txt", files)
|
||||
|
||||
def test_list_public_attachment(self):
|
||||
"""Ensure that users are able to list public attachments."""
|
||||
frappe.set_user("test@example.com")
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_public_attachment.txt",
|
||||
attached_to_doctype=self.attached_to_doctype,
|
||||
attached_to_name=self.attached_to_docname,
|
||||
content="Public Attachment",
|
||||
is_private=0,
|
||||
).insert()
|
||||
|
||||
frappe.set_user("test4@example.com")
|
||||
files = [file.file_name for file in get_files_in_folder("Home/Attachments")["files"]]
|
||||
self.assertIn("test_public_attachment.txt", files)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
frappe.set_user("Administrator")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ DEFAULT_LOGTYPES_RETENTION = {
|
|||
"Error Log": 30,
|
||||
"Activity Log": 90,
|
||||
"Email Queue": 30,
|
||||
"Error Snapshot": 30,
|
||||
"Scheduled Job Log": 90,
|
||||
"Route History": 90,
|
||||
"Submission Queue": 30,
|
||||
|
|
@ -45,11 +44,11 @@ def _supports_log_clearing(doctype: str) -> bool:
|
|||
|
||||
class LogSettings(Document):
|
||||
def validate(self):
|
||||
self._remove_unsupported_doctypes()
|
||||
self.remove_unsupported_doctypes()
|
||||
self._deduplicate_entries()
|
||||
self.add_default_logtypes()
|
||||
|
||||
def _remove_unsupported_doctypes(self):
|
||||
def remove_unsupported_doctypes(self):
|
||||
for entry in list(self.logs_to_clear):
|
||||
if _supports_log_clearing(entry.ref_doctype):
|
||||
continue
|
||||
|
|
@ -114,6 +113,7 @@ class LogSettings(Document):
|
|||
|
||||
def run_log_clean_up():
|
||||
doc = frappe.get_doc("Log Settings")
|
||||
doc.remove_unsupported_doctypes()
|
||||
doc.add_default_logtypes()
|
||||
doc.save()
|
||||
doc.clear_logs()
|
||||
|
|
@ -156,7 +156,6 @@ LOG_DOCTYPES = [
|
|||
"Route History",
|
||||
"Email Queue",
|
||||
"Email Queue Recipient",
|
||||
"Error Snapshot",
|
||||
"Error Log",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ class TestLogSettings(FrappeTestCase):
|
|||
"Activity Log",
|
||||
"Email Queue",
|
||||
"Route History",
|
||||
"Error Snapshot",
|
||||
"Scheduled Job Log",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "Report Name",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Queued",
|
||||
|
|
@ -35,8 +36,9 @@
|
|||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Error\nQueued\nCompleted",
|
||||
"read_only": 1
|
||||
"options": "Error\nQueued\nCompleted\nStarted",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
|
|
@ -104,7 +106,7 @@
|
|||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-19 15:41:03.428589",
|
||||
"modified": "2023-07-01 18:29:12.700239",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Prepared Report",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
|
||||
import json
|
||||
from contextlib import suppress
|
||||
from typing import Any
|
||||
|
||||
from rq import get_current_job
|
||||
|
|
@ -12,9 +13,13 @@ from frappe.desk.form.load import get_attachments
|
|||
from frappe.desk.query_report import generate_report_result
|
||||
from frappe.model.document import Document
|
||||
from frappe.monitor import add_data_to_monitor
|
||||
from frappe.utils import gzip_compress, gzip_decompress
|
||||
from frappe.utils import add_to_date, gzip_compress, gzip_decompress, now
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
# If prepared report runs for longer than this time it's automatically considered as failed
|
||||
FAILURE_THRESHOLD = 60 * 60
|
||||
REPORT_TIMEOUT = 25 * 60
|
||||
|
||||
|
||||
class PreparedReport(Document):
|
||||
@property
|
||||
|
|
@ -38,12 +43,21 @@ class PreparedReport(Document):
|
|||
def before_insert(self):
|
||||
self.status = "Queued"
|
||||
|
||||
def on_trash(self):
|
||||
# If job is running then send stop signal.
|
||||
if self.status != "Started":
|
||||
return
|
||||
|
||||
with suppress(Exception):
|
||||
job = frappe.get_doc("RQ Job", self.job_id)
|
||||
job.stop_job()
|
||||
|
||||
def after_insert(self):
|
||||
enqueue(
|
||||
generate_report,
|
||||
queue="long",
|
||||
prepared_report=self.name,
|
||||
timeout=1500,
|
||||
timeout=REPORT_TIMEOUT,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
|
|
@ -58,7 +72,7 @@ class PreparedReport(Document):
|
|||
|
||||
|
||||
def generate_report(prepared_report):
|
||||
update_job_id(prepared_report, get_current_job().id)
|
||||
update_job_id(prepared_report)
|
||||
|
||||
instance = frappe.get_doc("Prepared Report", prepared_report)
|
||||
report = frappe.get_doc("Report", instance.report_name)
|
||||
|
|
@ -95,8 +109,18 @@ def generate_report(prepared_report):
|
|||
)
|
||||
|
||||
|
||||
def update_job_id(prepared_report, job_id):
|
||||
frappe.db.set_value("Prepared Report", prepared_report, "job_id", job_id, update_modified=False)
|
||||
def update_job_id(prepared_report):
|
||||
job = get_current_job()
|
||||
|
||||
frappe.db.set_value(
|
||||
"Prepared Report",
|
||||
prepared_report,
|
||||
{
|
||||
"job_id": job and job.id,
|
||||
"status": "Started",
|
||||
},
|
||||
)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
|
|
@ -132,7 +156,7 @@ def get_reports_in_queued_state(report_name, filters):
|
|||
filters={
|
||||
"report_name": report_name,
|
||||
"filters": process_filters_for_prepared_report(filters),
|
||||
"status": "Queued",
|
||||
"status": ("in", ("Queued", "Started")),
|
||||
"owner": frappe.session.user,
|
||||
},
|
||||
)
|
||||
|
|
@ -151,6 +175,21 @@ def get_completed_prepared_report(filters, user, report_name):
|
|||
)
|
||||
|
||||
|
||||
def expire_stalled_report():
|
||||
frappe.db.set_value(
|
||||
"Prepared Report",
|
||||
{
|
||||
"status": "Started",
|
||||
"modified": ("<", add_to_date(now(), seconds=-FAILURE_THRESHOLD, as_datetime=True)),
|
||||
},
|
||||
{
|
||||
"status": "Failed",
|
||||
"error_message": frappe._("Report timed out."),
|
||||
},
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_prepared_reports(reports):
|
||||
reports = frappe.parse_json(reports)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@
|
|||
# License: MIT. See LICENSE
|
||||
import json
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
|
||||
import frappe
|
||||
from frappe.desk.query_report import generate_report_result, get_report_doc
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.query_builder.utils import db_type_is
|
||||
from frappe.tests.test_query_builder import run_only_if
|
||||
from frappe.tests.utils import FrappeTestCase, timeout
|
||||
|
||||
|
||||
class TestPreparedReport(FrappeTestCase):
|
||||
|
|
@ -16,11 +19,21 @@ class TestPreparedReport(FrappeTestCase):
|
|||
|
||||
frappe.db.commit()
|
||||
|
||||
def create_prepared_report(self, commit=False):
|
||||
@timeout(seconds=20)
|
||||
def wait_for_status(self, report, status):
|
||||
frappe.db.commit() # Flush changes first
|
||||
while True:
|
||||
frappe.db.rollback() # read new data
|
||||
report.reload()
|
||||
if report.status == status:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
def create_prepared_report(self, report=None, commit=True):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Prepared Report",
|
||||
"report_name": "Database Storage Usage By Tables",
|
||||
"report_name": report or "Database Storage Usage By Tables",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
|
@ -30,24 +43,50 @@ class TestPreparedReport(FrappeTestCase):
|
|||
return doc
|
||||
|
||||
def test_queueing(self):
|
||||
doc_ = self.create_prepared_report()
|
||||
self.assertEqual("Queued", doc_.status)
|
||||
self.assertTrue(doc_.queued_at)
|
||||
doc = self.create_prepared_report()
|
||||
self.assertEqual("Queued", doc.status)
|
||||
self.assertTrue(doc.queued_at)
|
||||
|
||||
frappe.db.commit()
|
||||
time.sleep(5)
|
||||
self.wait_for_status(doc, "Completed")
|
||||
|
||||
doc_ = frappe.get_last_doc("Prepared Report")
|
||||
self.assertEqual("Completed", doc_.status)
|
||||
self.assertTrue(doc_.job_id)
|
||||
self.assertTrue(doc_.report_end_time)
|
||||
doc = frappe.get_last_doc("Prepared Report")
|
||||
self.assertTrue(doc.job_id)
|
||||
self.assertTrue(doc.report_end_time)
|
||||
|
||||
def test_prepared_data(self):
|
||||
doc_ = self.create_prepared_report(commit=True)
|
||||
time.sleep(5)
|
||||
doc = self.create_prepared_report()
|
||||
self.wait_for_status(doc, "Completed")
|
||||
|
||||
prepared_data = json.loads(doc_.get_prepared_data().decode("utf-8"))
|
||||
prepared_data = json.loads(doc.get_prepared_data().decode("utf-8"))
|
||||
generated_data = generate_report_result(get_report_doc("Database Storage Usage By Tables"))
|
||||
self.assertEqual(len(prepared_data["columns"]), len(generated_data["columns"]))
|
||||
self.assertEqual(len(prepared_data["result"]), len(generated_data["result"]))
|
||||
self.assertEqual(len(prepared_data), len(generated_data))
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_start_status_and_kill_jobs(self):
|
||||
with test_report(report_type="Query Report", query="select sleep(10)") as report:
|
||||
doc = self.create_prepared_report(report.name)
|
||||
self.wait_for_status(doc, "Started")
|
||||
job_id = doc.job_id
|
||||
|
||||
doc.delete()
|
||||
time.sleep(1)
|
||||
job = frappe.get_doc("RQ Job", job_id)
|
||||
self.assertEqual(job.status, "stopped")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def test_report(**args):
|
||||
try:
|
||||
report = frappe.new_doc("Report")
|
||||
report.update(args)
|
||||
if not report.report_name:
|
||||
report.report_name = frappe.generate_hash()
|
||||
if not report.ref_doctype:
|
||||
report.ref_doctype = "ToDo"
|
||||
report.insert()
|
||||
frappe.db.commit()
|
||||
yield report
|
||||
finally:
|
||||
report.delete()
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ class Report(Document):
|
|||
def on_update(self):
|
||||
self.export_doc()
|
||||
|
||||
def before_export(self):
|
||||
self.letterhead = None
|
||||
self.prepared_report = 0
|
||||
|
||||
def on_trash(self):
|
||||
if (
|
||||
self.is_standard == "Yes"
|
||||
|
|
@ -121,7 +125,7 @@ class Report(Document):
|
|||
|
||||
def execute_script_report(self, filters):
|
||||
# save the timestamp to automatically set to prepared
|
||||
threshold = 30
|
||||
threshold = 15
|
||||
res = []
|
||||
|
||||
start_time = datetime.datetime.now()
|
||||
|
|
@ -135,7 +139,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 +386,7 @@ def get_group_by_column_label(args, meta):
|
|||
function=sql_fn_map[args.aggregate_function], fieldlabel=aggregate_on_label
|
||||
)
|
||||
return label
|
||||
|
||||
|
||||
def enable_prepared_report(report: str):
|
||||
frappe.db.set_value("Report", report, "prepared_report", 1)
|
||||
|
|
|
|||
|
|
@ -63,23 +63,15 @@ class RQJob(Document):
|
|||
|
||||
order_desc = "desc" in args.get("order_by", "")
|
||||
|
||||
matched_job_ids = RQJob.get_matching_job_ids(args)
|
||||
matched_job_ids = RQJob.get_matching_job_ids(args)[start : start + page_length]
|
||||
|
||||
jobs = []
|
||||
for job_ids in create_batch(matched_job_ids, 100):
|
||||
jobs.extend(
|
||||
serialize_job(job)
|
||||
for job in Job.fetch_many(job_ids=job_ids, connection=get_redis_conn())
|
||||
if job and for_current_site(job)
|
||||
)
|
||||
if len(jobs) > start + page_length:
|
||||
# we have fetched enough. This is inefficient but because of site filtering TINA
|
||||
break
|
||||
conn = get_redis_conn()
|
||||
jobs = [serialize_job(job) for job in Job.fetch_many(job_ids=matched_job_ids, connection=conn)]
|
||||
|
||||
return sorted(jobs, key=lambda j: j.modified, reverse=order_desc)[start : start + page_length]
|
||||
return sorted(jobs, key=lambda j: j.modified, reverse=order_desc)
|
||||
|
||||
@staticmethod
|
||||
def get_matching_job_ids(args):
|
||||
def get_matching_job_ids(args) -> list[str]:
|
||||
filters = make_filter_dict(args.get("filters"))
|
||||
|
||||
queues = _eval_filters(filters.get("queue"), QUEUES)
|
||||
|
|
@ -92,7 +84,7 @@ class RQJob(Document):
|
|||
for status in statuses:
|
||||
matched_job_ids.extend(fetch_job_ids(queue, status))
|
||||
|
||||
return matched_job_ids
|
||||
return filter_current_site_jobs(matched_job_ids)
|
||||
|
||||
@check_permissions
|
||||
def delete(self):
|
||||
|
|
@ -107,8 +99,7 @@ class RQJob(Document):
|
|||
|
||||
@staticmethod
|
||||
def get_count(args) -> int:
|
||||
# Can not be implemented efficiently due to site filtering hence ignored.
|
||||
return 0
|
||||
return len(RQJob.get_matching_job_ids(args))
|
||||
|
||||
# None of these methods apply to virtual job doctype, overriden for sanity.
|
||||
@staticmethod
|
||||
|
|
@ -155,6 +146,12 @@ def for_current_site(job: Job) -> bool:
|
|||
return job.kwargs.get("site") == frappe.local.site
|
||||
|
||||
|
||||
def filter_current_site_jobs(job_ids: list[str]) -> list[str]:
|
||||
site = frappe.local.site
|
||||
|
||||
return [j for j in job_ids if j.startswith(site)]
|
||||
|
||||
|
||||
def _eval_filters(filter, values: list[str]) -> list[str]:
|
||||
if filter:
|
||||
operator, operand = filter
|
||||
|
|
@ -186,10 +183,13 @@ def remove_failed_jobs():
|
|||
frappe.only_for("System Manager")
|
||||
for queue in get_queues():
|
||||
fail_registry = queue.failed_job_registry
|
||||
for job_ids in create_batch(fail_registry.get_job_ids(), 100):
|
||||
for job in Job.fetch_many(job_ids=job_ids, connection=get_redis_conn()):
|
||||
if job and for_current_site(job):
|
||||
fail_registry.remove(job, delete_job=True)
|
||||
failed_jobs = filter_current_site_jobs(fail_registry.get_job_ids())
|
||||
|
||||
# Delete in batches to avoid loading too many things in memory
|
||||
conn = get_redis_conn()
|
||||
for job_ids in create_batch(failed_jobs, 100):
|
||||
for job in Job.fetch_many(job_ids=job_ids, connection=conn):
|
||||
job and fail_registry.remove(job, delete_job=True)
|
||||
|
||||
|
||||
def get_all_queued_jobs():
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ frappe.listview_settings["RQ Job"] = {
|
|||
);
|
||||
|
||||
if (listview.list_view_settings) {
|
||||
listview.list_view_settings.disable_count = 1;
|
||||
listview.list_view_settings.disable_sidebar_stats = 1;
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +56,6 @@ frappe.listview_settings["RQ Job"] = {
|
|||
}
|
||||
|
||||
listview.refresh();
|
||||
}, 5000);
|
||||
}, 15000);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -97,13 +97,38 @@ class TestRQJob(FrappeTestCase):
|
|||
self.assertIn("quitting", cstr(stderr))
|
||||
|
||||
@timeout(20)
|
||||
def test_job_id_dedup(self):
|
||||
def test_multi_queue_burst_consumption_worker_pool(self):
|
||||
for _ in range(3):
|
||||
for q in ["default", "short"]:
|
||||
frappe.enqueue(self.BG_JOB, sleep=1, queue=q)
|
||||
|
||||
_, stderr = execute_in_shell(
|
||||
"bench worker-pool --queue short,default --burst --num-workers=4", check_exit_code=True
|
||||
)
|
||||
self.assertIn("quitting", cstr(stderr))
|
||||
|
||||
@timeout(20)
|
||||
def test_job_id_manual_dedup(self):
|
||||
job_id = "test_dedup"
|
||||
job = frappe.enqueue(self.BG_JOB, sleep=5, job_id=job_id)
|
||||
self.assertTrue(is_job_enqueued(job_id))
|
||||
self.check_status(job, "finished")
|
||||
self.assertFalse(is_job_enqueued(job_id))
|
||||
|
||||
@timeout(20)
|
||||
def test_auto_job_dedup(self):
|
||||
job_id = "test_dedup"
|
||||
job1 = frappe.enqueue(self.BG_JOB, sleep=2, job_id=job_id, deduplicate=True)
|
||||
job2 = frappe.enqueue(self.BG_JOB, sleep=5, job_id=job_id, deduplicate=True)
|
||||
self.assertIsNone(job2)
|
||||
self.check_status(job1, "finished") # wait
|
||||
|
||||
# Failed jobs last longer, subsequent job should still pass with same ID.
|
||||
job3 = frappe.enqueue(self.BG_JOB, fail=True, job_id=job_id, deduplicate=True)
|
||||
self.check_status(job3, "failed")
|
||||
job4 = frappe.enqueue(self.BG_JOB, sleep=1, job_id=job_id, deduplicate=True)
|
||||
self.check_status(job4, "finished")
|
||||
|
||||
@timeout(20)
|
||||
def test_enqueue_after_commit(self):
|
||||
job_id = frappe.generate_hash()
|
||||
|
|
|
|||
38
frappe/core/doctype/server_script/server_script_list.js
Normal file
38
frappe/core/doctype/server_script/server_script_list.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
frappe.listview_settings["Server Script"] = {
|
||||
onload: function (listview) {
|
||||
add_github_star_cta(listview);
|
||||
},
|
||||
};
|
||||
|
||||
function add_github_star_cta(listview) {
|
||||
try {
|
||||
const key = "show_github_star_banner";
|
||||
if (localStorage.getItem(key) == "false") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listview.github_star_banner) {
|
||||
listview.github_star_banner.remove();
|
||||
}
|
||||
|
||||
const message = "Loving Frappe Framework?";
|
||||
const link = "https://github.com/frappe/frappe";
|
||||
const cta = "Star us on GitHub";
|
||||
|
||||
listview.github_star_banner = $(`
|
||||
<div style="position: relative;">
|
||||
<div class="pr-3">
|
||||
${message} <br><a href="${link}" target="_blank" style="color: var(--primary-color)">${cta} → </a>
|
||||
</div>
|
||||
<div style="position: absolute; top: -1px; right: -4px; cursor: pointer;" title="Dismiss"
|
||||
onclick="localStorage.setItem('${key}', 'false') || this.parentElement.remove()">
|
||||
<svg class="icon icon-sm" style="">
|
||||
<use class="" href="#icon-close"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`).appendTo(listview.page.sidebar);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ def get_notification_config():
|
|||
"Communication": {"status": "Open", "communication_type": "Communication"},
|
||||
"ToDo": "frappe.core.notifications.get_things_todo",
|
||||
"Event": "frappe.core.notifications.get_todays_events",
|
||||
"Error Snapshot": {"seen": 0, "parent_error_snapshot": None},
|
||||
"Workflow Action": {"status": "Open"},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letter_head": "abc",
|
||||
"modified": "2022-10-19 02:59:00.365307",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
|
|
@ -25,4 +24,4 @@
|
|||
"role": "System Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -97,10 +97,13 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
|
|||
|
||||
handle_enter_press(e) {
|
||||
if (e.which === frappe.ui.keyCode.ENTER) {
|
||||
var $target = $(e.target);
|
||||
if ($target.hasClass("prev-btn")) {
|
||||
let $target = $(e.target);
|
||||
if ($target.hasClass("prev-btn") || $target.hasClass("next-btn")) {
|
||||
$target.trigger("click");
|
||||
} else {
|
||||
// hitting enter on autocomplete field shouldn't trigger next slide.
|
||||
if ($target.data().fieldtype == "Autocomplete") return;
|
||||
|
||||
this.container.find(".next-btn").trigger("click");
|
||||
e.preventDefault();
|
||||
}
|
||||
|
|
@ -545,15 +548,19 @@ frappe.setup.utils = {
|
|||
|
||||
slide.get_input("timezone").empty().add_options(data.all_timezones);
|
||||
|
||||
// set values if present
|
||||
if (frappe.wizard.values.country) {
|
||||
country_field.set_input(frappe.wizard.values.country);
|
||||
} else if (data.default_country) {
|
||||
country_field.set_input(data.default_country);
|
||||
}
|
||||
|
||||
slide.get_field("currency").set_input(frappe.wizard.values.currency);
|
||||
slide.get_field("timezone").set_input(frappe.wizard.values.timezone);
|
||||
|
||||
// set values if present
|
||||
let country =
|
||||
frappe.wizard.values.country ||
|
||||
data.default_country ||
|
||||
guess_country(frappe.setup.data.regional_data.country_info);
|
||||
|
||||
if (country) {
|
||||
country_field.set_input(country);
|
||||
$(country_field.input).change();
|
||||
}
|
||||
},
|
||||
|
||||
bind_language_events: function (slide) {
|
||||
|
|
@ -630,3 +637,16 @@ frappe.setup.utils = {
|
|||
});
|
||||
},
|
||||
};
|
||||
|
||||
function guess_country(country_info) {
|
||||
try {
|
||||
const system_timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
for ([country, info] of Object.entries(country_info)) {
|
||||
let possible_timezones = (info.timezones || []).filter((t) => t == system_timezone);
|
||||
if (possible_timezones.length) return country;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Could not guess country", e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,6 @@ def return_unsubscribed_page(email, doctype, name):
|
|||
def flush(from_test=False):
|
||||
"""flush email queue, every time: called from scheduler"""
|
||||
from frappe.email.doctype.email_queue.email_queue import send_mail
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
|
||||
# To avoid running jobs inside unit tests
|
||||
if frappe.are_emails_muted():
|
||||
|
|
@ -142,24 +141,16 @@ def flush(from_test=False):
|
|||
if cint(frappe.db.get_default("suspend_email_queue")) == 1:
|
||||
return
|
||||
|
||||
try:
|
||||
queued_jobs = set(get_jobs(site=frappe.local.site, key="job_name")[frappe.local.site])
|
||||
except Exception:
|
||||
queued_jobs = set()
|
||||
|
||||
for row in get_queue():
|
||||
try:
|
||||
job_name = f"email_queue_sendmail_{row.name}"
|
||||
if job_name not in queued_jobs:
|
||||
frappe.enqueue(
|
||||
method=send_mail,
|
||||
email_queue_name=row.name,
|
||||
now=from_test,
|
||||
job_name=job_name,
|
||||
queue="short",
|
||||
)
|
||||
else:
|
||||
frappe.logger().debug(f"Not queueing job {job_name} because it is in queue already")
|
||||
frappe.enqueue(
|
||||
method=send_mail,
|
||||
email_queue_name=row.name,
|
||||
now=from_test,
|
||||
job_id=f"email_queue_sendmail_{row.name}",
|
||||
queue="short",
|
||||
deduplicate=True,
|
||||
)
|
||||
except Exception:
|
||||
frappe.get_doc("Email Queue", row.name).log_error()
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ permission_query_conditions = {
|
|||
"Communication": "frappe.core.doctype.communication.communication.get_permission_query_conditions_for_communication",
|
||||
"Workflow Action": "frappe.workflow.doctype.workflow_action.workflow_action.get_permission_query_conditions",
|
||||
"Prepared Report": "frappe.core.doctype.prepared_report.prepared_report.get_permission_query_condition",
|
||||
"File": "frappe.core.doctype.file.file.get_permission_query_conditions",
|
||||
}
|
||||
|
||||
has_permission = {
|
||||
|
|
@ -195,9 +196,9 @@ scheduler_events = {
|
|||
"frappe.email.doctype.email_account.email_account.pull",
|
||||
],
|
||||
# Hourly but offset by 30 minutes
|
||||
# "30 * * * *": [
|
||||
#
|
||||
# ],
|
||||
"30 * * * *": [
|
||||
"frappe.core.doctype.prepared_report.prepared_report.expire_stalled_report",
|
||||
],
|
||||
# Daily but offset by 45 minutes
|
||||
"45 0 * * *": [
|
||||
"frappe.core.doctype.log_settings.log_settings.run_log_clean_up",
|
||||
|
|
@ -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",
|
||||
]:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class Webhook(Document):
|
|||
self.validate_request_url()
|
||||
self.validate_request_body()
|
||||
self.validate_repeating_fields()
|
||||
self.validate_secret()
|
||||
self.preview_document = None
|
||||
|
||||
def on_update(self):
|
||||
|
|
@ -74,6 +75,13 @@ class Webhook(Document):
|
|||
if len(webhook_data) != len(set(webhook_data)):
|
||||
frappe.throw(_("Same Field is entered more than once"))
|
||||
|
||||
def validate_secret(self):
|
||||
if self.enable_security:
|
||||
try:
|
||||
self.get_password("webhook_secret", False).encode("utf8")
|
||||
except Exception:
|
||||
frappe.throw(_("Invalid Webhook Secret"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_preview(self):
|
||||
# This function doesn't need to do anything specific as virtual fields
|
||||
|
|
@ -112,16 +120,21 @@ def get_context(doc):
|
|||
|
||||
|
||||
def enqueue_webhook(doc, webhook) -> None:
|
||||
webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name"))
|
||||
headers = get_webhook_headers(doc, webhook)
|
||||
data = get_webhook_data(doc, webhook)
|
||||
try:
|
||||
webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name"))
|
||||
headers = get_webhook_headers(doc, webhook)
|
||||
data = get_webhook_data(doc, webhook)
|
||||
|
||||
if webhook.is_dynamic_url:
|
||||
request_url = frappe.render_template(webhook.request_url, get_context(doc))
|
||||
else:
|
||||
request_url = webhook.request_url
|
||||
if webhook.is_dynamic_url:
|
||||
request_url = frappe.render_template(webhook.request_url, get_context(doc))
|
||||
else:
|
||||
request_url = webhook.request_url
|
||||
|
||||
except Exception as e:
|
||||
frappe.logger().debug({"enqueue_webhook_error": e})
|
||||
log_request(webhook.name, doc.name, request_url, headers, data)
|
||||
return
|
||||
|
||||
r = None
|
||||
for i in range(3):
|
||||
try:
|
||||
r = requests.request(
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ execute:frappe.reload_doc('core', 'doctype', 'user') #2017-10-27
|
|||
execute:frappe.reload_doc('core', 'doctype', 'report_column')
|
||||
execute:frappe.reload_doc('core', 'doctype', 'report_filter')
|
||||
execute:frappe.reload_doc('core', 'doctype', 'report') #2020-08-25
|
||||
execute:frappe.reload_doc('core', 'doctype', 'error_snapshot')
|
||||
execute:frappe.get_doc("User", "Guest").save()
|
||||
execute:frappe.delete_doc("DocType", "Control Panel", force=1)
|
||||
execute:frappe.delete_doc("DocType", "Tag")
|
||||
|
|
@ -42,7 +41,6 @@ execute:frappe.db.sql("delete from `tabProperty Setter` where `property` = 'idx'
|
|||
execute:frappe.db.sql("delete from tabSessions where user is null")
|
||||
execute:frappe.delete_doc("DocType", "Backup Manager")
|
||||
execute:frappe.permissions.reset_perms("Web Page")
|
||||
execute:frappe.permissions.reset_perms("Error Snapshot")
|
||||
execute:frappe.db.sql("delete from `tabWeb Page` where ifnull(template_path, '')!=''")
|
||||
execute:frappe.core.doctype.language.language.update_language_names() # 2017-04-12
|
||||
execute:frappe.db.set_value("Print Settings", "Print Settings", "add_draft_heading", 1)
|
||||
|
|
@ -227,3 +225,4 @@ frappe.patches.v15_0.remove_background_jobs_from_dropdown
|
|||
frappe.desk.doctype.form_tour.patches.introduce_ui_tours
|
||||
execute:frappe.delete_doc_if_exists("Workspace", "Customization")
|
||||
execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter")
|
||||
execute:frappe.delete_doc_if_exists("DocType", "Error Snapshot")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ def execute():
|
|||
"Email Queue": get_current_setting("clear_email_queue_after") or 30,
|
||||
# child table on email queue
|
||||
"Email Queue Recipient": get_current_setting("clear_email_queue_after") or 30,
|
||||
"Error Snapshot": get_current_setting("clear_error_log_after") or 90,
|
||||
# newly added
|
||||
"Scheduled Job Log": 90,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,17 +118,24 @@ def has_permission(
|
|||
|
||||
def false_if_not_shared():
|
||||
if ptype in ("read", "write", "share", "submit", "email", "print"):
|
||||
shared = frappe.share.get_shared(
|
||||
doctype, user, ["read" if ptype in ("email", "print") else ptype]
|
||||
)
|
||||
|
||||
rights = ["read" if ptype in ("email", "print") else ptype]
|
||||
|
||||
if doc:
|
||||
doc_name = get_doc_name(doc)
|
||||
if doc_name in shared:
|
||||
shared = frappe.share.get_shared(
|
||||
doctype,
|
||||
user,
|
||||
rights=rights,
|
||||
filters=[["share_name", "=", doc_name]],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if shared:
|
||||
if ptype in ("read", "write", "share", "submit") or meta.permissions[0].get(ptype):
|
||||
return True
|
||||
|
||||
elif shared:
|
||||
elif frappe.share.get_shared(doctype, user, rights=rights, limit=1):
|
||||
# if atleast one shared doc of that type, then return True
|
||||
# this is used in db_query to check if permission on DocType
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ function hide() {
|
|||
data.value = null;
|
||||
}
|
||||
function open_in_editor(location) {
|
||||
frappe.socketio.socket.emit("open_in_editor", location);
|
||||
frappe.realtime.emit("open_in_editor", location);
|
||||
}
|
||||
function error_component(error, i) {
|
||||
let location = data.value.error.errors[i].location;
|
||||
|
|
@ -40,7 +40,7 @@ function error_component(error, i) {
|
|||
template: `<div>${template}</div>`,
|
||||
methods: {
|
||||
open() {
|
||||
frappe.socketio.socket.emit("open_in_editor", location);
|
||||
frappe.realtime.emit("open_in_editor", location);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ frappe.Application = class Application {
|
|||
}
|
||||
|
||||
startup() {
|
||||
frappe.socketio.init();
|
||||
frappe.realtime.init();
|
||||
frappe.model.init();
|
||||
|
||||
this.load_bootinfo();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -174,6 +174,15 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
|
|||
if (typeof options[0] === "string") {
|
||||
options = options.map((o) => ({ label: o, value: o }));
|
||||
}
|
||||
|
||||
options = options.map((o) => {
|
||||
if (typeof o !== "string") {
|
||||
o.label = o.label.toString();
|
||||
o.value = o.value.toString();
|
||||
}
|
||||
return o;
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
page: this.page,
|
||||
});
|
||||
|
||||
this.viewers = new frappe.ui.form.FormViewers({
|
||||
frm: this,
|
||||
parent: $('<div class="form-viewers d-flex"></div>').prependTo(this.page.page_actions),
|
||||
});
|
||||
|
||||
// navigate records keyboard shortcuts
|
||||
this.add_form_keyboard_shortcuts();
|
||||
|
||||
|
|
@ -709,6 +714,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
this.toolbar.refresh();
|
||||
}
|
||||
this.viewers.refresh();
|
||||
|
||||
this.dashboard.refresh();
|
||||
frappe.breadcrumbs.update();
|
||||
|
|
@ -1944,7 +1950,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
let docname = this.docname;
|
||||
|
||||
if (this.doc && !this.is_new()) {
|
||||
frappe.socketio.doc_subscribe(doctype, docname);
|
||||
frappe.realtime.doc_subscribe(doctype, docname);
|
||||
}
|
||||
frappe.realtime.off("docinfo_update");
|
||||
frappe.realtime.on("docinfo_update", ({ doc, key, action = "update" }) => {
|
||||
|
|
|
|||
|
|
@ -3,69 +3,61 @@ frappe.ui.form.FormViewers = class FormViewers {
|
|||
this.frm = frm;
|
||||
this.parent = parent;
|
||||
this.parent.tooltip({ title: __("Currently Viewing") });
|
||||
|
||||
this.past_users = []; // Remember last users to compute changes
|
||||
this.active_users = []; // current users viewing this form
|
||||
this.setup_events();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
let users = this.frm.get_docinfo()["viewers"];
|
||||
if (!users || !users.current || !users.current.length) {
|
||||
if (!this.active_users.length) {
|
||||
this.parent.empty();
|
||||
return;
|
||||
}
|
||||
|
||||
let currently_viewing = users.current.filter((user) => user != frappe.session.user);
|
||||
let avatar_group = frappe.avatar_group(currently_viewing, 5, {
|
||||
let avatar_group = frappe.avatar_group(this.active_users, 5, {
|
||||
align: "left",
|
||||
overlap: true,
|
||||
});
|
||||
this.parent.empty().append(avatar_group);
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.FormViewers.set_users = function (data, type) {
|
||||
const doctype = data.doctype;
|
||||
const docname = data.docname;
|
||||
const docinfo = frappe.model.get_docinfo(doctype, docname);
|
||||
setup_events() {
|
||||
let me = this;
|
||||
frappe.realtime.off("doc_viewers");
|
||||
frappe.realtime.on("doc_viewers", function (data) {
|
||||
me.update_users(data);
|
||||
});
|
||||
}
|
||||
|
||||
const past_users = ((docinfo && docinfo[type]) || {}).past || [];
|
||||
const users = data.users || [];
|
||||
const new_users = users.filter((user) => !past_users.includes(user));
|
||||
async update_users({ doctype, docname, users = [] }) {
|
||||
users = users.filter((u) => u != frappe.session.user);
|
||||
|
||||
if (new_users.length === 0) return;
|
||||
const added_users = users.filter((user) => !this.past_users.includes(user));
|
||||
const removed_users = this.past_users.filter((user) => !users.includes(user));
|
||||
const changed_users = [...added_users, ...removed_users];
|
||||
|
||||
const set_and_refresh = () => {
|
||||
const info = {
|
||||
past: past_users.concat(new_users),
|
||||
new: new_users,
|
||||
current: users,
|
||||
};
|
||||
if (changed_users.length === 0) return;
|
||||
|
||||
frappe.model.set_docinfo(doctype, docname, type, info);
|
||||
await this.fetch_user_info(users);
|
||||
|
||||
if (
|
||||
cur_frm &&
|
||||
cur_frm.doc &&
|
||||
cur_frm.doc.doctype === doctype &&
|
||||
cur_frm.doc.name == docname &&
|
||||
cur_frm.viewers
|
||||
) {
|
||||
cur_frm.viewers.refresh(true, type);
|
||||
this.active_users = users;
|
||||
this.past_users = users;
|
||||
|
||||
if (this.frm?.doc?.doctype === doctype && this.frm?.doc?.name == docname) {
|
||||
this.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
let unknown_users = [];
|
||||
for (let user of users) {
|
||||
if (!frappe.boot.user_info[user]) unknown_users.push(user);
|
||||
}
|
||||
|
||||
if (unknown_users.length === 0) {
|
||||
set_and_refresh();
|
||||
} else {
|
||||
// load additional user info
|
||||
frappe
|
||||
.xcall("frappe.desk.form.load.get_user_info_for_viewers", { users: unknown_users })
|
||||
.then((data) => {
|
||||
Object.assign(frappe.boot.user_info, data);
|
||||
set_and_refresh();
|
||||
});
|
||||
async fetch_user_info(users) {
|
||||
let unknown_users = [];
|
||||
for (let user of users) {
|
||||
if (!frappe.boot.user_info[user]) unknown_users.push(user);
|
||||
}
|
||||
if (!unknown_users.length) return;
|
||||
|
||||
const data = await frappe.xcall("frappe.desk.form.load.get_user_info_for_viewers", {
|
||||
users: unknown_users,
|
||||
});
|
||||
Object.assign(frappe.boot.user_info, data);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
refresh() {
|
||||
this.make_menu();
|
||||
this.make_viewers();
|
||||
this.set_title();
|
||||
this.page.clear_user_actions();
|
||||
this.show_title_as_dirty();
|
||||
|
|
@ -109,7 +108,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
|
||||
let rename_document = () => {
|
||||
if (input_name != docname) frappe.socketio.doctype_subscribe(doctype, input_name);
|
||||
if (input_name != docname) frappe.realtime.doctype_subscribe(doctype, input_name);
|
||||
return frappe
|
||||
.xcall("frappe.model.rename_doc.update_document_title", {
|
||||
doctype,
|
||||
|
|
@ -272,18 +271,6 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
}
|
||||
|
||||
make_viewers() {
|
||||
if (this.frm.viewers) {
|
||||
return;
|
||||
}
|
||||
this.frm.viewers = new frappe.ui.form.FormViewers({
|
||||
frm: this.frm,
|
||||
parent: $('<div class="form-viewers d-flex"></div>').prependTo(
|
||||
this.frm.page.page_actions
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
make_navigation() {
|
||||
// Navigate
|
||||
if (!this.frm.is_new() && !this.frm.meta.issingle) {
|
||||
|
|
|
|||
|
|
@ -711,7 +711,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
get_column_html(col, doc) {
|
||||
if (col.type === "Status") {
|
||||
if (col.type === "Status" || col.df?.options == "Workflow State") {
|
||||
return `
|
||||
<div class="list-row-col hidden-xs ellipsis">
|
||||
${this.get_indicator_html(doc)}
|
||||
|
|
@ -1362,7 +1362,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
if (this.list_view_settings?.disable_auto_refresh || this.realtime_events_setup) {
|
||||
return;
|
||||
}
|
||||
frappe.socketio.doctype_subscribe(this.doctype);
|
||||
frappe.realtime.doctype_subscribe(this.doctype);
|
||||
frappe.realtime.off("list_update");
|
||||
frappe.realtime.on("list_update", (data) => {
|
||||
if (data?.doctype !== this.doctype) {
|
||||
|
|
@ -1385,7 +1385,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
disable_realtime_updates() {
|
||||
frappe.socketio.doctype_unsubscribe(this.doctype);
|
||||
frappe.realtime.doctype_unsubscribe(this.doctype);
|
||||
this.realtime_events_setup = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ frappe.call = function (opts) {
|
|||
var callback = function (data, response_text) {
|
||||
if (data.task_id) {
|
||||
// async call, subscribe
|
||||
frappe.socketio.subscribe(data.task_id, opts);
|
||||
frappe.realtime.subscribe(data.task_id, opts);
|
||||
|
||||
if (opts.queued) {
|
||||
opts.queued(data);
|
||||
|
|
|
|||
|
|
@ -1,42 +1,75 @@
|
|||
import { io } from "socket.io-client";
|
||||
frappe.socketio = {
|
||||
open_tasks: {},
|
||||
open_docs: [],
|
||||
emit_queue: [],
|
||||
|
||||
init: function (port = 3000) {
|
||||
frappe.provide("frappe.realtime");
|
||||
|
||||
class RealTimeClient {
|
||||
constructor() {
|
||||
this.open_tasks = {};
|
||||
this.open_docs = new Set();
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (this.socket) {
|
||||
this.connect();
|
||||
this.socket.on(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (this.socket) {
|
||||
this.socket.off(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.lazy_connect) {
|
||||
this.socket.connect();
|
||||
this.lazy_connect = false;
|
||||
}
|
||||
}
|
||||
|
||||
emit(event, ...args) {
|
||||
this.connect();
|
||||
this.socket.emit(event, ...args);
|
||||
}
|
||||
|
||||
init(port = 9000, lazy_connect = false) {
|
||||
if (frappe.boot.disable_async) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frappe.socketio.socket) {
|
||||
if (this.socket) {
|
||||
return;
|
||||
}
|
||||
this.lazy_connect = lazy_connect;
|
||||
let me = this;
|
||||
|
||||
// Enable secure option when using HTTPS
|
||||
if (window.location.protocol == "https:") {
|
||||
frappe.socketio.socket = io.connect(frappe.socketio.get_host(port), {
|
||||
this.socket = io(this.get_host(port), {
|
||||
secure: true,
|
||||
withCredentials: true,
|
||||
reconnectionAttempts: 3,
|
||||
autoConnect: !lazy_connect,
|
||||
});
|
||||
} else if (window.location.protocol == "http:") {
|
||||
frappe.socketio.socket = io.connect(frappe.socketio.get_host(port), {
|
||||
this.socket = io(this.get_host(port), {
|
||||
withCredentials: true,
|
||||
reconnectionAttempts: 3,
|
||||
autoConnect: !lazy_connect,
|
||||
});
|
||||
}
|
||||
|
||||
if (!frappe.socketio.socket) {
|
||||
console.log("Unable to connect to " + frappe.socketio.get_host(port));
|
||||
if (!this.socket) {
|
||||
console.log("Unable to connect to " + this.get_host(port));
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.socketio.socket.on("msgprint", function (message) {
|
||||
this.socket.on("msgprint", function (message) {
|
||||
frappe.msgprint(message);
|
||||
});
|
||||
|
||||
frappe.socketio.socket.on("progress", function (data) {
|
||||
this.socket.on("progress", function (data) {
|
||||
if (data.progress) {
|
||||
data.percent = (flt(data.progress[0]) / data.progress[1]) * 100;
|
||||
}
|
||||
|
|
@ -51,31 +84,20 @@ frappe.socketio = {
|
|||
}
|
||||
});
|
||||
|
||||
frappe.socketio.setup_listeners();
|
||||
frappe.socketio.setup_reconnect();
|
||||
this.setup_listeners();
|
||||
|
||||
$(document).on("form-load form-rename", function (e, frm) {
|
||||
if (!frm.doc || frm.is_new()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0, l = frappe.socketio.open_docs.length; i < l; i++) {
|
||||
var d = frappe.socketio.open_docs[i];
|
||||
if (frm.doctype == d.doctype && frm.docname == d.name) {
|
||||
// already subscribed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
frappe.socketio.doc_subscribe(frm.doctype, frm.docname);
|
||||
me.doc_subscribe(frm.doctype, frm.docname);
|
||||
});
|
||||
|
||||
$(document).on("form-refresh", function (e, frm) {
|
||||
if (!frm.doc || frm.is_new()) {
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.socketio.doc_open(frm.doctype, frm.docname);
|
||||
me.doc_open(frm.doctype, frm.docname);
|
||||
});
|
||||
|
||||
$(document).on("form-unload", function (e, frm) {
|
||||
|
|
@ -83,63 +105,49 @@ frappe.socketio = {
|
|||
return;
|
||||
}
|
||||
|
||||
// frappe.socketio.doc_unsubscribe(frm.doctype, frm.docname);
|
||||
frappe.socketio.doc_close(frm.doctype, frm.docname);
|
||||
me.doc_close(frm.doctype, frm.docname);
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on("form-typing", function (e, frm) {
|
||||
frappe.socketio.form_typing(frm.doctype, frm.docname);
|
||||
});
|
||||
|
||||
$(document).on("form-stopped-typing", function (e, frm) {
|
||||
frappe.socketio.form_stopped_typing(frm.doctype, frm.docname);
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
if (!cur_frm || !cur_frm.doc || cur_frm.is_new()) {
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.socketio.doc_close(cur_frm.doctype, cur_frm.docname);
|
||||
});
|
||||
},
|
||||
get_host: function (port = 3000) {
|
||||
var host = window.location.origin;
|
||||
get_host(port = 3000) {
|
||||
let host = window.location.origin;
|
||||
if (window.dev_server) {
|
||||
var parts = host.split(":");
|
||||
let parts = host.split(":");
|
||||
port = frappe.boot.socketio_port || port.toString() || "3000";
|
||||
if (parts.length > 2) {
|
||||
host = parts[0] + ":" + parts[1];
|
||||
}
|
||||
host = host + ":" + port;
|
||||
}
|
||||
return host;
|
||||
},
|
||||
subscribe: function (task_id, opts) {
|
||||
// TODO DEPRECATE
|
||||
return host + `/${frappe.boot.sitename}`;
|
||||
}
|
||||
|
||||
frappe.socketio.socket.emit("task_subscribe", task_id);
|
||||
frappe.socketio.socket.emit("progress_subscribe", task_id);
|
||||
subscribe(task_id, opts) {
|
||||
this.emit("task_subscribe", task_id);
|
||||
this.emit("progress_subscribe", task_id);
|
||||
|
||||
frappe.socketio.open_tasks[task_id] = opts;
|
||||
},
|
||||
task_subscribe: function (task_id) {
|
||||
frappe.socketio.socket.emit("task_subscribe", task_id);
|
||||
},
|
||||
task_unsubscribe: function (task_id) {
|
||||
frappe.socketio.socket.emit("task_unsubscribe", task_id);
|
||||
},
|
||||
doctype_subscribe: function (doctype) {
|
||||
frappe.socketio.socket.emit("doctype_subscribe", doctype);
|
||||
},
|
||||
doctype_unsubscribe: function (doctype) {
|
||||
frappe.socketio.socket.emit("doctype_unsubscribe", doctype);
|
||||
},
|
||||
doc_subscribe: function (doctype, docname) {
|
||||
this.open_tasks[task_id] = opts;
|
||||
}
|
||||
task_subscribe(task_id) {
|
||||
this.emit("task_subscribe", task_id);
|
||||
}
|
||||
task_unsubscribe(task_id) {
|
||||
this.emit("task_unsubscribe", task_id);
|
||||
}
|
||||
doctype_subscribe(doctype) {
|
||||
this.emit("doctype_subscribe", doctype);
|
||||
}
|
||||
doctype_unsubscribe(doctype) {
|
||||
this.emit("doctype_unsubscribe", doctype);
|
||||
}
|
||||
doc_subscribe(doctype, docname) {
|
||||
if (frappe.flags.doc_subscribe) {
|
||||
console.log("throttled");
|
||||
return;
|
||||
}
|
||||
if (this.open_docs.has(`${doctype}:${docname}`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.flags.doc_subscribe = true;
|
||||
|
||||
|
|
@ -148,90 +156,34 @@ frappe.socketio = {
|
|||
frappe.flags.doc_subscribe = false;
|
||||
}, 1000);
|
||||
|
||||
frappe.socketio.socket.emit("doc_subscribe", doctype, docname);
|
||||
frappe.socketio.open_docs.push({ doctype: doctype, docname: docname });
|
||||
},
|
||||
doc_unsubscribe: function (doctype, docname) {
|
||||
frappe.socketio.socket.emit("doc_unsubscribe", doctype, docname);
|
||||
frappe.socketio.open_docs = $.filter(frappe.socketio.open_docs, function (d) {
|
||||
if (d.doctype === doctype && d.name === docname) {
|
||||
return null;
|
||||
} else {
|
||||
return d;
|
||||
}
|
||||
this.emit("doc_subscribe", doctype, docname);
|
||||
this.open_docs.add(`${doctype}:${docname}`);
|
||||
}
|
||||
doc_unsubscribe(doctype, docname) {
|
||||
this.emit("doc_unsubscribe", doctype, docname);
|
||||
return this.open_docs.delete(`${doctype}:${docname}`);
|
||||
}
|
||||
doc_open(doctype, docname) {
|
||||
this.emit("doc_open", doctype, docname);
|
||||
}
|
||||
doc_close(doctype, docname) {
|
||||
this.emit("doc_close", doctype, docname);
|
||||
}
|
||||
setup_listeners() {
|
||||
this.socket.on("task_status_change", function (data) {
|
||||
this.process_response(data, data.status.toLowerCase());
|
||||
});
|
||||
},
|
||||
doc_open: function (doctype, docname) {
|
||||
// notify that the user has opened this doc, if not already notified
|
||||
if (
|
||||
!frappe.socketio.last_doc ||
|
||||
frappe.socketio.last_doc[0] != doctype ||
|
||||
frappe.socketio.last_doc[1] != docname
|
||||
) {
|
||||
frappe.socketio.socket.emit("doc_open", doctype, docname);
|
||||
|
||||
frappe.socketio.last_doc &&
|
||||
frappe.socketio.doc_close(
|
||||
frappe.socketio.last_doc[0],
|
||||
frappe.socketio.last_doc[1]
|
||||
);
|
||||
}
|
||||
frappe.socketio.last_doc = [doctype, docname];
|
||||
},
|
||||
doc_close: function (doctype, docname) {
|
||||
// notify that the user has closed this doc
|
||||
frappe.socketio.socket.emit("doc_close", doctype, docname);
|
||||
|
||||
// if the doc is closed the user has also stopped typing
|
||||
frappe.socketio.socket.emit("doc_typing_stopped", doctype, docname);
|
||||
},
|
||||
form_typing: function (doctype, docname) {
|
||||
// notifiy that the user is typing on the doc
|
||||
frappe.socketio.socket.emit("doc_typing", doctype, docname);
|
||||
},
|
||||
form_stopped_typing: function (doctype, docname) {
|
||||
// notifiy that the user has stopped typing
|
||||
frappe.socketio.socket.emit("doc_typing_stopped", doctype, docname);
|
||||
},
|
||||
setup_listeners: function () {
|
||||
frappe.socketio.socket.on("task_status_change", function (data) {
|
||||
frappe.socketio.process_response(data, data.status.toLowerCase());
|
||||
this.socket.on("task_progress", function (data) {
|
||||
this.process_response(data, "progress");
|
||||
});
|
||||
frappe.socketio.socket.on("task_progress", function (data) {
|
||||
frappe.socketio.process_response(data, "progress");
|
||||
});
|
||||
},
|
||||
setup_reconnect: function () {
|
||||
// subscribe again to open_tasks
|
||||
frappe.socketio.socket.on("connect", function () {
|
||||
// wait for 5 seconds before subscribing again
|
||||
// because it takes more time to start python server than nodejs server
|
||||
// and we use validation requests to python server for subscribing
|
||||
setTimeout(function () {
|
||||
$.each(frappe.socketio.open_tasks, function (task_id, opts) {
|
||||
frappe.socketio.subscribe(task_id, opts);
|
||||
});
|
||||
|
||||
// re-connect open docs
|
||||
$.each(frappe.socketio.open_docs, function (d) {
|
||||
if (locals[d.doctype] && locals[d.doctype][d.name]) {
|
||||
frappe.socketio.doc_subscribe(d.doctype, d.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (cur_frm && cur_frm.doc && !cur_frm.is_new()) {
|
||||
frappe.socketio.doc_open(cur_frm.doc.doctype, cur_frm.doc.name);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
},
|
||||
process_response: function (data, method) {
|
||||
}
|
||||
process_response(data, method) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// success
|
||||
var opts = frappe.socketio.open_tasks[data.task_id];
|
||||
let opts = this.open_tasks[data.task_id];
|
||||
if (opts[method]) {
|
||||
opts[method](data);
|
||||
}
|
||||
|
|
@ -251,20 +203,16 @@ frappe.socketio = {
|
|||
if (data.status_code && data.status_code > 400 && opts.error) {
|
||||
opts.error(data);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
frappe.provide("frappe.realtime");
|
||||
frappe.realtime.on = function (event, callback) {
|
||||
frappe.socketio.socket && frappe.socketio.socket.on(event, callback);
|
||||
};
|
||||
|
||||
frappe.realtime.off = function (event, callback) {
|
||||
frappe.socketio.socket && frappe.socketio.socket.off(event, callback);
|
||||
};
|
||||
|
||||
frappe.realtime.publish = function (event, message) {
|
||||
if (frappe.socketio.socket) {
|
||||
frappe.socketio.socket.emit(event, message);
|
||||
}
|
||||
};
|
||||
|
||||
publish(event, message) {
|
||||
if (this.socket) {
|
||||
this.emit(event, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frappe.realtime = new RealTimeClient();
|
||||
|
||||
// backward compatbility
|
||||
frappe.socketio = frappe.realtime;
|
||||
|
|
|
|||
|
|
@ -202,14 +202,16 @@ frappe.dashboard_utils = {
|
|||
},
|
||||
|
||||
get_all_filters(doc) {
|
||||
let filters = JSON.parse(doc.filters_json || "null");
|
||||
let dynamic_filters = JSON.parse(doc.dynamic_filters_json || "null");
|
||||
let filters = doc.filters_json ? JSON.parse(doc.filters_json) : null;
|
||||
let dynamic_filters = doc.dynamic_filters_json
|
||||
? JSON.parse(doc.dynamic_filters_json)
|
||||
: null;
|
||||
|
||||
if (!dynamic_filters) {
|
||||
if (!dynamic_filters || !Object.keys(dynamic_filters).length) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
if ($.isArray(dynamic_filters)) {
|
||||
if (Array.isArray(dynamic_filters)) {
|
||||
dynamic_filters.forEach((f) => {
|
||||
try {
|
||||
f[3] = eval(f[3]);
|
||||
|
|
|
|||
|
|
@ -47,16 +47,6 @@ frappe.views.FormFactory = class FormFactory extends frappe.views.Factory {
|
|||
$(document).on("page-change", function () {
|
||||
frappe.ui.form.close_grid_form();
|
||||
});
|
||||
|
||||
frappe.realtime.on("doc_viewers", function (data) {
|
||||
// set users that currently viewing the form
|
||||
frappe.ui.form.FormViewers.set_users(data, "viewers");
|
||||
});
|
||||
|
||||
frappe.realtime.on("doc_typers", function (data) {
|
||||
// set users that currently typing on the form
|
||||
frappe.ui.form.FormViewers.set_users(data, "typers");
|
||||
});
|
||||
}
|
||||
this.initialized = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
if (this.list_view_settings?.disable_auto_refresh) {
|
||||
return;
|
||||
}
|
||||
frappe.socketio.doctype_subscribe(this.doctype);
|
||||
frappe.realtime.doctype_subscribe(this.doctype);
|
||||
frappe.realtime.on("list_update", (data) => this.on_update(data));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,42 +45,5 @@
|
|||
$background-color: var(--bg-color),
|
||||
$border-radius: var(--border-radius),
|
||||
) {
|
||||
|
||||
@if $content {
|
||||
img:after {
|
||||
content: url($content);
|
||||
}
|
||||
} @else {
|
||||
img:after {
|
||||
content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='lightgrey' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-image'><rect x='3' y='3' width='18' height='18' rx='2' ry='2'/><circle cx='8.5' cy='8.5' r='1.5'/><polyline points='21 15 16 10 5 21'/></svg>");
|
||||
}
|
||||
}
|
||||
|
||||
img[alt]:after {
|
||||
height: $height;
|
||||
top: $top;
|
||||
left: $left;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
@include flex();
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// @mixin img-foreground() {
|
||||
// content: "\f1c5";
|
||||
// display: block;
|
||||
// font-style: normal;
|
||||
// font-family: FontAwesome;
|
||||
// font-size: 32px;
|
||||
// color: var(--text-muted);
|
||||
|
||||
// position: absolute;
|
||||
// top: 50%;
|
||||
// transform: translateY(-50%);
|
||||
// left: 0;
|
||||
// width: 100%;
|
||||
// text-align: center;
|
||||
// }
|
||||
// Deprecated: Does not work as expected anymore. Also, this never worked in Safari.
|
||||
}
|
||||
|
|
@ -97,11 +97,6 @@
|
|||
color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@include broken-img(
|
||||
$height: 70px,
|
||||
$top: -15px,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -153,11 +153,6 @@
|
|||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include broken-img(
|
||||
$height: 175px,
|
||||
$border-radius: 0
|
||||
);
|
||||
}
|
||||
|
||||
.image-title {
|
||||
|
|
|
|||
|
|
@ -181,11 +181,6 @@
|
|||
color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@include broken-img(
|
||||
$height: 125px,
|
||||
$top: -4px,
|
||||
)
|
||||
}
|
||||
|
||||
.kanban-card-body {
|
||||
|
|
|
|||
|
|
@ -100,11 +100,11 @@
|
|||
|
||||
.group-by-button {
|
||||
margin: 5px;
|
||||
max-width: 125px;
|
||||
}
|
||||
|
||||
.group-by-button.btn-primary-light {
|
||||
color: var(--text-on-blue);
|
||||
outline: 1px solid var(--bg-dark-blue);
|
||||
}
|
||||
|
||||
.group-by-icon.active {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue