Merge branch 'develop' into fix-ambigus-table-join
This commit is contained in:
commit
160f0b65fb
303 changed files with 6305 additions and 4513 deletions
1
.flake8
1
.flake8
|
|
@ -69,6 +69,7 @@ ignore =
|
|||
F841,
|
||||
E713,
|
||||
E712,
|
||||
B028,
|
||||
|
||||
max-line-length = 200
|
||||
exclude=,test_*.py
|
||||
|
|
|
|||
15
.github/workflows/linters.yml
vendored
15
.github/workflows/linters.yml
vendored
|
|
@ -80,7 +80,20 @@ jobs:
|
|||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- run: |
|
||||
pip install pip-audit
|
||||
pip-audit ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456
|
||||
pip-audit --desc on .
|
||||
|
|
|
|||
20
.mergify.yml
20
.mergify.yml
|
|
@ -71,23 +71,3 @@ pull_request_rules:
|
|||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
|
||||
- name: backport to version-13-pre-release
|
||||
conditions:
|
||||
- label="backport version-13-pre-release"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-13-pre-release
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-12-hotfix
|
||||
conditions:
|
||||
- label="backport version-12-hotfix"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-12-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ repos:
|
|||
)$
|
||||
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.9.1
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ context("Control Color", () => {
|
|||
|
||||
//Checking if the css attribute is correct
|
||||
cy.get(".color-map").should("have.css", "color", "rgb(79, 157, 217)");
|
||||
cy.get(".hue-map").should("have.css", "color", "rgb(0, 145, 255)");
|
||||
cy.get(".hue-map").should("have.css", "color", "rgb(0, 144, 255)");
|
||||
|
||||
//Checking if the correct color is being selected
|
||||
cy.get("@dialog").then((dialog) => {
|
||||
|
|
|
|||
|
|
@ -229,19 +229,15 @@ context("Control Link", () => {
|
|||
);
|
||||
cy.reload();
|
||||
cy.new_form("ToDo");
|
||||
cy.fill_field("description", "new", "Text Editor");
|
||||
cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
|
||||
cy.findByRole("button", { name: "Save" }).click();
|
||||
cy.wait("@save_form");
|
||||
cy.fill_field("description", "new", "Text Editor").wait(200);
|
||||
cy.save();
|
||||
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
|
||||
"contain",
|
||||
"Administrator"
|
||||
);
|
||||
// if user clears default value explicitly, system should not reset default again
|
||||
cy.get_field("assigned_by").clear().blur();
|
||||
cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
|
||||
cy.findByRole("button", { name: "Save" }).click();
|
||||
cy.wait("@save_form");
|
||||
cy.save();
|
||||
cy.get_field("assigned_by").should("have.value", "");
|
||||
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
|
||||
"contain",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ context("Folder Navigation", () => {
|
|||
cy.get(".filter-selector > .btn").findByText("1 filter").click();
|
||||
cy.findByRole("button", { name: "Clear Filters" }).click();
|
||||
cy.get(".filter-action-buttons > .text-muted").findByText("+ Add a Filter").click();
|
||||
cy.get(".fieldname-select-area > .awesomplete > .form-control").type("Fol{enter}");
|
||||
cy.get(".fieldname-select-area > .awesomplete > .form-control:last").type("Fol{enter}");
|
||||
cy.get(
|
||||
".filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback"
|
||||
).type("Home{enter}");
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ context("Form", () => {
|
|||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
});
|
||||
|
||||
it("create a new form", () => {
|
||||
cy.visit("/app/todo/new");
|
||||
cy.get_field("description", "Text Editor")
|
||||
|
|
@ -172,4 +177,57 @@ context("Form", () => {
|
|||
send_welcome_email: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("update docfield property using set_df_property in child table", () => {
|
||||
cy.visit("/app/contact/Test Form Contact 1");
|
||||
cy.window()
|
||||
.its("cur_frm")
|
||||
.then((frm) => {
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
|
||||
|
||||
// set property before form_render event of child table
|
||||
cy.get("@table")
|
||||
.find('[data-idx="1"]')
|
||||
.invoke("attr", "data-name")
|
||||
.then((cdn) => {
|
||||
frm.set_df_property(
|
||||
"phone_nos",
|
||||
"hidden",
|
||||
1,
|
||||
"Contact Phone",
|
||||
"is_primary_phone",
|
||||
cdn
|
||||
);
|
||||
});
|
||||
|
||||
cy.get("@table").find('[data-idx="1"] .edit-grid-row').click();
|
||||
cy.get(".grid-row-open").as("table-form");
|
||||
cy.get("@table-form")
|
||||
.find('.frappe-control[data-fieldname="is_primary_phone"]')
|
||||
.should("be.hidden");
|
||||
cy.get("@table-form").find(".grid-footer-toolbar").click();
|
||||
|
||||
// set property on form_render event of child table
|
||||
cy.get("@table").find('[data-idx="1"] .edit-grid-row').click();
|
||||
cy.get("@table")
|
||||
.find('[data-idx="1"]')
|
||||
.invoke("attr", "data-name")
|
||||
.then((cdn) => {
|
||||
frm.set_df_property(
|
||||
"phone_nos",
|
||||
"hidden",
|
||||
0,
|
||||
"Contact Phone",
|
||||
"is_primary_phone",
|
||||
cdn
|
||||
);
|
||||
});
|
||||
|
||||
cy.get(".grid-row-open").as("table-form");
|
||||
cy.get("@table-form")
|
||||
.find('.frappe-control[data-fieldname="is_primary_phone"]')
|
||||
.should("be.visible");
|
||||
cy.get("@table-form").find(".grid-footer-toolbar").click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ context("Form Builder", () => {
|
|||
cy.get_open_dialog().find(".msgprint").should("contain", "Options is required");
|
||||
cy.hide_dialog();
|
||||
|
||||
cy.get(first_field).click();
|
||||
cy.get(first_field).click({ force: true });
|
||||
|
||||
cy.get(".sidebar-container .frappe-control[data-fieldname='options'] input")
|
||||
.click()
|
||||
|
|
@ -114,7 +114,7 @@ context("Form Builder", () => {
|
|||
cy.get_open_dialog().find(".msgprint").should("contain", "In List View");
|
||||
cy.hide_dialog();
|
||||
|
||||
cy.get(first_field).click();
|
||||
cy.get(first_field).click({ force: true });
|
||||
cy.get(".sidebar-container .field label .label-area").contains("In List View").click();
|
||||
|
||||
// validate In Global Search
|
||||
|
|
@ -273,4 +273,29 @@ context("Form Builder", () => {
|
|||
.find(".msgprint")
|
||||
.should("contain", "cannot be hidden and mandatory without any default value");
|
||||
});
|
||||
|
||||
it("Undo/Redo", () => {
|
||||
cy.visit(`/app/form-builder/${doctype_name}`);
|
||||
|
||||
// click on second tab
|
||||
cy.get(".tabs .tab:last").click();
|
||||
|
||||
let first_column = ".tab-content.active .section-columns-container:first .column:first";
|
||||
let first_field = first_column + " .field:first";
|
||||
let label = "div[title='Double click to edit label'] span:first";
|
||||
|
||||
// drag the first field to second position
|
||||
cy.get(first_field).drag(first_column + " .field:nth-child(2)", {
|
||||
target: { x: 100, y: 10 },
|
||||
});
|
||||
cy.get(first_field).find(label).should("have.text", "Check");
|
||||
|
||||
// undo
|
||||
cy.get("body").type("{ctrl}z");
|
||||
cy.get(first_field).find(label).should("have.text", "Data");
|
||||
|
||||
// redo
|
||||
cy.get("body").type("{ctrl}{shift}z");
|
||||
cy.get(first_field).find(label).should("have.text", "Check");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,11 +15,13 @@ context("List View Settings", () => {
|
|||
cy.clear_filters();
|
||||
cy.wait(300);
|
||||
cy.get(".list-count").should("contain", "20 of");
|
||||
cy.get("[href='#icon-small-message']").should("be.visible");
|
||||
cy.get(".menu-btn-group button").click();
|
||||
cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click();
|
||||
cy.get(".modal-dialog").should("contain", "DocType Settings");
|
||||
|
||||
cy.findByLabelText("Disable Count").check({ force: true });
|
||||
cy.findByLabelText("Disable Comment Count").check({ force: true });
|
||||
cy.findByLabelText("Disable Sidebar Stats").check({ force: true });
|
||||
cy.findByRole("button", { name: "Save" }).click();
|
||||
|
||||
|
|
@ -27,11 +29,13 @@ context("List View Settings", () => {
|
|||
|
||||
cy.get(".list-count").should("be.empty");
|
||||
cy.get(".list-sidebar .list-tags").should("not.exist");
|
||||
cy.get("[href='#icon-small-message']").should("not.be.visible");
|
||||
|
||||
cy.get(".menu-btn-group button").click({ force: true });
|
||||
cy.get(".dropdown-menu li").filter(":visible").contains("List Settings").click();
|
||||
cy.get(".modal-dialog").should("contain", "DocType Settings");
|
||||
cy.findByLabelText("Disable Count").uncheck({ force: true });
|
||||
cy.findByLabelText("Disable Comment Count").uncheck({ force: true });
|
||||
cy.findByLabelText("Disable Sidebar Stats").uncheck({ force: true });
|
||||
cy.findByRole("button", { name: "Save" }).click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ context("Navigation", () => {
|
|||
it.only("Navigate to previous page after login", () => {
|
||||
cy.visit("/app/todo");
|
||||
cy.get(".page-head").findByTitle("To Do").should("be.visible");
|
||||
cy.clear_filters();
|
||||
cy.request("/api/method/logout");
|
||||
cy.reload().as("reload");
|
||||
cy.get("@reload").get(".page-card .btn-primary").contains("Login").click();
|
||||
|
|
|
|||
|
|
@ -103,8 +103,9 @@ context("View", () => {
|
|||
});
|
||||
|
||||
it("Route to File View", () => {
|
||||
cy.intercept("POST", "/api/method/frappe.desk.reportview.get").as("list_loaded");
|
||||
cy.visit("app/file");
|
||||
cy.wait(500);
|
||||
cy.wait("@list_loaded");
|
||||
cy.window()
|
||||
.its("cur_list")
|
||||
.then((list) => {
|
||||
|
|
@ -113,7 +114,7 @@ context("View", () => {
|
|||
});
|
||||
|
||||
cy.visit("app/file/view/home/Attachments");
|
||||
cy.wait(500);
|
||||
cy.wait("@list_loaded");
|
||||
cy.window()
|
||||
.its("cur_list")
|
||||
.then((list) => {
|
||||
|
|
|
|||
|
|
@ -148,4 +148,39 @@ context("Workspace Blocks", () => {
|
|||
.should("eq", "Pending");
|
||||
cy.go("back");
|
||||
});
|
||||
|
||||
it("Number Card Block", () => {
|
||||
cy.create_records([
|
||||
{
|
||||
doctype: "Number Card",
|
||||
label: "Test Number Card",
|
||||
document_type: "ToDo",
|
||||
color: "#f74343",
|
||||
},
|
||||
]);
|
||||
|
||||
cy.get(".codex-editor__redactor .ce-block");
|
||||
cy.get(".standard-actions .btn-secondary[data-label=Edit]").click();
|
||||
|
||||
cy.get(".ce-block").first().click({ force: true }).type("{enter}");
|
||||
cy.get(".block-list-container .block-list-item").contains("Number Card").click();
|
||||
|
||||
// add number card
|
||||
cy.fill_field("number_card_name", "Test Number Card", "Link");
|
||||
cy.get('[data-fieldname="number_card_name"] ul li').contains("Test Number Card").click();
|
||||
cy.click_modal_primary_button("Add");
|
||||
cy.get(".ce-block .number-widget-box").first().as("number_card");
|
||||
cy.get("@number_card").find(".widget-title").should("contain", "Test Number Card");
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
cy.get("@number_card").find(".widget-title").should("contain", "Test Number Card");
|
||||
|
||||
// edit number card
|
||||
cy.get(".standard-actions .btn-secondary[data-label=Edit]").click();
|
||||
cy.get("@number_card").realHover().find(".widget-control .edit-button").click();
|
||||
cy.get_field("label", "Data").invoke("val", "ToDo Count");
|
||||
cy.click_modal_primary_button("Save");
|
||||
cy.get("@number_card").find(".widget-title").should("contain", "ToDo Count");
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
cy.get("@number_card").find(".widget-title").should("contain", "ToDo Count");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ Cypress.Commands.add("get_open_dialog", () => {
|
|||
|
||||
Cypress.Commands.add("save", () => {
|
||||
cy.intercept("/api/method/frappe.desk.form.save.savedocs").as("save_call");
|
||||
cy.get(`button[data-label="Save"]:visible`).click({ scrollBehavior: "top", force: true });
|
||||
cy.get(`.page-container:visible button[data-label="Save"]`).click({ force: true });
|
||||
cy.wait("@save_call");
|
||||
});
|
||||
Cypress.Commands.add("hide_dialog", () => {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import click
|
|||
from werkzeug.local import Local, release_local
|
||||
|
||||
from frappe.query_builder import (
|
||||
get_qb_engine,
|
||||
get_query,
|
||||
get_query_builder,
|
||||
patch_query_aggregation,
|
||||
patch_query_execute,
|
||||
|
|
@ -238,13 +238,12 @@ def init(site: str, sites_path: str = ".", new_site: bool = False) -> None:
|
|||
local.jenv = None
|
||||
local.jloader = None
|
||||
local.cache = {}
|
||||
local.document_cache = {}
|
||||
local.form_dict = _dict()
|
||||
local.preload_assets = {"style": [], "script": []}
|
||||
local.session = _dict()
|
||||
local.dev_server = _dev_server
|
||||
local.qb = get_query_builder(local.conf.db_type or "mariadb")
|
||||
local.qb.engine = get_qb_engine()
|
||||
local.qb.get_query = get_query
|
||||
setup_module_map()
|
||||
|
||||
if not _qb_patched.get(local.conf.db_type):
|
||||
|
|
@ -571,7 +570,7 @@ def get_user():
|
|||
|
||||
def get_roles(username=None) -> list[str]:
|
||||
"""Returns roles of current user."""
|
||||
if not local.session:
|
||||
if not local.session or not local.session.user:
|
||||
return ["Guest"]
|
||||
import frappe.permissions
|
||||
|
||||
|
|
@ -623,6 +622,7 @@ def sendmail(
|
|||
header=None,
|
||||
print_letterhead=False,
|
||||
with_container=False,
|
||||
email_read_tracker_url=None,
|
||||
):
|
||||
"""Send email using user's default **Email Account** or global default **Email Account**.
|
||||
|
||||
|
|
@ -704,6 +704,7 @@ def sendmail(
|
|||
header=header,
|
||||
print_letterhead=print_letterhead,
|
||||
with_container=with_container,
|
||||
email_read_tracker_url=email_read_tracker_url,
|
||||
)
|
||||
|
||||
# build email queue and send the email if send_now is True.
|
||||
|
|
@ -770,7 +771,12 @@ def is_whitelisted(method):
|
|||
|
||||
is_guest = session["user"] == "Guest"
|
||||
if method not in whitelisted or is_guest and method not in guest_methods:
|
||||
throw(_("Not permitted"), PermissionError)
|
||||
summary = _("You are not permitted to access this resource.")
|
||||
detail = _("Function {0} is not whitelisted.").format(
|
||||
bold(f"{method.__module__}.{method.__name__}")
|
||||
)
|
||||
msg = f"<details><summary>{summary}</summary>{detail}</details>"
|
||||
throw(msg, PermissionError, title="Method Not Allowed")
|
||||
|
||||
if is_guest and method not in xss_safe_methods:
|
||||
# strictly sanitize form_dict
|
||||
|
|
@ -1070,25 +1076,10 @@ def set_value(doctype, docname, fieldname, value=None):
|
|||
|
||||
|
||||
def get_cached_doc(*args, **kwargs) -> "Document":
|
||||
def _respond(doc, from_redis=False):
|
||||
if isinstance(doc, dict):
|
||||
local.document_cache[key] = doc = get_doc(doc)
|
||||
|
||||
elif from_redis:
|
||||
local.document_cache[key] = doc
|
||||
|
||||
if (key := can_cache_doc(args)) and (doc := cache().hget("document_cache", key)):
|
||||
return doc
|
||||
|
||||
if key := can_cache_doc(args):
|
||||
# local cache - has "ready" `Document` objects
|
||||
if doc := local.document_cache.get(key):
|
||||
return _respond(doc)
|
||||
|
||||
# redis cache
|
||||
if doc := cache().hget("document_cache", key):
|
||||
return _respond(doc, True)
|
||||
|
||||
# Not found in local/redis, fetch from DB
|
||||
# Not found in cache, fetch from DB
|
||||
doc = get_doc(*args, **kwargs)
|
||||
|
||||
# Store in cache
|
||||
|
|
@ -1101,14 +1092,7 @@ def get_cached_doc(*args, **kwargs) -> "Document":
|
|||
|
||||
|
||||
def _set_document_in_cache(key: str, doc: "Document") -> None:
|
||||
local.document_cache[key] = doc
|
||||
|
||||
# Avoid setting in local.cache since we're already using local.document_cache above
|
||||
# Try pickling the doc object as-is first, else fallback to doc.as_dict()
|
||||
try:
|
||||
cache().hset("document_cache", key, doc, cache_locally=False)
|
||||
except Exception:
|
||||
cache().hset("document_cache", key, doc.as_dict(), cache_locally=False)
|
||||
cache().hset("document_cache", key, doc)
|
||||
|
||||
|
||||
def can_cache_doc(args) -> str | None:
|
||||
|
|
@ -1134,12 +1118,11 @@ def get_document_cache_key(doctype: str, name: str):
|
|||
|
||||
def clear_document_cache(doctype, name):
|
||||
cache().hdel("last_modified", doctype)
|
||||
key = get_document_cache_key(doctype, name)
|
||||
if key in local.document_cache:
|
||||
del local.document_cache[key]
|
||||
cache().hdel("document_cache", key)
|
||||
cache().hdel("document_cache", get_document_cache_key(doctype, name))
|
||||
|
||||
if doctype == "System Settings" and hasattr(local, "system_settings"):
|
||||
delattr(local, "system_settings")
|
||||
|
||||
if doctype == "Website Settings" and hasattr(local, "website_settings"):
|
||||
delattr(local, "website_settings")
|
||||
|
||||
|
|
@ -1399,23 +1382,37 @@ def get_all_apps(with_internal_apps=True, sites_path=None):
|
|||
|
||||
|
||||
@request_cache
|
||||
def get_installed_apps(sort=False, frappe_last=False):
|
||||
"""Get list of installed apps in current site."""
|
||||
def get_installed_apps(sort=False, frappe_last=False, *, _ensure_on_bench=False):
|
||||
"""
|
||||
Get list of installed apps in current site.
|
||||
|
||||
:param sort: [DEPRECATED] Sort installed apps based on the sequence in sites/apps.txt
|
||||
:param frappe_last: [DEPRECATED] Keep frappe last. Do not use this, reverse the app list instead.
|
||||
:param ensure_on_bench: Only return apps that are present on bench.
|
||||
"""
|
||||
from frappe.utils.deprecations import deprecation_warning
|
||||
|
||||
if getattr(flags, "in_install_db", True):
|
||||
return []
|
||||
|
||||
if not db:
|
||||
connect()
|
||||
|
||||
if not local.all_apps:
|
||||
local.all_apps = cache().get_value("all_apps", get_all_apps)
|
||||
|
||||
installed = json.loads(db.get_global("installed_apps") or "[]")
|
||||
|
||||
if sort:
|
||||
if not local.all_apps:
|
||||
local.all_apps = cache().get_value("all_apps", get_all_apps)
|
||||
|
||||
deprecation_warning("`sort` argument is deprecated and will be removed in v15.")
|
||||
installed = [app for app in local.all_apps if app in installed]
|
||||
|
||||
if _ensure_on_bench:
|
||||
all_apps = cache().get_value("all_apps", get_all_apps)
|
||||
installed = [app for app in installed if app in all_apps]
|
||||
|
||||
if frappe_last:
|
||||
deprecation_warning("`frappe_last` argument is deprecated and will be removed in v15.")
|
||||
if "frappe" in installed:
|
||||
installed.remove("frappe")
|
||||
installed.append("frappe")
|
||||
|
|
@ -1445,19 +1442,17 @@ def _load_app_hooks(app_name: str | None = None):
|
|||
import types
|
||||
|
||||
hooks = {}
|
||||
apps = [app_name] if app_name else get_installed_apps(sort=True)
|
||||
apps = [app_name] if app_name else get_installed_apps(_ensure_on_bench=True)
|
||||
|
||||
for app in apps:
|
||||
try:
|
||||
app_hooks = get_module(f"{app}.hooks")
|
||||
except ImportError:
|
||||
except ImportError as e:
|
||||
if local.flags.in_install_app:
|
||||
# if app is not installed while restoring
|
||||
# ignore it
|
||||
pass
|
||||
print(f'Could not find app "{app}"')
|
||||
if not request:
|
||||
raise SystemExit
|
||||
print(f'Could not find app "{app}": \n{e}')
|
||||
raise
|
||||
|
||||
def _is_valid_hook(obj):
|
||||
|
|
@ -1573,7 +1568,7 @@ def read_file(path, raise_not_found=False):
|
|||
|
||||
def get_attr(method_string: str) -> Any:
|
||||
"""Get python method object from its name."""
|
||||
app_name = method_string.split(".")[0]
|
||||
app_name = method_string.split(".", 1)[0]
|
||||
if (
|
||||
not local.flags.in_uninstall
|
||||
and not local.flags.in_install
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ class _RESTAPIHandler:
|
|||
doc = frappe.get_doc(self.doctype, self.name)
|
||||
if not doc.has_permission("read"):
|
||||
raise frappe.PermissionError
|
||||
doc.apply_fieldlevel_read_permissions()
|
||||
frappe.local.response.update({"data": doc})
|
||||
|
||||
def update_doc(self):
|
||||
|
|
|
|||
|
|
@ -41,9 +41,6 @@ def application(request: Request):
|
|||
|
||||
init_request(request)
|
||||
|
||||
frappe.recorder.record()
|
||||
frappe.monitor.start()
|
||||
frappe.rate_limiter.apply()
|
||||
frappe.api.validate_auth()
|
||||
|
||||
if request.method == "OPTIONS":
|
||||
|
|
@ -74,15 +71,15 @@ def application(request: Request):
|
|||
response = handle_exception(e)
|
||||
|
||||
else:
|
||||
rollback = after_request(rollback)
|
||||
rollback = sync_database(rollback)
|
||||
|
||||
finally:
|
||||
if request.method in UNSAFE_HTTP_METHODS and frappe.db and rollback:
|
||||
frappe.db.rollback()
|
||||
|
||||
frappe.rate_limiter.update()
|
||||
frappe.monitor.stop(response)
|
||||
frappe.recorder.dump()
|
||||
if getattr(frappe.local, "initialised", False):
|
||||
for after_request_task in frappe.get_hooks("after_request"):
|
||||
frappe.call(after_request_task, response=response, request=request)
|
||||
|
||||
log_request(request, response)
|
||||
process_response(response)
|
||||
|
|
@ -119,6 +116,9 @@ def init_request(request):
|
|||
if request.method != "OPTIONS":
|
||||
frappe.local.http_request = HTTPRequest()
|
||||
|
||||
for before_request_task in frappe.get_hooks("before_request"):
|
||||
frappe.call(before_request_task)
|
||||
|
||||
|
||||
def setup_read_only_mode():
|
||||
"""During maintenance_mode reads to DB can still be performed to reduce downtime. This
|
||||
|
|
@ -318,7 +318,7 @@ def handle_exception(e):
|
|||
return response
|
||||
|
||||
|
||||
def after_request(rollback):
|
||||
def sync_database(rollback: bool) -> bool:
|
||||
# if HTTP method would change server state, commit if necessary
|
||||
if (
|
||||
frappe.db
|
||||
|
|
@ -332,9 +332,8 @@ def after_request(rollback):
|
|||
rollback = False
|
||||
|
||||
# update session
|
||||
if getattr(frappe.local, "session_obj", None):
|
||||
updated_in_db = frappe.local.session_obj.update()
|
||||
if updated_in_db:
|
||||
if session := getattr(frappe.local, "session_obj", None):
|
||||
if session.update():
|
||||
frappe.db.commit()
|
||||
rollback = False
|
||||
|
||||
|
|
@ -376,6 +375,7 @@ def serve(
|
|||
"0.0.0.0",
|
||||
int(port),
|
||||
application,
|
||||
exclude_patterns=["test_*"],
|
||||
use_reloader=False if in_test_env else not no_reload,
|
||||
use_debugger=not in_test_env,
|
||||
use_evalex=not in_test_env,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ class HTTPRequest:
|
|||
|
||||
def set_request_ip(self):
|
||||
if frappe.get_request_header("X-Forwarded-For"):
|
||||
frappe.local.request_ip = (frappe.get_request_header("X-Forwarded-For").split(",")[0]).strip()
|
||||
frappe.local.request_ip = (
|
||||
frappe.get_request_header("X-Forwarded-For").split(",", 1)[0]
|
||||
).strip()
|
||||
|
||||
elif frappe.get_request_header("REMOTE_ADDR"):
|
||||
frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR")
|
||||
|
|
@ -305,7 +307,7 @@ class LoginManager:
|
|||
|
||||
current_hour = int(now_datetime().strftime("%H"))
|
||||
|
||||
if login_before and current_hour > login_before:
|
||||
if login_before and current_hour >= login_before:
|
||||
frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError)
|
||||
|
||||
if login_after and current_hour < login_after:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Copyright (c) 2018, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
from frappe.automation.doctype.auto_repeat.auto_repeat import (
|
||||
create_repeated_entries,
|
||||
|
|
@ -10,8 +12,11 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
|||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, add_months, getdate, today
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.custom.doctype.custom_field.custom_field import CustomField
|
||||
|
||||
def add_custom_fields():
|
||||
|
||||
def add_custom_fields() -> "CustomField":
|
||||
df = dict(
|
||||
fieldname="auto_repeat",
|
||||
label="Auto Repeat",
|
||||
|
|
@ -22,15 +27,17 @@ def add_custom_fields():
|
|||
print_hide=1,
|
||||
read_only=1,
|
||||
)
|
||||
create_custom_field("ToDo", df)
|
||||
return create_custom_field("ToDo", df) or frappe.get_doc(
|
||||
"Custom Field", dict(fieldname=df["fieldname"], dt="ToDo")
|
||||
)
|
||||
|
||||
|
||||
class TestAutoRepeat(FrappeTestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.sql(
|
||||
"SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"
|
||||
):
|
||||
add_custom_fields()
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.custom_field = add_custom_fields()
|
||||
cls.addClassCleanup(cls.custom_field.delete)
|
||||
return super().setUpClass()
|
||||
|
||||
def test_daily_auto_repeat(self):
|
||||
todo = frappe.get_doc(
|
||||
|
|
|
|||
8
frappe/automation/doctype/reminder/reminder.js
Normal file
8
frappe/automation/doctype/reminder/reminder.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2023, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Reminder", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
90
frappe/automation/doctype/reminder/reminder.json
Normal file
90
frappe/automation/doctype/reminder/reminder.json
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2023-02-22 11:23:58.183276",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user",
|
||||
"remind_at",
|
||||
"description",
|
||||
"reminder_doctype",
|
||||
"reminder_docname",
|
||||
"notified"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "__user",
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reminder_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reminder_docname",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Name",
|
||||
"options": "reminder_doctype",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "now",
|
||||
"fieldname": "remind_at",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Remind At",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "notified",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "notified"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-24 13:47:50.419648",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Reminder",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"if_owner": 1,
|
||||
"read": 1,
|
||||
"role": "All",
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "description"
|
||||
}
|
||||
78
frappe/automation/doctype/reminder/reminder.py
Normal file
78
frappe/automation/doctype/reminder/reminder.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Copyright (c) 2023, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.data import add_to_date, get_datetime, now_datetime
|
||||
|
||||
|
||||
class Reminder(Document):
|
||||
@staticmethod
|
||||
def clear_old_logs(days=30):
|
||||
from frappe.query_builder import Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
|
||||
table = frappe.qb.DocType("Reminder")
|
||||
frappe.db.delete(table, filters=(table.remind_at < (Now() - Interval(days=days))))
|
||||
|
||||
def validate(self):
|
||||
self.user = frappe.session.user
|
||||
if get_datetime(self.remind_at) < now_datetime():
|
||||
frappe.throw(_("Reminder cannot be created in past."))
|
||||
|
||||
def send_reminder(self):
|
||||
if self.notified:
|
||||
return
|
||||
|
||||
self.db_set("notified", 1, update_modified=False)
|
||||
|
||||
try:
|
||||
notification = frappe.new_doc("Notification Log")
|
||||
notification.for_user = self.user
|
||||
notification.set("type", "Alert")
|
||||
notification.document_type = self.reminder_doctype
|
||||
notification.document_name = self.reminder_docname
|
||||
notification.subject = self.description
|
||||
notification.insert()
|
||||
except Exception:
|
||||
self.log_error("Failed to send reminder")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_new_reminder(
|
||||
remind_at: str,
|
||||
description: str,
|
||||
reminder_doctype: str | None = None,
|
||||
reminder_docname: str | None = None,
|
||||
):
|
||||
reminder = frappe.new_doc("Reminder")
|
||||
|
||||
reminder.description = description
|
||||
reminder.remind_at = remind_at
|
||||
reminder.reminder_doctype = reminder_doctype
|
||||
reminder.reminder_docname = reminder_docname
|
||||
|
||||
return reminder.insert()
|
||||
|
||||
|
||||
def send_reminders():
|
||||
# Ensure that we send all reminders that might be before next job execution.
|
||||
job_freq = cint(frappe.get_conf().scheduler_interval) or 240
|
||||
upper_threshold = add_to_date(now_datetime(), seconds=job_freq, as_string=True, as_datetime=True)
|
||||
|
||||
lower_threshold = add_to_date(now_datetime(), hours=-8, as_string=True, as_datetime=True)
|
||||
|
||||
pending_reminders = frappe.get_all(
|
||||
"Reminder",
|
||||
filters=[
|
||||
("remind_at", "<=", upper_threshold),
|
||||
("remind_at", ">=", lower_threshold), # dont send too old reminders if failed to send
|
||||
("notified", "=", 0),
|
||||
],
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for reminder in pending_reminders:
|
||||
frappe.get_doc("Reminder", reminder).send_reminder()
|
||||
28
frappe/automation/doctype/reminder/test_reminder.py
Normal file
28
frappe/automation/doctype/reminder/test_reminder.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright (c) 2023, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.automation.doctype.reminder.reminder import create_new_reminder, send_reminders
|
||||
from frappe.desk.doctype.notification_log.notification_log import get_notification_logs
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_to_date, now_datetime
|
||||
|
||||
|
||||
class TestReminder(FrappeTestCase):
|
||||
def test_reminder(self):
|
||||
|
||||
description = "TEST_REMINDER"
|
||||
|
||||
create_new_reminder(
|
||||
remind_at=add_to_date(now_datetime(), minutes=1, as_datetime=True, as_string=True),
|
||||
description=description,
|
||||
)
|
||||
|
||||
send_reminders()
|
||||
|
||||
notifications = get_notification_logs()["notification_logs"]
|
||||
self.assertIn(
|
||||
description,
|
||||
[n.subject for n in notifications],
|
||||
msg=f"Failed to find reminder notification \n{notifications}",
|
||||
)
|
||||
|
|
@ -12,6 +12,7 @@ from frappe.desk.doctype.route_history.route_history import frequently_visited_l
|
|||
from frappe.desk.form.load import get_meta_bundle
|
||||
from frappe.email.inbox import get_email_accounts
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.permissions import has_permission
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.query_builder.terms import ParameterizedValueWrapper, SubQuery
|
||||
|
|
@ -101,7 +102,7 @@ def get_bootinfo():
|
|||
bootinfo.app_logo_url = get_app_logo()
|
||||
bootinfo.link_title_doctypes = get_link_title_doctypes()
|
||||
bootinfo.translated_doctypes = get_translated_doctypes()
|
||||
bootinfo.subscription_expiry = add_subscription_expiry()
|
||||
bootinfo.subscription_conf = add_subscription_conf()
|
||||
|
||||
return bootinfo
|
||||
|
||||
|
|
@ -234,7 +235,10 @@ def get_user_pages_or_reports(parent, cache=False):
|
|||
has_role[p.name] = {"modified": p.modified, "title": p.title}
|
||||
|
||||
elif parent == "Report":
|
||||
reports = frappe.get_all(
|
||||
if not has_permission("Report", raise_exception=False):
|
||||
return {}
|
||||
|
||||
reports = frappe.get_list(
|
||||
"Report",
|
||||
fields=["name", "report_type"],
|
||||
filters={"name": ("in", has_role.keys())},
|
||||
|
|
@ -243,6 +247,10 @@ def get_user_pages_or_reports(parent, cache=False):
|
|||
for report in reports:
|
||||
has_role[report.name]["report_type"] = report.report_type
|
||||
|
||||
non_permitted_reports = set(has_role.keys()) - {r.name for r in reports}
|
||||
for r in non_permitted_reports:
|
||||
has_role.pop(r, None)
|
||||
|
||||
# Expire every six hours
|
||||
_cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
|
||||
return has_role
|
||||
|
|
@ -431,8 +439,8 @@ def load_currency_docs(bootinfo):
|
|||
bootinfo.docs += currency_docs
|
||||
|
||||
|
||||
def add_subscription_expiry():
|
||||
def add_subscription_conf():
|
||||
try:
|
||||
return frappe.conf.subscription["expiry"]
|
||||
return frappe.conf.subscription
|
||||
except Exception:
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -127,8 +127,6 @@ def clear_doctype_cache(doctype=None):
|
|||
for key in ("is_table", "doctype_modules", "document_cache"):
|
||||
cache.delete_value(key)
|
||||
|
||||
frappe.local.document_cache = {}
|
||||
|
||||
def clear_single(dt):
|
||||
for name in doctype_cache_keys:
|
||||
cache.hdel(name, dt)
|
||||
|
|
@ -160,11 +158,10 @@ def clear_doctype_cache(doctype=None):
|
|||
|
||||
def clear_controller_cache(doctype=None):
|
||||
if not doctype:
|
||||
del frappe.controllers
|
||||
frappe.controllers = {}
|
||||
frappe.controllers.pop(frappe.local.site, None)
|
||||
return
|
||||
|
||||
for site_controllers in frappe.controllers.values():
|
||||
if site_controllers := frappe.controllers.get(frappe.local.site):
|
||||
site_controllers.pop(doctype, None)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ def get(doctype, name=None, filters=None, parent=None):
|
|||
doc = frappe.get_doc(doctype) # single
|
||||
|
||||
doc.check_permission()
|
||||
doc.apply_fieldlevel_read_permissions()
|
||||
|
||||
return doc.as_dict()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import click
|
|||
import frappe
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
@click.command("trigger-scheduler-event", help="Trigger a scheduler event")
|
||||
|
|
@ -74,36 +73,40 @@ def disable_scheduler(context):
|
|||
|
||||
@click.command("scheduler")
|
||||
@click.option("--site", help="site name")
|
||||
@click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable"]))
|
||||
@click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable", "status"]))
|
||||
@click.option(
|
||||
"--format", "-f", default="text", type=click.Choice(["json", "text"]), help="Output format"
|
||||
)
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
||||
@pass_context
|
||||
def scheduler(context, state, site=None):
|
||||
def scheduler(context, state: str, format: str, verbose: bool = False, site: str | None = None):
|
||||
"""Control scheduler state."""
|
||||
import frappe.utils.scheduler
|
||||
from frappe.installer import update_site_config
|
||||
import frappe
|
||||
from frappe.utils.scheduler import is_scheduler_inactive, toggle_scheduler
|
||||
|
||||
if not site:
|
||||
site = get_site(context)
|
||||
site = site or get_site(context)
|
||||
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
output = {
|
||||
"text": "Scheduler is {status} for site {site}",
|
||||
"json": '{{"status": "{status}", "site": "{site}"}}',
|
||||
}
|
||||
|
||||
if state == "pause":
|
||||
update_site_config("pause_scheduler", 1)
|
||||
elif state == "resume":
|
||||
update_site_config("pause_scheduler", 0)
|
||||
elif state == "disable":
|
||||
frappe.connect()
|
||||
frappe.utils.scheduler.disable_scheduler()
|
||||
frappe.db.commit()
|
||||
elif state == "enable":
|
||||
frappe.connect()
|
||||
frappe.utils.scheduler.enable_scheduler()
|
||||
frappe.db.commit()
|
||||
with frappe.init_site(site=site):
|
||||
match state:
|
||||
case "status":
|
||||
frappe.connect()
|
||||
status = "disabled" if is_scheduler_inactive(verbose=verbose) else "enabled"
|
||||
return print(output[format].format(status=status, site=site))
|
||||
case "pause" | "resume":
|
||||
from frappe.installer import update_site_config
|
||||
|
||||
print(f"Scheduler {state}d for site {site}")
|
||||
update_site_config("pause_scheduler", state == "pause")
|
||||
case "enable" | "disable":
|
||||
frappe.connect()
|
||||
toggle_scheduler(state == "enable")
|
||||
frappe.db.commit()
|
||||
|
||||
finally:
|
||||
frappe.destroy()
|
||||
print(output[format].format(status=f"{state}d", site=site))
|
||||
|
||||
|
||||
@click.command("set-maintenance-mode")
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ from frappe.exceptions import SiteNotSpecifiedError
|
|||
@click.option(
|
||||
"--force", help="Force restore if site/database already exists", is_flag=True, default=False
|
||||
)
|
||||
@click.option("--source_sql", help="Initiate database with a SQL file")
|
||||
@click.option("--source-sql", "--source_sql", help="Initiate database with a SQL file")
|
||||
@click.option("--install-app", multiple=True, help="Install app after installation")
|
||||
@click.option(
|
||||
"--set-default", is_flag=True, default=False, help="Set the new site as default site"
|
||||
|
|
@ -67,10 +67,13 @@ def new_site(
|
|||
set_default=False,
|
||||
):
|
||||
"Create a new site"
|
||||
from frappe.installer import _new_site
|
||||
from frappe.installer import _new_site, extract_sql_from_archive
|
||||
|
||||
frappe.init(site=site, new_site=True)
|
||||
|
||||
if source_sql:
|
||||
source_sql = extract_sql_from_archive(source_sql)
|
||||
|
||||
_new_site(
|
||||
db_name,
|
||||
site,
|
||||
|
|
@ -592,6 +595,8 @@ def disable_user(context, email):
|
|||
@pass_context
|
||||
def migrate(context, skip_failing=False, skip_search_index=False):
|
||||
"Run patches, sync schema and rebuild files/translations"
|
||||
from traceback_with_variables import activate_by_import
|
||||
|
||||
from frappe.migrate import SiteMigration
|
||||
|
||||
for site in context.sites:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ DATA_IMPORT_DEPRECATION = (
|
|||
"[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n"
|
||||
"Use `data-import` command instead to import data via 'Data Import'."
|
||||
)
|
||||
EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
|
||||
|
||||
|
||||
@click.command("build")
|
||||
|
|
@ -485,9 +486,10 @@ def bulk_rename(context, doctype, path):
|
|||
frappe.destroy()
|
||||
|
||||
|
||||
@click.command("db-console")
|
||||
@click.command("db-console", context_settings=EXTRA_ARGS_CTX)
|
||||
@click.argument("extra_args", nargs=-1)
|
||||
@pass_context
|
||||
def database(context):
|
||||
def database(context, extra_args):
|
||||
"""
|
||||
Enter into the Database console for given site.
|
||||
"""
|
||||
|
|
@ -496,14 +498,18 @@ def database(context):
|
|||
raise SiteNotSpecifiedError
|
||||
frappe.init(site=site)
|
||||
if not frappe.conf.db_type or frappe.conf.db_type == "mariadb":
|
||||
_mariadb()
|
||||
_mariadb(extra_args=extra_args)
|
||||
elif frappe.conf.db_type == "postgres":
|
||||
_psql()
|
||||
_psql(extra_args=extra_args)
|
||||
|
||||
|
||||
@click.command("mariadb")
|
||||
@click.command(
|
||||
"mariadb",
|
||||
context_settings=EXTRA_ARGS_CTX,
|
||||
)
|
||||
@click.argument("extra_args", nargs=-1)
|
||||
@pass_context
|
||||
def mariadb(context):
|
||||
def mariadb(context, extra_args):
|
||||
"""
|
||||
Enter into mariadb console for a given site.
|
||||
"""
|
||||
|
|
@ -511,21 +517,22 @@ def mariadb(context):
|
|||
if not site:
|
||||
raise SiteNotSpecifiedError
|
||||
frappe.init(site=site)
|
||||
_mariadb()
|
||||
_mariadb(extra_args=extra_args)
|
||||
|
||||
|
||||
@click.command("postgres")
|
||||
@click.command("postgres", context_settings=EXTRA_ARGS_CTX)
|
||||
@click.argument("extra_args", nargs=-1)
|
||||
@pass_context
|
||||
def postgres(context):
|
||||
def postgres(context, extra_args):
|
||||
"""
|
||||
Enter into postgres console for a given site.
|
||||
"""
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
_psql()
|
||||
_psql(extra_args=extra_args)
|
||||
|
||||
|
||||
def _mariadb():
|
||||
def _mariadb(extra_args=None):
|
||||
from frappe.database.mariadb.database import MariaDBDatabase
|
||||
|
||||
mysql = which("mysql")
|
||||
|
|
@ -543,10 +550,12 @@ def _mariadb():
|
|||
"--safe-updates",
|
||||
"-A",
|
||||
]
|
||||
if extra_args:
|
||||
command += list(extra_args)
|
||||
os.execv(mysql, command)
|
||||
|
||||
|
||||
def _psql():
|
||||
def _psql(extra_args=None):
|
||||
psql = which("psql")
|
||||
|
||||
host = frappe.conf.db_host or "127.0.0.1"
|
||||
|
|
@ -554,7 +563,10 @@ def _psql():
|
|||
env = os.environ.copy()
|
||||
env["PGPASSWORD"] = frappe.conf.db_password
|
||||
conn_string = f"postgresql://{frappe.conf.db_name}@{host}:{port}/{frappe.conf.db_name}"
|
||||
subprocess.run([psql, conn_string], check=True, env=env)
|
||||
psql_cmd = [psql, conn_string]
|
||||
if extra_args:
|
||||
psql_cmd = psql_cmd + list(extra_args)
|
||||
subprocess.run(psql_cmd, check=True, env=env)
|
||||
|
||||
|
||||
@click.command("jupyter")
|
||||
|
|
@ -562,7 +574,7 @@ def _psql():
|
|||
def jupyter(context):
|
||||
"""Start an interactive jupyter notebook"""
|
||||
installed_packages = (
|
||||
r.split("==")[0]
|
||||
r.split("==", 1)[0]
|
||||
for r in subprocess.check_output([sys.executable, "-m", "pip", "freeze"], encoding="utf8")
|
||||
)
|
||||
|
||||
|
|
@ -896,7 +908,7 @@ def run_ui_tests(
|
|||
|
||||
os.chdir(app_base_path)
|
||||
|
||||
node_bin = subprocess.getoutput("npm bin")
|
||||
node_bin = subprocess.getoutput("yarn bin")
|
||||
cypress_path = f"{node_bin}/cypress"
|
||||
drag_drop_plugin_path = f"{node_bin}/../@4tw/cypress-drag-drop"
|
||||
real_events_plugin_path = f"{node_bin}/../cypress-real-events"
|
||||
|
|
@ -1001,7 +1013,7 @@ def request(context, args=None, path=None):
|
|||
frappe.local.form_dict = frappe._dict()
|
||||
|
||||
if args.startswith("/api/method"):
|
||||
frappe.local.form_dict.cmd = args.split("?")[0].split("/")[-1]
|
||||
frappe.local.form_dict.cmd = args.split("?", 1)[0].split("/")[-1]
|
||||
elif path:
|
||||
with open(os.path.join("..", path)) as f:
|
||||
args = json.loads(f.read())
|
||||
|
|
@ -1030,6 +1042,16 @@ def make_app(destination, app_name, no_git=False):
|
|||
make_boilerplate(destination, app_name, no_git=no_git)
|
||||
|
||||
|
||||
@click.command("create-patch")
|
||||
def create_patch():
|
||||
"Creates a new patch interactively"
|
||||
from frappe.utils.boilerplate import PatchCreator
|
||||
|
||||
pc = PatchCreator()
|
||||
pc.fetch_user_inputs()
|
||||
pc.create_patch_file()
|
||||
|
||||
|
||||
@click.command("set-config")
|
||||
@click.argument("key")
|
||||
@click.argument("value")
|
||||
|
|
@ -1061,6 +1083,8 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False):
|
|||
common_site_config_path = os.path.join(sites_path, "common_site_config.json")
|
||||
update_site_config(key, value, validate=False, site_config_path=common_site_config_path)
|
||||
else:
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
update_site_config(key, value, validate=False)
|
||||
|
|
@ -1176,6 +1200,7 @@ commands = [
|
|||
data_import,
|
||||
import_doc,
|
||||
make_app,
|
||||
create_patch,
|
||||
mariadb,
|
||||
postgres,
|
||||
request,
|
||||
|
|
|
|||
|
|
@ -204,7 +204,6 @@
|
|||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ city }}<br>
|
||||
{% if state %}{{ state }}<br>{% endif -%}
|
||||
{% if pincode %}{{ pincode }}<br>{% endif -%}
|
||||
{{ country }}<br>
|
||||
<br>
|
||||
{% if phone %}{{ _("Phone") }}: {{ phone }}<br>{% endif -%}
|
||||
{% if fax %}{{ _("Fax") }}: {{ fax }}<br>{% endif -%}
|
||||
{% if email_id %}{{ _("Email") }}: {{ email_id }}<br>{% endif -%}
|
||||
|
|
@ -53,7 +53,6 @@
|
|||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
|
|
@ -62,4 +61,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,52 +4,36 @@
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.jinja import validate_template
|
||||
|
||||
|
||||
class AddressTemplate(Document):
|
||||
def validate(self):
|
||||
validate_template(self.template)
|
||||
|
||||
if not self.template:
|
||||
self.template = get_default_address_template()
|
||||
|
||||
self.defaults = frappe.db.get_values(
|
||||
"Address Template", {"is_default": 1, "name": ("!=", self.name)}
|
||||
)
|
||||
if not self.is_default:
|
||||
if not self.defaults:
|
||||
self.is_default = 1
|
||||
if cint(frappe.db.get_single_value("System Settings", "setup_complete")):
|
||||
frappe.msgprint(_("Setting this Address Template as default as there is no other default"))
|
||||
|
||||
validate_template(self.template)
|
||||
if not self.is_default and not self._get_previous_default():
|
||||
self.is_default = 1
|
||||
if frappe.db.get_single_value("System Settings", "setup_complete"):
|
||||
frappe.msgprint(_("Setting this Address Template as default as there is no other default"))
|
||||
|
||||
def on_update(self):
|
||||
if self.is_default and self.defaults:
|
||||
for d in self.defaults:
|
||||
frappe.db.set_value("Address Template", d[0], "is_default", 0)
|
||||
if self.is_default and (previous_default := self._get_previous_default()):
|
||||
frappe.db.set_value("Address Template", previous_default, "is_default", 0)
|
||||
|
||||
def on_trash(self):
|
||||
if self.is_default:
|
||||
frappe.throw(_("Default Address Template cannot be deleted"))
|
||||
|
||||
def _get_previous_default(self) -> str | None:
|
||||
return frappe.db.get_value("Address Template", {"is_default": 1, "name": ("!=", self.name)})
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_address_template():
|
||||
"""Get default address template (translated)"""
|
||||
return (
|
||||
"""{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}\
|
||||
{{ city }}<br>
|
||||
{% if state %}{{ state }}<br>{% endif -%}
|
||||
{% if pincode %}{{ pincode }}<br>{% endif -%}
|
||||
{{ country }}<br>
|
||||
{% if phone %}"""
|
||||
+ _("Phone")
|
||||
+ """: {{ phone }}<br>{% endif -%}
|
||||
{% if fax %}"""
|
||||
+ _("Fax")
|
||||
+ """: {{ fax }}<br>{% endif -%}
|
||||
{% if email_id %}"""
|
||||
+ _("Email")
|
||||
+ """: {{ email_id }}<br>{% endif -%}"""
|
||||
)
|
||||
def get_default_address_template() -> str:
|
||||
"""Return the default address template."""
|
||||
from pathlib import Path
|
||||
|
||||
return (Path(__file__).parent / "address_template.jinja").read_text()
|
||||
|
|
|
|||
|
|
@ -1,39 +1,39 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
from frappe.contacts.doctype.address_template.address_template import get_default_address_template
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils.jinja import validate_template
|
||||
|
||||
|
||||
class TestAddressTemplate(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.make_default_address_template()
|
||||
def setUp(self) -> None:
|
||||
frappe.db.delete("Address Template", {"country": "India"})
|
||||
frappe.db.delete("Address Template", {"country": "Brazil"})
|
||||
|
||||
def test_default_address_template(self):
|
||||
validate_template(get_default_address_template())
|
||||
|
||||
def test_default_is_unset(self):
|
||||
a = frappe.get_doc("Address Template", "India")
|
||||
a.is_default = 1
|
||||
a.save()
|
||||
frappe.get_doc({"doctype": "Address Template", "country": "India", "is_default": 1}).insert()
|
||||
|
||||
b = frappe.get_doc("Address Template", "Brazil")
|
||||
b.is_default = 1
|
||||
b.save()
|
||||
self.assertEqual(frappe.db.get_value("Address Template", "India", "is_default"), 1)
|
||||
|
||||
frappe.get_doc({"doctype": "Address Template", "country": "Brazil", "is_default": 1}).insert()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Address Template", "India", "is_default"), 0)
|
||||
self.assertEqual(frappe.db.get_value("Address Template", "Brazil", "is_default"), 1)
|
||||
|
||||
def tearDown(self):
|
||||
a = frappe.get_doc("Address Template", "India")
|
||||
a.is_default = 1
|
||||
a.save()
|
||||
def test_delete_address_template(self):
|
||||
india = frappe.get_doc(
|
||||
{"doctype": "Address Template", "country": "India", "is_default": 0}
|
||||
).insert()
|
||||
|
||||
@classmethod
|
||||
def make_default_address_template(self):
|
||||
template = """{{ address_line1 }}<br>{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}{{ city }}<br>{% if state %}{{ state }}<br>{% endif -%}{% if pincode %}{{ pincode }}<br>{% endif -%}{{ country }}<br>{% if phone %}Phone: {{ phone }}<br>{% endif -%}{% if fax %}Fax: {{ fax }}<br>{% endif -%}{% if email_id %}Email: {{ email_id }}<br>{% endif -%}"""
|
||||
brazil = frappe.get_doc(
|
||||
{"doctype": "Address Template", "country": "Brazil", "is_default": 1}
|
||||
).insert()
|
||||
|
||||
if not frappe.db.exists("Address Template", "India"):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Address Template", "country": "India", "is_default": 1, "template": template}
|
||||
).insert()
|
||||
india.reload() # might have been modified by the second template
|
||||
india.delete() # should not raise an error
|
||||
|
||||
if not frappe.db.exists("Address Template", "Brazil"):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Address Template", "country": "Brazil", "template": template}
|
||||
).insert()
|
||||
self.assertRaises(frappe.ValidationError, brazil.delete)
|
||||
|
|
|
|||
|
|
@ -275,7 +275,6 @@
|
|||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
|
|
|
|||
|
|
@ -112,6 +112,3 @@ class TestAddressesAndContacts(FrappeTestCase):
|
|||
1,
|
||||
]
|
||||
self.assertListEqual(test_item, report_data[idx])
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ from frappe.utils import cstr
|
|||
|
||||
|
||||
class AccessLog(Document):
|
||||
pass
|
||||
@staticmethod
|
||||
def clear_old_logs(days=30):
|
||||
from frappe.query_builder import Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
|
||||
table = frappe.qb.DocType("Access Log")
|
||||
frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -487,28 +487,32 @@ def parse_email(communication, email_strings):
|
|||
"""
|
||||
Parse email to add timeline links.
|
||||
When automatic email linking is enabled, an email from email_strings can contain
|
||||
a doctype and docname ie in the format `admin+doctype+docname@example.com`,
|
||||
a doctype and docname ie in the format `admin+doctype+docname@example.com` or `admin+doctype=docname@example.com`,
|
||||
the email is parsed and doctype and docname is extracted and timeline link is added.
|
||||
"""
|
||||
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
|
||||
if not frappe.db.get_value("Email Account", filters={"enable_automatic_linking": 1}):
|
||||
return
|
||||
|
||||
delimiter = "+"
|
||||
|
||||
for email_string in email_strings:
|
||||
if email_string:
|
||||
for email in email_string.split(","):
|
||||
if delimiter in email:
|
||||
email = email.split("@")[0]
|
||||
email_local_parts = email.split(delimiter)
|
||||
if not len(email_local_parts) == 3:
|
||||
continue
|
||||
|
||||
email_username = email.split("@", 1)[0]
|
||||
email_local_parts = email_username.split("+")
|
||||
docname = doctype = None
|
||||
if len(email_local_parts) == 3:
|
||||
doctype = unquote(email_local_parts[1])
|
||||
docname = unquote(email_local_parts[2])
|
||||
|
||||
if doctype and docname and frappe.db.exists(doctype, docname):
|
||||
communication.add_link(doctype, docname)
|
||||
elif len(email_local_parts) == 2:
|
||||
document_parts = email_local_parts[1].split("=", 1)
|
||||
if len(document_parts) != 2:
|
||||
continue
|
||||
|
||||
doctype = unquote(document_parts[0])
|
||||
docname = unquote(document_parts[1])
|
||||
|
||||
if doctype and docname and frappe.db.get_value(doctype, docname, ignore=True):
|
||||
communication.add_link(doctype, docname)
|
||||
|
||||
|
||||
def get_email_without_link(email):
|
||||
|
|
@ -521,7 +525,7 @@ def get_email_without_link(email):
|
|||
|
||||
try:
|
||||
_email = email.split("@")
|
||||
email_id = _email[0].split("+")[0]
|
||||
email_id = _email[0].split("+", 1)[0]
|
||||
email_host = _email[1]
|
||||
except IndexError:
|
||||
return email
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from frappe.utils import (
|
|||
cint,
|
||||
get_datetime,
|
||||
get_formatted_email,
|
||||
get_imaginary_pixel_response,
|
||||
get_string_between,
|
||||
list_to_str,
|
||||
split_emails,
|
||||
|
|
@ -249,18 +250,7 @@ def mark_email_as_seen(name: str = None):
|
|||
frappe.log_error("Unable to mark as seen", None, "Communication", name)
|
||||
|
||||
finally:
|
||||
frappe.response.update(
|
||||
{
|
||||
"type": "binary",
|
||||
"filename": "imaginary_pixel.png",
|
||||
"filecontent": (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
|
||||
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r"
|
||||
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0"
|
||||
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
),
|
||||
}
|
||||
)
|
||||
frappe.response.update(frappe.utils.get_imaginary_pixel_response())
|
||||
|
||||
|
||||
def update_communication_as_read(name):
|
||||
|
|
|
|||
|
|
@ -219,17 +219,17 @@ class TestCommunication(FrappeTestCase):
|
|||
self.assertIn(comm_note_2.name, data)
|
||||
|
||||
def test_link_in_email(self):
|
||||
frappe.delete_doc_if_exists("Note", "test document link in email")
|
||||
|
||||
create_email_account()
|
||||
|
||||
note = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Note",
|
||||
"title": "test document link in email",
|
||||
"content": "test document link in email",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
notes = {}
|
||||
for i in range(2):
|
||||
frappe.delete_doc_if_exists("Note", f"test document link in email {i}")
|
||||
notes[i] = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Note",
|
||||
"title": f"test document link in email {i}",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
comm = frappe.get_doc(
|
||||
{
|
||||
|
|
@ -237,14 +237,15 @@ class TestCommunication(FrappeTestCase):
|
|||
"communication_medium": "Email",
|
||||
"subject": "Document Link in Email",
|
||||
"sender": "comm_sender@example.com",
|
||||
"recipients": f'comm_recipient+{quote("Note")}+{quote(note.name)}@example.com',
|
||||
"recipients": f'comm_recipient+{quote("Note")}+{quote(notes[0].name)}@example.com,comm_recipient+{quote("Note")}={quote(notes[1].name)}@example.com',
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
doc_links = [
|
||||
(timeline_link.link_doctype, timeline_link.link_name) for timeline_link in comm.timeline_links
|
||||
]
|
||||
self.assertIn(("Note", note.name), doc_links)
|
||||
self.assertIn(("Note", notes[0].name), doc_links)
|
||||
self.assertIn(("Note", notes[1].name), doc_links)
|
||||
|
||||
def test_parse_emails(self):
|
||||
emails = get_emails(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@
|
|||
"report",
|
||||
"export",
|
||||
"import",
|
||||
"set_user_permissions",
|
||||
"column_break_19",
|
||||
"share",
|
||||
"print",
|
||||
|
|
@ -179,13 +178,6 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Import"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "This role update User Permissions for a user",
|
||||
"fieldname": "set_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set User Permissions"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break"
|
||||
|
|
@ -223,7 +215,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-12-03 15:20:48.296730",
|
||||
"modified": "2023-02-20 13:19:04.889081",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Custom DocPerm",
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@
|
|||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Pending\nSuccess\nPartial Success\nError",
|
||||
"read_only": 1
|
||||
},
|
||||
|
|
@ -170,7 +171,7 @@
|
|||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-01 20:08:37.624914",
|
||||
"modified": "2022-02-14 10:08:37.624914",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Data Import",
|
||||
|
|
@ -194,4 +195,4 @@
|
|||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -304,6 +304,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list(['Section Break', 'Column Break', 'Tab Break'], doc.fieldtype)",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"label": "Perm Level",
|
||||
|
|
@ -555,7 +556,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-11 20:46:43.164926",
|
||||
"modified": "2023-02-20 12:07:29.552523",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@
|
|||
"report",
|
||||
"export",
|
||||
"import",
|
||||
"set_user_permissions",
|
||||
"column_break_19",
|
||||
"share",
|
||||
"print",
|
||||
|
|
@ -178,13 +177,6 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Import"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "This role update User Permissions for a user",
|
||||
"fieldname": "set_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set User Permissions"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break"
|
||||
|
|
@ -218,7 +210,7 @@
|
|||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-03 15:15:30.488212",
|
||||
"modified": "2023-02-20 13:21:45.071310",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocPerm",
|
||||
|
|
|
|||
|
|
@ -125,3 +125,17 @@ class TestDocShare(FrappeTestCase):
|
|||
)
|
||||
|
||||
frappe.share.remove(doctype, submittable_doc.name, self.user)
|
||||
|
||||
def test_share_int_pk(self):
|
||||
test_doc = frappe.new_doc("Console Log")
|
||||
|
||||
test_doc.insert()
|
||||
frappe.share.add("Console Log", test_doc.name, self.user)
|
||||
|
||||
frappe.set_user(self.user)
|
||||
self.assertIn(
|
||||
str(test_doc.name), [str(name) for name in frappe.get_list("Console Log", pluck="name")]
|
||||
)
|
||||
|
||||
test_doc.reload()
|
||||
self.assertTrue(test_doc.has_permission("read"))
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@
|
|||
# import frappe
|
||||
{base_class_import}
|
||||
|
||||
|
||||
class {classname}({base_class}):
|
||||
{custom_controller}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ frappe.ui.form.on("DocType", {
|
|||
msg += __("If you just want to customize for your site, use {0} instead.", [
|
||||
customize_form_link,
|
||||
]);
|
||||
frm.dashboard.add_comment(msg, "yellow");
|
||||
frm.dashboard.add_comment(msg, "yellow", true);
|
||||
}
|
||||
|
||||
if (frm.is_new()) {
|
||||
|
|
@ -104,88 +104,7 @@ frappe.ui.form.on("DocType", {
|
|||
|
||||
frappe.ui.form.on("DocField", {
|
||||
form_render(frm, doctype, docname) {
|
||||
// Render two select fields for Fetch From instead of Small Text for better UX
|
||||
let field = frm.cur_grid.grid_form.fields_dict.fetch_from;
|
||||
$(field.input_area).hide();
|
||||
|
||||
let $doctype_select = $(`<select class="form-control">`);
|
||||
let $field_select = $(`<select class="form-control">`);
|
||||
let $wrapper = $('<div class="fetch-from-select row"><div>');
|
||||
$wrapper.append($doctype_select, $field_select);
|
||||
field.$input_wrapper.append($wrapper);
|
||||
$doctype_select.wrap('<div class="col"></div>');
|
||||
$field_select.wrap('<div class="col"></div>');
|
||||
|
||||
let row = frappe.get_doc(doctype, docname);
|
||||
let curr_value = { doctype: null, fieldname: null };
|
||||
if (row.fetch_from) {
|
||||
let [doctype, fieldname] = row.fetch_from.split(".");
|
||||
curr_value.doctype = doctype;
|
||||
curr_value.fieldname = fieldname;
|
||||
}
|
||||
|
||||
let doctypes = frm.doc.fields
|
||||
.filter((df) => df.fieldtype == "Link")
|
||||
.filter((df) => df.options && df.fieldname != row.fieldname)
|
||||
.sort((a, b) => a.options.localeCompare(b.options))
|
||||
.map((df) => ({
|
||||
label: `${df.options} (${df.fieldname})`,
|
||||
value: df.fieldname,
|
||||
}));
|
||||
$doctype_select.add_options([
|
||||
{ label: __("Select DocType"), value: "", selected: true },
|
||||
...doctypes,
|
||||
]);
|
||||
|
||||
$doctype_select.on("change", () => {
|
||||
row.fetch_from = "";
|
||||
frm.dirty();
|
||||
update_fieldname_options();
|
||||
});
|
||||
|
||||
function update_fieldname_options() {
|
||||
$field_select.find("option").remove();
|
||||
|
||||
let link_fieldname = $doctype_select.val();
|
||||
if (!link_fieldname) return;
|
||||
let link_field = frm.doc.fields.find((df) => df.fieldname === link_fieldname);
|
||||
let link_doctype = link_field.options;
|
||||
frappe.model.with_doctype(link_doctype, () => {
|
||||
let fields = frappe.meta
|
||||
.get_docfields(link_doctype, null, {
|
||||
fieldtype: ["not in", frappe.model.no_value_type],
|
||||
})
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((df) => ({
|
||||
label: `${df.label} (${df.fieldtype})`,
|
||||
value: df.fieldname,
|
||||
}));
|
||||
$field_select.add_options([
|
||||
{
|
||||
label: __("Select Field"),
|
||||
value: "",
|
||||
selected: true,
|
||||
disabled: true,
|
||||
},
|
||||
...fields,
|
||||
]);
|
||||
|
||||
if (curr_value.fieldname) {
|
||||
$field_select.val(curr_value.fieldname);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$field_select.on("change", () => {
|
||||
let fetch_from = `${$doctype_select.val()}.${$field_select.val()}`;
|
||||
row.fetch_from = fetch_from;
|
||||
frm.dirty();
|
||||
});
|
||||
|
||||
if (curr_value.doctype) {
|
||||
$doctype_select.val(curr_value.doctype);
|
||||
update_fieldname_options();
|
||||
}
|
||||
frm.trigger("setup_fetch_from_fields", doctype, docname);
|
||||
},
|
||||
|
||||
fieldtype: function (frm) {
|
||||
|
|
|
|||
|
|
@ -604,6 +604,7 @@
|
|||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.is_submittable",
|
||||
"description": "Enabling this will submit documents in background",
|
||||
"fieldname": "queue_in_background",
|
||||
"fieldtype": "Check",
|
||||
"label": "Queue in Background"
|
||||
|
|
@ -707,7 +708,7 @@
|
|||
"link_fieldname": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"modified": "2022-12-14 09:47:27.315351",
|
||||
"modified": "2023-01-04 17:23:09.206018",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
@ -744,4 +745,4 @@
|
|||
"states": [],
|
||||
"track_changes": 1,
|
||||
"translated_doctype": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -366,8 +366,10 @@ class DocType(Document):
|
|||
d.fieldname = d.fieldname + "_column"
|
||||
elif d.fieldtype == "Tab Break":
|
||||
d.fieldname = d.fieldname + "_tab"
|
||||
else:
|
||||
elif d.fieldtype in ("Section Break", "Column Break", "Tab Break"):
|
||||
d.fieldname = d.fieldtype.lower().replace(" ", "_") + "_" + str(random_string(4))
|
||||
else:
|
||||
frappe.throw(_("Row #{}: Fieldname is required").format(d.idx), title="Missing Fieldname")
|
||||
else:
|
||||
if d.fieldname in restricted:
|
||||
frappe.throw(_("Fieldname {0} is restricted").format(d.fieldname), InvalidFieldNameError)
|
||||
|
|
@ -883,7 +885,7 @@ def validate_series(dt, autoname=None, name=None):
|
|||
if not autoname and dt.get("fields", {"fieldname": "naming_series"}):
|
||||
dt.autoname = "naming_series:"
|
||||
elif dt.autoname and dt.autoname.startswith("naming_series:"):
|
||||
fieldname = dt.autoname.split("naming_series:")[0] or "naming_series"
|
||||
fieldname = dt.autoname.split("naming_series:", 1)[0] or "naming_series"
|
||||
if not dt.get("fields", {"fieldname": fieldname}):
|
||||
frappe.throw(
|
||||
_("Fieldname called {0} must exist to enable autonaming").format(frappe.bold(fieldname)),
|
||||
|
|
@ -911,7 +913,7 @@ def validate_series(dt, autoname=None, name=None):
|
|||
and (not autoname.startswith("format:"))
|
||||
):
|
||||
|
||||
prefix = autoname.split(".")[0]
|
||||
prefix = autoname.split(".", 1)[0]
|
||||
doctype = frappe.qb.DocType("DocType")
|
||||
used_in = (
|
||||
frappe.qb.from_(doctype)
|
||||
|
|
@ -981,7 +983,7 @@ def change_name_column_type(doctype_name: str, type: str) -> None:
|
|||
|
||||
def validate_links_table_fieldnames(meta):
|
||||
"""Validate fieldnames in Links table"""
|
||||
if not meta.links or frappe.flags.in_patch or frappe.flags.in_fixtures:
|
||||
if not meta.links or frappe.flags.in_patch or frappe.flags.in_fixtures or frappe.flags.in_migrate:
|
||||
return
|
||||
|
||||
fieldnames = tuple(field.fieldname for field in meta.fields)
|
||||
|
|
@ -1096,10 +1098,7 @@ def validate_fields(meta):
|
|||
)
|
||||
|
||||
def check_link_table_options(docname, d):
|
||||
if frappe.flags.in_patch:
|
||||
return
|
||||
|
||||
if frappe.flags.in_fixtures:
|
||||
if frappe.flags.in_patch or frappe.flags.in_fixtures:
|
||||
return
|
||||
|
||||
if d.fieldtype in ("Link",) + table_fields:
|
||||
|
|
@ -1133,7 +1132,7 @@ def validate_fields(meta):
|
|||
d.options = options
|
||||
|
||||
def check_hidden_and_mandatory(docname, d):
|
||||
if d.hidden and d.reqd and not d.default:
|
||||
if d.hidden and d.reqd and not d.default and not frappe.flags.in_migrate:
|
||||
frappe.throw(
|
||||
_("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format(
|
||||
docname, d.label, d.idx
|
||||
|
|
@ -1346,7 +1345,7 @@ def validate_fields(meta):
|
|||
if meta.sort_field:
|
||||
sort_fields = [meta.sort_field]
|
||||
if "," in meta.sort_field:
|
||||
sort_fields = [d.split()[0] for d in meta.sort_field.split(",")]
|
||||
sort_fields = [d.split(maxsplit=1)[0] for d in meta.sort_field.split(",")]
|
||||
|
||||
for fieldname in sort_fields:
|
||||
if fieldname not in (fieldname_list + list(default_fields) + list(child_table_fields)):
|
||||
|
|
@ -1416,10 +1415,9 @@ def validate_fields(meta):
|
|||
)
|
||||
df_options_str = "<ul><li>" + "</li><li>".join(_(x) for x in data_field_options) + "</ul>"
|
||||
|
||||
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
|
||||
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", alert=True)
|
||||
|
||||
def check_child_table_option(docfield):
|
||||
|
||||
if frappe.flags.in_fixtures:
|
||||
return
|
||||
if docfield.fieldtype not in ["Table MultiSelect", "Table"]:
|
||||
|
|
@ -1462,31 +1460,34 @@ def validate_fields(meta):
|
|||
check_invalid_fieldnames(meta.get("name"), d.fieldname)
|
||||
check_unique_fieldname(meta.get("name"), d.fieldname)
|
||||
check_fieldname_length(d.fieldname)
|
||||
check_illegal_mandatory(meta.get("name"), d)
|
||||
check_link_table_options(meta.get("name"), d)
|
||||
check_dynamic_link_options(d)
|
||||
check_hidden_and_mandatory(meta.get("name"), d)
|
||||
check_in_list_view(meta.get("istable"), d)
|
||||
check_in_global_search(d)
|
||||
check_illegal_default(d)
|
||||
check_unique_and_text(meta.get("name"), d)
|
||||
check_illegal_depends_on_conditions(d)
|
||||
check_child_table_option(d)
|
||||
check_table_multiselect_option(d)
|
||||
scrub_options_in_select(d)
|
||||
scrub_fetch_from(d)
|
||||
validate_data_field_type(d)
|
||||
check_max_height(d)
|
||||
check_no_of_ratings(d)
|
||||
|
||||
check_fold(fields)
|
||||
check_search_fields(meta, fields)
|
||||
check_title_field(meta)
|
||||
check_timeline_field(meta)
|
||||
check_is_published_field(meta)
|
||||
check_website_search_field(meta)
|
||||
check_sort_field(meta)
|
||||
check_image_field(meta)
|
||||
if not frappe.flags.in_migrate:
|
||||
check_link_table_options(meta.get("name"), d)
|
||||
check_illegal_mandatory(meta.get("name"), d)
|
||||
check_dynamic_link_options(d)
|
||||
check_in_list_view(meta.get("istable"), d)
|
||||
check_in_global_search(d)
|
||||
check_illegal_depends_on_conditions(d)
|
||||
check_illegal_default(d)
|
||||
check_child_table_option(d)
|
||||
check_max_height(d)
|
||||
check_no_of_ratings(d)
|
||||
|
||||
if not frappe.flags.in_migrate:
|
||||
check_fold(fields)
|
||||
check_search_fields(meta, fields)
|
||||
check_title_field(meta)
|
||||
check_timeline_field(meta)
|
||||
check_is_published_field(meta)
|
||||
check_website_search_field(meta)
|
||||
check_sort_field(meta)
|
||||
check_image_field(meta)
|
||||
|
||||
|
||||
def get_fields_not_allowed_in_list_view(meta) -> list[str]:
|
||||
|
|
@ -1603,11 +1604,6 @@ def validate_permissions(doctype, for_remove=False, alert=False):
|
|||
d.set("import", 0)
|
||||
d.set("export", 0)
|
||||
|
||||
for ptype, label in [["set_user_permissions", _("Set User Permissions")]]:
|
||||
if d.get(ptype):
|
||||
d.set(ptype, 0)
|
||||
frappe.msgprint(_("{0} cannot be set for Single types").format(label))
|
||||
|
||||
def check_if_submittable(d):
|
||||
if d.submit and not issubmittable:
|
||||
frappe.throw(_("{0}: Cannot set Assign Submit if not Submittable").format(get_txt(d)))
|
||||
|
|
|
|||
|
|
@ -172,32 +172,6 @@ class TestDocType(FrappeTestCase):
|
|||
if condition:
|
||||
self.assertFalse(re.match(pattern, condition))
|
||||
|
||||
def test_data_field_options(self):
|
||||
doctype_name = "Test Data Fields"
|
||||
valid_data_field_options = frappe.model.data_field_options + ("",)
|
||||
invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5))
|
||||
|
||||
for field_option in valid_data_field_options + invalid_data_field_options:
|
||||
test_doctype = frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"name": doctype_name,
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [
|
||||
{"fieldname": f"{field_option}_field", "fieldtype": "Data", "options": field_option}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if field_option in invalid_data_field_options:
|
||||
# assert that only data options in frappe.model.data_field_options are valid
|
||||
self.assertRaises(frappe.ValidationError, test_doctype.insert)
|
||||
else:
|
||||
test_doctype.insert()
|
||||
self.assertEqual(test_doctype.name, doctype_name)
|
||||
test_doctype.delete()
|
||||
|
||||
def test_sync_field_order(self):
|
||||
import os
|
||||
|
||||
|
|
@ -552,13 +526,14 @@ class TestDocType(FrappeTestCase):
|
|||
self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc)
|
||||
|
||||
def test_create_virtual_doctype(self):
|
||||
"""Test virtual DOcTYpe."""
|
||||
"""Test virtual DocType."""
|
||||
virtual_doc = new_doctype("Test Virtual Doctype")
|
||||
virtual_doc.is_virtual = 1
|
||||
virtual_doc.insert()
|
||||
virtual_doc.save()
|
||||
virtual_doc.insert(ignore_if_duplicate=True)
|
||||
virtual_doc.reload()
|
||||
doc = frappe.get_doc("DocType", "Test Virtual Doctype")
|
||||
|
||||
self.assertDictEqual(doc.as_dict(), virtual_doc.as_dict())
|
||||
self.assertEqual(doc.is_virtual, 1)
|
||||
self.assertFalse(frappe.db.table_exists("Test Virtual Doctype"))
|
||||
|
||||
|
|
|
|||
|
|
@ -81,10 +81,10 @@
|
|||
},
|
||||
{
|
||||
"depends_on": "transaction_type",
|
||||
"description": "Generate 3 preview of names generate by any valid series.",
|
||||
"description": "Get a preview of generated names with a series.",
|
||||
"fieldname": "try_naming_series",
|
||||
"fieldtype": "Data",
|
||||
"label": "Try a naming Series"
|
||||
"label": "Try a Naming Series"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_type",
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
"icon": "fa fa-sort-by-order",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-30 23:51:36.136535",
|
||||
"modified": "2023-02-20 13:11:56.662100",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Document Naming Settings",
|
||||
|
|
@ -130,4 +130,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
# 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):
|
||||
pass
|
||||
def test_form_dict_sanitization(self):
|
||||
self.assertNotEqual(sanitized_dict({"pwd": "SECRET", "usr": "WHAT"}).get("pwd"), "SECRET")
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ frappe.ui.form.on("File", {
|
|||
|
||||
preview_file: function (frm) {
|
||||
let $preview = "";
|
||||
let file_name = frm.doc.file_name.split("?")[0];
|
||||
let file_extension = file_name.split(".").pop()?.toLowerCase();
|
||||
|
||||
if (frappe.utils.is_image_file(frm.doc.file_url)) {
|
||||
$preview = $(`<div class="img_preview">
|
||||
|
|
@ -40,7 +42,7 @@ frappe.ui.form.on("File", {
|
|||
${__("Your browser does not support the video element.")}
|
||||
</video>
|
||||
</div>`);
|
||||
} else if (frm.doc.file_name.split("?")[0].endsWith(".pdf")) {
|
||||
} else if (file_extension === "pdf") {
|
||||
$preview = $(`<div class="img_preview">
|
||||
<object style="background:#323639;" width="100%">
|
||||
<embed
|
||||
|
|
@ -51,7 +53,7 @@ frappe.ui.form.on("File", {
|
|||
>
|
||||
</object>
|
||||
</div>`);
|
||||
} else if (frm.doc.file_name.split("?")[0].endsWith(".mp3")) {
|
||||
} else if (file_extension === "mp3") {
|
||||
$preview = $(`<div class="img_preview">
|
||||
<audio width="480" height="60" controls>
|
||||
<source src="${frm.doc.file_url}" type="audio/mpeg">
|
||||
|
|
|
|||
|
|
@ -329,7 +329,11 @@ class File(Document):
|
|||
self.file_url = duplicate_file.file_url
|
||||
|
||||
def set_file_name(self):
|
||||
if not self.file_name and self.file_url:
|
||||
if not self.file_name and not self.file_url:
|
||||
frappe.throw(
|
||||
_("Fields `file_name` or `file_url` must be set for File"), exc=frappe.MandatoryError
|
||||
)
|
||||
elif not self.file_name and self.file_url:
|
||||
self.file_name = self.file_url.split("/")[-1]
|
||||
else:
|
||||
self.file_name = re.sub(r"/", "", self.file_name)
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ def extract_images_from_html(doc: "Document", content: str, is_private: bool = F
|
|||
def _save_file(match):
|
||||
data = match.group(1).split("data:")[1]
|
||||
headers, content = data.split(",")
|
||||
mtype = headers.split(";")[0]
|
||||
mtype = headers.split(";", 1)[0]
|
||||
|
||||
if isinstance(content, str):
|
||||
content = content.encode("utf-8")
|
||||
|
|
@ -237,7 +237,7 @@ def extract_images_from_html(doc: "Document", content: str, is_private: bool = F
|
|||
|
||||
if "filename=" in headers:
|
||||
filename = headers.split("filename=")[-1]
|
||||
filename = safe_decode(filename).split(";")[0]
|
||||
filename = safe_decode(filename).split(";", 1)[0]
|
||||
|
||||
else:
|
||||
filename = get_random_filename(content_type=mtype)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,66 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Installed Applications", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
refresh: function (frm) {
|
||||
frm.add_custom_button(__("Update Hooks Resolution Order"), () => {
|
||||
frm.trigger("show_update_order_dialog");
|
||||
});
|
||||
},
|
||||
|
||||
show_update_order_dialog() {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Update Hooks Resolution Order"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "apps",
|
||||
fieldtype: "Table",
|
||||
label: __("Installed Apps"),
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
in_place_edit: true,
|
||||
data: [],
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "app_name",
|
||||
label: __("App Name"),
|
||||
in_list_view: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
const new_order = this.get_values()["apps"].map((row) => row.app_name);
|
||||
frappe.call({
|
||||
method: "frappe.core.doctype.installed_applications.installed_applications.update_installed_apps_order",
|
||||
freeze: true,
|
||||
args: {
|
||||
new_order: new_order,
|
||||
},
|
||||
});
|
||||
this.hide();
|
||||
},
|
||||
primary_action_label: __("Update Order"),
|
||||
});
|
||||
|
||||
frappe
|
||||
.xcall(
|
||||
"frappe.core.doctype.installed_applications.installed_applications.get_installed_app_order"
|
||||
)
|
||||
.then((data) => {
|
||||
data.forEach((app) => {
|
||||
dialog.fields_dict.apps.df.data.push({
|
||||
app_name: app,
|
||||
});
|
||||
});
|
||||
|
||||
dialog.fields_dict.apps.grid.refresh();
|
||||
// hack: change checkboxes to drag handles.
|
||||
let grid = $(dialog.fields_dict.apps.grid.parent);
|
||||
grid.find(".grid-row-check:first").remove() &&
|
||||
grid.find(".grid-row-check").replaceWith(frappe.utils.icon("menu"));
|
||||
dialog.show();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class InvalidAppOrder(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InstalledApplications(Document):
|
||||
def update_versions(self):
|
||||
self.delete_key("installed_applications")
|
||||
|
|
@ -18,3 +25,51 @@ class InstalledApplications(Document):
|
|||
},
|
||||
)
|
||||
self.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_installed_apps_order(new_order: list[str] | str):
|
||||
"""Change the ordering of `installed_apps` global
|
||||
|
||||
This list is used to resolve hooks and by default it's order of installation on site.
|
||||
|
||||
Sometimes it might not be the ordering you want, so thie function is provided to override it.
|
||||
"""
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
if isinstance(new_order, str):
|
||||
new_order = json.loads(new_order)
|
||||
|
||||
frappe.local.request_cache and frappe.local.request_cache.clear()
|
||||
existing_order = frappe.get_installed_apps(_ensure_on_bench=True)
|
||||
|
||||
if set(existing_order) != set(new_order) or not isinstance(new_order, list):
|
||||
frappe.throw(
|
||||
_("You are only allowed to update order, do not remove or add apps."), exc=InvalidAppOrder
|
||||
)
|
||||
|
||||
# Ensure frappe is always first regardless of user's preference.
|
||||
if "frappe" in new_order:
|
||||
new_order.remove("frappe")
|
||||
new_order.insert(0, "frappe")
|
||||
|
||||
frappe.db.set_global("installed_apps", json.dumps(new_order))
|
||||
|
||||
_create_version_log_for_change(existing_order, new_order)
|
||||
|
||||
|
||||
def _create_version_log_for_change(old, new):
|
||||
version = frappe.new_doc("Version")
|
||||
version.ref_doctype = "DefaultValue"
|
||||
version.docname = "installed_apps"
|
||||
version.data = frappe.as_json({"changed": [["current", json.dumps(old), json.dumps(new)]]})
|
||||
version.flags.ignore_links = True # This is a fake doctype
|
||||
version.flags.ignore_permissions = True
|
||||
version.insert()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_installed_app_order() -> list[str]:
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
return frappe.get_installed_apps(_ensure_on_bench=True)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
# import frappe
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.installed_applications.installed_applications import (
|
||||
InvalidAppOrder,
|
||||
update_installed_apps_order,
|
||||
)
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestInstalledApplications(FrappeTestCase):
|
||||
pass
|
||||
def test_order_change(self):
|
||||
update_installed_apps_order(["frappe"])
|
||||
self.assertRaises(InvalidAppOrder, update_installed_apps_order, [])
|
||||
self.assertRaises(InvalidAppOrder, update_installed_apps_order, ["frappe", "deepmind"])
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ DEFAULT_LOGTYPES_RETENTION = {
|
|||
"Route History": 90,
|
||||
"Submission Queue": 30,
|
||||
"Prepared Report": 30,
|
||||
"Webhook Request Log": 30,
|
||||
"Reminder": 30,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class PackageImport(Document):
|
|||
attachment = attachment[0]
|
||||
|
||||
# get package_name from file (package_name-0.0.0.tar.gz)
|
||||
package_name = attachment.file_name.split(".")[0].rsplit("-", 1)[0]
|
||||
package_name = attachment.file_name.split(".", 1)[0].rsplit("-", 1)[0]
|
||||
if not os.path.exists(frappe.get_site_path("packages")):
|
||||
os.makedirs(frappe.get_site_path("packages"))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "PATCHLOG.#####",
|
||||
"autoname": "hash",
|
||||
"creation": "2013-01-17 11:36:45",
|
||||
"description": "List of patches executed",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -20,11 +20,11 @@
|
|||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-13 05:34:37.845368",
|
||||
"modified": "2023-01-17 15:35:11.688615",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Patch Log",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -141,6 +141,8 @@ def serialize_job(job: Job) -> frappe._dict:
|
|||
creation=convert_utc_to_user_timezone(job.created_at),
|
||||
modified=convert_utc_to_user_timezone(modified),
|
||||
_comment_count=0,
|
||||
owner=job.kwargs.get("user"),
|
||||
modified_by=job.kwargs.get("user"),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,17 @@
|
|||
|
||||
frappe.ui.form.on("Submission Queue", {
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.status === "Queued" && frm.doc.job_id) {
|
||||
if (frm.doc.status === "Queued" && frappe.boot.user.roles.includes("System Manager")) {
|
||||
frm.add_custom_button(__("Unlock Reference Document"), () => {
|
||||
frappe.confirm(__("Are you sure you want to go ahead with this action?"), () => {
|
||||
frm.call("unlock_doc");
|
||||
});
|
||||
frappe.confirm(
|
||||
`
|
||||
Are you sure you want to go ahead with this action?
|
||||
Doing this could unlock other submissions of this document which are in queue (if present)
|
||||
and could lead to non-ideal conditions.`,
|
||||
() => {
|
||||
frm.call("unlock_doc");
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@
|
|||
"fields": [
|
||||
{
|
||||
"fieldname": "job_id",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Link",
|
||||
"label": "Job Id",
|
||||
"options": "RQ Job",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -80,14 +81,14 @@
|
|||
},
|
||||
{
|
||||
"fieldname": "exception",
|
||||
"fieldtype": "Text",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "Exception",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-12 16:48:37.797232",
|
||||
"modified": "2023-01-23 12:45:53.997708",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Submission Queue",
|
||||
|
|
@ -102,6 +103,11 @@
|
|||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"if_owner": 1,
|
||||
"read": 1,
|
||||
"role": "All"
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@
|
|||
from urllib.parse import quote
|
||||
|
||||
from rq import get_current_job
|
||||
from rq.exceptions import NoSuchJobError
|
||||
from rq.job import Job
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
@ -13,7 +11,6 @@ from frappe.desk.doctype.notification_log.notification_log import enqueue_create
|
|||
from frappe.model.document import Document
|
||||
from frappe.monitor import add_data_to_monitor
|
||||
from frappe.utils import now, time_diff_in_seconds
|
||||
from frappe.utils.background_jobs import get_redis_conn
|
||||
from frappe.utils.data import cint
|
||||
|
||||
|
||||
|
|
@ -39,6 +36,7 @@ class SubmissionQueue(Document):
|
|||
frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
|
||||
|
||||
def insert(self, to_be_queued_doc: Document, action: str):
|
||||
self.status = "Queued"
|
||||
self.to_be_queued_doc = to_be_queued_doc
|
||||
self.action_for_queuing = action
|
||||
super().insert(ignore_permissions=True)
|
||||
|
|
@ -70,6 +68,7 @@ class SubmissionQueue(Document):
|
|||
def background_submission(self, to_be_queued_doc: Document, action_for_queuing: str):
|
||||
# Set the job id for that submission doctype
|
||||
self.update_job_id(get_current_job().id)
|
||||
|
||||
_action = action_for_queuing.lower()
|
||||
if _action == "update":
|
||||
_action = "submit"
|
||||
|
|
@ -85,7 +84,7 @@ class SubmissionQueue(Document):
|
|||
)
|
||||
values = {"status": "Finished"}
|
||||
except Exception:
|
||||
values = {"status": "Failed", "exception": frappe.get_traceback()}
|
||||
values = {"status": "Failed", "exception": frappe.get_traceback(with_context=True)}
|
||||
frappe.db.rollback()
|
||||
|
||||
values["ended_at"] = now()
|
||||
|
|
@ -96,22 +95,27 @@ class SubmissionQueue(Document):
|
|||
if submission_status == "Failed":
|
||||
doctype = self.doctype
|
||||
docname = self.name
|
||||
message = _("Submission of {0} {1} with action {2} failed")
|
||||
message = _("Action {0} failed on {1} {2}. View it {3}")
|
||||
else:
|
||||
doctype = self.ref_doctype
|
||||
docname = self.ref_docname
|
||||
message = _("Submission of {0} {1} with action {2} completed successfully")
|
||||
message = _("Action {0} completed successfully on {1} {2}. View it {3}")
|
||||
|
||||
message = message.format(
|
||||
frappe.bold(str(self.ref_doctype)), frappe.bold(self.ref_docname), frappe.bold(action)
|
||||
message_replacements = (
|
||||
frappe.bold(action),
|
||||
frappe.bold(str(self.ref_doctype)),
|
||||
frappe.bold(str(self.ref_docname)),
|
||||
)
|
||||
|
||||
time_diff = time_diff_in_seconds(now(), self.created_at)
|
||||
if cint(time_diff) <= 60:
|
||||
frappe.publish_realtime(
|
||||
"msgprint",
|
||||
{
|
||||
"message": message
|
||||
+ f". View it <a href='/app/{quote(doctype.lower().replace(' ', '-'))}/{quote(docname)}'><b>here</b></a>",
|
||||
"message": message.format(
|
||||
*message_replacements,
|
||||
f"<a href='/app/{quote(doctype.lower().replace(' ', '-'))}/{quote(docname)}'><b>here</b></a>",
|
||||
),
|
||||
"alert": True,
|
||||
"indicator": "red" if submission_status == "Failed" else "green",
|
||||
},
|
||||
|
|
@ -122,50 +126,27 @@ class SubmissionQueue(Document):
|
|||
"type": "Alert",
|
||||
"document_type": doctype,
|
||||
"document_name": docname,
|
||||
"subject": message,
|
||||
"subject": message.format(*message_replacements, "here"),
|
||||
}
|
||||
|
||||
notify_to = frappe.db.get_value("User", self.enqueued_by, fieldname="email")
|
||||
enqueue_create_notification([notify_to], notification_doc)
|
||||
|
||||
def _unlock_reference_doc(self):
|
||||
"""
|
||||
Only execute if self.job_id is defined.
|
||||
"""
|
||||
try:
|
||||
job = Job.fetch(self.job_id, connection=get_redis_conn())
|
||||
status = job.get_status(refresh=True)
|
||||
exc = job.exc_info
|
||||
except NoSuchJobError:
|
||||
exc = None
|
||||
status = "failed"
|
||||
|
||||
if status in ("queued", "started"):
|
||||
frappe.msgprint(_("Document in queue for execution!"))
|
||||
return
|
||||
|
||||
self.queued_doc.unlock()
|
||||
values = (
|
||||
{"status": "Finished"} if status == "finished" else {"status": "Failed", "exception": exc}
|
||||
)
|
||||
frappe.db.set_value(self.doctype, self.name, values, update_modified=False)
|
||||
frappe.msgprint(_("Document Unlocked"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def unlock_doc(self):
|
||||
# NOTE: this can lead to some weird unlocking/locking behaviours.
|
||||
# for example: hitting unlock on a submission could lead to unlocking of another submission
|
||||
# of the same reference document.
|
||||
|
||||
if self.status != "Queued" and not self.job_id:
|
||||
if self.status != "Queued":
|
||||
return
|
||||
|
||||
self._unlock_reference_doc()
|
||||
self.queued_doc.unlock()
|
||||
frappe.msgprint(_("Document Unlocked"))
|
||||
|
||||
|
||||
def queue_submission(doc: Document, action: str, alert: bool = True):
|
||||
queue = frappe.new_doc("Submission Queue")
|
||||
queue.state = "Queued"
|
||||
queue.ref_doctype = doc.doctype
|
||||
queue.ref_docname = doc.name
|
||||
queue.insert(doc, action)
|
||||
|
|
@ -185,9 +166,25 @@ def get_latest_submissions(doctype, docname):
|
|||
# NOTE: not used creation as orderby intentianlly as we have used update_modified=False everywhere
|
||||
# hence assuming modified will be equal to creation for submission queue documents
|
||||
|
||||
dt = "Submission Queue"
|
||||
filters = {"ref_doctype": doctype, "ref_docname": docname}
|
||||
return {
|
||||
"latest_submission": frappe.db.get_value(dt, filters),
|
||||
"latest_failed_submission": frappe.db.get_value(dt, filters | {"status": "Failed"}),
|
||||
}
|
||||
latest_submission = frappe.db.get_value(
|
||||
"Submission Queue",
|
||||
filters={"ref_doctype": doctype, "ref_docname": docname},
|
||||
fieldname=["name", "exception", "status"],
|
||||
)
|
||||
|
||||
out = None
|
||||
if latest_submission:
|
||||
out = {
|
||||
"latest_submission": latest_submission[0],
|
||||
"exc": format_tb(latest_submission[1]),
|
||||
"status": latest_submission[2],
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def format_tb(traceback: str | None = None):
|
||||
if not traceback:
|
||||
return
|
||||
|
||||
return traceback.strip().split("\n")[-1]
|
||||
|
|
|
|||
|
|
@ -30,13 +30,6 @@ frappe.ui.form.on("System Settings", {
|
|||
frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0);
|
||||
}
|
||||
},
|
||||
enable_prepared_report_auto_deletion: function (frm) {
|
||||
if (frm.doc.enable_prepared_report_auto_deletion) {
|
||||
if (!frm.doc.prepared_report_expiry_period) {
|
||||
frm.set_value("prepared_report_expiry_period", 7);
|
||||
}
|
||||
}
|
||||
},
|
||||
on_update: function (frm) {
|
||||
if (frappe.boot.time_zone && frappe.boot.time_zone.system !== frm.doc.time_zone) {
|
||||
// Clear cache after saving to refresh the values of boot.
|
||||
|
|
|
|||
|
|
@ -769,7 +769,6 @@
|
|||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
|
|
@ -793,4 +792,4 @@
|
|||
"states": [],
|
||||
"title_field": "full_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,11 +122,20 @@ class User(Document):
|
|||
now = frappe.flags.in_test or frappe.flags.in_install
|
||||
self.send_password_notification(self.__new_password)
|
||||
frappe.enqueue(
|
||||
"frappe.core.doctype.user.user.create_contact", user=self, ignore_mandatory=True, now=now
|
||||
"frappe.core.doctype.user.user.create_contact",
|
||||
user=self,
|
||||
ignore_mandatory=True,
|
||||
now=now,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
if self.name not in STANDARD_USERS and not self.user_image:
|
||||
frappe.enqueue("frappe.core.doctype.user.user.update_gravatar", name=self.name, now=now)
|
||||
frappe.enqueue(
|
||||
"frappe.core.doctype.user.user.update_gravatar",
|
||||
name=self.name,
|
||||
now=now,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
# Set user selected timezone
|
||||
if self.time_zone:
|
||||
|
|
@ -305,12 +314,10 @@ class User(Document):
|
|||
.from_(user_role_doctype)
|
||||
.select(user_doctype.name)
|
||||
.where(user_role_doctype.role == "System Manager")
|
||||
.where(user_doctype.docstatus < 2)
|
||||
.where(user_doctype.enabled == 1)
|
||||
.where(user_role_doctype.parent == user_doctype.name)
|
||||
.where(user_role_doctype.parent.notin(["Administrator", self.name]))
|
||||
.limit(1)
|
||||
.distinct()
|
||||
).run()
|
||||
|
||||
def get_fullname(self):
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ def create_user(email, *roles):
|
|||
|
||||
user = frappe.new_doc("User")
|
||||
user.email = email
|
||||
user.first_name = email.split("@")[0]
|
||||
user.first_name = email.split("@", 1)[0]
|
||||
|
||||
if not roles:
|
||||
roles = ("System Manager",)
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ def user_linked_with_permission_on_doctype(doc, user):
|
|||
|
||||
def apply_permissions_for_non_standard_user_type(doc, method=None):
|
||||
"""Create user permission for the non standard user type"""
|
||||
if not frappe.db.table_exists("User Type"):
|
||||
if not frappe.db.table_exists("User Type") or frappe.flags.in_migrate:
|
||||
return
|
||||
|
||||
user_types = frappe.cache().get_value(
|
||||
|
|
|
|||
|
|
@ -64,9 +64,9 @@ class Dashboard {
|
|||
let title = this.dashboard_name;
|
||||
if (!this.dashboard_name.toLowerCase().includes(__("dashboard"))) {
|
||||
// ensure dashboard title has "dashboard"
|
||||
title = __("{0} Dashboard", [title]);
|
||||
title = __("{0} Dashboard", [__(title)]);
|
||||
}
|
||||
this.page.set_title(title);
|
||||
this.page.set_title(__(title));
|
||||
this.set_dropdown();
|
||||
this.container.empty();
|
||||
this.refresh();
|
||||
|
|
|
|||
|
|
@ -320,7 +320,6 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
"report",
|
||||
"import",
|
||||
"export",
|
||||
"set_user_permissions",
|
||||
"share",
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,8 +62,8 @@ def get_roles_and_doctypes():
|
|||
roles_list = [{"label": _(d.get("name")), "value": d.get("name")} for d in roles]
|
||||
|
||||
return {
|
||||
"doctypes": sorted(doctypes_list, key=lambda d: d["label"]),
|
||||
"roles": sorted(roles_list, key=lambda d: d["label"]),
|
||||
"doctypes": sorted(doctypes_list, key=lambda d: d["label"].casefold()),
|
||||
"roles": sorted(roles_list, key=lambda d: d["label"].casefold()),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,25 +7,24 @@
|
|||
"allow_print": 0,
|
||||
"apply_document_permissions": 0,
|
||||
"breadcrumbs": "[{\"title\": _(\"My Account\"), \"route\": \"me\"}]",
|
||||
"client_script": "frappe.web_form.after_load = () => {\n if (window.location.pathname.endsWith(\"/new\") && frappe.session.user) {\n let current_path = window.location.href;\n window.location.href = current_path.replace(\"/new\", \"/\" + frappe.session.user);\n }\n}",
|
||||
"creation": "2016-09-19 05:16:59.242754",
|
||||
"doc_type": "User",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Form",
|
||||
"idx": 0,
|
||||
"introduction_text": "",
|
||||
"is_multi_step_form": 0,
|
||||
"is_standard": 1,
|
||||
"list_columns": [],
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2022-07-18 16:51:19.796411",
|
||||
"modified": "2023-01-18 10:26:26.766414",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "edit-profile",
|
||||
"owner": "Administrator",
|
||||
"published": 1,
|
||||
"route": "update-profile",
|
||||
"route_to_success_link": 0,
|
||||
"show_attachments": 0,
|
||||
"show_list": 0,
|
||||
"show_sidebar": 0,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,18 @@ class CustomField(Document):
|
|||
self.name = self.dt + "-" + self.fieldname
|
||||
|
||||
def set_fieldname(self):
|
||||
restricted = (
|
||||
"name",
|
||||
"parent",
|
||||
"creation",
|
||||
"modified",
|
||||
"modified_by",
|
||||
"parentfield",
|
||||
"parenttype",
|
||||
"file_list",
|
||||
"flags",
|
||||
"docstatus",
|
||||
)
|
||||
if not self.fieldname:
|
||||
label = self.label
|
||||
if not label:
|
||||
|
|
@ -34,6 +46,9 @@ class CustomField(Document):
|
|||
# fieldnames should be lowercase
|
||||
self.fieldname = self.fieldname.lower()
|
||||
|
||||
if self.fieldname in restricted:
|
||||
self.fieldname = self.fieldname + "1"
|
||||
|
||||
def before_insert(self):
|
||||
self.set_fieldname()
|
||||
|
||||
|
|
@ -142,7 +157,7 @@ def get_fields_label(doctype=None):
|
|||
return frappe.msgprint(_("Custom Fields can only be added to a standard DocType."))
|
||||
|
||||
return [
|
||||
{"value": df.fieldname or "", "label": _(df.label or "")}
|
||||
{"value": df.fieldname or "", "label": _(df.label) if df.label else ""}
|
||||
for df in frappe.get_meta(doctype).get("fields")
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -263,6 +263,10 @@ frappe.ui.form.on("Customize Form Field", {
|
|||
f.is_custom_field = true;
|
||||
frm.trigger("setup_default_views");
|
||||
},
|
||||
|
||||
form_render(frm, doctype, docname) {
|
||||
frm.trigger("setup_fetch_from_fields", doctype, docname);
|
||||
},
|
||||
});
|
||||
|
||||
// can't delete standard links
|
||||
|
|
@ -310,22 +314,59 @@ frappe.ui.form.on("DocType State", {
|
|||
},
|
||||
});
|
||||
|
||||
frappe.customize_form.set_primary_action = function (frm) {
|
||||
frm.page.set_primary_action(__("Update"), function () {
|
||||
if (frm.doc.doc_type) {
|
||||
return frm.call({
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
btn: frm.page.btn_primary,
|
||||
method: "save_customization",
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.customize_form.clear_locals_and_refresh(frm);
|
||||
frm.script_manager.trigger("doc_type");
|
||||
}
|
||||
},
|
||||
});
|
||||
frappe.customize_form.validate_fieldnames = async function (frm) {
|
||||
for (let i = 0; i < frm.doc.fields.length; i++) {
|
||||
let field = frm.doc.fields[i];
|
||||
|
||||
let fieldname = field.label && frappe.model.scrub(field.label).toLowerCase();
|
||||
if (
|
||||
field.label &&
|
||||
!field.fieldname &&
|
||||
in_list(frappe.model.restricted_fields, fieldname)
|
||||
) {
|
||||
let message = __(
|
||||
"For field <b>{0}</b> in row <b>{1}</b>, fieldname <b>{2}</b> is restricted it will be renamed as <b>{2}1</b>. Do you want to continue?",
|
||||
[field.label, field.idx, fieldname]
|
||||
);
|
||||
await pause_to_confirm(message);
|
||||
}
|
||||
}
|
||||
|
||||
function pause_to_confirm(message) {
|
||||
return new Promise((resolve) => {
|
||||
frappe.confirm(
|
||||
message,
|
||||
() => resolve(),
|
||||
() => {
|
||||
frm.page.btn_primary.prop("disabled", false);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
frappe.customize_form.save_customization = function (frm) {
|
||||
if (frm.doc.doc_type) {
|
||||
return frm.call({
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __("Saving Customization..."),
|
||||
btn: frm.page.btn_primary,
|
||||
method: "save_customization",
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.customize_form.clear_locals_and_refresh(frm);
|
||||
frm.script_manager.trigger("doc_type");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
frappe.customize_form.set_primary_action = function (frm) {
|
||||
frm.page.set_primary_action(__("Update"), async () => {
|
||||
await this.validate_fieldnames(frm);
|
||||
this.save_customization(frm);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list(['Section Break', 'Column Break', 'Tab Break'], doc.fieldtype)",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
|
|
@ -467,7 +468,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-30 14:25:50.649449",
|
||||
"modified": "2023-02-20 12:07:40.242470",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-01 03:22:33.973058",
|
||||
"modified": "2023-02-14 17:53:24.486171",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "DocType Layout",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Guest"
|
||||
"role": "All"
|
||||
}
|
||||
],
|
||||
"route": "doctype-layout",
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@
|
|||
"report": 1,
|
||||
"export": 1,
|
||||
"import": 0,
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"print": 1,
|
||||
"email": 1,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@
|
|||
"report": 1,
|
||||
"export": 1,
|
||||
"import": 0,
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"print": 1,
|
||||
"email": 1,
|
||||
|
|
|
|||
0
frappe/custom/report/__init__.py
Normal file
0
frappe/custom/report/__init__.py
Normal file
0
frappe/custom/report/audit_system_hooks/__init__.py
Normal file
0
frappe/custom/report/audit_system_hooks/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (c) 2023, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Audit System Hooks"] = {
|
||||
filters: [],
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2023-01-25 15:02:21.896117",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letter_head": "",
|
||||
"modified": "2023-01-31 14:53:37.778576",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Audit System Hooks",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "",
|
||||
"ref_doctype": "Property Setter",
|
||||
"report_name": "Audit System Hooks",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "System Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# Copyright (c) 2023, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
return get_columns(), get_data()
|
||||
|
||||
|
||||
def get_columns():
|
||||
values_field_type = "Data" # TODO: better text wrapping in reportview
|
||||
columns = [
|
||||
{"label": "Hook name", "fieldname": "hook_name", "fieldtype": "Data", "width": 200},
|
||||
{"label": "Hook key (optional)", "fieldname": "hook_key", "fieldtype": "Data", "width": 200},
|
||||
{"label": "Hook Values (resolved)", "fieldname": "hook_values", "fieldtype": values_field_type},
|
||||
]
|
||||
|
||||
# Each app is shown in order as a column
|
||||
installed_apps = frappe.get_installed_apps(_ensure_on_bench=True)
|
||||
columns += [
|
||||
{"label": app, "fieldname": app, "fieldtype": values_field_type} for app in installed_apps
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def get_data():
|
||||
hooks = frappe.get_hooks()
|
||||
installed_apps = frappe.get_installed_apps(_ensure_on_bench=True)
|
||||
|
||||
def fmt_hook_values(v):
|
||||
"""Improve readability by discarding falsy values and removing containers when only 1
|
||||
value is in container"""
|
||||
if not v:
|
||||
return ""
|
||||
|
||||
v = delist(v)
|
||||
|
||||
if isinstance(v, (dict, list)):
|
||||
try:
|
||||
return frappe.as_json(v)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return str(v)
|
||||
|
||||
data = []
|
||||
for hook, values in hooks.items():
|
||||
if isinstance(values, dict):
|
||||
for k, v in values.items():
|
||||
row = {"hook_name": hook, "hook_key": fmt_hook_values(k), "hook_values": fmt_hook_values(v)}
|
||||
for app in installed_apps:
|
||||
if app_hooks := delist(frappe.get_hooks(hook, app_name=app)):
|
||||
row[app] = fmt_hook_values(app_hooks.get(k))
|
||||
data.append(row)
|
||||
else:
|
||||
row = {"hook_name": hook, "hook_values": fmt_hook_values(values)}
|
||||
for app in installed_apps:
|
||||
row[app] = fmt_hook_values(frappe.get_hooks(hook, app_name=app))
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def delist(val):
|
||||
if isinstance(val, list) and len(val) == 1:
|
||||
return val[0]
|
||||
return val
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Copyright (c) 2022, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from frappe.custom.report.audit_system_hooks.audit_system_hooks import execute
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestAuditSystemHooksReport(FrappeTestCase):
|
||||
def test_basic_query(self):
|
||||
_, data = execute()
|
||||
for row in data:
|
||||
if row.get("hook_name") == "app_name":
|
||||
self.assertEqual(row.get("hook_values"), "frappe")
|
||||
break
|
||||
else:
|
||||
self.fail("Failed to generate hooks report")
|
||||
|
|
@ -20,6 +20,7 @@ import frappe.defaults
|
|||
import frappe.model.meta
|
||||
from frappe import _
|
||||
from frappe.database.utils import (
|
||||
DefaultOrderBy,
|
||||
EmptyQueryValues,
|
||||
FallBackDateTimeStr,
|
||||
LazyMogrify,
|
||||
|
|
@ -221,7 +222,7 @@ class Database:
|
|||
self._cursor.execute(query, values)
|
||||
except Exception as e:
|
||||
if self.is_syntax_error(e):
|
||||
frappe.errprint(f"Syntax error in query:\n{query} {values}")
|
||||
frappe.errprint(f"Syntax error in query:\n{query} {values or ''}")
|
||||
|
||||
elif self.is_deadlocked(e):
|
||||
raise frappe.QueryDeadlockError(e) from e
|
||||
|
|
@ -232,7 +233,7 @@ class Database:
|
|||
elif self.is_read_only_mode_error(e):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Site is running in read only mode, this action can not be performed right now. Please try again later."
|
||||
"Site is running in read only mode for maintenance or site update, this action can not be performed right now. Please try again later."
|
||||
),
|
||||
title=_("In Read Only Mode"),
|
||||
exc=frappe.InReadOnlyMode,
|
||||
|
|
@ -422,7 +423,7 @@ class Database:
|
|||
ignore=None,
|
||||
as_dict=False,
|
||||
debug=False,
|
||||
order_by="KEEP_DEFAULT_ORDERING",
|
||||
order_by=DefaultOrderBy,
|
||||
cache=False,
|
||||
for_update=False,
|
||||
*,
|
||||
|
|
@ -492,7 +493,7 @@ class Database:
|
|||
ignore=None,
|
||||
as_dict=False,
|
||||
debug=False,
|
||||
order_by="KEEP_DEFAULT_ORDERING",
|
||||
order_by=DefaultOrderBy,
|
||||
update=None,
|
||||
cache=False,
|
||||
for_update=False,
|
||||
|
|
@ -551,7 +552,7 @@ class Database:
|
|||
if (filters is not None) and (filters != doctype or doctype == "DocType"):
|
||||
try:
|
||||
if order_by:
|
||||
order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
|
||||
order_by = "modified" if order_by == DefaultOrderBy else order_by
|
||||
out = self._get_values_from_table(
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
|
|
@ -622,7 +623,7 @@ class Database:
|
|||
return [map(values.get, fields)]
|
||||
|
||||
else:
|
||||
r = frappe.qb.engine.get_query(
|
||||
r = frappe.qb.get_query(
|
||||
"Singles",
|
||||
filters={"field": ("in", tuple(fields)), "doctype": doctype},
|
||||
fields=["field", "value"],
|
||||
|
|
@ -655,7 +656,7 @@ class Database:
|
|||
# Get coulmn and value of the single doctype Accounts Settings
|
||||
account_settings = frappe.db.get_singles_dict("Accounts Settings")
|
||||
"""
|
||||
queried_result = frappe.qb.engine.get_query(
|
||||
queried_result = frappe.qb.get_query(
|
||||
"Singles",
|
||||
filters={"doctype": doctype},
|
||||
fields=["field", "value"],
|
||||
|
|
@ -761,7 +762,7 @@ class Database:
|
|||
if cache and fieldname in self.value_cache[doctype]:
|
||||
return self.value_cache[doctype][fieldname]
|
||||
|
||||
val = frappe.qb.engine.get_query(
|
||||
val = frappe.qb.get_query(
|
||||
table="Singles",
|
||||
filters={"doctype": doctype, "field": fieldname},
|
||||
fields="value",
|
||||
|
|
@ -772,7 +773,9 @@ class Database:
|
|||
|
||||
if not df:
|
||||
frappe.throw(
|
||||
_("Invalid field name: {0}").format(frappe.bold(fieldname)), self.InvalidColumnName
|
||||
_("Field {0} does not exist on {1}").format(
|
||||
frappe.bold(fieldname), frappe.bold(doctype), self.InvalidColumnName
|
||||
)
|
||||
)
|
||||
|
||||
val = cast_fieldtype(df.fieldtype, val)
|
||||
|
|
@ -801,16 +804,16 @@ class Database:
|
|||
distinct=False,
|
||||
limit=None,
|
||||
):
|
||||
query = frappe.qb.engine.get_query(
|
||||
query = frappe.qb.get_query(
|
||||
table=doctype,
|
||||
filters=filters,
|
||||
orderby=order_by,
|
||||
order_by=order_by,
|
||||
for_update=for_update,
|
||||
fields=fields,
|
||||
distinct=distinct,
|
||||
limit=limit,
|
||||
)
|
||||
if fields == "*" and not isinstance(fields, (list, tuple)) and not isinstance(fields, Criterion):
|
||||
if isinstance(fields, str) and fields == "*":
|
||||
as_dict = True
|
||||
|
||||
return query.run(as_dict=as_dict, debug=debug, update=update, run=run, pluck=pluck)
|
||||
|
|
@ -830,15 +833,14 @@ class Database:
|
|||
as_dict=False,
|
||||
):
|
||||
if names := list(filter(None, names)):
|
||||
return frappe.qb.engine.get_query(
|
||||
return frappe.qb.get_query(
|
||||
doctype,
|
||||
fields=field,
|
||||
filters=names,
|
||||
order_by=order_by,
|
||||
pluck=pluck,
|
||||
distinct=distinct,
|
||||
limit=limit,
|
||||
).run(debug=debug, run=run, as_dict=as_dict)
|
||||
).run(debug=debug, run=run, as_dict=as_dict, pluck=pluck)
|
||||
return {}
|
||||
|
||||
def set_value(
|
||||
|
|
@ -887,7 +889,7 @@ class Database:
|
|||
field, val, modified=modified, modified_by=modified_by, update_modified=update_modified
|
||||
)
|
||||
|
||||
query = frappe.qb.engine.build_conditions(table=dt, filters=dn, update=True)
|
||||
query = frappe.qb.get_query(table=dt, filters=dn, update=True)
|
||||
|
||||
if isinstance(dn, str):
|
||||
frappe.clear_document_cache(dt, dn)
|
||||
|
|
@ -1044,7 +1046,7 @@ class Database:
|
|||
dt = dt.copy() # don't modify the original dict
|
||||
dt, dn = dt.pop("doctype"), dt
|
||||
|
||||
return self.get_value(dt, dn, ignore=True, cache=cache)
|
||||
return self.get_value(dt, dn, ignore=True, cache=cache, order_by=None)
|
||||
|
||||
def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True):
|
||||
"""Returns `COUNT(*)` for given DocType and filters."""
|
||||
|
|
@ -1052,9 +1054,9 @@ class Database:
|
|||
cache_count = frappe.cache().get_value(f"doctype:count:{dt}")
|
||||
if cache_count is not None:
|
||||
return cache_count
|
||||
count = frappe.qb.engine.get_query(
|
||||
table=dt, filters=filters, fields=Count("*"), distinct=distinct
|
||||
).run(debug=debug)[0][0]
|
||||
count = frappe.qb.get_query(table=dt, filters=filters, fields=Count("*"), distinct=distinct).run(
|
||||
debug=debug
|
||||
)[0][0]
|
||||
if not filters and cache:
|
||||
frappe.cache().set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
|
||||
return count
|
||||
|
|
@ -1068,13 +1070,7 @@ class Database:
|
|||
if not datetime:
|
||||
return FallBackDateTimeStr
|
||||
|
||||
if isinstance(datetime, str):
|
||||
if ":" not in datetime:
|
||||
datetime = datetime + " 00:00:00.000000"
|
||||
else:
|
||||
datetime = datetime.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
|
||||
return datetime
|
||||
return get_datetime(datetime).strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
|
||||
def get_creation_count(self, doctype, minutes):
|
||||
"""Get count of records created in the last x minutes"""
|
||||
|
|
@ -1195,7 +1191,7 @@ class Database:
|
|||
Doctype name can be passed directly, it will be pre-pended with `tab`.
|
||||
"""
|
||||
filters = filters or kwargs.get("conditions")
|
||||
query = frappe.qb.engine.build_conditions(table=doctype, filters=filters).delete()
|
||||
query = frappe.qb.get_query(table=doctype, filters=filters, delete=True)
|
||||
if "debug" not in kwargs:
|
||||
kwargs["debug"] = debug
|
||||
return query.run(**kwargs)
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ CREATE TABLE `tabDocPerm` (
|
|||
-- Table structure for table `tabDocType Action`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `tabDocType Action`;
|
||||
CREATE TABLE `tabDocType Action` (
|
||||
`name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`creation` datetime(6) DEFAULT NULL,
|
||||
|
|
@ -137,9 +138,10 @@ CREATE TABLE `tabDocType Action` (
|
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
--
|
||||
-- Table structure for table `tabDocType Action`
|
||||
-- Table structure for table `tabDocType Link`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `tabDocType Link`;
|
||||
CREATE TABLE `tabDocType Link` (
|
||||
`name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`creation` datetime(6) DEFAULT NULL,
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ class MariaDBTable(DBTable):
|
|||
if not frappe.db.get_column_index(self.table_name, col.fieldname, unique=False):
|
||||
add_index_query.append(f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)")
|
||||
|
||||
for col in self.drop_index + self.drop_unique:
|
||||
for col in {*self.drop_index, *self.drop_unique}:
|
||||
if col.fieldname == "name":
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ def get_mariadb_version(version_string: str = ""):
|
|||
# MariaDB classifies their versions as Major (1st and 2nd number), and Minor (3rd number)
|
||||
# Example: Version 10.3.13 is Major Version = 10.3, Minor Version = 13
|
||||
version_string = version_string or get_mariadb_variables().get("version")
|
||||
version = version_string.split("-")[0]
|
||||
version = version_string.split("-", 1)[0]
|
||||
return version.rsplit(".", 1)
|
||||
|
||||
|
||||
|
|
|
|||
138
frappe/database/operator_map.py
Normal file
138
frappe/database/operator_map.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import operator
|
||||
from typing import Callable
|
||||
|
||||
import frappe
|
||||
from frappe.database.utils import NestedSetHierarchy
|
||||
from frappe.model.db_query import get_timespan_date_range
|
||||
from frappe.query_builder import Field
|
||||
|
||||
|
||||
def like(key: Field, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `LIKE`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `LIKE`
|
||||
"""
|
||||
return key.like(value)
|
||||
|
||||
|
||||
def func_in(key: Field, value: list | tuple) -> frappe.qb:
|
||||
"""Wrapper method for `IN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (Union[int, str]): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `IN`
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
value = value.split(",")
|
||||
return key.isin(value)
|
||||
|
||||
|
||||
def not_like(key: Field, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `NOT LIKE`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `NOT LIKE`
|
||||
"""
|
||||
return key.not_like(value)
|
||||
|
||||
|
||||
def func_not_in(key: Field, value: list | tuple | str):
|
||||
"""Wrapper method for `NOT IN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (Union[int, str]): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `NOT IN`
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
value = value.split(",")
|
||||
return key.notin(value)
|
||||
|
||||
|
||||
def func_regex(key: Field, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `REGEX`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `REGEX`
|
||||
"""
|
||||
return key.regex(value)
|
||||
|
||||
|
||||
def func_between(key: Field, value: list | tuple) -> frappe.qb:
|
||||
"""Wrapper method for `BETWEEN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (Union[int, str]): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `BETWEEN`
|
||||
"""
|
||||
return key[slice(*value)]
|
||||
|
||||
|
||||
def func_is(key, value):
|
||||
"Wrapper for IS"
|
||||
return key.isnotnull() if value.lower() == "set" else key.isnull()
|
||||
|
||||
|
||||
def func_timespan(key: Field, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `TIMESPAN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `TIMESPAN`
|
||||
"""
|
||||
|
||||
return func_between(key, get_timespan_date_range(value))
|
||||
|
||||
|
||||
# default operators
|
||||
OPERATOR_MAP: dict[str, Callable] = {
|
||||
"+": operator.add,
|
||||
"=": operator.eq,
|
||||
"-": operator.sub,
|
||||
"!=": operator.ne,
|
||||
"<": operator.lt,
|
||||
">": operator.gt,
|
||||
"<=": operator.le,
|
||||
"=<": operator.le,
|
||||
">=": operator.ge,
|
||||
"=>": operator.ge,
|
||||
"/": operator.truediv,
|
||||
"*": operator.mul,
|
||||
"in": func_in,
|
||||
"not in": func_not_in,
|
||||
"like": like,
|
||||
"not like": not_like,
|
||||
"regex": func_regex,
|
||||
"between": func_between,
|
||||
"is": func_is,
|
||||
"timespan": func_timespan,
|
||||
"nested_set": NestedSetHierarchy,
|
||||
# TODO: Add support for custom operators (WIP) - via filters_config hooks
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -17,7 +17,7 @@ QueryValues = tuple | list | dict | NoneType
|
|||
|
||||
EmptyQueryValues = object()
|
||||
FallBackDateTimeStr = "0001-01-01 00:00:00.000000"
|
||||
|
||||
DefaultOrderBy = "KEEP_DEFAULT_ORDERING"
|
||||
NestedSetHierarchy = (
|
||||
"ancestors of",
|
||||
"descendants of",
|
||||
|
|
@ -34,6 +34,14 @@ def is_pypika_function_object(field: str) -> bool:
|
|||
return getattr(field, "__module__", None) == "pypika.functions" or isinstance(field, Function)
|
||||
|
||||
|
||||
def get_doctype_name(table_name: str) -> str:
|
||||
if table_name.startswith(("tab", "`tab", '"tab')):
|
||||
table_name = table_name.replace("tab", "", 1)
|
||||
table_name = table_name.replace("`", "")
|
||||
table_name = table_name.replace('"', "")
|
||||
return table_name
|
||||
|
||||
|
||||
class LazyString:
|
||||
def _setup(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
|
|
|||
|
|
@ -158,14 +158,11 @@ class Workspace:
|
|||
|
||||
def build_workspace(self):
|
||||
self.cards = {"items": self.get_links()}
|
||||
|
||||
self.charts = {"items": self.get_charts()}
|
||||
|
||||
self.shortcuts = {"items": self.get_shortcuts()}
|
||||
|
||||
self.onboardings = {"items": self.get_onboardings()}
|
||||
|
||||
self.quick_lists = {"items": self.get_quick_lists()}
|
||||
self.number_cards = {"items": self.get_number_cards()}
|
||||
|
||||
def _doctype_contains_a_record(self, name):
|
||||
exists = self.table_counts.get(name, False)
|
||||
|
|
@ -332,6 +329,21 @@ class Workspace:
|
|||
|
||||
return steps
|
||||
|
||||
@handle_not_exist
|
||||
def get_number_cards(self):
|
||||
all_number_cards = []
|
||||
if frappe.has_permission("Number Card", throw=False):
|
||||
number_cards = self.doc.number_cards
|
||||
for number_card in number_cards:
|
||||
if frappe.has_permission("Number Card", doc=number_card.number_card_name):
|
||||
# Translate label
|
||||
number_card.label = (
|
||||
_(number_card.label) if number_card.label else _(number_card.number_card_name)
|
||||
)
|
||||
all_number_cards.append(number_card)
|
||||
|
||||
return all_number_cards
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
|
|
@ -354,6 +366,7 @@ def get_desktop_page(page):
|
|||
"cards": workspace.cards,
|
||||
"onboardings": workspace.onboardings,
|
||||
"quick_lists": workspace.quick_lists,
|
||||
"number_cards": workspace.number_cards,
|
||||
}
|
||||
except DoesNotExistError:
|
||||
frappe.log_error("Workspace Missing")
|
||||
|
|
@ -482,6 +495,10 @@ def save_new_widget(doc, page, blocks, new_widgets):
|
|||
doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts"))
|
||||
if widgets.quick_list:
|
||||
doc.quick_lists.extend(new_widget(widgets.quick_list, "Workspace Quick List", "quick_lists"))
|
||||
if widgets.number_card:
|
||||
doc.number_cards.extend(
|
||||
new_widget(widgets.number_card, "Workspace Number Card", "number_cards")
|
||||
)
|
||||
if widgets.card:
|
||||
doc.build_links_table_from_card(widgets.card)
|
||||
|
||||
|
|
@ -511,12 +528,12 @@ def save_new_widget(doc, page, blocks, new_widgets):
|
|||
def clean_up(original_page, blocks):
|
||||
page_widgets = {}
|
||||
|
||||
for wid in ["shortcut", "card", "chart", "quick_list"]:
|
||||
for wid in ["shortcut", "card", "chart", "quick_list", "number_card"]:
|
||||
# get list of widget's name from blocks
|
||||
page_widgets[wid] = [x["data"][wid + "_name"] for x in loads(blocks) if x["type"] == wid]
|
||||
|
||||
# shortcut, chart & quick_list cleanup
|
||||
for wid in ["shortcut", "chart", "quick_list"]:
|
||||
# shortcut, chart, quick_list & number_card cleanup
|
||||
for wid in ["shortcut", "chart", "quick_list", "number_card"]:
|
||||
updated_widgets = []
|
||||
original_page.get(wid + "s").reverse()
|
||||
|
||||
|
|
|
|||
|
|
@ -278,15 +278,14 @@ def get_group_by_chart_config(chart, filters):
|
|||
group_by_field = chart.group_by_based_on
|
||||
doctype = chart.document_type
|
||||
|
||||
data = frappe.db.get_list(
|
||||
data = frappe.get_list(
|
||||
doctype,
|
||||
fields=[
|
||||
f"{group_by_field} as name",
|
||||
"{aggregate_function}({value_field}) as count".format(
|
||||
aggregate_function=aggregate_function, value_field=value_field
|
||||
),
|
||||
f"{aggregate_function}({value_field}) as count",
|
||||
],
|
||||
filters=filters,
|
||||
parent_doctype=chart.parent_document_type,
|
||||
group_by=group_by_field,
|
||||
order_by="count desc",
|
||||
ignore_ifnull=True,
|
||||
|
|
|
|||
|
|
@ -306,8 +306,8 @@ def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[
|
|||
)
|
||||
|
||||
# process recurring events
|
||||
start = start.split(" ")[0]
|
||||
end = end.split(" ")[0]
|
||||
start = start.split(" ", 1)[0]
|
||||
end = end.split(" ", 1)[0]
|
||||
add_events = []
|
||||
remove_events = []
|
||||
|
||||
|
|
@ -315,7 +315,7 @@ def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[
|
|||
new_event = e.copy()
|
||||
|
||||
enddate = (
|
||||
add_days(date, int(date_diff(e.ends_on.split(" ")[0], e.starts_on.split(" ")[0])))
|
||||
add_days(date, int(date_diff(e.ends_on.split(" ", 1)[0], e.starts_on.split(" ", 1)[0])))
|
||||
if (e.starts_on and e.ends_on)
|
||||
else date
|
||||
)
|
||||
|
|
@ -337,8 +337,8 @@ def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[
|
|||
repeat = "3000-01-01" if cstr(e.repeat_till) == "" else e.repeat_till
|
||||
|
||||
if e.repeat_on == "Yearly":
|
||||
start_year = cint(start.split("-")[0])
|
||||
end_year = cint(end.split("-")[0])
|
||||
start_year = cint(start.split("-", 1)[0])
|
||||
end_year = cint(end.split("-", 1)[0])
|
||||
|
||||
# creates a string with date (27) and month (07) eg: 07-27
|
||||
event_start = "-".join(event_start.split("-")[1:])
|
||||
|
|
@ -357,12 +357,13 @@ def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[
|
|||
|
||||
if e.repeat_on == "Monthly":
|
||||
# creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27
|
||||
date = start.split("-")[0] + "-" + start.split("-")[1] + "-" + event_start.split("-")[2]
|
||||
year, month = start.split("-", maxsplit=2)[:2]
|
||||
date = f"{year}-{month}-" + event_start.split("-", maxsplit=3)[2]
|
||||
|
||||
# last day of month issue, start from prev month!
|
||||
try:
|
||||
getdate(date)
|
||||
except ValueError:
|
||||
except Exception:
|
||||
date = date.split("-")
|
||||
date = date[0] + "-" + str(cint(date[1]) - 1) + "-" + date[2]
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"disable_count",
|
||||
"disable_comment_count",
|
||||
"disable_sidebar_stats",
|
||||
"disable_auto_refresh",
|
||||
"total_fields",
|
||||
|
|
@ -49,13 +50,20 @@
|
|||
"hidden": 1,
|
||||
"label": "Fields",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_comment_count",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Comment Count"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-05-12 18:27:15.568199",
|
||||
"modified": "2023-02-14 14:46:43.764229",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "List View Settings",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -72,5 +80,6 @@
|
|||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ def mark_as_seen(note):
|
|||
note = frappe.get_doc("Note", note)
|
||||
if frappe.session.user not in [d.user for d in note.seen_by]:
|
||||
note.append("seen_by", {"user": frappe.session.user})
|
||||
note.save(ignore_version=True)
|
||||
note.save(ignore_version=True, ignore_permissions=True)
|
||||
|
||||
|
||||
def get_permission_query_conditions(user):
|
||||
|
|
|
|||
|
|
@ -92,4 +92,7 @@ def get_permission_query_conditions(user):
|
|||
|
||||
@frappe.whitelist()
|
||||
def set_seen_value(value, user):
|
||||
if frappe.flags.read_only:
|
||||
return
|
||||
|
||||
frappe.db.set_value("Notification Settings", user, "seen", value, update_modified=False)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue