Merge branch 'develop' into txt-attachment-privacy
This commit is contained in:
commit
aa51492697
185 changed files with 45117 additions and 3383 deletions
37
.github/helper/update_pot_file.sh
vendored
Normal file
37
.github/helper/update_pot_file.sh
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
cd ~ || exit
|
||||
|
||||
echo "Setting Up Bench..."
|
||||
|
||||
pip install frappe-bench
|
||||
bench -v init frappe-bench --skip-assets --skip-redis-config-generation --python "$(which python)" --frappe-path "${GITHUB_WORKSPACE}"
|
||||
cd ./frappe-bench || exit
|
||||
|
||||
echo "Generating POT file..."
|
||||
bench generate-pot-file --app frappe
|
||||
|
||||
cd ./apps/frappe || exit
|
||||
|
||||
echo "Configuring git user..."
|
||||
git config user.email "developers@erpnext.com"
|
||||
git config user.name "frappe-pr-bot"
|
||||
|
||||
echo "Setting the correct git remote..."
|
||||
# Here, the git remote is a local file path by default. Let's change it to the upstream repo.
|
||||
git remote set-url upstream https://github.com/frappe/frappe.git
|
||||
|
||||
echo "Creating a new branch..."
|
||||
isodate=$(date -u +"%Y-%m-%d")
|
||||
branch_name="pot_${BASE_BRANCH}_${isodate}"
|
||||
git checkout -b "${branch_name}"
|
||||
|
||||
echo "Commiting changes..."
|
||||
git add .
|
||||
git commit -m "chore: update POT file"
|
||||
|
||||
gh auth setup-git
|
||||
git push -u upstream "${branch_name}"
|
||||
|
||||
echo "Creating a PR..."
|
||||
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" -R frappe/frappe
|
||||
38
.github/workflows/generate-pot-file.yml
vendored
Normal file
38
.github/workflows/generate-pot-file.yml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# This workflow is agnostic to branches. Only maintain on develop branch.
|
||||
# To add/remove branches just modify the matrix.
|
||||
|
||||
name: Regenerate POT file (translatable strings)
|
||||
on:
|
||||
schedule:
|
||||
# 9:30 UTC => 3 PM IST Sunday
|
||||
- cron: "30 9 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
regeneratee-pot-file:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branch: ["develop"]
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ matrix.branch }}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Run script to update POT file
|
||||
run: |
|
||||
bash ${GITHUB_WORKSPACE}/.github/helper/update_pot_file.sh
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
BASE_BRANCH: ${{ matrix.branch }}
|
||||
6
.github/workflows/server-tests.yml
vendored
6
.github/workflows/server-tests.yml
vendored
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: checkrun
|
||||
if: ${{ needs.checkrun.outputs.build == 'strawberry' }}
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
NODE_ENV: "production"
|
||||
|
||||
|
|
@ -160,6 +160,10 @@ jobs:
|
|||
cat $f
|
||||
done
|
||||
|
||||
- name: Setup tmate session
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
if: ${{ contains( github.event.pull_request.labels.*.name, 'debug-gha') }}
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,7 +2,6 @@
|
|||
*.py~
|
||||
*.comp.js
|
||||
*.DS_Store
|
||||
locale
|
||||
.wnf-lang-status
|
||||
*.swp
|
||||
*.egg-info
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
files:
|
||||
- source: /frappe/locale/main.pot
|
||||
translation: /frappe/locale/%two_letters_code%.po
|
||||
pull_request_title: "chore: sync translations from crowdin"
|
||||
pull_request_title: "fix: sync translations from crowdin"
|
||||
pull_request_labels:
|
||||
- translation
|
||||
commit_message: "fix: %language% translations"
|
||||
append_commit_message: false
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const { defineConfig } = require("cypress");
|
||||
const fs = require("fs");
|
||||
|
||||
module.exports = defineConfig({
|
||||
projectId: "92odwv",
|
||||
|
|
@ -7,7 +8,6 @@ module.exports = defineConfig({
|
|||
defaultCommandTimeout: 20000,
|
||||
pageLoadTimeout: 15000,
|
||||
video: true,
|
||||
videoUploadOnPasses: false,
|
||||
viewportHeight: 960,
|
||||
viewportWidth: 1400,
|
||||
retries: {
|
||||
|
|
@ -18,6 +18,19 @@ module.exports = defineConfig({
|
|||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
// Delete videos for specs without failing or retried tests
|
||||
// https://docs.cypress.io/guides/guides/screenshots-and-videos#Delete-videos-for-specs-without-failing-or-retried-tests
|
||||
on("after:spec", (spec, results) => {
|
||||
if (results && results.video) {
|
||||
const failures = results.tests.some((test) =>
|
||||
test.attempts.some((attempt) => attempt.state === "failed")
|
||||
);
|
||||
if (!failures) {
|
||||
fs.unlinkSync(results.video);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return require("./cypress/plugins/index.js")(on, config);
|
||||
},
|
||||
testIsolation: false,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,6 @@ context("List Paging", () => {
|
|||
cy.get(".list-paging-area .list-count").should("contain.text", "500 of");
|
||||
cy.get(".list-paging-area .btn-more").click();
|
||||
|
||||
cy.get(".list-paging-area .list-count").should("contain.text", "1000 of");
|
||||
cy.get(".list-paging-area .list-count").should("contain.text", "1,000 of");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ async function update_assets_json_from_built_assets(apps) {
|
|||
|
||||
async function update_assets_obj(app, assets, assets_rtl) {
|
||||
const app_path = path.join(apps_path, app, app);
|
||||
const dist_path = path.join(app_path, "public, dist");
|
||||
const dist_path = path.join(app_path, "public", "dist");
|
||||
const files = await glob("**/*.bundle.*.{js,css}", { cwd: dist_path });
|
||||
const prefix = path.join("/", "assets", app, "dist");
|
||||
|
||||
|
|
|
|||
|
|
@ -504,9 +504,9 @@ def setup_redis_cache_connection():
|
|||
global cache
|
||||
|
||||
if not cache:
|
||||
from frappe.utils.redis_wrapper import RedisWrapper
|
||||
from frappe.utils.redis_wrapper import setup_cache
|
||||
|
||||
cache = RedisWrapper.from_url(conf.get("redis_cache"))
|
||||
cache = setup_cache()
|
||||
|
||||
|
||||
def get_traceback(with_context: bool = False) -> str:
|
||||
|
|
@ -536,8 +536,7 @@ def log(msg: str) -> None:
|
|||
|
||||
:param msg: Message."""
|
||||
if not request:
|
||||
if conf.get("logging"):
|
||||
print(repr(msg))
|
||||
print(repr(msg))
|
||||
|
||||
debug_log.append(as_unicode(msg))
|
||||
|
||||
|
|
@ -1699,11 +1698,19 @@ def append_hook(target, key, value):
|
|||
target[key].extend(value)
|
||||
|
||||
|
||||
def setup_module_map(include_all_apps=True):
|
||||
"""Rebuild map of all modules (internal)."""
|
||||
if conf.db_name:
|
||||
def setup_module_map(include_all_apps: bool = True) -> None:
|
||||
"""
|
||||
Function to rebuild map of all modules
|
||||
|
||||
:param: include_all_apps: Include all apps on bench, or just apps installed on the site.
|
||||
:return: Nothing
|
||||
"""
|
||||
if include_all_apps:
|
||||
local.app_modules = cache.get_value("app_modules")
|
||||
local.module_app = cache.get_value("module_app")
|
||||
else:
|
||||
local.app_modules = cache.get_value("installed_app_modules")
|
||||
local.module_app = cache.get_value("module_installed_app")
|
||||
|
||||
if not (local.app_modules and local.module_app):
|
||||
local.module_app, local.app_modules = {}, {}
|
||||
|
|
@ -1722,9 +1729,12 @@ def setup_module_map(include_all_apps=True):
|
|||
local.module_app[module] = app
|
||||
local.app_modules[app].append(module)
|
||||
|
||||
if conf.db_name:
|
||||
if include_all_apps:
|
||||
cache.set_value("app_modules", local.app_modules)
|
||||
cache.set_value("module_app", local.module_app)
|
||||
else:
|
||||
cache.set_value("installed_app_modules", local.app_modules)
|
||||
cache.set_value("module_installed_app", local.module_app)
|
||||
|
||||
|
||||
def get_file_items(path, raise_not_found=False, ignore_empty_lines=True):
|
||||
|
|
@ -2178,24 +2188,27 @@ def get_print(
|
|||
:param as_pdf: Return as PDF. Default False.
|
||||
:param password: Password to encrypt the pdf with. Default None"""
|
||||
from frappe.utils.pdf import get_pdf
|
||||
from frappe.website.serve import get_response_content
|
||||
from frappe.website.serve import get_response_without_exception_handling
|
||||
|
||||
original_form_dict = copy.deepcopy(local.form_dict)
|
||||
try:
|
||||
local.form_dict.doctype = doctype
|
||||
local.form_dict.name = name
|
||||
local.form_dict.format = print_format
|
||||
local.form_dict.style = style
|
||||
local.form_dict.doc = doc
|
||||
local.form_dict.no_letterhead = no_letterhead
|
||||
local.form_dict.letterhead = letterhead
|
||||
|
||||
local.form_dict.doctype = doctype
|
||||
local.form_dict.name = name
|
||||
local.form_dict.format = print_format
|
||||
local.form_dict.style = style
|
||||
local.form_dict.doc = doc
|
||||
local.form_dict.no_letterhead = no_letterhead
|
||||
local.form_dict.letterhead = letterhead
|
||||
pdf_options = pdf_options or {}
|
||||
if password:
|
||||
pdf_options["password"] = password
|
||||
|
||||
pdf_options = pdf_options or {}
|
||||
if password:
|
||||
pdf_options["password"] = password
|
||||
response = get_response_without_exception_handling("printview", 200)
|
||||
html = str(response.data, "utf-8")
|
||||
finally:
|
||||
local.form_dict = original_form_dict
|
||||
|
||||
html = get_response_content("printview")
|
||||
local.form_dict = original_form_dict
|
||||
return get_pdf(html, options=pdf_options, output=output) if as_pdf else html
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -295,14 +295,15 @@ def make_form_dict(request: Request):
|
|||
args.update(request.args or {})
|
||||
args.update(request.form or {})
|
||||
|
||||
if not isinstance(args, dict):
|
||||
if isinstance(args, dict):
|
||||
frappe.local.form_dict = frappe._dict(args)
|
||||
# _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict
|
||||
frappe.local.form_dict.pop("_", None)
|
||||
elif isinstance(args, list):
|
||||
frappe.local.form_dict["data"] = args
|
||||
else:
|
||||
frappe.throw(_("Invalid request arguments"))
|
||||
|
||||
frappe.local.form_dict = frappe._dict(args)
|
||||
|
||||
# _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict
|
||||
frappe.local.form_dict.pop("_", None)
|
||||
|
||||
|
||||
def handle_exception(e):
|
||||
response = None
|
||||
|
|
@ -398,11 +399,7 @@ def handle_exception(e):
|
|||
|
||||
def sync_database(rollback: bool) -> bool:
|
||||
# if HTTP method would change server state, commit if necessary
|
||||
if (
|
||||
frappe.db
|
||||
and (frappe.local.flags.commit or frappe.local.request.method in UNSAFE_HTTP_METHODS)
|
||||
and frappe.db.transaction_writes
|
||||
):
|
||||
if frappe.db and (frappe.local.flags.commit or frappe.local.request.method in UNSAFE_HTTP_METHODS):
|
||||
frappe.db.commit()
|
||||
rollback = False
|
||||
elif frappe.db:
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ class AssignmentRule(Document):
|
|||
description: DF.SmallText
|
||||
disabled: DF.Check
|
||||
document_type: DF.Link
|
||||
due_date_based_on: DF.Literal
|
||||
field: DF.Literal
|
||||
due_date_based_on: DF.Literal[None]
|
||||
field: DF.Literal[None]
|
||||
last_user: DF.Link | None
|
||||
priority: DF.Int
|
||||
rule: DF.Literal["Round Robin", "Load Balancing", "Based on Field"]
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class MilestoneTracker(Document):
|
|||
|
||||
disabled: DF.Check
|
||||
document_type: DF.Link
|
||||
track_field: DF.Literal
|
||||
track_field: DF.Literal[None]
|
||||
# end: auto-generated types
|
||||
|
||||
def on_update(self):
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ global_cache_keys = (
|
|||
"installed_apps",
|
||||
"all_apps",
|
||||
"app_modules",
|
||||
"installed_app_modules",
|
||||
"module_app",
|
||||
"module_installed_app",
|
||||
"system_settings",
|
||||
"scheduler_events",
|
||||
"time_zone",
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None):
|
|||
with decrypt_backup(sql_file_path, key):
|
||||
if not is_partial(sql_file_path):
|
||||
click.secho(
|
||||
"Full backup file detected.Use `bench restore` to restore a Frappe Site.",
|
||||
"Full backup file detected. Use `bench restore` to restore a Frappe Site.",
|
||||
fg="red",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
|
@ -364,7 +364,7 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None):
|
|||
else:
|
||||
if not is_partial(sql_file_path):
|
||||
click.secho(
|
||||
"Full backup file detected.Use `bench restore` to restore a Frappe Site.",
|
||||
"Full backup file detected. Use `bench restore` to restore a Frappe Site.",
|
||||
fg="red",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import json
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
|
||||
import click
|
||||
|
||||
|
|
@ -14,6 +15,9 @@ from frappe.utils import cint, update_progress_bar
|
|||
|
||||
EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
|
||||
|
||||
@click.command("build")
|
||||
@click.option("--app", help="Build assets for app")
|
||||
|
|
@ -514,11 +518,17 @@ def postgres(context, extra_args):
|
|||
|
||||
def _enter_console(extra_args=None):
|
||||
from frappe.database import get_command
|
||||
from frappe.utils import get_site_path
|
||||
|
||||
if frappe.conf.db_type == "mariadb":
|
||||
os.environ["MYSQL_HISTFILE"] = os.path.abspath(get_site_path("logs", "mariadb_console.log"))
|
||||
else:
|
||||
os.environ["PSQL_HISTORY"] = os.path.abspath(get_site_path("logs", "postgresql_console.log"))
|
||||
|
||||
bin, args, bin_name = get_command(
|
||||
host=frappe.conf.db_host,
|
||||
port=frappe.conf.db_port,
|
||||
user=frappe.conf.db_name,
|
||||
user=frappe.conf.db_user,
|
||||
password=frappe.conf.db_password,
|
||||
db_name=frappe.conf.db_name,
|
||||
extra=list(extra_args) if extra_args else [],
|
||||
|
|
@ -584,6 +594,18 @@ def _console_cleanup():
|
|||
frappe.destroy()
|
||||
|
||||
|
||||
def store_logs(terminal: "InteractiveShellEmbed") -> None:
|
||||
from contextlib import suppress
|
||||
|
||||
frappe.log_level = 20 # info
|
||||
with suppress(Exception):
|
||||
logger = frappe.logger("ipython")
|
||||
logger.info("=== bench console session ===")
|
||||
for line in terminal.history_manager.get_range():
|
||||
logger.info(line[2])
|
||||
logger.info("=== session end ===")
|
||||
|
||||
|
||||
@click.command("console")
|
||||
@click.option("--autoreload", is_flag=True, help="Reload changes to code automatically")
|
||||
@pass_context
|
||||
|
|
@ -607,6 +629,7 @@ def console(context, autoreload=False):
|
|||
|
||||
all_apps = frappe.get_installed_apps()
|
||||
failed_to_import = []
|
||||
register(store_logs, terminal) # Note: atexit runs in reverse order of registration
|
||||
|
||||
for app in list(all_apps):
|
||||
try:
|
||||
|
|
@ -619,6 +642,14 @@ def console(context, autoreload=False):
|
|||
if failed_to_import:
|
||||
print("\nFailed to import:\n{}".format(", ".join(failed_to_import)))
|
||||
|
||||
# ref: https://stackoverflow.com/a/74681224
|
||||
try:
|
||||
from IPython.core import ultratb
|
||||
|
||||
ultratb.VerboseTB._tb_highlight = "bg:ansibrightblack"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
terminal.colors = "neutral"
|
||||
terminal.display_banner = False
|
||||
terminal()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ def make(
|
|||
email_template=None,
|
||||
communication_type=None,
|
||||
send_after=None,
|
||||
print_language=None,
|
||||
now=False,
|
||||
**kwargs,
|
||||
) -> dict[str, str]:
|
||||
"""Make a new communication. Checks for email permissions for specified Document.
|
||||
|
|
@ -102,6 +104,8 @@ def make(
|
|||
communication_type=communication_type,
|
||||
add_signature=False,
|
||||
send_after=send_after,
|
||||
print_language=print_language,
|
||||
now=now,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -128,6 +132,8 @@ def _make(
|
|||
communication_type=None,
|
||||
add_signature=True,
|
||||
send_after=None,
|
||||
print_language=None,
|
||||
now=False,
|
||||
) -> dict[str, str]:
|
||||
"""Internal method to make a new communication that ignores Permission checks."""
|
||||
|
||||
|
|
@ -181,6 +187,8 @@ def _make(
|
|||
print_format=print_format,
|
||||
send_me_a_copy=send_me_a_copy,
|
||||
print_letterhead=print_letterhead,
|
||||
print_language=print_language,
|
||||
now=now,
|
||||
)
|
||||
|
||||
emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy)
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ class CommunicationEmailMixin:
|
|||
)
|
||||
return self._incoming_email_account
|
||||
|
||||
def mail_attachments(self, print_format=None, print_html=None):
|
||||
def mail_attachments(self, print_format=None, print_html=None, print_language=None):
|
||||
final_attachments = []
|
||||
|
||||
if print_format or print_html:
|
||||
|
|
@ -194,6 +194,7 @@ class CommunicationEmailMixin:
|
|||
"print_format_attachment": 1,
|
||||
"doctype": self.reference_doctype,
|
||||
"name": self.reference_name,
|
||||
"lang": print_language or frappe.local.lang,
|
||||
}
|
||||
final_attachments.append(d)
|
||||
|
||||
|
|
@ -256,6 +257,7 @@ class CommunicationEmailMixin:
|
|||
send_me_a_copy=None,
|
||||
print_letterhead=None,
|
||||
is_inbound_mail_communcation=None,
|
||||
print_language=None,
|
||||
) -> dict:
|
||||
outgoing_email_account = self.get_outgoing_email_account()
|
||||
if not outgoing_email_account:
|
||||
|
|
@ -272,7 +274,9 @@ class CommunicationEmailMixin:
|
|||
if not (recipients or cc):
|
||||
return {}
|
||||
|
||||
final_attachments = self.mail_attachments(print_format=print_format, print_html=print_html)
|
||||
final_attachments = self.mail_attachments(
|
||||
print_format=print_format, print_html=print_html, print_language=print_language
|
||||
)
|
||||
incoming_email_account = self.get_incoming_email_account()
|
||||
return {
|
||||
"recipients": recipients,
|
||||
|
|
@ -303,6 +307,8 @@ class CommunicationEmailMixin:
|
|||
send_me_a_copy=None,
|
||||
print_letterhead=None,
|
||||
is_inbound_mail_communcation=None,
|
||||
print_language=None,
|
||||
now=False,
|
||||
):
|
||||
if input_dict := self.sendmail_input_dict(
|
||||
print_html=print_html,
|
||||
|
|
@ -310,5 +316,6 @@ class CommunicationEmailMixin:
|
|||
send_me_a_copy=send_me_a_copy,
|
||||
print_letterhead=print_letterhead,
|
||||
is_inbound_mail_communcation=is_inbound_mail_communcation,
|
||||
print_language=print_language,
|
||||
):
|
||||
frappe.sendmail(**input_dict)
|
||||
frappe.sendmail(now=now, **input_dict)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
import frappe
|
||||
from frappe.core.doctype.communication.communication import Communication, get_emails, parse_email
|
||||
from frappe.core.doctype.communication.email import add_attachments
|
||||
from frappe.core.doctype.communication.email import add_attachments, make
|
||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class DocType(Document):
|
|||
custom: DF.Check
|
||||
default_email_template: DF.Link | None
|
||||
default_print_format: DF.Data | None
|
||||
default_view: DF.Literal
|
||||
default_view: DF.Literal[None]
|
||||
description: DF.SmallText | None
|
||||
document_type: DF.Literal["", "Document", "Setup", "System", "Other"]
|
||||
documentation: DF.Data | None
|
||||
|
|
@ -355,6 +355,8 @@ class DocType(Document):
|
|||
for df in new_fields_to_fetch:
|
||||
if df.fieldname not in old_fields_to_fetch:
|
||||
link_fieldname, source_fieldname = df.fetch_from.split(".", 1)
|
||||
if not source_fieldname:
|
||||
continue # Invalid expression
|
||||
link_df = new_meta.get_field(link_fieldname)
|
||||
|
||||
if frappe.db.db_type == "postgres":
|
||||
|
|
@ -1187,7 +1189,7 @@ def validate_fields_for_doctype(doctype):
|
|||
|
||||
|
||||
# this is separate because it is also called via custom field
|
||||
def validate_fields(meta):
|
||||
def validate_fields(meta: Meta):
|
||||
"""Validate doctype fields. Checks
|
||||
1. There are no illegal characters in fieldnames
|
||||
2. If fieldnames are unique.
|
||||
|
|
@ -1597,6 +1599,8 @@ def validate_fields(meta):
|
|||
fields = meta.get("fields")
|
||||
fieldname_list = [d.fieldname for d in fields]
|
||||
|
||||
in_ci = os.environ.get("CI")
|
||||
|
||||
not_allowed_in_list_view = get_fields_not_allowed_in_list_view(meta)
|
||||
|
||||
for d in fields:
|
||||
|
|
@ -1617,7 +1621,7 @@ def validate_fields(meta):
|
|||
scrub_fetch_from(d)
|
||||
validate_data_field_type(d)
|
||||
|
||||
if not frappe.flags.in_migrate:
|
||||
if not frappe.flags.in_migrate or in_ci:
|
||||
check_unique_fieldname(meta.get("name"), d.fieldname)
|
||||
check_link_table_options(meta.get("name"), d)
|
||||
check_illegal_mandatory(meta.get("name"), d)
|
||||
|
|
@ -1630,7 +1634,7 @@ def validate_fields(meta):
|
|||
check_max_height(d)
|
||||
check_no_of_ratings(d)
|
||||
|
||||
if not frappe.flags.in_migrate:
|
||||
if not frappe.flags.in_migrate or in_ci:
|
||||
check_fold(fields)
|
||||
check_search_fields(meta, fields)
|
||||
check_title_field(meta)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class DocumentNamingRuleCondition(Document):
|
|||
from frappe.types import DF
|
||||
|
||||
condition: DF.Literal["=", "!=", ">", "<", ">=", "<="]
|
||||
field: DF.Literal
|
||||
field: DF.Literal[None]
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.database.schema import SPECIAL_CHAR_PATTERN
|
||||
from frappe.model.document import Document
|
||||
from frappe.permissions import get_doctypes_with_read
|
||||
from frappe.permissions import SYSTEM_USER_ROLE, get_doctypes_with_read
|
||||
from frappe.utils import call_hook_method, cint, get_files_path, get_hook_method, get_url
|
||||
from frappe.utils.file_manager import is_safe_path
|
||||
from frappe.utils.image import optimize_image, strip_exif_data
|
||||
|
|
@ -789,10 +789,13 @@ def on_doctype_update():
|
|||
def has_permission(doc, ptype=None, user=None, debug=False):
|
||||
user = user or frappe.session.user
|
||||
|
||||
if ptype == "create":
|
||||
return frappe.has_permission("File", "create", user=user, debug=debug)
|
||||
if user == "Administrator":
|
||||
return True
|
||||
|
||||
if not doc.is_private or (user != "Guest" and doc.owner == user) or user == "Administrator":
|
||||
if not doc.is_private and ptype in ("read", "select"):
|
||||
return True
|
||||
|
||||
if user != "Guest" and doc.owner == user:
|
||||
return True
|
||||
|
||||
if doc.attached_to_doctype and doc.attached_to_name:
|
||||
|
|
@ -818,10 +821,13 @@ def get_permission_query_conditions(user: str | None = None) -> str:
|
|||
if user == "Administrator":
|
||||
return ""
|
||||
|
||||
if SYSTEM_USER_ROLE not in frappe.get_roles(user):
|
||||
return f""" `tabFile`.`owner` = {frappe.db.escape(user)} """
|
||||
|
||||
readable_doctypes = ", ".join(repr(dt) for dt in get_doctypes_with_read())
|
||||
return f"""
|
||||
(`tabFile`.`is_private` = 0)
|
||||
OR (`tabFile`.`attached_to_doctype` IS NULL AND `tabFile`.`owner` = {user !r})
|
||||
OR (`tabFile`.`attached_to_doctype` IS NULL AND `tabFile`.`owner` = {frappe.db.escape(user)})
|
||||
OR (`tabFile`.`attached_to_doctype` IN ({readable_doctypes}))
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -681,41 +681,9 @@ class TestAttachmentsAccess(FrappeTestCase):
|
|||
self.assertNotIn("test_user_standalone.txt", system_manager_files)
|
||||
|
||||
self.assertIn("test_sm_attachment.txt", system_manager_attachments_files)
|
||||
self.assertIn("test_sm_attachment.txt", user_attachments_files)
|
||||
self.assertIn("test_user_attachment.txt", system_manager_attachments_files)
|
||||
self.assertIn("test_user_attachment.txt", user_attachments_files)
|
||||
|
||||
def test_list_public_single_file(self):
|
||||
"""Ensure that users are able to list public standalone files."""
|
||||
frappe.set_user("test@example.com")
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_public_single.txt",
|
||||
content="Public single File",
|
||||
is_private=0,
|
||||
).insert()
|
||||
|
||||
frappe.set_user("test4@example.com")
|
||||
files = [file.file_name for file in get_files_in_folder("Home")["files"]]
|
||||
self.assertIn("test_public_single.txt", files)
|
||||
|
||||
def test_list_public_attachment(self):
|
||||
"""Ensure that users are able to list public attachments."""
|
||||
frappe.set_user("test@example.com")
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
frappe.new_doc(
|
||||
"File",
|
||||
file_name="test_public_attachment.txt",
|
||||
attached_to_doctype=self.attached_to_doctype,
|
||||
attached_to_name=self.attached_to_docname,
|
||||
content="Public Attachment",
|
||||
is_private=0,
|
||||
).insert()
|
||||
|
||||
frappe.set_user("test4@example.com")
|
||||
files = [file.file_name for file in get_files_in_folder("Home/Attachments")["files"]]
|
||||
self.assertIn("test_public_attachment.txt", files)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.rollback()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class ModuleDef(Document):
|
|||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
app_name: DF.Literal
|
||||
app_name: DF.Literal[None]
|
||||
custom: DF.Check
|
||||
module_name: DF.Data
|
||||
package: DF.Link | None
|
||||
|
|
|
|||
16
frappe/core/doctype/module_def/module_def_list.js
Normal file
16
frappe/core/doctype/module_def/module_def_list.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
frappe.listview_settings["Module Def"] = {
|
||||
onload: function (list_view) {
|
||||
frappe.call({
|
||||
method: "frappe.core.doctype.module_def.module_def.get_installed_apps",
|
||||
callback: (r) => {
|
||||
const field = list_view.page.fields_dict.app_name;
|
||||
if (!field) return;
|
||||
|
||||
const options = JSON.parse(r.message);
|
||||
options.unshift(""); // Add empty option
|
||||
field.df.options = options;
|
||||
field.set_options();
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -60,15 +60,15 @@ class PermissionInspector(Document):
|
|||
...
|
||||
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
def get_list():
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def get_count(args):
|
||||
def get_count():
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
def get_stats():
|
||||
...
|
||||
|
||||
def delete(self):
|
||||
|
|
|
|||
|
|
@ -83,10 +83,9 @@
|
|||
},
|
||||
{
|
||||
"fieldname": "job_id",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"label": "Job ID",
|
||||
"no_copy": 1,
|
||||
"options": "RQ Job",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -106,7 +105,7 @@
|
|||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-01 18:29:12.700239",
|
||||
"modified": "2024-03-07 12:01:58.209879",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Prepared Report",
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class PreparedReport(Document):
|
|||
|
||||
error_message: DF.Text | None
|
||||
filters: DF.SmallText | None
|
||||
job_id: DF.Link | None
|
||||
job_id: DF.Data | None
|
||||
queued_at: DF.Datetime | None
|
||||
queued_by: DF.Data | None
|
||||
report_end_time: DF.Datetime | None
|
||||
|
|
@ -91,7 +91,7 @@ class PreparedReport(Document):
|
|||
def generate_report(prepared_report):
|
||||
update_job_id(prepared_report)
|
||||
|
||||
instance = frappe.get_doc("Prepared Report", prepared_report)
|
||||
instance: PreparedReport = frappe.get_doc("Prepared Report", prepared_report)
|
||||
report = frappe.get_doc("Report", instance.report_name)
|
||||
|
||||
add_data_to_monitor(report=instance.report_name)
|
||||
|
|
@ -109,7 +109,7 @@ def generate_report(prepared_report):
|
|||
report.custom_columns = data["columns"]
|
||||
|
||||
result = generate_report_result(report=report, filters=instance.filters, user=instance.owner)
|
||||
create_json_gz_file(result, instance.doctype, instance.name)
|
||||
create_json_gz_file(result, instance.doctype, instance.name, instance.report_name)
|
||||
|
||||
instance.status = "Completed"
|
||||
except Exception:
|
||||
|
|
@ -215,11 +215,13 @@ def delete_prepared_reports(reports):
|
|||
prepared_report.delete(ignore_permissions=True, delete_permanently=True)
|
||||
|
||||
|
||||
def create_json_gz_file(data, dt, dn):
|
||||
def create_json_gz_file(data, dt, dn, report_name):
|
||||
# Storing data in CSV file causes information loss
|
||||
# Reports like P&L Statement were completely unsuable because of this
|
||||
json_filename = "{}.json.gz".format(frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H:M"))
|
||||
encoded_content = frappe.safe_encode(frappe.as_json(data))
|
||||
json_filename = "{}_{}.json.gz".format(
|
||||
frappe.scrub(report_name), frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H-M")
|
||||
)
|
||||
encoded_content = frappe.safe_encode(frappe.as_json(data, indent=None, separators=(",", ":")))
|
||||
compressed_content = gzip.compress(encoded_content)
|
||||
|
||||
# Call save() file function to upload and attach the file
|
||||
|
|
|
|||
|
|
@ -39,12 +39,10 @@ class Recorder(Document):
|
|||
super(Document, self).__init__(request)
|
||||
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
start = cint(args.get("start"))
|
||||
page_length = cint(args.get("page_length")) or 20
|
||||
requests = Recorder.get_filtered_requests(args)[start : start + page_length]
|
||||
def get_list(filters=None, start=0, page_length=20, order_by="duration desc"):
|
||||
requests = Recorder.get_filtered_requests(filters)[start : start + page_length]
|
||||
|
||||
if order_by_statment := args.get("order_by"):
|
||||
if order_by_statment := order_by:
|
||||
if "." in order_by_statment:
|
||||
order_by_statment = order_by_statment.split(".")[1]
|
||||
|
||||
|
|
@ -60,12 +58,11 @@ class Recorder(Document):
|
|||
return sorted(requests, key=lambda r: r.duration, reverse=1)
|
||||
|
||||
@staticmethod
|
||||
def get_count(args):
|
||||
return len(Recorder.get_filtered_requests(args))
|
||||
def get_count(filters=None):
|
||||
return len(Recorder.get_filtered_requests(filters))
|
||||
|
||||
@staticmethod
|
||||
def get_filtered_requests(args):
|
||||
filters = args.get("filters")
|
||||
def get_filtered_requests(filters):
|
||||
requests = [serialize_request(request) for request in get_recorder_data()]
|
||||
return [req for req in requests if evaluate_filters(req, filters)]
|
||||
|
||||
|
|
|
|||
|
|
@ -39,15 +39,15 @@ class RecorderQuery(Document):
|
|||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
def get_list():
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_count(args):
|
||||
def get_count():
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
def get_stats():
|
||||
pass
|
||||
|
||||
def delete(self):
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@
|
|||
"options": "Domain"
|
||||
},
|
||||
{
|
||||
"description": "Route: Example \"/desk\"",
|
||||
"description": "Route: Example \"/app\"",
|
||||
"fieldname": "home_page",
|
||||
"fieldtype": "Data",
|
||||
"label": "Home Page"
|
||||
|
|
@ -148,7 +148,7 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-08 15:52:37.525003",
|
||||
"modified": "2024-03-13 20:59:37.875253",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Role",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.website.path_resolver import validate_path
|
||||
|
||||
desk_properties = (
|
||||
"search_bar",
|
||||
|
|
@ -14,6 +15,7 @@ desk_properties = (
|
|||
"timeline",
|
||||
"dashboard",
|
||||
)
|
||||
from frappe.website.router import clear_routing_cache
|
||||
|
||||
STANDARD_ROLES = ("Administrator", "System Manager", "Script Manager", "All", "Guest")
|
||||
|
||||
|
|
@ -56,6 +58,7 @@ class Role(Document):
|
|||
self.disable_role()
|
||||
else:
|
||||
self.set_desk_properties()
|
||||
self.validate_homepage()
|
||||
|
||||
def disable_role(self):
|
||||
if self.name in STANDARD_ROLES:
|
||||
|
|
@ -63,6 +66,13 @@ class Role(Document):
|
|||
else:
|
||||
self.remove_roles()
|
||||
|
||||
def validate_homepage(self):
|
||||
if frappe.request and self.home_page:
|
||||
validate_path(self.home_page)
|
||||
|
||||
if self.has_value_changed("home_page"):
|
||||
clear_routing_cache()
|
||||
|
||||
def set_desk_properties(self):
|
||||
# set if desk_access is not allowed, unset all desk properties
|
||||
if self.name == "Guest":
|
||||
|
|
|
|||
|
|
@ -76,22 +76,18 @@ class RQJob(Document):
|
|||
return self._job_obj
|
||||
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
start = cint(args.get("start"))
|
||||
page_length = cint(args.get("page_length")) or 20
|
||||
|
||||
order_desc = "desc" in args.get("order_by", "")
|
||||
|
||||
matched_job_ids = RQJob.get_matching_job_ids(args)[start : start + page_length]
|
||||
def get_list(filters=None, start=0, page_length=20, order_by="modified desc"):
|
||||
matched_job_ids = RQJob.get_matching_job_ids(filters=filters)[start : start + page_length]
|
||||
|
||||
conn = get_redis_conn()
|
||||
jobs = [serialize_job(job) for job in Job.fetch_many(job_ids=matched_job_ids, connection=conn) if job]
|
||||
|
||||
order_desc = "desc" in order_by
|
||||
return sorted(jobs, key=lambda j: j.modified, reverse=order_desc)
|
||||
|
||||
@staticmethod
|
||||
def get_matching_job_ids(args) -> list[str]:
|
||||
filters = make_filter_dict(args.get("filters"))
|
||||
def get_matching_job_ids(filters) -> list[str]:
|
||||
filters = make_filter_dict(filters or [])
|
||||
|
||||
queues = _eval_filters(filters.get("queue"), QUEUES)
|
||||
statuses = _eval_filters(filters.get("status"), JOB_STATUSES)
|
||||
|
|
@ -117,12 +113,12 @@ class RQJob(Document):
|
|||
frappe.msgprint(_("Job is not running."), title=_("Invalid Operation"))
|
||||
|
||||
@staticmethod
|
||||
def get_count(args) -> int:
|
||||
return len(RQJob.get_matching_job_ids(args))
|
||||
def get_count(filters=None) -> int:
|
||||
return len(RQJob.get_matching_job_ids(filters))
|
||||
|
||||
# None of these methods apply to virtual job doctype, overriden for sanity.
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
def get_stats():
|
||||
return {}
|
||||
|
||||
def db_insert(self, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -61,18 +61,22 @@ class TestRQJob(FrappeTestCase):
|
|||
def test_get_list_filtering(self):
|
||||
# Check failed job clearning and filtering
|
||||
remove_failed_jobs()
|
||||
jobs = RQJob.get_list({"filters": [["RQ Job", "status", "=", "failed"]]})
|
||||
jobs = frappe.get_all("RQ Job", {"status": "failed"})
|
||||
self.assertEqual(jobs, [])
|
||||
|
||||
# Pass a job
|
||||
job = frappe.enqueue(method=self.BG_JOB, queue="short")
|
||||
self.check_status(job, "finished")
|
||||
|
||||
# Fail a job
|
||||
job = frappe.enqueue(method=self.BG_JOB, queue="short", fail=True)
|
||||
self.check_status(job, "failed")
|
||||
jobs = RQJob.get_list({"filters": [["RQ Job", "status", "=", "failed"]]})
|
||||
jobs = frappe.get_all("RQ Job", {"status": "failed"})
|
||||
self.assertEqual(len(jobs), 1)
|
||||
self.assertTrue(jobs[0].exc_info)
|
||||
|
||||
# Assert that non-failed job still exists
|
||||
non_failed_jobs = RQJob.get_list({"filters": [["RQ Job", "status", "!=", "failed"]]})
|
||||
non_failed_jobs = frappe.get_all("RQ Job", {"status": ("!=", "failed")})
|
||||
self.assertGreaterEqual(len(non_failed_jobs), 1)
|
||||
|
||||
# Create a slow job and check if it's stuck in "Started"
|
||||
|
|
@ -174,7 +178,7 @@ class TestRQJob(FrappeTestCase):
|
|||
|
||||
jobs = [frappe.enqueue(method=self.BG_JOB, queue="short", fail=True) for _ in range(limit * 2)]
|
||||
self.check_status(jobs[-1], "failed")
|
||||
self.assertLessEqual(RQJob.get_count({"filters": [["RQ Job", "status", "=", "failed"]]}), limit * 1.1)
|
||||
self.assertLessEqual(RQJob.get_count(filters=[["RQ Job", "status", "=", "failed"]]), limit * 1.1)
|
||||
|
||||
|
||||
def test_func(fail=False, sleep=0):
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@
|
|||
"in_create": 1,
|
||||
"is_virtual": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-13 10:36:13.034784",
|
||||
"modified": "2024-02-29 19:31:08.502527",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "RQ Worker",
|
||||
|
|
@ -141,5 +141,6 @@
|
|||
"color": "Yellow",
|
||||
"title": "busy"
|
||||
}
|
||||
]
|
||||
],
|
||||
"title_field": "pid"
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ class RQWorker(Document):
|
|||
|
||||
def load_from_db(self):
|
||||
all_workers = get_workers()
|
||||
workers = [w for w in all_workers if w.pid == cint(self.name)]
|
||||
workers = [w for w in all_workers if w.name == self.name]
|
||||
if not workers:
|
||||
raise frappe.DoesNotExistError
|
||||
d = serialize_worker(workers[0])
|
||||
|
|
@ -46,22 +46,19 @@ class RQWorker(Document):
|
|||
super(Document, self).__init__(d)
|
||||
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
start = cint(args.get("start"))
|
||||
page_length = cint(args.get("page_length")) or 20
|
||||
|
||||
def get_list(start=0, page_length=20):
|
||||
workers = get_workers()
|
||||
|
||||
valid_workers = [w for w in workers if w.pid][start : start + page_length]
|
||||
return [serialize_worker(worker) for worker in valid_workers]
|
||||
|
||||
@staticmethod
|
||||
def get_count(args) -> int:
|
||||
def get_count() -> int:
|
||||
return len(get_workers())
|
||||
|
||||
# None of these methods apply to virtual workers, overriden for sanity.
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
def get_stats():
|
||||
return {}
|
||||
|
||||
def db_insert(self, *args, **kwargs):
|
||||
|
|
@ -85,7 +82,7 @@ def serialize_worker(worker: Worker) -> frappe._dict:
|
|||
current_job = None
|
||||
|
||||
return frappe._dict(
|
||||
name=worker.pid,
|
||||
name=worker.name,
|
||||
queue=queue,
|
||||
queue_type=queue_types,
|
||||
worker_name=worker.name,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ from frappe.tests.utils import FrappeTestCase
|
|||
|
||||
class TestRQWorker(FrappeTestCase):
|
||||
def test_get_worker_list(self):
|
||||
workers = RQWorker.get_list({})
|
||||
workers = RQWorker.get_list()
|
||||
self.assertGreaterEqual(len(workers), 1)
|
||||
self.assertTrue(any("short" in w.queue_type for w in workers))
|
||||
|
||||
def test_worker_serialization(self):
|
||||
workers = RQWorker.get_list({})
|
||||
frappe.get_doc("RQ Worker", workers[0].pid)
|
||||
workers = RQWorker.get_list()
|
||||
frappe.get_doc("RQ Worker", workers[0].name)
|
||||
|
|
|
|||
|
|
@ -49,14 +49,15 @@
|
|||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType"
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.script_type==='DocType Event'",
|
||||
"fieldname": "doctype_event",
|
||||
"fieldtype": "Select",
|
||||
"label": "DocType Event",
|
||||
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization\nOn Payment Paid\nOn Payment Failed"
|
||||
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Rename\nAfter Rename\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization\nOn Payment Paid\nOn Payment Failed"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.script_type==='API'",
|
||||
|
|
@ -106,7 +107,8 @@
|
|||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module (for export)",
|
||||
"options": "Module Def"
|
||||
"options": "Module Def",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.script_type==='API'",
|
||||
|
|
@ -149,7 +151,7 @@
|
|||
"link_fieldname": "server_script"
|
||||
}
|
||||
],
|
||||
"modified": "2024-02-06 07:09:45.478533",
|
||||
"modified": "2024-02-27 11:44:46.397495",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Server Script",
|
||||
|
|
@ -173,4 +175,4 @@
|
|||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,8 @@ class ServerScript(Document):
|
|||
"Before Save",
|
||||
"After Insert",
|
||||
"After Save",
|
||||
"Before Rename",
|
||||
"After Rename",
|
||||
"Before Submit",
|
||||
"After Submit",
|
||||
"Before Cancel",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ EVENT_MAP = {
|
|||
"before_validate": "Before Validate",
|
||||
"validate": "Before Save",
|
||||
"on_update": "After Save",
|
||||
"before_rename": "Before Rename",
|
||||
"after_rename": "After Rename",
|
||||
"before_submit": "Before Submit",
|
||||
"on_submit": "After Submit",
|
||||
"before_cancel": "Before Cancel",
|
||||
|
|
|
|||
|
|
@ -82,6 +82,26 @@ frappe.db.commit()
|
|||
disabled=1,
|
||||
script="""
|
||||
frappe.db.add_index("Todo", ["color", "date"])
|
||||
""",
|
||||
),
|
||||
dict(
|
||||
name="test_before_rename",
|
||||
script_type="DocType Event",
|
||||
doctype_event="After Rename",
|
||||
reference_doctype="Role",
|
||||
script="""
|
||||
doc.desk_access =0
|
||||
doc.save()
|
||||
""",
|
||||
),
|
||||
dict(
|
||||
name="test_after_rename",
|
||||
script_type="DocType Event",
|
||||
doctype_event="After Rename",
|
||||
reference_doctype="Role",
|
||||
script="""
|
||||
doc.disabled =1
|
||||
doc.save()
|
||||
""",
|
||||
),
|
||||
]
|
||||
|
|
@ -121,6 +141,12 @@ class TestServerScript(FrappeTestCase):
|
|||
frappe.ValidationError, frappe.get_doc(doctype="ToDo", description="validate me").insert
|
||||
)
|
||||
|
||||
role = frappe.get_doc(doctype="Role", role_name="_Test Role 9").insert(ignore_if_duplicate=True)
|
||||
role.rename("_Test Role 10")
|
||||
role.reload()
|
||||
self.assertEqual(role.disabled, 1)
|
||||
self.assertEqual(role.desk_access, 0)
|
||||
|
||||
def test_api(self):
|
||||
response = requests.post(get_site_url(frappe.local.site) + "/api/method/test_server_script")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@
|
|||
"disable_standard_email_footer",
|
||||
"hide_footer_in_auto_email_reports",
|
||||
"attach_view_link",
|
||||
"store_attached_pdf_document",
|
||||
"welcome_email_template",
|
||||
"reset_password_template",
|
||||
"files_tab",
|
||||
|
|
@ -648,12 +649,19 @@
|
|||
"fieldtype": "Int",
|
||||
"label": "Link Field Results Limit",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "When sending document using email, store the PDF on Communication. Warning: This can increase your storage usage.",
|
||||
"fieldname": "store_attached_pdf_document",
|
||||
"fieldtype": "Check",
|
||||
"label": "Store Attached PDF Document"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-26 11:29:20.924425",
|
||||
"modified": "2024-03-14 15:18:01.465057",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -87,9 +87,10 @@ class SystemSettings(Document):
|
|||
rounding_method: DF.Literal["Banker's Rounding (legacy)", "Banker's Rounding", "Commercial Rounding"]
|
||||
session_expiry: DF.Data | None
|
||||
setup_complete: DF.Check
|
||||
store_attached_pdf_document: DF.Check
|
||||
strip_exif_metadata_from_uploaded_images: DF.Check
|
||||
time_format: DF.Literal["HH:mm:ss", "HH:mm"]
|
||||
time_zone: DF.Literal
|
||||
time_zone: DF.Literal[None]
|
||||
two_factor_method: DF.Literal["OTP App", "SMS", "Email"]
|
||||
welcome_email_template: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
|
|
|||
|
|
@ -459,7 +459,7 @@ class TestUser(FrappeTestCase):
|
|||
|
||||
class TestImpersonation(FrappeAPITestCase):
|
||||
def test_impersonation(self):
|
||||
with test_user(roles=["System Manager"]) as user:
|
||||
with test_user(roles=["System Manager"], commit=True) as user:
|
||||
self.post(
|
||||
self.method_path("frappe.core.doctype.user.user.impersonate"),
|
||||
{"user": user.name, "reason": "test", "sid": self.sid},
|
||||
|
|
@ -469,7 +469,9 @@ class TestImpersonation(FrappeAPITestCase):
|
|||
|
||||
|
||||
@contextmanager
|
||||
def test_user(*, first_name: str | None = None, email: str | None = None, roles: list[str], **kwargs):
|
||||
def test_user(
|
||||
*, first_name: str | None = None, email: str | None = None, roles: list[str], commit=False, **kwargs
|
||||
):
|
||||
try:
|
||||
first_name = first_name or frappe.generate_hash()
|
||||
email = email or (first_name + "@example.com")
|
||||
|
|
@ -485,7 +487,7 @@ def test_user(*, first_name: str | None = None, email: str | None = None, roles:
|
|||
yield user
|
||||
finally:
|
||||
user.delete(force=True, ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
commit and frappe.db.commit()
|
||||
|
||||
|
||||
def delete_contact(user):
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from frappe.desk.doctype.notification_settings.notification_settings import (
|
|||
toggle_notifications,
|
||||
)
|
||||
from frappe.desk.notifications import clear_notifications
|
||||
from frappe.model.delete_doc import check_if_doc_is_linked
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.rate_limiter import rate_limit
|
||||
|
|
@ -587,6 +588,15 @@ class User(Document):
|
|||
frappe.db.delete("OAuth Authorization Code", {"user": self.name})
|
||||
frappe.db.delete("Token Cache", {"user": self.name})
|
||||
|
||||
# Delete EPS data
|
||||
frappe.db.delete("Energy Point Log", {"user": self.name})
|
||||
|
||||
# Ask user to disable instead if document is still linked
|
||||
try:
|
||||
check_if_doc_is_linked(self)
|
||||
except frappe.LinkExistsError:
|
||||
frappe.throw(_("You can disable the user instead of deleting it."), frappe.LinkExistsError)
|
||||
|
||||
def before_rename(self, old_name, new_name, merge=False):
|
||||
frappe.clear_cache(user=old_name)
|
||||
self.validate_rename(old_name, new_name)
|
||||
|
|
@ -841,7 +851,7 @@ def get_perm_info(role):
|
|||
return get_all_perms(role)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist(allow_guest=True, methods=["POST"])
|
||||
def update_password(
|
||||
new_password: str, logout_all_sessions: int = 0, key: str | None = None, old_password: str | None = None
|
||||
):
|
||||
|
|
@ -989,7 +999,7 @@ def reset_user_data(user):
|
|||
return user_doc, redirect_url
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def verify_password(password):
|
||||
frappe.local.login_manager.check_password(frappe.session.user, password)
|
||||
|
||||
|
|
@ -1045,7 +1055,7 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
|
|||
return 2, _("Please ask your administrator to verify your sign-up")
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist(allow_guest=True, methods=["POST"])
|
||||
@rate_limit(limit=get_password_reset_limit, seconds=60 * 60)
|
||||
def reset_password(user: str) -> str:
|
||||
try:
|
||||
|
|
@ -1311,7 +1321,7 @@ def get_restricted_ip_list(user):
|
|||
return [i.strip() for i in user.restrict_ip.split(",")]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def generate_keys(user: str):
|
||||
"""
|
||||
generate api key and api secret
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class UserType(Document):
|
|||
role: DF.Link | None
|
||||
select_doctypes: DF.Table[UserSelectDocumentType]
|
||||
user_doctypes: DF.Table[UserDocumentType]
|
||||
user_id_field: DF.Literal
|
||||
user_id_field: DF.Literal[None]
|
||||
user_type_modules: DF.Table[UserTypeModule]
|
||||
# end: auto-generated types
|
||||
|
||||
|
|
|
|||
|
|
@ -123,13 +123,15 @@ frappe.ui.form.on("Custom Field", {
|
|||
default: frm.doc.fieldname,
|
||||
},
|
||||
function (data) {
|
||||
frappe.call({
|
||||
method: "frappe.custom.doctype.custom_field.custom_field.rename_fieldname",
|
||||
args: {
|
||||
custom_field: frm.doc.name,
|
||||
fieldname: data.fieldname,
|
||||
},
|
||||
});
|
||||
frappe
|
||||
.xcall(
|
||||
"frappe.custom.doctype.custom_field.custom_field.rename_fieldname",
|
||||
{
|
||||
custom_field: frm.doc.name,
|
||||
fieldname: data.fieldname,
|
||||
}
|
||||
)
|
||||
.then(() => frm.reload());
|
||||
},
|
||||
__("Rename Fieldname"),
|
||||
__("Rename")
|
||||
|
|
|
|||
|
|
@ -362,7 +362,7 @@
|
|||
"label": "Ignore XSS Filter"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"default": "0",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
|
|
@ -464,7 +464,7 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-21 18:15:19.384933",
|
||||
"modified": "2024-03-07 17:34:47.167183",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class CustomField(Document):
|
|||
in_list_view: DF.Check
|
||||
in_preview: DF.Check
|
||||
in_standard_filter: DF.Check
|
||||
insert_after: DF.Literal
|
||||
insert_after: DF.Literal[None]
|
||||
is_system_generated: DF.Check
|
||||
is_virtual: DF.Check
|
||||
label: DF.Data | None
|
||||
|
|
@ -370,6 +370,7 @@ def rename_fieldname(custom_field: str, fieldname: str):
|
|||
field.db_set("fieldname", field.fieldname, notify=True)
|
||||
_update_fieldname_references(field, old_fieldname, new_fieldname)
|
||||
|
||||
frappe.msgprint(_("Custom field renamed to {0} successfully.").format(fieldname), alert=True)
|
||||
frappe.db.commit()
|
||||
frappe.clear_cache()
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class CustomizeForm(Document):
|
|||
autoname: DF.Data | None
|
||||
default_email_template: DF.Link | None
|
||||
default_print_format: DF.Link | None
|
||||
default_view: DF.Literal
|
||||
default_view: DF.Literal[None]
|
||||
doc_type: DF.Link | None
|
||||
editable_grid: DF.Check
|
||||
email_append_to: DF.Check
|
||||
|
|
@ -75,7 +75,7 @@ class CustomizeForm(Document):
|
|||
sender_name_field: DF.Data | None
|
||||
show_preview_popup: DF.Check
|
||||
show_title_field_in_link: DF.Check
|
||||
sort_field: DF.Literal
|
||||
sort_field: DF.Literal[None]
|
||||
sort_order: DF.Literal["ASC", "DESC"]
|
||||
states: DF.Table[DocTypeState]
|
||||
subject_field: DF.Data | None
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class DocTypeLayoutField(Document):
|
|||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
fieldname: DF.Literal
|
||||
fieldname: DF.Literal[None]
|
||||
label: DF.Data | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
from contextlib import suppress
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""Remove invalid fetch from expressions"""
|
||||
with suppress(Exception):
|
||||
property_setters = frappe.get_all(
|
||||
"Property Setter", {"doctype_or_field": "DocField", "property": "fetch_from"}, ["name", "value"]
|
||||
)
|
||||
for ps in property_setters:
|
||||
if not is_valid_expression(ps.value):
|
||||
frappe.db.delete("Property Setter", {"name": ps.name})
|
||||
|
||||
custom_fields = frappe.get_all("Custom Field", {"fetch_from": ("is", "set")}, ["name", "fetch_from"])
|
||||
for cf in custom_fields:
|
||||
if not is_valid_expression(cf.fetch_from):
|
||||
frappe.db.set_value("Custom Field", cf.name, "fetch_from", "")
|
||||
|
||||
|
||||
def is_valid_expression(expr) -> bool:
|
||||
if not expr or "." not in expr:
|
||||
return False
|
||||
source_field, target_field = expr.split(".", maxsplit=1)
|
||||
if not source_field or not target_field:
|
||||
return False
|
||||
return True
|
||||
|
|
@ -184,7 +184,7 @@ class Database:
|
|||
|
||||
"""
|
||||
if isinstance(query, MySQLQueryBuilder | PostgreSQLQueryBuilder):
|
||||
frappe.errprint("Use run method to execute SQL queries generated by Query Engine")
|
||||
frappe.log("Use run method to execute SQL queries generated by Query Engine")
|
||||
|
||||
debug = debug or getattr(self, "debug", False)
|
||||
query = str(query)
|
||||
|
|
@ -224,7 +224,7 @@ class Database:
|
|||
self._cursor.execute(query, values)
|
||||
except Exception as e:
|
||||
if self.is_syntax_error(e):
|
||||
frappe.errprint(f"Syntax error in query:\n{query} {values or ''}")
|
||||
frappe.log(f"Syntax error in query:\n{query} {values or ''}")
|
||||
|
||||
elif self.is_deadlocked(e):
|
||||
raise frappe.QueryDeadlockError(e) from e
|
||||
|
|
@ -244,13 +244,13 @@ class Database:
|
|||
# TODO: added temporarily
|
||||
elif self.db_type == "postgres":
|
||||
traceback.print_stack()
|
||||
frappe.errprint(f"Error in query:\n{e}")
|
||||
frappe.log(f"Error in query:\n{e}")
|
||||
raise
|
||||
|
||||
elif isinstance(e, self.ProgrammingError):
|
||||
if frappe.conf.developer_mode:
|
||||
traceback.print_stack()
|
||||
frappe.errprint(f"Error in query:\n{query, values}")
|
||||
frappe.log(f"Error in query:\n{query, values}")
|
||||
raise
|
||||
|
||||
if not (
|
||||
|
|
@ -261,7 +261,7 @@ class Database:
|
|||
|
||||
if debug:
|
||||
time_end = time()
|
||||
frappe.errprint(f"Execution time: {time_end - time_start:.2f} sec")
|
||||
frappe.log(f"Execution time: {time_end - time_start:.2f} sec")
|
||||
|
||||
self.log_query(query, values, debug, explain)
|
||||
|
||||
|
|
@ -333,7 +333,7 @@ class Database:
|
|||
_query = _query or str(mogrified_query)
|
||||
if explain and is_query_type(_query, "select"):
|
||||
self.explain_query(_query)
|
||||
frappe.errprint(_query)
|
||||
frappe.log(_query)
|
||||
|
||||
if frappe.conf.logging == 2:
|
||||
_query = _query or str(mogrified_query)
|
||||
|
|
@ -381,14 +381,14 @@ class Database:
|
|||
|
||||
def explain_query(self, query, values=None):
|
||||
"""Print `EXPLAIN` in error log."""
|
||||
frappe.errprint("--- query explain ---")
|
||||
frappe.log("--- query explain ---")
|
||||
try:
|
||||
self._cursor.execute(f"EXPLAIN {query}", values)
|
||||
except Exception as e:
|
||||
frappe.errprint(f"error in query explain: {e}")
|
||||
frappe.log(f"error in query explain: {e}")
|
||||
else:
|
||||
frappe.errprint(json.dumps(self.fetch_as_dict(), indent=1))
|
||||
frappe.errprint("--- query explain end ---")
|
||||
frappe.log(json.dumps(self.fetch_as_dict(), indent=1))
|
||||
frappe.log("--- query explain end ---")
|
||||
|
||||
def sql_list(self, query, values=(), debug=False, **kwargs):
|
||||
"""Return data as list of single elements (first column).
|
||||
|
|
@ -474,6 +474,7 @@ class Database:
|
|||
pluck=False,
|
||||
distinct=False,
|
||||
skip_locked=False,
|
||||
wait=True,
|
||||
):
|
||||
"""Return a document property or list of properties.
|
||||
|
||||
|
|
@ -488,6 +489,7 @@ class Database:
|
|||
:param pluck: pluck first column instead of returning as nested list or dict.
|
||||
:param for_update: All the affected/read rows will be locked.
|
||||
:param skip_locked: Skip selecting currently locked rows.
|
||||
:param wait: Wait for aquiring lock
|
||||
|
||||
Example:
|
||||
|
||||
|
|
@ -519,6 +521,7 @@ class Database:
|
|||
distinct=distinct,
|
||||
limit=1,
|
||||
skip_locked=skip_locked,
|
||||
wait=wait,
|
||||
)
|
||||
|
||||
if not run:
|
||||
|
|
@ -552,6 +555,7 @@ class Database:
|
|||
distinct=False,
|
||||
limit=None,
|
||||
skip_locked=False,
|
||||
wait=True,
|
||||
):
|
||||
"""Return multiple document properties.
|
||||
|
||||
|
|
@ -592,6 +596,7 @@ class Database:
|
|||
limit=limit,
|
||||
as_dict=as_dict,
|
||||
skip_locked=skip_locked,
|
||||
wait=True,
|
||||
for_update=for_update,
|
||||
)
|
||||
|
||||
|
|
@ -619,6 +624,7 @@ class Database:
|
|||
limit=limit,
|
||||
for_update=for_update,
|
||||
skip_locked=skip_locked,
|
||||
wait=wait,
|
||||
)
|
||||
except Exception as e:
|
||||
if ignore and (
|
||||
|
|
@ -856,6 +862,7 @@ class Database:
|
|||
update=None,
|
||||
for_update=False,
|
||||
skip_locked=False,
|
||||
wait=True,
|
||||
run=True,
|
||||
pluck=False,
|
||||
distinct=False,
|
||||
|
|
@ -867,6 +874,7 @@ class Database:
|
|||
order_by=order_by,
|
||||
for_update=for_update,
|
||||
skip_locked=skip_locked,
|
||||
wait=wait,
|
||||
fields=fields,
|
||||
distinct=distinct,
|
||||
limit=limit,
|
||||
|
|
@ -892,6 +900,7 @@ class Database:
|
|||
as_dict=False,
|
||||
for_update=False,
|
||||
skip_locked=False,
|
||||
wait=True,
|
||||
):
|
||||
if names := list(filter(None, names)):
|
||||
return frappe.qb.get_query(
|
||||
|
|
@ -904,6 +913,7 @@ class Database:
|
|||
validate_filters=True,
|
||||
for_update=for_update,
|
||||
skip_locked=skip_locked,
|
||||
wait=wait,
|
||||
).run(debug=debug, run=run, as_dict=as_dict, pluck=pluck)
|
||||
return {}
|
||||
|
||||
|
|
|
|||
|
|
@ -437,7 +437,6 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
|
|||
db_table = MariaDBTable(doctype, meta)
|
||||
db_table.validate()
|
||||
|
||||
self.commit()
|
||||
db_table.sync()
|
||||
self.commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ CREATE TABLE `tabDocType` (
|
|||
`idx` int(8) NOT NULL DEFAULT 0,
|
||||
`search_fields` varchar(255) DEFAULT NULL,
|
||||
`issingle` int(1) NOT NULL DEFAULT 0,
|
||||
`is_virtual` int(1) NOT NULL DEFAULT 0,
|
||||
`is_tree` int(1) NOT NULL DEFAULT 0,
|
||||
`istable` int(1) NOT NULL DEFAULT 0,
|
||||
`editable_grid` int(1) NOT NULL DEFAULT 1,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class MariaDBTable(DBTable):
|
|||
CHARACTER SET=utf8mb4
|
||||
COLLATE=utf8mb4_unicode_ci"""
|
||||
|
||||
frappe.db.sql(query)
|
||||
frappe.db.sql_ddl(query)
|
||||
|
||||
def alter(self):
|
||||
for col in self.columns.values():
|
||||
|
|
|
|||
|
|
@ -12,7 +12,12 @@ from psycopg2.errorcodes import (
|
|||
UNDEFINED_TABLE,
|
||||
UNIQUE_VIOLATION,
|
||||
)
|
||||
from psycopg2.errors import ReadOnlySqlTransaction, SequenceGeneratorLimitExceeded, SyntaxError
|
||||
from psycopg2.errors import (
|
||||
LockNotAvailable,
|
||||
ReadOnlySqlTransaction,
|
||||
SequenceGeneratorLimitExceeded,
|
||||
SyntaxError,
|
||||
)
|
||||
from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
|
||||
|
||||
import frappe
|
||||
|
|
@ -53,7 +58,7 @@ class PostgresExceptionUtil:
|
|||
@staticmethod
|
||||
def is_timedout(e):
|
||||
# http://initd.org/psycopg/docs/extensions.html?highlight=datatype#psycopg2.extensions.QueryCanceledError
|
||||
return isinstance(e, psycopg2.extensions.QueryCanceledError)
|
||||
return isinstance(e, (psycopg2.extensions.QueryCanceledError | LockNotAvailable))
|
||||
|
||||
@staticmethod
|
||||
def is_read_only_mode_error(e) -> bool:
|
||||
|
|
@ -323,7 +328,6 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
|
|||
db_table = PostgresTable(doctype, meta)
|
||||
db_table.validate()
|
||||
|
||||
self.commit()
|
||||
db_table.sync()
|
||||
self.commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ CREATE TABLE "tabDocType" (
|
|||
"idx" bigint NOT NULL DEFAULT 0,
|
||||
"search_fields" varchar(255) DEFAULT NULL,
|
||||
"issingle" smallint NOT NULL DEFAULT 0,
|
||||
"is_virtual" smallint NOT NULL DEFAULT 0,
|
||||
"is_tree" smallint NOT NULL DEFAULT 0,
|
||||
"istable" smallint NOT NULL DEFAULT 0,
|
||||
"editable_grid" smallint NOT NULL DEFAULT 1,
|
||||
|
|
|
|||
|
|
@ -19,10 +19,9 @@ def setup_database():
|
|||
root_conn.sql(f"CREATE USER \"{frappe.conf.db_user}\" WITH PASSWORD '{frappe.conf.db_password}'")
|
||||
root_conn.sql(f'CREATE DATABASE "{frappe.conf.db_name}"')
|
||||
root_conn.sql(f'GRANT ALL PRIVILEGES ON DATABASE "{frappe.conf.db_name}" TO "{frappe.conf.db_user}"')
|
||||
if psql_version := root_conn.sql("SELECT VERSION()", as_dict=True):
|
||||
version_string = psql_version[0].get("version") or "PostgreSQL 14"
|
||||
major_version = cint(re.split(r"[\w\.]", version_string)[1])
|
||||
if major_version > 15:
|
||||
if psql_version := root_conn.sql("SHOW server_version_num", as_dict=True):
|
||||
semver_version_num = psql_version[0].get("server_version_num") or "140000"
|
||||
if cint(semver_version_num) > 150000:
|
||||
root_conn.sql(f'ALTER DATABASE "{frappe.conf.db_name}" OWNER TO "{frappe.conf.db_user}"')
|
||||
root_conn.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class Engine:
|
|||
*,
|
||||
validate_filters: bool = False,
|
||||
skip_locked: bool = False,
|
||||
wait: bool = True,
|
||||
) -> QueryBuilder:
|
||||
self.is_mariadb = frappe.db.db_type == "mariadb"
|
||||
self.is_postgres = frappe.db.db_type == "postgres"
|
||||
|
|
@ -84,7 +85,7 @@ class Engine:
|
|||
self.query = self.query.distinct()
|
||||
|
||||
if for_update:
|
||||
self.query = self.query.for_update(skip_locked=skip_locked)
|
||||
self.query = self.query.for_update(skip_locked=skip_locked, nowait=not wait)
|
||||
|
||||
if group_by:
|
||||
self.query = self.query.groupby(group_by)
|
||||
|
|
|
|||
|
|
@ -338,6 +338,7 @@ class Workspace:
|
|||
for doc in onboarding_doc.get_steps():
|
||||
step = doc.as_dict().copy()
|
||||
step.label = _(doc.title)
|
||||
step.description = _(doc.description)
|
||||
if step.action == "Create Entry":
|
||||
step.is_submittable = frappe.db.get_value(
|
||||
"DocType", step.reference_document, "is_submittable", cache=True
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class BulkUpdate(Document):
|
|||
|
||||
condition: DF.SmallText | None
|
||||
document_type: DF.Link
|
||||
field: DF.Literal
|
||||
field: DF.Literal[None]
|
||||
limit: DF.Int
|
||||
update_value: DF.SmallText
|
||||
# end: auto-generated types
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ class CalendarView(Document):
|
|||
from frappe.types import DF
|
||||
|
||||
all_day: DF.Check
|
||||
end_date_field: DF.Literal
|
||||
end_date_field: DF.Literal[None]
|
||||
reference_doctype: DF.Link
|
||||
start_date_field: DF.Literal
|
||||
subject_field: DF.Literal
|
||||
start_date_field: DF.Literal[None]
|
||||
subject_field: DF.Literal[None]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-19 13:02:56.332137",
|
||||
"modified": "2024-03-12 20:35:43.921009",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Console Log",
|
||||
|
|
@ -44,7 +44,6 @@
|
|||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
@ -19,4 +19,6 @@ class ConsoleLog(Document):
|
|||
type: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
def after_delete(self):
|
||||
# because on_trash can be bypassed
|
||||
frappe.throw(frappe._("Console Logs can not be deleted"))
|
||||
|
|
|
|||
|
|
@ -335,8 +335,8 @@ class DashboardChart(Document):
|
|||
from frappe.desk.doctype.dashboard_chart_field.dashboard_chart_field import DashboardChartField
|
||||
from frappe.types import DF
|
||||
|
||||
aggregate_function_based_on: DF.Literal
|
||||
based_on: DF.Literal
|
||||
aggregate_function_based_on: DF.Literal[None]
|
||||
based_on: DF.Literal[None]
|
||||
chart_name: DF.Data
|
||||
chart_type: DF.Literal["Count", "Sum", "Average", "Group By", "Custom", "Report"]
|
||||
color: DF.Color | None
|
||||
|
|
@ -345,9 +345,9 @@ class DashboardChart(Document):
|
|||
dynamic_filters_json: DF.Code | None
|
||||
filters_json: DF.Code
|
||||
from_date: DF.Date | None
|
||||
group_by_based_on: DF.Literal
|
||||
group_by_based_on: DF.Literal[None]
|
||||
group_by_type: DF.Literal["Count", "Sum", "Average"]
|
||||
heatmap_year: DF.Literal
|
||||
heatmap_year: DF.Literal[None]
|
||||
is_public: DF.Check
|
||||
is_standard: DF.Check
|
||||
last_synced_on: DF.Datetime | None
|
||||
|
|
@ -363,8 +363,8 @@ class DashboardChart(Document):
|
|||
to_date: DF.Date | None
|
||||
type: DF.Literal["Line", "Bar", "Percentage", "Pie", "Donut", "Heatmap"]
|
||||
use_report_chart: DF.Check
|
||||
value_based_on: DF.Literal
|
||||
x_field: DF.Literal
|
||||
value_based_on: DF.Literal[None]
|
||||
x_field: DF.Literal[None]
|
||||
y_axis: DF.Table[DashboardChartField]
|
||||
# end: auto-generated types
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class DashboardChartField(Document):
|
|||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
y_field: DF.Literal
|
||||
y_field: DF.Literal[None]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules import get_module_path, scrub
|
||||
from frappe.modules.export_file import export_to_files
|
||||
|
||||
FOLDER_NAME = "dashboard_chart_source"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_config(name):
|
||||
doc = frappe.get_doc("Dashboard Chart Source", name)
|
||||
with open(
|
||||
os.path.join(
|
||||
get_module_path(doc.module), "dashboard_chart_source", scrub(doc.name), scrub(doc.name) + ".js"
|
||||
),
|
||||
) as f:
|
||||
return f.read()
|
||||
def get_config(name: str) -> str:
|
||||
doc: "DashboardChartSource" = frappe.get_doc("Dashboard Chart Source", name)
|
||||
return doc.read_config()
|
||||
|
||||
|
||||
class DashboardChartSource(Document):
|
||||
|
|
@ -35,4 +34,31 @@ class DashboardChartSource(Document):
|
|||
# end: auto-generated types
|
||||
|
||||
def on_update(self):
|
||||
if not frappe.request:
|
||||
return
|
||||
|
||||
if not frappe.conf.developer_mode:
|
||||
frappe.throw(_("Creation of this document is only permitted in developer mode."))
|
||||
|
||||
export_to_files(record_list=[[self.doctype, self.name]], record_module=self.module, create_init=True)
|
||||
|
||||
def on_trash(self):
|
||||
if not frappe.conf.developer_mode and not frappe.flags.in_migrate:
|
||||
frappe.throw(_("Deletion of this document is only permitted in developer mode."))
|
||||
|
||||
frappe.db.after_commit.add(self.delete_folder)
|
||||
|
||||
def read_config(self) -> str:
|
||||
"""Return the config JS file for this dashboard chart source."""
|
||||
config_path = self.get_folder_path() / f"{scrub(self.name)}.js"
|
||||
return config_path.read_text() if config_path.exists() else ""
|
||||
|
||||
def delete_folder(self):
|
||||
"""Delete the folder for this dashboard chart source."""
|
||||
path = self.get_folder_path()
|
||||
if path.exists():
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
def get_folder_path(self) -> Path:
|
||||
"""Return the path of the folder for this dashboard chart source."""
|
||||
return Path(get_module_path(self.module)) / FOLDER_NAME / frappe.scrub(self.name)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class FormTourStep(Document):
|
|||
child_doctype: DF.Data | None
|
||||
description: DF.HTMLEditor
|
||||
element_selector: DF.Data | None
|
||||
fieldname: DF.Literal
|
||||
fieldname: DF.Literal[None]
|
||||
fieldtype: DF.Data | None
|
||||
has_next_condition: DF.Check
|
||||
hide_buttons: DF.Check
|
||||
|
|
@ -32,7 +32,7 @@ class FormTourStep(Document):
|
|||
ondemand_description: DF.HTMLEditor | None
|
||||
parent: DF.Data
|
||||
parent_element_selector: DF.Data | None
|
||||
parent_fieldname: DF.Literal
|
||||
parent_fieldname: DF.Literal[None]
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
popover_element: DF.Check
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class KanbanBoard(Document):
|
|||
from frappe.types import DF
|
||||
|
||||
columns: DF.Table[KanbanBoardColumn]
|
||||
field_name: DF.Literal
|
||||
field_name: DF.Literal[None]
|
||||
fields: DF.Code | None
|
||||
filters: DF.Code | None
|
||||
kanban_board_name: DF.Data
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class NumberCard(Document):
|
|||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
aggregate_function_based_on: DF.Literal
|
||||
aggregate_function_based_on: DF.Literal[None]
|
||||
color: DF.Color | None
|
||||
document_type: DF.Link | None
|
||||
dynamic_filters_json: DF.Code | None
|
||||
|
|
@ -35,7 +35,7 @@ class NumberCard(Document):
|
|||
method: DF.Data | None
|
||||
module: DF.Link | None
|
||||
parent_document_type: DF.Link | None
|
||||
report_field: DF.Literal
|
||||
report_field: DF.Literal[None]
|
||||
report_function: DF.Literal["Sum", "Average", "Minimum", "Maximum"]
|
||||
report_name: DF.Link | None
|
||||
show_percentage_stats: DF.Check
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class OnboardingStep(Document):
|
|||
callback_message: DF.SmallText | None
|
||||
callback_title: DF.Data | None
|
||||
description: DF.MarkdownEditor | None
|
||||
field: DF.Literal
|
||||
field: DF.Literal[None]
|
||||
form_tour: DF.Link | None
|
||||
intro_video_url: DF.Data | None
|
||||
is_complete: DF.Check
|
||||
|
|
|
|||
|
|
@ -57,7 +57,10 @@ def execute_code(doc):
|
|||
@frappe.whitelist()
|
||||
def show_processlist():
|
||||
frappe.only_for("System Manager")
|
||||
return _show_processlist()
|
||||
|
||||
|
||||
def _show_processlist():
|
||||
return frappe.db.multisql(
|
||||
{
|
||||
"postgres": """
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ def update_page(name, title, icon, indicator_color, parent, public):
|
|||
)
|
||||
|
||||
if doc:
|
||||
child_docs = frappe.get_all("Workspace", filters={"parent_page": doc.title, "public": doc.public})
|
||||
doc.title = title
|
||||
doc.icon = icon
|
||||
doc.indicator_color = indicator_color
|
||||
|
|
@ -314,7 +315,6 @@ def update_page(name, title, icon, indicator_color, parent, public):
|
|||
rename_doc("Workspace", name, new_name, force=True, ignore_permissions=True)
|
||||
|
||||
# update new name and public in child pages
|
||||
child_docs = frappe.get_all("Workspace", filters={"parent_page": doc.title, "public": doc.public})
|
||||
if child_docs:
|
||||
for child in child_docs:
|
||||
child_doc = frappe.get_doc("Workspace", child.name)
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ def get_point_logs(doctype, docname):
|
|||
def _get_communications(doctype, name, start=0, limit=20):
|
||||
communications = get_communication_data(doctype, name, start, limit)
|
||||
for c in communications:
|
||||
if c.communication_type == "Communication":
|
||||
if c.communication_type in ("Communication", "Automated Message"):
|
||||
c.attachments = json.dumps(
|
||||
frappe.get_all(
|
||||
"File",
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ def build_xlsx_data(data, visible_idx, include_indentation, include_filters=Fals
|
|||
datetime.timedelta,
|
||||
)
|
||||
|
||||
if len(visible_idx) == len(data.result):
|
||||
if len(visible_idx) == len(data.result) or not visible_idx:
|
||||
# It's not possible to have same length and different content.
|
||||
ignore_visible_idx = True
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ from frappe.model import child_table_fields, default_fields, get_permitted_field
|
|||
from frappe.model.base_document import get_controller
|
||||
from frappe.model.db_query import DatabaseQuery
|
||||
from frappe.model.utils import is_virtual_doctype
|
||||
from frappe.utils import add_user_info, format_duration
|
||||
from frappe.utils import add_user_info, cint, format_duration
|
||||
from frappe.utils.data import sbool
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -23,7 +24,7 @@ def get():
|
|||
# If virtual doctype, get data from controller get_list method
|
||||
if is_virtual_doctype(args.doctype):
|
||||
controller = get_controller(args.doctype)
|
||||
data = compress(controller.get_list(args))
|
||||
data = compress(frappe.call(controller.get_list, args=args, **args))
|
||||
else:
|
||||
data = compress(execute(**args), args=args)
|
||||
return data
|
||||
|
|
@ -36,7 +37,7 @@ def get_list():
|
|||
|
||||
if is_virtual_doctype(args.doctype):
|
||||
controller = get_controller(args.doctype)
|
||||
data = controller.get_list(args)
|
||||
data = frappe.call(controller.get_list, args=args, **args)
|
||||
else:
|
||||
# uncompressed (refactored from frappe.model.db_query.get_list)
|
||||
data = execute(**args)
|
||||
|
|
@ -51,13 +52,23 @@ def get_count() -> int:
|
|||
|
||||
if is_virtual_doctype(args.doctype):
|
||||
controller = get_controller(args.doctype)
|
||||
data = controller.get_count(args)
|
||||
count = frappe.call(controller.get_count, args=args, **args)
|
||||
else:
|
||||
distinct = "distinct " if args.distinct == "true" else ""
|
||||
args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"]
|
||||
data = execute(**args)[0].get("total_count")
|
||||
args.distinct = sbool(args.distinct)
|
||||
distinct = "distinct " if args.distinct else ""
|
||||
args.limit = cint(args.limit)
|
||||
fieldname = f"{distinct}`tab{args.doctype}`.name"
|
||||
args.order_by = None
|
||||
|
||||
return data
|
||||
if args.limit:
|
||||
args.fields = [fieldname]
|
||||
partial_query = execute(**args, run=0)
|
||||
count = frappe.db.sql(f"""select count(*) from ( {partial_query} ) p""")[0][0]
|
||||
else:
|
||||
args.fields = [f"count({fieldname}) as total_count"]
|
||||
count = execute(**args)[0].get("total_count")
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def execute(doctype, *args, **kwargs):
|
||||
|
|
@ -227,6 +238,10 @@ def parse_json(data):
|
|||
data["save_user_settings"] = json.loads(data["save_user_settings"])
|
||||
else:
|
||||
data["save_user_settings"] = True
|
||||
if isinstance(data.get("start"), str):
|
||||
data["start"] = cint(data.get("start"))
|
||||
if isinstance(data.get("page_length"), str):
|
||||
data["page_length"] = cint(data.get("page_length"))
|
||||
|
||||
|
||||
def get_parenttype_and_fieldname(field, data):
|
||||
|
|
@ -509,7 +524,7 @@ def get_sidebar_stats(stats, doctype, filters=None):
|
|||
if is_virtual_doctype(doctype):
|
||||
controller = get_controller(doctype)
|
||||
args = {"stats": stats, "filters": filters}
|
||||
data = controller.get_stats(args)
|
||||
data = frappe.call(controller.get_stats, args=args, **args)
|
||||
else:
|
||||
data = get_stats(stats, doctype, filters)
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,8 @@ def build_for_autosuggest(res: list[tuple], doctype: str) -> list[LinkSearchResu
|
|||
if meta.show_title_field_in_link:
|
||||
for item in res:
|
||||
item = list(item)
|
||||
if len(item) == 1:
|
||||
item = [item[0], item[0]]
|
||||
label = item[1] # use title as label
|
||||
item[1] = item[0] # show name in description instead of title
|
||||
if len(item) >= 3 and item[2] == label:
|
||||
|
|
|
|||
|
|
@ -53,14 +53,14 @@ class AutoEmailReport(Document):
|
|||
filters: DF.Text | None
|
||||
format: DF.Literal["HTML", "XLSX", "CSV"]
|
||||
frequency: DF.Literal["Daily", "Weekdays", "Weekly", "Monthly"]
|
||||
from_date_field: DF.Literal
|
||||
from_date_field: DF.Literal[None]
|
||||
no_of_rows: DF.Int
|
||||
reference_report: DF.Data | None
|
||||
report: DF.Link
|
||||
report_type: DF.ReadOnly | None
|
||||
send_if_data: DF.Check
|
||||
sender: DF.Link | None
|
||||
to_date_field: DF.Literal
|
||||
to_date_field: DF.Literal[None]
|
||||
use_first_day_of_period: DF.Check
|
||||
user: DF.Link
|
||||
# end: auto-generated types
|
||||
|
|
@ -345,7 +345,9 @@ def make_links(columns, data):
|
|||
if col.options and row.get(col.options):
|
||||
row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname])
|
||||
elif col.fieldtype == "Currency":
|
||||
doc = frappe.get_doc(col.parent, doc_name) if doc_name and col.get("parent") else None
|
||||
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)
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -386,11 +386,33 @@ class SendMailContext:
|
|||
elif attachment.get("print_format_attachment") == 1:
|
||||
attachment.pop("print_format_attachment", None)
|
||||
print_format_file = frappe.attach_print(**attachment)
|
||||
self._store_file(print_format_file["fname"], print_format_file["fcontent"])
|
||||
print_format_file.update({"parent": message_obj})
|
||||
add_attachment(**print_format_file)
|
||||
|
||||
return safe_encode(message_obj.as_string())
|
||||
|
||||
def _store_file(self, file_name, content):
|
||||
if not frappe.get_system_settings("store_attached_pdf_document"):
|
||||
return
|
||||
|
||||
file_data = frappe._dict(file_name=file_name, is_private=1)
|
||||
|
||||
# Store on communication if available, else email queue doc
|
||||
if self.queue_doc.communication:
|
||||
file_data.attached_to_doctype = "Communication"
|
||||
file_data.attached_to_name = self.queue_doc.communication
|
||||
else:
|
||||
file_data.attached_to_doctype = self.queue_doc.doctype
|
||||
file_data.attached_to_name = self.queue_doc.name
|
||||
|
||||
if frappe.db.exists("File", file_data):
|
||||
return
|
||||
|
||||
file = frappe.new_doc("File", **file_data)
|
||||
file.content = content
|
||||
file.insert()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def bulk_retry(queues):
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class Notification(Document):
|
|||
attach_print: DF.Check
|
||||
channel: DF.Literal["Email", "Slack", "System Notification", "SMS"]
|
||||
condition: DF.Code | None
|
||||
date_changed: DF.Literal
|
||||
date_changed: DF.Literal[None]
|
||||
days_in_advance: DF.Int
|
||||
document_type: DF.Link
|
||||
enabled: DF.Check
|
||||
|
|
@ -62,10 +62,10 @@ class Notification(Document):
|
|||
send_to_all_assignees: DF.Check
|
||||
sender: DF.Link | None
|
||||
sender_email: DF.Data | None
|
||||
set_property_after_alert: DF.Literal
|
||||
set_property_after_alert: DF.Literal[None]
|
||||
slack_webhook_url: DF.Link | None
|
||||
subject: DF.Data | None
|
||||
value_changed: DF.Literal
|
||||
value_changed: DF.Literal[None]
|
||||
# end: auto-generated types
|
||||
|
||||
def onload(self):
|
||||
|
|
@ -282,6 +282,9 @@ def get_context(context):
|
|||
bcc=bcc,
|
||||
communication_type="Automated Message",
|
||||
).get("name")
|
||||
# set the outgoing email account because we did in fact send it via sendmail above
|
||||
comm = frappe.get_doc("Communication", communication)
|
||||
comm.get_outgoing_email_account()
|
||||
|
||||
frappe.sendmail(
|
||||
recipients=recipients,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class NotificationRecipient(Document):
|
|||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
receiver_by_document_field: DF.Literal
|
||||
receiver_by_document_field: DF.Literal[None]
|
||||
receiver_by_role: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
|
|
|
|||
|
|
@ -187,7 +187,8 @@ def upload_file():
|
|||
optimize = frappe.form_dict.optimize
|
||||
content = None
|
||||
|
||||
if frappe.form_dict.get("library_file_name", False):
|
||||
if library_file := frappe.form_dict.get("library_file_name"):
|
||||
frappe.has_permission("File", doc=library_file, throw=True)
|
||||
doc = frappe.get_value(
|
||||
"File",
|
||||
frappe.form_dict.library_file_name,
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ doc_events = {
|
|||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
|
||||
"frappe.automation.doctype.assignment_rule.assignment_rule.apply",
|
||||
"frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date",
|
||||
"frappe.core.doctype.file.utils.attach_files_to_document",
|
||||
],
|
||||
"on_change": [
|
||||
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points",
|
||||
|
|
@ -413,6 +414,8 @@ ignore_links_on_delete = [
|
|||
"Unhandled Email",
|
||||
"Webhook Request Log",
|
||||
"Workspace",
|
||||
"Route History",
|
||||
"Access Log",
|
||||
]
|
||||
|
||||
# Request Hooks
|
||||
|
|
@ -438,7 +441,7 @@ after_job = [
|
|||
"frappe.recorder.dump",
|
||||
"frappe.monitor.stop",
|
||||
"frappe.utils.file_lock.release_document_locks",
|
||||
"frappe.utils.telemetry.flush",
|
||||
"frappe.utils.background_jobs.flush_telemetry",
|
||||
]
|
||||
|
||||
extend_bootinfo = [
|
||||
|
|
|
|||
|
|
@ -374,6 +374,14 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
|
|||
click.secho(f"App {app_name} not installed on Site {site}", fg="yellow")
|
||||
return
|
||||
|
||||
# Don't allow uninstalling if we have dependent apps installed
|
||||
for app in frappe.get_installed_apps():
|
||||
if app != app_name:
|
||||
hooks = frappe.get_hooks(app_name=app)
|
||||
if hooks.required_apps and any(app_name in required_app for required_app in hooks.required_apps):
|
||||
click.secho(f"App {app_name} is a dependency of {app}. Uninstall {app} first.", fg="yellow")
|
||||
return
|
||||
|
||||
print(f"Uninstalling App {app_name} from Site {site}...")
|
||||
|
||||
if not dry_run and not yes:
|
||||
|
|
@ -783,7 +791,7 @@ def is_downgrade(sql_file_path, verbose=False):
|
|||
is_downgrade = backup_version > current_version
|
||||
|
||||
if verbose and is_downgrade:
|
||||
print(f"Your site will be downgraded from Frappe {current_version} to {backup_version}")
|
||||
print(f"Your site is currently on Frappe {current_version} and your backup is {backup_version}.")
|
||||
|
||||
return is_downgrade
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2024, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Push Notification Settings", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"beta": 1,
|
||||
"creation": "2024-01-04 11:36:08.013039",
|
||||
"description": "Enabling this will register your site on a central relay server to send push notifications for all installed apps through Firebase Cloud Messaging. This server only stores user tokens and error logs, and no messages are saved.",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_qgjr",
|
||||
"enable_push_notification_relay",
|
||||
"authentication_credential_section",
|
||||
"api_key",
|
||||
"api_secret"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_push_notification_relay",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Push Notification Relay"
|
||||
},
|
||||
{
|
||||
"description": "API Key and Secret to interact with the relay server. These will be auto-generated when the first push notification is sent from any of the apps installed on this site.",
|
||||
"fieldname": "authentication_credential_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Authentication"
|
||||
},
|
||||
{
|
||||
"fieldname": "api_key",
|
||||
"fieldtype": "Data",
|
||||
"label": "API Key"
|
||||
},
|
||||
{
|
||||
"fieldname": "api_secret",
|
||||
"fieldtype": "Password",
|
||||
"label": "API Secret"
|
||||
},
|
||||
{
|
||||
"description": "Enabling this will register your site on a central relay server to send push notifications for all installed apps through Firebase Cloud Messaging. This server only stores user tokens and error logs, and no messages are saved. ",
|
||||
"fieldname": "section_break_qgjr",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Relay Settings"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-28 11:03:30.518196",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Push Notification Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PushNotificationSettings(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
|
||||
|
||||
api_key: DF.Data | None
|
||||
api_secret: DF.Password | None
|
||||
enable_push_notification_relay: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_relay_server_setup()
|
||||
|
||||
def validate_relay_server_setup(self):
|
||||
if self.enable_push_notification_relay and not frappe.conf.get("push_relay_server_url"):
|
||||
frappe.throw(
|
||||
_("The Push Relay Server URL key (`push_relay_server_url`) is missing in your site config"),
|
||||
title=_("Relay Server URL missing"),
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestPushNotificationSettings(FrappeTestCase):
|
||||
pass
|
||||
|
|
@ -14,7 +14,7 @@ class WebhookData(Document):
|
|||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
fieldname: DF.Literal
|
||||
fieldname: DF.Literal[None]
|
||||
key: DF.Data
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
|
||||
"content": "[{\"id\":\"NPK_AfSLQ2\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"lDOo58F7ZI\",\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"id\":\"ij1pcK8jst\",\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"id\":\"aTlMujEHpN\",\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"id\":\"gY5NXKtXss\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"n_CI3GGqW-\",\"type\":\"card\",\"data\":{\"card_name\":\"Push Notifications\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:16:18.714190",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
|
|
@ -197,9 +197,28 @@
|
|||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Push Notifications",
|
||||
"link_count": 1,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Push Notification Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "Push Notification Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2023-05-24 14:58:55.910408",
|
||||
"modified": "2024-02-28 10:47:38.188832",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Integrations",
|
||||
|
|
|
|||
1253
frappe/locale/ar.po
1253
frappe/locale/ar.po
File diff suppressed because it is too large
Load diff
1351
frappe/locale/de.po
1351
frappe/locale/de.po
File diff suppressed because it is too large
Load diff
1488
frappe/locale/es.po
1488
frappe/locale/es.po
File diff suppressed because it is too large
Load diff
39435
frappe/locale/fa.po
Normal file
39435
frappe/locale/fa.po
Normal file
File diff suppressed because it is too large
Load diff
1253
frappe/locale/fr.po
1253
frappe/locale/fr.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -385,7 +385,7 @@ class BaseDocument:
|
|||
value = cint(value)
|
||||
|
||||
elif df.fieldtype == "JSON" and isinstance(value, dict):
|
||||
value = json.dumps(value, sort_keys=True, indent=4, separators=(",", ": "))
|
||||
value = json.dumps(value, separators=(",", ":"))
|
||||
|
||||
elif df.fieldtype in float_like_fields and not isinstance(value, float):
|
||||
value = flt(value)
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ class DatabaseQuery:
|
|||
"pluck": pluck,
|
||||
"parent_doctype": parent_doctype,
|
||||
} | self.__dict__
|
||||
return controller.get_list(kwargs)
|
||||
return frappe.call(controller.get_list, args=kwargs, **kwargs)
|
||||
|
||||
self.columns = self.get_table_columns()
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue