Merge branch 'develop' into undebounce-autocomplete
This commit is contained in:
commit
0b95b28bfe
102 changed files with 13633 additions and 19460 deletions
|
|
@ -30,7 +30,7 @@ context("Navigation", () => {
|
|||
cy.get("@reload").get(".page-card .btn-primary").contains("Login").click();
|
||||
cy.location("pathname").should("eq", "/login");
|
||||
cy.login();
|
||||
cy.visit("/app");
|
||||
cy.reload().as("reload");
|
||||
cy.location("pathname").should("eq", "/app/todo");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None)
|
|||
from frappe.database.mariadb.database import MariaDBDatabase
|
||||
|
||||
return {
|
||||
"mariadb": MariaDBDatabase.default_port, # 3306
|
||||
"mariadb": MariaDBDatabase.default_port,
|
||||
"postgres": 5432,
|
||||
}[db_type]
|
||||
|
||||
|
|
@ -435,7 +435,7 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None)
|
|||
config["db_type"] = os.environ.get("FRAPPE_DB_TYPE") or config.get("db_type") or "mariadb"
|
||||
config["db_socket"] = os.environ.get("FRAPPE_DB_SOCKET") or config.get("db_socket")
|
||||
config["db_host"] = os.environ.get("FRAPPE_DB_HOST") or config.get("db_host") or "127.0.0.1"
|
||||
config["db_port"] = (
|
||||
config["db_port"] = int(
|
||||
os.environ.get("FRAPPE_DB_PORT") or config.get("db_port") or db_default_ports(config["db_type"])
|
||||
)
|
||||
|
||||
|
|
@ -2572,8 +2572,8 @@ def _register_fault_handler():
|
|||
import io
|
||||
|
||||
# Some libraries monkey patch stderr, we need actual fd
|
||||
if isinstance(sys.stderr, io.TextIOWrapper):
|
||||
faulthandler.register(signal.SIGUSR1, file=sys.stderr)
|
||||
if isinstance(sys.__stderr__, io.TextIOWrapper):
|
||||
faulthandler.register(signal.SIGUSR1, file=sys.__stderr__)
|
||||
|
||||
|
||||
from frappe.utils.error import log_error
|
||||
|
|
|
|||
|
|
@ -115,6 +115,11 @@ def delete_doc(doctype: str, name: str):
|
|||
return "ok"
|
||||
|
||||
|
||||
def get_meta(doctype: str):
|
||||
frappe.only_for("All")
|
||||
return frappe.get_meta(doctype)
|
||||
|
||||
|
||||
def execute_doc_method(doctype: str, name: str, method: str | None = None):
|
||||
"""Get a document from DB and execute method on it.
|
||||
|
||||
|
|
@ -188,6 +193,6 @@ url_rules = [
|
|||
endpoint=execute_doc_method,
|
||||
),
|
||||
# Collection level APIs
|
||||
Rule("/doctype/<doctype>/meta", methods=["GET"], endpoint=frappe.get_meta),
|
||||
Rule("/doctype/<doctype>/meta", methods=["GET"], endpoint=get_meta),
|
||||
Rule("/doctype/<doctype>/count", methods=["GET"], endpoint=count),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ def after_response_wrapper(app):
|
|||
app(environ, start_response),
|
||||
(
|
||||
frappe.rate_limiter.update,
|
||||
frappe.monitor.stop,
|
||||
frappe.recorder.dump,
|
||||
frappe.request.after_response.run,
|
||||
frappe.destroy,
|
||||
|
|
|
|||
|
|
@ -421,7 +421,18 @@ def clear_cookies():
|
|||
|
||||
|
||||
def validate_ip_address(user):
|
||||
"""check if IP Address is valid"""
|
||||
"""
|
||||
Method to check if the user has IP restrictions enabled, and if so is the IP address they are
|
||||
connecting from allowlisted.
|
||||
|
||||
Certain methods called from our socketio backend need direct access, and so the IP is not
|
||||
checked for those
|
||||
"""
|
||||
if hasattr(frappe.local, "request") and frappe.local.request.path.startswith(
|
||||
"/api/method/frappe.realtime."
|
||||
):
|
||||
return True
|
||||
|
||||
from frappe.core.doctype.user.user import get_restricted_ip_list
|
||||
|
||||
# Only fetch required fields - for perf
|
||||
|
|
|
|||
|
|
@ -120,7 +120,10 @@ def get_bootinfo():
|
|||
|
||||
def get_letter_heads():
|
||||
letter_heads = {}
|
||||
for letter_head in frappe.get_all("Letter Head", fields=["name", "content", "footer"]):
|
||||
|
||||
if not frappe.has_permission("Letter Head"):
|
||||
return letter_heads
|
||||
for letter_head in frappe.get_list("Letter Head", fields=["name", "content", "footer"]):
|
||||
letter_heads.setdefault(
|
||||
letter_head.name, {"header": letter_head.content, "footer": letter_head.footer}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -174,8 +174,11 @@ def purge_jobs(site=None, queue=None, event=None):
|
|||
@click.command("schedule")
|
||||
def start_scheduler():
|
||||
"""Start scheduler process which is responsible for enqueueing the scheduled job types."""
|
||||
import time
|
||||
|
||||
from frappe.utils.scheduler import start_scheduler
|
||||
|
||||
time.sleep(0.5) # Delayed start. TODO: find better way to handle this.
|
||||
start_scheduler()
|
||||
|
||||
|
||||
|
|
@ -222,12 +225,7 @@ def start_worker_pool(queue, quiet=False, num_workers=2, burst=False):
|
|||
"""Start a pool of background workers"""
|
||||
from frappe.utils.background_jobs import start_worker_pool
|
||||
|
||||
start_worker_pool(
|
||||
queue=queue,
|
||||
quiet=quiet,
|
||||
burst=burst,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
start_worker_pool(queue=queue, quiet=quiet, burst=burst, num_workers=num_workers)
|
||||
|
||||
|
||||
@click.command("ready-for-migration")
|
||||
|
|
|
|||
|
|
@ -1501,6 +1501,33 @@ def add_new_user(
|
|||
update_password(user=user.name, pwd=password)
|
||||
|
||||
|
||||
@click.command("bypass-patch")
|
||||
@click.argument("patch_name")
|
||||
@click.option("--yes", "-y", is_flag=True, default=False, help="Pass --yes to skip confirmation")
|
||||
@pass_context
|
||||
def bypass_patch(context, patch_name: str, yes: bool):
|
||||
"""Bypass a patch permanently instead of migrating using the --skip-failing flag."""
|
||||
from frappe.modules.patch_handler import update_patch_log
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
if not yes:
|
||||
click.confirm(
|
||||
f"This will bypass the patch {patch_name!r} forever and register it as successful.\nAre you sure you want to continue?",
|
||||
abort=True,
|
||||
)
|
||||
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
try:
|
||||
update_patch_log(patch_name)
|
||||
frappe.db.commit()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
commands = [
|
||||
add_system_manager,
|
||||
add_user_for_sites,
|
||||
|
|
@ -1535,4 +1562,5 @@ commands = [
|
|||
trim_tables,
|
||||
trim_database,
|
||||
clear_log_table,
|
||||
bypass_patch,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -288,9 +288,21 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
else:
|
||||
search_condition += f" or `tabAddress`.`{field}` like %(txt)s"
|
||||
|
||||
# Use custom title field if set
|
||||
if meta.show_title_field_in_link and meta.title_field:
|
||||
title = f"`tabAddress`.{meta.title_field}"
|
||||
else:
|
||||
title = "`tabAddress`.city"
|
||||
|
||||
# Get additional search fields
|
||||
if searchfields:
|
||||
extra_query_fields = ",".join([f"`tabAddress`.{field}" for field in searchfields])
|
||||
else:
|
||||
extra_query_fields = "`tabAddress`.country"
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select
|
||||
`tabAddress`.name, `tabAddress`.city, `tabAddress`.country
|
||||
`tabAddress`.name, {title}, {extra_query_fields}
|
||||
from
|
||||
`tabAddress`
|
||||
join `tabDynamic Link`
|
||||
|
|
@ -312,6 +324,8 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
mcond=get_match_cond(doctype),
|
||||
search_condition=search_condition,
|
||||
condition=condition or "",
|
||||
title=title,
|
||||
extra_query_fields=extra_query_fields,
|
||||
),
|
||||
{
|
||||
"txt": "%" + txt + "%",
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class Contact(Document):
|
|||
|
||||
# concat party name if reqd
|
||||
for link in self.links:
|
||||
self.name = self.name + "-" + link.link_name.strip()
|
||||
self.name = self.name + "-" + cstr(link.link_name).strip()
|
||||
break
|
||||
|
||||
if frappe.db.exists("Contact", self.name):
|
||||
|
|
@ -129,6 +129,9 @@ class Contact(Document):
|
|||
if len([email.email_id for email in self.email_ids if email.is_primary]) > 1:
|
||||
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold("Email ID")))
|
||||
|
||||
if len(self.email_ids) == 1:
|
||||
self.email_ids[0].is_primary = 1
|
||||
|
||||
primary_email_exists = False
|
||||
for d in self.email_ids:
|
||||
if d.is_primary == 1:
|
||||
|
|
|
|||
|
|
@ -118,7 +118,6 @@ class Exporter:
|
|||
for doc in data:
|
||||
rows = []
|
||||
rows = self.add_data_row(self.doctype, None, doc, rows, 0)
|
||||
|
||||
if table_fields:
|
||||
# add child table data
|
||||
for f in table_fields:
|
||||
|
|
@ -144,6 +143,8 @@ class Exporter:
|
|||
if df.fieldtype == "Duration":
|
||||
value = format_duration(flt(value), df.hide_days)
|
||||
|
||||
if df.fieldtype == "Text Editor":
|
||||
value = frappe.core.utils.html2text(value)
|
||||
row[i] = value
|
||||
return rows
|
||||
|
||||
|
|
|
|||
|
|
@ -870,8 +870,10 @@ class DocType(Document):
|
|||
def make_amendable(self):
|
||||
"""If is_submittable is set, add amended_from docfields."""
|
||||
if self.is_submittable:
|
||||
docfield_exists = [f for f in self.fields if f.fieldname == "amended_from"]
|
||||
if not docfield_exists:
|
||||
docfield = [f for f in self.fields if f.fieldname == "amended_from"]
|
||||
if docfield:
|
||||
docfield[0].options = self.name
|
||||
else:
|
||||
self.append(
|
||||
"fields",
|
||||
{
|
||||
|
|
@ -903,7 +905,7 @@ class DocType(Document):
|
|||
no_copy=1,
|
||||
print_hide=1,
|
||||
)
|
||||
create_custom_field(self.name, df)
|
||||
create_custom_field(self.name, df, ignore_validate=True)
|
||||
|
||||
def validate_nestedset(self):
|
||||
if not self.get("is_tree"):
|
||||
|
|
@ -1558,9 +1560,21 @@ def validate_fields(meta: Meta):
|
|||
options_list.append(_option)
|
||||
field.options = "\n".join(options_list)
|
||||
|
||||
def scrub_fetch_from(field):
|
||||
if hasattr(field, "fetch_from") and field.fetch_from:
|
||||
field.fetch_from = field.fetch_from.strip("\n").strip()
|
||||
def validate_fetch_from(field):
|
||||
if not field.get("fetch_from"):
|
||||
return
|
||||
|
||||
field.fetch_from = field.fetch_from.strip()
|
||||
|
||||
if "." not in field.fetch_from:
|
||||
return
|
||||
source_field, _target_field = field.fetch_from.split(".", maxsplit=1)
|
||||
|
||||
if source_field == field.fieldname:
|
||||
msg = _(
|
||||
"{0} contains an invalid Fetch From expression, Fetch From can't be self-referential."
|
||||
).format(_(field.label, context=field.parent))
|
||||
frappe.throw(msg, title=_("Recursive Fetch From"))
|
||||
|
||||
def validate_data_field_type(docfield):
|
||||
if docfield.get("is_virtual"):
|
||||
|
|
@ -1636,7 +1650,7 @@ def validate_fields(meta: Meta):
|
|||
check_unique_and_text(meta.get("name"), d)
|
||||
check_table_multiselect_option(d)
|
||||
scrub_options_in_select(d)
|
||||
scrub_fetch_from(d)
|
||||
validate_fetch_from(d)
|
||||
validate_data_field_type(d)
|
||||
|
||||
if not frappe.flags.in_migrate or in_ci:
|
||||
|
|
|
|||
|
|
@ -774,6 +774,19 @@ class TestDocType(FrappeTestCase):
|
|||
self.assertTrue(doctype.fields[1].in_list_view)
|
||||
frappe.delete_doc("DocType", doctype.name)
|
||||
|
||||
def test_no_recursive_fetch(self):
|
||||
recursive_dt = new_doctype(
|
||||
fields=[
|
||||
{
|
||||
"label": "User",
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"fetch_from": "user.email",
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, recursive_dt.insert)
|
||||
|
||||
|
||||
def new_doctype(
|
||||
name: str | None = None,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@
|
|||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Reference Name",
|
||||
"read_only": 1
|
||||
},
|
||||
|
|
@ -70,7 +72,7 @@
|
|||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:03:25.381120",
|
||||
"modified": "2024-06-05 05:34:35.048489",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Error Log",
|
||||
|
|
@ -92,4 +94,4 @@
|
|||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "method"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ class ErrorLog(Document):
|
|||
trace_id: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.method = str(self.method)
|
||||
self.error = str(self.error)
|
||||
|
||||
if len(self.method) > 140:
|
||||
self.error = f"{self.method}\n{self.error}"
|
||||
self.method = self.method[:140]
|
||||
|
||||
def onload(self):
|
||||
if not self.seen and not frappe.flags.read_only:
|
||||
self.db_set("seen", 1, update_modified=0)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,16 @@ frappe.ui.form.on("File", {
|
|||
if (frm.doc.file_name) {
|
||||
file_url = file_url.replace(/#/g, "%23");
|
||||
}
|
||||
window.open(file_url);
|
||||
|
||||
// create temporary link element to simulate a download click
|
||||
var link = document.createElement("a");
|
||||
link.href = file_url;
|
||||
link.download = frm.doc.file_name;
|
||||
link.style.display = "none";
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
|
||||
optimize: function (frm) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:language_code",
|
||||
"creation": "2014-08-22 16:12:17.249590",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -27,7 +26,8 @@
|
|||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Language Name",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "flag",
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
"icon": "fa fa-globe",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:03:28.477169",
|
||||
"modified": "2024-06-06 18:25:01.010821",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Language",
|
||||
|
|
|
|||
|
|
@ -122,5 +122,8 @@ class PackageRelease(Document):
|
|||
attached_to_name=self.name,
|
||||
)
|
||||
|
||||
# Set path to tarball
|
||||
self.path = file.file_url
|
||||
|
||||
file.flags.ignore_duplicate_entry_error = True
|
||||
file.insert()
|
||||
|
|
|
|||
|
|
@ -114,9 +114,8 @@ def generate_report(prepared_report):
|
|||
|
||||
instance.status = "Completed"
|
||||
except Exception:
|
||||
instance.status = "Error"
|
||||
instance.error_message = frappe.get_traceback(with_context=True)
|
||||
_save_instance(instance) # we need to ensure that error gets stored
|
||||
# we need to ensure that error gets stored
|
||||
_save_error(instance, error=frappe.get_traceback(with_context=True))
|
||||
|
||||
instance.report_end_time = frappe.utils.now()
|
||||
instance.save(ignore_permissions=True)
|
||||
|
|
@ -129,7 +128,10 @@ def generate_report(prepared_report):
|
|||
|
||||
|
||||
@dangerously_reconnect_on_connection_abort
|
||||
def _save_instance(instance):
|
||||
def _save_error(instance, error):
|
||||
instance.reload()
|
||||
instance.status = "Error"
|
||||
instance.error_message = error
|
||||
instance.save(ignore_permissions=True)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -71,8 +71,9 @@ class ScheduledJobType(Document):
|
|||
enqueue(
|
||||
"frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job",
|
||||
queue=self.get_queue_name(),
|
||||
job_type=self.method,
|
||||
job_type=self.method, # Not actually used, kept for logging
|
||||
job_id=self.rq_job_id,
|
||||
scheduled_job_type=self.name,
|
||||
)
|
||||
return True
|
||||
else:
|
||||
|
|
@ -93,7 +94,7 @@ class ScheduledJobType(Document):
|
|||
@property
|
||||
def rq_job_id(self):
|
||||
"""Unique ID created to deduplicate jobs with single RQ call."""
|
||||
return f"scheduled_job::{self.method}"
|
||||
return f"scheduled_job::{self.name}"
|
||||
|
||||
@property
|
||||
def next_execution(self):
|
||||
|
|
@ -188,10 +189,10 @@ def execute_event(doc: str):
|
|||
return doc
|
||||
|
||||
|
||||
def run_scheduled_job(job_type: str):
|
||||
def run_scheduled_job(scheduled_job_type: str, job_type: str | None = None):
|
||||
"""This is a wrapper function that runs a hooks.scheduler_events method"""
|
||||
try:
|
||||
frappe.get_doc("Scheduled Job Type", dict(method=job_type)).execute()
|
||||
frappe.get_doc("Scheduled Job Type", scheduled_job_type).execute()
|
||||
except Exception:
|
||||
print(frappe.get_traceback())
|
||||
|
||||
|
|
@ -199,8 +200,8 @@ def run_scheduled_job(job_type: str):
|
|||
def sync_jobs(hooks: dict | None = None):
|
||||
frappe.reload_doc("core", "doctype", "scheduled_job_type")
|
||||
scheduler_events = hooks or frappe.get_hooks("scheduler_events")
|
||||
all_events = insert_events(scheduler_events)
|
||||
clear_events(all_events)
|
||||
insert_events(scheduler_events)
|
||||
clear_events(scheduler_events)
|
||||
|
||||
|
||||
def insert_events(scheduler_events: dict) -> list:
|
||||
|
|
@ -261,12 +262,19 @@ def insert_single_event(frequency: str, event: str, cron_format: str | None = No
|
|||
doc.insert()
|
||||
|
||||
|
||||
def clear_events(all_events: list):
|
||||
for event in frappe.get_all("Scheduled Job Type", fields=["name", "method", "server_script"]):
|
||||
is_server_script = event.server_script
|
||||
is_defined_in_hooks = event.method in all_events
|
||||
def clear_events(scheduler_events: dict):
|
||||
def event_exists(event) -> bool:
|
||||
if event.server_script:
|
||||
return True
|
||||
|
||||
if not (is_defined_in_hooks or is_server_script):
|
||||
freq = frappe.scrub(event.frequency)
|
||||
if freq == "cron":
|
||||
return event.method in scheduler_events.get(freq, {}).get(event.cron_format, [])
|
||||
else:
|
||||
return event.method in scheduler_events.get(freq, [])
|
||||
|
||||
for event in frappe.get_all("Scheduled Job Type", fields=["*"]):
|
||||
if not event_exists(event):
|
||||
frappe.delete_doc("Scheduled Job Type", event.name)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class User(Document):
|
|||
if self.name not in STANDARD_USERS:
|
||||
self.email = self.name
|
||||
self.validate_email_type(self.name)
|
||||
self.add_system_manager_role()
|
||||
|
||||
self.move_role_profile_name_to_role_profiles()
|
||||
self.populate_role_profile_roles()
|
||||
self.check_roles_added()
|
||||
|
|
@ -279,9 +279,6 @@ class User(Document):
|
|||
if not cint(self.enabled) and self.name in STANDARD_USERS:
|
||||
frappe.throw(_("User {0} cannot be disabled").format(self.name))
|
||||
|
||||
if not cint(self.enabled):
|
||||
self.a_system_manager_should_exist()
|
||||
|
||||
# clear sessions if disabled
|
||||
if not cint(self.enabled) and getattr(frappe.local, "login_manager", None):
|
||||
frappe.local.login_manager.logout(user=self.name)
|
||||
|
|
@ -289,38 +286,6 @@ class User(Document):
|
|||
# toggle notifications based on the user's status
|
||||
toggle_notifications(self.name, enable=cint(self.enabled), ignore_permissions=True)
|
||||
|
||||
def add_system_manager_role(self):
|
||||
if self.is_system_manager_disabled():
|
||||
return
|
||||
|
||||
# if adding system manager, do nothing
|
||||
if not cint(self.enabled) or (
|
||||
"System Manager" in [user_role.role for user_role in self.get("roles")]
|
||||
):
|
||||
return
|
||||
|
||||
if (
|
||||
self.name not in STANDARD_USERS
|
||||
and self.user_type == "System User"
|
||||
and not self.get_other_system_managers()
|
||||
and cint(frappe.db.get_single_value("System Settings", "setup_complete"))
|
||||
):
|
||||
msgprint(_("Adding System Manager to this User as there must be atleast one System Manager"))
|
||||
self.append("roles", {"doctype": "Has Role", "role": "System Manager"})
|
||||
|
||||
if self.name == "Administrator":
|
||||
# Administrator should always have System Manager Role
|
||||
self.extend(
|
||||
"roles",
|
||||
[
|
||||
{"doctype": "Has Role", "role": "System Manager"},
|
||||
{"doctype": "Has Role", "role": "Administrator"},
|
||||
],
|
||||
)
|
||||
|
||||
def is_system_manager_disabled(self):
|
||||
return frappe.db.get_value("Role", {"name": "System Manager"}, ["disabled"])
|
||||
|
||||
def email_new_password(self, new_password=None):
|
||||
if new_password and not self.flags.in_insert:
|
||||
_update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions)
|
||||
|
|
@ -430,20 +395,6 @@ class User(Document):
|
|||
|
||||
return link
|
||||
|
||||
def get_other_system_managers(self):
|
||||
user_doctype = DocType("User").as_("user")
|
||||
user_role_doctype = DocType("Has Role").as_("user_role")
|
||||
return (
|
||||
frappe.qb.from_(user_doctype)
|
||||
.from_(user_role_doctype)
|
||||
.select(user_doctype.name)
|
||||
.where(user_role_doctype.role == "System Manager")
|
||||
.where(user_doctype.enabled == 1)
|
||||
.where(user_role_doctype.parent == user_doctype.name)
|
||||
.where(user_role_doctype.parent.notin(["Administrator", self.name]))
|
||||
.limit(1)
|
||||
).run()
|
||||
|
||||
def get_fullname(self):
|
||||
"""get first_name space last_name"""
|
||||
return (self.first_name or "") + (self.first_name and " " or "") + (self.last_name or "")
|
||||
|
|
@ -528,20 +479,11 @@ class User(Document):
|
|||
retry=3,
|
||||
)
|
||||
|
||||
def a_system_manager_should_exist(self):
|
||||
if self.is_system_manager_disabled():
|
||||
return
|
||||
|
||||
if not self.get_other_system_managers():
|
||||
throw(_("There should remain at least one System Manager"))
|
||||
|
||||
def on_trash(self):
|
||||
frappe.clear_cache(user=self.name)
|
||||
if self.name in STANDARD_USERS:
|
||||
throw(_("User {0} cannot be deleted").format(self.name))
|
||||
|
||||
self.a_system_manager_should_exist()
|
||||
|
||||
# disable the user and log him/her out
|
||||
self.enabled = 0
|
||||
if getattr(frappe.local, "login_manager", None):
|
||||
|
|
@ -601,6 +543,10 @@ class User(Document):
|
|||
frappe.throw(_("You can disable the user instead of deleting it."), frappe.LinkExistsError)
|
||||
|
||||
def before_rename(self, old_name, new_name, merge=False):
|
||||
# if merging, delete the old user notification settings
|
||||
if merge:
|
||||
frappe.delete_doc("Notification Settings", old_name, ignore_permissions=True)
|
||||
|
||||
frappe.clear_cache(user=old_name)
|
||||
self.validate_rename(old_name, new_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -335,10 +335,14 @@ frappe.ui.form.on("Customize Form Field", {
|
|||
},
|
||||
});
|
||||
|
||||
let parenttype, parent; // used in the form events for the child tables: links, actions and states
|
||||
|
||||
// can't delete standard links
|
||||
frappe.ui.form.on("DocType Link", {
|
||||
before_links_remove: function (frm, doctype, name) {
|
||||
let row = frappe.get_doc(doctype, name);
|
||||
parenttype = row.parenttype; // used in the event links_remove
|
||||
parent = row.parent; // used in the event links_remove
|
||||
if (!(row.custom || row.__islocal)) {
|
||||
frappe.msgprint(__("Cannot delete standard link. You can hide it if you want"));
|
||||
throw "cannot delete standard link";
|
||||
|
|
@ -348,12 +352,19 @@ frappe.ui.form.on("DocType Link", {
|
|||
let f = frappe.model.get_doc(cdt, cdn);
|
||||
f.custom = 1;
|
||||
},
|
||||
links_remove: function (frm, doctype, name) {
|
||||
// replicate the changed rows from the browser's copy of the parent doc to the current 'Customize Form' doc
|
||||
let parent_doc = locals[parenttype][parent];
|
||||
frm.doc.links = parent_doc.links;
|
||||
},
|
||||
});
|
||||
|
||||
// can't delete standard actions
|
||||
frappe.ui.form.on("DocType Action", {
|
||||
before_actions_remove: function (frm, doctype, name) {
|
||||
let row = frappe.get_doc(doctype, name);
|
||||
parenttype = row.parenttype; // used in the event actions_remove
|
||||
parent = row.parent; // used in the event actions_remove
|
||||
if (!(row.custom || row.__islocal)) {
|
||||
frappe.msgprint(__("Cannot delete standard action. You can hide it if you want"));
|
||||
throw "cannot delete standard action";
|
||||
|
|
@ -363,12 +374,19 @@ frappe.ui.form.on("DocType Action", {
|
|||
let f = frappe.model.get_doc(cdt, cdn);
|
||||
f.custom = 1;
|
||||
},
|
||||
actions_remove: function (frm, doctype, name) {
|
||||
// replicate the changed rows from the browser's copy of the parent doc to the current 'Customize Form' doc
|
||||
let parent_doc = locals[parenttype][parent];
|
||||
frm.doc.actions = parent_doc.actions;
|
||||
},
|
||||
});
|
||||
|
||||
// can't delete standard states
|
||||
frappe.ui.form.on("DocType State", {
|
||||
before_states_remove: function (frm, doctype, name) {
|
||||
let row = frappe.get_doc(doctype, name);
|
||||
parenttype = row.parenttype; // used in the event states_remove
|
||||
parent = row.parent; // used in the event states_remove
|
||||
if (!(row.custom || row.__islocal)) {
|
||||
frappe.msgprint(__("Cannot delete standard document state."));
|
||||
throw "cannot delete standard document state";
|
||||
|
|
@ -378,6 +396,11 @@ frappe.ui.form.on("DocType State", {
|
|||
let f = frappe.model.get_doc(cdt, cdn);
|
||||
f.custom = 1;
|
||||
},
|
||||
states_remove: function (frm, doctype, name) {
|
||||
// replicate the changed rows from the browser's copy of the parent doc to the current 'Customize Form' doc
|
||||
let parent_doc = locals[parenttype][parent];
|
||||
frm.doc.states = parent_doc.states;
|
||||
},
|
||||
});
|
||||
|
||||
frappe.customize_form.save_customization = function (frm) {
|
||||
|
|
|
|||
|
|
@ -200,7 +200,12 @@ class DbColumn:
|
|||
self.not_nullable = not_nullable
|
||||
|
||||
def get_definition(self, for_modification=False):
|
||||
column_def = get_definition(self.fieldtype, precision=self.precision, length=self.length)
|
||||
column_def = get_definition(
|
||||
self.fieldtype,
|
||||
precision=self.precision,
|
||||
length=self.length,
|
||||
options=self.options,
|
||||
)
|
||||
|
||||
if not column_def:
|
||||
return column_def
|
||||
|
|
@ -356,9 +361,20 @@ def validate_column_length(fieldname):
|
|||
frappe.throw(_("Fieldname is limited to 64 characters ({0})").format(fieldname))
|
||||
|
||||
|
||||
def get_definition(fieldtype, precision=None, length=None):
|
||||
def get_definition(fieldtype, precision=None, length=None, *, options=None):
|
||||
d = frappe.db.type_map.get(fieldtype)
|
||||
|
||||
if (
|
||||
fieldtype == "Link"
|
||||
and options
|
||||
# XXX: This might not trigger if referred doctype is not yet created
|
||||
# This is largely limitation of how migration happens though.
|
||||
# Maybe we can sort by creation and then modified?
|
||||
and frappe.db.exists("DocType", options)
|
||||
and frappe.get_meta(options).autoname == "UUID"
|
||||
):
|
||||
d = ("uuid", None)
|
||||
|
||||
if not d:
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -469,7 +469,11 @@ def get_workspace_sidebar_items():
|
|||
pages = [frappe.get_doc("Workspace", "Welcome Workspace").as_dict()]
|
||||
pages[0]["label"] = _("Welcome Workspace")
|
||||
|
||||
return {"pages": pages, "has_access": has_access}
|
||||
return {
|
||||
"pages": pages,
|
||||
"has_access": has_access,
|
||||
"has_create_access": frappe.has_permission(doctype="Workspace", ptype="create"),
|
||||
}
|
||||
|
||||
|
||||
def get_table_with_counts():
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@
|
|||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Standard",
|
||||
"no_copy": 1,
|
||||
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
|
||||
},
|
||||
{
|
||||
|
|
@ -288,7 +289,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:02:16.230433",
|
||||
"modified": "2024-06-03 13:29:57.960271",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Chart",
|
||||
|
|
|
|||
|
|
@ -147,7 +147,8 @@ def update_order_for_single_card(board_name, docname, from_colname, to_colname,
|
|||
if from_colname == to_colname:
|
||||
from_col_order = to_col_order
|
||||
|
||||
to_col_order.insert(new_index, from_col_order.pop(old_index))
|
||||
if from_col_order:
|
||||
to_col_order.insert(new_index, from_col_order.pop(old_index))
|
||||
|
||||
# save updated order
|
||||
board.columns[from_col_idx].order = frappe.as_json(from_col_order)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Indicator",
|
||||
"options": "Blue\nCyan\nGray\nGreen\nLight Blue\nOrange\nPink\nPurple\nRed\nRed\nYellow"
|
||||
"options": "Blue\nCyan\nGray\nGreen\nLight Blue\nOrange\nPink\nPurple\nRed\nYellow"
|
||||
},
|
||||
{
|
||||
"fieldname": "order",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class KanbanBoardColumn(Document):
|
|||
|
||||
column_name: DF.Data | None
|
||||
indicator: DF.Literal[
|
||||
"Blue", "Cyan", "Gray", "Green", "Light Blue", "Orange", "Pink", "Purple", "Red", "Red", "Yellow"
|
||||
"Blue", "Cyan", "Gray", "Green", "Light Blue", "Orange", "Pink", "Purple", "Red", "Yellow"
|
||||
]
|
||||
order: DF.Code | None
|
||||
parent: DF.Data
|
||||
|
|
|
|||
|
|
@ -273,6 +273,9 @@ frappe.ui.form.on("Number Card", {
|
|||
}
|
||||
|
||||
table.on("click", () => {
|
||||
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
|
||||
frappe.throw(__("Cannot edit filters for standard number cards"));
|
||||
}
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Set Filters"),
|
||||
fields: fields.filter((f) => !is_dynamic_filter(f)),
|
||||
|
|
@ -357,6 +360,9 @@ frappe.ui.form.on("Number Card", {
|
|||
);
|
||||
|
||||
frm.dynamic_filter_table.on("click", () => {
|
||||
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
|
||||
frappe.throw(__("Cannot edit filters for standard number cards"));
|
||||
}
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Set Dynamic Filters"),
|
||||
fields: fields,
|
||||
|
|
|
|||
|
|
@ -90,15 +90,17 @@ class ToDo(Document):
|
|||
return
|
||||
|
||||
try:
|
||||
assignments = frappe.get_all(
|
||||
assignments = frappe.db.get_values(
|
||||
"ToDo",
|
||||
filters={
|
||||
{
|
||||
"reference_type": self.reference_type,
|
||||
"reference_name": self.reference_name,
|
||||
"status": ("not in", ("Cancelled", "Closed")),
|
||||
"allocated_to": ("is", "set"),
|
||||
},
|
||||
pluck="allocated_to",
|
||||
"allocated_to",
|
||||
pluck=True,
|
||||
for_update=True,
|
||||
)
|
||||
assignments.reverse()
|
||||
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@
|
|||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:04:05.604044",
|
||||
"modified": "2024-05-30 17:30:36.791171",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Workspace",
|
||||
|
|
@ -237,6 +237,18 @@
|
|||
"role": "Workspace Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Desk User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
|
|
|
|||
|
|
@ -23,9 +23,7 @@ class Workspace(Document):
|
|||
if TYPE_CHECKING:
|
||||
from frappe.core.doctype.has_role.has_role import HasRole
|
||||
from frappe.desk.doctype.workspace_chart.workspace_chart import WorkspaceChart
|
||||
from frappe.desk.doctype.workspace_custom_block.workspace_custom_block import (
|
||||
WorkspaceCustomBlock,
|
||||
)
|
||||
from frappe.desk.doctype.workspace_custom_block.workspace_custom_block import WorkspaceCustomBlock
|
||||
from frappe.desk.doctype.workspace_link.workspace_link import WorkspaceLink
|
||||
from frappe.desk.doctype.workspace_number_card.workspace_number_card import WorkspaceNumberCard
|
||||
from frappe.desk.doctype.workspace_quick_list.workspace_quick_list import WorkspaceQuickList
|
||||
|
|
@ -251,6 +249,12 @@ def new_page(new_page):
|
|||
):
|
||||
frappe.throw(_("Cannot create private workspace of other users"), frappe.PermissionError)
|
||||
|
||||
elif not frappe.has_permission(doctype="Workspace", ptype="create"):
|
||||
frappe.flags.error_message = _("User {0} does not have the permission to create a Workspace.").format(
|
||||
frappe.bold(frappe.session.user)
|
||||
)
|
||||
raise frappe.PermissionError
|
||||
|
||||
doc = frappe.new_doc("Workspace")
|
||||
doc.title = page.get("title")
|
||||
doc.icon = page.get("icon")
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ def create_or_update_user(args): # nosemgrep
|
|||
}
|
||||
)
|
||||
user.append_roles(*_get_default_roles())
|
||||
user.append_roles("System Manager")
|
||||
user.flags.no_welcome_mail = True
|
||||
user.insert()
|
||||
|
||||
|
|
@ -305,7 +306,7 @@ def load_languages():
|
|||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def load_country():
|
||||
from frappe.sessions import get_geo_ip_country
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ def get_report_doc(report_name):
|
|||
doc.custom_filters = data.get("filters")
|
||||
doc.is_custom_report = True
|
||||
|
||||
# Follow whatever the custom report has set for prepared report field
|
||||
doc.prepared_report = custom_report_doc.prepared_report
|
||||
|
||||
if not doc.is_permitted():
|
||||
frappe.throw(
|
||||
_("You don't have access to Report: {0}").format(report_name),
|
||||
|
|
|
|||
|
|
@ -216,7 +216,8 @@ def update_wildcard_field_param(data):
|
|||
if (isinstance(data.fields, str) and data.fields == "*") or (
|
||||
isinstance(data.fields, list | tuple) and len(data.fields) == 1 and data.fields[0] == "*"
|
||||
):
|
||||
data.fields = get_permitted_fields(data.doctype, parenttype=data.parenttype)
|
||||
parent_type = data.parenttype or data.parent_doctype
|
||||
data.fields = get_permitted_fields(data.doctype, parenttype=parent_type, ignore_virtual=True)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -347,7 +347,9 @@ def make_links(columns, data):
|
|||
elif col.fieldtype == "Currency":
|
||||
doc = None
|
||||
if doc_name and col.get("parent") and not frappe.get_meta(col.parent).istable:
|
||||
doc = frappe.get_doc(col.parent, doc_name)
|
||||
if frappe.db.exists(col.parent, doc_name):
|
||||
doc = frappe.get_doc(col.parent, doc_name)
|
||||
|
||||
# Pass the Document to get the currency based on docfield option
|
||||
row[col.fieldname] = frappe.format_value(row[col.fieldname], col, doc=doc)
|
||||
return columns, data
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ class SendMailContext:
|
|||
self.sent_to_atleast_one_recipient = any(
|
||||
rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent()
|
||||
)
|
||||
self.email_account_doc = None
|
||||
|
||||
def fetch_smtp_server(self):
|
||||
self.email_account_doc = self.queue_doc.get_email_account(raise_error=True)
|
||||
|
|
@ -326,7 +327,11 @@ class SendMailContext:
|
|||
}
|
||||
tracker_url = get_url(f"{email_read_tracker_url}?{get_signed_params(params)}")
|
||||
|
||||
elif frappe.conf.use_ssl and self.email_account_doc.track_email_status:
|
||||
elif (
|
||||
self.email_account_doc
|
||||
and self.email_account_doc.track_email_status
|
||||
and self.queue_doc.communication
|
||||
):
|
||||
tracker_url = f"{get_url()}/api/method/frappe.core.doctype.communication.email.mark_email_as_seen?name={self.queue_doc.communication}"
|
||||
|
||||
if tracker_url:
|
||||
|
|
|
|||
|
|
@ -300,3 +300,10 @@ class InvalidKeyError(ValidationError):
|
|||
http_status_code = 401
|
||||
title = "Invalid Key"
|
||||
message = "The document key is invalid"
|
||||
|
||||
|
||||
class CommandFailedError(Exception):
|
||||
def __init__(self, message: str, out: str, err: str):
|
||||
super().__init__(message)
|
||||
self.out = out
|
||||
self.err = err
|
||||
|
|
|
|||
|
|
@ -194,11 +194,16 @@ doc_events = {
|
|||
|
||||
scheduler_events = {
|
||||
"cron": {
|
||||
# 15 minutes
|
||||
"0/15 * * * *": [
|
||||
"frappe.oauth.delete_oauth2_data",
|
||||
"frappe.website.doctype.web_page.web_page.check_publish_status",
|
||||
"frappe.twofactor.delete_all_barcodes_for_users",
|
||||
"frappe.email.doctype.email_account.email_account.notify_unreplied",
|
||||
"frappe.utils.global_search.sync_global_search",
|
||||
"frappe.deferred_insert.save_to_db",
|
||||
],
|
||||
# 10 minutes
|
||||
"0/10 * * * *": [
|
||||
"frappe.email.doctype.email_account.email_account.pull",
|
||||
],
|
||||
|
|
@ -213,8 +218,6 @@ scheduler_events = {
|
|||
},
|
||||
"all": [
|
||||
"frappe.email.queue.flush",
|
||||
"frappe.email.doctype.email_account.email_account.notify_unreplied",
|
||||
"frappe.utils.global_search.sync_global_search",
|
||||
"frappe.monitor.flush",
|
||||
"frappe.automation.doctype.reminder.reminder.send_reminders",
|
||||
],
|
||||
|
|
@ -222,7 +225,6 @@ scheduler_events = {
|
|||
"frappe.model.utils.link_count.update_link_count",
|
||||
"frappe.model.utils.user_settings.sync_user_settings",
|
||||
"frappe.desk.page.backups.backups.delete_downloadable_backups",
|
||||
"frappe.deferred_insert.save_to_db",
|
||||
"frappe.desk.form.document_follow.send_hourly_updates",
|
||||
"frappe.integrations.doctype.google_calendar.google_calendar.sync",
|
||||
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email",
|
||||
|
|
@ -371,6 +373,7 @@ global_search_doctypes = {
|
|||
|
||||
override_whitelisted_methods = {
|
||||
# Legacy File APIs
|
||||
"frappe.utils.file_manager.download_file": "download_file",
|
||||
"frappe.core.doctype.file.file.download_file": "download_file",
|
||||
"frappe.core.doctype.file.file.unzip_file": "frappe.core.api.file.unzip_file",
|
||||
"frappe.core.doctype.file.file.get_attached_images": "frappe.core.api.file.get_attached_images",
|
||||
|
|
@ -420,6 +423,10 @@ before_request = [
|
|||
"frappe.rate_limiter.apply",
|
||||
]
|
||||
|
||||
after_request = [
|
||||
"frappe.monitor.stop",
|
||||
]
|
||||
|
||||
# Background Job Hooks
|
||||
before_job = [
|
||||
"frappe.recorder.record",
|
||||
|
|
@ -551,4 +558,5 @@ persistent_cache_keys = [
|
|||
"changelog-*", # version update notifications
|
||||
"insert_queue_for_*", # Deferred Insert
|
||||
"recorder-*", # Recorder
|
||||
"global_search_queue",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -21,15 +21,15 @@ def make_request(method: str, url: str, auth=None, headers=None, data=None, json
|
|||
)
|
||||
response.raise_for_status()
|
||||
|
||||
content_type = response.headers.get("content-type")
|
||||
if content_type == "text/plain; charset=utf-8":
|
||||
return parse_qs(response.text)
|
||||
elif content_type.startswith("application/") and content_type.split(";")[0].endswith("json"):
|
||||
return response.json()
|
||||
elif response.text:
|
||||
return response.text
|
||||
else:
|
||||
return
|
||||
# Check whether the response has a content-type, before trying to check what it is
|
||||
if content_type := response.headers.get("content-type"):
|
||||
if content_type == "text/plain; charset=utf-8":
|
||||
return parse_qs(response.text)
|
||||
elif content_type.startswith("application/") and content_type.split(";")[0].endswith("json"):
|
||||
return response.json()
|
||||
elif response.text:
|
||||
return response.text
|
||||
return
|
||||
except Exception as exc:
|
||||
if frappe.flags.integration_request_doc:
|
||||
frappe.flags.integration_request_doc.log_error()
|
||||
|
|
|
|||
3393
frappe/locale/ar.po
3393
frappe/locale/ar.po
File diff suppressed because it is too large
Load diff
2972
frappe/locale/bs.po
2972
frappe/locale/bs.po
File diff suppressed because it is too large
Load diff
2790
frappe/locale/de.po
2790
frappe/locale/de.po
File diff suppressed because it is too large
Load diff
3395
frappe/locale/eo.po
3395
frappe/locale/eo.po
File diff suppressed because it is too large
Load diff
3026
frappe/locale/es.po
3026
frappe/locale/es.po
File diff suppressed because it is too large
Load diff
3306
frappe/locale/fa.po
3306
frappe/locale/fa.po
File diff suppressed because it is too large
Load diff
3393
frappe/locale/fr.po
3393
frappe/locale/fr.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
4090
frappe/locale/tr.po
4090
frappe/locale/tr.po
File diff suppressed because it is too large
Load diff
|
|
@ -136,17 +136,34 @@ class SiteMigration:
|
|||
* Sync Installed Applications Version History
|
||||
* Execute `after_migrate` hooks
|
||||
"""
|
||||
print("Syncing jobs...")
|
||||
sync_jobs()
|
||||
|
||||
print("Syncing fixtures...")
|
||||
sync_fixtures()
|
||||
|
||||
print("Syncing dashboards...")
|
||||
sync_dashboards()
|
||||
|
||||
print("Syncing customizations...")
|
||||
sync_customizations()
|
||||
|
||||
print("Syncing languages...")
|
||||
sync_languages()
|
||||
|
||||
print("Flushing deferred inserts...")
|
||||
flush_deferred_inserts()
|
||||
|
||||
print("Removing orphan doctypes...")
|
||||
frappe.model.sync.remove_orphan_doctypes()
|
||||
|
||||
print("Syncing portal menu...")
|
||||
frappe.get_single("Portal Settings").sync_menu()
|
||||
|
||||
print("Updating installed applications...")
|
||||
frappe.get_single("Installed Applications").update_versions()
|
||||
|
||||
print("Executing `after_migrate` hooks...")
|
||||
for app in frappe.get_installed_apps():
|
||||
for fn in frappe.get_hooks("after_migrate", app_name=app):
|
||||
frappe.get_attr(fn)()
|
||||
|
|
|
|||
|
|
@ -669,7 +669,7 @@ class DatabaseQuery:
|
|||
|
||||
if wrap_grave_quotes(table) in self.query_tables:
|
||||
permitted_child_table_fields = get_permitted_fields(
|
||||
doctype=ch_doctype, parenttype=self.doctype
|
||||
doctype=ch_doctype, parenttype=self.doctype, ignore_virtual=True
|
||||
)
|
||||
if column in permitted_child_table_fields or column in optional_fields:
|
||||
continue
|
||||
|
|
@ -728,7 +728,7 @@ class DatabaseQuery:
|
|||
df = df[0] if df else None
|
||||
|
||||
# primary key is never nullable, modified is usually indexed by default and always present
|
||||
can_be_null = f.fieldname not in ("name", "modified")
|
||||
can_be_null = f.fieldname not in ("name", "modified", "creation")
|
||||
|
||||
value = None
|
||||
|
||||
|
|
@ -810,12 +810,20 @@ class DatabaseQuery:
|
|||
|
||||
if f.operator.lower() in ("previous", "next", "timespan"):
|
||||
date_range = get_date_range(f.operator.lower(), f.value)
|
||||
f.operator = "Between"
|
||||
f.operator = "between"
|
||||
f.value = date_range
|
||||
fallback = f"'{FallBackDateTimeStr}'"
|
||||
|
||||
if f.operator.lower() in (">", ">=") and (
|
||||
f.fieldname in ("creation", "modified")
|
||||
or (df and (df.fieldtype == "Date" or df.fieldtype == "Datetime"))
|
||||
):
|
||||
# Null values can never be greater than any non-null value
|
||||
can_be_null = False
|
||||
|
||||
if f.operator in (">", "<", ">=", "<=") and (f.fieldname in ("creation", "modified")):
|
||||
value = cstr(f.value)
|
||||
can_be_null = False
|
||||
fallback = f"'{FallBackDateTimeStr}'"
|
||||
|
||||
elif f.operator.lower() in ("between") and (
|
||||
|
|
@ -823,6 +831,17 @@ class DatabaseQuery:
|
|||
or (df and (df.fieldtype == "Date" or df.fieldtype == "Datetime"))
|
||||
):
|
||||
escape = False
|
||||
# Between operator never needs to check for null
|
||||
# Explanation: Consider SQL -> `COLUMN between X and Y`
|
||||
# Actual computation:
|
||||
# for row in rows:
|
||||
# if Y > row.COLUMN > X:
|
||||
# yield row
|
||||
|
||||
# Since Y and X can't be null, null value in column will never match filter, so
|
||||
# coalesce is extra cost that prevents index usage
|
||||
can_be_null = False
|
||||
|
||||
value = get_between_date_filter(f.value, df)
|
||||
fallback = f"'{FallBackDateTimeStr}'"
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<svg width="264" height="88" viewBox="0 0 264 88" xmlns="http://www.w3.org/2000/svg"><title>default-skin 2</title><g fill="none" fill-rule="evenodd"><g><path d="M67.002 59.5v3.768c-6.307.84-9.184 5.75-10.002 9.732 2.22-2.83 5.564-5.098 10.002-5.098V71.5L73 65.585 67.002 59.5z" id="Shape" fill="#fff"/><g fill="#fff"><path d="M13 29v-5h2v3h3v2h-5zM13 15h5v2h-3v3h-2v-5zM31 15v5h-2v-3h-3v-2h5zM31 29h-5v-2h3v-3h2v5z" id="Shape"/></g><g fill="#fff"><path d="M62 24v5h-2v-3h-3v-2h5zM62 20h-5v-2h3v-3h2v5zM70 20v-5h2v3h3v2h-5zM70 24h5v2h-3v3h-2v-5z"/></g><path d="M20.586 66l-5.656-5.656 1.414-1.414L22 64.586l5.656-5.656 1.414 1.414L23.414 66l5.656 5.656-1.414 1.414L22 67.414l-5.656 5.656-1.414-1.414L20.586 66z" fill="#fff"/><path d="M111.785 65.03L110 63.5l3-3.5h-10v-2h10l-3-3.5 1.785-1.468L117 59l-5.215 6.03z" fill="#fff"/><path d="M152.215 65.03L154 63.5l-3-3.5h10v-2h-10l3-3.5-1.785-1.468L147 59l5.215 6.03z" fill="#fff"/><g><path id="Rectangle-11" fill="#fff" d="M160.957 28.543l-3.25-3.25-1.413 1.414 3.25 3.25z"/><path d="M152.5 27c3.038 0 5.5-2.462 5.5-5.5s-2.462-5.5-5.5-5.5-5.5 2.462-5.5 5.5 2.462 5.5 5.5 5.5z" id="Oval-1" stroke="#fff" stroke-width="1.5"/><path fill="#fff" d="M150 21h5v1h-5z"/></g><g><path d="M116.957 28.543l-1.414 1.414-3.25-3.25 1.414-1.414 3.25 3.25z" fill="#fff"/><path d="M108.5 27c3.038 0 5.5-2.462 5.5-5.5s-2.462-5.5-5.5-5.5-5.5 2.462-5.5 5.5 2.462 5.5 5.5 5.5z" stroke="#fff" stroke-width="1.5"/><path fill="#fff" d="M106 21h5v1h-5z"/><path fill="#fff" d="M109.043 19.008l-.085 5-1-.017.085-5z"/></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -386,7 +386,7 @@ frappe.Application = class Application {
|
|||
$(document.activeElement).blur();
|
||||
// wait for possible JS validations triggered after blur (it might change primary button)
|
||||
setTimeout(() => {
|
||||
if (window.cur_dialog && cur_dialog.display) {
|
||||
if (window.cur_dialog && cur_dialog.display && !cur_dialog.is_minimized) {
|
||||
// trigger primary
|
||||
cur_dialog.get_primary_btn().trigger("click");
|
||||
} else if (cur_frm && cur_frm.page.btn_primary.is(":visible")) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex
|
|||
return; // Don't show copy button in write mode
|
||||
}
|
||||
|
||||
if (this.copy_button) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.copy_button = $(
|
||||
`<button
|
||||
class="btn icon-btn"
|
||||
|
|
|
|||
|
|
@ -2131,14 +2131,33 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
set_active_tab(tab) {
|
||||
if (!this.active_tab_map) {
|
||||
this.active_tab_map = {};
|
||||
const previous_tab_name = this.active_tab_map?.[this.docname]?.df?.fieldname || "";
|
||||
const next_tab_name = tab?.df?.fieldname || "";
|
||||
const has_changed = previous_tab_name !== next_tab_name;
|
||||
|
||||
// A change is always detected on first render, because next_tab_name is always set (= fieldname)
|
||||
// but the previous_tab_name is always empty.
|
||||
|
||||
if (!has_changed) {
|
||||
return; // No change in tab, don't trigger on_tab_change, don't update URL hash
|
||||
}
|
||||
|
||||
this.active_tab_map ??= {};
|
||||
this.active_tab_map[this.docname] = tab;
|
||||
|
||||
// Update URL hash to reflect the active tab
|
||||
const new_hash = next_tab_name.replace("__details", "");
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = new_hash;
|
||||
if (url.href !== window.location.href) {
|
||||
history.replaceState(null, null, url);
|
||||
}
|
||||
|
||||
this.script_manager.trigger("on_tab_change");
|
||||
}
|
||||
|
||||
get_active_tab() {
|
||||
return this.active_tab_map && this.active_tab_map[this.docname];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -838,7 +838,10 @@ export default class Grid {
|
|||
acc[d.fieldname] = d.default;
|
||||
return acc;
|
||||
}, {});
|
||||
this.df.data.push({ idx: this.df.data.length + 1, __islocal: true, ...defaults });
|
||||
|
||||
const row_idx = this.df.data.length + 1;
|
||||
this.df.data.push({ idx: row_idx, __islocal: true, ...defaults });
|
||||
this.df.on_add_row && this.df.on_add_row(row_idx);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1442,7 +1442,9 @@ export default class GridRow {
|
|||
let field = this.on_grid_fields_dict[fieldname];
|
||||
// reset field value
|
||||
if (field) {
|
||||
field.docname = this.doc.name;
|
||||
// the below if statement is added to factor in the exception when this.doc is undefined -
|
||||
// - after row removals via customize_form.js on links, actions and states child-tables
|
||||
if (this.doc) field.docname = this.doc.name;
|
||||
field.refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -413,6 +413,14 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
|
||||
set_tab_as_active() {
|
||||
// Set active tab based on hash
|
||||
const tab_from_hash = window.location.hash.replace("#", "");
|
||||
const tab = this.tabs.find((tab) => tab.df.fieldname === tab_from_hash);
|
||||
if (tab) {
|
||||
tab.set_active();
|
||||
return;
|
||||
}
|
||||
|
||||
let frm_active_tab = this.frm?.get_active_tab?.();
|
||||
if (frm_active_tab) {
|
||||
frm_active_tab.set_active();
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ frappe.ui.get_print_settings = function (pdf, callback, letter_head, pick_column
|
|||
label: __("With Letter head"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
fieldtype: "Link",
|
||||
fieldname: "letter_head",
|
||||
label: __("Letter Head"),
|
||||
depends_on: "with_letter_head",
|
||||
options: Object.keys(frappe.boot.letter_heads),
|
||||
options: "Letter Head",
|
||||
default: letter_head || default_letter_head,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -191,7 +191,10 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
|
|||
doc: me.dialog.doc,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (frappe.model.is_submittable(me.doctype)) {
|
||||
if (
|
||||
frappe.model.is_submittable(me.doctype) &&
|
||||
!frappe.model.has_workflow(me.doctype)
|
||||
) {
|
||||
frappe.run_serially([
|
||||
() => (me.dialog.working = true),
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default class Tab {
|
|||
href="#${id}"
|
||||
role="tab"
|
||||
aria-controls="${id}">
|
||||
${__(this.label)}
|
||||
${__(this.label, null, this.doctype)}
|
||||
</a>
|
||||
</li>
|
||||
`).appendTo(this.tab_link_container);
|
||||
|
|
@ -76,6 +76,7 @@ export default class Tab {
|
|||
add_field(fieldobj) {
|
||||
fieldobj.tab = this;
|
||||
}
|
||||
|
||||
replace_field(fieldobj) {
|
||||
fieldobj.tab = this;
|
||||
}
|
||||
|
|
@ -96,7 +97,7 @@ export default class Tab {
|
|||
|
||||
setup_listeners() {
|
||||
this.tab_link.find(".nav-link").on("shown.bs.tab", () => {
|
||||
this?.frm.set_active_tab?.(this);
|
||||
this.frm?.set_active_tab?.(this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
this.frm.perm[0].write &&
|
||||
!this.frm.doc.__islocal &&
|
||||
doc_field.fieldtype === "Data" &&
|
||||
!doc_field.read_only
|
||||
!doc_field.read_only &&
|
||||
!doc_field.set_only_once
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
|
|
@ -550,7 +551,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
|
||||
if (frappe.model.can_create("DocType")) {
|
||||
if (frappe.boot.developer_mode === 1 && !is_doctype_form) {
|
||||
if (frappe.boot.developer_mode && !is_doctype_form) {
|
||||
// edit doctype
|
||||
this.page.add_menu_item(
|
||||
__("Edit DocType"),
|
||||
|
|
|
|||
|
|
@ -762,6 +762,10 @@ class FilterArea {
|
|||
}
|
||||
return frappe.run_serially(promises).then(() => {
|
||||
this.trigger_refresh = true;
|
||||
if (promises.length === 0) {
|
||||
// refresh if there are no standard fields
|
||||
this.debounced_refresh_list_view();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1730,7 +1730,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
shortcut: "Ctrl+K",
|
||||
});
|
||||
|
||||
if (frappe.user.has_role("System Manager") && frappe.boot.developer_mode === 1) {
|
||||
if (frappe.user.has_role("System Manager") && frappe.boot.developer_mode) {
|
||||
// edit doctype
|
||||
items.push({
|
||||
label: __("Edit DocType", null, "Button in list view menu"),
|
||||
|
|
|
|||
|
|
@ -15,15 +15,6 @@ frappe.route_options = null;
|
|||
frappe.open_in_new_tab = false;
|
||||
frappe.route_hooks = {};
|
||||
|
||||
$(window).on("hashchange", function (e) {
|
||||
// v1 style routing, route is in hash
|
||||
if (window.location.hash && !frappe.router.is_app_route(e.currentTarget.pathname)) {
|
||||
let sub_path = frappe.router.get_sub_path(window.location.hash);
|
||||
frappe.router.push_state(sub_path);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", (e) => {
|
||||
// forward-back button, just re-render based on current route
|
||||
frappe.router.route();
|
||||
|
|
@ -58,11 +49,6 @@ $("body").on("click", "a", function (e) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (href && href.startsWith("#")) {
|
||||
// target startswith "#", this is a v1 style route, so remake it.
|
||||
return override(target_element.hash);
|
||||
}
|
||||
|
||||
if (frappe.router.is_app_route(target_element.pathname)) {
|
||||
// target has "/app, this is a v2 style route.
|
||||
if (target_element.search) {
|
||||
|
|
@ -523,13 +509,8 @@ frappe.router = {
|
|||
|
||||
get_sub_path_string(route) {
|
||||
// return clean sub_path from hash or url
|
||||
// supports both v1 and v2 routing
|
||||
if (!route) {
|
||||
route = window.location.pathname;
|
||||
if (route.includes("app#")) {
|
||||
// to support v1
|
||||
route = window.location.hash;
|
||||
}
|
||||
}
|
||||
|
||||
return this.strip_prefix(route);
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@ frappe.ui.LinkPreview = class {
|
|||
}
|
||||
|
||||
this.popover_timeout = setTimeout(() => {
|
||||
if (this.popover && this.popover.options) {
|
||||
if (this.popover && this.popover.config) {
|
||||
let new_content = this.get_popover_html(preview_data);
|
||||
this.popover.options.content = new_content;
|
||||
this.popover.config.content = new_content;
|
||||
} else {
|
||||
this.init_preview_popover(preview_data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1339,10 +1339,19 @@ Object.assign(frappe.utils, {
|
|||
|
||||
// return number if total digits is lesser than min_length
|
||||
const len = String(number).match(/\d/g).length;
|
||||
if (len < min_length) return number.toString();
|
||||
if (len < min_length) {
|
||||
return number.toString();
|
||||
}
|
||||
|
||||
const number_system = this.get_number_system(country);
|
||||
let x = Math.abs(Math.round(number));
|
||||
|
||||
// if rounding was sufficient to get below min_length, return the rounded number
|
||||
const x_string = x.toString();
|
||||
if (x_string.length < min_length) {
|
||||
return x_string;
|
||||
}
|
||||
|
||||
for (const map of number_system) {
|
||||
if (x >= map.divisor) {
|
||||
let result = number / map.divisor;
|
||||
|
|
|
|||
|
|
@ -40,14 +40,16 @@ frappe.views.ImageView = class ImageView extends frappe.views.ListView {
|
|||
}
|
||||
|
||||
render() {
|
||||
this.get_attached_images().then(() => {
|
||||
this.render_image_view();
|
||||
this.load_lib.then(() => {
|
||||
this.get_attached_images().then(() => {
|
||||
this.render_image_view();
|
||||
|
||||
if (!this.gallery) {
|
||||
this.setup_gallery();
|
||||
} else {
|
||||
this.gallery.prepare_pswp_items(this.items, this.images_map);
|
||||
}
|
||||
if (!this.gallery) {
|
||||
this.setup_gallery();
|
||||
} else {
|
||||
this.gallery.prepare_pswp_items(this.items, this.images_map);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -155,24 +157,6 @@ frappe.views.ImageView = class ImageView extends frappe.views.ListView {
|
|||
});
|
||||
}
|
||||
|
||||
get_header_html() {
|
||||
// return this.get_header_html_skeleton(`
|
||||
// <div class="list-image-header">
|
||||
// <div class="list-image-header-item">
|
||||
// <input class="level-item list-check-all hidden-xs" type="checkbox" title="Select All">
|
||||
// <div>${__(this.doctype)} </div>
|
||||
// (<span class="text-muted list-count"></span>)
|
||||
// </div>
|
||||
// <div class="list-image-header-item">
|
||||
// <div class="level-item list-liked-by-me">
|
||||
// ${frappe.utils.icon('heart', 'sm', 'like-icon')}
|
||||
// </div>
|
||||
// <div>${__('Liked')}</div>
|
||||
// </div>
|
||||
// </div>
|
||||
// `);
|
||||
}
|
||||
|
||||
setup_gallery() {
|
||||
var me = this;
|
||||
this.gallery = new frappe.views.GalleryView({
|
||||
|
|
@ -190,17 +174,20 @@ frappe.views.ImageView = class ImageView extends frappe.views.ListView {
|
|||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
get required_libs() {
|
||||
return [
|
||||
"assets/frappe/node_modules/photoswipe/src/photoswipe.css",
|
||||
"photoswipe.bundle.js",
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
frappe.views.GalleryView = class GalleryView {
|
||||
constructor(opts) {
|
||||
$.extend(this, opts);
|
||||
var me = this;
|
||||
|
||||
this.lib_ready = this.load_lib();
|
||||
this.lib_ready.then(function () {
|
||||
me.prepare();
|
||||
});
|
||||
me.prepare();
|
||||
}
|
||||
prepare() {
|
||||
// keep only one pswp dom element
|
||||
|
|
@ -220,146 +207,51 @@ frappe.views.GalleryView = class GalleryView {
|
|||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const items = this.items.map(function (i) {
|
||||
const query = 'img[data-name="' + i._name + '"]';
|
||||
let el = me.wrapper.find(query).get(0);
|
||||
const items = this.items
|
||||
.filter((i) => i.image !== null)
|
||||
.map(function (i) {
|
||||
const query = 'img[data-name="' + i._name + '"]';
|
||||
let el = me.wrapper.find(query).get(0);
|
||||
|
||||
let width, height;
|
||||
if (el) {
|
||||
width = el.naturalWidth;
|
||||
height = el.naturalHeight;
|
||||
}
|
||||
let width, height;
|
||||
if (el) {
|
||||
width = el.naturalWidth;
|
||||
height = el.naturalHeight;
|
||||
}
|
||||
|
||||
if (!el) {
|
||||
el = me.wrapper.find('.image-field[data-name="' + i._name + '"]').get(0);
|
||||
width = el.getBoundingClientRect().width;
|
||||
height = el.getBoundingClientRect().height;
|
||||
}
|
||||
if (!el) {
|
||||
el = me.wrapper.find('.image-field[data-name="' + i._name + '"]').get(0);
|
||||
width = el.getBoundingClientRect().width;
|
||||
height = el.getBoundingClientRect().height;
|
||||
}
|
||||
|
||||
return {
|
||||
src: i._image_url,
|
||||
msrc: i._image_url,
|
||||
name: i.name,
|
||||
w: width,
|
||||
h: height,
|
||||
el: el,
|
||||
};
|
||||
});
|
||||
return {
|
||||
src: i._image_url,
|
||||
name: i.name,
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
});
|
||||
this.pswp_items = items;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
show(docname) {
|
||||
this.lib_ready.then(() => this.prepare_pswp_items()).then(() => this._show(docname));
|
||||
this.prepare_pswp_items().then(() => this._show(docname));
|
||||
}
|
||||
_show(docname) {
|
||||
const me = this;
|
||||
const items = this.pswp_items;
|
||||
const item_index = items.findIndex((item) => item.name === docname);
|
||||
|
||||
var options = {
|
||||
index: item_index,
|
||||
getThumbBoundsFn: function (index) {
|
||||
const query = 'img[data-name="' + items[index]._name + '"]';
|
||||
let thumbnail = me.wrapper.find(query).get(0);
|
||||
|
||||
if (!thumbnail) {
|
||||
return;
|
||||
}
|
||||
|
||||
var pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
|
||||
rect = thumbnail.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
x: rect.left,
|
||||
y: rect.top + pageYScroll,
|
||||
w: rect.width,
|
||||
};
|
||||
},
|
||||
history: false,
|
||||
shareEl: false,
|
||||
showHideOpacity: true,
|
||||
dataSource: items,
|
||||
};
|
||||
|
||||
// init
|
||||
this.pswp = new PhotoSwipe(this.pswp_root.get(0), PhotoSwipeUI_Default, items, options);
|
||||
this.browse_images();
|
||||
this.pswp = new frappe.PhotoSwipe(options);
|
||||
this.pswp.init();
|
||||
}
|
||||
browse_images() {
|
||||
const $more_items = this.pswp_root.find(".pswp__more-items");
|
||||
const images_map = this.images_map;
|
||||
let last_hide_timeout = null;
|
||||
|
||||
this.pswp.listen("afterChange", function () {
|
||||
const images = images_map[this.currItem.name];
|
||||
if (!images || images.length === 1) {
|
||||
$more_items.html("");
|
||||
return;
|
||||
}
|
||||
|
||||
hide_more_items_after_2s();
|
||||
const html = images.map(img_html).join("");
|
||||
$more_items.html(html);
|
||||
});
|
||||
|
||||
this.pswp.listen("beforeChange", hide_more_items);
|
||||
this.pswp.listen("initialZoomOut", hide_more_items);
|
||||
this.pswp.listen("destroy", () => {
|
||||
$(document).off("mousemove", hide_more_items_after_2s);
|
||||
});
|
||||
|
||||
// Replace current image on click
|
||||
$more_items.on("click", ".pswp__more-item", (e) => {
|
||||
const img_el = e.target;
|
||||
const index = this.pswp.items.findIndex((i) => i.name === this.pswp.currItem.name);
|
||||
|
||||
this.pswp.goTo(index);
|
||||
this.pswp.items.splice(index, 1, {
|
||||
src: img_el.src,
|
||||
w: img_el.naturalWidth,
|
||||
h: img_el.naturalHeight,
|
||||
name: this.pswp.currItem.name,
|
||||
});
|
||||
this.pswp.invalidateCurrItems();
|
||||
this.pswp.updateSize(true);
|
||||
});
|
||||
|
||||
// hide more-images 2s after mousemove
|
||||
$(document).on("mousemove", hide_more_items_after_2s);
|
||||
|
||||
function hide_more_items_after_2s() {
|
||||
clearTimeout(last_hide_timeout);
|
||||
show_more_items();
|
||||
last_hide_timeout = setTimeout(hide_more_items, 2000);
|
||||
}
|
||||
|
||||
function show_more_items() {
|
||||
$more_items.show();
|
||||
}
|
||||
|
||||
function hide_more_items() {
|
||||
$more_items.hide();
|
||||
}
|
||||
|
||||
function img_html(src) {
|
||||
return `<div class="pswp__more-item">
|
||||
<img src="${src}">
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
load_lib() {
|
||||
return new Promise((resolve) => {
|
||||
var asset_dir = "assets/frappe/js/lib/photoswipe/";
|
||||
frappe.require(
|
||||
[
|
||||
asset_dir + "photoswipe.css",
|
||||
asset_dir + "default-skin.css",
|
||||
asset_dir + "photoswipe.js",
|
||||
asset_dir + "photoswipe-ui-default.js",
|
||||
],
|
||||
resolve
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ frappe.provide("frappe.views");
|
|||
var state = context.state;
|
||||
var _cards = cards
|
||||
.map((card) => prepare_card(card, state))
|
||||
.concat(state.cards)
|
||||
.uniqBy((card) => card.name);
|
||||
|
||||
context.commit("update_state", {
|
||||
|
|
@ -303,7 +302,7 @@ frappe.provide("frappe.views");
|
|||
// update cards internally
|
||||
opts.cards = cards;
|
||||
|
||||
if (self.wrapper.find(".kanban").length > 0 && self.cur_list.start !== 0) {
|
||||
if (self.wrapper.find(".kanban").length > 0) {
|
||||
store.dispatch("update_cards", cards);
|
||||
} else {
|
||||
init();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="kanban-column" data-column-value="{{title}}">
|
||||
<div class="kanban-column" data-column-value="{{title}}" style="background-color: var(--bg-{{indicator}});">
|
||||
<div class="kanban-column-header">
|
||||
<span class="kanban-column-title">
|
||||
<span class="indicator-pill {{indicator}}"></span>
|
||||
|
|
|
|||
|
|
@ -136,10 +136,13 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
this.hide_card_layout = true;
|
||||
this.hide_sort_selector = true;
|
||||
super.setup_page();
|
||||
|
||||
this.page.disable_sidebar_toggle = true;
|
||||
this.page.setup_sidebar_toggle();
|
||||
}
|
||||
|
||||
setup_view() {
|
||||
if (this.board.columns.length > 5) {
|
||||
if (this.board.columns.filter((col) => col.status !== "Archived").length > 4) {
|
||||
this.page.container.addClass("full-width");
|
||||
}
|
||||
this.setup_realtime_updates();
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ frappe.views.Workspace = class Workspace {
|
|||
this.cached_pages = $.extend(true, {}, this.sidebar_pages);
|
||||
this.all_pages = this.sidebar_pages.pages;
|
||||
this.has_access = this.sidebar_pages.has_access;
|
||||
this.has_create_access = this.sidebar_pages.has_create_access;
|
||||
|
||||
this.all_pages.forEach((page) => {
|
||||
page.is_editable = !page.public || this.has_access;
|
||||
|
|
@ -473,9 +474,10 @@ frappe.views.Workspace = class Workspace {
|
|||
"es-line-edit"
|
||||
);
|
||||
// need to add option for icons in inner buttons as well
|
||||
this.page.add_inner_button(__("Create Workspace"), () => {
|
||||
this.initialize_new_page();
|
||||
});
|
||||
if (this.has_create_access)
|
||||
this.page.add_inner_button(__("Create Workspace"), () => {
|
||||
this.initialize_new_page(true);
|
||||
});
|
||||
}
|
||||
|
||||
initialize_editorjs_undo() {
|
||||
|
|
|
|||
|
|
@ -223,9 +223,9 @@ export default class NumberCardWidget extends Widget {
|
|||
|
||||
const symbol = number_parts[1] || "";
|
||||
number_parts[0] = window.convert_old_to_new_number_format(number_parts[0]);
|
||||
const formatted_number = $(frappe.format(number_parts[0], df, null, doc)).text();
|
||||
|
||||
this.formatted_number = formatted_number + " " + __(symbol);
|
||||
const formatted_number = frappe.format(number_parts[0], df, null, doc);
|
||||
this.formatted_number =
|
||||
($(formatted_number).text() || formatted_number) + " " + __(symbol);
|
||||
}
|
||||
|
||||
_generate_common_doc(rows) {
|
||||
|
|
|
|||
|
|
@ -178,11 +178,6 @@ export default class OnboardingWidget extends Widget {
|
|||
actions[step.action](step);
|
||||
});
|
||||
|
||||
// Fire only once, on hashchange
|
||||
$(window).one("hashchange", () => {
|
||||
plyr.pause();
|
||||
});
|
||||
|
||||
$(`<button class="btn btn-secondary ml-2 btn-sm">${__("Back")}</button>`)
|
||||
.appendTo(this.step_footer)
|
||||
.on("click", toggle_content);
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 Dmitry Semenov, http://dimsemenov.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -1,483 +0,0 @@
|
|||
/*! PhotoSwipe Default UI CSS by Dmitry Semenov | photoswipe.com | MIT license */
|
||||
/*
|
||||
|
||||
Contents:
|
||||
|
||||
1. Buttons
|
||||
2. Share modal and links
|
||||
3. Index indicator ("1 of X" counter)
|
||||
4. Caption
|
||||
5. Loading indicator
|
||||
6. Additional styles (root element, top bar, idle state, hidden state, etc.)
|
||||
|
||||
*/
|
||||
/*
|
||||
|
||||
1. Buttons
|
||||
|
||||
*/
|
||||
/* <button> css reset */
|
||||
.pswp__button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
overflow: visible;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
float: right;
|
||||
opacity: 0.75;
|
||||
-webkit-transition: opacity 0.2s;
|
||||
transition: opacity 0.2s;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none; }
|
||||
.pswp__button:focus,
|
||||
.pswp__button:hover {
|
||||
opacity: 1; }
|
||||
.pswp__button:active {
|
||||
outline: none;
|
||||
opacity: 0.9; }
|
||||
.pswp__button::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0; }
|
||||
|
||||
/* pswp__ui--over-close class it added when mouse is over element that should close gallery */
|
||||
.pswp__ui--over-close .pswp__button--close {
|
||||
opacity: 1; }
|
||||
|
||||
.pswp__button,
|
||||
.pswp__button--arrow--left:before,
|
||||
.pswp__button--arrow--right:before {
|
||||
background: url(default-skin.png) 0 0 no-repeat;
|
||||
background-size: 264px 88px;
|
||||
width: 44px;
|
||||
height: 44px; }
|
||||
|
||||
@media (-webkit-min-device-pixel-ratio: 1.1), (-webkit-min-device-pixel-ratio: 1.09375), (min-resolution: 105dpi), (min-resolution: 1.1dppx) {
|
||||
/* Serve SVG sprite if browser supports SVG and resolution is more than 105dpi */
|
||||
.pswp--svg .pswp__button,
|
||||
.pswp--svg .pswp__button--arrow--left:before,
|
||||
.pswp--svg .pswp__button--arrow--right:before {
|
||||
background-image: url(default-skin.svg); }
|
||||
.pswp--svg .pswp__button--arrow--left,
|
||||
.pswp--svg .pswp__button--arrow--right {
|
||||
background: none; } }
|
||||
|
||||
.pswp__button--close {
|
||||
background-position: 0 -44px; }
|
||||
|
||||
.pswp__button--share {
|
||||
background-position: -44px -44px; }
|
||||
|
||||
.pswp__button--fs {
|
||||
display: none; }
|
||||
|
||||
.pswp--supports-fs .pswp__button--fs {
|
||||
display: block; }
|
||||
|
||||
.pswp--fs .pswp__button--fs {
|
||||
background-position: -44px 0; }
|
||||
|
||||
.pswp__button--zoom {
|
||||
display: none;
|
||||
background-position: -88px 0; }
|
||||
|
||||
.pswp--zoom-allowed .pswp__button--zoom {
|
||||
display: block; }
|
||||
|
||||
.pswp--zoomed-in .pswp__button--zoom {
|
||||
background-position: -132px 0; }
|
||||
|
||||
/* no arrows on touch screens */
|
||||
.pswp--touch .pswp__button--arrow--left,
|
||||
.pswp--touch .pswp__button--arrow--right {
|
||||
visibility: hidden; }
|
||||
|
||||
/*
|
||||
Arrow buttons hit area
|
||||
(icon is added to :before pseudo-element)
|
||||
*/
|
||||
.pswp__button--arrow--left,
|
||||
.pswp__button--arrow--right {
|
||||
background: none;
|
||||
top: 50%;
|
||||
margin-top: -50px;
|
||||
width: 70px;
|
||||
height: 100px;
|
||||
position: absolute; }
|
||||
|
||||
.pswp__button--arrow--left {
|
||||
left: 0; }
|
||||
|
||||
.pswp__button--arrow--right {
|
||||
right: 0; }
|
||||
|
||||
.pswp__button--arrow--left:before,
|
||||
.pswp__button--arrow--right:before {
|
||||
content: '';
|
||||
top: 35px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
height: 30px;
|
||||
width: 32px;
|
||||
position: absolute; }
|
||||
|
||||
.pswp__button--arrow--left:before {
|
||||
left: 6px;
|
||||
background-position: -138px -44px; }
|
||||
|
||||
.pswp__button--arrow--right:before {
|
||||
right: 6px;
|
||||
background-position: -94px -44px; }
|
||||
|
||||
/*
|
||||
|
||||
2. Share modal/popup and links
|
||||
|
||||
*/
|
||||
.pswp__counter,
|
||||
.pswp__share-modal {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none; }
|
||||
|
||||
.pswp__share-modal {
|
||||
display: block;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
z-index: 1600;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.25s ease-out;
|
||||
transition: opacity 0.25s ease-out;
|
||||
-webkit-backface-visibility: hidden;
|
||||
will-change: opacity; }
|
||||
|
||||
.pswp__share-modal--hidden {
|
||||
display: none; }
|
||||
|
||||
.pswp__share-tooltip {
|
||||
z-index: 1620;
|
||||
position: absolute;
|
||||
background: #FFF;
|
||||
top: 56px;
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
width: auto;
|
||||
right: 44px;
|
||||
-webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
|
||||
-webkit-transform: translateY(6px);
|
||||
-ms-transform: translateY(6px);
|
||||
transform: translateY(6px);
|
||||
-webkit-transition: -webkit-transform 0.25s;
|
||||
transition: transform 0.25s;
|
||||
-webkit-backface-visibility: hidden;
|
||||
will-change: transform; }
|
||||
.pswp__share-tooltip a {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 18px; }
|
||||
.pswp__share-tooltip a:hover {
|
||||
text-decoration: none;
|
||||
color: #000; }
|
||||
.pswp__share-tooltip a:first-child {
|
||||
/* round corners on the first/last list item */
|
||||
border-radius: 2px 2px 0 0; }
|
||||
.pswp__share-tooltip a:last-child {
|
||||
border-radius: 0 0 2px 2px; }
|
||||
|
||||
.pswp__share-modal--fade-in {
|
||||
opacity: 1; }
|
||||
.pswp__share-modal--fade-in .pswp__share-tooltip {
|
||||
-webkit-transform: translateY(0);
|
||||
-ms-transform: translateY(0);
|
||||
transform: translateY(0); }
|
||||
|
||||
/* increase size of share links on touch devices */
|
||||
.pswp--touch .pswp__share-tooltip a {
|
||||
padding: 16px 12px; }
|
||||
|
||||
a.pswp__share--facebook:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
right: 15px;
|
||||
border: 6px solid transparent;
|
||||
border-bottom-color: #FFF;
|
||||
-webkit-pointer-events: none;
|
||||
-moz-pointer-events: none;
|
||||
pointer-events: none; }
|
||||
|
||||
a.pswp__share--facebook:hover {
|
||||
background: #3E5C9A;
|
||||
color: #FFF; }
|
||||
a.pswp__share--facebook:hover:before {
|
||||
border-bottom-color: #3E5C9A; }
|
||||
|
||||
a.pswp__share--twitter:hover {
|
||||
background: #55ACEE;
|
||||
color: #FFF; }
|
||||
|
||||
a.pswp__share--pinterest:hover {
|
||||
background: #CCC;
|
||||
color: #CE272D; }
|
||||
|
||||
a.pswp__share--download:hover {
|
||||
background: #DDD; }
|
||||
|
||||
/*
|
||||
|
||||
3. Index indicator ("1 of X" counter)
|
||||
|
||||
*/
|
||||
.pswp__counter {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 44px;
|
||||
font-size: 13px;
|
||||
line-height: 44px;
|
||||
color: #FFF;
|
||||
opacity: 0.75;
|
||||
padding: 0 10px; }
|
||||
|
||||
/*
|
||||
|
||||
4. Caption
|
||||
|
||||
*/
|
||||
.pswp__caption {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
min-height: 44px; }
|
||||
.pswp__caption small {
|
||||
font-size: 11px;
|
||||
color: #BBB; }
|
||||
|
||||
.pswp__caption__center {
|
||||
text-align: left;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
font-size: 13px;
|
||||
padding: 10px;
|
||||
line-height: 20px;
|
||||
color: #CCC; }
|
||||
|
||||
.pswp__caption--empty {
|
||||
display: none; }
|
||||
|
||||
/* Fake caption element, used to calculate height of next/prev image */
|
||||
.pswp__caption--fake {
|
||||
visibility: hidden; }
|
||||
|
||||
/*
|
||||
|
||||
5. Loading indicator (preloader)
|
||||
|
||||
You can play with it here - http://codepen.io/dimsemenov/pen/yyBWoR
|
||||
|
||||
*/
|
||||
.pswp__preloader {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
margin-left: -22px;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.25s ease-out;
|
||||
transition: opacity 0.25s ease-out;
|
||||
will-change: opacity;
|
||||
direction: ltr; }
|
||||
|
||||
.pswp__preloader__icn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 12px; }
|
||||
|
||||
.pswp__preloader--active {
|
||||
opacity: 1; }
|
||||
.pswp__preloader--active .pswp__preloader__icn {
|
||||
/* We use .gif in browsers that don't support CSS animation */
|
||||
background: url(preloader.gif) 0 0 no-repeat; }
|
||||
|
||||
.pswp--css_animation .pswp__preloader--active {
|
||||
opacity: 1; }
|
||||
.pswp--css_animation .pswp__preloader--active .pswp__preloader__icn {
|
||||
-webkit-animation: clockwise 500ms linear infinite;
|
||||
animation: clockwise 500ms linear infinite; }
|
||||
.pswp--css_animation .pswp__preloader--active .pswp__preloader__donut {
|
||||
-webkit-animation: donut-rotate 1000ms cubic-bezier(0.4, 0, 0.22, 1) infinite;
|
||||
animation: donut-rotate 1000ms cubic-bezier(0.4, 0, 0.22, 1) infinite; }
|
||||
|
||||
.pswp--css_animation .pswp__preloader__icn {
|
||||
background: none;
|
||||
opacity: 0.75;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 15px;
|
||||
margin: 0; }
|
||||
|
||||
.pswp--css_animation .pswp__preloader__cut {
|
||||
/*
|
||||
The idea of animating inner circle is based on Polymer ("material") loading indicator
|
||||
by Keanu Lee https://blog.keanulee.com/2014/10/20/the-tale-of-three-spinners.html
|
||||
*/
|
||||
position: relative;
|
||||
width: 7px;
|
||||
height: 14px;
|
||||
overflow: hidden; }
|
||||
|
||||
.pswp--css_animation .pswp__preloader__donut {
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #FFF;
|
||||
border-radius: 50%;
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: none;
|
||||
margin: 0; }
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.pswp__preloader {
|
||||
position: relative;
|
||||
left: auto;
|
||||
top: auto;
|
||||
margin: 0;
|
||||
float: right; } }
|
||||
|
||||
@-webkit-keyframes clockwise {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg); } }
|
||||
|
||||
@keyframes clockwise {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg); } }
|
||||
|
||||
@-webkit-keyframes donut-rotate {
|
||||
0% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0); }
|
||||
50% {
|
||||
-webkit-transform: rotate(-140deg);
|
||||
transform: rotate(-140deg); }
|
||||
100% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0); } }
|
||||
|
||||
@keyframes donut-rotate {
|
||||
0% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0); }
|
||||
50% {
|
||||
-webkit-transform: rotate(-140deg);
|
||||
transform: rotate(-140deg); }
|
||||
100% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0); } }
|
||||
|
||||
/*
|
||||
|
||||
6. Additional styles
|
||||
|
||||
*/
|
||||
/* root element of UI */
|
||||
.pswp__ui {
|
||||
-webkit-font-smoothing: auto;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
z-index: 1550; }
|
||||
|
||||
/* top black bar with buttons and "1 of X" indicator */
|
||||
.pswp__top-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 44px;
|
||||
width: 100%; }
|
||||
|
||||
.pswp__caption,
|
||||
.pswp__top-bar,
|
||||
.pswp--has_mouse .pswp__button--arrow--left,
|
||||
.pswp--has_mouse .pswp__button--arrow--right {
|
||||
-webkit-backface-visibility: hidden;
|
||||
will-change: opacity;
|
||||
-webkit-transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
|
||||
transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); }
|
||||
|
||||
/* pswp--has_mouse class is added only when two subsequent mousemove events occur */
|
||||
.pswp--has_mouse .pswp__button--arrow--left,
|
||||
.pswp--has_mouse .pswp__button--arrow--right {
|
||||
visibility: visible; }
|
||||
|
||||
.pswp__top-bar,
|
||||
.pswp__caption {
|
||||
background-color: rgba(0, 0, 0, 0.5); }
|
||||
|
||||
/* pswp__ui--fit class is added when main image "fits" between top bar and bottom bar (caption) */
|
||||
.pswp__ui--fit .pswp__top-bar,
|
||||
.pswp__ui--fit .pswp__caption {
|
||||
background-color: rgba(0, 0, 0, 0.3); }
|
||||
|
||||
/* pswp__ui--idle class is added when mouse isn't moving for several seconds (JS option timeToIdle) */
|
||||
.pswp__ui--idle .pswp__top-bar {
|
||||
opacity: 0; }
|
||||
|
||||
.pswp__ui--idle .pswp__button--arrow--left,
|
||||
.pswp__ui--idle .pswp__button--arrow--right {
|
||||
opacity: 0; }
|
||||
|
||||
/*
|
||||
pswp__ui--hidden class is added when controls are hidden
|
||||
e.g. when user taps to toggle visibility of controls
|
||||
*/
|
||||
.pswp__ui--hidden .pswp__top-bar,
|
||||
.pswp__ui--hidden .pswp__caption,
|
||||
.pswp__ui--hidden .pswp__button--arrow--left,
|
||||
.pswp__ui--hidden .pswp__button--arrow--right {
|
||||
/* Force paint & create composition layer for controls. */
|
||||
opacity: 0.001; }
|
||||
|
||||
/* pswp__ui--one-slide class is added when there is just one item in gallery */
|
||||
.pswp__ui--one-slide .pswp__button--arrow--left,
|
||||
.pswp__ui--one-slide .pswp__button--arrow--right,
|
||||
.pswp__ui--one-slide .pswp__counter {
|
||||
display: none; }
|
||||
|
||||
.pswp__element--disabled {
|
||||
display: none !important; }
|
||||
|
||||
.pswp--minimal--dark .pswp__top-bar {
|
||||
background: none; }
|
||||
|
|
@ -1,861 +0,0 @@
|
|||
/*! PhotoSwipe Default UI - 4.1.1 - 2015-12-24
|
||||
* http://photoswipe.com
|
||||
* Copyright (c) 2015 Dmitry Semenov; */
|
||||
/**
|
||||
*
|
||||
* UI on top of main sliding area (caption, arrows, close button, etc.).
|
||||
* Built just using public methods/properties of PhotoSwipe.
|
||||
*
|
||||
*/
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define(factory);
|
||||
} else if (typeof exports === 'object') {
|
||||
module.exports = factory();
|
||||
} else {
|
||||
root.PhotoSwipeUI_Default = factory();
|
||||
}
|
||||
})(this, function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
|
||||
|
||||
var PhotoSwipeUI_Default =
|
||||
function(pswp, framework) {
|
||||
|
||||
var ui = this;
|
||||
var _overlayUIUpdated = false,
|
||||
_controlsVisible = true,
|
||||
_fullscrenAPI,
|
||||
_controls,
|
||||
_captionContainer,
|
||||
_fakeCaptionContainer,
|
||||
_indexIndicator,
|
||||
_shareButton,
|
||||
_shareModal,
|
||||
_shareModalHidden = true,
|
||||
_initalCloseOnScrollValue,
|
||||
_isIdle,
|
||||
_listen,
|
||||
|
||||
_loadingIndicator,
|
||||
_loadingIndicatorHidden,
|
||||
_loadingIndicatorTimeout,
|
||||
|
||||
_galleryHasOneSlide,
|
||||
|
||||
_options,
|
||||
_defaultUIOptions = {
|
||||
barsSize: {top:44, bottom:'auto'},
|
||||
closeElClasses: ['item', 'caption', 'zoom-wrap', 'ui', 'top-bar'],
|
||||
timeToIdle: 4000,
|
||||
timeToIdleOutside: 1000,
|
||||
loadingIndicatorDelay: 1000, // 2s
|
||||
|
||||
addCaptionHTMLFn: function(item, captionEl /*, isFake */) {
|
||||
if(!item.title) {
|
||||
captionEl.children[0].innerHTML = '';
|
||||
return false;
|
||||
}
|
||||
captionEl.children[0].innerHTML = item.title;
|
||||
return true;
|
||||
},
|
||||
|
||||
closeEl:true,
|
||||
captionEl: true,
|
||||
fullscreenEl: true,
|
||||
zoomEl: true,
|
||||
shareEl: true,
|
||||
counterEl: true,
|
||||
arrowEl: true,
|
||||
preloaderEl: true,
|
||||
|
||||
tapToClose: false,
|
||||
tapToToggleControls: true,
|
||||
|
||||
clickToCloseNonZoomable: true,
|
||||
|
||||
shareButtons: [
|
||||
{id:'facebook', label:'Share on Facebook', url:'https://www.facebook.com/sharer/sharer.php?u={{url}}'},
|
||||
{id:'twitter', label:'Tweet', url:'https://twitter.com/intent/tweet?text={{text}}&url={{url}}'},
|
||||
{id:'pinterest', label:'Pin it', url:'http://www.pinterest.com/pin/create/button/'+
|
||||
'?url={{url}}&media={{image_url}}&description={{text}}'},
|
||||
{id:'download', label:'Download image', url:'{{raw_image_url}}', download:true}
|
||||
],
|
||||
getImageURLForShare: function( /* shareButtonData */ ) {
|
||||
return pswp.currItem.src || '';
|
||||
},
|
||||
getPageURLForShare: function( /* shareButtonData */ ) {
|
||||
return window.location.href;
|
||||
},
|
||||
getTextForShare: function( /* shareButtonData */ ) {
|
||||
return pswp.currItem.title || '';
|
||||
},
|
||||
|
||||
indexIndicatorSep: ' / ',
|
||||
fitControlsWidth: 1200
|
||||
|
||||
},
|
||||
_blockControlsTap,
|
||||
_blockControlsTapTimeout;
|
||||
|
||||
|
||||
|
||||
var _onControlsTap = function(e) {
|
||||
if(_blockControlsTap) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
e = e || window.event;
|
||||
|
||||
if(_options.timeToIdle && _options.mouseUsed && !_isIdle) {
|
||||
// reset idle timer
|
||||
_onIdleMouseMove();
|
||||
}
|
||||
|
||||
|
||||
var target = e.target || e.srcElement,
|
||||
uiElement,
|
||||
clickedClass = target.getAttribute('class') || '',
|
||||
found;
|
||||
|
||||
for(var i = 0; i < _uiElements.length; i++) {
|
||||
uiElement = _uiElements[i];
|
||||
if(uiElement.onTap && clickedClass.indexOf('pswp__' + uiElement.name ) > -1 ) {
|
||||
uiElement.onTap();
|
||||
found = true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if(found) {
|
||||
if(e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
_blockControlsTap = true;
|
||||
|
||||
// Some versions of Android don't prevent ghost click event
|
||||
// when preventDefault() was called on touchstart and/or touchend.
|
||||
//
|
||||
// This happens on v4.3, 4.2, 4.1,
|
||||
// older versions strangely work correctly,
|
||||
// but just in case we add delay on all of them)
|
||||
var tapDelay = framework.features.isOldAndroid ? 600 : 30;
|
||||
_blockControlsTapTimeout = setTimeout(function() {
|
||||
_blockControlsTap = false;
|
||||
}, tapDelay);
|
||||
}
|
||||
|
||||
},
|
||||
_fitControlsInViewport = function() {
|
||||
return !pswp.likelyTouchDevice || _options.mouseUsed || screen.width > _options.fitControlsWidth;
|
||||
},
|
||||
_togglePswpClass = function(el, cName, add) {
|
||||
framework[ (add ? 'add' : 'remove') + 'Class' ](el, 'pswp__' + cName);
|
||||
},
|
||||
|
||||
// add class when there is just one item in the gallery
|
||||
// (by default it hides left/right arrows and 1ofX counter)
|
||||
_countNumItems = function() {
|
||||
var hasOneSlide = (_options.getNumItemsFn() === 1);
|
||||
|
||||
if(hasOneSlide !== _galleryHasOneSlide) {
|
||||
_togglePswpClass(_controls, 'ui--one-slide', hasOneSlide);
|
||||
_galleryHasOneSlide = hasOneSlide;
|
||||
}
|
||||
},
|
||||
_toggleShareModalClass = function() {
|
||||
_togglePswpClass(_shareModal, 'share-modal--hidden', _shareModalHidden);
|
||||
},
|
||||
_toggleShareModal = function() {
|
||||
|
||||
_shareModalHidden = !_shareModalHidden;
|
||||
|
||||
|
||||
if(!_shareModalHidden) {
|
||||
_toggleShareModalClass();
|
||||
setTimeout(function() {
|
||||
if(!_shareModalHidden) {
|
||||
framework.addClass(_shareModal, 'pswp__share-modal--fade-in');
|
||||
}
|
||||
}, 30);
|
||||
} else {
|
||||
framework.removeClass(_shareModal, 'pswp__share-modal--fade-in');
|
||||
setTimeout(function() {
|
||||
if(_shareModalHidden) {
|
||||
_toggleShareModalClass();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
if(!_shareModalHidden) {
|
||||
_updateShareURLs();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_openWindowPopup = function(e) {
|
||||
e = e || window.event;
|
||||
var target = e.target || e.srcElement;
|
||||
|
||||
pswp.shout('shareLinkClick', e, target);
|
||||
|
||||
if(!target.href) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if( target.hasAttribute('download') ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
window.open(target.href, 'pswp_share', 'scrollbars=yes,resizable=yes,toolbar=no,'+
|
||||
'location=yes,width=550,height=420,top=100,left=' +
|
||||
(window.screen ? Math.round(screen.width / 2 - 275) : 100) );
|
||||
|
||||
if(!_shareModalHidden) {
|
||||
_toggleShareModal();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
_updateShareURLs = function() {
|
||||
var shareButtonOut = '',
|
||||
shareButtonData,
|
||||
shareURL,
|
||||
image_url,
|
||||
page_url,
|
||||
share_text;
|
||||
|
||||
for(var i = 0; i < _options.shareButtons.length; i++) {
|
||||
shareButtonData = _options.shareButtons[i];
|
||||
|
||||
image_url = _options.getImageURLForShare(shareButtonData);
|
||||
page_url = _options.getPageURLForShare(shareButtonData);
|
||||
share_text = _options.getTextForShare(shareButtonData);
|
||||
|
||||
shareURL = shareButtonData.url.replace('{{url}}', encodeURIComponent(page_url) )
|
||||
.replace('{{image_url}}', encodeURIComponent(image_url) )
|
||||
.replace('{{raw_image_url}}', image_url )
|
||||
.replace('{{text}}', encodeURIComponent(share_text) );
|
||||
|
||||
shareButtonOut += '<a href="' + shareURL + '" target="_blank" '+
|
||||
'class="pswp__share--' + shareButtonData.id + '"' +
|
||||
(shareButtonData.download ? 'download' : '') + '>' +
|
||||
shareButtonData.label + '</a>';
|
||||
|
||||
if(_options.parseShareButtonOut) {
|
||||
shareButtonOut = _options.parseShareButtonOut(shareButtonData, shareButtonOut);
|
||||
}
|
||||
}
|
||||
_shareModal.children[0].innerHTML = shareButtonOut;
|
||||
_shareModal.children[0].onclick = _openWindowPopup;
|
||||
|
||||
},
|
||||
_hasCloseClass = function(target) {
|
||||
for(var i = 0; i < _options.closeElClasses.length; i++) {
|
||||
if( framework.hasClass(target, 'pswp__' + _options.closeElClasses[i]) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
_idleInterval,
|
||||
_idleTimer,
|
||||
_idleIncrement = 0,
|
||||
_onIdleMouseMove = function() {
|
||||
clearTimeout(_idleTimer);
|
||||
_idleIncrement = 0;
|
||||
if(_isIdle) {
|
||||
ui.setIdle(false);
|
||||
}
|
||||
},
|
||||
_onMouseLeaveWindow = function(e) {
|
||||
e = e ? e : window.event;
|
||||
var from = e.relatedTarget || e.toElement;
|
||||
if (!from || from.nodeName === 'HTML') {
|
||||
clearTimeout(_idleTimer);
|
||||
_idleTimer = setTimeout(function() {
|
||||
ui.setIdle(true);
|
||||
}, _options.timeToIdleOutside);
|
||||
}
|
||||
},
|
||||
_setupFullscreenAPI = function() {
|
||||
if(_options.fullscreenEl && !framework.features.isOldAndroid) {
|
||||
if(!_fullscrenAPI) {
|
||||
_fullscrenAPI = ui.getFullscreenAPI();
|
||||
}
|
||||
if(_fullscrenAPI) {
|
||||
framework.bind(document, _fullscrenAPI.eventK, ui.updateFullscreen);
|
||||
ui.updateFullscreen();
|
||||
framework.addClass(pswp.template, 'pswp--supports-fs');
|
||||
} else {
|
||||
framework.removeClass(pswp.template, 'pswp--supports-fs');
|
||||
}
|
||||
}
|
||||
},
|
||||
_setupLoadingIndicator = function() {
|
||||
// Setup loading indicator
|
||||
if(_options.preloaderEl) {
|
||||
|
||||
_toggleLoadingIndicator(true);
|
||||
|
||||
_listen('beforeChange', function() {
|
||||
|
||||
clearTimeout(_loadingIndicatorTimeout);
|
||||
|
||||
// display loading indicator with delay
|
||||
_loadingIndicatorTimeout = setTimeout(function() {
|
||||
|
||||
if(pswp.currItem && pswp.currItem.loading) {
|
||||
|
||||
if( !pswp.allowProgressiveImg() || (pswp.currItem.img && !pswp.currItem.img.naturalWidth) ) {
|
||||
// show preloader if progressive loading is not enabled,
|
||||
// or image width is not defined yet (because of slow connection)
|
||||
_toggleLoadingIndicator(false);
|
||||
// items-controller.js function allowProgressiveImg
|
||||
}
|
||||
|
||||
} else {
|
||||
_toggleLoadingIndicator(true); // hide preloader
|
||||
}
|
||||
|
||||
}, _options.loadingIndicatorDelay);
|
||||
|
||||
});
|
||||
_listen('imageLoadComplete', function(index, item) {
|
||||
if(pswp.currItem === item) {
|
||||
_toggleLoadingIndicator(true);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
_toggleLoadingIndicator = function(hide) {
|
||||
if( _loadingIndicatorHidden !== hide ) {
|
||||
_togglePswpClass(_loadingIndicator, 'preloader--active', !hide);
|
||||
_loadingIndicatorHidden = hide;
|
||||
}
|
||||
},
|
||||
_applyNavBarGaps = function(item) {
|
||||
var gap = item.vGap;
|
||||
|
||||
if( _fitControlsInViewport() ) {
|
||||
|
||||
var bars = _options.barsSize;
|
||||
if(_options.captionEl && bars.bottom === 'auto') {
|
||||
if(!_fakeCaptionContainer) {
|
||||
_fakeCaptionContainer = framework.createEl('pswp__caption pswp__caption--fake');
|
||||
_fakeCaptionContainer.appendChild( framework.createEl('pswp__caption__center') );
|
||||
_controls.insertBefore(_fakeCaptionContainer, _captionContainer);
|
||||
framework.addClass(_controls, 'pswp__ui--fit');
|
||||
}
|
||||
if( _options.addCaptionHTMLFn(item, _fakeCaptionContainer, true) ) {
|
||||
|
||||
var captionSize = _fakeCaptionContainer.clientHeight;
|
||||
gap.bottom = parseInt(captionSize,10) || 44;
|
||||
} else {
|
||||
gap.bottom = bars.top; // if no caption, set size of bottom gap to size of top
|
||||
}
|
||||
} else {
|
||||
gap.bottom = bars.bottom === 'auto' ? 0 : bars.bottom;
|
||||
}
|
||||
|
||||
// height of top bar is static, no need to calculate it
|
||||
gap.top = bars.top;
|
||||
} else {
|
||||
gap.top = gap.bottom = 0;
|
||||
}
|
||||
},
|
||||
_setupIdle = function() {
|
||||
// Hide controls when mouse is used
|
||||
if(_options.timeToIdle) {
|
||||
_listen('mouseUsed', function() {
|
||||
|
||||
framework.bind(document, 'mousemove', _onIdleMouseMove);
|
||||
framework.bind(document, 'mouseout', _onMouseLeaveWindow);
|
||||
|
||||
_idleInterval = setInterval(function() {
|
||||
_idleIncrement++;
|
||||
if(_idleIncrement === 2) {
|
||||
ui.setIdle(true);
|
||||
}
|
||||
}, _options.timeToIdle / 2);
|
||||
});
|
||||
}
|
||||
},
|
||||
_setupHidingControlsDuringGestures = function() {
|
||||
|
||||
// Hide controls on vertical drag
|
||||
_listen('onVerticalDrag', function(now) {
|
||||
if(_controlsVisible && now < 0.95) {
|
||||
ui.hideControls();
|
||||
} else if(!_controlsVisible && now >= 0.95) {
|
||||
ui.showControls();
|
||||
}
|
||||
});
|
||||
|
||||
// Hide controls when pinching to close
|
||||
var pinchControlsHidden;
|
||||
_listen('onPinchClose' , function(now) {
|
||||
if(_controlsVisible && now < 0.9) {
|
||||
ui.hideControls();
|
||||
pinchControlsHidden = true;
|
||||
} else if(pinchControlsHidden && !_controlsVisible && now > 0.9) {
|
||||
ui.showControls();
|
||||
}
|
||||
});
|
||||
|
||||
_listen('zoomGestureEnded', function() {
|
||||
pinchControlsHidden = false;
|
||||
if(pinchControlsHidden && !_controlsVisible) {
|
||||
ui.showControls();
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
var _uiElements = [
|
||||
{
|
||||
name: 'caption',
|
||||
option: 'captionEl',
|
||||
onInit: function(el) {
|
||||
_captionContainer = el;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'share-modal',
|
||||
option: 'shareEl',
|
||||
onInit: function(el) {
|
||||
_shareModal = el;
|
||||
},
|
||||
onTap: function() {
|
||||
_toggleShareModal();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'button--share',
|
||||
option: 'shareEl',
|
||||
onInit: function(el) {
|
||||
_shareButton = el;
|
||||
},
|
||||
onTap: function() {
|
||||
_toggleShareModal();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'button--zoom',
|
||||
option: 'zoomEl',
|
||||
onTap: pswp.toggleDesktopZoom
|
||||
},
|
||||
{
|
||||
name: 'counter',
|
||||
option: 'counterEl',
|
||||
onInit: function(el) {
|
||||
_indexIndicator = el;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'button--close',
|
||||
option: 'closeEl',
|
||||
onTap: pswp.close
|
||||
},
|
||||
{
|
||||
name: 'button--arrow--left',
|
||||
option: 'arrowEl',
|
||||
onTap: pswp.prev
|
||||
},
|
||||
{
|
||||
name: 'button--arrow--right',
|
||||
option: 'arrowEl',
|
||||
onTap: pswp.next
|
||||
},
|
||||
{
|
||||
name: 'button--fs',
|
||||
option: 'fullscreenEl',
|
||||
onTap: function() {
|
||||
if(_fullscrenAPI.isFullscreen()) {
|
||||
_fullscrenAPI.exit();
|
||||
} else {
|
||||
_fullscrenAPI.enter();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'preloader',
|
||||
option: 'preloaderEl',
|
||||
onInit: function(el) {
|
||||
_loadingIndicator = el;
|
||||
}
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
var _setupUIElements = function() {
|
||||
var item,
|
||||
classAttr,
|
||||
uiElement;
|
||||
|
||||
var loopThroughChildElements = function(sChildren) {
|
||||
if(!sChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
var l = sChildren.length;
|
||||
for(var i = 0; i < l; i++) {
|
||||
item = sChildren[i];
|
||||
classAttr = item.className;
|
||||
|
||||
for(var a = 0; a < _uiElements.length; a++) {
|
||||
uiElement = _uiElements[a];
|
||||
|
||||
if(classAttr.indexOf('pswp__' + uiElement.name) > -1 ) {
|
||||
|
||||
if( _options[uiElement.option] ) { // if element is not disabled from options
|
||||
|
||||
framework.removeClass(item, 'pswp__element--disabled');
|
||||
if(uiElement.onInit) {
|
||||
uiElement.onInit(item);
|
||||
}
|
||||
|
||||
//item.style.display = 'block';
|
||||
} else {
|
||||
framework.addClass(item, 'pswp__element--disabled');
|
||||
//item.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
loopThroughChildElements(_controls.children);
|
||||
|
||||
var topBar = framework.getChildByClass(_controls, 'pswp__top-bar');
|
||||
if(topBar) {
|
||||
loopThroughChildElements( topBar.children );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
ui.init = function() {
|
||||
|
||||
// extend options
|
||||
framework.extend(pswp.options, _defaultUIOptions, true);
|
||||
|
||||
// create local link for fast access
|
||||
_options = pswp.options;
|
||||
|
||||
// find pswp__ui element
|
||||
_controls = framework.getChildByClass(pswp.scrollWrap, 'pswp__ui');
|
||||
|
||||
// create local link
|
||||
_listen = pswp.listen;
|
||||
|
||||
|
||||
_setupHidingControlsDuringGestures();
|
||||
|
||||
// update controls when slides change
|
||||
_listen('beforeChange', ui.update);
|
||||
|
||||
// toggle zoom on double-tap
|
||||
_listen('doubleTap', function(point) {
|
||||
var initialZoomLevel = pswp.currItem.initialZoomLevel;
|
||||
if(pswp.getZoomLevel() !== initialZoomLevel) {
|
||||
pswp.zoomTo(initialZoomLevel, point, 333);
|
||||
} else {
|
||||
pswp.zoomTo(_options.getDoubleTapZoom(false, pswp.currItem), point, 333);
|
||||
}
|
||||
});
|
||||
|
||||
// Allow text selection in caption
|
||||
_listen('preventDragEvent', function(e, isDown, preventObj) {
|
||||
var t = e.target || e.srcElement;
|
||||
if(
|
||||
t &&
|
||||
t.getAttribute('class') && e.type.indexOf('mouse') > -1 &&
|
||||
( t.getAttribute('class').indexOf('__caption') > 0 || (/(SMALL|STRONG|EM)/i).test(t.tagName) )
|
||||
) {
|
||||
preventObj.prevent = false;
|
||||
}
|
||||
});
|
||||
|
||||
// bind events for UI
|
||||
_listen('bindEvents', function() {
|
||||
framework.bind(_controls, 'pswpTap click', _onControlsTap);
|
||||
framework.bind(pswp.scrollWrap, 'pswpTap', ui.onGlobalTap);
|
||||
|
||||
if(!pswp.likelyTouchDevice) {
|
||||
framework.bind(pswp.scrollWrap, 'mouseover', ui.onMouseOver);
|
||||
}
|
||||
});
|
||||
|
||||
// unbind events for UI
|
||||
_listen('unbindEvents', function() {
|
||||
if(!_shareModalHidden) {
|
||||
_toggleShareModal();
|
||||
}
|
||||
|
||||
if(_idleInterval) {
|
||||
clearInterval(_idleInterval);
|
||||
}
|
||||
framework.unbind(document, 'mouseout', _onMouseLeaveWindow);
|
||||
framework.unbind(document, 'mousemove', _onIdleMouseMove);
|
||||
framework.unbind(_controls, 'pswpTap click', _onControlsTap);
|
||||
framework.unbind(pswp.scrollWrap, 'pswpTap', ui.onGlobalTap);
|
||||
framework.unbind(pswp.scrollWrap, 'mouseover', ui.onMouseOver);
|
||||
|
||||
if(_fullscrenAPI) {
|
||||
framework.unbind(document, _fullscrenAPI.eventK, ui.updateFullscreen);
|
||||
if(_fullscrenAPI.isFullscreen()) {
|
||||
_options.hideAnimationDuration = 0;
|
||||
_fullscrenAPI.exit();
|
||||
}
|
||||
_fullscrenAPI = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// clean up things when gallery is destroyed
|
||||
_listen('destroy', function() {
|
||||
if(_options.captionEl) {
|
||||
if(_fakeCaptionContainer) {
|
||||
_controls.removeChild(_fakeCaptionContainer);
|
||||
}
|
||||
framework.removeClass(_captionContainer, 'pswp__caption--empty');
|
||||
}
|
||||
|
||||
if(_shareModal) {
|
||||
_shareModal.children[0].onclick = null;
|
||||
}
|
||||
framework.removeClass(_controls, 'pswp__ui--over-close');
|
||||
framework.addClass( _controls, 'pswp__ui--hidden');
|
||||
ui.setIdle(false);
|
||||
});
|
||||
|
||||
|
||||
if(!_options.showAnimationDuration) {
|
||||
framework.removeClass( _controls, 'pswp__ui--hidden');
|
||||
}
|
||||
_listen('initialZoomIn', function() {
|
||||
if(_options.showAnimationDuration) {
|
||||
framework.removeClass( _controls, 'pswp__ui--hidden');
|
||||
}
|
||||
});
|
||||
_listen('initialZoomOut', function() {
|
||||
framework.addClass( _controls, 'pswp__ui--hidden');
|
||||
});
|
||||
|
||||
_listen('parseVerticalMargin', _applyNavBarGaps);
|
||||
|
||||
_setupUIElements();
|
||||
|
||||
if(_options.shareEl && _shareButton && _shareModal) {
|
||||
_shareModalHidden = true;
|
||||
}
|
||||
|
||||
_countNumItems();
|
||||
|
||||
_setupIdle();
|
||||
|
||||
_setupFullscreenAPI();
|
||||
|
||||
_setupLoadingIndicator();
|
||||
};
|
||||
|
||||
ui.setIdle = function(isIdle) {
|
||||
_isIdle = isIdle;
|
||||
_togglePswpClass(_controls, 'ui--idle', isIdle);
|
||||
};
|
||||
|
||||
ui.update = function() {
|
||||
// Don't update UI if it's hidden
|
||||
if(_controlsVisible && pswp.currItem) {
|
||||
|
||||
ui.updateIndexIndicator();
|
||||
|
||||
if(_options.captionEl) {
|
||||
_options.addCaptionHTMLFn(pswp.currItem, _captionContainer);
|
||||
|
||||
_togglePswpClass(_captionContainer, 'caption--empty', !pswp.currItem.title);
|
||||
}
|
||||
|
||||
_overlayUIUpdated = true;
|
||||
|
||||
} else {
|
||||
_overlayUIUpdated = false;
|
||||
}
|
||||
|
||||
if(!_shareModalHidden) {
|
||||
_toggleShareModal();
|
||||
}
|
||||
|
||||
_countNumItems();
|
||||
};
|
||||
|
||||
ui.updateFullscreen = function(e) {
|
||||
|
||||
if(e) {
|
||||
// some browsers change window scroll position during the fullscreen
|
||||
// so PhotoSwipe updates it just in case
|
||||
setTimeout(function() {
|
||||
pswp.setScrollOffset( 0, framework.getScrollY() );
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// toogle pswp--fs class on root element
|
||||
framework[ (_fullscrenAPI.isFullscreen() ? 'add' : 'remove') + 'Class' ](pswp.template, 'pswp--fs');
|
||||
};
|
||||
|
||||
ui.updateIndexIndicator = function() {
|
||||
if(_options.counterEl) {
|
||||
_indexIndicator.innerHTML = (pswp.getCurrentIndex()+1) +
|
||||
_options.indexIndicatorSep +
|
||||
_options.getNumItemsFn();
|
||||
}
|
||||
};
|
||||
|
||||
ui.onGlobalTap = function(e) {
|
||||
e = e || window.event;
|
||||
var target = e.target || e.srcElement;
|
||||
|
||||
if(_blockControlsTap) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(e.detail && e.detail.pointerType === 'mouse') {
|
||||
|
||||
// close gallery if clicked outside of the image
|
||||
if(_hasCloseClass(target)) {
|
||||
pswp.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if(framework.hasClass(target, 'pswp__img')) {
|
||||
if(pswp.getZoomLevel() === 1 && pswp.getZoomLevel() <= pswp.currItem.fitRatio) {
|
||||
if(_options.clickToCloseNonZoomable) {
|
||||
pswp.close();
|
||||
}
|
||||
} else {
|
||||
pswp.toggleDesktopZoom(e.detail.releasePoint);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// tap anywhere (except buttons) to toggle visibility of controls
|
||||
if(_options.tapToToggleControls) {
|
||||
if(_controlsVisible) {
|
||||
ui.hideControls();
|
||||
} else {
|
||||
ui.showControls();
|
||||
}
|
||||
}
|
||||
|
||||
// tap to close gallery
|
||||
if(_options.tapToClose && (framework.hasClass(target, 'pswp__img') || _hasCloseClass(target)) ) {
|
||||
pswp.close();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
ui.onMouseOver = function(e) {
|
||||
e = e || window.event;
|
||||
var target = e.target || e.srcElement;
|
||||
|
||||
// add class when mouse is over an element that should close the gallery
|
||||
_togglePswpClass(_controls, 'ui--over-close', _hasCloseClass(target));
|
||||
};
|
||||
|
||||
ui.hideControls = function() {
|
||||
framework.addClass(_controls,'pswp__ui--hidden');
|
||||
_controlsVisible = false;
|
||||
};
|
||||
|
||||
ui.showControls = function() {
|
||||
_controlsVisible = true;
|
||||
if(!_overlayUIUpdated) {
|
||||
ui.update();
|
||||
}
|
||||
framework.removeClass(_controls,'pswp__ui--hidden');
|
||||
};
|
||||
|
||||
ui.supportsFullscreen = function() {
|
||||
var d = document;
|
||||
return !!(d.exitFullscreen || d.mozCancelFullScreen || d.webkitExitFullscreen || d.msExitFullscreen);
|
||||
};
|
||||
|
||||
ui.getFullscreenAPI = function() {
|
||||
var dE = document.documentElement,
|
||||
api,
|
||||
tF = 'fullscreenchange';
|
||||
|
||||
if (dE.requestFullscreen) {
|
||||
api = {
|
||||
enterK: 'requestFullscreen',
|
||||
exitK: 'exitFullscreen',
|
||||
elementK: 'fullscreenElement',
|
||||
eventK: tF
|
||||
};
|
||||
|
||||
} else if(dE.mozRequestFullScreen ) {
|
||||
api = {
|
||||
enterK: 'mozRequestFullScreen',
|
||||
exitK: 'mozCancelFullScreen',
|
||||
elementK: 'mozFullScreenElement',
|
||||
eventK: 'moz' + tF
|
||||
};
|
||||
|
||||
|
||||
|
||||
} else if(dE.webkitRequestFullscreen) {
|
||||
api = {
|
||||
enterK: 'webkitRequestFullscreen',
|
||||
exitK: 'webkitExitFullscreen',
|
||||
elementK: 'webkitFullscreenElement',
|
||||
eventK: 'webkit' + tF
|
||||
};
|
||||
|
||||
} else if(dE.msRequestFullscreen) {
|
||||
api = {
|
||||
enterK: 'msRequestFullscreen',
|
||||
exitK: 'msExitFullscreen',
|
||||
elementK: 'msFullscreenElement',
|
||||
eventK: 'MSFullscreenChange'
|
||||
};
|
||||
}
|
||||
|
||||
if(api) {
|
||||
api.enter = function() {
|
||||
// disable close-on-scroll in fullscreen
|
||||
_initalCloseOnScrollValue = _options.closeOnScroll;
|
||||
_options.closeOnScroll = false;
|
||||
|
||||
if(this.enterK === 'webkitRequestFullscreen') {
|
||||
pswp.template[this.enterK]( Element.ALLOW_KEYBOARD_INPUT );
|
||||
} else {
|
||||
return pswp.template[this.enterK]();
|
||||
}
|
||||
};
|
||||
api.exit = function() {
|
||||
_options.closeOnScroll = _initalCloseOnScrollValue;
|
||||
|
||||
return document[this.exitK]();
|
||||
|
||||
};
|
||||
api.isFullscreen = function() { return document[this.elementK]; };
|
||||
}
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
|
||||
|
||||
};
|
||||
return PhotoSwipeUI_Default;
|
||||
|
||||
|
||||
});
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
/*! PhotoSwipe main CSS by Dmitry Semenov | photoswipe.com | MIT license */
|
||||
/*
|
||||
Styles for basic PhotoSwipe functionality (sliding area, open/close transitions)
|
||||
*/
|
||||
/* pswp = photoswipe */
|
||||
.pswp {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
z-index: 1500;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* create separate layer, to avoid paint on window.onscroll in webkit/blink */
|
||||
-webkit-backface-visibility: hidden;
|
||||
outline: none; }
|
||||
.pswp * {
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box; }
|
||||
.pswp img {
|
||||
max-width: none; }
|
||||
|
||||
/* style is added when JS option showHideOpacity is set to true */
|
||||
.pswp--animate_opacity {
|
||||
/* 0.001, because opacity:0 doesn't trigger Paint action, which causes lag at start of transition */
|
||||
opacity: 0.001;
|
||||
will-change: opacity;
|
||||
/* for open/close transition */
|
||||
-webkit-transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
|
||||
transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); }
|
||||
|
||||
.pswp--open {
|
||||
display: block; }
|
||||
|
||||
.pswp--zoom-allowed .pswp__img {
|
||||
/* autoprefixer: off */
|
||||
cursor: -webkit-zoom-in;
|
||||
cursor: -moz-zoom-in;
|
||||
cursor: zoom-in; }
|
||||
|
||||
.pswp--zoomed-in .pswp__img {
|
||||
/* autoprefixer: off */
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab; }
|
||||
|
||||
.pswp--dragging .pswp__img {
|
||||
/* autoprefixer: off */
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing; }
|
||||
|
||||
/*
|
||||
Background is added as a separate element.
|
||||
As animating opacity is much faster than animating rgba() background-color.
|
||||
*/
|
||||
.pswp__bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
opacity: 0;
|
||||
transform: translateZ(0);
|
||||
-webkit-backface-visibility: hidden;
|
||||
will-change: opacity; }
|
||||
|
||||
.pswp__scroll-wrap {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden; }
|
||||
|
||||
.pswp__container,
|
||||
.pswp__zoom-wrap {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0; }
|
||||
|
||||
/* Prevent selection and tap highlights */
|
||||
.pswp__container,
|
||||
.pswp__img {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none; }
|
||||
|
||||
.pswp__zoom-wrap {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
-webkit-transform-origin: left top;
|
||||
-ms-transform-origin: left top;
|
||||
transform-origin: left top;
|
||||
/* for open/close transition */
|
||||
-webkit-transition: -webkit-transform 333ms cubic-bezier(0.4, 0, 0.22, 1);
|
||||
transition: transform 333ms cubic-bezier(0.4, 0, 0.22, 1); }
|
||||
|
||||
.pswp__bg {
|
||||
will-change: opacity;
|
||||
/* for open/close transition */
|
||||
-webkit-transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
|
||||
transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); }
|
||||
|
||||
.pswp--animated-in .pswp__bg,
|
||||
.pswp--animated-in .pswp__zoom-wrap {
|
||||
-webkit-transition: none;
|
||||
transition: none; }
|
||||
|
||||
.pswp__container,
|
||||
.pswp__zoom-wrap {
|
||||
-webkit-backface-visibility: hidden; }
|
||||
|
||||
.pswp__item {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden; }
|
||||
|
||||
.pswp__img {
|
||||
position: absolute;
|
||||
width: auto;
|
||||
height: auto;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
/*
|
||||
stretched thumbnail or div placeholder element (see below)
|
||||
style is added to avoid flickering in webkit/blink when layers overlap
|
||||
*/
|
||||
.pswp__img--placeholder {
|
||||
-webkit-backface-visibility: hidden; }
|
||||
|
||||
/*
|
||||
div element that matches size of large image
|
||||
large image loads on top of it
|
||||
*/
|
||||
.pswp__img--placeholder--blank {
|
||||
background: #222; }
|
||||
|
||||
.pswp--ie .pswp__img {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
left: 0;
|
||||
top: 0; }
|
||||
|
||||
/*
|
||||
Error message appears when image is not loaded
|
||||
(JS option errorMsg controls markup)
|
||||
*/
|
||||
.pswp__error-msg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
margin-top: -8px;
|
||||
color: #CCC; }
|
||||
|
||||
.pswp__error-msg a {
|
||||
color: #CCC;
|
||||
text-decoration: underline; }
|
||||
File diff suppressed because it is too large
Load diff
3
frappe/public/js/photoswipe.bundle.js
Normal file
3
frappe/public/js/photoswipe.bundle.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import PhotoSwipe from "photoswipe";
|
||||
|
||||
frappe.PhotoSwipe = PhotoSwipe;
|
||||
|
|
@ -25,7 +25,6 @@
|
|||
|
||||
// checkbox
|
||||
--checkbox-right-margin: var(--margin-xs);
|
||||
--checkbox-size: 14px;
|
||||
--checkbox-focus-shadow: var(--focus-default);
|
||||
|
||||
// date-picker
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
|
||||
.dt-cell {
|
||||
color: var(--text-color) !important;
|
||||
background-color: var(--bg-color) !important;
|
||||
}
|
||||
|
||||
.frappe-control,
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@
|
|||
opacity: 0;
|
||||
font-size: $font-size-lg;
|
||||
position: absolute;
|
||||
background: var(--gray-300);
|
||||
background: var(--gray-400);
|
||||
border-radius: $border-radius;
|
||||
|
||||
.icon {
|
||||
|
|
@ -192,11 +192,6 @@
|
|||
stroke: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
// show zoom button on mobile devices
|
||||
// @media (max-width: $screen-xs) {
|
||||
// opacity: 0.5
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -211,10 +206,6 @@
|
|||
background: none !important;
|
||||
}
|
||||
|
||||
.pswp__bg {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
.pswp__more-items {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
.kanban {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
overflow-y: hidden;
|
||||
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
|
|
@ -27,19 +28,17 @@
|
|||
.kanban-column {
|
||||
@include transition();
|
||||
|
||||
flex: 0 0 260px;
|
||||
max-width: 260px;
|
||||
flex: 1 0 260px;
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--padding-md);
|
||||
min-height: calc(100vh - 250px);
|
||||
max-height: calc(100vh - var(--navbar-height) - var(--page-bottom-margin) - 80px);
|
||||
min-height: calc(100vh - 150px);
|
||||
|
||||
.add-card {
|
||||
@include flex(flex, center, center, null);
|
||||
@include transition();
|
||||
|
||||
color: var(--text-light);
|
||||
background-color: var(--kanban-new-card-bg);
|
||||
background-color: var(--bg-color);
|
||||
height: 27px;
|
||||
font-size: var(--text-md);
|
||||
margin-bottom: var(--margin-sm);
|
||||
|
|
@ -105,10 +104,6 @@
|
|||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-column-title {
|
||||
|
|
@ -123,6 +118,7 @@
|
|||
.kanban-title {
|
||||
@include get_textstyle("lg", "semibold");
|
||||
margin-left: var(--margin-sm);
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +178,7 @@
|
|||
|
||||
.kanban-card-body {
|
||||
cursor: grab;
|
||||
padding: var(--padding-sm);
|
||||
padding: var(--padding-md);
|
||||
|
||||
.kanban-title-area {
|
||||
margin-bottom: 12px;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
{{ item }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="more-block py-6 {% if not show_more -%} hidden {%- endif %}">
|
||||
<div class="more-block py-6 {% if not show_more -%} d-none {%- endif %}">
|
||||
<button class="btn btn-light btn-more btn-sm">{{ _("More") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ login.bind_events = function () {
|
|||
login.route();
|
||||
});
|
||||
|
||||
|
||||
$(".form-login").on("submit", function (event) {
|
||||
event.preventDefault();
|
||||
var args = {};
|
||||
|
|
@ -300,13 +299,6 @@ login.login_handlers = (function () {
|
|||
frappe.ready(function () {
|
||||
|
||||
login.bind_events();
|
||||
|
||||
if (!window.location.hash) {
|
||||
window.location.hash = "#login";
|
||||
} else {
|
||||
$(window).trigger("hashchange");
|
||||
}
|
||||
|
||||
if (window.show_footer_on_login) {
|
||||
$("body .web-footer").show();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1109,6 +1109,15 @@ class TestDBQuery(FrappeTestCase):
|
|||
self.assertNotIn("ifnull", frappe.get_all("User", {"name": ("in", (""))}, run=0))
|
||||
self.assertNotIn("ifnull", frappe.get_all("User", {"name": ("in", ())}, run=0))
|
||||
|
||||
def test_coalesce_with_datetime_ops(self):
|
||||
self.assertNotIn("ifnull", frappe.get_all("User", {"last_active": (">", "2022-01-01")}, run=0))
|
||||
self.assertNotIn("ifnull", frappe.get_all("User", {"creation": ("<", "2022-01-01")}, run=0))
|
||||
self.assertNotIn(
|
||||
"ifnull",
|
||||
frappe.get_all("User", {"last_active": ("between", ("2022-01-01", "2023-01-01"))}, run=0),
|
||||
)
|
||||
self.assertIn("ifnull", frappe.get_all("User", {"last_active": ("<", "2022-01-01")}, run=0))
|
||||
|
||||
def test_ambiguous_linked_tables(self):
|
||||
from frappe.desk.reportview import get
|
||||
|
||||
|
|
|
|||
|
|
@ -163,6 +163,17 @@ class TestDBUpdate(FrappeTestCase):
|
|||
self.assertIn(varchar, frappe.db.get_column_type(doctype.name, "name"))
|
||||
doc.reload() # ensure that docs are still accesible
|
||||
|
||||
def test_uuid_link_field(self):
|
||||
uuid_doctype = new_doctype().update({"autoname": "UUID"}).insert()
|
||||
self.assertEqual(frappe.db.get_column_type(uuid_doctype.name, "name"), "uuid")
|
||||
|
||||
link = "link_field"
|
||||
referring_doctype = new_doctype(
|
||||
fields=[{"fieldname": link, "fieldtype": "Link", "options": uuid_doctype.name}]
|
||||
).insert()
|
||||
|
||||
self.assertEqual(frappe.db.get_column_type(referring_doctype.name, link), "uuid")
|
||||
|
||||
|
||||
def get_fieldtype_from_def(field_def):
|
||||
fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ("", 0))
|
||||
|
|
|
|||
|
|
@ -119,7 +119,6 @@ class TestEmail(FrappeTestCase):
|
|||
self.assertTrue("CC: test1@example.com" in message)
|
||||
|
||||
def test_cc_footer(self):
|
||||
frappe.conf.use_ssl = True
|
||||
# test if sending with cc's makes it into header
|
||||
frappe.sendmail(
|
||||
recipients=["test@example.com"],
|
||||
|
|
@ -151,10 +150,6 @@ class TestEmail(FrappeTestCase):
|
|||
in frappe.safe_decode(frappe.flags.sent_mail)
|
||||
)
|
||||
|
||||
# check for email tracker
|
||||
self.assertTrue("mark_email_as_seen" in frappe.safe_decode(frappe.flags.sent_mail))
|
||||
frappe.conf.use_ssl = False
|
||||
|
||||
def test_expose(self):
|
||||
from frappe.utils import set_request
|
||||
from frappe.utils.verified_command import verify_request
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ class TestOverheadCalls(FrappeAPITestCase):
|
|||
|
||||
def test_ping_overheads(self):
|
||||
self.get(self.method("ping"), {"sid": "Guest"})
|
||||
with self.assertRedisCallCounts(12), self.assertQueryCount(self.BASE_SQL_CALLS):
|
||||
with self.assertRedisCallCounts(13), self.assertQueryCount(self.BASE_SQL_CALLS):
|
||||
self.get(self.method("ping"), {"sid": "Guest"})
|
||||
|
||||
def test_list_view_overheads(self):
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import ScheduledJobType, sync_jobs
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, get_datetime
|
||||
from frappe.utils.data import now_datetime
|
||||
from frappe.utils.doctor import purge_pending_jobs
|
||||
from frappe.utils.scheduler import (
|
||||
DEFAULT_SCHEDULER_TICK,
|
||||
_get_last_creation_timestamp,
|
||||
enqueue_events,
|
||||
is_dormant,
|
||||
schedule_jobs_based_on_activity,
|
||||
sleep_duration,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -23,7 +28,7 @@ def test_method():
|
|||
pass
|
||||
|
||||
|
||||
class TestScheduler(TestCase):
|
||||
class TestScheduler(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
|
@ -77,6 +82,7 @@ class TestScheduler(TestCase):
|
|||
job_log.db_set(
|
||||
"creation", add_days(_get_last_creation_timestamp("Activity Log"), 5), update_modified=False
|
||||
)
|
||||
schedule_jobs_based_on_activity.clear_cache()
|
||||
|
||||
# inactive site with recent job, don't run
|
||||
self.assertFalse(
|
||||
|
|
@ -92,6 +98,21 @@ class TestScheduler(TestCase):
|
|||
)
|
||||
)
|
||||
|
||||
def test_real_time_alignment(self):
|
||||
test_cases = {
|
||||
timedelta(minutes=0): DEFAULT_SCHEDULER_TICK,
|
||||
timedelta(minutes=0, seconds=12): DEFAULT_SCHEDULER_TICK - 12,
|
||||
timedelta(minutes=1, seconds=12): DEFAULT_SCHEDULER_TICK - (1 * 60 + 12),
|
||||
timedelta(hours=23, minutes=59): 60,
|
||||
timedelta(hours=23, minutes=59, seconds=30): 30,
|
||||
timedelta(minutes=0, seconds=1): DEFAULT_SCHEDULER_TICK - 1,
|
||||
timedelta(minutes=2): DEFAULT_SCHEDULER_TICK - 2 * 60,
|
||||
}
|
||||
for delta, expected_sleep in test_cases.items():
|
||||
fake_time = datetime(2024, 1, 1) + delta
|
||||
with self.freeze_time(fake_time, is_utc=True):
|
||||
self.assertEqual(sleep_duration(DEFAULT_SCHEDULER_TICK), expected_sleep, delta)
|
||||
|
||||
|
||||
def get_test_job(method="frappe.tests.test_scheduler.test_timeout_10", frequency="All") -> ScheduledJobType:
|
||||
if not frappe.db.exists("Scheduled Job Type", dict(method=method)):
|
||||
|
|
|
|||
|
|
@ -226,14 +226,15 @@ class FrappeTestCase(unittest.TestCase):
|
|||
frappe.connect()
|
||||
|
||||
@contextmanager
|
||||
def freeze_time(self, time_to_freeze, *args, **kwargs):
|
||||
def freeze_time(self, time_to_freeze, is_utc=False, *args, **kwargs):
|
||||
from freezegun import freeze_time
|
||||
|
||||
# Freeze time expects UTC or tzaware objects. We have neither, so convert to UTC.
|
||||
timezone = pytz.timezone(get_system_timezone())
|
||||
fake_time_with_tz = timezone.localize(get_datetime(time_to_freeze)).astimezone(pytz.utc)
|
||||
if not is_utc:
|
||||
# Freeze time expects UTC or tzaware objects. We have neither, so convert to UTC.
|
||||
timezone = pytz.timezone(get_system_timezone())
|
||||
time_to_freeze = timezone.localize(get_datetime(time_to_freeze)).astimezone(pytz.utc)
|
||||
|
||||
with freeze_time(fake_time_with_tz, *args, **kwargs):
|
||||
with freeze_time(time_to_freeze, *args, **kwargs):
|
||||
yield
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -482,7 +482,9 @@ def execute_in_shell(cmd, verbose=False, low_priority=False, check_exit_code=Fal
|
|||
print(out)
|
||||
|
||||
if failed:
|
||||
raise Exception("Command failed")
|
||||
raise frappe.CommandFailedError(
|
||||
"Command failed", out.decode(errors="replace"), err.decode(errors="replace")
|
||||
)
|
||||
|
||||
return err, out
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from collections import defaultdict
|
|||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from functools import lru_cache
|
||||
from threading import Thread
|
||||
from typing import Any, NoReturn
|
||||
from uuid import uuid4
|
||||
|
||||
|
|
@ -283,7 +284,7 @@ def start_worker(
|
|||
rq_password: str | None = None,
|
||||
burst: bool = False,
|
||||
strategy: DequeueStrategy | None = DequeueStrategy.DEFAULT,
|
||||
) -> None: # pragma: no cover
|
||||
) -> NoReturn: # pragma: no cover
|
||||
"""Wrapper to start rq worker. Connects to redis and monitors these queues."""
|
||||
|
||||
if not strategy:
|
||||
|
|
@ -319,6 +320,23 @@ def start_worker(
|
|||
)
|
||||
|
||||
|
||||
class FrappeWorker(Worker):
|
||||
def work(self, *args, **kwargs):
|
||||
self.start_frappe_scheduler()
|
||||
kwargs["with_scheduler"] = False # Always disable RQ scheduler
|
||||
return super().work(*args, **kwargs)
|
||||
|
||||
def run_maintenance_tasks(self, *args, **kwargs):
|
||||
"""Attempt to start a scheduler in case the worker doing scheduling died."""
|
||||
self.start_frappe_scheduler()
|
||||
return super().run_maintenance_tasks(*args, **kwargs)
|
||||
|
||||
def start_frappe_scheduler(self):
|
||||
from frappe.utils.scheduler import start_scheduler
|
||||
|
||||
Thread(target=start_scheduler, daemon=True).start()
|
||||
|
||||
|
||||
def start_worker_pool(
|
||||
queue: str | None = None,
|
||||
num_workers: int = 1,
|
||||
|
|
@ -335,8 +353,9 @@ def start_worker_pool(
|
|||
# If gc.freeze is done then importing modules before forking allows us to share the memory
|
||||
import frappe.database.query # sqlparse and indirect imports
|
||||
import frappe.query_builder # pypika
|
||||
import frappe.utils.data # common utils
|
||||
import frappe.utils # common utils
|
||||
import frappe.utils.safe_exec
|
||||
import frappe.utils.scheduler
|
||||
import frappe.utils.typing_validations # any whitelisted method uses this
|
||||
import frappe.website.path_resolver # all the page types and resolver
|
||||
|
||||
|
|
@ -363,6 +382,7 @@ def start_worker_pool(
|
|||
queues=queues,
|
||||
connection=redis_connection,
|
||||
num_workers=num_workers,
|
||||
worker_class=FrappeWorker, # Auto starts scheduler with workerpool
|
||||
)
|
||||
pool.start(logging_level=logging_level, burst=burst)
|
||||
|
||||
|
|
|
|||
|
|
@ -352,12 +352,21 @@ class BackupGenerator:
|
|||
else:
|
||||
cmd_string = "tar -cf {0} {1}"
|
||||
|
||||
frappe.utils.execute_in_shell(
|
||||
cmd_string.format(backup_path, files_path),
|
||||
verbose=self.verbose,
|
||||
low_priority=True,
|
||||
check_exit_code=True,
|
||||
)
|
||||
try:
|
||||
frappe.utils.execute_in_shell(
|
||||
cmd_string.format(backup_path, files_path),
|
||||
verbose=self.verbose,
|
||||
low_priority=True,
|
||||
check_exit_code=True,
|
||||
)
|
||||
except frappe.CommandFailedError as e:
|
||||
if e.err and "file changed as we read it" in e.err:
|
||||
click.secho(
|
||||
"Ignoring `tar: file changed as we read it` to prevent backup failure",
|
||||
fg="red",
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def copy_site_config(self):
|
||||
site_config_backup_path = self.backup_path_conf
|
||||
|
|
|
|||
|
|
@ -318,6 +318,8 @@ def parse_github_url(remote_url: str) -> tuple[str, str] | tuple[None, None]:
|
|||
def get_source_url(app: str) -> str | None:
|
||||
"""Get the remote URL of the app."""
|
||||
pyproject = get_pyproject(app)
|
||||
if not pyproject:
|
||||
return
|
||||
if remote_url := pyproject.get("project", {}).get("urls", {}).get("Repository"):
|
||||
return remote_url.rstrip("/")
|
||||
|
||||
|
|
|
|||
|
|
@ -395,27 +395,6 @@ def get_file_name(fname, optional_suffix):
|
|||
return fname
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_file(file_url):
|
||||
"""
|
||||
Download file using token and REST API. Valid session or
|
||||
token is required to download private files.
|
||||
|
||||
Method : GET
|
||||
Endpoint : frappe.utils.file_manager.download_file
|
||||
URL Params : file_name = /path/to/file relative to site path
|
||||
"""
|
||||
file_doc = frappe.get_doc("File", {"file_url": file_url})
|
||||
file_doc.check_permission("read")
|
||||
path = os.path.join(get_files_path(), os.path.basename(file_url))
|
||||
|
||||
with open(path, "rb") as fileobj:
|
||||
filedata = fileobj.read()
|
||||
frappe.local.response.filename = os.path.basename(file_url)
|
||||
frappe.local.response.filecontent = filedata
|
||||
frappe.local.response.type = "download"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_attachments(doctype, name, attachments):
|
||||
"""Add attachments to the given DocType"""
|
||||
|
|
|
|||
|
|
@ -8,22 +8,24 @@ Events:
|
|||
weekly
|
||||
"""
|
||||
|
||||
# imports - standard imports
|
||||
import datetime
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from typing import NoReturn
|
||||
|
||||
import pytz
|
||||
import setproctitle
|
||||
from croniter import CroniterBadCronError
|
||||
from filelock import FileLock, Timeout
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
from frappe.utils import cint, get_datetime, get_sites, now_datetime
|
||||
from frappe.utils import cint, get_bench_path, get_datetime, get_sites, now_datetime
|
||||
from frappe.utils.background_jobs import set_niceness
|
||||
from frappe.utils.synchronization import filelock
|
||||
from frappe.utils.caching import redis_cache
|
||||
|
||||
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
DEFAULT_SCHEDULER_TICK = 4 * 60
|
||||
|
||||
|
||||
def cprint(*args, **kwargs):
|
||||
|
|
@ -36,7 +38,7 @@ def cprint(*args, **kwargs):
|
|||
|
||||
|
||||
def _proctitle(message):
|
||||
setproctitle.setproctitle(f"frappe-scheduler: {message}")
|
||||
setproctitle.setthreadtitle(f"frappe-scheduler: {message}")
|
||||
|
||||
|
||||
def start_scheduler() -> NoReturn:
|
||||
|
|
@ -46,20 +48,40 @@ def start_scheduler() -> NoReturn:
|
|||
tick = get_scheduler_tick()
|
||||
set_niceness()
|
||||
|
||||
with filelock("scheduler_process", timeout=1, is_global=True):
|
||||
while True:
|
||||
_proctitle("idle")
|
||||
time.sleep(tick)
|
||||
enqueue_events_for_all_sites()
|
||||
lock_path = os.path.abspath(os.path.join(get_bench_path(), "config", "scheduler_process"))
|
||||
|
||||
try:
|
||||
lock = FileLock(lock_path)
|
||||
lock.acquire(blocking=False)
|
||||
except Timeout:
|
||||
frappe.logger("scheduler").debug("Scheduler already running")
|
||||
return
|
||||
|
||||
while True:
|
||||
_proctitle("idle")
|
||||
time.sleep(sleep_duration(tick))
|
||||
enqueue_events_for_all_sites()
|
||||
|
||||
|
||||
def sleep_duration(tick):
|
||||
if tick != DEFAULT_SCHEDULER_TICK:
|
||||
# Assuming user knows what they want.
|
||||
return tick
|
||||
|
||||
# Sleep until next multiple of tick.
|
||||
# This makes scheduler aligned with real clock,
|
||||
# so event scheduled at 12:00 happen at 12:00 and not 12:00:35.
|
||||
minutes = tick // 60
|
||||
now = datetime.datetime.now(pytz.UTC)
|
||||
left_minutes = minutes - now.minute % minutes
|
||||
next_execution = now.replace(second=0) + datetime.timedelta(minutes=left_minutes)
|
||||
|
||||
return (next_execution - now).total_seconds()
|
||||
|
||||
|
||||
def enqueue_events_for_all_sites() -> None:
|
||||
"""Loop through sites and enqueue events that are not already queued"""
|
||||
|
||||
if os.path.exists(os.path.join(".", ".restarting")):
|
||||
# Don't add task to queue if webserver is in restart mode
|
||||
return
|
||||
|
||||
with frappe.init_site():
|
||||
sites = get_sites()
|
||||
|
||||
|
|
@ -99,7 +121,9 @@ def enqueue_events_for_site(site: str) -> None:
|
|||
def enqueue_events() -> list[str] | None:
|
||||
if schedule_jobs_based_on_activity():
|
||||
enqueued_jobs = []
|
||||
for job_type in frappe.get_all("Scheduled Job Type", filters={"stopped": 0}, fields="*"):
|
||||
all_jobs = frappe.get_all("Scheduled Job Type", filters={"stopped": 0}, fields="*")
|
||||
random.shuffle(all_jobs)
|
||||
for job_type in all_jobs:
|
||||
job_type = frappe.get_doc(doctype="Scheduled Job Type", **job_type)
|
||||
try:
|
||||
if job_type.enqueue():
|
||||
|
|
@ -156,6 +180,7 @@ def disable_scheduler():
|
|||
toggle_scheduler(False)
|
||||
|
||||
|
||||
@redis_cache(ttl=60 * 60)
|
||||
def schedule_jobs_based_on_activity(check_time=None):
|
||||
"""Return True for active sites as defined by `Activity Log`.
|
||||
Also return True for inactive sites once every 24 hours based on `Scheduled Job Log`."""
|
||||
|
|
@ -215,4 +240,5 @@ def get_scheduler_status():
|
|||
|
||||
|
||||
def get_scheduler_tick() -> int:
|
||||
return cint(frappe.get_conf().scheduler_tick_interval) or 60
|
||||
conf = frappe.get_conf()
|
||||
return cint(conf.scheduler_tick_interval) or DEFAULT_SCHEDULER_TICK
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ no_cache = 1
|
|||
|
||||
import json
|
||||
import re
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import frappe
|
||||
import frappe.sessions
|
||||
|
|
@ -18,11 +19,13 @@ CLOSING_SCRIPT_TAG_PATTERN = re.compile(r"</script\>")
|
|||
|
||||
def get_context(context):
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.throw(_("Log in to access this page."), frappe.PermissionError)
|
||||
frappe.response["status_code"] = 403
|
||||
frappe.msgprint(_("Log in to access this page."))
|
||||
frappe.redirect(f"/login?{urlencode({'redirect-to': frappe.request.path})}")
|
||||
|
||||
elif frappe.db.get_value("User", frappe.session.user, "user_type", order_by=None) == "Website User":
|
||||
frappe.throw(_("You are not permitted to access this page."), frappe.PermissionError)
|
||||
|
||||
hooks = frappe.get_hooks()
|
||||
try:
|
||||
boot = frappe.sessions.get()
|
||||
except Exception as e:
|
||||
|
|
@ -42,6 +45,7 @@ def get_context(context):
|
|||
boot_json = CLOSING_SCRIPT_TAG_PATTERN.sub("", boot_json)
|
||||
boot_json = json.dumps(boot_json)
|
||||
|
||||
hooks = frappe.get_hooks()
|
||||
include_js = hooks.get("app_include_js", []) + frappe.conf.get("app_include_js", [])
|
||||
include_css = hooks.get("app_include_css", []) + frappe.conf.get("app_include_css", [])
|
||||
include_icons = hooks.get("app_include_icons", [])
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@
|
|||
"@frappe/esbuild-plugin-postcss2": "^0.1.3",
|
||||
"@fullcalendar/core": "^6.1.11",
|
||||
"@fullcalendar/daygrid": "^6.1.11",
|
||||
"@fullcalendar/interaction": "^6.1.11",
|
||||
"@fullcalendar/list": "^6.1.11",
|
||||
"@fullcalendar/timegrid": "^6.1.11",
|
||||
"@fullcalendar/interaction": "^6.1.11",
|
||||
"@headlessui/vue": "^1.7.16",
|
||||
"@popperjs/core": "^2.11.2",
|
||||
"@redis/client": "^1.5.8",
|
||||
|
|
@ -64,6 +64,7 @@
|
|||
"md5": "^2.3.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"photoswipe": "^5.4.3",
|
||||
"pinia": "^2.0.23",
|
||||
"plyr": "^3.7.8",
|
||||
"popper.js": "^1.16.0",
|
||||
|
|
@ -92,4 +93,4 @@
|
|||
"bufferutil": "^4.0.8",
|
||||
"utf-8-validate": "^6.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue