Merge branch 'develop' into txt-attachment-privacy

This commit is contained in:
Ankush Menat 2024-03-15 14:53:45 +05:30
commit aa51492697
185 changed files with 45117 additions and 3383 deletions

37
.github/helper/update_pot_file.sh vendored Normal file
View 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
View 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 }}

View file

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

@ -2,7 +2,6 @@
*.py~
*.comp.js
*.DS_Store
locale
.wnf-lang-status
*.swp
*.egg-info

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -36,6 +36,8 @@ class ServerScript(Document):
"Before Save",
"After Insert",
"After Save",
"Before Rename",
"After Rename",
"Before Submit",
"After Submit",
"Before Cancel",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -437,7 +437,6 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
db_table = MariaDBTable(doctype, meta)
db_table.validate()
self.commit()
db_table.sync()
self.commit()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
{
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; 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 &amp; 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",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

39435
frappe/locale/fa.po Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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