Merge branch 'develop' into permlevel-apis
This commit is contained in:
commit
6b0e4695a8
83 changed files with 828 additions and 489 deletions
|
|
@ -1469,14 +1469,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 +1590,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")
|
||||
|
|
|
|||
|
|
@ -160,11 +160,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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -562,7 +562,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 +1001,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 +1030,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")
|
||||
|
|
@ -1176,6 +1186,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)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ frappe.ui.form.on("Installed Applications", {
|
|||
});
|
||||
|
||||
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();
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,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("):
|
||||
|
|
@ -452,13 +452,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:
|
||||
|
|
|
|||
|
|
@ -36,54 +36,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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -442,7 +442,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:]
|
||||
|
|
@ -974,8 +974,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 self.doctype_meta.sort_field.split(",")
|
||||
if (f_split := f.split(maxsplit=2))
|
||||
)
|
||||
else:
|
||||
sort_field = self.doctype_meta.sort_field or "modified"
|
||||
|
|
@ -1106,8 +1107,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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -896,7 +896,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;
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
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 = []
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class TestSearch(FrappeTestCase):
|
|||
user.update(
|
||||
{
|
||||
"email": email,
|
||||
"first_name": email.split("@")[0],
|
||||
"first_name": email.split("@", 1)[0],
|
||||
"enabled": False,
|
||||
"allowed_in_mentions": True,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ def get_translations_from_apps(lang, apps=None):
|
|||
path = os.path.join(frappe.get_pymodule_path(app), "translations", lang + ".csv")
|
||||
translations.update(get_translation_dict_from_file(path, lang, app) or {})
|
||||
if "-" in lang:
|
||||
parent = lang.split("-")[0]
|
||||
parent = lang.split("-", 1)[0]
|
||||
parent_translations = get_translations_from_apps(parent)
|
||||
parent_translations.update(translations)
|
||||
return parent_translations
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@ def decode_dict(d, encoding="utf-8"):
|
|||
|
||||
@functools.lru_cache
|
||||
def get_site_name(hostname):
|
||||
return hostname.split(":")[0]
|
||||
return hostname.split(":", 1)[0]
|
||||
|
||||
|
||||
def get_disk_usage():
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ class BackupGenerator:
|
|||
|
||||
def backup_time(file_path):
|
||||
file_name = file_path.split(os.sep)[-1]
|
||||
file_timestamp = file_name.split("-")[0]
|
||||
file_timestamp = file_name.split("-", 1)[0]
|
||||
return timegm(datetime.strptime(file_timestamp, "%Y%m%d_%H%M%S").utctimetuple())
|
||||
|
||||
def get_latest(file_pattern):
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import contextlib
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
import click
|
||||
import git
|
||||
|
|
@ -162,6 +166,113 @@ def _create_github_workflow_files(dest, hooks):
|
|||
f.write(github_workflow_template.format(**hooks))
|
||||
|
||||
|
||||
PATCH_TEMPLATE = textwrap.dedent(
|
||||
'''
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
"""{docstring}"""
|
||||
|
||||
# Write your patch here.
|
||||
pass
|
||||
'''
|
||||
)
|
||||
|
||||
|
||||
class PatchCreator:
|
||||
def __init__(self):
|
||||
self.all_apps = frappe.get_all_apps(sites_path=".", with_internal_apps=False)
|
||||
|
||||
self.app = None
|
||||
self.app_dir = None
|
||||
self.patch_dir = None
|
||||
self.filename = None
|
||||
self.docstring = None
|
||||
self.patch_file = None
|
||||
|
||||
def fetch_user_inputs(self):
|
||||
self._ask_app_name()
|
||||
self._ask_doctype_name()
|
||||
self._ask_patch_meta_info()
|
||||
|
||||
def _ask_app_name(self):
|
||||
self.app = click.prompt("Select app for new patch", type=click.Choice(self.all_apps))
|
||||
self.app_dir = pathlib.Path(frappe.get_app_path(self.app))
|
||||
|
||||
def _ask_doctype_name(self):
|
||||
def _doctype_name(filename):
|
||||
with contextlib.suppress(Exception):
|
||||
with open(filename) as f:
|
||||
return json.load(f).get("name")
|
||||
|
||||
doctype_files = list(glob.glob(f"{self.app_dir}/**/doctype/**/*.json"))
|
||||
doctype_map = {_doctype_name(file): file for file in doctype_files}
|
||||
doctype_map.pop(None, None)
|
||||
|
||||
doctype = click.prompt(
|
||||
"Provide DocType name on which this patch will apply",
|
||||
type=click.Choice(doctype_map.keys()),
|
||||
show_choices=False,
|
||||
)
|
||||
self.patch_dir = pathlib.Path(doctype_map[doctype]).parents[0] / "patches"
|
||||
|
||||
def _ask_patch_meta_info(self):
|
||||
self.docstring = click.prompt("Describe what this patch does", type=str)
|
||||
default_filename = frappe.scrub(self.docstring) + ".py"
|
||||
|
||||
def _valid_filename(name):
|
||||
if not name:
|
||||
return
|
||||
|
||||
match name.partition("."):
|
||||
case filename, ".", "py" if filename.isidentifier():
|
||||
return True
|
||||
case _:
|
||||
click.echo(f"{name} is not a valid python file name")
|
||||
|
||||
while not _valid_filename(self.filename):
|
||||
self.filename = click.prompt(
|
||||
"Provide filename for this patch", type=str, default=default_filename
|
||||
)
|
||||
|
||||
def create_patch_file(self):
|
||||
self._create_parent_folder_if_not_exists()
|
||||
|
||||
self.patch_file = self.patch_dir / self.filename
|
||||
|
||||
if self.patch_file.exists():
|
||||
raise Exception(f"Patch {self.patch_file} already exists")
|
||||
|
||||
*path, _filename = self.patch_file.relative_to(self.app_dir.parents[0]).parts
|
||||
dotted_path = ".".join(path + [self.patch_file.stem])
|
||||
|
||||
patches_txt = self.app_dir / "patches.txt"
|
||||
existing_patches = patches_txt.read_text()
|
||||
|
||||
if dotted_path in existing_patches:
|
||||
raise Exception(f"Patch {dotted_path} is already present in patches.txt")
|
||||
|
||||
self.patch_file.write_text(PATCH_TEMPLATE.format(docstring=self.docstring))
|
||||
|
||||
with open(patches_txt, "a+") as f:
|
||||
if not existing_patches.endswith("\n"):
|
||||
f.write("\n") # ensure EOF
|
||||
f.write(dotted_path + "\n")
|
||||
click.echo(f"Created {self.patch_file} and updated patches.txt")
|
||||
|
||||
def _create_parent_folder_if_not_exists(self):
|
||||
if not self.patch_dir.exists():
|
||||
click.confirm(
|
||||
f"Patch folder '{self.patch_dir}' doesn't exist, create it?",
|
||||
abort=True,
|
||||
default=True,
|
||||
)
|
||||
self.patch_dir.mkdir()
|
||||
|
||||
init_py = self.patch_dir / "__init__.py"
|
||||
init_py.touch()
|
||||
|
||||
|
||||
manifest_template = """include MANIFEST.in
|
||||
include requirements.txt
|
||||
include *.json
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ def check_for_update():
|
|||
# Get local instance's current version or the app
|
||||
|
||||
branch_version = (
|
||||
apps[app]["branch_version"].split(" ")[0] if apps[app].get("branch_version", "") else ""
|
||||
apps[app]["branch_version"].split(" ", 1)[0] if apps[app].get("branch_version", "") else ""
|
||||
)
|
||||
instance_version = Version(branch_version or apps[app].get("version"))
|
||||
# Compare and popup update message
|
||||
|
|
|
|||
|
|
@ -1191,7 +1191,7 @@ def fmt_money(
|
|||
if flt(amount) < 0:
|
||||
minus = "-"
|
||||
|
||||
amount = cstr(abs(flt(amount))).split(".")[0]
|
||||
amount = cstr(abs(flt(amount))).split(".", 1)[0]
|
||||
|
||||
if len(amount) > 3:
|
||||
parts.append(amount[-3:])
|
||||
|
|
@ -1348,7 +1348,7 @@ def is_image(filepath: str) -> bool:
|
|||
from mimetypes import guess_type
|
||||
|
||||
# filepath can be https://example.com/bed.jpg?v=129
|
||||
filepath = (filepath or "").split("?")[0]
|
||||
filepath = (filepath or "").split("?", 1)[0]
|
||||
return (guess_type(filepath)[0] or "").startswith("image/")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ def parse_date(date):
|
|||
|
||||
if " " in date:
|
||||
# as date-timestamp, remove the time part
|
||||
date = date.split(" ")[0]
|
||||
date = date.split(" ", 1)[0]
|
||||
|
||||
# why the sorting? checking should be done in a predictable order
|
||||
check_formats = [None] + sorted(
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ def get_snapshot(exception, context=10):
|
|||
|
||||
s = {
|
||||
"pyver": "Python {version:s}: {executable:s} (prefix: {prefix:s})".format(
|
||||
version=sys.version.split()[0], executable=sys.executable, prefix=sys.prefix
|
||||
version=sys.version.split(maxsplit=1)[0], executable=sys.executable, prefix=sys.prefix
|
||||
),
|
||||
"timestamp": cstr(datetime.datetime.now()),
|
||||
"traceback": traceback.format_exc(),
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ def get_routes_to_index():
|
|||
filepath = os.path.join(dirpath, f)
|
||||
|
||||
route = os.path.relpath(filepath, base)
|
||||
route = route.split(".")[0]
|
||||
route = route.split(".", 1)[0]
|
||||
|
||||
if route.endswith("index"):
|
||||
route = route.rsplit("index", 1)[0]
|
||||
|
|
|
|||
|
|
@ -424,6 +424,7 @@ acceptable_attributes = [
|
|||
"cols",
|
||||
"colspan",
|
||||
"compact",
|
||||
"content",
|
||||
"contenteditable",
|
||||
"controls",
|
||||
"coords",
|
||||
|
|
|
|||
|
|
@ -62,7 +62,10 @@ def get_decrypted_password(doctype, name, fieldname="password", raise_exception=
|
|||
return decrypt(result[0][0])
|
||||
|
||||
elif raise_exception:
|
||||
frappe.throw(_("Password not found"), frappe.AuthenticationError)
|
||||
frappe.throw(
|
||||
_("Password not found for {0} {1} {2}").format(doctype, name, fieldname),
|
||||
frappe.AuthenticationError,
|
||||
)
|
||||
|
||||
|
||||
def set_encrypted_password(doctype, name, pwd, fieldname="password"):
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
try:
|
||||
from zxcvbn import zxcvbn
|
||||
except Exception:
|
||||
import zxcvbn
|
||||
from zxcvbn import zxcvbn
|
||||
from zxcvbn.scoring import ALL_UPPER, START_UPPER
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
@ -12,8 +10,14 @@ from frappe import _
|
|||
|
||||
def test_password_strength(password, user_inputs=None):
|
||||
"""Wrapper around zxcvbn.password_strength"""
|
||||
if len(password) > 128:
|
||||
# zxcvbn takes forever when checking long, random passwords.
|
||||
# repetion patterns or user inputs in the first 128 characters
|
||||
# will still be checked.
|
||||
password = password[:128]
|
||||
|
||||
result = zxcvbn(password, user_inputs)
|
||||
result.update({"feedback": get_feedback(result.get("score"), result.get("sequence"))})
|
||||
result["feedback"] = get_feedback(result.get("score"), result.get("sequence"))
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -21,13 +25,7 @@ def test_password_strength(password, user_inputs=None):
|
|||
# -------------------------------------------
|
||||
# feedback functionality code from https://github.com/sans-serif/python-zxcvbn/blob/master/zxcvbn/feedback.py
|
||||
# see license for feedback code at https://github.com/sans-serif/python-zxcvbn/blob/master/LICENSE.txt
|
||||
|
||||
# Used for regex matching capitalization
|
||||
import re
|
||||
|
||||
# Used to get the regex patterns for capitalization
|
||||
# (Used the same way in the original zxcvbn)
|
||||
from zxcvbn import scoring
|
||||
# -------------------------------------------
|
||||
|
||||
# Default feedback value
|
||||
default_feedback = {
|
||||
|
|
@ -177,9 +175,9 @@ def get_dictionary_match_feedback(match, is_sole_match):
|
|||
|
||||
word = match.get("token")
|
||||
# Variations of the match like UPPERCASES
|
||||
if scoring.START_UPPER.match(word):
|
||||
if START_UPPER.match(word):
|
||||
suggestions.append(_("Capitalization doesn't help very much."))
|
||||
elif scoring.ALL_UPPER.match(word):
|
||||
elif ALL_UPPER.match(word):
|
||||
suggestions.append(_("All-uppercase is almost as easy to guess as all-lowercase."))
|
||||
|
||||
# Match contains l33t speak substitutions
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class Blogger(Document):
|
|||
if self.user and not frappe.db.exists("User", self.user):
|
||||
# for data import
|
||||
frappe.get_doc(
|
||||
{"doctype": "User", "email": self.user, "first_name": self.user.split("@")[0]}
|
||||
{"doctype": "User", "email": self.user, "first_name": self.user.split("@", 1)[0]}
|
||||
).insert()
|
||||
|
||||
def on_update(self):
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ def create_user_if_not_exists(email, first_name=None):
|
|||
"user_type": "Website User",
|
||||
"email": email,
|
||||
"send_welcome_email": 0,
|
||||
"first_name": first_name or email.split("@")[0],
|
||||
"first_name": first_name or email.split("@", 1)[0],
|
||||
"birth_date": frappe.utils.now_datetime(),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ class WebPage(WebsiteGenerator):
|
|||
def check_for_redirect(self, context):
|
||||
if "<!-- redirect:" in context.main_section:
|
||||
frappe.local.flags.redirect_location = (
|
||||
context.main_section.split("<!-- redirect:")[1].split("-->")[0].strip()
|
||||
context.main_section.split("<!-- redirect:", 2)[1].split("-->", 1)[0].strip()
|
||||
)
|
||||
raise frappe.Redirect
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ def make_view_log(path, referrer=None, browser=None, version=None, url=None, use
|
|||
user_agent = request_dict.get("environ", {}).get("HTTP_USER_AGENT")
|
||||
|
||||
if referrer:
|
||||
referrer = referrer.split("?")[0]
|
||||
referrer = referrer.split("?", 1)[0]
|
||||
|
||||
is_unique = True
|
||||
if referrer.startswith(url):
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ dependencies = [
|
|||
"terminaltables~=3.1.0",
|
||||
"traceback-with-variables~=2.0.4",
|
||||
"xlrd~=2.0.1",
|
||||
"zxcvbn-python~=4.4.24",
|
||||
"zxcvbn~=4.4.28",
|
||||
"markdownify~=0.11.2",
|
||||
|
||||
# integration dependencies
|
||||
|
|
|
|||
|
|
@ -742,9 +742,9 @@ cookie@^0.4.0, cookie@~0.4.1:
|
|||
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
|
||||
|
||||
cookiejar@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
|
||||
integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
|
||||
integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==
|
||||
|
||||
copy-anything@^2.0.1:
|
||||
version "2.0.3"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue