Merge branch 'develop' into test_address_template
This commit is contained in:
commit
0df4bd27fb
125 changed files with 1966 additions and 1319 deletions
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 .
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -1075,25 +1074,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
|
||||
|
|
@ -1106,14 +1090,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:
|
||||
|
|
@ -1139,12 +1116,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")
|
||||
|
||||
|
|
@ -1469,14 +1445,12 @@ def _load_app_hooks(app_name: str | None = None):
|
|||
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):
|
||||
|
|
@ -1592,7 +1566,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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
)
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -499,7 +499,7 @@ def parse_email(communication, email_strings):
|
|||
if email_string:
|
||||
for email in email_string.split(","):
|
||||
if delimiter in email:
|
||||
email = email.split("@")[0]
|
||||
email = email.split("@", 1)[0]
|
||||
email_local_parts = email.split(delimiter)
|
||||
if not len(email_local_parts) == 3:
|
||||
continue
|
||||
|
|
@ -521,7 +521,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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -1133,7 +1135,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 +1348,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)):
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -142,7 +142,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")
|
||||
]
|
||||
|
||||
|
|
|
|||
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-25 15:03:31.263337",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Audit System Hooks",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "",
|
||||
"ref_doctype": "System Settings",
|
||||
"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")
|
||||
|
|
@ -622,7 +622,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 +655,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 +761,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 +772,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,10 +803,10 @@ 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,
|
||||
|
|
@ -830,15 +832,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 +888,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)
|
||||
|
|
@ -1052,9 +1053,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
|
||||
|
|
@ -1195,7 +1196,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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,7 +357,8 @@ 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:
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
|
|||
if txt:
|
||||
search_conditions = [numberCard[field].like(f"%{txt}%") for field in searchfields]
|
||||
|
||||
condition_query = frappe.qb.engine.build_conditions(doctype, filters)
|
||||
condition_query = frappe.qb.get_query(doctype, filters=filters)
|
||||
|
||||
return (
|
||||
condition_query.select(numberCard.name, numberCard.label, numberCard.document_type)
|
||||
|
|
|
|||
|
|
@ -28,14 +28,10 @@ def getdoc(doctype, name, user=None):
|
|||
if not (doctype and name):
|
||||
raise Exception("doctype and name required!")
|
||||
|
||||
if not name:
|
||||
name = doctype
|
||||
|
||||
if not is_virtual_doctype(doctype) and not frappe.db.exists(doctype, name):
|
||||
return []
|
||||
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
run_onload(doc)
|
||||
|
||||
if not doc.has_permission("read"):
|
||||
frappe.flags.error_message = _("Insufficient Permission for {0}").format(
|
||||
|
|
@ -43,6 +39,8 @@ def getdoc(doctype, name, user=None):
|
|||
)
|
||||
raise frappe.PermissionError(("read", doctype, name))
|
||||
|
||||
|
||||
run_onload(doc)
|
||||
doc.apply_fieldlevel_read_permissions()
|
||||
|
||||
# add file list
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import io
|
|||
import os
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.build import scrub_html_template
|
||||
from frappe.model.meta import Meta
|
||||
from frappe.model.utils import render_include
|
||||
from frappe.modules import get_module_path, load_doctype_module, scrub
|
||||
from frappe.translate import extract_messages_from_code, make_dict_from_messages
|
||||
from frappe.utils import get_html_format
|
||||
from frappe.utils.data import get_link_to_form
|
||||
|
||||
ASSET_KEYS = (
|
||||
"__js",
|
||||
|
|
@ -50,7 +52,7 @@ def get_meta(doctype, cached=True):
|
|||
|
||||
class FormMeta(Meta):
|
||||
def __init__(self, doctype):
|
||||
super().__init__(doctype)
|
||||
self.__dict__.update(frappe.get_meta(doctype).__dict__)
|
||||
self.load_assets()
|
||||
|
||||
def load_assets(self):
|
||||
|
|
@ -132,7 +134,7 @@ class FormMeta(Meta):
|
|||
for fname in os.listdir(path):
|
||||
if fname.endswith(".html"):
|
||||
with open(os.path.join(path, fname), encoding="utf-8") as f:
|
||||
templates[fname.split(".")[0]] = scrub_html_template(f.read())
|
||||
templates[fname.split(".", 1)[0]] = scrub_html_template(f.read())
|
||||
|
||||
self.set("__templates", templates or None)
|
||||
|
||||
|
|
@ -184,19 +186,40 @@ class FormMeta(Meta):
|
|||
"""add search fields found in the doctypes indicated by link fields' options"""
|
||||
for df in self.get("fields", {"fieldtype": "Link", "options": ["!=", "[Select]"]}):
|
||||
if df.options:
|
||||
search_fields = frappe.get_meta(df.options).search_fields
|
||||
try:
|
||||
search_fields = frappe.get_meta(df.options).search_fields
|
||||
except frappe.DoesNotExistError:
|
||||
self._show_missing_doctype_msg(df)
|
||||
|
||||
if search_fields:
|
||||
search_fields = search_fields.split(",")
|
||||
df.search_fields = [sf.strip() for sf in search_fields]
|
||||
|
||||
def _show_missing_doctype_msg(self, df):
|
||||
# A link field is referring to non-existing doctype, this usually happens when
|
||||
# customizations are removed or some custom app is removed but hasn't cleaned
|
||||
# up after itself.
|
||||
frappe.clear_last_message()
|
||||
|
||||
msg = _("Field {0} is referring to non-existing doctype {1}.").format(
|
||||
frappe.bold(df.fieldname), frappe.bold(df.options)
|
||||
)
|
||||
|
||||
if df.get("is_custom_field"):
|
||||
custom_field_link = get_link_to_form("Custom Field", df.name)
|
||||
msg += " " + _("Please delete the field from {2} or add the required doctype.").format(
|
||||
custom_field_link
|
||||
)
|
||||
|
||||
frappe.throw(msg, title=_("Missing DocType"))
|
||||
|
||||
def add_linked_document_type(self):
|
||||
for df in self.get("fields", {"fieldtype": "Link"}):
|
||||
if df.options:
|
||||
try:
|
||||
df.linked_document_type = frappe.get_meta(df.options).document_type
|
||||
except frappe.DoesNotExistError:
|
||||
# edge case where options="[Select]"
|
||||
pass
|
||||
self._show_missing_doctype_msg(df)
|
||||
|
||||
def load_print_formats(self):
|
||||
print_formats = frappe.db.sql(
|
||||
|
|
@ -226,7 +249,7 @@ class FormMeta(Meta):
|
|||
def load_templates(self):
|
||||
if not self.custom:
|
||||
module = load_doctype_module(self.name)
|
||||
app = module.__name__.split(".")[0]
|
||||
app = module.__name__.split(".", 1)[0]
|
||||
templates = {}
|
||||
if hasattr(module, "form_grid_templates"):
|
||||
for key, path in module.form_grid_templates.items():
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d
|
|||
ToDo = DocType("ToDo")
|
||||
User = DocType("User")
|
||||
count = Count("*").as_("count")
|
||||
filtered_records = frappe.qb.engine.build_conditions(doctype, current_filters).select("name")
|
||||
filtered_records = frappe.qb.get_query(doctype, filters=current_filters, fields=["name"])
|
||||
|
||||
return (
|
||||
frappe.qb.from_(ToDo)
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None):
|
|||
if isinstance(columns[0], str):
|
||||
first_col = columns[0].split(":")
|
||||
if len(first_col) > 1:
|
||||
first_col_fieldtype = first_col[1].split("/")[0]
|
||||
first_col_fieldtype = first_col[1].split("/", 1)[0]
|
||||
else:
|
||||
first_col_fieldtype = columns[0].get("fieldtype")
|
||||
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ def extract_fieldname(field):
|
|||
fieldname = field
|
||||
for sep in (" as ", " AS "):
|
||||
if sep in fieldname:
|
||||
fieldname = fieldname.split(sep)[0]
|
||||
fieldname = fieldname.split(sep, 1)[0]
|
||||
|
||||
# certain functions allowed, extract the fieldname from the function
|
||||
if fieldname.startswith("count(") or fieldname.startswith("sum(") or fieldname.startswith("avg("):
|
||||
|
|
@ -456,13 +456,14 @@ def handle_duration_fieldtype_values(doctype, data, fields):
|
|||
|
||||
def parse_field(field: str) -> tuple[str | None, str]:
|
||||
"""Parse a field into parenttype and fieldname."""
|
||||
key = field.split(" as ")[0]
|
||||
key = field.split(" as ", 1)[0]
|
||||
|
||||
if key.startswith(("count(", "sum(", "avg(")):
|
||||
raise ValueError
|
||||
|
||||
if "." in key:
|
||||
return key.split(".")[0][4:-1], key.split(".")[1].strip("`")
|
||||
table, column = key.split(".", 2)[:2]
|
||||
return table[4:-1], column.strip("`")
|
||||
|
||||
return None, key.strip("`")
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ def search_widget(
|
|||
|
||||
standard_queries = frappe.get_hooks().standard_queries or {}
|
||||
|
||||
if query and query.split()[0].lower() != "select":
|
||||
if query and query.split(maxsplit=1)[0].lower() != "select":
|
||||
# by method
|
||||
try:
|
||||
is_whitelisted(frappe.get_attr(query))
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import frappe
|
|||
import frappe.utils
|
||||
from frappe import _
|
||||
from frappe.email.doctype.email_group.email_group import add_subscribers
|
||||
from frappe.rate_limiter import rate_limit
|
||||
from frappe.utils.safe_exec import is_job_queued
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
|
|
@ -227,7 +228,6 @@ class Newsletter(WebsiteGenerator):
|
|||
)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def confirmed_unsubscribe(email, group):
|
||||
"""unsubscribe the email(user) from the mailing list(email_group)"""
|
||||
frappe.flags.ignore_permissions = True
|
||||
|
|
@ -238,9 +238,13 @@ def confirmed_unsubscribe(email, group):
|
|||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def subscribe(email, email_group=_("Website")): # noqa
|
||||
@rate_limit(limit=10, seconds=60 * 60)
|
||||
def subscribe(email, email_group=None): # noqa
|
||||
"""API endpoint to subscribe an email to a particular email group. Triggers a confirmation email."""
|
||||
|
||||
if email_group is None:
|
||||
email_group = _("Website")
|
||||
|
||||
# build subscription confirmation URL
|
||||
api_endpoint = frappe.utils.get_url(
|
||||
"/api/method/frappe.email.doctype.newsletter.newsletter.confirm_subscription"
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class Notification(Document):
|
|||
frappe.cache().hdel("notifications", self.document_type)
|
||||
|
||||
def on_update(self):
|
||||
frappe.cache().hdel("notifications", self.document_type)
|
||||
path = export_module_json(self, self.is_standard, self.module)
|
||||
if path:
|
||||
# js
|
||||
|
|
|
|||
|
|
@ -288,7 +288,11 @@ class FrappeClient:
|
|||
|
||||
if doctype != "User" and not frappe.db.exists("User", doc.get("owner")):
|
||||
frappe.get_doc(
|
||||
{"doctype": "User", "email": doc.get("owner"), "first_name": doc.get("owner").split("@")[0]}
|
||||
{
|
||||
"doctype": "User",
|
||||
"email": doc.get("owner"),
|
||||
"first_name": doc.get("owner").split("@", 1)[0],
|
||||
}
|
||||
).insert()
|
||||
|
||||
if update:
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ def parse_app_name(name: str) -> str:
|
|||
_repo = name.split(":")[1].rsplit("/", 1)[1]
|
||||
else:
|
||||
_repo = name.rsplit("/", 2)[2]
|
||||
repo = _repo.split(".")[0]
|
||||
repo = _repo.split(".", 1)[0]
|
||||
else:
|
||||
_, repo, _ = fetch_details_from_tag(name)
|
||||
return repo
|
||||
|
|
@ -271,7 +271,7 @@ def install_app(name, verbose=False, set_as_patched=True, force=False):
|
|||
frappe.clear_cache()
|
||||
|
||||
if name not in frappe.get_all_apps():
|
||||
raise Exception("App not in apps.txt")
|
||||
raise Exception(f"App {name} not in apps.txt")
|
||||
|
||||
if not force and name in installed_apps:
|
||||
click.secho(f"App {name} already installed", fg="yellow")
|
||||
|
|
@ -785,7 +785,7 @@ def is_downgrade(sql_file_path, verbose=False):
|
|||
|
||||
for app in all_apps:
|
||||
app_name = app[0]
|
||||
app_version = app[1].split(" ")[0]
|
||||
app_version = app[1].split(" ", 1)[0]
|
||||
|
||||
if app_name == "frappe":
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -88,8 +88,7 @@
|
|||
"fieldtype": "Link",
|
||||
"label": "Default User Role",
|
||||
"mandatory_depends_on": "eval: doc.default_user_type == \"System User\"",
|
||||
"options": "Role",
|
||||
"reqd": 1
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"description": "Must be enclosed in '()' and include '{0}', which is a placeholder for the user/login name. i.e. (&(objectclass=user)(uid={0}))",
|
||||
|
|
@ -302,7 +301,7 @@
|
|||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-05 21:52:31.146035",
|
||||
"modified": "2023-01-24 11:20:06.049708",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "LDAP Settings",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ if TYPE_CHECKING:
|
|||
|
||||
class LDAPSettings(Document):
|
||||
def validate(self):
|
||||
self.default_user_type = self.default_user_type or "System User"
|
||||
self.default_user_type = self.default_user_type or "Website User"
|
||||
|
||||
if not self.enabled:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -173,7 +173,6 @@ class LDAP_TestCase:
|
|||
"ldap_username_field",
|
||||
"ldap_first_name_field",
|
||||
"require_trusted_certificate",
|
||||
"default_role",
|
||||
] # fields that are required to have ldap functioning need to be mandatory
|
||||
|
||||
for mandatory_field in mandatory_fields:
|
||||
|
|
|
|||
|
|
@ -35,54 +35,60 @@ DOCTYPES_FOR_DOCTYPE = {"DocType", *TABLE_DOCTYPES_FOR_DOCTYPE.values()}
|
|||
|
||||
|
||||
def get_controller(doctype):
|
||||
"""Returns the **class** object of the given DocType.
|
||||
"""
|
||||
Returns the locally cached **class** object of the given DocType.
|
||||
For `custom` type, returns `frappe.model.document.Document`.
|
||||
|
||||
:param doctype: DocType name as string."""
|
||||
|
||||
def _get_controller():
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
|
||||
module_name, custom = frappe.db.get_value(
|
||||
"DocType", doctype, ("module", "custom"), cache=True
|
||||
) or ("Core", False)
|
||||
|
||||
if custom:
|
||||
is_tree = frappe.db.get_value("DocType", doctype, "is_tree", ignore=True, cache=True)
|
||||
_class = NestedSet if is_tree else Document
|
||||
else:
|
||||
class_overrides = frappe.get_hooks("override_doctype_class")
|
||||
if class_overrides and class_overrides.get(doctype):
|
||||
import_path = class_overrides[doctype][-1]
|
||||
module_path, classname = import_path.rsplit(".", 1)
|
||||
module = frappe.get_module(module_path)
|
||||
if not hasattr(module, classname):
|
||||
raise ImportError(f"{doctype}: {classname} does not exist in module {module_path}")
|
||||
else:
|
||||
module = load_doctype_module(doctype, module_name)
|
||||
classname = doctype.replace(" ", "").replace("-", "")
|
||||
|
||||
if hasattr(module, classname):
|
||||
_class = getattr(module, classname)
|
||||
if issubclass(_class, BaseDocument):
|
||||
_class = getattr(module, classname)
|
||||
else:
|
||||
raise ImportError(doctype)
|
||||
else:
|
||||
raise ImportError(doctype)
|
||||
return _class
|
||||
:param doctype: DocType name as string.
|
||||
"""
|
||||
|
||||
if frappe.local.dev_server:
|
||||
return _get_controller()
|
||||
return import_controller(doctype)
|
||||
|
||||
site_controllers = frappe.controllers.setdefault(frappe.local.site, {})
|
||||
if doctype not in site_controllers:
|
||||
site_controllers[doctype] = _get_controller()
|
||||
site_controllers[doctype] = import_controller(doctype)
|
||||
|
||||
return site_controllers[doctype]
|
||||
|
||||
|
||||
def import_controller(doctype):
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
|
||||
module_name = "Core"
|
||||
if doctype not in DOCTYPES_FOR_DOCTYPE:
|
||||
meta = frappe.get_meta(doctype)
|
||||
if meta.custom:
|
||||
return NestedSet if meta.get("is_tree") else Document
|
||||
|
||||
module_name = meta.module
|
||||
|
||||
module_path = None
|
||||
class_overrides = frappe.get_hooks("override_doctype_class")
|
||||
if class_overrides and class_overrides.get(doctype):
|
||||
import_path = class_overrides[doctype][-1]
|
||||
module_path, classname = import_path.rsplit(".", 1)
|
||||
module = frappe.get_module(module_path)
|
||||
|
||||
else:
|
||||
module = load_doctype_module(doctype, module_name)
|
||||
classname = doctype.replace(" ", "").replace("-", "")
|
||||
|
||||
class_ = getattr(module, classname, None)
|
||||
if class_ is None:
|
||||
raise ImportError(
|
||||
doctype
|
||||
if module_path is None
|
||||
else f"{doctype}: {classname} does not exist in module {module_path}"
|
||||
)
|
||||
|
||||
if not issubclass(class_, BaseDocument):
|
||||
raise ImportError(f"{doctype}: {classname} is not a subclass of BaseDocument")
|
||||
|
||||
return class_
|
||||
|
||||
|
||||
class BaseDocument:
|
||||
_reserved_keywords = {
|
||||
"doctype",
|
||||
|
|
@ -1062,7 +1068,7 @@ class BaseDocument:
|
|||
def is_dummy_password(self, pwd):
|
||||
return "".join(set(pwd)) == "*"
|
||||
|
||||
def precision(self, fieldname, parentfield=None):
|
||||
def precision(self, fieldname, parentfield=None) -> int | None:
|
||||
"""Returns float precision for a particular field (or get global default).
|
||||
|
||||
:param fieldname: Fieldname for which precision is required.
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ def get_static_default_value(df, doctype_user_permissions, allowed_records):
|
|||
return df.default
|
||||
|
||||
elif df.fieldtype == "Select" and df.options and df.options not in ("[Select]", "Loading..."):
|
||||
return df.options.split("\n")[0]
|
||||
return df.options.split("\n", 1)[0]
|
||||
|
||||
|
||||
def validate_value_via_user_permissions(
|
||||
|
|
|
|||
|
|
@ -435,7 +435,7 @@ class DatabaseQuery:
|
|||
if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field):
|
||||
continue
|
||||
|
||||
table_name = field.split(".")[0]
|
||||
table_name = field.split(".", 1)[0]
|
||||
|
||||
if table_name.lower().startswith("group_concat("):
|
||||
table_name = table_name[13:]
|
||||
|
|
@ -897,8 +897,9 @@ class DatabaseQuery:
|
|||
# will covert to
|
||||
# `tabItem`.`idx` desc, `tabItem`.`modified` desc
|
||||
args.order_by = ", ".join(
|
||||
f"`tab{self.doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}"
|
||||
f"`tab{self.doctype}`.`{f_split[0].strip()}` {f_split[1].strip()}"
|
||||
for f in meta.sort_field.split(",")
|
||||
if (f_split := f.split(maxsplit=2))
|
||||
)
|
||||
else:
|
||||
sort_field = meta.sort_field or "modified"
|
||||
|
|
@ -1029,8 +1030,9 @@ def get_order_by(doctype, meta):
|
|||
# will covert to
|
||||
# `tabItem`.`idx` desc, `tabItem`.`modified` desc
|
||||
order_by = ", ".join(
|
||||
f"`tab{doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}"
|
||||
f"`tab{doctype}`.`{f_split[0].strip()}` {f_split[1].strip()}"
|
||||
for f in meta.sort_field.split(",")
|
||||
if (f_split := f.split(maxsplit=2))
|
||||
)
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ def update_naming_series(doc):
|
|||
if doc.meta.autoname.startswith("naming_series:") and getattr(doc, "naming_series", None):
|
||||
revert_series_if_last(doc.naming_series, doc.name, doc)
|
||||
|
||||
elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash", "autoincrement"):
|
||||
elif doc.meta.autoname.split(":", 1)[0] not in ("Prompt", "field", "hash", "autoincrement"):
|
||||
revert_series_if_last(doc.meta.autoname, doc.name, doc)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ class NamingSeries:
|
|||
if not NAMING_SERIES_PATTERN.match(self.series):
|
||||
frappe.throw(
|
||||
_(
|
||||
'Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series',
|
||||
),
|
||||
"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}"
|
||||
).format(frappe.bold(self.series)),
|
||||
exc=InvalidNamingSeriesError,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def rename_field(doctype, old_fieldname, new_fieldname):
|
|||
frappe.db.sql(
|
||||
"""update `tab%s` set parentfield=%s
|
||||
where parentfield=%s"""
|
||||
% (new_field.options.split("\n")[0], "%s", "%s"),
|
||||
% (new_field.options.split("\n", 1)[0], "%s", "%s"),
|
||||
(new_fieldname, old_fieldname),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ def load_code_properties(doc, path):
|
|||
if hasattr(doc, "get_code_fields"):
|
||||
dirname, filename = os.path.split(path)
|
||||
for key, extn in doc.get_code_fields().items():
|
||||
codefile = os.path.join(dirname, filename.split(".")[0] + "." + extn)
|
||||
codefile = os.path.join(dirname, filename.split(".", 1)[0] + "." + extn)
|
||||
if os.path.exists(codefile):
|
||||
with open(codefile) as txtfile:
|
||||
doc.set(key, txtfile.read())
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ def run_single(patchmodule=None, method=None, methodargs=None, force=False):
|
|||
return True
|
||||
|
||||
|
||||
def execute_patch(patchmodule, method=None, methodargs=None):
|
||||
def execute_patch(patchmodule: str, method=None, methodargs=None):
|
||||
"""execute the patch"""
|
||||
_patch_mode(True)
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ def execute_patch(patchmodule, method=None, methodargs=None):
|
|||
docstring = ""
|
||||
else:
|
||||
has_patch_file = True
|
||||
patch = f"{patchmodule.split()[0]}.execute"
|
||||
patch = f"{patchmodule.split(maxsplit=1)[0]}.execute"
|
||||
_patch = frappe.get_attr(patch)
|
||||
docstring = _patch.__doc__ or ""
|
||||
|
||||
|
|
|
|||
|
|
@ -196,7 +196,6 @@ frappe.patches.v14_0.setup_likes_from_feedback
|
|||
frappe.patches.v14_0.update_webforms
|
||||
frappe.patches.v14_0.delete_payment_gateways
|
||||
frappe.patches.v15_0.remove_event_streaming
|
||||
frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings
|
||||
frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report
|
||||
|
||||
[post_model_sync]
|
||||
|
|
@ -221,3 +220,4 @@ frappe.patches.v14_0.update_attachment_comment
|
|||
frappe.patches.v15_0.set_contact_full_name
|
||||
execute:frappe.delete_doc("Page", "activity", force=1)
|
||||
frappe.patches.v14_0.disable_email_accounts_with_oauth
|
||||
frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings
|
||||
|
|
|
|||
|
|
@ -4,12 +4,6 @@ from frappe.utils import cint
|
|||
|
||||
def execute():
|
||||
expiry_period = (
|
||||
cint(frappe.db.get_single_value("System Settings", "prepared_report_expiry_period")) or 30
|
||||
cint(frappe.db.get_singles_dict("System Settings").get("prepared_report_expiry_period")) or 30
|
||||
)
|
||||
frappe.get_single("Log Settings").register_doctype("Prepared Report", expiry_period)
|
||||
|
||||
singles = frappe.qb.DocType("Singles")
|
||||
frappe.qb.from_(singles).delete().where(
|
||||
(singles.doctype == "System Settings")
|
||||
& (singles.field.isin(["enable_prepared_report_auto_deletion", "prepared_report_expiry_period"]))
|
||||
).run()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Field from "./Field.vue";
|
|||
import EditableInput from "./EditableInput.vue";
|
||||
import { ref } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { move_children_to_parent } from "../utils";
|
||||
import { move_children_to_parent, confirm_dialog } from "../utils";
|
||||
|
||||
let props = defineProps(["section", "column"]);
|
||||
let store = useStore();
|
||||
|
|
@ -24,32 +24,61 @@ function remove_column() {
|
|||
if (store.is_customize_form && props.column.df.is_custom_field == 0) {
|
||||
frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
|
||||
throw "cannot delete standard field";
|
||||
} else if (props.column.fields.length == 0 || store.has_standard_field(props.column)) {
|
||||
delete_column();
|
||||
} else {
|
||||
confirm_dialog(
|
||||
__("Delete Column", null, "Title of confirmation dialog"),
|
||||
__("Are you sure you want to delete the column? All the fields in the column will be moved to the previous column.", null, "Confirmation dialog message"),
|
||||
() => delete_column(),
|
||||
__("Delete column", null, "Button text"),
|
||||
() => delete_column(true),
|
||||
__("Delete entire column with fields", null, "Button text")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function delete_column(with_children) {
|
||||
// move all fields to previous column
|
||||
let columns = props.section.columns;
|
||||
let index = columns.indexOf(props.column);
|
||||
|
||||
if (index > 0) {
|
||||
let prev_column = columns[index - 1];
|
||||
prev_column.fields = [...prev_column.fields, ...props.column.fields];
|
||||
} else {
|
||||
if (props.column.fields.length != 0) {
|
||||
// create a new column if current column has fields and push fields to it
|
||||
columns.unshift({
|
||||
df: store.get_df("Column Break"),
|
||||
fields: props.column.fields,
|
||||
is_first: true,
|
||||
});
|
||||
index++;
|
||||
if (with_children && index == 0 && columns.length == 1) {
|
||||
if (props.column.fields.length == 0) {
|
||||
frappe.msgprint(__("Section must have at least one column"));
|
||||
throw "section must have at least one column";
|
||||
}
|
||||
|
||||
columns.unshift({
|
||||
df: store.get_df("Column Break"),
|
||||
fields: [],
|
||||
is_first: true,
|
||||
});
|
||||
index++;
|
||||
}
|
||||
|
||||
if (!with_children) {
|
||||
if (index > 0) {
|
||||
let prev_column = columns[index - 1];
|
||||
prev_column.fields = [...prev_column.fields, ...props.column.fields];
|
||||
} else {
|
||||
// set next column as first column
|
||||
let next_column = columns[index + 1];
|
||||
if (next_column) {
|
||||
next_column.is_first = true;
|
||||
if (props.column.fields.length == 0) {
|
||||
// set next column as first column
|
||||
let next_column = columns[index + 1];
|
||||
if (next_column) {
|
||||
next_column.is_first = true;
|
||||
} else {
|
||||
frappe.msgprint(__("Section must have at least one column"));
|
||||
throw "section must have at least one column";
|
||||
}
|
||||
} else {
|
||||
frappe.msgprint(__("Section must have at least one column"));
|
||||
throw "section must have at least one column";
|
||||
// create a new column if current column has fields and push fields to it
|
||||
columns.unshift({
|
||||
df: store.get_df("Column Break"),
|
||||
fields: props.column.fields,
|
||||
is_first: true,
|
||||
});
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,10 +32,21 @@ function move_fields_to_column() {
|
|||
function duplicate_field() {
|
||||
let duplicate_field = clone_field(props.field);
|
||||
|
||||
if (store.is_customize_form) {
|
||||
duplicate_field.df.is_custom_field = 1;
|
||||
}
|
||||
|
||||
if (duplicate_field.df.label) {
|
||||
duplicate_field.df.label = duplicate_field.df.label + " Copy";
|
||||
}
|
||||
duplicate_field.df.fieldname = "";
|
||||
duplicate_field.df.__islocal = 1;
|
||||
duplicate_field.df.__unsaved = 1;
|
||||
duplicate_field.df.owner = frappe.session.user;
|
||||
|
||||
delete duplicate_field.df.creation;
|
||||
delete duplicate_field.df.modified;
|
||||
delete duplicate_field.df.modified_by;
|
||||
|
||||
// push duplicate_field after props.field in the same column
|
||||
let index = props.column.fields.indexOf(props.field);
|
||||
|
|
|
|||
|
|
@ -109,6 +109,12 @@ onMounted(() => {
|
|||
box-shadow: var(--card-shadow);
|
||||
background-color: var(--card-bg);
|
||||
|
||||
:deep(.section-columns.has-one-column .field) {
|
||||
input.form-control, .signature-field {
|
||||
width: calc(50% - 19px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.column-container .field.sortable-chosen) {
|
||||
background-color: var(--bg-light-gray);
|
||||
border-radius: var(--border-radius-sm);
|
||||
|
|
@ -191,6 +197,8 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
:deep(.preview) {
|
||||
--field-placeholder-color: var(--fg-bg-color);
|
||||
|
||||
.tab, .column, .field, [data-is-custom="1"] {
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
|
|
@ -221,6 +229,12 @@ onMounted(() => {
|
|||
.section-columns {
|
||||
margin-top: 8px;
|
||||
|
||||
&.has-one-column .field {
|
||||
input.form-control, .signature-field {
|
||||
width: calc(50% - 15px);
|
||||
}
|
||||
}
|
||||
|
||||
.section-columns-container {
|
||||
.column {
|
||||
padding-left: 15px;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Column from "./Column.vue";
|
|||
import EditableInput from "./EditableInput.vue";
|
||||
import { ref } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { section_boilerplate, move_children_to_parent } from "../utils";
|
||||
import { section_boilerplate, move_children_to_parent, confirm_dialog } from "../utils";
|
||||
|
||||
let props = defineProps(["tab", "section"]);
|
||||
let store = useStore();
|
||||
|
|
@ -27,25 +27,42 @@ function remove_section() {
|
|||
if (store.is_customize_form && props.section.df.is_custom_field == 0) {
|
||||
frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
|
||||
throw "cannot delete standard field";
|
||||
} else if (store.has_standard_field(props.section)) {
|
||||
delete_section();
|
||||
} else if (is_section_empty()) {
|
||||
delete_section(true);
|
||||
} else {
|
||||
confirm_dialog(
|
||||
__("Delete Section", null, "Title of confirmation dialog"),
|
||||
__("Are you sure you want to delete the section? All the columns along with fields in the section will be moved to the previous section.", null, "Confirmation dialog message"),
|
||||
() => delete_section(),
|
||||
__("Delete section", null, "Button text"),
|
||||
() => delete_section(true),
|
||||
__("Delete entire section with columns", null, "Button text")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function delete_section(with_children) {
|
||||
let sections = props.tab.sections;
|
||||
let index = sections.indexOf(props.section);
|
||||
|
||||
if (index > 0) {
|
||||
let prev_section = sections[index - 1];
|
||||
if (!is_section_empty()) {
|
||||
// move all columns from current section to previous section
|
||||
prev_section.columns = [...prev_section.columns, ...props.section.columns];
|
||||
if (!with_children) {
|
||||
if (index > 0) {
|
||||
let prev_section = sections[index - 1];
|
||||
if (!is_section_empty()) {
|
||||
// move all columns from current section to previous section
|
||||
prev_section.columns = [...prev_section.columns, ...props.section.columns];
|
||||
}
|
||||
} else if (index == 0 && !is_section_empty()) {
|
||||
// create a new section and push columns to it
|
||||
sections.unshift({
|
||||
df: store.get_df("Section Break"),
|
||||
columns: props.section.columns,
|
||||
is_first: true,
|
||||
});
|
||||
index++;
|
||||
}
|
||||
} else if (index == 0 && !is_section_empty()) {
|
||||
// create a new section and push columns to it
|
||||
sections.unshift({
|
||||
df: store.get_df("Section Break"),
|
||||
columns: props.section.columns,
|
||||
is_first: true,
|
||||
});
|
||||
index++;
|
||||
}
|
||||
|
||||
// remove section
|
||||
|
|
@ -130,7 +147,13 @@ function move_sections_to_tab() {
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="section.df.description" class="section-description">{{ section.df.description }}</div>
|
||||
<div class="section-columns" :class="{ hidden: section.df.collapsible && collapsed }">
|
||||
<div
|
||||
class="section-columns"
|
||||
:class="{
|
||||
hidden: section.df.collapsible && collapsed,
|
||||
'has-one-column': section.columns.length === 1
|
||||
}"
|
||||
>
|
||||
<draggable
|
||||
class="section-columns-container"
|
||||
:style="{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Section from "./Section.vue";
|
|||
import EditableInput from "./EditableInput.vue";
|
||||
import draggable from "vuedraggable";
|
||||
import { useStore } from "../store";
|
||||
import { section_boilerplate } from "../utils";
|
||||
import { section_boilerplate, confirm_dialog } from "../utils";
|
||||
import { ref, computed, nextTick } from "vue";
|
||||
|
||||
let store = useStore();
|
||||
|
|
@ -51,44 +51,51 @@ function add_new_section() {
|
|||
|
||||
function is_current_tab_empty() {
|
||||
// check if sections have columns and it contains fields
|
||||
return !store.current_tab.sections.some(section => {
|
||||
// if section doesnt have fields remove the section
|
||||
let has_fields = section.columns.some(column => column.fields.length);
|
||||
|
||||
if (!has_fields) {
|
||||
// remove section if empty
|
||||
let index = store.current_tab.sections.indexOf(section);
|
||||
store.current_tab.sections.splice(index, 1);
|
||||
has_fields = true;
|
||||
}
|
||||
|
||||
return has_fields;
|
||||
});
|
||||
return !store.current_tab.sections.some(
|
||||
section => section.columns.some(column => column.fields.length)
|
||||
);
|
||||
}
|
||||
|
||||
function remove_tab() {
|
||||
if (store.is_customize_form && store.current_tab.df.is_custom_field == 0) {
|
||||
frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
|
||||
throw "cannot delete standard field";
|
||||
} else if (store.has_standard_field(store.current_tab)) {
|
||||
delete_tab();
|
||||
} else if (is_current_tab_empty()) {
|
||||
delete_tab(true);
|
||||
} else {
|
||||
confirm_dialog(
|
||||
__("Delete Tab", null, "Title of confirmation dialog"),
|
||||
__("Are you sure you want to delete the tab? All the sections along with fields in the tab will be moved to the previous tab.", null, "Confirmation dialog message"),
|
||||
() => delete_tab(),
|
||||
__("Delete tab", null, "Button text"),
|
||||
() => delete_tab(true),
|
||||
__("Delete entire tab with sections", null, "Button text")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function delete_tab(with_children) {
|
||||
let tabs = layout.value.tabs;
|
||||
let index = tabs.indexOf(store.current_tab);
|
||||
|
||||
if (index > 0) {
|
||||
let prev_tab = tabs[index - 1];
|
||||
if (!is_current_tab_empty()) {
|
||||
// move all sections from current tab to previous tab
|
||||
prev_tab.sections = [...prev_tab.sections, ...store.current_tab.sections];
|
||||
if (!with_children) {
|
||||
if (index > 0) {
|
||||
let prev_tab = tabs[index - 1];
|
||||
if (!is_current_tab_empty()) {
|
||||
// move all sections from current tab to previous tab
|
||||
prev_tab.sections = [...prev_tab.sections, ...store.current_tab.sections];
|
||||
}
|
||||
} else {
|
||||
// create a new tab and push sections to it
|
||||
tabs.unshift({
|
||||
df: store.get_df("Tab Break", "", __("Details")),
|
||||
sections: store.current_tab.sections,
|
||||
is_first: true,
|
||||
});
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
// create a new tab and push sections to it
|
||||
tabs.unshift({
|
||||
df: store.get_df("Tab Break", "", __("Details")),
|
||||
sections: store.current_tab.sections,
|
||||
is_first: true,
|
||||
});
|
||||
index++;
|
||||
}
|
||||
|
||||
// remove tab
|
||||
|
|
@ -185,7 +192,7 @@ function remove_tab() {
|
|||
</template>
|
||||
</draggable>
|
||||
<div class="empty-tab" :hidden="store.read_only">
|
||||
<div>{{ __("Drag & Drop a section here") }}</div>
|
||||
<div>{{ __("Drag & Drop a section here from another tab") }}</div>
|
||||
<div>{{ __("OR") }}</div>
|
||||
<button class="btn btn-default btn-sm" @click="add_new_section">
|
||||
{{ __("Add a new section") }}
|
||||
|
|
|
|||
|
|
@ -182,8 +182,10 @@ export const useStore = defineStore("form-builder-store", {
|
|||
} else {
|
||||
this.doc.fields = this.get_updated_fields();
|
||||
this.validate_fields(this.doc.fields, this.doc.istable);
|
||||
await frappe.call("frappe.client.save", { doc: this.doc });
|
||||
frappe.toast("Fields Table Updated");
|
||||
await frappe.call({
|
||||
method: "frappe.desk.form.save.savedocs",
|
||||
args: { doc: this.doc, action: "Save" },
|
||||
});
|
||||
}
|
||||
this.fetch();
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -324,3 +324,28 @@ export function clone_field(field) {
|
|||
cloned_field.df.name = frappe.utils.get_random(8);
|
||||
return cloned_field;
|
||||
}
|
||||
|
||||
export function confirm_dialog(
|
||||
title,
|
||||
message,
|
||||
primary_action,
|
||||
primary_action_label,
|
||||
secondary_action,
|
||||
secondary_action_label
|
||||
) {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: title,
|
||||
primary_action_label: primary_action_label || __("Yes"),
|
||||
primary_action: () => {
|
||||
primary_action && primary_action();
|
||||
d.hide();
|
||||
},
|
||||
secondary_action_label: secondary_action_label || __("No"),
|
||||
secondary_action: () => {
|
||||
secondary_action && secondary_action();
|
||||
d.hide();
|
||||
},
|
||||
});
|
||||
d.show();
|
||||
d.set_message(message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,22 +7,49 @@ frappe.ui.form.ControlPassword = class ControlPassword extends frappe.ui.form.Co
|
|||
make_input() {
|
||||
var me = this;
|
||||
super.make_input();
|
||||
this.$input
|
||||
.parent()
|
||||
.append($('<span class="password-strength-indicator indicator"></span>'));
|
||||
this.$wrapper
|
||||
.find(".control-input-wrapper")
|
||||
.append($('<p class="password-strength-message text-muted small hidden"></p>'));
|
||||
|
||||
this.indicator = this.$wrapper.find(".password-strength-indicator");
|
||||
this.indicator = $(
|
||||
`<div class="password-strength-indicator hidden">
|
||||
<div class="progress-text"></div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
aria-valuenow="0"
|
||||
aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
).insertAfter(this.$input);
|
||||
|
||||
this.progress_text = this.indicator.find(".progress-text");
|
||||
this.progress_bar = this.indicator.find(".progress-bar");
|
||||
this.message = this.$wrapper.find(".help-box");
|
||||
|
||||
this.$input.on("keyup", () => {
|
||||
clearTimeout(this.check_password_timeout);
|
||||
this.check_password_timeout = setTimeout(() => {
|
||||
this.$input.on(
|
||||
"keyup",
|
||||
frappe.utils.debounce(() => {
|
||||
let hide_icon = me.$input.val() && !me.$input.val().includes("*");
|
||||
me.toggle_password.toggleClass("hidden", !hide_icon);
|
||||
me.get_password_strength(me.$input.val());
|
||||
}, 500);
|
||||
}, 500)
|
||||
);
|
||||
|
||||
this.toggle_password = $(`
|
||||
<div class="toggle-password hidden">
|
||||
${frappe.utils.icon("unhide", "sm")}
|
||||
</div>
|
||||
`).insertAfter(this.$input);
|
||||
|
||||
this.toggle_password.on("click", () => {
|
||||
if (this.$input.attr("type") === "password") {
|
||||
this.$input.attr("type", "text");
|
||||
this.toggle_password.html(frappe.utils.icon("hide", "sm"));
|
||||
} else {
|
||||
this.$input.attr("type", "password");
|
||||
this.toggle_password.html(frappe.utils.icon("unhide", "sm"));
|
||||
}
|
||||
});
|
||||
|
||||
!this.value && this.toggle_password.removeClass("hidden");
|
||||
}
|
||||
|
||||
disable_password_checks() {
|
||||
|
|
@ -33,6 +60,13 @@ frappe.ui.form.ControlPassword = class ControlPassword extends frappe.ui.form.Co
|
|||
if (!this.enable_password_checks) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this.indicator.addClass("hidden");
|
||||
this.message.addClass("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
var me = this;
|
||||
frappe.call({
|
||||
type: "POST",
|
||||
|
|
@ -43,15 +77,34 @@ frappe.ui.form.ControlPassword = class ControlPassword extends frappe.ui.form.Co
|
|||
callback: function (r) {
|
||||
if (r.message) {
|
||||
let score = r.message.score;
|
||||
var indicators = ["red", "red", "orange", "yellow", "green"];
|
||||
var indicators = ["red", "red", "orange", "blue", "green"];
|
||||
me.set_strength_indicator(indicators[score]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
set_strength_indicator(color) {
|
||||
var message = __("Include symbols, numbers and capital letters in the password");
|
||||
this.indicator.removeClass().addClass("password-strength-indicator indicator " + color);
|
||||
let strength = {
|
||||
red: [__("Weak"), "danger", 25],
|
||||
orange: [__("Average"), "warning", 50],
|
||||
blue: [__("Strong"), "info", 75],
|
||||
green: [__("Excellent"), "success", 100],
|
||||
};
|
||||
let progress_text = strength[color][0];
|
||||
let progress_color = strength[color][1];
|
||||
let progress_percent = strength[color][2];
|
||||
|
||||
this.indicator.removeClass("hidden");
|
||||
|
||||
this.progress_text.html(progress_text).css("color", `var(--${color}-500)`);
|
||||
|
||||
this.progress_bar
|
||||
.css("width", progress_percent + "%")
|
||||
.attr("aria-valuenow", progress_percent)
|
||||
.removeClass()
|
||||
.addClass("progress-bar progress-bar-" + progress_color);
|
||||
|
||||
let message = __("Include symbols, numbers and capital letters in the password");
|
||||
this.message.html(message).toggleClass("hidden", color == "green");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -621,10 +621,6 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
|
||||
this.$wrapper.trigger("render_complete");
|
||||
|
||||
if (!this.hidden) {
|
||||
this.layout.show_empty_form_message();
|
||||
}
|
||||
|
||||
frappe.after_ajax(() => {
|
||||
$(document).ready(() => {
|
||||
this.scroll_to_element();
|
||||
|
|
@ -1520,7 +1516,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
if (this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name]) {
|
||||
this.fields_dict[fieldname].grid.grid_rows_by_docname[
|
||||
table_row_name
|
||||
].refresh_field(fieldname);
|
||||
].refresh_field(table_field);
|
||||
}
|
||||
} else {
|
||||
this.refresh_field(fieldname);
|
||||
|
|
@ -2051,16 +2047,12 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
this.doc.docstatus === 0
|
||||
)
|
||||
) {
|
||||
if (wrapper.length) {
|
||||
wrapper.hide();
|
||||
wrapper.html("");
|
||||
}
|
||||
|
||||
wrapper.length && wrapper.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wrapper.length) {
|
||||
wrapper = $('<div class="submission-queue-banner form-message yellow">');
|
||||
wrapper = $('<div class="submission-queue-banner form-message">');
|
||||
this.layout.wrapper.prepend(wrapper);
|
||||
}
|
||||
|
||||
|
|
@ -2070,49 +2062,40 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
args: { doctype: this.doctype, docname: this.docname },
|
||||
})
|
||||
.then((r) => {
|
||||
if (r.message.latest_submission) {
|
||||
if (r.message?.latest_submission) {
|
||||
// if we are here that means some submission(s) were queued and are in queued/failed state
|
||||
let col_width = 4;
|
||||
let failed_link = "";
|
||||
let submission_label = __("Previous Submission");
|
||||
let secondary = "";
|
||||
let div_class = "col-md-12";
|
||||
|
||||
if (r.message.latest_failed_submission) {
|
||||
if (r.message.latest_failed_submission !== r.message.latest_submission) {
|
||||
col_width = 3;
|
||||
failed_link = `<div class="col-md-3">
|
||||
<a href='/app/submission-queue/${r.message.latest_failed_submission}'>${__(
|
||||
"Previous Falied Submission"
|
||||
)}</a>
|
||||
</div>`;
|
||||
} else {
|
||||
submission_label = __("Previous Falied Submission");
|
||||
}
|
||||
if (r.message.exc) {
|
||||
secondary = `: <span>${r.message.exc}</span>`;
|
||||
} else {
|
||||
div_class = "col-md-6";
|
||||
secondary = `
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<a href='/app/submission-queue?ref_doctype=${encodeURIComponent(
|
||||
this.doctype
|
||||
)}&ref_docname=${encodeURIComponent(this.docname)}'>${__(
|
||||
"All Submissions"
|
||||
)}</a>
|
||||
`;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="row">
|
||||
<div class="col-md-${col_width}">
|
||||
<strong>${__("Submission Status:")}</strong>
|
||||
<div class="row">
|
||||
<div class="${div_class}">
|
||||
<a href='/app/submission-queue/${r.message.latest_submission}'>${submission_label} (${r.message.status})</a>${secondary}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-${col_width}">
|
||||
<a href='/app/submission-queue/${r.message.latest_submission}'>${submission_label}</a>
|
||||
</div>
|
||||
${failed_link}
|
||||
<div class="col-md-${col_width}">
|
||||
<a href='/app/submission-queue?ref_doctype=${encodeURIComponent(
|
||||
this.doctype
|
||||
)}&ref_docname=${encodeURIComponent(this.docname)}'>${__(
|
||||
"All Submissions"
|
||||
)}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
|
||||
wrapper.show();
|
||||
wrapper.removeClass("red").removeClass("yellow");
|
||||
wrapper.addClass(r.message.status == "Failed" ? "red" : "yellow");
|
||||
wrapper.html(html);
|
||||
} else {
|
||||
wrapper.hide();
|
||||
wrapper.html("");
|
||||
wrapper.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,17 +52,6 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.setup_events();
|
||||
}
|
||||
|
||||
show_empty_form_message() {
|
||||
if (
|
||||
!(
|
||||
this.wrapper.find(".frappe-control:visible").length ||
|
||||
this.wrapper.find(".section-head.collapsed").length
|
||||
)
|
||||
) {
|
||||
this.show_message(__("This form does not have any input"));
|
||||
}
|
||||
}
|
||||
|
||||
get_doctype_fields() {
|
||||
let fields = [this.get_new_name_field()];
|
||||
if (this.doctype_layout) {
|
||||
|
|
|
|||
|
|
@ -146,6 +146,10 @@ export default class BulkOperations {
|
|||
.call({
|
||||
method: "frappe.desk.reportview.delete_items",
|
||||
freeze: true,
|
||||
freeze_message:
|
||||
docnames.length <= 10
|
||||
? __("Deleting {0} records...", [docnames.length])
|
||||
: null,
|
||||
args: {
|
||||
items: docnames,
|
||||
doctype: this.doctype,
|
||||
|
|
|
|||
|
|
@ -584,7 +584,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
</div>
|
||||
`);
|
||||
this.setup_new_doc_event();
|
||||
this.list_sidebar && this.list_sidebar.reload_stats();
|
||||
if (this.list_view_settings && !this.list_view_settings.disable_sidebar_stats) {
|
||||
this.list_sidebar && this.list_sidebar.reload_stats();
|
||||
}
|
||||
this.toggle_paging && this.$paging_area.toggle(true);
|
||||
}
|
||||
|
||||
|
|
@ -896,7 +898,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
const comment_count = `<span class="comment-count">
|
||||
${frappe.utils.icon("small-message")}
|
||||
${doc._comment_count > 99 ? "99+" : doc._comment_count}
|
||||
${doc._comment_count > 99 ? "99+" : doc._comment_count || 0}
|
||||
</span>`;
|
||||
|
||||
html += `
|
||||
|
|
|
|||
|
|
@ -112,9 +112,14 @@ $.extend(frappe.model, {
|
|||
{ fieldname: "name", fieldtype: "Link", label: __("ID") },
|
||||
{ fieldname: "owner", fieldtype: "Link", label: __("Created By"), options: "User" },
|
||||
{ fieldname: "idx", fieldtype: "Int", label: __("Index") },
|
||||
{ fieldname: "creation", fieldtype: "Date", label: __("Created On") },
|
||||
{ fieldname: "modified", fieldtype: "Date", label: __("Last Updated On") },
|
||||
{ fieldname: "modified_by", fieldtype: "Data", label: __("Last Updated By") },
|
||||
{ fieldname: "creation", fieldtype: "Datetime", label: __("Created On") },
|
||||
{ fieldname: "modified", fieldtype: "Datetime", label: __("Last Updated On") },
|
||||
{
|
||||
fieldname: "modified_by",
|
||||
fieldtype: "Link",
|
||||
label: __("Last Updated By"),
|
||||
options: "User",
|
||||
},
|
||||
{ fieldname: "_user_tags", fieldtype: "Data", label: __("Tags") },
|
||||
{ fieldname: "_liked_by", fieldtype: "Data", label: __("Liked By") },
|
||||
{ fieldname: "_comments", fieldtype: "Text", label: __("Comments") },
|
||||
|
|
|
|||
|
|
@ -195,7 +195,9 @@ $.extend(frappe.perm, {
|
|||
}
|
||||
|
||||
if (!perm) {
|
||||
return df && (cint(df.hidden) || cint(df.hidden_due_to_dependency)) ? "None" : "Write";
|
||||
let is_hidden = df && (cint(df.hidden) || cint(df.hidden_due_to_dependency));
|
||||
let is_read_only = df && cint(df.read_only);
|
||||
return is_hidden ? "None" : is_read_only ? "Read" : "Write";
|
||||
}
|
||||
|
||||
if (!df.permlevel) df.permlevel = 0;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
|
|||
this.get_close_btn().hide();
|
||||
}
|
||||
|
||||
if (!this.size) this.set_modal_size();
|
||||
|
||||
this.wrapper = this.$wrapper.find(".modal-dialog").get(0);
|
||||
if (this.size == "small") $(this.wrapper).addClass("modal-sm");
|
||||
else if (this.size == "large") $(this.wrapper).addClass("modal-lg");
|
||||
|
|
@ -123,6 +125,31 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
|
|||
});
|
||||
}
|
||||
|
||||
set_modal_size() {
|
||||
if (!this.fields) {
|
||||
this.size = "";
|
||||
return;
|
||||
}
|
||||
|
||||
let col_brk = 0;
|
||||
let cur_col_brk = 0;
|
||||
|
||||
// if fields have more than 2 Column Breaks before encountering Section Break, make it large
|
||||
this.fields.forEach((field) => {
|
||||
if (field.fieldtype == "Column Break") {
|
||||
cur_col_brk++;
|
||||
|
||||
if (cur_col_brk > col_brk) {
|
||||
col_brk = cur_col_brk;
|
||||
}
|
||||
} else if (field.fieldtype == "Section Break") {
|
||||
cur_col_brk = 0;
|
||||
}
|
||||
});
|
||||
|
||||
this.size = col_brk >= 4 ? "extra-large" : col_brk >= 2 ? "large" : "";
|
||||
}
|
||||
|
||||
get_primary_btn() {
|
||||
return this.standard_actions.find(".btn-primary");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,13 +39,16 @@ frappe.ui.Filter = class {
|
|||
|
||||
this.invalid_condition_map = {
|
||||
Date: ["like", "not like"],
|
||||
Datetime: ["like", "not like"],
|
||||
Datetime: ["like", "not like", "in", "not in", "=", "!="],
|
||||
Data: ["Between", "Timespan"],
|
||||
Select: ["like", "not like", "Between", "Timespan"],
|
||||
Link: ["Between", "Timespan", ">", "<", ">=", "<="],
|
||||
Currency: ["Between", "Timespan"],
|
||||
Color: ["Between", "Timespan"],
|
||||
Check: this.conditions.map((c) => c[0]).filter((c) => c !== "="),
|
||||
Code: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
|
||||
Password: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"],
|
||||
Rating: ["like", "not like", "Between", "in", "not in", "Timespan"],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -497,10 +500,14 @@ frappe.ui.filter_utils = {
|
|||
"Small Text",
|
||||
"Text Editor",
|
||||
"Code",
|
||||
"Attach",
|
||||
"Attach Image",
|
||||
"Markdown Editor",
|
||||
"HTML Editor",
|
||||
"Tag",
|
||||
"Phone",
|
||||
"Comments",
|
||||
"Barcode",
|
||||
"Dynamic Link",
|
||||
"Read Only",
|
||||
"Assign",
|
||||
|
|
|
|||
|
|
@ -381,6 +381,8 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
web_form: this.name,
|
||||
for_payment,
|
||||
},
|
||||
btn: $("btn-primary"),
|
||||
freeze: true,
|
||||
callback: (response) => {
|
||||
// Check for any exception in response
|
||||
if (!response.exc) {
|
||||
|
|
|
|||
|
|
@ -5,20 +5,43 @@
|
|||
@import "phone_picker";
|
||||
|
||||
// password
|
||||
.form-control[data-fieldtype="Password"] {
|
||||
position: inherit;
|
||||
}
|
||||
.frappe-control[data-fieldtype="Password"] {
|
||||
.control-input-wrapper {
|
||||
position: relative;
|
||||
|
||||
.password-strength-indicator {
|
||||
// TODO: Review
|
||||
float: right;
|
||||
padding: 15px;
|
||||
margin-top: -41px;
|
||||
margin-right: -7px;
|
||||
}
|
||||
.form-control[data-fieldtype="Password"] {
|
||||
position: inherit;
|
||||
}
|
||||
|
||||
.password-strength-message {
|
||||
margin-top: -10px;
|
||||
.password-strength-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
gap: 5px;
|
||||
top: -20px;
|
||||
right: 0px;
|
||||
|
||||
.progress-text {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: var(--bg-light-gray);
|
||||
width: 100px;
|
||||
height: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-password {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
padding: 3px;
|
||||
z-index: 3;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// select
|
||||
|
|
@ -232,6 +255,10 @@ a.progress-small {
|
|||
background-color: var(--red-500);
|
||||
}
|
||||
|
||||
.progress-bar-info {
|
||||
background-color: var(--blue-500);
|
||||
}
|
||||
|
||||
.progress-bar-warning {
|
||||
background-color: var(--orange-500);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,10 +163,12 @@ $input-height: 28px !default;
|
|||
--bg-green: var(--dark-green-50);
|
||||
--bg-yellow: var(--yellow-50);
|
||||
--bg-orange: var(--orange-50);
|
||||
--bg-red: var(--red-50);
|
||||
--bg-red: var(--red-100);
|
||||
--bg-gray: var(--gray-200);
|
||||
--bg-grey: var(--gray-200);
|
||||
--bg-light-gray: var(--gray-100);
|
||||
--bg-dark-gray: var(--gray-900);
|
||||
--bg-dark-gray: var(--gray-400);
|
||||
--bg-darkgrey: var(--gray-400);
|
||||
--bg-purple: var(--purple-100);
|
||||
--bg-pink: var(--pink-50);
|
||||
--bg-cyan: var(--cyan-50);
|
||||
|
|
@ -186,12 +188,15 @@ $input-height: 28px !default;
|
|||
--text-on-dark-blue: var(--blue-800);
|
||||
--text-on-green: var(--dark-green-700);
|
||||
--text-on-yellow: var(--yellow-700);
|
||||
--text-on-orange: var(--orange-600);
|
||||
--text-on-orange: var(--orange-700);
|
||||
--text-on-red: var(--red-600);
|
||||
--text-on-gray: var(--gray-600);
|
||||
--text-on-gray: var(--gray-700);
|
||||
--text-on-grey: var(--gray-700);
|
||||
--text-on-darkgrey: var(--gray-800);
|
||||
--text-on-dark-gray: var(--gray-800);
|
||||
--text-on-light-gray: var(--gray-800);
|
||||
--text-on-purple: var(--purple-700);
|
||||
--text-on-pink: var(--pink-600);
|
||||
--text-on-pink: var(--pink-700);
|
||||
--text-on-cyan: var(--cyan-800);
|
||||
|
||||
// alert colors
|
||||
|
|
|
|||
|
|
@ -43,16 +43,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
// hide row index in 6 column child tables
|
||||
.form-column.col-sm-6 .form-grid {
|
||||
.row-index {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-open-row {
|
||||
.edit-grid-row {
|
||||
// hide row index in 6/4 column child tables
|
||||
.form-column.col-sm-6, .form-column.col-sm-4 {
|
||||
.form-grid {
|
||||
.row-index {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-open-row {
|
||||
.edit-grid-row {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,3 @@
|
|||
@mixin indicator-pill-color($color) {
|
||||
background: var(--bg-#{$color});
|
||||
color: var(--text-on-#{$color});
|
||||
&::before,
|
||||
&::after {
|
||||
background: var(--text-on-#{$color});
|
||||
}
|
||||
}
|
||||
|
||||
@mixin indicator-color($color) {
|
||||
&::before,
|
||||
&::after {
|
||||
background: var(--text-on-#{$color});
|
||||
}
|
||||
}
|
||||
|
||||
.indicator,
|
||||
.indicator-pill,
|
||||
.indicator-pill-right,
|
||||
|
|
@ -67,111 +51,30 @@
|
|||
margin: 0 0 0 4px;
|
||||
}
|
||||
|
||||
.indicator.green {
|
||||
@include indicator-color('green');
|
||||
$indicator-colors: green, cyan, blue, orange, yellow, gray, grey, red, pink, darkgrey, purple, light-blue;
|
||||
@each $color in $indicator-colors {
|
||||
.indicator.#{"" + $color} {
|
||||
&::before,
|
||||
&::after {
|
||||
background: var(--indicator-dot-#{$color});
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-pill.#{"" + $color},
|
||||
.indicator-pill-right.#{"" + $color},
|
||||
.indicator-pill-round.#{"" + $color} {
|
||||
background: var(--bg-#{$color});
|
||||
color: var(--text-on-#{$color});
|
||||
&::before,
|
||||
&::after {
|
||||
background: var(--text-on-#{$color});
|
||||
}
|
||||
}
|
||||
.indicator {
|
||||
--indicator-dot-#{"" + $color}: var(--text-on-#{$color});
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-pill.green,
|
||||
.indicator-pill-right.green,
|
||||
.indicator-pill-round.green {
|
||||
@include indicator-pill-color('green');
|
||||
}
|
||||
|
||||
.indicator.cyan {
|
||||
@include indicator-color('cyan');
|
||||
}
|
||||
|
||||
.indicator-pill.cyan,
|
||||
.indicator-pill-right.cyan,
|
||||
.indicator-pill-round.cyan {
|
||||
@include indicator-pill-color('cyan');
|
||||
}
|
||||
|
||||
.indicator.blue {
|
||||
@include indicator-color('blue');
|
||||
}
|
||||
|
||||
.indicator-pill.blue,
|
||||
.indicator-pill-right.blue,
|
||||
.indicator-pill-round.blue {
|
||||
@include indicator-pill-color('blue');
|
||||
}
|
||||
|
||||
.indicator.orange {
|
||||
@include indicator-color('orange');
|
||||
}
|
||||
|
||||
.indicator-pill.orange,
|
||||
.indicator-pill-right.orange
|
||||
.indicator-pill-round.orange {
|
||||
@include indicator-pill-color('orange');
|
||||
}
|
||||
|
||||
.indicator.yellow {
|
||||
@include indicator-color('yellow');
|
||||
}
|
||||
|
||||
.indicator-pill.yellow,
|
||||
.indicator-pill-right.yellow
|
||||
.indicator-pill-round.yellow {
|
||||
@include indicator-pill-color('yellow');
|
||||
}
|
||||
|
||||
.indicator.gray,
|
||||
.indicator.grey {
|
||||
@include indicator-color('gray');
|
||||
}
|
||||
|
||||
.indicator-pill.gray,
|
||||
.indicator-pill-right.gray,
|
||||
.indicator-pill-round.gray,
|
||||
.indicator-pill.grey,
|
||||
.indicator-pill-right.grey,
|
||||
.indicator-pill-round.grey {
|
||||
@include indicator-pill-color('light-gray');
|
||||
}
|
||||
|
||||
.indicator.red {
|
||||
@include indicator-color('red');
|
||||
}
|
||||
|
||||
.indicator-pill.red,
|
||||
.indicator-pill-right.red,
|
||||
.indicator-pill-round.red {
|
||||
@include indicator-pill-color('red');
|
||||
}
|
||||
|
||||
.indicator.pink {
|
||||
@include indicator-color('pink');
|
||||
}
|
||||
|
||||
.indicator-pill.pink,
|
||||
.indicator-pill-right.pink,
|
||||
.indicator-pill-round.pink {
|
||||
@include indicator-pill-color('pink');
|
||||
}
|
||||
|
||||
.indicator-pill.darkgrey,
|
||||
.indicator-pill-right.darkgrey,
|
||||
.indicator-pill-round.darkgrey {
|
||||
@include indicator-pill-color('gray');
|
||||
}
|
||||
|
||||
.indicator-pill.purple,
|
||||
.indicator-pill-right.purple,
|
||||
.indicator-pill-round.purple {
|
||||
@include indicator-pill-color('purple');
|
||||
}
|
||||
|
||||
.indicator.light-blue {
|
||||
@include indicator-color('light-blue');
|
||||
}
|
||||
|
||||
.indicator-pill.light-blue,
|
||||
.indicator-pill-right.light-blue,
|
||||
.indicator-pill-round.light-blue {
|
||||
@include indicator-pill-color('light-blue');
|
||||
}
|
||||
|
||||
.indicator.blink {
|
||||
animation: blink 1s linear infinite;
|
||||
|
|
|
|||
|
|
@ -47,27 +47,36 @@
|
|||
|
||||
// Background Text Color Pairs
|
||||
--bg-blue: var(--blue-600);
|
||||
--bg-light-blue: var(--blue-400);
|
||||
--bg-light-blue: var(--blue-600);
|
||||
--bg-dark-blue: var(--blue-900);
|
||||
--bg-green: var(--dark-green-500);
|
||||
--bg-yellow: var(--yellow-500);
|
||||
--bg-orange: var(--orange-500);
|
||||
--bg-red: var(--red-500);
|
||||
--bg-gray: var(--gray-600);
|
||||
--bg-green: var(--green-800);
|
||||
--bg-yellow: var(--yellow-700);
|
||||
--bg-orange: var(--orange-700);
|
||||
--bg-red: var(--red-600);
|
||||
--bg-gray: var(--gray-400);
|
||||
--bg-grey: var(--gray-400);
|
||||
--bg-darkgrey: var(--gray-600);
|
||||
--bg-dark-gray: var(--gray-600);
|
||||
--bg-light-gray: var(--gray-700);
|
||||
--bg-dark-gray: var(--gray-300);
|
||||
--bg-purple: var(--purple-600);
|
||||
--bg-purple: var(--purple-700);
|
||||
--bg-pink: var(--pink-700);
|
||||
--bg-cyan: var(--cyan-800);
|
||||
|
||||
--text-on-blue: var(--blue-50);
|
||||
--text-on-light-blue: var(--blue-100);
|
||||
--text-on-light-blue: var(--blue-50);
|
||||
--text-on-dark-blue: var(--blue-300);
|
||||
--text-on-green: var(--dark-green-50);
|
||||
--text-on-yellow: var(--yellow-50);
|
||||
--text-on-orange: var(--orange-100);
|
||||
--text-on-red: var(--red-50);
|
||||
--text-on-gray: var(--gray-300);
|
||||
--text-on-gray: var(--gray-50);
|
||||
--text-on-grey: var(--gray-50);
|
||||
--text-on-darkgrey: var(--gray-200);
|
||||
--text-on-dark-gray: var(--gray-200);
|
||||
--text-on-light-gray: var(--gray-100);
|
||||
--text-on-purple: var(--purple-100);
|
||||
--text-on-pink: var(--pink-100);
|
||||
--text-on-cyan: var(--cyan-100);
|
||||
|
||||
// alert colors
|
||||
--alert-text-danger: var(--red-300);
|
||||
|
|
@ -190,4 +199,11 @@
|
|||
color: var(--text-color);
|
||||
background: var(--gray-500);
|
||||
}
|
||||
|
||||
$indicator-colors: green, cyan, blue, orange, yellow, gray, grey, red, pink, darkgrey, purple, light-blue;
|
||||
@each $color in $indicator-colors {
|
||||
.indicator {
|
||||
--indicator-dot-#{"" + $color}: var(--bg-#{$color});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -424,26 +424,19 @@ kbd {
|
|||
background-color: var(--bg-color);
|
||||
|
||||
.freeze-message-container {
|
||||
inset: 0;
|
||||
padding: 3rem;
|
||||
background-color: var(--bg-light-gray);
|
||||
color: var(--text-on-light-gray);
|
||||
font-size: larger;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.freeze-message {
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
#freeze.dark {
|
||||
background-color: var(--gray-900);
|
||||
}
|
||||
|
||||
#freeze.in {
|
||||
opacity: 0.5;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.msg-box {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from frappe.query_builder.terms import ParameterizedFunction, ParameterizedValue
|
|||
from frappe.query_builder.utils import (
|
||||
Column,
|
||||
DocType,
|
||||
get_qb_engine,
|
||||
get_query,
|
||||
get_query_builder,
|
||||
patch_query_aggregation,
|
||||
patch_query_execute,
|
||||
|
|
|
|||
|
|
@ -119,9 +119,9 @@ class Cast_(Function):
|
|||
|
||||
def _aggregate(function, dt, fieldname, filters, **kwargs):
|
||||
return (
|
||||
frappe.qb.engine.build_conditions(dt, filters)
|
||||
.select(function(PseudoColumn(fieldname)))
|
||||
.run(**kwargs)[0][0]
|
||||
frappe.qb.get_query(dt, filters=filters, fields=[function(PseudoColumn(fieldname))]).run(
|
||||
**kwargs
|
||||
)[0][0]
|
||||
or 0
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ from enum import Enum
|
|||
from importlib import import_module
|
||||
from typing import Any, Callable, get_type_hints
|
||||
|
||||
from pypika import Query
|
||||
from pypika.queries import Column
|
||||
from pypika.queries import Column, QueryBuilder
|
||||
from pypika.terms import PseudoColumn
|
||||
|
||||
import frappe
|
||||
|
|
@ -55,10 +54,10 @@ def get_query_builder(type_of_db: str) -> Postgres | MariaDB:
|
|||
return picks[db]
|
||||
|
||||
|
||||
def get_qb_engine():
|
||||
def get_query(*args, **kwargs) -> QueryBuilder:
|
||||
from frappe.database.query import Engine
|
||||
|
||||
return Engine()
|
||||
return Engine().get_query(*args, **kwargs)
|
||||
|
||||
|
||||
def get_attr(method_string):
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ def get_static_pages_from_all_apps():
|
|||
files_to_index = glob(path_to_index + "/**/*.html", recursive=True)
|
||||
files_to_index.extend(glob(path_to_index + "/**/*.md", recursive=True))
|
||||
for file in files_to_index:
|
||||
route = os.path.relpath(file, path_to_index).split(".")[0]
|
||||
route = os.path.relpath(file, path_to_index).split(".", maxsplit=1)[0]
|
||||
if route.endswith("index"):
|
||||
route = route.rsplit("index", 1)[0]
|
||||
routes_to_index.append(route)
|
||||
|
|
|
|||
|
|
@ -54,9 +54,12 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
|
|||
pass
|
||||
else:
|
||||
# notify creator
|
||||
creator_email = frappe.db.get_value("User", doc.owner, "email") or doc.owner
|
||||
subject = _("New Comment on {0}: {1}").format(doc.doctype, doc.get_title())
|
||||
|
||||
frappe.sendmail(
|
||||
recipients=frappe.db.get_value("User", doc.owner, "email") or doc.owner,
|
||||
subject=_("New Comment on {0}: {1}").format(doc.doctype, doc.name),
|
||||
recipients=creator_email,
|
||||
subject=subject,
|
||||
message=content,
|
||||
reference_doctype=doc.doctype,
|
||||
reference_name=doc.name,
|
||||
|
|
|
|||
|
|
@ -2,22 +2,25 @@ import ast
|
|||
import copy
|
||||
import glob
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import unittest
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
import yaml
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.modules.patch_handler import get_all_patches
|
||||
from frappe.utils.boilerplate import (
|
||||
PatchCreator,
|
||||
_create_app_boilerplate,
|
||||
_get_user_inputs,
|
||||
github_workflow_template,
|
||||
)
|
||||
|
||||
|
||||
class TestBoilerPlate(FrappeTestCase):
|
||||
class TestBoilerPlate(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
|
@ -180,3 +183,30 @@ class TestBoilerPlate(FrappeTestCase):
|
|||
ast.parse(p.read())
|
||||
except Exception as e:
|
||||
self.fail(f"Can't parse python file in new app: {python_file}\n" + str(e))
|
||||
|
||||
def test_new_patch_util(self):
|
||||
user_inputs = {
|
||||
"app_name": "frappe",
|
||||
"doctype": "User",
|
||||
"docstring": "Delete all users",
|
||||
"file_name": "", # Accept default
|
||||
"patch_folder_confirmation": "Y",
|
||||
}
|
||||
|
||||
patches_txt = pathlib.Path(pathlib.Path(frappe.get_app_path("frappe", "patches.txt")))
|
||||
original_patches = patches_txt.read_text()
|
||||
|
||||
with patch("sys.stdin", self.get_user_input_stream(user_inputs)):
|
||||
patch_creator = PatchCreator()
|
||||
patch_creator.fetch_user_inputs()
|
||||
patch_creator.create_patch_file()
|
||||
|
||||
patches = get_all_patches()
|
||||
expected_patch = "frappe.core.doctype.user.patches.delete_all_users"
|
||||
self.assertIn(expected_patch, patches)
|
||||
|
||||
self.assertTrue(patch_creator.patch_file.exists())
|
||||
|
||||
# Cleanup
|
||||
shutil.rmtree(patch_creator.patch_file.parents[0])
|
||||
patches_txt.write_text(original_patches)
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ class TestCommands(BaseTestCommands):
|
|||
# test 2: bare functionality for single site
|
||||
self.execute("bench --site {site} list-apps")
|
||||
self.assertEqual(self.returncode, 0)
|
||||
list_apps = {_x.split()[0] for _x in self.stdout.split("\n")}
|
||||
list_apps = {_x.split(maxsplit=1)[0] for _x in self.stdout.split("\n")}
|
||||
doctype = frappe.get_single("Installed Applications").installed_applications
|
||||
if doctype:
|
||||
installed_apps = {x.app_name for x in doctype}
|
||||
|
|
|
|||
|
|
@ -545,7 +545,7 @@ class TestDB(FrappeTestCase):
|
|||
self.assertEqual((frappe.db.count("Note")), 2)
|
||||
|
||||
# simple filters
|
||||
self.assertEqual((frappe.db.count("Note", ["title", "=", "note1"])), 1)
|
||||
self.assertEqual((frappe.db.count("Note", [["title", "=", "note1"]])), 1)
|
||||
|
||||
frappe.get_doc(doctype="Note", title="note3", content="something other").insert()
|
||||
|
||||
|
|
|
|||
|
|
@ -229,9 +229,7 @@ class TestReportview(FrappeTestCase):
|
|||
)
|
||||
|
||||
def test_none_filter(self):
|
||||
query = frappe.qb.engine.get_query(
|
||||
"DocType", fields="name", filters={"restrict_to_domain": None}
|
||||
)
|
||||
query = frappe.qb.get_query("DocType", fields="name", filters={"restrict_to_domain": None})
|
||||
sql = str(query).replace("`", "").replace('"', "")
|
||||
condition = "restrict_to_domain IS NULL"
|
||||
self.assertIn(condition, sql)
|
||||
|
|
|
|||
18
frappe/tests/test_password_strength.py
Normal file
18
frappe/tests/test_password_strength.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import random
|
||||
from string import printable
|
||||
from time import time
|
||||
from unittest import TestCase
|
||||
|
||||
from frappe.utils.password_strength import test_password_strength
|
||||
|
||||
|
||||
class TestPasswordStrength(TestCase):
|
||||
def test_long_password(self):
|
||||
password = "".join(random.choice(printable) for _ in range(600))
|
||||
|
||||
start_second = time()
|
||||
result = test_password_strength(password)
|
||||
end_second = time()
|
||||
|
||||
self.assertLess(end_second - start_second, 10)
|
||||
self.assertIn("feedback", result)
|
||||
|
|
@ -59,7 +59,7 @@ class TestPatches(FrappeTestCase):
|
|||
else:
|
||||
if patchmodule.startswith("finally:"):
|
||||
patchmodule = patchmodule.split("finally:")[-1]
|
||||
self.assertTrue(frappe.get_attr(patchmodule.split()[0] + ".execute"))
|
||||
self.assertTrue(frappe.get_attr(patchmodule.split(maxsplit=1)[0] + ".execute"))
|
||||
|
||||
frappe.flags.in_install = False
|
||||
|
||||
|
|
@ -149,7 +149,7 @@ def check_patch_files(app):
|
|||
|
||||
patch_dir = Path(frappe.get_app_path(app)) / "patches"
|
||||
|
||||
app_patches = [p.split()[0] for p in patch_handler.get_patches_from_app(app)]
|
||||
app_patches = [p.split(maxsplit=1)[0] for p in patch_handler.get_patches_from_app(app)]
|
||||
|
||||
missing_patches = []
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue