Merge branch 'develop' into qb-fixes
This commit is contained in:
commit
0a76f1fc36
177 changed files with 22155 additions and 20989 deletions
|
|
@ -64,3 +64,6 @@ e9bbe03354079cfcef65a77b0c33f57b047a7c93
|
|||
|
||||
# another ruff update
|
||||
6ca4d4d167a1a009d99062747711de7a994aa633
|
||||
|
||||
# some more ruff
|
||||
8723a2b6ee9dbec800077f18202ba53b0ef553e7
|
||||
|
|
|
|||
12
.github/actions/setup/action.yml
vendored
12
.github/actions/setup/action.yml
vendored
|
|
@ -4,11 +4,11 @@ inputs:
|
|||
python-version:
|
||||
description: 'Python version to use'
|
||||
required: false
|
||||
default: '3.12.6'
|
||||
default: '3.14'
|
||||
node-version:
|
||||
description: 'Node.js version to use'
|
||||
required: false
|
||||
default: '22'
|
||||
default: '24'
|
||||
build-assets:
|
||||
required: false
|
||||
description: 'Wether to build assets'
|
||||
|
|
@ -45,12 +45,12 @@ runs:
|
|||
git config --global advice.detachedHead false
|
||||
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: apps/${{ github.event.repository.name }}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
|
|
@ -64,14 +64,14 @@ runs:
|
|||
fi
|
||||
|
||||
- name: Checkout Frappe
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ env.FRAPPE_GH_ORG || github.repository_owner }}/frappe
|
||||
ref: ${{ github.event.client_payload.frappe_sha || github.base_ref || github.ref_name }}
|
||||
path: apps/frappe
|
||||
if: github.event.repository.name != 'frappe'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
check-latest: true
|
||||
|
|
|
|||
4
.github/stale.yml
vendored
4
.github/stale.yml
vendored
|
|
@ -1,11 +1,11 @@
|
|||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 28
|
||||
daysUntilStale: 60
|
||||
|
||||
# Number of days of inactivity before a stale Issue or Pull Request is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 3
|
||||
daysUntilClose: 5
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
|
|
|
|||
19
.github/workflows/_base-migration.yml
vendored
19
.github/workflows/_base-migration.yml
vendored
|
|
@ -12,11 +12,11 @@ on:
|
|||
python-version:
|
||||
required: false
|
||||
type: string
|
||||
default: '3.10'
|
||||
default: '3.14'
|
||||
node-version:
|
||||
required: false
|
||||
type: number
|
||||
default: 22
|
||||
default: 24
|
||||
db-artifact-url:
|
||||
required: false
|
||||
type: string
|
||||
|
|
@ -49,6 +49,15 @@ jobs:
|
|||
disable-socketio: true
|
||||
disable-web: true
|
||||
db-root-password: ${{ env.DB_ROOT_PASSWORD }}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: |
|
||||
3.11
|
||||
3.13
|
||||
${{ inputs.python-version }}
|
||||
|
||||
|
||||
- name: Execute pre-migration tasks
|
||||
if: inputs.pre
|
||||
|
|
@ -108,7 +117,7 @@ jobs:
|
|||
fi
|
||||
|
||||
echo "Setting up environment..."
|
||||
if rm -rf ${GITHUB_WORKSPACE}/env && python -m venv ${GITHUB_WORKSPACE}/env; then
|
||||
if rm -rf ${GITHUB_WORKSPACE}/env && python"$2" -m venv ${GITHUB_WORKSPACE}/env; then
|
||||
source ${GITHUB_WORKSPACE}/env/bin/activate
|
||||
pip install --quiet --upgrade pip
|
||||
pip install --quiet frappe-bench
|
||||
|
|
@ -148,13 +157,13 @@ jobs:
|
|||
- name: Update to v14
|
||||
run: |
|
||||
source $RUNNER_TEMP/migrate
|
||||
update_to_version 14
|
||||
update_to_version 14 3.11
|
||||
exit $?
|
||||
|
||||
- name: Update to v15
|
||||
run: |
|
||||
source $RUNNER_TEMP/migrate
|
||||
update_to_version 15
|
||||
update_to_version 15 3.13
|
||||
exit $?
|
||||
|
||||
- name: Update to last commit
|
||||
|
|
|
|||
2
.github/workflows/_base-server-tests.yml
vendored
2
.github/workflows/_base-server-tests.yml
vendored
|
|
@ -13,7 +13,7 @@ on:
|
|||
node-version:
|
||||
required: false
|
||||
type: number
|
||||
default: 22
|
||||
default: 24
|
||||
parallel-runs:
|
||||
required: false
|
||||
type: number
|
||||
|
|
|
|||
2
.github/workflows/_base-ui-tests.yml
vendored
2
.github/workflows/_base-ui-tests.yml
vendored
|
|
@ -13,7 +13,7 @@ on:
|
|||
node-version:
|
||||
required: false
|
||||
type: number
|
||||
default: 22
|
||||
default: 24
|
||||
parallel-runs:
|
||||
required: false
|
||||
type: number
|
||||
|
|
|
|||
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||
|
|
|
|||
2
.github/workflows/linters.yml
vendored
2
.github/workflows/linters.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
fetch-depth: 200
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
check-latest: true
|
||||
|
||||
- name: Check commit titles
|
||||
|
|
|
|||
2
.github/workflows/on_release.yml
vendored
2
.github/workflows/on_release.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/publish-assets-develop.yml
vendored
2
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
path: 'frappe'
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
|
|
|||
2
.github/workflows/run-indinvidual-tests.yml
vendored
2
.github/workflows/run-indinvidual-tests.yml
vendored
|
|
@ -79,7 +79,7 @@ jobs:
|
|||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
|
|
|
|||
4
.github/workflows/server-tests.yml
vendored
4
.github/workflows/server-tests.yml
vendored
|
|
@ -58,8 +58,8 @@ jobs:
|
|||
uses: ./.github/workflows/_base-migration.yml
|
||||
with:
|
||||
db-artifact-url: https://frappeframework.com/files/v13-frappe.sql.gz
|
||||
python-version: '3.10'
|
||||
node-version: 22
|
||||
python-version: '3.14'
|
||||
node-version: 24
|
||||
fake-success: ${{ needs.checkrun.outputs.build != 'strawberry' }}
|
||||
|
||||
coverage:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ fail_fast: false
|
|||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
files: "frappe.*"
|
||||
|
|
@ -22,7 +22,7 @@ repos:
|
|||
exclude: ^frappe/tests/classes/context_managers\.py$
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.13.2
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Run ruff import sorter"
|
||||
|
|
@ -71,7 +71,7 @@ repos:
|
|||
)$
|
||||
|
||||
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||
rev: v9.22.0
|
||||
rev: v9.23.0
|
||||
hooks:
|
||||
- id: commitlint
|
||||
stages: [commit-msg]
|
||||
|
|
|
|||
|
|
@ -72,8 +72,9 @@ context("Sidebar", () => {
|
|||
|
||||
// attach 1 more image to reach attachment limit
|
||||
attach_file("cypress/fixtures/sample_attachments/attachment-11.txt");
|
||||
cy.get(".layout-side-section").scrollTo("top", { ensureScrollable: false });
|
||||
cy.get(".add-attachment-btn").should("be.hidden");
|
||||
cy.get(".explore-link").should("be.visible");
|
||||
// cy.get(".explore-link").should("be.visible");
|
||||
|
||||
// test "Show All" button
|
||||
cy.get(".attachment-row").should("have.length", 10);
|
||||
|
|
|
|||
|
|
@ -226,6 +226,6 @@ context("View", () => {
|
|||
|
||||
it("Route to Website Workspace", () => {
|
||||
cy.visit("/desk/website");
|
||||
cy.get(".workspace-title").should("contain", "Website");
|
||||
cy.get(".navbar-breadcrumbs:visible").get("li > a").should("contain", "Website");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ if TYPE_CHECKING: # pragma: no cover
|
|||
controllers: dict[str, type] = {}
|
||||
lazy_controllers: dict[str, type] = {}
|
||||
local = Local()
|
||||
cache: Optional["RedisWrapper"] = None
|
||||
client_cache: Optional["ClientCache"] = None
|
||||
cache: "RedisWrapper" | None = None
|
||||
client_cache: "ClientCache" | None = None
|
||||
STANDARD_USERS = ("Guest", "Administrator")
|
||||
|
||||
# this global may be subsequently changed by frappe.tests.utils.toggle_test_mode()
|
||||
|
|
@ -88,22 +88,20 @@ if _dev_server:
|
|||
|
||||
|
||||
# local-globals
|
||||
ConfType: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
type ConfType = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
# TODO: make session a dataclass instead of undtyped _dict
|
||||
SessionType: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
type SessionType = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
# TODO: implement dataclass
|
||||
LogMessageType: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
type LogMessageType = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
# TODO: implement dataclass
|
||||
# holds job metadata if the code is run in a background job context
|
||||
JobMetaType: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
ResponseDict: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
FlagsDict: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
FormDict: TypeAlias = _dict[str, str]
|
||||
type JobMetaType = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
type ResponseDict = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
type FlagsDict = _dict[str, Any] # type: ignore[no-any-explicit]
|
||||
type FormDict = _dict[str, str]
|
||||
|
||||
db: LocalProxy[Union["PyMariaDBDatabase", "MariaDBDatabase", "PostgresDatabase", "SQLiteDatabase"]] = local(
|
||||
"db"
|
||||
)
|
||||
qb: LocalProxy[Union["MariaDB", "Postgres", "SQLite"]] = local("qb")
|
||||
db: LocalProxy["PyMariaDBDatabase" | "MariaDBDatabase" | "PostgresDatabase" | "SQLiteDatabase"] = local("db")
|
||||
qb: LocalProxy["MariaDB" | "Postgres" | "SQLite"] = local("qb")
|
||||
conf: LocalProxy[ConfType] = local("conf")
|
||||
form_dict: LocalProxy[FormDict] = local("form_dict")
|
||||
form = form_dict
|
||||
|
|
@ -675,7 +673,7 @@ def is_table(doctype: str) -> bool:
|
|||
|
||||
|
||||
def get_precision(
|
||||
doctype: str, fieldname: str, currency: str | None = None, doc: Optional["Document"] = None
|
||||
doctype: str, fieldname: str, currency: str | None = None, doc: "Document" | None = None
|
||||
) -> int:
|
||||
"""Get precision for a given field"""
|
||||
from frappe.model.meta import get_field_precision
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ from frappe.core.doctype.installed_applications.installed_applications import (
|
|||
get_setup_wizard_completed_apps,
|
||||
get_setup_wizard_not_required_apps,
|
||||
)
|
||||
from frappe.utils.caching import request_cache
|
||||
|
||||
# check if route is /desk or /desk/* and not /app1 or /app1/*
|
||||
DESK_APP_PATTERN = re.compile(r"^/desk(/.*)?$")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@request_cache
|
||||
def get_apps():
|
||||
apps = frappe.get_installed_apps()
|
||||
app_list = []
|
||||
|
|
|
|||
|
|
@ -67,8 +67,11 @@ frappe.ui.form.on("Assignment Rule", {
|
|||
[{ label: "Owner", value: "owner" }]
|
||||
);
|
||||
if (doctype) {
|
||||
frm.set_fields_as_options("due_date_based_on", doctype, (df) =>
|
||||
["Date", "Datetime"].includes(df.fieldtype)
|
||||
frm.set_fields_as_options(
|
||||
"due_date_based_on",
|
||||
doctype,
|
||||
(df) => ["Date", "Datetime"].includes(df.fieldtype),
|
||||
[{ value: " ", label: " " }]
|
||||
).then((options) =>
|
||||
frm.set_df_property("due_date_based_on", "hidden", !options.length)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -537,6 +537,7 @@ def get_sentry_dsn():
|
|||
|
||||
|
||||
def get_sidebar_items():
|
||||
from frappe import _
|
||||
from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import auto_generate_sidebar_from_module
|
||||
|
||||
sidebars = frappe.get_all("Workspace Sidebar", fields=["name", "header_icon"])
|
||||
|
|
@ -560,7 +561,7 @@ def get_sidebar_items():
|
|||
}
|
||||
for si in w.items:
|
||||
workspace_sidebar = {
|
||||
"label": si.label,
|
||||
"label": _(si.label),
|
||||
"link_to": si.link_to,
|
||||
"link_type": si.link_type,
|
||||
"type": si.type,
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@
|
|||
"idx": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-02-20 19:19:29.427081",
|
||||
"modified": "2025-12-25 19:19:29.427081",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Communication",
|
||||
|
|
@ -437,4 +437,4 @@
|
|||
"title_field": "subject",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
html_signature = soup.find("div", {"class": "ql-editor read-mode"})
|
||||
_signature = None
|
||||
if html_signature:
|
||||
_signature = html_signature.renderContents()
|
||||
_signature = html_signature.encode_contents()
|
||||
|
||||
if (cstr(_signature) or signature) not in self.content:
|
||||
self.content = f'{self.content}</p><br><p class="signature">{signature}'
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ def restore(name, alert=True):
|
|||
frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored)
|
||||
|
||||
doc = frappe.get_doc(json.loads(deleted.data))
|
||||
|
||||
doc.flags.from_restore = True
|
||||
try:
|
||||
doc.insert()
|
||||
except frappe.DocstatusTransitionError:
|
||||
|
|
|
|||
|
|
@ -629,7 +629,7 @@
|
|||
"options": "\nDefault\nPrimary\nInfo\nSuccess\nWarning\nDanger"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == \"Tab Break\"",
|
||||
"depends_on": "eval: doc.fieldtype == \"Tab Break\"",
|
||||
"fieldname": "icon",
|
||||
"fieldtype": "Icon",
|
||||
"label": "Icon"
|
||||
|
|
|
|||
|
|
@ -1140,10 +1140,10 @@ def validate_empty_name(dt, autoname):
|
|||
frappe.toast(_("Warning: Naming is not set"), indicator="yellow")
|
||||
|
||||
|
||||
def validate_autoincrement_autoname(dt: Union[DocType, "CustomizeForm"]) -> bool:
|
||||
def validate_autoincrement_autoname(dt: DocType | "CustomizeForm") -> bool:
|
||||
"""Checks if can doctype can change to/from autoincrement autoname"""
|
||||
|
||||
def get_autoname_before_save(dt: Union[DocType, "CustomizeForm"]) -> str:
|
||||
def get_autoname_before_save(dt: DocType | "CustomizeForm") -> str:
|
||||
if dt.doctype == "Customize Form":
|
||||
property_value = frappe.db.get_value(
|
||||
"Property Setter", {"doc_type": dt.doc_type, "property": "autoname"}, "value"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ def get_extension(
|
|||
filename,
|
||||
extn: str | None = None,
|
||||
content: bytes | None = None,
|
||||
response: Optional["Response"] = None,
|
||||
response: "Response" | None = None,
|
||||
) -> str:
|
||||
mimetype = None
|
||||
|
||||
|
|
@ -426,7 +426,7 @@ def decode_file_content(content: bytes) -> bytes:
|
|||
return safe_b64decode(content)
|
||||
|
||||
|
||||
def find_file_by_url(path: str, name: str | None = None) -> Optional["File"]:
|
||||
def find_file_by_url(path: str, name: str | None = None) -> "File" | None:
|
||||
filters = {"file_url": str(path)}
|
||||
if name:
|
||||
filters["name"] = str(name)
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ def make_perm_log(doc, method=None):
|
|||
def insert_perm_log(
|
||||
doc: Document,
|
||||
doc_before_save: Document = None,
|
||||
for_doctype: Optional["str"] = None,
|
||||
for_document: Optional["str"] = None,
|
||||
fields: Optional["list | tuple"] = None,
|
||||
for_doctype: "str" | None = None,
|
||||
for_document: "str" | None = None,
|
||||
fields: "list | tuple" | None = None,
|
||||
):
|
||||
if frappe.flags.in_install or frappe.flags.in_migrate:
|
||||
# no need to log changes when migrating or installing app/site
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class Report(Document):
|
|||
|
||||
if self.is_standard == "No":
|
||||
# allow only script manager to edit scripts
|
||||
if self.report_type != "Report Builder":
|
||||
if self.report_type not in ("Report Builder", "Custom Report"):
|
||||
frappe.only_for("Script Manager", True)
|
||||
|
||||
if frappe.db.get_value("Report", self.name, "is_standard") == "Yes":
|
||||
|
|
|
|||
|
|
@ -178,10 +178,7 @@ class TestRQJob(IntegrationTestCase):
|
|||
LAST_MEASURED_USAGE += 2
|
||||
|
||||
# Observed higher usage on 3.14. Temporarily raising the limit
|
||||
from sys import version_info
|
||||
|
||||
if version_info >= (3, 14):
|
||||
LAST_MEASURED_USAGE += 5
|
||||
LAST_MEASURED_USAGE += 5
|
||||
|
||||
self.assertLessEqual(rss, LAST_MEASURED_USAGE * 1.05, msg)
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,6 @@ def serialize_worker(worker: Worker) -> frappe._dict:
|
|||
def compute_utilization(worker: Worker) -> float:
|
||||
with suppress(Exception):
|
||||
total_time = (
|
||||
datetime.datetime.now(datetime.timezone.utc)
|
||||
- worker.birth_date.replace(tzinfo=datetime.timezone.utc)
|
||||
datetime.datetime.now(datetime.UTC) - worker.birth_date.replace(tzinfo=datetime.UTC)
|
||||
).total_seconds()
|
||||
return worker.total_working_time / total_time * 100
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from frappe.tests.classes.context_managers import change_settings
|
|||
from frappe.tests.test_api import FrappeAPITestCase
|
||||
from frappe.tests.utils import toggle_test_mode
|
||||
from frappe.utils import get_url
|
||||
from frappe.www.login import sanitize_redirect
|
||||
|
||||
user_module = frappe.core.doctype.user.user
|
||||
|
||||
|
|
@ -333,7 +334,9 @@ class TestUser(IntegrationTestCase):
|
|||
sign_up(random_user, random_user_name, "/welcome"),
|
||||
(1, "Please check your email for verification"),
|
||||
)
|
||||
self.assertEqual(frappe.cache.hget("redirect_after_login", random_user), "/welcome")
|
||||
self.assertEqual(
|
||||
frappe.cache.hget("redirect_after_login", random_user), sanitize_redirect("/welcome")
|
||||
)
|
||||
|
||||
# re-register
|
||||
self.assertTupleEqual(sign_up(random_user, random_user_name, "/welcome"), (0, "Already Registered"))
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ from frappe.utils.password import check_password, get_password_reset_limit
|
|||
from frappe.utils.password import update_password as _update_password
|
||||
from frappe.utils.user import get_system_managers
|
||||
from frappe.website.utils import get_home_page, is_signup_disabled
|
||||
from frappe.www.login import sanitize_redirect
|
||||
|
||||
desk_properties = (
|
||||
"search_bar",
|
||||
|
|
@ -1115,7 +1116,7 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
|
|||
user.add_roles(default_role)
|
||||
|
||||
if redirect_to:
|
||||
frappe.cache.hset("redirect_after_login", user.name, redirect_to)
|
||||
frappe.cache.hset("redirect_after_login", user.name, sanitize_redirect(redirect_to))
|
||||
|
||||
if user.flags.email_sent:
|
||||
return 1, _("Please check your email for verification")
|
||||
|
|
|
|||
|
|
@ -265,6 +265,15 @@ frappe.ui.form.on("Customize Form", {
|
|||
),
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
fieldname: "apply_module_export_filter",
|
||||
label: __("Apply Module Export Filter"),
|
||||
description: __(
|
||||
"Export only customizations assigned to the selected module.<br><span class='text-muted'><strong>Note:</strong> You must set the <em>Module (for export)</em> field on Custom Field and Property Setter records before applying this filter.</span><p class='alert alert-warning'> <strong>Warning:</strong> Customizations from other modules will be excluded.</p>"
|
||||
),
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
function (data) {
|
||||
frappe.call({
|
||||
|
|
@ -274,6 +283,7 @@ frappe.ui.form.on("Customize Form", {
|
|||
module: data.module,
|
||||
sync_on_migrate: data.sync_on_migrate,
|
||||
with_permissions: data.with_permissions,
|
||||
apply_module_export_filter: data.apply_module_export_filter,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -807,6 +807,7 @@ docfield_properties = {
|
|||
"link_filters": "JSON",
|
||||
"placeholder": "Data",
|
||||
"button_color": "Select",
|
||||
"mask": "Check",
|
||||
}
|
||||
|
||||
doctype_link_properties = {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class TestCustomizeForm(IntegrationTestCase):
|
|||
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEqual(d.doc_type, "Event")
|
||||
self.assertEqual(len(d.get("fields")), 44)
|
||||
self.assertEqual(len(d.get("fields")), 49)
|
||||
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEqual(d.doc_type, "Event")
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"no_copy",
|
||||
"allow_in_quick_entry",
|
||||
"translatable",
|
||||
"mask",
|
||||
"link_filters",
|
||||
"column_break_7",
|
||||
"default",
|
||||
|
|
@ -494,6 +495,13 @@
|
|||
"fieldtype": "Select",
|
||||
"label": "Button Color",
|
||||
"options": "\nDefault\nPrimary\nInfo\nSuccess\nWarning\nDanger"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Select\", \"Read Only\", \"Phone\", \"Percent\", \"Password\", \"Link\", \"Int\", \"Float\", \"Dynamic Link\", \"Duration\", \"Datetime\", \"Currency\", \"Data\", \"Date\"].includes(doc.fieldtype)",
|
||||
"fieldname": "mask",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mask"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
|
@ -501,7 +509,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-12 01:13:53.053888",
|
||||
"modified": "2025-12-23 23:04:19.800253",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ class CustomizeFormField(Document):
|
|||
length: DF.Int
|
||||
link_filters: DF.JSON | None
|
||||
mandatory_depends_on: DF.Code | None
|
||||
mask: DF.Check
|
||||
no_copy: DF.Check
|
||||
non_negative: DF.Check
|
||||
options: DF.SmallText | None
|
||||
|
|
|
|||
|
|
@ -315,6 +315,12 @@ class Database:
|
|||
if auto_commit:
|
||||
self.commit()
|
||||
|
||||
if self.db_type == "postgres" and getattr(self._cursor, "name", None):
|
||||
"""named cursors in Postgres are lazy and don't retrieve column names immediately,
|
||||
so explicitly performed here to avoid early exit during `unbuffered_cursor` usage
|
||||
"""
|
||||
self._cursor.fetchmany(0)
|
||||
|
||||
if not self._cursor.description:
|
||||
return ()
|
||||
|
||||
|
|
|
|||
|
|
@ -570,5 +570,9 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
|
|||
|
||||
table = get_table_name(doctype)
|
||||
|
||||
count = self.sql("select table_rows from information_schema.tables where table_name = %s", table)
|
||||
# Scope to current database to avoid cross-site estimates
|
||||
count = self.sql(
|
||||
"select table_rows from information_schema.tables where table_name = %s and table_schema = %s",
|
||||
(table, frappe.db.cur_db_name),
|
||||
)
|
||||
return cint(count[0][0]) if count else 0
|
||||
|
|
|
|||
|
|
@ -596,5 +596,9 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
|
|||
|
||||
table = get_table_name(doctype)
|
||||
|
||||
count = self.sql("select table_rows from information_schema.tables where table_name = %s", table)
|
||||
# Scope to current database to avoid cross-site estimates
|
||||
count = self.sql(
|
||||
"select table_rows from information_schema.tables where table_name = %s and table_schema = %s",
|
||||
(table, frappe.db.cur_db_name),
|
||||
)
|
||||
return cint(count[0][0]) if count else 0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import re
|
||||
from contextlib import contextmanager
|
||||
|
||||
import psycopg2
|
||||
import psycopg2.extensions
|
||||
|
|
@ -488,9 +489,31 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
|
|||
from frappe.utils.data import cint
|
||||
|
||||
table = get_table_name(doctype)
|
||||
count = self.sql("select reltuples from pg_class where relname = %s", table)
|
||||
|
||||
# Scope to current database to avoid cross-site estimates
|
||||
count = self.sql(
|
||||
"select c.reltuples from pg_class c join pg_namespace n on n.oid = c.relnamespace where c.relname = %s and n.nspname = %s and c.relkind = 'r'",
|
||||
(table, self.db_schema),
|
||||
)
|
||||
return cint(count[0][0]) if count else 0
|
||||
|
||||
@contextmanager
|
||||
def unbuffered_cursor(self):
|
||||
"""Unbuffered cursor in Postgres can only call .execute() once,
|
||||
usage:
|
||||
with frappe.db.unbuffered_cursor():
|
||||
frappe.db.sql()
|
||||
"""
|
||||
try:
|
||||
if not self._conn:
|
||||
self.connect()
|
||||
original_cursor = self._cursor
|
||||
new_cursor = self._cursor = self._conn.cursor(name="ss_cursor")
|
||||
yield
|
||||
finally:
|
||||
self._cursor = original_cursor
|
||||
new_cursor.close()
|
||||
|
||||
|
||||
def modify_query(query):
|
||||
""" "Modifies query according to the requirements of postgres"""
|
||||
|
|
|
|||
|
|
@ -1440,15 +1440,22 @@ class Engine:
|
|||
|
||||
For child tables (when parent_doctype is specified):
|
||||
- permissions are checked against the parent doctype
|
||||
- a join to the parent table is added
|
||||
- conditions reference the parent table's fields
|
||||
- for non-single parent doctypes: a join to the parent table is added,
|
||||
conditions reference parent fields
|
||||
- for single parent doctypes: all permissions are already checked by has_permission,
|
||||
we exit early without adding any conditions
|
||||
"""
|
||||
|
||||
if not self.apply_permissions:
|
||||
return
|
||||
|
||||
# For child tables, join to parent table so permission conditions can reference it
|
||||
if self.permission_doctype != self.doctype:
|
||||
parent_meta = frappe.get_meta(self.permission_doctype)
|
||||
if parent_meta.issingle:
|
||||
# Child table of single doctype
|
||||
# permissions are already checked by has_permission
|
||||
return
|
||||
|
||||
self.query = self.query.inner_join(self.permission_table).on(
|
||||
self.table.parent == self.permission_table.name
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|||
queue_prefix = "insert_queue_for_"
|
||||
|
||||
|
||||
def deferred_insert(doctype: str, records: list[Union[dict, "Document"]] | str):
|
||||
def deferred_insert(doctype: str, records: list[dict | "Document"] | str):
|
||||
if isinstance(records, dict | list):
|
||||
_records = json.dumps(records)
|
||||
else:
|
||||
|
|
@ -48,7 +48,7 @@ def save_to_db():
|
|||
frappe.db.commit()
|
||||
|
||||
|
||||
def insert_record(record: Union[dict, "Document"], doctype: str):
|
||||
def insert_record(record: dict | "Document", doctype: str):
|
||||
try:
|
||||
record.update({"doctype": doctype})
|
||||
frappe.get_doc(record).insert()
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ def _bulk_action(doctype, docnames, action, data, task_id=None):
|
|||
if data:
|
||||
data = frappe.parse_json(data)
|
||||
|
||||
child_table_updates = data.get("child_table_updates") if data else None
|
||||
failed = []
|
||||
num_documents = len(docnames)
|
||||
|
||||
|
|
@ -90,7 +91,28 @@ def _bulk_action(doctype, docnames, action, data, task_id=None):
|
|||
doc.cancel()
|
||||
message = _("Cancelling {0}").format(doctype)
|
||||
elif action == "update" and not doc.docstatus.is_cancelled():
|
||||
doc.update(data)
|
||||
# Handle child table updates
|
||||
if child_table_updates:
|
||||
table_fields = doc.meta.get_table_fields()
|
||||
for child_doctype, field_updates in child_table_updates.items():
|
||||
# Find the table field that contains this child doctype
|
||||
table_fieldname = next(
|
||||
(field.fieldname for field in table_fields if field.options == child_doctype),
|
||||
None,
|
||||
)
|
||||
|
||||
if table_fieldname and hasattr(doc, table_fieldname):
|
||||
child_meta = frappe.get_meta(child_doctype)
|
||||
child_docs = getattr(doc, table_fieldname)
|
||||
for child_doc in child_docs:
|
||||
for fieldname, value in field_updates.items():
|
||||
if child_meta.has_field(fieldname):
|
||||
setattr(child_doc, fieldname, value)
|
||||
|
||||
# Handle regular field updates
|
||||
if data:
|
||||
doc.update(data)
|
||||
|
||||
doc.save()
|
||||
message = _("Updating {0}").format(doctype)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class TestBulkUpdate(IntegrationTestCase):
|
|||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls.doctype = new_doctype(is_submittable=1, custom=1).insert().name
|
||||
cls.child_doctype = new_doctype(istable=1, custom=1).insert().name
|
||||
frappe.db.commit()
|
||||
for _ in range(50):
|
||||
frappe.new_doc(cls.doctype, some_fieldname=frappe.mock("name")).insert()
|
||||
|
|
@ -46,3 +47,59 @@ class TestBulkUpdate(IntegrationTestCase):
|
|||
submitted = frappe.get_all(self.doctype, {"docstatus": 1}, limit=20, pluck="name")
|
||||
submit_cancel_or_update_docs(self.doctype, submitted, action="cancel")
|
||||
self.wait_for_assertion(lambda: check_docstatus(submitted, 2))
|
||||
|
||||
def test_bulk_update_parent_fields(self):
|
||||
docnames = frappe.get_all(self.doctype, {"docstatus": 0}, limit=5, pluck="name")
|
||||
failed = submit_cancel_or_update_docs(
|
||||
self.doctype, docnames, action="update", data={"some_fieldname": "_Test Sync"}
|
||||
)
|
||||
self.assertEqual(failed, [])
|
||||
|
||||
def check_field_values(docs, expected):
|
||||
frappe.db.rollback()
|
||||
values = frappe.get_all(self.doctype, {"name": ["in", docs]}, ["name", "some_fieldname"])
|
||||
return all(v.some_fieldname == expected for v in values)
|
||||
|
||||
docnames_bg = frappe.get_all(self.doctype, {"docstatus": 0}, limit=20, pluck="name")
|
||||
submit_cancel_or_update_docs(
|
||||
self.doctype, docnames_bg, action="update", data={"some_fieldname": "_Test Background"}
|
||||
)
|
||||
|
||||
self.wait_for_assertion(lambda: check_field_values(docnames_bg, "_Test Background"))
|
||||
|
||||
def test_bulk_update_child_fields(self):
|
||||
doctype_doc = frappe.get_doc("DocType", self.doctype)
|
||||
doctype_doc.append(
|
||||
"fields", {"fieldname": "child_table", "fieldtype": "Table", "options": self.child_doctype}
|
||||
)
|
||||
doctype_doc.save()
|
||||
frappe.db.commit()
|
||||
|
||||
existing_docs = frappe.get_all(self.doctype, {"docstatus": 0}, pluck="name")
|
||||
for docname in existing_docs:
|
||||
doc = frappe.get_doc(self.doctype, docname)
|
||||
doc.append("child_table", {"some_fieldname": "_Test Child Value"})
|
||||
doc.save()
|
||||
frappe.db.commit()
|
||||
|
||||
update_data = {
|
||||
"child_table_updates": {
|
||||
self.child_doctype: {"some_fieldname": "_Test Child Updated"},
|
||||
}
|
||||
}
|
||||
|
||||
def check_child_field(docs, expected):
|
||||
frappe.db.rollback()
|
||||
for docname in docs:
|
||||
doc = frappe.get_doc(self.doctype, docname)
|
||||
if not doc.child_table or doc.child_table[0].some_fieldname != expected:
|
||||
return False
|
||||
return True
|
||||
|
||||
docnames = frappe.get_all(self.doctype, {"docstatus": 0}, limit=5, pluck="name")
|
||||
failed = submit_cancel_or_update_docs(self.doctype, docnames, action="update", data=update_data)
|
||||
self.assertEqual(failed, [])
|
||||
|
||||
docnames_bg = frappe.get_all(self.doctype, {"docstatus": 0}, limit=20, pluck="name")
|
||||
submit_cancel_or_update_docs(self.doctype, docnames_bg, action="update", data=update_data)
|
||||
self.wait_for_assertion(lambda: check_child_field(docnames_bg, "_Test Child Updated"))
|
||||
|
|
|
|||
|
|
@ -13,13 +13,14 @@
|
|||
"event_category",
|
||||
"event_type",
|
||||
"color",
|
||||
"send_reminder",
|
||||
"repeat_this_event",
|
||||
"location",
|
||||
"column_break_4",
|
||||
"starts_on",
|
||||
"ends_on",
|
||||
"status",
|
||||
"sender",
|
||||
"attending",
|
||||
"all_day",
|
||||
"sync_with_google_calendar",
|
||||
"add_video_conferencing",
|
||||
|
|
@ -43,14 +44,18 @@
|
|||
"sunday",
|
||||
"section_break_8",
|
||||
"description",
|
||||
"participants",
|
||||
"participants_tab",
|
||||
"event_participants",
|
||||
"links_tab",
|
||||
"section_break_jlqd",
|
||||
"reference_doctype",
|
||||
"column_break_pjkw",
|
||||
"reference_docname",
|
||||
"section_break_qemp",
|
||||
"links"
|
||||
"links",
|
||||
"notifications_tab",
|
||||
"send_reminder",
|
||||
"notifications"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -210,12 +215,6 @@
|
|||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"fieldname": "participants",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Participants",
|
||||
"oldfieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "event_participants",
|
||||
"fieldtype": "Table",
|
||||
|
|
@ -327,14 +326,46 @@
|
|||
{
|
||||
"fieldname": "section_break_qemp",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "notifications_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Notifications"
|
||||
},
|
||||
{
|
||||
"fieldname": "notifications",
|
||||
"fieldtype": "Table",
|
||||
"label": "Notifications",
|
||||
"options": "Event Notifications"
|
||||
},
|
||||
{
|
||||
"fieldname": "location",
|
||||
"fieldtype": "Data",
|
||||
"label": "Location"
|
||||
},
|
||||
{
|
||||
"fieldname": "participants_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Participants"
|
||||
},
|
||||
{
|
||||
"fieldname": "links_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Links"
|
||||
},
|
||||
{
|
||||
"fieldname": "attending",
|
||||
"fieldtype": "Select",
|
||||
"label": "Attending",
|
||||
"options": "\nYes\nNo\nMaybe"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-12 13:37:36.319985",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-10-07 08:53:24.732646",
|
||||
"modified_by": "shariq@frappe.io",
|
||||
"module": "Desk",
|
||||
"name": "Event",
|
||||
"naming_rule": "Expression (old style)",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from frappe.desk.doctype.notification_settings.notification_settings import (
|
|||
)
|
||||
from frappe.desk.reportview import get_filters_cond
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.utils.user_settings import get_user_settings, sync_user_settings, update_user_settings
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
|
|
@ -53,11 +54,13 @@ class Event(Document):
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.core.doctype.dynamic_link.dynamic_link import DynamicLink
|
||||
from frappe.desk.doctype.event_notifications.event_notifications import EventNotifications
|
||||
from frappe.desk.doctype.event_participants.event_participants import EventParticipants
|
||||
from frappe.types import DF
|
||||
|
||||
add_video_conferencing: DF.Check
|
||||
all_day: DF.Check
|
||||
attending: DF.Literal["", "Yes", "No", "Maybe"]
|
||||
color: DF.Color | None
|
||||
description: DF.TextEditor | None
|
||||
ends_on: DF.Datetime | None
|
||||
|
|
@ -68,9 +71,11 @@ class Event(Document):
|
|||
google_calendar: DF.Link | None
|
||||
google_calendar_event_id: DF.Data | None
|
||||
google_calendar_id: DF.Data | None
|
||||
google_meet_link: DF.Data | None
|
||||
google_meet_link: DF.SmallText | None
|
||||
links: DF.Table[DynamicLink]
|
||||
location: DF.Data | None
|
||||
monday: DF.Check
|
||||
notifications: DF.Table[EventNotifications]
|
||||
pulled_from_google_calendar: DF.Check
|
||||
reference_docname: DF.DynamicLink | None
|
||||
reference_doctype: DF.Link | None
|
||||
|
|
@ -124,6 +129,8 @@ class Event(Document):
|
|||
for communication in communications:
|
||||
frappe.delete_doc("Communication", communication, force=True)
|
||||
|
||||
self.remove_event_from_user_settings()
|
||||
|
||||
def sync_communication(self):
|
||||
if not self.event_participants:
|
||||
return
|
||||
|
|
@ -209,6 +216,40 @@ class Event(Document):
|
|||
frappe.get_value("Contact", participant_contact, "email_id") if participant_contact else None
|
||||
)
|
||||
|
||||
def remove_event_from_user_settings(self):
|
||||
user_settings = get_user_settings("Event", for_update=True)
|
||||
if user_settings:
|
||||
user_settings = json.loads(user_settings)
|
||||
|
||||
if "notifications" in user_settings:
|
||||
notifications = user_settings.get("notifications")
|
||||
completedEvents = notifications.get("completedEvents", [])
|
||||
if self.name in completedEvents:
|
||||
completedEvents.remove(self.name)
|
||||
notifications["completedEvents"] = completedEvents
|
||||
updated_notifications = notifications
|
||||
user_settings["notifications"] = updated_notifications
|
||||
|
||||
update_user_settings("Event", json.dumps(user_settings), for_update=True)
|
||||
sync_user_settings()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_attending_status(event_name, attendee, status):
|
||||
event_doc = frappe.get_doc("Event", event_name)
|
||||
|
||||
if event_doc.owner == attendee == frappe.session.user:
|
||||
frappe.db.set_value("Event", event_name, "attending", status)
|
||||
return
|
||||
|
||||
for participant in event_doc.event_participants:
|
||||
if participant.email == attendee:
|
||||
frappe.db.set_value("Event Participants", participant.name, "attending", status)
|
||||
return
|
||||
|
||||
if not has_permission(event_doc, user=attendee):
|
||||
frappe.throw(_("You are not allowed to update the status of this event."))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_communication(event, reference_doctype, reference_docname):
|
||||
|
|
@ -238,13 +279,22 @@ def get_permission_query_conditions(user):
|
|||
query = f"""(`tabEvent`.`event_type`='Public' or `tabEvent`.`owner`={frappe.db.escape(user)})"""
|
||||
if shared_events := frappe.share.get_shared("Event", user=user):
|
||||
query += f" or `tabEvent`.`name` in ({', '.join([frappe.db.escape(e) for e in shared_events])})"
|
||||
|
||||
query += f" or exists (select 'x' from `tabEvent Participants` ep where ep.parent=`tabEvent`.name and ep.email={frappe.db.escape(user)})"
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def has_permission(doc, user):
|
||||
def has_permission(doc, ptype=None, user=None):
|
||||
if doc.event_type == "Public" or doc.owner == user:
|
||||
return True
|
||||
|
||||
for participant in doc.event_participants:
|
||||
if participant.email == user:
|
||||
if ptype in ["write", "create", "delete"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -285,7 +335,7 @@ def get_events(
|
|||
start: date, end: date, user: str | None = None, for_reminder: bool = False, filters=None
|
||||
) -> list[frappe._dict]:
|
||||
user = user or frappe.session.user
|
||||
EventLikeDict: TypeAlias = Event | frappe._dict
|
||||
type EventLikeDict = Event | frappe._dict
|
||||
resolved_events: list[EventLikeDict] = []
|
||||
|
||||
if isinstance(filters, str):
|
||||
|
|
|
|||
0
frappe/desk/doctype/event_notifications/__init__.py
Normal file
0
frappe/desk/doctype/event_notifications/__init__.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-10-01 14:19:43.271468",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"type",
|
||||
"before",
|
||||
"interval",
|
||||
"time"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "Notification",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "Notification\nEmail"
|
||||
},
|
||||
{
|
||||
"fieldname": "before",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Before"
|
||||
},
|
||||
{
|
||||
"fieldname": "interval",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Interval"
|
||||
},
|
||||
{
|
||||
"fieldname": "time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "Time"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-01 14:26:21.526868",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Event Notifications",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright (c) 2025, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class EventNotifications(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
before: DF.Int
|
||||
interval: DF.Literal[None]
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
time: DF.Time | None
|
||||
type: DF.Literal["Notification", "Email"]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
@ -7,7 +7,8 @@
|
|||
"field_order": [
|
||||
"reference_doctype",
|
||||
"reference_docname",
|
||||
"email"
|
||||
"email",
|
||||
"attending"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -31,19 +32,27 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "Email",
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"fieldname": "attending",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Attending",
|
||||
"options": "\nYes\nNo\nMaybe"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:03:25.717745",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-10-06 15:03:36.186683",
|
||||
"modified_by": "shariq@frappe.io",
|
||||
"module": "Desk",
|
||||
"name": "Event Participants",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class EventParticipants(Document):
|
|||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
attending: DF.Literal["", "Yes", "No", "Maybe"]
|
||||
email: DF.Data | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ frappe.ui.form.on("System Console", {
|
|||
frm.sql_output.destroy();
|
||||
frm.get_field("sql_output").html("");
|
||||
}
|
||||
frm.trigger("load_completions");
|
||||
}
|
||||
|
||||
frm.trigger("load_completions");
|
||||
const field = frm.get_field("console");
|
||||
field.df.options = frm.doc.type;
|
||||
field.set_language();
|
||||
|
|
@ -130,6 +130,7 @@ frappe.ui.form.on("System Console", {
|
|||
load_completions(frm) {
|
||||
if (frm.doc.type != "Python") {
|
||||
frm.set_df_property("console", "autocompletions", []);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
frappe
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ if typing.TYPE_CHECKING:
|
|||
def getdoc(doctype, name):
|
||||
"""
|
||||
Loads a doclist for a given document. This method is called directly from the client.
|
||||
Requries "doctype", "name" as form variables.
|
||||
Requires "doctype", "name" as form variables.
|
||||
Will also call the "onload" method on the document.
|
||||
"""
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ def get_milestones(doctype, name):
|
|||
def get_attachments(dt, dn):
|
||||
return frappe.get_all(
|
||||
"File",
|
||||
fields=["name", "file_name", "file_url", "is_private", "file_type", "file_size"],
|
||||
fields=["name", "file_name", "file_url", "is_private"],
|
||||
filters={"attached_to_name": str(dn), "attached_to_doctype": dt},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from frappe.utils.data import convert_utc_to_system_timezone
|
|||
|
||||
def get_time(path: Path):
|
||||
return convert_utc_to_system_timezone(
|
||||
datetime.datetime.fromtimestamp(path.stat().st_mtime, tz=datetime.timezone.utc)
|
||||
datetime.datetime.fromtimestamp(path.stat().st_mtime, tz=datetime.UTC)
|
||||
).strftime("%a %b %d %H:%M %Y")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
.navbar-container{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 10px 20px 10px 20px;
|
||||
|
|
@ -41,11 +42,15 @@
|
|||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.desktop-navbar-modal-search{
|
||||
.desktop-navbar-modal-search {
|
||||
background-color: var(--control-bg);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 6px 10px;
|
||||
height: 32px;
|
||||
padding: 0px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-right: 0px;
|
||||
}
|
||||
#brand-logo{
|
||||
width: auto;
|
||||
|
|
@ -60,6 +65,11 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
.icons-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-stroke{
|
||||
|
|
@ -91,6 +101,10 @@
|
|||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 13px 16px 12px 16px;
|
||||
position: relative;
|
||||
}
|
||||
.desktop-icon.edit-mode .hide-button {
|
||||
display: flex;
|
||||
}
|
||||
.icon-container:has(.app-logo) {
|
||||
padding: 0;
|
||||
|
|
@ -99,10 +113,16 @@
|
|||
.icon-container img{
|
||||
width: var(--desktop-icon-dimension);
|
||||
height: var(--desktop-icon-dimension);
|
||||
user-drag: none;
|
||||
-webkit-user-drag: none;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
.icon-container{
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -112,10 +132,6 @@
|
|||
.icon-container:has(.icon){
|
||||
background-color: var(--surface-gray-3);
|
||||
}
|
||||
.icon-container .icon{
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
}
|
||||
.icon-container:hover{
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.1s;
|
||||
|
|
@ -162,6 +178,7 @@
|
|||
.desktop-modal{
|
||||
backdrop-filter: var(--desktop-blur);
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
& .modal-dialog{
|
||||
& .modal-content {
|
||||
top: 120px;
|
||||
|
|
@ -234,6 +251,7 @@
|
|||
}
|
||||
|
||||
.folder-icon{
|
||||
border-radius: 10px;
|
||||
background-color: var(--folder-icon-background-color) !important;
|
||||
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.14);
|
||||
padding: 7px;
|
||||
|
|
@ -286,4 +304,132 @@
|
|||
}
|
||||
.right-page-arrow, .left-page-arrow{
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.bounce {
|
||||
animation: bounceIn 2s infinite 2s;
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
20% {
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.hide-button{
|
||||
display: none;
|
||||
background-color: var(--surface-gray-2);
|
||||
position: absolute;
|
||||
border-radius: 67px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
align-items: center;
|
||||
top: -5px;
|
||||
left: 108px;
|
||||
}
|
||||
|
||||
.edit-mode{
|
||||
border: 1px dashed var(--outline-gray-2);
|
||||
border-radius: 20px;
|
||||
}
|
||||
.edit-mode-buttons{
|
||||
display: none;
|
||||
gap: 4px;
|
||||
}
|
||||
.desktop-wrapper[data-mode="Edit"]{
|
||||
.edit-mode-buttons{
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
bottom: 5%;
|
||||
right: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
/* @media screen and (max-width: 320px) {
|
||||
.icons{
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
}
|
||||
} */
|
||||
@media screen and (max-width: 570px) {
|
||||
:root {
|
||||
--desktop-icon-dimension: 50px;
|
||||
--desktop-icon-container: 117px;
|
||||
--folder-thumbnail-icon-height:17px;
|
||||
}
|
||||
|
||||
.desktop-container {
|
||||
padding: 20px 13px;
|
||||
|
||||
> .icons-container {
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
|
||||
> .icons {
|
||||
gap: 8px;
|
||||
row-gap: 12px;
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
--desktop-icon-container: 100px;
|
||||
.folder-icon > .icons{
|
||||
grid-template-columns: repeat(2, 1fr) I !important;
|
||||
}
|
||||
}
|
||||
|
||||
> .desktop-icon {
|
||||
width: var(--desktop-icon-container);
|
||||
height: var(--desktop-icon-container);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-modal-body {
|
||||
width: 90vw;
|
||||
> .icons-container {
|
||||
width: 100%;
|
||||
overflow: hidden !important;
|
||||
margin: 0px;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
|
||||
> .icons {
|
||||
position: relative;
|
||||
right: 6%;
|
||||
column-gap: 4px;
|
||||
row-gap: 8px;
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
--desktop-icon-container: 100px;
|
||||
.folder-icon > .icons{
|
||||
grid-template-columns: repeat(2, 1fr) I !important;
|
||||
}
|
||||
}
|
||||
|
||||
> .desktop-icon {
|
||||
width: var(--desktop-icon-container);
|
||||
height: var(--desktop-icon-container);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.folder-icon > .icons-container {
|
||||
overflow: hidden;
|
||||
> .icons {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
grid-template-rows: repeat(2, 1fr) !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,22 @@
|
|||
{{ _("Search") }}
|
||||
</span>
|
||||
<span>
|
||||
{{ "⌘ K" if is_mac else "Ctrl K" }}
|
||||
{{ "⌘K" if is_mac else "Ctrl+K" }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="desktop-avatar" style="margin-left: -10px;">
|
||||
<div class="desktop-avatar">
|
||||
|
||||
</div>
|
||||
</header>
|
||||
<div class="desktop-container">
|
||||
</div>
|
||||
<div class="edit-mode-buttons">
|
||||
<button class="discard btn btn-default ellipsis">
|
||||
{{ _("Discard") }}
|
||||
</button>
|
||||
<button class="save btn btn-primary ellipsis">
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>`
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
frappe.desktop_utils = {};
|
||||
frappe.desktop_grids = [];
|
||||
frappe.desktop_icons_objects = [];
|
||||
$.extend(frappe.desktop_utils, {
|
||||
modal: null,
|
||||
modal_stack: [],
|
||||
|
|
@ -59,10 +61,14 @@ function get_route(desktop_icon) {
|
|||
}
|
||||
|
||||
function get_desktop_icon_by_label(title, filters) {
|
||||
let icons = frappe.desktop_icons;
|
||||
if (frappe.pages["desktop"].desktop_page.edit_mode) {
|
||||
icons = frappe.new_desktop_icons;
|
||||
}
|
||||
if (!filters) {
|
||||
return frappe.boot.desktop_icons.find((f) => f.label === title && f.hidden != 1);
|
||||
return icons.find((f) => f.label === title && f.hidden != 1);
|
||||
} else {
|
||||
return frappe.boot.desktop_icons.find((f) => {
|
||||
return icons.find((f) => {
|
||||
return (
|
||||
f.label === title &&
|
||||
Object.keys(filters).every((key) => f[key] === filters[key]) &&
|
||||
|
|
@ -76,12 +82,9 @@ function get_desktop_icon_by_idx(idx, parent_icon) {
|
|||
return frappe.boot.desktop_icons.find((f) => f.idx == idx && f.parent_icon == parent_icon);
|
||||
}
|
||||
|
||||
function save_desktop() {
|
||||
function save_desktop(icons) {
|
||||
// saving in localStorage;
|
||||
localStorage.setItem(
|
||||
`${frappe.session.user}:desktop`,
|
||||
JSON.stringify(frappe.boot.desktop_icons)
|
||||
);
|
||||
localStorage.setItem(`${frappe.session.user}:desktop`, JSON.stringify(icons));
|
||||
frappe.toast("Desktop Saved");
|
||||
frappe.pages["desktop"].desktop_page.update();
|
||||
}
|
||||
|
|
@ -103,8 +106,10 @@ function toggle_icons(icons) {
|
|||
class DesktopPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.edit_mode = false;
|
||||
this.prepare();
|
||||
this.make(page);
|
||||
this.setup_events();
|
||||
}
|
||||
update() {
|
||||
this.prepare();
|
||||
|
|
@ -116,10 +121,11 @@ class DesktopPage {
|
|||
this.apps_icons = [];
|
||||
|
||||
const icon_map = {};
|
||||
const all_icons = (
|
||||
frappe.desktop_icons =
|
||||
JSON.parse(localStorage.getItem(`${frappe.session.user}:desktop`)) ||
|
||||
frappe.boot.desktop_icons
|
||||
).filter((icon) => {
|
||||
frappe.boot.desktop_icons;
|
||||
let icons = this.edit_mode ? frappe.new_desktop_icons : frappe.desktop_icons;
|
||||
const all_icons = icons.filter((icon) => {
|
||||
if (icon.hidden != 1) {
|
||||
icon.child_icons = [];
|
||||
icon_map[icon.label] = icon;
|
||||
|
|
@ -127,7 +133,6 @@ class DesktopPage {
|
|||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
all_icons.forEach((icon) => {
|
||||
if (icon.parent_icon && icon_map[icon.parent_icon]) {
|
||||
icon_map[icon.parent_icon].child_icons.push(icon);
|
||||
|
|
@ -138,7 +143,16 @@ class DesktopPage {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
setup_events() {
|
||||
this.wrapper.find(".hide-button").on("click", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
let desktop_label = event.currentTarget.parentElement.dataset.id;
|
||||
let desktop_icon = get_desktop_icon_by_label(desktop_label);
|
||||
desktop_icon.hidden = 1;
|
||||
frappe.pages["desktop"].desktop_page.update();
|
||||
});
|
||||
}
|
||||
make() {
|
||||
this.page.page_head.hide();
|
||||
$(this.page.body).empty();
|
||||
|
|
@ -152,22 +166,103 @@ class DesktopPage {
|
|||
col: 3,
|
||||
},
|
||||
});
|
||||
this.setup_editing_mode();
|
||||
if (this.edit_mode) {
|
||||
this.start_editing_layout();
|
||||
}
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.setup_avatar();
|
||||
this.setup_navbar();
|
||||
this.setup_awesomebar();
|
||||
this.setup_editing_mode();
|
||||
this.handle_route_change();
|
||||
this.setup_events();
|
||||
}
|
||||
setup_editing_mode() {
|
||||
const me = this;
|
||||
let menu_items = [
|
||||
{
|
||||
label: "Edit Layout",
|
||||
icon: "edit",
|
||||
onClick: function () {
|
||||
frappe.new_desktop_icons = JSON.parse(JSON.stringify(frappe.desktop_icons));
|
||||
me.start_editing_layout();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Reset Layout",
|
||||
icon: "rotate-ccw",
|
||||
onClick: function () {
|
||||
reset_to_default();
|
||||
me.update();
|
||||
},
|
||||
},
|
||||
];
|
||||
frappe.ui.create_menu({
|
||||
parent: this.wrapper,
|
||||
menu_items: menu_items,
|
||||
right_click: true,
|
||||
});
|
||||
}
|
||||
stop_editing_layout(action) {
|
||||
this.edit_mode = false;
|
||||
|
||||
$(".desktop-icon").removeClass("edit-mode");
|
||||
$(".desktop-wrapper").removeAttr("data-mode");
|
||||
if (action === "cancel") {
|
||||
frappe.new_desktop_icons = null;
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
|
||||
// submit
|
||||
save_desktop(frappe.new_desktop_icons);
|
||||
}
|
||||
|
||||
start_editing_layout() {
|
||||
this.edit_mode = true;
|
||||
$(".desktop-icon").addClass("edit-mode");
|
||||
$(".desktop-wrapper").attr("data-mode", "Edit");
|
||||
frappe.desktop_grids.forEach((desktop_grid) => {
|
||||
if (!desktop_grid.no_dragging) {
|
||||
desktop_grid.grids.forEach((grid) => {
|
||||
desktop_grid.setup_reordering(grid);
|
||||
});
|
||||
}
|
||||
});
|
||||
frappe.desktop_icons_objects.forEach((icon_object) => {
|
||||
icon_object.setup_dragging();
|
||||
});
|
||||
if (this.edit_mode) this.setup_edit_buttons();
|
||||
}
|
||||
setup_edit_buttons() {
|
||||
const me = this;
|
||||
this.$edit_button = $(".edit-mode-buttons");
|
||||
this.$edit_button.find(".discard").on("click", function () {
|
||||
me.stop_editing_layout("cancel");
|
||||
});
|
||||
this.$edit_button.find(".save").on("click", function () {
|
||||
me.stop_editing_layout("submit");
|
||||
});
|
||||
}
|
||||
setup_avatar() {
|
||||
$(".desktop-avatar").html(frappe.avatar(frappe.session.user, "avatar-medium"));
|
||||
let is_dark = document.documentElement.getAttribute("data-theme") === "dark";
|
||||
let menu_items = [
|
||||
{
|
||||
icon: "edit",
|
||||
label: "Edit Profile",
|
||||
url: `/update-profile/${frappe.session.user}`,
|
||||
},
|
||||
{
|
||||
icon: is_dark ? "sun" : "moon",
|
||||
label: "Toggle Theme",
|
||||
onClick: function () {
|
||||
new frappe.ui.ThemeSwitcher().show();
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "lock",
|
||||
label: "Reset Password",
|
||||
|
|
@ -281,6 +376,7 @@ class DesktopIconGrid {
|
|||
this.grids = [];
|
||||
this.prepare();
|
||||
this.make();
|
||||
frappe.desktop_grids.push(this);
|
||||
}
|
||||
|
||||
prepare() {
|
||||
|
|
@ -308,9 +404,9 @@ class DesktopIconGrid {
|
|||
}
|
||||
this.grids.push($(template).appendTo(this.icons_container));
|
||||
this.make_icons(this.icons_data_by_page, this.grids[i]);
|
||||
if (!this.no_dragging) {
|
||||
this.setup_reordering(this.grids[i]);
|
||||
}
|
||||
// if (!this.no_dragging) {
|
||||
// this.setup_reordering(this.grids[i]);
|
||||
// }
|
||||
}
|
||||
if (!this.in_folder && this.total_pages > 1) {
|
||||
this.add_page_indicators();
|
||||
|
|
@ -450,6 +546,7 @@ class DesktopIconGrid {
|
|||
if (!frappe.is_mobile()) {
|
||||
this.sortable = new Sortable($(grid).get(0), {
|
||||
swapThreshold: 0.09,
|
||||
desktop: true,
|
||||
animation: 150,
|
||||
sort: true, // keep sorting normally
|
||||
dragoverBubble: true,
|
||||
|
|
@ -458,6 +555,9 @@ class DesktopIconGrid {
|
|||
put: true,
|
||||
pull: true,
|
||||
},
|
||||
onStart(evt) {
|
||||
frappe.desktop_utils.dragged_item = evt.item;
|
||||
},
|
||||
setData: function (/** DataTransfer */ dataTransfer, /** HTMLElement*/ dragEl) {
|
||||
let title = $(dragEl).find(".icon-title").text();
|
||||
let icon = me.icons.find((d) => {
|
||||
|
|
@ -465,7 +565,11 @@ class DesktopIconGrid {
|
|||
});
|
||||
dataTransfer.setData("text/plain", JSON.stringify(icon.icon_data)); // `dataTransfer` object of HTML5 DragEvent
|
||||
},
|
||||
onMove() {
|
||||
return frappe.desktop_utils.allow_move || false;
|
||||
},
|
||||
onEnd: function (evt) {
|
||||
if (frappe.desktop_utils.in_folder_creation) return;
|
||||
if (evt.oldIndex !== evt.newIndex) {
|
||||
if (evt.to.parentElement == evt.from.parentElement) {
|
||||
let reordered_icons = me.sortable.toArray();
|
||||
|
|
@ -490,10 +594,8 @@ class DesktopIconGrid {
|
|||
selected_icon.parent_icon = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
frappe.toast("Nothing changed");
|
||||
}
|
||||
save_desktop();
|
||||
// save_desktop();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -505,7 +607,7 @@ class DesktopIconGrid {
|
|||
icon.idx = idx;
|
||||
}
|
||||
});
|
||||
frappe.boot.desktop_icons.sort((a, b) => a.idx - b.idx);
|
||||
frappe.desktop_icons.sort((a, b) => a.idx - b.idx);
|
||||
}
|
||||
add_to_main_screen(title) {
|
||||
let icon = get_desktop_icon_by_label(title);
|
||||
|
|
@ -520,11 +622,14 @@ class DesktopIcon {
|
|||
this.icon_subtitle = "";
|
||||
this.icon_type = this.icon_data.icon_type;
|
||||
this.in_folder = in_folder;
|
||||
this.icon_data.in_folder = in_folder;
|
||||
this.link_type = this.icon_data.link_type;
|
||||
if (this.icon_type != "Folder" && !this.icon_data.sidebar) {
|
||||
this.icon_route = get_route(this.icon_data);
|
||||
}
|
||||
this.child_icons = this.get_child_icons_data();
|
||||
if (this.icon_data.child_icons) {
|
||||
this.child_icons = this.get_child_icons_data();
|
||||
}
|
||||
let render = this.validate_icon();
|
||||
if (render) {
|
||||
this.icon = $(
|
||||
|
|
@ -537,7 +642,7 @@ class DesktopIcon {
|
|||
this.parent_icon = this.icon_data.icon;
|
||||
this.setup_click();
|
||||
this.render_folder_thumbnail();
|
||||
this.setup_dragging();
|
||||
frappe.desktop_icons_objects.push(this);
|
||||
}
|
||||
|
||||
// this.child_icons = this.get_desktop_icon(this.icon_title).child_icons;
|
||||
|
|
@ -563,7 +668,7 @@ class DesktopIcon {
|
|||
}
|
||||
setup_click() {
|
||||
const me = this;
|
||||
if (this.child_icons.length && (this.icon_type == "App" || this.icon_type == "Folder")) {
|
||||
if (this.child_icons?.length && (this.icon_type == "App" || this.icon_type == "Folder")) {
|
||||
$(this.icon).on("click", () => {
|
||||
let modal = frappe.desktop_utils.create_desktop_modal(me);
|
||||
modal.setup(me.icon_title, me.child_icons, 4);
|
||||
|
|
@ -621,6 +726,7 @@ class DesktopIcon {
|
|||
}
|
||||
|
||||
setup_dragging() {
|
||||
if (!frappe.pages["desktop"].desktop_page.edit_mode) return;
|
||||
this.icon.on("drag", (event) => {
|
||||
const mouse_x = event.clientX;
|
||||
const mouse_y = event.clientY;
|
||||
|
|
@ -639,6 +745,51 @@ class DesktopIcon {
|
|||
}
|
||||
}
|
||||
});
|
||||
this.icon.on("dragstart", function (event) {
|
||||
frappe.desktop_utils.dragged_item = event.target;
|
||||
});
|
||||
this.icon.on("dragover", function (event) {
|
||||
console.log(event.target);
|
||||
if (frappe.desktop_utils.dragged_item == event.target.parentElement) return;
|
||||
if (
|
||||
event.target == frappe.desktop_utils.dragged_item ||
|
||||
frappe.desktop_utils.dragged_item.contains(event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (event.target.parentElement.classList.contains("icon-container")) {
|
||||
frappe.desktop_utils.allow_move = false;
|
||||
frappe.desktop_utils.in_folder_creation = true;
|
||||
|
||||
let icon_list = [];
|
||||
icon_list.push(
|
||||
get_desktop_icon_by_label(event.target.parentElement.parentElement.dataset.id)
|
||||
);
|
||||
icon_list.push(
|
||||
get_desktop_icon_by_label(frappe.desktop_utils.dragged_item.dataset.id)
|
||||
);
|
||||
|
||||
let icon = {
|
||||
label: "Untitled Folder",
|
||||
icon_type: "Folder",
|
||||
child_icons: icon_list,
|
||||
};
|
||||
let modal = frappe.desktop_utils.create_desktop_modal(icon);
|
||||
modal.setup(icon.label, icon_list, 4);
|
||||
$(event.target.parentElement).addClass("folder-icon");
|
||||
$(event.target.parentElement).empty();
|
||||
modal.show();
|
||||
frappe.boot.desktop_icons.push(icon);
|
||||
icon_list.forEach((icon) => {
|
||||
let desktop_icon = frappe.utils.get_desktop_icon_by_label(icon.label);
|
||||
desktop_icon.parent_icon = "Untitled Folder";
|
||||
frappe.new_desktop_icons.splice(frappe.boot.desktop_icons.indexOf(icon), 1);
|
||||
frappe.new_desktop_icons.push(desktop_icon);
|
||||
});
|
||||
} else {
|
||||
frappe.desktop_utils.allow_move = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -497,10 +497,9 @@ def build_xlsx_data(
|
|||
include_hidden_columns = cint(include_hidden_columns)
|
||||
include_indentation = cint(include_indentation)
|
||||
|
||||
if cint(include_filters):
|
||||
if cint(include_filters) and data.filters:
|
||||
filter_data = []
|
||||
filters = data.filters
|
||||
for filter_name, filter_value in filters.items():
|
||||
for filter_name, filter_value in data.filters.items():
|
||||
if not filter_value:
|
||||
continue
|
||||
filter_value = (
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
|
||||
import json
|
||||
import re
|
||||
from typing import TypedDict
|
||||
|
||||
from typing_extensions import NotRequired # not required in 3.11+
|
||||
from typing import (
|
||||
NotRequired, # not required in 3.11+
|
||||
TypedDict,
|
||||
)
|
||||
|
||||
import frappe
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Optional
|
||||
|
||||
import frappe
|
||||
|
|
@ -148,7 +150,7 @@ def sendmail(
|
|||
email_read_tracker_url=None,
|
||||
x_priority: Literal[1, 3, 5] = 3,
|
||||
email_headers=None,
|
||||
) -> Optional["EmailQueue"]:
|
||||
) -> EmailQueue | None:
|
||||
"""Send email using user's default **Email Account** or global default **Email Account**.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -403,7 +403,7 @@ class Email:
|
|||
if self.mail["Date"]:
|
||||
try:
|
||||
utc = email.utils.mktime_tz(email.utils.parsedate_tz(self.mail["Date"]))
|
||||
utc_dt = datetime.datetime.fromtimestamp(utc, tz=datetime.timezone.utc)
|
||||
utc_dt = datetime.datetime.fromtimestamp(utc, tz=datetime.UTC)
|
||||
self.date = convert_utc_to_system_timezone(utc_dt).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
self.date = now()
|
||||
|
|
|
|||
|
|
@ -38,9 +38,10 @@ app_include_css = [
|
|||
"report.bundle.css",
|
||||
]
|
||||
app_include_icons = [
|
||||
"/assets/frappe/icons/icons.svg",
|
||||
"/assets/frappe/icons/lucide/icons.svg",
|
||||
"/assets/frappe/icons/timeless/icons.svg",
|
||||
"/assets/frappe/icons/espresso/icons.svg",
|
||||
"/assets/frappe/icons/desktop_icons/alphabets.svg",
|
||||
]
|
||||
|
||||
doctype_js = {
|
||||
|
|
@ -51,6 +52,7 @@ doctype_js = {
|
|||
web_include_js = ["website_script.js"]
|
||||
web_include_css = []
|
||||
web_include_icons = [
|
||||
"/assets/frappe/icons/lucide/icons.svg",
|
||||
"/assets/frappe/icons/timeless/icons.svg",
|
||||
"/assets/frappe/icons/espresso/icons.svg",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -74,8 +74,8 @@ class TokenCache(Document):
|
|||
def get_expires_in(self):
|
||||
system_timezone = ZoneInfo(get_system_timezone())
|
||||
modified: datetime.datetime = get_datetime(self.modified).replace(tzinfo=system_timezone)
|
||||
expiry_utc = modified.astimezone(datetime.timezone.utc) + datetime.timedelta(seconds=self.expires_in)
|
||||
now_utc = datetime.datetime.now(datetime.timezone.utc)
|
||||
expiry_utc = modified.astimezone(datetime.UTC) + datetime.timedelta(seconds=self.expires_in)
|
||||
now_utc = datetime.datetime.now(datetime.UTC)
|
||||
return cint((expiry_utc - now_utc).total_seconds())
|
||||
|
||||
def is_expired(self):
|
||||
|
|
|
|||
1340
frappe/locale/ar.po
1340
frappe/locale/ar.po
File diff suppressed because it is too large
Load diff
1354
frappe/locale/bs.po
1354
frappe/locale/bs.po
File diff suppressed because it is too large
Load diff
1340
frappe/locale/cs.po
1340
frappe/locale/cs.po
File diff suppressed because it is too large
Load diff
1340
frappe/locale/da.po
1340
frappe/locale/da.po
File diff suppressed because it is too large
Load diff
1346
frappe/locale/de.po
1346
frappe/locale/de.po
File diff suppressed because it is too large
Load diff
1346
frappe/locale/eo.po
1346
frappe/locale/eo.po
File diff suppressed because it is too large
Load diff
1346
frappe/locale/es.po
1346
frappe/locale/es.po
File diff suppressed because it is too large
Load diff
1538
frappe/locale/fa.po
1538
frappe/locale/fa.po
File diff suppressed because it is too large
Load diff
1342
frappe/locale/fr.po
1342
frappe/locale/fr.po
File diff suppressed because it is too large
Load diff
1354
frappe/locale/hr.po
1354
frappe/locale/hr.po
File diff suppressed because it is too large
Load diff
1368
frappe/locale/hu.po
1368
frappe/locale/hu.po
File diff suppressed because it is too large
Load diff
1342
frappe/locale/id.po
1342
frappe/locale/id.po
File diff suppressed because it is too large
Load diff
1342
frappe/locale/it.po
1342
frappe/locale/it.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1340
frappe/locale/my.po
1340
frappe/locale/my.po
File diff suppressed because it is too large
Load diff
1346
frappe/locale/nb.po
1346
frappe/locale/nb.po
File diff suppressed because it is too large
Load diff
1340
frappe/locale/nl.po
1340
frappe/locale/nl.po
File diff suppressed because it is too large
Load diff
1340
frappe/locale/pl.po
1340
frappe/locale/pl.po
File diff suppressed because it is too large
Load diff
1342
frappe/locale/pt.po
1342
frappe/locale/pt.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1346
frappe/locale/ru.po
1346
frappe/locale/ru.po
File diff suppressed because it is too large
Load diff
1340
frappe/locale/sl.po
1340
frappe/locale/sl.po
File diff suppressed because it is too large
Load diff
1354
frappe/locale/sr.po
1354
frappe/locale/sr.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1354
frappe/locale/sv.po
1354
frappe/locale/sv.po
File diff suppressed because it is too large
Load diff
1340
frappe/locale/ta.po
1340
frappe/locale/ta.po
File diff suppressed because it is too large
Load diff
1342
frappe/locale/th.po
1342
frappe/locale/th.po
File diff suppressed because it is too large
Load diff
1346
frappe/locale/tr.po
1346
frappe/locale/tr.po
File diff suppressed because it is too large
Load diff
1342
frappe/locale/vi.po
1342
frappe/locale/vi.po
File diff suppressed because it is too large
Load diff
1346
frappe/locale/zh.po
1346
frappe/locale/zh.po
File diff suppressed because it is too large
Load diff
|
|
@ -9,9 +9,8 @@ from collections.abc import Generator, Iterable
|
|||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeAlias, Union, overload
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Self, TypeAlias, Union, overload, override
|
||||
|
||||
from typing_extensions import Self, override
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
import frappe
|
||||
|
|
@ -34,7 +33,7 @@ from frappe.utils.data import get_absolute_url, get_datetime, get_timedelta, get
|
|||
from frappe.utils.global_search import update_global_search
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
from typing import Self
|
||||
|
||||
from frappe.core.doctype.docfield.docfield import DocField
|
||||
|
||||
|
|
@ -43,8 +42,8 @@ DOCUMENT_LOCK_EXPIRY = 3 * 60 * 60 # All locks expire in 3 hours automatically
|
|||
DOCUMENT_LOCK_SOFT_EXPIRY = 30 * 60 # Let users force-unlock after 30 minutes
|
||||
|
||||
|
||||
_SingleDocument: TypeAlias = "Document"
|
||||
_NewDocument: TypeAlias = "Document"
|
||||
type _SingleDocument = "Document"
|
||||
type _NewDocument = "Document"
|
||||
|
||||
|
||||
@overload
|
||||
|
|
@ -614,7 +613,7 @@ class Document(BaseDocument):
|
|||
for df in self.meta.get_table_fields():
|
||||
self.update_child_table(df.fieldname, df)
|
||||
|
||||
def update_child_table(self, fieldname: str, df: Optional["DocField"] = None):
|
||||
def update_child_table(self, fieldname: str, df: "DocField" | None = None):
|
||||
"""sync child table for given fieldname"""
|
||||
df: DocField = df or self.meta.get_field(fieldname)
|
||||
if df.is_virtual:
|
||||
|
|
@ -1994,7 +1993,7 @@ def bulk_insert(
|
|||
def _document_values_generator(
|
||||
documents: Iterable["Document"],
|
||||
columns: list[str],
|
||||
) -> Generator[tuple[Any], None, None]:
|
||||
) -> Generator[tuple[Any]]:
|
||||
for doc in documents:
|
||||
doc.creation = doc.modified = now()
|
||||
doc.owner = doc.modified_by = frappe.session.user
|
||||
|
|
@ -2140,7 +2139,7 @@ def copy_doc(doc: "Document", ignore_no_copy: bool = True) -> "Document":
|
|||
def new_doc(
|
||||
doctype: str,
|
||||
*,
|
||||
parent_doc: Optional["Document"] = None,
|
||||
parent_doc: "Document" | None = None,
|
||||
parentfield: str | None = None,
|
||||
as_dict: bool = False,
|
||||
**kwargs,
|
||||
|
|
|
|||
|
|
@ -502,7 +502,9 @@ class Meta(Document):
|
|||
recent_change = frappe.db.sql(
|
||||
f"SELECT `creation` FROM `tab{self.name}` ORDER BY `creation` DESC LIMIT 1"
|
||||
) # nosemgrep
|
||||
if get_datetime(recent_change[0][0]) > add_to_date(None, days=-1 * LARGE_TABLE_RECENCY_THRESHOLD):
|
||||
if recent_change and get_datetime(recent_change[0][0]) > add_to_date(
|
||||
None, days=-1 * LARGE_TABLE_RECENCY_THRESHOLD
|
||||
):
|
||||
self.is_large_table = True
|
||||
|
||||
@cached_property
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ def set_new_name(doc):
|
|||
doc.name = validate_name(doc.doctype, doc.name)
|
||||
|
||||
|
||||
def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool:
|
||||
def is_autoincremented(doctype: str, meta: "Meta" | None = None) -> bool:
|
||||
"""Checks if the doctype has autoincrement autoname set"""
|
||||
|
||||
if not meta:
|
||||
|
|
@ -328,7 +328,7 @@ def _generate_random_string(length=10):
|
|||
def parse_naming_series(
|
||||
parts: list[str] | str,
|
||||
doctype=None,
|
||||
doc: Optional["Document"] = None,
|
||||
doc: "Document" | None = None,
|
||||
number_generator: Callable[[str, int], str] | None = None,
|
||||
) -> str:
|
||||
"""Parse the naming series and get next name.
|
||||
|
|
|
|||
|
|
@ -166,9 +166,9 @@ def update_user_settings(doctype, old_fieldname, new_fieldname):
|
|||
sync_user_settings()
|
||||
|
||||
user_settings = frappe.db.sql(
|
||||
''' select user, doctype, data from `__UserSettings`
|
||||
where doctype=%s and data like "%%%s%%"''',
|
||||
(doctype, old_fieldname),
|
||||
""" select user, doctype, data from `__UserSettings`
|
||||
where doctype=%s and data like %s""",
|
||||
(doctype, f"%{old_fieldname}%"),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
@ -40,7 +43,7 @@ def get_workflow_name(doctype):
|
|||
|
||||
@frappe.whitelist()
|
||||
def get_transitions(
|
||||
doc: Union["Document", str, dict], workflow: "Workflow" = None, raise_exception: bool = False
|
||||
doc: Document | str | dict, workflow: Workflow = None, raise_exception: bool = False
|
||||
) -> list[dict]:
|
||||
"""Return list of possible transitions for the given doc"""
|
||||
from frappe.model.document import Document
|
||||
|
|
|
|||
|
|
@ -56,21 +56,41 @@ def get_doc_module(module: str, doctype: str, name: str) -> "ModuleType":
|
|||
|
||||
@frappe.whitelist()
|
||||
def export_customizations(
|
||||
module: str, doctype: str, sync_on_migrate: bool = False, with_permissions: bool = False
|
||||
module: str,
|
||||
doctype: str,
|
||||
sync_on_migrate: bool = False,
|
||||
with_permissions: bool = False,
|
||||
apply_module_export_filter: bool = False,
|
||||
):
|
||||
"""Export Custom Field and Property Setter for the current document to the app folder.
|
||||
This will be synced with bench migrate"""
|
||||
|
||||
sync_on_migrate = cint(sync_on_migrate)
|
||||
with_permissions = cint(with_permissions)
|
||||
apply_module_export_filter = cint(apply_module_export_filter)
|
||||
|
||||
cf_filters = {"dt": doctype}
|
||||
ps_filters = {"doc_type": doctype}
|
||||
|
||||
if apply_module_export_filter:
|
||||
cf_filters["module"] = module
|
||||
ps_filters["module"] = module
|
||||
|
||||
if not frappe.conf.developer_mode:
|
||||
frappe.throw(_("Only allowed to export customizations in developer mode"))
|
||||
|
||||
custom = {
|
||||
"custom_fields": frappe.get_all("Custom Field", fields="*", filters={"dt": doctype}, order_by="name"),
|
||||
"custom_fields": frappe.get_all(
|
||||
"Custom Field",
|
||||
fields="*",
|
||||
filters=cf_filters,
|
||||
order_by="name",
|
||||
),
|
||||
"property_setters": frappe.get_all(
|
||||
"Property Setter", fields="*", filters={"doc_type": doctype}, order_by="name"
|
||||
"Property Setter",
|
||||
fields="*",
|
||||
filters=ps_filters,
|
||||
order_by="name",
|
||||
),
|
||||
"custom_perms": [],
|
||||
"links": frappe.get_all("DocType Link", fields="*", filters={"parent": doctype}, order_by="name"),
|
||||
|
|
@ -85,7 +105,9 @@ def export_customizations(
|
|||
|
||||
# also update the custom fields and property setters for all child tables
|
||||
for d in frappe.get_meta(doctype).get_table_fields():
|
||||
export_customizations(module, d.options, sync_on_migrate, with_permissions)
|
||||
export_customizations(
|
||||
module, d.options, sync_on_migrate, with_permissions, apply_module_export_filter
|
||||
)
|
||||
|
||||
if custom["custom_fields"] or custom["property_setters"] or custom["custom_perms"]:
|
||||
folder_path = os.path.join(get_module_path(module), "custom")
|
||||
|
|
@ -301,9 +323,7 @@ def get_app_publisher(module: str) -> str:
|
|||
return frappe.get_hooks(hook="app_publisher", app_name=app)[0]
|
||||
|
||||
|
||||
def make_boilerplate(
|
||||
template: str, doc: Union["Document", "frappe._dict"], opts: Union[dict, "frappe._dict"] = None
|
||||
):
|
||||
def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict | "frappe._dict" = None):
|
||||
target_path = get_doc_path(doc.module, doc.doctype, doc.name)
|
||||
template_name = template.replace("controller", scrub(doc.name))
|
||||
if template_name.endswith("._py"):
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class Monitor:
|
|||
self.data = frappe._dict(
|
||||
{
|
||||
"site": frappe.local.site,
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||
"timestamp": datetime.datetime.now(datetime.UTC),
|
||||
"transaction_type": transaction_type,
|
||||
"uuid": str(uuid.uuid4()),
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ class Monitor:
|
|||
|
||||
if job := rq.get_current_job():
|
||||
self.data.job_id = job.id
|
||||
waitdiff = self.data.timestamp - job.enqueued_at.replace(tzinfo=datetime.timezone.utc)
|
||||
waitdiff = self.data.timestamp - job.enqueued_at.replace(tzinfo=datetime.UTC)
|
||||
self.data.job.wait = int(waitdiff.total_seconds() * 1000000)
|
||||
|
||||
def add_custom_data(self, **kwargs):
|
||||
|
|
@ -94,7 +94,7 @@ class Monitor:
|
|||
|
||||
def dump(self, response=None):
|
||||
try:
|
||||
timediff = datetime.datetime.now(datetime.timezone.utc) - self.data.timestamp
|
||||
timediff = datetime.datetime.now(datetime.UTC) - self.data.timestamp
|
||||
# Obtain duration in microseconds
|
||||
self.data.duration = int(timediff.total_seconds() * 1000000)
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue