Merge branch 'develop' into permlevel-apis

This commit is contained in:
Gavin D'souza 2023-01-25 12:08:47 +05:30
commit 6b0e4695a8
83 changed files with 828 additions and 489 deletions

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -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,

View file

@ -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

View file

@ -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
}
}

View file

@ -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)):

View file

@ -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)

View file

@ -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)

View file

@ -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();
});
},

View file

@ -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"))

View file

@ -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": [
{

View file

@ -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");
}
);
});
}
},

View file

@ -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",

View file

@ -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]

View file

@ -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",)

View file

@ -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(

View file

@ -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)

View file

@ -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:

View file

@ -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():

View file

@ -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")

View file

@ -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("`")

View file

@ -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))

View file

@ -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"

View file

@ -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

View file

@ -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:

View file

@ -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:

View file

@ -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",

View file

@ -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

View file

@ -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:

View file

@ -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",

View file

@ -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(

View file

@ -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:

View file

@ -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)

View file

@ -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,
)

View file

@ -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),
)

View file

@ -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())

View file

@ -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 ""

View file

@ -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++;
}
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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="{

View file

@ -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") }}

View file

@ -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) {

View file

@ -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);
}

View file

@ -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");
}
};

View file

@ -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();
}
});
}

View file

@ -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) {

View file

@ -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 += `

View file

@ -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") },

View file

@ -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;

View file

@ -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",

View file

@ -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) {

View file

@ -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);
}

View file

@ -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

View file

@ -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;
}
}
}
}

View file

@ -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;

View file

@ -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});
}
}
}

View file

@ -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)

View file

@ -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,

View file

@ -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)

View file

@ -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}

View 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)

View file

@ -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 = []

View file

@ -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,
}

View file

@ -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

View file

@ -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():

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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/")

View file

@ -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(

View file

@ -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(),

View file

@ -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]

View file

@ -424,6 +424,7 @@ acceptable_attributes = [
"cols",
"colspan",
"compact",
"content",
"contenteditable",
"controls",
"coords",

View file

@ -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"):

View file

@ -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

View file

@ -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):

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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"