Merge remote-tracking branch 'upstream/develop' into google-drive-folders

This commit is contained in:
barredterra 2024-02-04 22:12:39 +01:00
commit 82c68048e0
165 changed files with 3251 additions and 2221 deletions

View file

@ -66,6 +66,6 @@ bench --site test_site reinstall --yes
if [ "$TYPE" == "server" ]
then
# wait till assets are built succesfully
# wait till assets are built successfully
wait $build_pid
fi

View file

@ -73,8 +73,9 @@ def has_label(pr_number, label, repo="frappe/frappe"):
)
def is_py(file):
return file.endswith("py")
def is_server_side_code(file):
"""File exclusively affects server side code"""
return file.endswith("py") or file.endswith(".po")
def is_ci(file):
@ -112,7 +113,7 @@ if __name__ == "__main__":
ci_files_changed = any(f for f in files_list if is_ci(f))
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
updated_py_file_count = len(list(filter(is_py, files_list)))
updated_py_file_count = len(list(filter(is_server_side_code, files_list)))
only_py_changed = updated_py_file_count == len(files_list)
if has_skip_ci_label(pr_number, repo):

View file

@ -51,7 +51,7 @@ jobs:
python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER
linter:
name: 'Frappe Linter'
name: 'Semgrep Rules'
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
@ -61,7 +61,6 @@ jobs:
with:
python-version: '3.10'
cache: pip
- uses: pre-commit/action@v3.0.0
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
@ -83,7 +82,7 @@ jobs:
- uses: actions/checkout@v4
- name: Cache pip
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}

View file

@ -76,7 +76,7 @@ jobs:
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
@ -88,7 +88,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

26
.github/workflows/pre-commit.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: Pre-commit
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: precommit-frappe-${{ github.event_name }}-${{ github.event.number }}
cancel-in-progress: true
jobs:
linter:
name: 'precommit'
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: pip
- uses: pre-commit/action@v3.0.0

View file

@ -43,6 +43,8 @@ jobs:
needs: checkrun
if: ${{ needs.checkrun.outputs.build == 'strawberry' }}
timeout-minutes: 60
env:
NODE_ENV: "production"
strategy:
fail-fast: false
@ -104,7 +106,7 @@ jobs:
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
@ -116,7 +118,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View file

@ -42,6 +42,8 @@ jobs:
needs: checkrun
if: ${{ needs.checkrun.outputs.build == 'strawberry' && github.repository_owner == 'frappe' }}
timeout-minutes: 60
env:
NODE_ENV: "production"
strategy:
fail-fast: false
@ -87,7 +89,7 @@ jobs:
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
@ -99,7 +101,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@ -108,7 +110,7 @@ jobs:
${{ runner.os }}-yarn-ui-
- name: Cache cypress binary
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/Cypress
key: ${{ runner.os }}-cypress

View file

@ -6,4 +6,4 @@ hooks.py,frappe.gettext.extractors.navbar.extract
**/report/*/*.json,frappe.gettext.extractors.report.extract
**.py,frappe.gettext.extractors.python.extract
**.js,frappe.gettext.extractors.javascript.extract
**.html,frappe.gettext.extractors.jinja2.extract
**.html,frappe.gettext.extractors.html_template.extract
1 hooks.py frappe.gettext.extractors.navbar.extract
6 **/report/*/*.json frappe.gettext.extractors.report.extract
7 **.py frappe.gettext.extractors.python.extract
8 **.js frappe.gettext.extractors.javascript.extract
9 **.html frappe.gettext.extractors.jinja2.extract frappe.gettext.extractors.html_template.extract

View file

@ -64,6 +64,11 @@ const argv = yargs
description:
"Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
})
.option("using-cached", {
type: "boolean",
description:
"Skips build and uses cached build artifacts to update assets.json (used by Bench)",
})
.example("node esbuild --apps frappe,erpnext", "Run build only for frappe and erpnext")
.example(
"node esbuild --files frappe/website.bundle.js,frappe/desk.bundle.js",
@ -88,6 +93,7 @@ const NODE_PATHS = [].concat(
// import js file of any app if you provide the full path
app_list.map((app) => path.resolve(get_app_path(app), "..")).filter(fs.existsSync)
);
const USING_CACHED = Boolean(argv["using-cached"]);
execute().catch((e) => {
console.error(e);
@ -101,6 +107,12 @@ if (WATCH_MODE) {
async function execute() {
console.time(TOTAL_BUILD_TIME);
if (USING_CACHED) {
await update_assets_json_from_built_assets(APPS);
await update_assets_json_in_cache();
console.timeEnd(TOTAL_BUILD_TIME);
process.exit(0);
}
let results;
try {
@ -131,6 +143,44 @@ async function execute() {
}
}
async function update_assets_json_from_built_assets(apps) {
const assets = await get_assets_json_path_and_obj(false);
const assets_rtl = await get_assets_json_path_and_obj(true);
for (const app in apps) {
await update_assets_obj(app, assets.obj, assets_rtl.obj);
}
for (const { obj, path } of [assets, assets_rtl]) {
const data = JSON.stringify(obj, null, 4);
await fs.promises.writeFile(path, data);
}
}
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 files = await glob("**/*.bundle.*.{js,css}", { cwd: dist_path });
const prefix = path.join("/", "assets", app, "dist");
// eg: "js/marketplace.bundle.6SCSPSGQ.js"
for (const file of files) {
// eg: [ "marketplace", "bundle", "6SCSPSGQ", "js" ]
const parts = path.parse(file).base.split(".");
// eg: "marketplace.bundle.js"
const key = [...parts.slice(0, -2), parts.at(-1)].join(".");
// eg: "js/marketplace.bundle.6SCSPSGQ.js"
const value = path.join(prefix, file);
if (file.includes("-rtl")) {
assets_rtl[`rtl_${key}`] = value;
} else {
assets[key] = value;
}
}
}
function build_assets_for_apps(apps, files) {
let { include_patterns, ignore_patterns } = files.length
? get_files_to_build(files)
@ -393,14 +443,7 @@ async function write_assets_json(metafile) {
}
}
let assets_json_path = path.resolve(assets_path, `assets${rtl ? "-rtl" : ""}.json`);
let assets_json;
try {
assets_json = await fs.promises.readFile(assets_json_path, "utf-8");
} catch (error) {
assets_json = "{}";
}
assets_json = JSON.parse(assets_json);
let { obj: assets_json, path: assets_json_path } = await get_assets_json_path_and_obj(rtl);
// update with new values
let new_assets_json = Object.assign({}, assets_json, out);
curr_assets_json = new_assets_json;
@ -434,6 +477,19 @@ async function update_assets_json_in_cache() {
});
}
async function get_assets_json_path_and_obj(is_rtl) {
const file_name = is_rtl ? "assets-rtl.json" : "assets.json";
const assets_json_path = path.resolve(assets_path, file_name);
let assets_json;
try {
assets_json = await fs.promises.readFile(assets_json_path, "utf-8");
} catch (error) {
assets_json = "{}";
}
assets_json = JSON.parse(assets_json);
return { obj: assets_json, path: assets_json_path };
}
function run_build_command_for_apps(apps) {
let cwd = process.cwd();
let { execSync } = require("child_process");

View file

@ -130,9 +130,45 @@ def _lt(msg: str, lang: str | None = None, context: str | None = None):
Note: Result is not guaranteed to equivalent to pure strings for all operations.
"""
from frappe.translate import LazyTranslate
return _LazyTranslate(msg, lang, context)
return LazyTranslate(msg, lang, context)
@functools.total_ordering
class _LazyTranslate:
__slots__ = ("msg", "lang", "context")
def __init__(self, msg: str, lang: str | None = None, context: str | None = None) -> None:
self.msg = msg
self.lang = lang
self.context = context
@property
def value(self) -> str:
return _(str(self.msg), self.lang, self.context)
def __str__(self):
return self.value
def __add__(self, other):
if isinstance(other, (str, _LazyTranslate)):
return self.value + str(other)
raise NotImplementedError
def __radd__(self, other):
if isinstance(other, (str, _LazyTranslate)):
return str(other) + self.value
return NotImplementedError
def __repr__(self) -> str:
return f"'{self.value}'"
# NOTE: it's required to override these methods and raise error as default behaviour will
# return `False` in all cases.
def __eq__(self, other):
raise NotImplementedError
def __lt__(self, other):
raise NotImplementedError
def as_unicode(text, encoding: str = "utf-8") -> str:
@ -275,20 +311,38 @@ def connect(
) -> None:
"""Connect to site database instance.
:param site: If site is given, calls `frappe.init`.
:param db_name: Optional. Will use from `site_config.json`.
:param site: (Deprecated) If site is given, calls `frappe.init`.
:param db_name: (Deprecated) Optional. Will use from `site_config.json`.
:param set_admin_as_user: Set Administrator as current user.
"""
from frappe.database import get_db
if site:
from frappe.utils.deprecations import deprecation_warning
deprecation_warning(
"Calling frappe.connect with the site argument is deprecated and will be removed in next major version. "
"Instead, explicitly invoke frappe.init(site) prior to calling frappe.connect(), if initializing the site is necessary."
)
init(site)
if db_name:
from frappe.utils.deprecations import deprecation_warning
deprecation_warning(
"Calling frappe.connect with the db_name argument is deprecated and will be removed in next major version. "
"Instead, explicitly invoke frappe.init(site) with the right config prior to calling frappe.connect(), if necessary."
)
assert db_name or local.conf.db_user, "site must be fully initialized, db_user missing"
assert db_name or local.conf.db_name, "site must be fully initialized, db_name missing"
assert local.conf.db_password, "site must be fully initialized, db_password missing"
local.db = get_db(
host=local.conf.db_host,
port=local.conf.db_port,
user=db_name or local.conf.db_name,
password=None,
user=local.conf.db_user or db_name,
password=local.conf.db_password,
cur_db_name=local.conf.db_name or db_name,
)
if set_admin_as_user:
set_user("Administrator")
@ -300,15 +354,21 @@ def connect_replica() -> bool:
if local and hasattr(local, "replica_db") and hasattr(local, "primary_db"):
return False
user = local.conf.db_name
user = local.conf.db_user
password = local.conf.db_password
port = local.conf.replica_db_port
if local.conf.different_credentials_for_replica:
user = local.conf.replica_db_name
user = local.conf.replica_db_user or local.conf.replica_db_name
password = local.conf.replica_db_password
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password, port=port)
local.replica_db = get_db(
host=local.conf.replica_host,
port=port,
user=user,
password=password,
cur_db_name=local.conf.db_name,
)
# swap db connections
local.primary_db = local.db
@ -370,6 +430,11 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None)
os.environ.get("FRAPPE_DB_PORT") or config.get("db_port") or db_default_ports(config["db_type"])
)
# Set the user as database name if not set in config
config["db_user"] = (
os.environ.get("FRAPPE_DB_USER") or config.get("db_user") or config.get("db_name")
)
return config

View file

@ -4,13 +4,15 @@ import base64
import binascii
from urllib.parse import quote, urlencode, urlparse
from werkzeug.wrappers import Response
import frappe
import frappe.database
import frappe.utils
import frappe.utils.user
from frappe import _
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.sessions import Session, clear_sessions, delete_session
from frappe.sessions import Session, clear_sessions, delete_session, get_expiry_in_seconds
from frappe.translate import get_language
from frappe.twofactor import (
authenticate_for_2factor,
@ -272,9 +274,7 @@ class LoginManager:
if self.user in frappe.STANDARD_USERS:
return False
reset_pwd_after_days = cint(
frappe.db.get_single_value("System Settings", "force_user_to_reset_password")
)
reset_pwd_after_days = cint(frappe.get_system_settings("force_user_to_reset_password"))
if reset_pwd_after_days:
last_password_reset_date = (
@ -356,12 +356,19 @@ class CookieManager:
if not frappe.local.session.get("sid"):
return
# sid expires in 3 days
expires = datetime.datetime.now() + datetime.timedelta(days=3)
if frappe.session.sid:
self.set_cookie("sid", frappe.session.sid, expires=expires, httponly=True)
self.set_cookie("sid", frappe.session.sid, max_age=get_expiry_in_seconds(), httponly=True)
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
def set_cookie(
self,
key,
value,
expires=None,
secure=False,
httponly=False,
samesite="Lax",
max_age=None,
):
if not secure and hasattr(frappe.local, "request"):
secure = frappe.local.request.scheme == "https"
@ -371,6 +378,7 @@ class CookieManager:
"secure": secure,
"httponly": httponly,
"samesite": samesite,
"max_age": max_age,
}
def delete_cookie(self, to_delete):
@ -379,7 +387,7 @@ class CookieManager:
self.to_delete.extend(to_delete)
def flush_cookies(self, response):
def flush_cookies(self, response: Response):
for key, opts in self.cookies.items():
response.set_cookie(
key,
@ -388,6 +396,7 @@ class CookieManager:
secure=opts.get("secure"),
httponly=opts.get("httponly"),
samesite=opts.get("samesite"),
max_age=opts.get("max_age"),
)
# expires yesterday!

View file

@ -9,6 +9,7 @@ from frappe.cache_manager import clear_doctype_map, get_doctype_map
from frappe.desk.form import assign_to
from frappe.model import log_types
from frappe.model.document import Document
from frappe.utils.data import comma_and
class AssignmentRule(Document):
@ -55,14 +56,10 @@ class AssignmentRule(Document):
def validate_assignment_days(self):
assignment_days = self.get_assignment_days()
if len(set(assignment_days)) != len(assignment_days):
repeated_days = get_repeated(assignment_days)
plural = "s" if len(repeated_days) > 1 else ""
frappe.throw(
_("Assignment Day{0} {1} has been repeated.").format(
plural, frappe.bold(", ".join(repeated_days))
_("The following Assignment Days have been repeated: {0}").format(
comma_and([_(day) for day in get_repeated(assignment_days)], add_quotes=False)
)
)

View file

@ -550,7 +550,7 @@ def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters
docs += [r.name for r in res]
docs = set(list(docs))
return [[d] for d in docs]
return [[d] for d in docs if txt in d]
@frappe.whitelist()

View file

@ -229,6 +229,7 @@ def bundle(
skip_frappe=False,
files=None,
save_metafiles=False,
using_cached=False,
):
"""concat / minify js files"""
setup()
@ -246,7 +247,10 @@ def bundle(
if files:
command += " --files {files}".format(files=",".join(files))
command += " --run-build-command"
if using_cached:
command += " --using-cached"
else:
command += " --run-build-command"
if save_metafiles:
command += " --save-metafiles"

View file

@ -53,6 +53,7 @@ from frappe.exceptions import SiteNotSpecifiedError
default=True,
help="Create user and database in mariadb/postgres; only bootstrap if false",
)
@click.option("--db-user", help="Database user if you already have one")
def new_site(
site,
db_root_username=None,
@ -68,6 +69,7 @@ def new_site(
db_type=None,
db_host=None,
db_port=None,
db_user=None,
set_default=False,
setup_db=True,
):
@ -91,6 +93,7 @@ def new_site(
db_type=db_type,
db_host=db_host,
db_port=db_port,
db_user=db_user,
setup_db=setup_db,
)
@ -319,7 +322,7 @@ def restore_backup(
)
except Exception as err:
print(err.args[1])
print(err)
sys.exit(1)
@ -339,7 +342,7 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None):
site = get_site(context)
verbose = context.verbose or verbose
frappe.init(site=site)
frappe.connect(site=site)
frappe.connect()
err, out = frappe.utils.execute_in_shell(f"file {sql_file_path}", check_exit_code=True)
if err:
click.secho("Failed to detect type of backup file", fg="red")
@ -535,7 +538,8 @@ def add_db_index(context, doctype, column):
columns = column # correct naming
for site in context.sites:
frappe.connect(site=site)
frappe.init(site=site)
frappe.connect()
try:
frappe.db.add_index(doctype, columns)
if len(columns) == 1:
@ -577,7 +581,8 @@ def describe_database_table(context, doctype, column):
import json
for site in context.sites:
frappe.connect(site=site)
frappe.init(site=site)
frappe.connect()
try:
data = _extract_table_stats(doctype, column)
# NOTE: Do not print anything else in this to avoid clobbering the output.
@ -663,7 +668,8 @@ def add_system_manager(context, email, first_name, last_name, send_welcome_email
import frappe.utils.user
for site in context.sites:
frappe.connect(site=site)
frappe.init(site=site)
frappe.connect()
try:
frappe.utils.user.add_system_manager(email, first_name, last_name, send_welcome_email, password)
frappe.db.commit()
@ -689,7 +695,8 @@ def add_user_for_sites(
import frappe.utils.user
for site in context.sites:
frappe.connect(site=site)
frappe.init(site=site)
frappe.connect()
try:
add_new_user(email, first_name, last_name, user_type, send_welcome_email, password, add_role)
frappe.db.commit()
@ -1058,7 +1065,11 @@ def _drop_site(
sys.exit(1)
click.secho("Dropping site database and user", fg="green")
drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password)
frappe.flags.root_login = db_root_username
frappe.flags.root_password = db_root_password
drop_user_and_database(frappe.conf.db_name, frappe.conf.db_user)
archived_sites_path = archived_sites_path or os.path.join(
frappe.utils.get_bench_path(), "archived", "sites"
@ -1336,7 +1347,6 @@ def build_search_index(context):
@click.option("--no-backup", is_flag=True, default=False, help="Do not backup the table")
@pass_context
def clear_log_table(context, doctype, days, no_backup):
"""If any logtype table grows too large then clearing it with DELETE query
is not feasible in reasonable time. This command copies recent data to new
table and replaces current table with new smaller table.

View file

@ -34,7 +34,8 @@ def new_language(context, lang_code, app):
raise Exception("--site is required")
# init site
frappe.connect(site=context["sites"][0])
frappe.init(site=context["sites"][0])
frappe.connect()
frappe.translate.write_translations_file(app, lang_code)
print(

View file

@ -36,6 +36,13 @@ EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
default=False,
help="Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
)
@click.option(
"--using-cached",
is_flag=True,
default=False,
envvar="USING_CACHED",
help="Skips build and uses cached build artifacts (cache is set by Bench). Ignored if developer_mode enabled.",
)
def build(
app=None,
apps=None,
@ -44,6 +51,7 @@ def build(
verbose=False,
force=False,
save_metafiles=False,
using_cached=False,
):
"Compile JS and CSS source files"
from frappe.build import bundle, download_frappe_assets
@ -69,6 +77,9 @@ def build(
if production:
mode = "production"
if development:
using_cached = False
bundle(
mode,
apps=apps,
@ -76,6 +87,7 @@ def build(
verbose=verbose,
skip_frappe=skip_frappe,
save_metafiles=save_metafiles,
using_cached=using_cached,
)
if apps and isinstance(apps, str):
@ -108,7 +120,8 @@ def clear_cache(context):
for site in context.sites:
try:
frappe.connect(site)
frappe.init(site=site)
frappe.connect()
frappe.clear_cache()
clear_website_cache()
finally:
@ -601,7 +614,7 @@ def console(context, autoreload=False):
all_apps = frappe.get_installed_apps()
failed_to_import = []
for app in all_apps:
for app in list(all_apps):
try:
locals()[app] = __import__(app)
except ModuleNotFoundError:
@ -759,12 +772,8 @@ def run_tests(
click.secho(f"bench --site {site} set-config allow_tests true", fg="green")
return
frappe.init(site=site)
frappe.flags.skip_before_tests = skip_before_tests
frappe.flags.skip_test_records = skip_test_records
ret = frappe.test_runner.main(
site,
app,
module,
doctype,
@ -777,6 +786,8 @@ def run_tests(
doctype_list_path=doctype_list_path,
failfast=failfast,
case=case,
skip_test_records=skip_test_records,
skip_before_tests=skip_before_tests,
)
if len(ret.failures) == 0 and len(ret.errors) == 0:

View file

@ -28,7 +28,7 @@ class AddressTemplate(Document):
if not self.is_default and not self._get_previous_default():
self.is_default = 1
if frappe.db.get_single_value("System Settings", "setup_complete"):
if frappe.get_system_settings("setup_complete"):
frappe.msgprint(_("Setting this Address Template as default as there is no other default"))
def on_update(self):

View file

@ -93,7 +93,7 @@ class Comment(Document):
def remove_comment_from_cache(self):
_comments = get_comments_from_parent(self)
for c in _comments:
for c in list(_comments):
if c.get("name") == self.name:
_comments.remove(c)

View file

@ -146,7 +146,7 @@ class CommunicationEmailMixin:
return get_formatted_email(self.mail_sender_fullname(), mail=self.mail_sender())
def get_content(self, print_format=None):
if print_format and frappe.db.get_single_value("System Settings", "attach_view_link"):
if print_format and frappe.get_system_settings("attach_view_link"):
return self.content + self.get_attach_link(print_format)
return self.content

View file

@ -41,7 +41,7 @@ const can_export = (frm) => {
if (!doctype) {
frappe.msgprint(__("Please select the Document Type."));
} else if (!parent_multicheck_options.length) {
frappe.msgprint(__("Atleast one field of Parent Document Type is mandatory"));
frappe.msgprint(__("At least one field of Parent Document Type is mandatory"));
} else {
is_valid_form = true;
}
@ -153,7 +153,7 @@ const add_doctype_field_multicheck_control = (doctype, parent_wrapper) => {
const options = fields.map((df) => {
return {
label: df.label,
label: __(df.label),
value: df.fieldname,
danger: df.reqd,
checked: 1,
@ -163,7 +163,7 @@ const add_doctype_field_multicheck_control = (doctype, parent_wrapper) => {
const multicheck_control = frappe.ui.form.make_control({
parent: parent_wrapper,
df: {
label: doctype,
label: __(doctype),
fieldname: doctype + "_fields",
fieldtype: "MultiCheck",
options: options,

View file

@ -135,34 +135,29 @@ frappe.ui.form.on("Data Import", {
let failed_records = cint(r.message.failed);
let total_records = cint(r.message.total_records);
if (!total_records) return;
let action, message;
if (frm.doc.import_type === "Insert New Records") {
action = "imported";
} else {
action = "updated";
if (!total_records) {
return;
}
if (failed_records === 0) {
let message_args = [action, successful_records];
if (successful_records === 1) {
message = __("Successfully {0} 1 record.", message_args);
} else {
message = __("Successfully {0} {1} records.", message_args);
}
let message;
if (frm.doc.import_type === "Insert New Records") {
message = __("Successfully imported {0} out of {1} records.", [
successful_records,
total_records,
]);
} else {
let message_args = [action, successful_records, total_records];
if (successful_records === 1) {
message = __(
"Successfully {0} {1} record out of {2}. Click on Export Errored Rows, fix the errors and import again.",
message_args
message = __("Successfully updated {0} out of {1} records.", [
successful_records,
total_records,
]);
}
if (failed_records > 0) {
message +=
"<br/>" +
__(
"Please click on 'Export Errored Rows', fix the errors and import again."
);
} else {
message = __(
"Successfully {0} {1} records out of {2}. Click on Export Errored Rows, fix the errors and import again.",
message_args
);
}
}
// If the job timed out, display an extra hint
@ -506,13 +501,7 @@ frappe.ui.form.on("Data Import", {
},
show_import_log(frm) {
if (!frm.doc.show_failed_logs) {
frm.toggle_display("import_log_preview", false);
return;
}
frm.toggle_display("import_log_section", false);
frm.toggle_display("import_log_preview", true);
if (frm.import_in_progress) {
return;

View file

@ -139,7 +139,7 @@
"default": "0",
"fieldname": "show_failed_logs",
"fieldtype": "Check",
"label": "Show Failed Logs"
"label": "Show Only Failed Logs"
},
{
"depends_on": "eval:!doc.__islocal && !doc.import_file",
@ -171,7 +171,7 @@
],
"hide_toolbar": 1,
"links": [],
"modified": "2023-12-15 12:45:49.452834",
"modified": "2024-01-30 17:08:05.566686",
"modified_by": "Administrator",
"module": "Core",
"name": "Data Import",

View file

@ -38,7 +38,6 @@ class DataImport(Document):
submit_after_import: DF.Check
template_options: DF.Code | None
template_warnings: DF.Code | None
# end: auto-generated types
def validate(self):
@ -93,7 +92,8 @@ class DataImport(Document):
def start_import(self):
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
run_now = frappe.flags.in_test or frappe.conf.developer_mode
if is_scheduler_inactive() and not run_now:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
job_id = f"data_import::{self.name}"
@ -106,7 +106,7 @@ class DataImport(Document):
event="data_import",
job_id=job_id,
data_import=self.name,
now=frappe.conf.developer_mode or frappe.flags.in_test,
now=run_now,
)
return True

View file

@ -157,6 +157,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.is_virtual",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View",
@ -580,7 +581,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-16 11:26:56.364594",
"modified": "2024-02-01 15:55:44.007917",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -117,6 +117,7 @@ class DocField(Document):
unique: DF.Check
width: DF.Data | None
# end: auto-generated types
def get_link_doctype(self):
"""Return the Link doctype for the `docfield` (if applicable).

View file

@ -56,6 +56,24 @@ class TestDocShare(FrappeTestCase):
with self.assertRowsRead(1):
self.assertTrue(self.event.has_permission())
def test_list_permission(self):
frappe.set_user(self.user)
with self.assertRaises(frappe.PermissionError):
frappe.get_list("Web Page")
frappe.set_user("Administrator")
doc = frappe.new_doc("Web Page")
doc.update({"title": "test document for docshare permissions"})
doc.insert()
frappe.share.add("Web Page", doc.name, self.user)
frappe.set_user(self.user)
self.assertEqual(len(frappe.get_list("Web Page")), 1)
doc.delete(ignore_permissions=True)
with self.assertRaises(frappe.PermissionError):
frappe.get_list("Web Page")
def test_share_permission(self):
frappe.share.add("Event", self.event.name, self.user, write=1, share=1)

View file

@ -4,13 +4,10 @@
import copy
import json
import os
# imports - standard imports
import re
import shutil
from typing import TYPE_CHECKING, Union
# imports - module imports
import frappe
from frappe import _
from frappe.cache_manager import clear_controller_cache, clear_user_cache
@ -1614,7 +1611,6 @@ def validate_fields(meta):
check_illegal_characters(d.fieldname)
check_invalid_fieldnames(meta.get("name"), d.fieldname)
check_unique_fieldname(meta.get("name"), d.fieldname)
check_fieldname_length(d.fieldname)
check_hidden_and_mandatory(meta.get("name"), d)
check_unique_and_text(meta.get("name"), d)
@ -1624,6 +1620,7 @@ def validate_fields(meta):
validate_data_field_type(d)
if not frappe.flags.in_migrate:
check_unique_fieldname(meta.get("name"), d.fieldname)
check_link_table_options(meta.get("name"), d)
check_illegal_mandatory(meta.get("name"), d)
check_dynamic_link_options(d)

View file

@ -3,7 +3,19 @@ frappe.listview_settings["DocType"] = {
this.new_doctype_dialog();
},
new_doctype_dialog() {
new_doctype_dialog(args) {
const {
doctype_name = "",
doctype_module = "",
is_submittable = 0,
is_child = 0,
is_virtual = 0,
is_single = 0,
is_tree = 0,
is_custom = 0,
editable_grid = 1,
} = args || {};
let non_developer = frappe.session.user !== "Administrator" || !frappe.boot.developer_mode;
let fields = [
{
@ -11,6 +23,7 @@ frappe.listview_settings["DocType"] = {
fieldname: "name",
fieldtype: "Data",
reqd: 1,
default: doctype_name,
},
{ fieldtype: "Column Break" },
{
@ -19,6 +32,7 @@ frappe.listview_settings["DocType"] = {
fieldtype: "Link",
options: "Module Def",
reqd: 1,
default: doctype_module,
},
{ fieldtype: "Section Break" },
{
@ -29,6 +43,7 @@ frappe.listview_settings["DocType"] = {
"Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended."
),
depends_on: "eval:!doc.istable && !doc.issingle",
default: is_submittable,
},
{
label: __("Is Child Table"),
@ -36,13 +51,14 @@ frappe.listview_settings["DocType"] = {
fieldtype: "Check",
description: __("Child Tables are shown as a Grid in other DocTypes"),
depends_on: "eval:!doc.is_submittable && !doc.issingle",
default: is_child,
},
{
label: __("Editable Grid"),
fieldname: "editable_grid",
fieldtype: "Check",
depends_on: "istable",
default: 1,
default: editable_grid,
},
{
label: __("Is Single"),
@ -52,12 +68,13 @@ frappe.listview_settings["DocType"] = {
"Single Types have only one record no tables associated. Values are stored in tabSingles"
),
depends_on: "eval:!doc.istable && !doc.is_submittable",
default: is_single,
},
{
label: "Is Tree",
fieldname: "is_tree",
fieldtype: "Check",
default: "0",
default: is_tree,
depends_on: "eval:!doc.istable",
description: "Tree structures are implemented using Nested Set",
},
@ -65,7 +82,7 @@ frappe.listview_settings["DocType"] = {
label: __("Custom?"),
fieldname: "custom",
fieldtype: "Check",
default: non_developer,
default: non_developer || is_custom,
read_only: non_developer,
},
];
@ -75,7 +92,7 @@ frappe.listview_settings["DocType"] = {
label: "Is Virtual",
fieldname: "is_virtual",
fieldtype: "Check",
default: "0",
default: is_virtual,
});
}

View file

@ -40,8 +40,8 @@ class TestNamingSeries(FrappeTestCase):
def get_valid_serieses(self):
VALID_SERIES = ["SINV-", "SI-.{field}.", "SI-#.###", ""]
exisiting_series = self.dns.get_transactions_and_prefixes()["prefixes"]
return VALID_SERIES + exisiting_series
existing_series = self.dns.get_transactions_and_prefixes()["prefixes"]
return VALID_SERIES + existing_series
def test_naming_preview(self):
self.dns.transaction_type = self.ns_doctype

View file

@ -756,6 +756,13 @@ class File(Document):
self.save_file(content=optimized_content, overwrite=True)
self.save()
@property
def unique_url(self) -> str:
"""Unique URL contains file ID in URL to speed up permisison checks."""
from urllib.parse import urlencode
return self.file_url + "?" + urlencode({"fid": self.name})
@staticmethod
def zip_files(files):
zip_file = io.BytesIO()

View file

@ -25,7 +25,11 @@ class RoleProfile(Document):
self.name = self.role_profile
def on_update(self):
self.queue_action("update_all_users", now=frappe.flags.in_test)
self.queue_action(
"update_all_users",
now=frappe.flags.in_test or frappe.flags.in_install,
enqueue_after_commit=True,
)
def update_all_users(self):
"""Changes in role_profile reflected across all its user"""

View file

@ -238,7 +238,6 @@ frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run()
script.execute_method()
def test_server_script_rate_limiting(self):
# why not
script1 = frappe.get_doc(
doctype="Server Script",
name="rate_limited_server_script",

View file

@ -94,7 +94,9 @@
"dormant_days",
"telemetry_section",
"allow_error_traceback",
"enable_telemetry"
"enable_telemetry",
"search_section",
"link_field_results_limit"
],
"fields": [
{
@ -634,12 +636,24 @@
{
"fieldname": "column_break_uhqk",
"fieldtype": "Column Break"
},
{
"fieldname": "search_section",
"fieldtype": "Section Break",
"label": "Search"
},
{
"default": "10",
"fieldname": "link_field_results_limit",
"fieldtype": "Int",
"label": "Link Field Results Limit",
"non_negative": 1
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2023-12-08 15:52:37.525003",
"modified": "2024-01-26 11:29:20.924425",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -61,6 +61,7 @@ class SystemSettings(Document):
hide_footer_in_auto_email_reports: DF.Check
language: DF.Link
lifespan_qrcode_image: DF.Int
link_field_results_limit: DF.Int
login_with_email_link: DF.Check
login_with_email_link_expiry: DF.Int
logout_on_password_reset: DF.Check
@ -94,6 +95,7 @@ class SystemSettings(Document):
two_factor_method: DF.Literal["OTP App", "SMS", "Email"]
welcome_email_template: DF.Link | None
# end: auto-generated types
def validate(self):
from frappe.twofactor import toggle_two_factor_auth
@ -130,6 +132,16 @@ class SystemSettings(Document):
self.validate_backup_limit()
self.validate_file_extensions()
if not self.link_field_results_limit:
self.link_field_results_limit = 10
if self.link_field_results_limit > 50:
self.link_field_results_limit = 50
label = _(self.meta.get_label("link_field_results_limit"))
frappe.msgprint(
_("{0} can not be more than {1}").format(label, 50), alert=True, indicator="yellow"
)
def validate_user_pass_login(self):
if not self.disable_user_pass_login:
return

View file

@ -462,7 +462,7 @@
"read_only": 1
},
{
"default": "1",
"default": "2",
"fieldname": "simultaneous_sessions",
"fieldtype": "Int",
"label": "Simultaneous Sessions"

View file

@ -725,12 +725,8 @@ class User(Document):
3. If allow_login_using_user_name is set, you can use username while finding the user.
"""
login_with_mobile = cint(
frappe.db.get_single_value("System Settings", "allow_login_using_mobile_number")
)
login_with_username = cint(
frappe.db.get_single_value("System Settings", "allow_login_using_user_name")
)
login_with_mobile = cint(frappe.get_system_settings("allow_login_using_mobile_number"))
login_with_username = cint(frappe.get_system_settings("allow_login_using_user_name"))
or_filters = [{"name": user_name}]
if login_with_mobile:
@ -840,8 +836,8 @@ def update_password(
else:
user = res["user"]
logout_all_sessions = cint(logout_all_sessions) or frappe.db.get_single_value(
"System Settings", "logout_on_password_reset"
logout_all_sessions = cint(logout_all_sessions) or frappe.get_system_settings(
"logout_on_password_reset"
)
_update_password(user, new_password, logout_all_sessions=cint(logout_all_sessions))
@ -933,7 +929,7 @@ def _get_user_for_update_password(key, old_password):
result.user, last_reset_password_key_generated_on = user or (None, None)
if result.user:
reset_password_link_expiry = cint(
frappe.db.get_single_value("System Settings", "reset_password_link_expiry_duration")
frappe.get_system_settings("reset_password_link_expiry_duration")
)
if (
reset_password_link_expiry
@ -1018,7 +1014,7 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
@frappe.whitelist(allow_guest=True)
@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60)
@rate_limit(limit=get_password_reset_limit, seconds=60 * 60)
def reset_password(user: str) -> str:
if user == "Administrator":
return "not allowed"
@ -1254,34 +1250,37 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
except frappe.DuplicateEntryError:
pass
else:
contact = frappe.get_doc("Contact", contact_name)
contact.first_name = user.first_name
contact.last_name = user.last_name
contact.gender = user.gender
try:
contact = frappe.get_doc("Contact", contact_name)
contact.first_name = user.first_name
contact.last_name = user.last_name
contact.gender = user.gender
# Add mobile number if phone does not exists in contact
if user.phone and not any(new_contact.phone == user.phone for new_contact in contact.phone_nos):
# Set primary phone if there is no primary phone number
contact.add_phone(
user.phone,
is_primary_phone=not any(
new_contact.is_primary_phone == 1 for new_contact in contact.phone_nos
),
)
# Add mobile number if phone does not exists in contact
if user.phone and not any(new_contact.phone == user.phone for new_contact in contact.phone_nos):
# Set primary phone if there is no primary phone number
contact.add_phone(
user.phone,
is_primary_phone=not any(
new_contact.is_primary_phone == 1 for new_contact in contact.phone_nos
),
)
# Add mobile number if mobile does not exists in contact
if user.mobile_no and not any(
new_contact.phone == user.mobile_no for new_contact in contact.phone_nos
):
# Set primary mobile if there is no primary mobile number
contact.add_phone(
user.mobile_no,
is_primary_mobile_no=not any(
new_contact.is_primary_mobile_no == 1 for new_contact in contact.phone_nos
),
)
# Add mobile number if mobile does not exists in contact
if user.mobile_no and not any(
new_contact.phone == user.mobile_no for new_contact in contact.phone_nos
):
# Set primary mobile if there is no primary mobile number
contact.add_phone(
user.mobile_no,
is_primary_mobile_no=not any(
new_contact.is_primary_mobile_no == 1 for new_contact in contact.phone_nos
),
)
contact.save(ignore_permissions=True)
contact.save(ignore_permissions=True)
except frappe.TimestampMismatchError:
raise frappe.RetryBackgroundJobError
def get_restricted_ip_list(user):

View file

@ -121,7 +121,7 @@ frappe.listview_settings["User Permission"] = {
callback: function (r) {
if (r.message === 1) {
frappe.show_alert({
message: __("User Permissions created sucessfully"),
message: __("User Permissions created successfully"),
indicator: "blue",
});
} else {

View file

@ -253,6 +253,10 @@ frappe.PermissionEngine = class PermissionEngine {
if (!d.is_submittable && ["submit", "cancel", "amend"].includes(r)) return;
if (d.in_create && ["create", "delete"].includes(r)) return;
this.add_check(perm_container, d, r);
if (d.if_owner && r == "report") {
perm_container.find("div[data-fieldname='report']").toggle(false);
}
});
// buttons
@ -414,6 +418,13 @@ frappe.PermissionEngine = class PermissionEngine {
chk.prop("checked", !chk.prop("checked"));
} else {
me.get_perm(args.role)[args.ptype] = args.value;
if (args.ptype == "if_owner") {
let report_checkbox = chk
.closest("div.row")
.find("div[data-fieldname='report']");
report_checkbox.toggle(!args.value);
}
}
},
});

View file

@ -129,8 +129,15 @@ def update(
frappe.clear_cache(doctype=doctype)
frappe.only_for("System Manager")
if ptype == "report" and value == "1" and if_owner == "1":
frappe.throw(_("Cannot set 'Report' permission if 'Only If Creator' permission is set"))
out = update_permission_property(doctype, role, permlevel, ptype, value, if_owner=if_owner)
if ptype == "if_owner" and value == "1":
update_permission_property(doctype, role, permlevel, "report", "0", if_owner=value)
frappe.db.after_commit.add(clear_cache)
return "refresh" if out else None

View file

@ -8,7 +8,7 @@ import frappe
def get_parent_doc(doc):
"""Return document of `reference_doctype`, `reference_doctype`."""
if not hasattr(doc, "parent_doc"):
if not getattr(doc, "parent_doc", None):
if doc.reference_doctype and doc.reference_name:
doc.parent_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
else:

View file

@ -13,21 +13,71 @@
"label": "Build",
"links": [
{
"description": "Customize properties, naming, fields and more for standard doctypes",
"hidden": 0,
"is_query_report": 0,
"label": "Models",
"link_count": 0,
"label": "Customization",
"link_count": 4,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "DocType",
"label": "Customize Form",
"link_count": 0,
"link_to": "DocType",
"link_to": "Customize Form",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Custom Field",
"link_count": 0,
"link_to": "Custom Field",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Custom Translation",
"link_count": 0,
"link_to": "Translation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Navbar Settings",
"link_count": 0,
"link_to": "Navbar Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"description": "Group your custom doctypes under modules",
"hidden": 0,
"is_query_report": 0,
"label": "Modules",
"link_count": 2,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Module Def",
"link_count": 0,
"link_to": "Module Def",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@ -36,22 +86,112 @@
{
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
"label": "Module Onboarding",
"link_count": 0,
"link_to": "Workflow",
"link_to": "Module Onboarding",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"description": "Monitor logs for errors, background jobs, communications, and user activity",
"hidden": 0,
"is_query_report": 0,
"label": "System Logs",
"link_count": 5,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Background Jobs",
"link_count": 0,
"link_to": "RQ Job",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Scheduled Jobs Logs",
"link_count": 0,
"link_to": "Scheduled Job Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Error Logs",
"link_count": 0,
"link_to": "Error Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Communication Logs",
"link_count": 0,
"link_to": "Communication",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Activity Log",
"link_count": 0,
"link_to": "Activity Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"description": "Packages are lightweight apps (collection of Module Defs) that can be created, imported, or released right from the UI",
"hidden": 0,
"is_query_report": 0,
"label": "Packages",
"link_count": 2,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Package",
"link_count": 0,
"link_to": "Package",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Package Import",
"link_count": 0,
"link_to": "Package Import",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"description": "Automate processes and extend standard functionality using scripts and background jobs",
"hidden": 0,
"is_query_report": 0,
"label": "Scripting",
"link_count": 0,
"link_count": 3,
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Card Break"
},
{
@ -88,38 +228,12 @@
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Packages",
"link_count": 2,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Package",
"link_count": 0,
"link_to": "Package",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Package Import",
"link_count": 0,
"link_to": "Package Import",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"description": "Build your own reports, print formats, and dashboards. Create personalized workspaces for easier navigation",
"hidden": 0,
"is_query_report": 0,
"label": "Views",
"link_count": 5,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@ -177,115 +291,10 @@
"type": "Link"
},
{
"description": "Create new forms and views with doctypes. Set up multi-level workflows for approval",
"hidden": 0,
"is_query_report": 0,
"label": "Customization",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Customize Form",
"link_count": 0,
"link_to": "Customize Form",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Custom Field",
"link_count": 0,
"link_to": "Custom Field",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Custom Translation",
"link_count": 0,
"link_to": "Translation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Navbar Settings",
"link_count": 0,
"link_to": "Navbar Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "System Logs",
"link_count": 5,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Background Jobs",
"link_count": 0,
"link_to": "RQ Job",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Scheduled Jobs Logs",
"link_count": 0,
"link_to": "Scheduled Job Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Error Logs",
"link_count": 0,
"link_to": "Error Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Communication Logs",
"link_count": 0,
"link_to": "Communication",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Activity Log",
"link_count": 0,
"link_to": "Activity Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Modules",
"label": "Models",
"link_count": 2,
"link_type": "DocType",
"onboard": 0,
@ -294,9 +303,9 @@
{
"hidden": 0,
"is_query_report": 0,
"label": "Module Def",
"label": "DocType",
"link_count": 0,
"link_to": "Module Def",
"link_to": "DocType",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
@ -305,16 +314,16 @@
{
"hidden": 0,
"is_query_report": 0,
"label": "Module Onboarding",
"label": "Workflow",
"link_count": 0,
"link_to": "Module Onboarding",
"link_to": "Workflow",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
}
],
"modified": "2024-01-02 15:38:42.806824",
"modified": "2024-01-23 17:27:44.769958",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
@ -325,7 +334,7 @@
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 16.0,
"sequence_id": 27.0,
"shortcuts": [
{
"color": "Grey",

View file

@ -112,27 +112,30 @@ frappe.ui.form.on("Custom Field", {
}
},
add_rename_field(frm) {
frm.add_custom_button(__("Rename Fieldname"), () => {
frappe.prompt(
{
fieldtype: "Data",
label: __("Fieldname"),
fieldname: "fieldname",
reqd: 1,
},
function (data) {
frappe.call({
method: "frappe.custom.doctype.custom_field.custom_field.rename_fieldname",
args: {
custom_field: frm.doc.name,
fieldname: data.fieldname,
},
});
},
__("Rename Fieldname"),
__("Rename")
);
});
if (!frm.is_new()) {
frm.add_custom_button(__("Rename Fieldname"), () => {
frappe.prompt(
{
fieldtype: "Data",
label: __("Fieldname"),
fieldname: "fieldname",
reqd: 1,
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,
},
});
},
__("Rename Fieldname"),
__("Rename")
);
});
}
},
});

View file

@ -130,6 +130,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.is_virtual",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
@ -483,7 +484,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-12-08 15:52:37.525003",
"modified": "2024-02-01 15:56:39.171633",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -110,4 +110,5 @@ class CustomizeFormField(Document):
unique: DF.Check
width: DF.Data | None
# end: auto-generated types
pass

View file

@ -23,47 +23,45 @@ def setup_database(force, verbose=None, no_mariadb_socket=False):
)
def bootstrap_database(db_name, verbose=None, source_sql=None):
def bootstrap_database(verbose=None, source_sql=None):
import frappe
if frappe.conf.db_type == "postgres":
import frappe.database.postgres.setup_db
return frappe.database.postgres.setup_db.bootstrap_database(db_name, verbose, source_sql)
return frappe.database.postgres.setup_db.bootstrap_database(verbose, source_sql)
else:
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.bootstrap_database(db_name, verbose, source_sql)
return frappe.database.mariadb.setup_db.bootstrap_database(verbose, source_sql)
def drop_user_and_database(db_name, root_login=None, root_password=None):
def drop_user_and_database(db_name, db_user):
import frappe
if frappe.conf.db_type == "postgres":
import frappe.database.postgres.setup_db
return frappe.database.postgres.setup_db.drop_user_and_database(
db_name, root_login, root_password
)
return frappe.database.postgres.setup_db.drop_user_and_database(db_name, db_user)
else:
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.drop_user_and_database(
db_name, root_login, root_password
)
return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, db_user)
def get_db(host=None, user=None, password=None, port=None):
def get_db(host=None, user=None, password=None, port=None, cur_db_name=None):
import frappe
if frappe.conf.db_type == "postgres":
import frappe.database.postgres.database
return frappe.database.postgres.database.PostgresDatabase(host, user, password, port=port)
return frappe.database.postgres.database.PostgresDatabase(
host, user, password, port, cur_db_name
)
else:
import frappe.database.mariadb.database
return frappe.database.mariadb.database.MariaDBDatabase(host, user, password, port=port)
return frappe.database.mariadb.database.MariaDBDatabase(host, user, password, port, cur_db_name)
def get_command(
@ -77,12 +75,7 @@ def get_command(
else:
bin, bin_name = which("psql"), "psql"
host = frappe.utils.esc(host, "$ ")
user = frappe.utils.esc(user, "$ ")
db_name = frappe.utils.esc(db_name, "$ ")
if password:
password = frappe.utils.esc(password, "$ ")
conn_string = f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
else:
conn_string = f"postgresql://{user}@{host}:{port}/{db_name}"
@ -98,10 +91,6 @@ def get_command(
else:
bin, bin_name = which("mariadb") or which("mysql"), "mariadb"
host = frappe.utils.esc(host, "$ ")
user = frappe.utils.esc(user, "$ ")
db_name = frappe.utils.esc(db_name, "$ ")
command = [
f"--user={user}",
f"--host={host}",
@ -109,7 +98,6 @@ def get_command(
]
if password:
password = frappe.utils.esc(password, "$ ")
command.append(f"--password={password}")
if dump:

View file

@ -40,7 +40,6 @@ if TYPE_CHECKING:
from pymysql.connections import Connection as MariadbConnection
from pymysql.cursors import Cursor as MariadbCursor
IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE)
INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*")
SINGLE_WORD_PATTERN = re.compile(r'([`"]?)(tab([A-Z]\w+))\1')
@ -74,27 +73,20 @@ class Database:
host=None,
user=None,
password=None,
ac_name=None,
use_default=0,
port=None,
cur_db_name=None,
):
self.setup_type_map()
self.host = host or frappe.conf.db_host
self.port = port or frappe.conf.db_port
self.user = user or frappe.conf.db_name
self.cur_db_name = frappe.conf.db_name
self.host = host
self.port = port
self.user = user
self.password = password
self.cur_db_name = cur_db_name
self._conn = None
if ac_name:
self.user = ac_name or frappe.conf.db_name
if use_default:
self.user = frappe.conf.db_name
self.transaction_writes = 0
self.auto_commit_on_many_writes = 0
self.password = password or frappe.conf.db_password
self.value_cache = {}
self.logger = frappe.logger("database")
self.logger.setLevel("WARNING")
@ -104,8 +96,8 @@ class Database:
self.before_rollback = CallbackManager()
self.after_rollback = CallbackManager()
# self.db_type: str
# self.last_query (lazy) attribute of last sql query executed
# self.db_type: str
# self.last_query (lazy) attribute of last sql query executed
def setup_type_map(self):
pass
@ -784,7 +776,7 @@ class Database:
Example:
# Update the `deny_multiple_sessions` field in System Settings DocType.
company = frappe.db.set_single_value("System Settings", "deny_multiple_sessions", True)
frappe.db.set_single_value("System Settings", "deny_multiple_sessions", True)
"""
to_update = self._get_update_dict(

View file

@ -16,7 +16,7 @@ class DbManager:
def create_user(self, user, password, host=None):
host = host or self.get_current_host()
password_predicate = f" IDENTIFIED BY '{password}'" if password else ""
self.db.sql(f"CREATE USER '{user}'@'{host}'{password_predicate}")
self.db.sql(f"CREATE USER IF NOT EXISTS '{user}'@'{host}'{password_predicate}")
def delete_user(self, target, host=None):
host = host or self.get_current_host()

View file

@ -124,7 +124,7 @@ class MariaDBConnectionUtil:
"use_unicode": True,
}
if self.user not in (frappe.flags.root_login, "root"):
if self.cur_db_name:
conn_settings["database"] = self.cur_db_name
if self.port:

View file

@ -26,55 +26,58 @@ def get_mariadb_version(version_string: str = ""):
def setup_database(force, verbose, no_mariadb_socket=False):
frappe.local.session = frappe._dict({"user": "Administrator"})
db_user = frappe.conf.db_user
db_name = frappe.local.conf.db_name
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
root_conn = get_root_connection()
dbman = DbManager(root_conn)
dbman_kwargs = {}
if no_mariadb_socket:
dbman_kwargs["host"] = "%"
dbman.create_user(db_user, frappe.conf.db_password, **dbman_kwargs)
if verbose:
print(f"Created or updated user {db_user}")
if force or (db_name not in dbman.get_database_list()):
dbman.delete_user(db_name, **dbman_kwargs)
dbman.drop_database(db_name)
else:
raise Exception(f"Database {db_name} already exists")
dbman.create_user(db_name, frappe.conf.db_password, **dbman_kwargs)
if verbose:
print("Created user %s" % db_name)
dbman.create_database(db_name)
if verbose:
print("Created database %s" % db_name)
dbman.grant_all_privileges(db_name, db_name, **dbman_kwargs)
dbman.grant_all_privileges(db_name, db_user, **dbman_kwargs)
dbman.flush_privileges()
if verbose:
print(f"Granted privileges to user {db_name} and database {db_name}")
print(f"Granted privileges to user {db_user} and database {db_name}")
# close root connection
root_conn.close()
def drop_user_and_database(db_name, root_login, root_password):
frappe.local.db = get_root_connection(root_login, root_password)
def drop_user_and_database(
db_name,
db_user,
):
frappe.local.db = get_root_connection()
dbman = DbManager(frappe.local.db)
dbman.drop_database(db_name)
dbman.delete_user(db_name, host="%")
dbman.delete_user(db_name)
dbman.delete_user(db_user, host="%")
dbman.delete_user(db_user)
def bootstrap_database(db_name, verbose, source_sql=None):
def bootstrap_database(verbose, source_sql=None):
import sys
frappe.connect(db_name=db_name)
frappe.connect()
if not check_database_settings():
print("Database settings do not match expected values; stopping database setup.")
sys.exit(1)
import_db_from_sql(source_sql, verbose)
frappe.connect(db_name=db_name)
frappe.connect()
if "tabDefaultValue" not in frappe.db.get_tables(cached=False):
from click import secho
@ -96,14 +99,13 @@ def import_db_from_sql(source_sql=None, verbose=False):
if not source_sql:
source_sql = os.path.join(os.path.dirname(__file__), "framework_mariadb.sql")
DbManager(frappe.local.db).restore_database(
verbose, db_name, source_sql, db_name, frappe.conf.db_password
verbose, db_name, source_sql, frappe.conf.db_user, frappe.conf.db_password
)
if verbose:
print("Imported from database %s" % source_sql)
def check_database_settings():
check_compatible_versions()
# Check each expected value vs. actuals:
@ -152,24 +154,26 @@ def check_compatible_versions():
)
def get_root_connection(root_login, root_password):
import getpass
def get_root_connection():
if not frappe.local.flags.root_connection:
if not root_login:
root_login = "root"
from getpass import getpass
if not root_password:
root_password = frappe.conf.get("root_password") or None
if not frappe.flags.root_login:
frappe.flags.root_login = (
frappe.conf.get("root_login") or input("Enter mysql super user [root]: ") or "root"
)
if not root_password:
root_password = getpass.getpass("MySQL root password: ")
if not frappe.flags.root_password:
frappe.flags.root_password = frappe.conf.get("root_password") or getpass(
"MySQL root password: "
)
frappe.local.flags.root_connection = frappe.database.get_db(
host=frappe.conf.db_host,
port=frappe.conf.db_port,
user=root_login,
password=root_password,
user=frappe.flags.root_login,
password=frappe.flags.root_password,
cur_db_name=None,
)
return frappe.local.flags.root_connection

View file

@ -161,12 +161,11 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
def get_connection(self):
conn_settings = {
"dbname": self.cur_db_name,
"user": self.user,
"host": self.host,
"password": self.password,
}
if self.user not in (frappe.flags.root_login, "root"):
conn_settings["dbname"] = self.cur_db_name
if self.port:
conn_settings["port"] = self.port

View file

@ -7,27 +7,34 @@ from frappe.utils import cint
def setup_database():
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
root_conn = get_root_connection()
root_conn.commit()
root_conn.sql("end")
root_conn.sql(f"DROP DATABASE IF EXISTS `{frappe.conf.db_name}`")
root_conn.sql(f"DROP USER IF EXISTS {frappe.conf.db_name}")
root_conn.sql(f"CREATE DATABASE `{frappe.conf.db_name}`")
root_conn.sql(f"CREATE user {frappe.conf.db_name} password '{frappe.conf.db_password}'")
root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(frappe.conf.db_name))
root_conn.sql(f'DROP DATABASE IF EXISTS "{frappe.conf.db_name}"')
# If user exists, just update password
if root_conn.sql(f"SELECT 1 FROM pg_roles WHERE rolname='{frappe.conf.db_user}'"):
root_conn.sql(f"ALTER USER \"{frappe.conf.db_user}\" WITH PASSWORD '{frappe.conf.db_password}'")
else:
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:
root_conn.sql("ALTER DATABASE `{0}` OWNER TO {0}".format(frappe.conf.db_name))
root_conn.sql(f'ALTER DATABASE "{frappe.conf.db_name}" OWNER TO "{frappe.conf.db_user}"')
root_conn.close()
def bootstrap_database(db_name, verbose, source_sql=None):
frappe.connect(db_name=db_name)
import_db_from_sql(source_sql, verbose)
frappe.connect(db_name=db_name)
def bootstrap_database(verbose, source_sql=None):
frappe.connect()
import_db_from_sql(source_sql, verbose)
frappe.connect()
if "tabDefaultValue" not in frappe.db.get_tables():
import sys
@ -49,42 +56,39 @@ def import_db_from_sql(source_sql=None, verbose=False):
if not source_sql:
source_sql = os.path.join(os.path.dirname(__file__), "framework_postgres.sql")
DbManager(frappe.local.db).restore_database(
verbose, db_name, source_sql, db_name, frappe.conf.db_password
verbose, db_name, source_sql, frappe.conf.db_user, frappe.conf.db_password
)
if verbose:
print("Imported from database %s" % source_sql)
def get_root_connection(root_login=None, root_password=None):
def get_root_connection():
if not frappe.local.flags.root_connection:
if not root_login:
root_login = frappe.conf.get("root_login") or None
from getpass import getpass
if not root_login:
root_login = input("Enter postgres super user: ")
if not frappe.flags.root_login:
frappe.flags.root_login = (
frappe.conf.get("root_login") or input("Enter postgres super user [postgres]: ") or "postgres"
)
if not root_password:
root_password = frappe.conf.get("root_password") or None
if not root_password:
from getpass import getpass
root_password = getpass("Postgres super user password: ")
if not frappe.flags.root_password:
frappe.flags.root_password = frappe.conf.get("root_password") or getpass(
"Postgres super user password: "
)
frappe.local.flags.root_connection = frappe.database.get_db(
host=frappe.conf.db_host,
port=frappe.conf.db_port,
user=root_login,
password=root_password,
user=frappe.flags.root_login,
password=frappe.flags.root_password,
cur_db_name=frappe.flags.root_login,
)
return frappe.local.flags.root_connection
def drop_user_and_database(db_name, root_login, root_password):
root_conn = get_root_connection(
frappe.flags.root_login or root_login, frappe.flags.root_password or root_password
)
def drop_user_and_database(db_name, db_user):
root_conn = get_root_connection()
root_conn.commit()
root_conn.sql(
"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s",
@ -92,4 +96,4 @@ def drop_user_and_database(db_name, root_login, root_password):
)
root_conn.sql("end")
root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}")
root_conn.sql(f"DROP USER IF EXISTS {db_name}")
root_conn.sql(f"DROP USER IF EXISTS {db_user}")

View file

@ -271,7 +271,7 @@ def set_desktop_icons(visible_list, ignore_duplicate=True):
frappe.db.sql("update `tabDesktop Icon` set blocked=0, hidden=1 where standard=1")
# set as visible if present, or add icon
for module_name in visible_list:
for module_name in list(visible_list):
name = frappe.db.get_value("Desktop Icon", {"module_name": module_name})
if name:
frappe.db.set_value("Desktop Icon", name, "hidden", 0)

View file

@ -44,9 +44,13 @@ frappe.ui.form.on("Event", {
const [ends_on_date] = frm.doc.ends_on
? frm.doc.ends_on.split(" ")
: frm.doc.starts_on.split(" ");
: frm.doc.starts_on?.split(" ") || [];
if (frm.doc.google_meet_link && frappe.datetime.now_date() <= ends_on_date) {
if (
ends_on_date &&
frm.doc.google_meet_link &&
frappe.datetime.now_date() <= ends_on_date
) {
frm.dashboard.set_headline(
__("Join video conference with {0}", [
`<a target='_blank' href='${frm.doc.google_meet_link}'>Google Meet</a>`,

View file

@ -238,7 +238,7 @@ def update_column_order(board_name, order):
new_columns = []
for col in order:
for column in old_columns:
for column in list(old_columns):
if col == column.column_name:
new_columns.append(column)
old_columns.remove(column)

View file

@ -28,6 +28,9 @@ class Note(Document):
# expire this notification in a week (default)
self.expire_notification_on = frappe.utils.add_days(self.creation, 7)
if not self.public and self.notify_on_login:
self.notify_on_login = 0
if not self.content:
self.content = "<span></span>"

View file

@ -186,6 +186,7 @@ class Workspace(Document):
"label": card.get("label"),
"type": "Card Break",
"icon": card.get("icon"),
"description": card.get("description"),
"hidden": card.get("hidden") or False,
"link_count": card.get("link_count"),
"idx": 1 if not self.links else self.links[-1].idx + 1,

View file

@ -8,6 +8,7 @@
"type",
"label",
"icon",
"description",
"hidden",
"link_details_section",
"link_type",
@ -107,12 +108,20 @@
"fieldtype": "Int",
"hidden": 1,
"label": "Link Count"
},
{
"depends_on": "eval:doc.type == \"Card Break\"",
"fieldname": "description",
"fieldtype": "HTML Editor",
"ignore_xss_filter": 1,
"label": "Description",
"max_height": "7rem"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-01 11:23:28.990593",
"modified": "2024-01-23 17:39:16.833318",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Link",
@ -121,5 +130,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -15,6 +15,7 @@ class WorkspaceLink(Document):
from frappe.types import DF
dependencies: DF.Data | None
description: DF.HTMLEditor | None
hidden: DF.Check
icon: DF.Data | None
is_query_report: DF.Check
@ -29,4 +30,5 @@ class WorkspaceLink(Document):
parenttype: DF.Data
type: DF.Literal["Link", "Card Break"]
# end: auto-generated types
pass

View file

@ -11,10 +11,3 @@ class TestForm(FrappeTestCase):
results = get_linked_docs("Role", "System Manager", linkinfo=get_linked_doctypes("Role"))
self.assertTrue("User" in results)
self.assertTrue("DocType" in results)
if __name__ == "__main__":
import unittest
frappe.connect()
unittest.main()

View file

@ -25,7 +25,7 @@ def get_notifications():
"open_count_doctype": {},
"targets": {},
}
if frappe.flags.in_install or not frappe.db.get_single_value("System Settings", "setup_complete"):
if frappe.flags.in_install or not frappe.get_system_settings("setup_complete"):
return out
config = get_notification_config()

View file

@ -476,18 +476,15 @@ frappe.setup.slides_settings = [
onload: function (slide) {
if (frappe.session.user !== "Administrator") {
slide.form.fields_dict.email.$wrapper.toggle(false);
slide.form.fields_dict.password.$wrapper.toggle(false);
// remove password field
delete slide.form.fields_dict.password;
if (frappe.boot.user.first_name || frappe.boot.user.last_name) {
const { first_name, last_name, email } = frappe.boot.user;
if (first_name || last_name) {
slide.form.fields_dict.full_name.set_input(
[frappe.boot.user.first_name, frappe.boot.user.last_name].join(" ").trim()
[first_name, last_name].join(" ").trim()
);
}
delete slide.form.fields_dict.email;
slide.form.fields_dict.email.set_input(email);
slide.form.fields_dict.email.df.read_only = 1;
slide.form.fields_dict.email.refresh();
} else {
slide.form.fields_dict.email.df.reqd = 1;
slide.form.fields_dict.email.refresh();

View file

@ -14,7 +14,7 @@ from frappe.utils.password import update_password
from . import install_fixtures
def get_setup_stages(args):
def get_setup_stages(args): # nosemgrep
# App setup stage functions should not include frappe.db.commit
# That is done by frappe after successful completion of all stages
@ -104,18 +104,18 @@ def process_setup_stages(stages, user_input, is_background_task=False):
frappe.flags.in_setup_wizard = False
def update_global_settings(args):
def update_global_settings(args): # nosemgrep
if args.language and args.language != "English":
set_default_language(get_language_code(args.lang))
frappe.db.commit()
frappe.clear_cache()
update_system_settings(args)
update_user_name(args)
create_or_update_user(args)
set_timezone(args)
def run_post_setup_complete(args):
def run_post_setup_complete(args): # nosemgrep
disable_future_access()
frappe.db.commit()
frappe.clear_cache()
@ -124,20 +124,20 @@ def run_post_setup_complete(args):
frappe.get_cached_doc("System Settings") and frappe.get_doc("System Settings")
def run_setup_success(args):
def run_setup_success(args): # nosemgrep
for hook in frappe.get_hooks("setup_wizard_success"):
frappe.get_attr(hook)(args)
install_fixtures.install()
def get_stages_hooks(args):
def get_stages_hooks(args): # nosemgrep
stages = []
for method in frappe.get_hooks("setup_wizard_stages"):
stages += frappe.get_attr(method)(args)
return stages
def get_setup_complete_hooks(args):
def get_setup_complete_hooks(args): # nosemgrep
return [
{
"status": "Executing method",
@ -154,7 +154,7 @@ def get_setup_complete_hooks(args):
]
def handle_setup_exception(args):
def handle_setup_exception(args): # nosemgrep
frappe.db.rollback()
if args:
traceback = frappe.get_traceback(with_context=True)
@ -163,7 +163,7 @@ def handle_setup_exception(args):
frappe.get_attr(hook)(traceback, args)
def update_system_settings(args):
def update_system_settings(args): # nosemgrep
number_format = get_country_info(args.get("country")).get("number_format", "#,###.##")
# replace these as float number formats, as they have 0 precision
@ -194,72 +194,51 @@ def update_system_settings(args):
frappe.db.set_default("session_recording_start", now())
def update_user_name(args):
def create_or_update_user(args): # nosemgrep
email = args.get("email")
if not email:
return
first_name, last_name = args.get("full_name", ""), ""
if " " in first_name:
first_name, last_name = first_name.split(" ", 1)
if args.get("email"):
if frappe.db.exists("User", args.get("email")):
# running again
return
args["name"] = args.get("email")
if user := frappe.db.get_value("User", email, ["first_name", "last_name"], as_dict=True):
if user.first_name != first_name or user.last_name != last_name:
(
frappe.qb.update("User")
.set("first_name", first_name)
.set("last_name", last_name)
.set("full_name", args.get("full_name"))
).run()
else:
_mute_emails, frappe.flags.mute_emails = frappe.flags.mute_emails, True
doc = frappe.get_doc(
user = frappe.new_doc("User")
user.update(
{
"doctype": "User",
"email": args.get("email"),
"email": email,
"first_name": first_name,
"last_name": last_name,
}
)
user.append_roles(*_get_default_roles())
user.flags.no_welcome_mail = True
user.insert()
doc.append_roles(*_get_default_roles())
doc.flags.no_welcome_mail = True
doc.insert()
frappe.flags.mute_emails = _mute_emails
update_password(args.get("email"), args.get("password"))
elif first_name:
args.update({"name": frappe.session.user, "first_name": first_name, "last_name": last_name})
frappe.db.sql(
"""update `tabUser` SET first_name=%(first_name)s,
last_name=%(last_name)s WHERE name=%(name)s""",
args,
)
if args.get("attach_user"):
attach_user = args.get("attach_user").split(",")
if len(attach_user) == 3:
filename, filetype, content = attach_user
_file = frappe.get_doc(
{
"doctype": "File",
"file_name": filename,
"attached_to_doctype": "User",
"attached_to_name": args.get("name"),
"content": content,
"decode": True,
}
)
_file.save()
fileurl = _file.file_url
frappe.db.set_value("User", args.get("name"), "user_image", fileurl)
if args.get("name"):
add_all_roles_to(args.get("name"))
if args.get("password"):
update_password(email, args.get("password"))
def set_timezone(args):
def set_timezone(args): # nosemgrep
if args.get("timezone"):
for name in frappe.STANDARD_USERS:
frappe.db.set_value("User", name, "time_zone", args.get("timezone"))
def parse_args(args):
def parse_args(args): # nosemgrep
if not args:
args = frappe.local.form_dict
if isinstance(args, str):
@ -344,7 +323,7 @@ def load_user_details():
}
def prettify_args(args):
def prettify_args(args): # nosemgrep
# remove attachments
for key, val in args.items():
if isinstance(val, str) and "data:image" in val:
@ -357,7 +336,7 @@ def prettify_args(args):
return pretty_args
def email_setup_wizard_exception(traceback, args):
def email_setup_wizard_exception(traceback, args): # nosemgrep
if not frappe.conf.setup_wizard_exception_email:
return
@ -402,7 +381,7 @@ def email_setup_wizard_exception(traceback, args):
)
def log_setup_wizard_exception(traceback, args):
def log_setup_wizard_exception(traceback, args): # nosemgrep
with open("../logs/setup-wizard.log", "w+") as setup_log:
setup_log.write(traceback)
setup_log.write(json.dumps(args))

View file

@ -25,6 +25,7 @@
"to_date_field",
"column_break_17",
"dynamic_date_period",
"use_first_day_of_period",
"email_settings",
"email_to",
"day_of_week",
@ -207,10 +208,18 @@
"fieldtype": "Link",
"label": "Sender",
"options": "Email Account"
},
{
"default": "0",
"description": "To begin the date range at the start of the chosen period. For example, if 'Year' is selected as the period, the report will start from January 1st of the current year.",
"fieldname": "use_first_day_of_period",
"fieldtype": "Check",
"depends_on": "eval: doc.dynamic_date_period != 'Daily'",
"label": "Use First Day of Period"
}
],
"links": [],
"modified": "2022-09-08 15:31:55.031023",
"modified": "2024-01-29 11:42:27.433958",
"modified_by": "Administrator",
"module": "Email",
"name": "Auto Email Report",
@ -245,4 +254,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import calendar
import datetime
from datetime import timedelta
from email.utils import formataddr
@ -14,8 +15,13 @@ from frappe.utils import (
add_to_date,
cint,
format_time,
get_first_day,
get_first_day_of_week,
get_link_to_form,
get_quarter_start,
get_url_to_report,
get_year_start,
getdate,
global_date_format,
now,
now_datetime,
@ -57,8 +63,10 @@ class AutoEmailReport(Document):
send_if_data: DF.Check
sender: DF.Link | None
to_date_field: DF.Literal
use_first_day_of_period: DF.Check
user: DF.Link
# end: auto-generated types
def autoname(self):
self.name = _(self.report)
if frappe.db.exists("Auto Email Report", self.name):
@ -92,7 +100,7 @@ class AutoEmailReport(Document):
max_reports_per_user = (
cint(frappe.local.conf.max_reports_per_user) # kept for backward compatibilty
or cint(frappe.db.get_single_value("System Settings", "max_auto_email_report_per_user"))
or cint(frappe.get_system_settings("max_auto_email_report_per_user"))
or 20
)
@ -207,17 +215,37 @@ class AutoEmailReport(Document):
self.filters = frappe.parse_json(self.filters)
to_date = today()
from_date_value = {
"Daily": ("days", -1),
"Weekly": ("weeks", -1),
"Monthly": ("months", -1),
"Quarterly": ("months", -3),
"Half Yearly": ("months", -6),
"Yearly": ("years", -1),
}[self.dynamic_date_period]
from_date = add_to_date(to_date, **{from_date_value[0]: from_date_value[1]})
if self.use_first_day_of_period:
from_date = to_date
if self.dynamic_date_period == "Daily":
from_date = add_to_date(to_date, days=-1)
elif self.dynamic_date_period == "Weekly":
from_date = get_first_day_of_week(from_date)
elif self.dynamic_date_period == "Monthly":
from_date = get_first_day(from_date)
elif self.dynamic_date_period == "Quarterly":
from_date = get_quarter_start(from_date)
elif self.dynamic_date_period == "Half Yearly":
from_date = get_half_year_start(from_date)
elif self.dynamic_date_period == "Yearly":
from_date = get_year_start(from_date)
self.set_date_filters(from_date, to_date)
else:
from_date_value = {
"Daily": ("days", -1),
"Weekly": ("weeks", -1),
"Monthly": ("months", -1),
"Quarterly": ("months", -3),
"Half Yearly": ("months", -6),
"Yearly": ("years", -1),
}[self.dynamic_date_period]
from_date = add_to_date(to_date, **{from_date_value[0]: from_date_value[1]})
self.set_date_filters(from_date, to_date)
def set_date_filters(self, from_date, to_date):
self.filters[self.from_date_field] = from_date
self.filters[self.to_date_field] = to_date
@ -332,3 +360,23 @@ def update_field_types(columns):
col.fieldtype = "Data"
col.options = ""
return columns
DATE_FORMAT = "%Y-%m-%d"
def get_half_year_start(as_str=False):
"""
Returns the first day of the current half-year based on the current date.
"""
today_date = getdate(today())
half_year = 1 if today_date.month <= 6 else 2
year = today_date.year if half_year == 1 else today_date.year + 1
month = 1 if half_year == 1 else 7
day = 1
result_date = datetime.date(year, month, day)
return result_date if not as_str else result_date.strftime(DATE_FORMAT)

View file

@ -131,12 +131,12 @@ class EmailQueue(Document):
def attachments_list(self):
return json.loads(self.attachments) if self.attachments else []
def get_email_account(self):
def get_email_account(self, raise_error=False):
if self.email_account:
return frappe.get_cached_doc("Email Account", self.email_account)
return EmailAccount.find_outgoing(
match_by_email=self.sender, match_by_doctype=self.reference_doctype
match_by_email=self.sender, match_by_doctype=self.reference_doctype, _raise_error=raise_error
)
def is_to_be_sent(self):
@ -158,6 +158,7 @@ class EmailQueue(Document):
return
with SendMailContext(self, smtp_server_instance) as ctx:
ctx.fetch_smtp_server()
message = None
for recipient in self.recipients:
if recipient.is_mail_sent():
@ -233,14 +234,16 @@ class SendMailContext:
smtp_server_instance: SMTPServer = None,
):
self.queue_doc: EmailQueue = queue_doc
self.email_account_doc = queue_doc.get_email_account()
self.smtp_server: SMTPServer = smtp_server_instance or self.email_account_doc.get_smtp_server()
self.smtp_server: SMTPServer = smtp_server_instance
self.sent_to_atleast_one_recipient = any(
rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent()
)
def fetch_smtp_server(self):
self.email_account_doc = self.queue_doc.get_email_account(raise_error=True)
if not self.smtp_server:
self.smtp_server = self.email_account_doc.get_smtp_server()
def __enter__(self):
self.queue_doc.update_status(status="Sending", commit=True)
return self
@ -733,7 +736,7 @@ class QueueBuilder:
recipients = list(set([r] + self.final_cc() + self.bcc))
q = EmailQueue.new({**queue_data, **{"recipients": recipients}}, ignore_permissions=True)
if not smtp_server_instance:
email_account = q.get_email_account()
email_account = q.get_email_account(raise_error=True)
smtp_server_instance = email_account.get_smtp_server()
with suppress(Exception):

View file

@ -3,7 +3,7 @@
"allow_guest_to_view": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:31",
"description": "Create and Send Newsletters",
"description": "Create and send emails to a specific group of subscribers periodically.",
"doctype": "DocType",
"document_type": "Other",
"engine": "InnoDB",
@ -244,8 +244,7 @@
"fieldname": "campaign",
"fieldtype": "Link",
"label": "Campaign",
"options": "Marketing Campaign",
"reqd": 0
"options": "Marketing Campaign"
}
],
"has_web_view": 1,
@ -254,7 +253,7 @@
"index_web_pages_for_search": 1,
"is_published_field": "published",
"links": [],
"modified": "2023-12-29 18:04:13.270523",
"modified": "2024-01-30 14:05:50.645802",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",

View file

@ -673,7 +673,7 @@ class InboundMail(Email):
content = self.content
for file in attachments:
if file.name in self.cid_map and self.cid_map[file.name]:
content = content.replace(f"cid:{self.cid_map[file.name]}", file.file_url)
content = content.replace(f"cid:{self.cid_map[file.name]}", file.unique_url)
return content
def is_notification(self):

View file

@ -4,7 +4,7 @@
"allow_rename": 1,
"autoname": "field:currency_name",
"creation": "2013-01-28 10:06:02",
"description": "**Currency** Master",
"description": "Currency list stores the currency value, its symbol and fraction unit",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@ -82,7 +82,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-07-04 09:42:52.425440",
"modified": "2024-01-30 13:18:12.053557",
"modified_by": "Administrator",
"module": "Geo",
"name": "Currency",
@ -102,6 +102,10 @@
"share": 1,
"write": 1
},
{
"read": 1,
"role": "Accounts Manager"
},
{
"read": 1,
"role": "Accounts User"

View file

@ -0,0 +1,26 @@
from jinja2.ext import babel_extract
from .utils import extract_messages_from_code
def extract(*args, **kwargs):
"""Extract messages from Jinja and JS microtemplates.
Reuse the babel_extract function from jinja2.ext, but handle our own implementation of `_()`.
To handle JS microtemplates, parse all code again using regex."""
fileobj = args[0] or kwargs["fileobj"]
print(fileobj.name)
code = fileobj.read().decode("utf-8")
for lineno, funcname, messages, comments in babel_extract(*args, **kwargs):
if funcname == "_" and isinstance(messages, tuple) and len(messages) > 1:
funcname = "pgettext"
messages = (messages[-1], messages[0]) # (context, message)
yield lineno, funcname, messages, comments
for lineno, message, context in extract_messages_from_code(code):
if context:
yield lineno, "pgettext", (context, message), []
else:
yield lineno, "_", message, []

View file

@ -1,11 +0,0 @@
from jinja2.ext import babel_extract
def extract(*args, **kwargs):
"""Reuse the babel_extract function from jinja2.ext, but handle our own implementation of `_()`"""
for lineno, funcname, messages, comments in babel_extract(*args, **kwargs):
if funcname == "_" and isinstance(messages, tuple) and len(messages) > 1:
funcname = "pgettext"
messages = (messages[-1], messages[0]) # (context, message)
yield lineno, funcname, messages, comments

View file

@ -0,0 +1,81 @@
import re
import frappe
TRANSLATE_PATTERN = re.compile(
r"_\(\s*" # starts with literal `_(`, ignore following whitespace/newlines
# BEGIN: message search
r"([\"']{,3})" # start of message string identifier - allows: ', ", """, '''; 1st capture group
r"(?P<message>((?!\1).)*)" # Keep matching until string closing identifier is met which is same as 1st capture group
r"\1" # match exact string closing identifier
# END: message search
# BEGIN: python context search
r"(\s*,\s*context\s*=\s*" # capture `context=` with ignoring whitespace
r"([\"'])" # start of context string identifier; 5th capture group
r"(?P<py_context>((?!\5).)*)" # capture context string till closing id is found
r"\5" # match context string closure
r")?" # match 0 or 1 context strings
# END: python context search
# BEGIN: JS context search
r"(\s*,\s*(.)*?\s*(,\s*" # skip message format replacements: ["format", ...] | null | []
r"([\"'])" # start of context string; 11th capture group
r"(?P<js_context>((?!\11).)*)" # capture context string till closing id is found
r"\11" # match context string closure
r")*"
r")*" # match one or more context string
# END: JS context search
r"\s*\)" # Closing function call ignore leading whitespace/newlines
)
def extract_messages_from_code(code):
"""
Extracts translatable strings from a code file
:param code: code from which translatable files are to be extracted
"""
from jinja2 import TemplateError
from frappe.model.utils import InvalidIncludePath, render_include
try:
code = frappe.as_unicode(render_include(code))
# Exception will occur when it encounters John Resig's microtemplating code
except (TemplateError, ImportError, InvalidIncludePath, OSError) as e:
if isinstance(e, InvalidIncludePath) and hasattr(frappe.local, "message_log"):
frappe.clear_last_message()
messages = []
for m in TRANSLATE_PATTERN.finditer(code):
message = m.group("message")
context = m.group("py_context") or m.group("js_context")
pos = m.start()
if is_translatable(message):
messages.append([pos, message, context])
return add_line_number(messages, code)
def is_translatable(m):
return bool(
re.search("[a-zA-Z]", m)
and not m.startswith("fa fa-")
and not m.endswith("px")
and not m.startswith("eval:")
)
def add_line_number(messages, code):
ret = []
messages = sorted(messages, key=lambda x: x[0])
newlines = [m.start() for m in re.compile(r"\n").finditer(code)]
line = 1
newline_i = 0
for pos, message, context in messages:
while newline_i < len(newlines) and pos > newlines[newline_i]:
line += 1
newline_i += 1
ret.append([line, message, context])
return ret

View file

@ -250,7 +250,7 @@ def check_write_permission(doctype: str = None, name: str = None):
if doctype and name:
try:
doc = frappe.get_doc(doctype, name)
doc.has_permission("write")
doc.check_permission("write")
except frappe.DoesNotExistError:
# doc has not been inserted yet, name is set to "new-some-doctype"
check_doctype = True

View file

@ -532,15 +532,15 @@ standard_help_items = [
# log doctype cleanups to automatically add in log settings
default_log_clearing_doctypes = {
"Error Log": 30,
"Activity Log": 90,
"Error Log": 14,
"Email Queue": 30,
"Scheduled Job Log": 90,
"Route History": 90,
"Submission Queue": 30,
"Prepared Report": 30,
"Scheduled Job Log": 7,
"Submission Queue": 7,
"Prepared Report": 14,
"Webhook Request Log": 30,
"Integration Request": 90,
"Unhandled Email": 30,
"Reminder": 30,
"Integration Request": 90,
"Activity Log": 90,
"Route History": 90,
}

View file

@ -20,9 +20,10 @@ from frappe.utils.dashboard import sync_dashboards
from frappe.utils.synchronization import filelock
def _is_scheduler_enabled() -> bool:
def _is_scheduler_enabled(site) -> bool:
enable_scheduler = False
try:
frappe.init(site=site)
frappe.connect()
enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler"))
except Exception:
@ -49,6 +50,7 @@ def _new_site(
db_type=None,
db_host=None,
db_port=None,
db_user=None,
setup_db=True,
):
"""Install a new Frappe site"""
@ -77,7 +79,7 @@ def _new_site(
try:
# enable scheduler post install?
enable_scheduler = _is_scheduler_enabled()
enable_scheduler = _is_scheduler_enabled(site)
except Exception:
enable_scheduler = False
@ -97,6 +99,7 @@ def _new_site(
db_type=db_type,
db_host=db_host,
db_port=db_port,
db_user=db_user,
no_mariadb_socket=no_mariadb_socket,
setup=setup_db,
)
@ -135,6 +138,7 @@ def install_db(
db_type=None,
db_host=None,
db_port=None,
db_user=None,
no_mariadb_socket=False,
setup=True,
):
@ -156,6 +160,7 @@ def install_db(
db_type=db_type,
db_host=db_host,
db_port=db_port,
db_user=db_user,
)
frappe.flags.in_install_db = True
@ -166,7 +171,6 @@ def install_db(
setup_database(force, verbose, no_mariadb_socket)
bootstrap_database(
db_name=frappe.conf.db_name,
verbose=verbose,
source_sql=source_sql,
)
@ -533,11 +537,23 @@ def init_singles():
def make_conf(
db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None
db_name=None,
db_password=None,
site_config=None,
db_type=None,
db_host=None,
db_port=None,
db_user=None,
):
site = frappe.local.site
make_site_config(
db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port
db_name,
db_password,
site_config,
db_type=db_type,
db_host=db_host,
db_port=db_port,
db_user=db_user,
)
sites_path = frappe.local.sites_path
frappe.destroy()
@ -545,7 +561,13 @@ def make_conf(
def make_site_config(
db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None
db_name=None,
db_password=None,
site_config=None,
db_type=None,
db_host=None,
db_port=None,
db_user=None,
):
frappe.create_folder(os.path.join(frappe.local.site_path))
site_file = get_site_config_path()
@ -563,6 +585,8 @@ def make_site_config(
if db_port:
site_config["db_port"] = db_port
site_config["db_user"] = db_user or db_name
with open(site_file, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))

View file

@ -239,7 +239,7 @@ def check_google_calendar(account, google_calendar):
# If no Calendar ID create a new Calendar
calendar = {
"summary": account.calendar_name,
"timeZone": frappe.db.get_single_value("System Settings", "time_zone"),
"timeZone": frappe.get_system_settings("time_zone"),
}
created_calendar = google_calendar.calendars().insert(body=calendar).execute()
frappe.db.set_value(

View file

@ -13,13 +13,14 @@ frappe.ui.form.on("Google Drive", {
frappe.realtime.on("upload_to_google_drive", (data) => {
if (data.progress) {
const progress_title = __("Uploading to Google Drive");
frm.dashboard.show_progress(
"Uploading to Google Drive",
progress_title,
(data.progress / data.total) * 100,
__("{0}", [data.message])
data.message
);
if (data.progress === data.total) {
frm.dashboard.hide_progress("Uploading to Google Drive");
frm.dashboard.hide_progress(progress_title);
}
}
});

View file

@ -170,7 +170,7 @@ def upload_system_backup_to_google_drive():
validate_file_size()
if frappe.flags.create_new_backup:
set_progress(1, "Backing up Data.")
set_progress(1, _("Backing up Data."))
backup = new_backup()
file_urls = []
file_urls.append(backup.backup_path_db)
@ -196,12 +196,12 @@ def upload_system_backup_to_google_drive():
frappe.throw(_("Google Drive - Could not locate - {0}").format(e))
try:
set_progress(2, "Uploading backup to Google Drive.")
set_progress(2, _("Uploading backup to Google Drive."))
google_drive.files().create(body=file_metadata, media_body=media, fields="id").execute()
except HttpError as e:
send_email(False, "Google Drive", "Google Drive", "email", error_status=e)
set_progress(3, "Uploading successful.")
set_progress(3, _("Uploading successful."))
frappe.db.set_single_value("Google Drive", "last_backup_on", frappe.utils.now_datetime())
send_email(True, "Google Drive", "Google Drive", "email")
return _("Google Drive Backup Successful.")

View file

@ -39,8 +39,7 @@
"description": "The browser API key obtained from the Google Cloud Console under <a href=\"https://console.cloud.google.com/apis/credentials\">\n\"APIs &amp; Services\" &gt; \"Credentials\"\n</a>",
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key",
"mandatory_depends_on": "google_drive_picker_enabled"
"label": "API Key"
},
{
"depends_on": "enable",
@ -76,7 +75,7 @@
],
"issingle": 1,
"links": [],
"modified": "2021-06-29 18:26:07.094851",
"modified": "2024-01-16 13:19:22.365362",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Settings",
@ -96,5 +95,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View file

@ -21,6 +21,7 @@ class GoogleSettings(Document):
enable: DF.Check
google_drive_picker_enabled: DF.Check
# end: auto-generated types
pass
@ -34,6 +35,5 @@ def get_file_picker_settings():
return {
"enabled": True,
"appId": google_settings.app_id,
"developerKey": google_settings.api_key,
"clientId": google_settings.client_id,
}

View file

@ -40,4 +40,3 @@ class TestGoogleSettings(FrappeTestCase):
self.assertEqual(True, settings.get("enabled", False))
self.assertEqual("test_client_id", settings.get("clientId", ""))
self.assertEqual("test_app_id", settings.get("appId", ""))
self.assertEqual("test_api_key", settings.get("developerKey", ""))

View file

@ -438,7 +438,7 @@ class LDAP_TestCase:
for user_role in updated_user_roles: # match each users role mapped to ldap groups
self.assertTrue(
role_to_group_map[user_role] in test_user_data[test_user],
f"during sync_roles(), the user was given role {user_role} which should not have occured",
f"during sync_roles(), the user was given role {user_role} which should not have occurred",
)
@mock_ldap_connection

View file

@ -41,9 +41,6 @@ def send_email(success, service_name, doctype, email_field, error_status=None):
def get_recipients(doctype, email_field):
if not frappe.db:
frappe.connect()
return split_emails(frappe.db.get_value(doctype, None, email_field))
@ -52,7 +49,7 @@ def get_latest_backup_file(with_files=False):
odb = BackupGenerator(
frappe.conf.db_name,
frappe.conf.db_name,
frappe.conf.db_user,
frappe.conf.db_password,
db_host=frappe.conf.db_host,
db_port=frappe.conf.db_port,
@ -110,7 +107,7 @@ def generate_files_backup():
backup = BackupGenerator(
frappe.conf.db_name,
frappe.conf.db_name,
frappe.conf.db_user,
frappe.conf.db_password,
db_host=frappe.conf.db_host,
db_port=frappe.conf.db_port,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -134,22 +134,22 @@ log_types = (
)
std_fields = [
{"fieldname": "name", "fieldtype": "Link", "label": _lt("ID")},
{"fieldname": "owner", "fieldtype": "Link", "label": _lt("Created By"), "options": "User"},
{"fieldname": "idx", "fieldtype": "Int", "label": _lt("Index")},
{"fieldname": "creation", "fieldtype": "Datetime", "label": _lt("Created On")},
{"fieldname": "modified", "fieldtype": "Datetime", "label": _lt("Last Updated On")},
{"fieldname": "name", "fieldtype": "Link", "label": "ID"},
{"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"},
{"fieldname": "idx", "fieldtype": "Int", "label": "Index"},
{"fieldname": "creation", "fieldtype": "Datetime", "label": "Created On"},
{"fieldname": "modified", "fieldtype": "Datetime", "label": "Last Updated On"},
{
"fieldname": "modified_by",
"fieldtype": "Link",
"label": _lt("Last Updated By"),
"label": "Last Updated By",
"options": "User",
},
{"fieldname": "_user_tags", "fieldtype": "Data", "label": _lt("Tags")},
{"fieldname": "_liked_by", "fieldtype": "Data", "label": _lt("Liked By")},
{"fieldname": "_comments", "fieldtype": "Text", "label": _lt("Comments")},
{"fieldname": "_assign", "fieldtype": "Text", "label": _lt("Assigned To")},
{"fieldname": "docstatus", "fieldtype": "Int", "label": _lt("Document Status")},
{"fieldname": "_user_tags", "fieldtype": "Data", "label": "Tags"},
{"fieldname": "_liked_by", "fieldtype": "Data", "label": "Liked By"},
{"fieldname": "_comments", "fieldtype": "Text", "label": "Comments"},
{"fieldname": "_assign", "fieldtype": "Text", "label": "Assigned To"},
{"fieldname": "docstatus", "fieldtype": "Int", "label": "Document Status"},
]
@ -230,6 +230,9 @@ def get_permitted_fields(
if permission_type is None:
permission_type = "select" if frappe.only_has_select_perm(doctype, user=user) else "read"
meta_fields = meta.default_fields.copy()
optional_meta_fields = [x for x in optional_fields if x in valid_columns]
if permitted_fields := meta.get_permitted_fieldnames(
parenttype=parenttype,
user=user,
@ -239,15 +242,12 @@ def get_permitted_fields(
if permission_type == "select":
return permitted_fields
meta_fields = meta.default_fields.copy()
optional_meta_fields = [x for x in optional_fields if x in valid_columns]
if meta.istable:
meta_fields.extend(child_table_fields)
return meta_fields + permitted_fields + optional_meta_fields
return []
return meta_fields + optional_meta_fields
def is_default_field(fieldname: str) -> bool:

View file

@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import datetime
import json
import weakref
from functools import cached_property
from typing import TYPE_CHECKING, TypeVar
@ -163,6 +164,7 @@ class BaseDocument:
state.pop("meta", None)
state.pop("permitted_fieldnames", None)
state.pop("_parent_doc", None)
def update(self, d):
"""Update multiple fields of a doctype using a dictionary of key-value pairs.
@ -261,11 +263,28 @@ class BaseDocument:
ret_value = self._init_child(value, key)
table.append(ret_value)
# reference parent document
ret_value.parent_doc = self
# reference parent document but with weak reference, parent_doc will be deleted if self is garbage collected.
ret_value.parent_doc = weakref.ref(self)
return ret_value
@property
def parent_doc(self):
parent_doc_ref = getattr(self, "_parent_doc", None)
if isinstance(parent_doc_ref, BaseDocument):
return parent_doc_ref
elif isinstance(parent_doc_ref, weakref.ReferenceType):
return parent_doc_ref()
@parent_doc.setter
def parent_doc(self, value):
self._parent_doc = value
@parent_doc.deleter
def parent_doc(self):
self._parent_doc = None
def extend(self, key, value):
try:
value = iter(value)
@ -1231,7 +1250,7 @@ class BaseDocument:
ref_doc = frappe.new_doc(self.doctype)
else:
# get values from old doc
if self.get("parent_doc"):
if self.parent_doc:
parent_doc = self.parent_doc.get_latest()
child_docs = [d for d in parent_doc.get(self.parentfield) if d.name == self.name]
if not child_docs:

View file

@ -217,6 +217,10 @@ class DatabaseQuery:
args = self.prepare_args()
args.limit = self.add_limit()
if not args.fields:
# apply_fieldlevel_read_permissions has likely removed ALL the fields that user asked for
return []
if args.conditions:
args.conditions = "where " + args.conditions
@ -754,7 +758,7 @@ class DatabaseQuery:
ref_doctype = field.options if field else f.doctype
lft, rgt = "", ""
if f.value:
lft, rgt = frappe.db.get_value(ref_doctype, f.value, ["lft", "rgt"])
lft, rgt = frappe.db.get_value(ref_doctype, f.value, ["lft", "rgt"]) or (0, 0)
# Get descendants elements of a DocType with a tree structure
if f.operator.lower() in (

View file

@ -133,6 +133,7 @@ def delete_doc(
doctype=doc.doctype,
name=doc.name,
now=frappe.flags.in_test,
enqueue_after_commit=True,
)
# clear cache for Document

View file

@ -197,11 +197,7 @@ def map_fields(source_doc, target_doc, table_map, source_parent):
for d in source_doc.meta.get("fields")
if (d.no_copy == 1 or d.fieldtype in table_fields)
]
+ [
d.fieldname
for d in target_doc.meta.get("fields")
if (d.no_copy == 1 or d.fieldtype in table_fields)
]
+ [d.fieldname for d in target_doc.meta.get("fields") if (d.fieldtype in table_fields)]
+ list(default_fields)
+ list(child_table_fields)
+ list(table_map.get("field_no_map", []))

View file

@ -596,6 +596,10 @@ class Meta(Document):
self.get_permlevel_access(permission_type=permission_type, parenttype=parenttype, user=user)
)
if 0 not in permlevel_access and permission_type in ("read", "select"):
if frappe.share.get_shared(self.name, user, rights=[permission_type], limit=1):
permlevel_access.add(0)
permitted_fieldnames.extend(
df.fieldname
for df in self.get_fieldnames_with_value(

View file

@ -66,7 +66,7 @@ def render_include(content):
if "{% include" in content:
paths = INCLUDE_DIRECTIVE_PATTERN.findall(content)
if not paths:
frappe.throw(_("Invalid include path"), InvalidIncludePath)
raise InvalidIncludePath
for path in paths:
app, app_path = path.split("/", 1)

View file

@ -343,7 +343,10 @@ def has_user_permission(doc, user=None, debug=False):
debug and _debug_log("User permission bypassed because user can modify user permissions.")
return True
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
# don't apply strict user permissions for single doctypes since they contain empty link fields
apply_strict_user_permissions = (
False if doc.meta.issingle else frappe.get_system_settings("apply_strict_user_permissions")
)
if apply_strict_user_permissions:
debug and _debug_log("Strict user permissions will be applied")

View file

@ -2,7 +2,60 @@
// For license information, please see license.txt
frappe.ui.form.on("Letter Head", {
setup(frm) {
frm.get_field("instructions").html(INSTRUCTIONS);
},
refresh: function (frm) {
frm.flag_public_attachments = true;
},
validate: (frm) => {
["header_script", "footer_script"].forEach((field) => {
if (!frm.doc[field]) return;
try {
eval(frm.doc[field]);
} catch (e) {
frappe.throw({
title: __("Error in Header/Footer Script"),
indicator: "orange",
message: '<pre class="small"><code>' + e.stack + "</code></pre>",
});
}
});
},
});
const INSTRUCTIONS = `<h4>${__("Letter Head Scripts")}</h4>
<p>${__("Header/Footer scripts can be used to add dynamic behaviours.")}</p>
<pre>
<code>
// ${__(
"The following Header Script will add the current date to an element in 'Header HTML' with class 'header-content'"
)}
var el = document.getElementsByClassName("header-content");
if (el.length > 0) {
el[0].textContent += " " + new Date().toGMTString();
}
</code>
</pre>
<p>${__("You can also access wkhtmltopdf variables (valid only in PDF print):")}</p>
<pre>
<code>
// ${__("Get Header and Footer wkhtmltopdf variables")}
// ${__("Snippet and more variables: {0}", ["https://wkhtmltopdf.org/usage/wkhtmltopdf.txt"])}
var vars = {};
var query_strings_from_url = document.location.search.substring(1).split('&');
for (var query_string in query_strings_from_url) {
if (query_strings_from_url.hasOwnProperty(query_string)) {
var temp_var = query_strings_from_url[query_string].split('=', 2);
vars[temp_var[0]] = decodeURI(temp_var[1]);
}
}
var el = document.getElementsByClassName("header-content");
if (el.length > 0 && vars["page"] == 1) {
el[0].textContent += " : " + vars["date"];
}
</code>
</pre>`;

View file

@ -26,7 +26,11 @@
"footer_image",
"footer_image_height",
"footer_image_width",
"footer_align"
"footer_align",
"scripts_section",
"header_script",
"footer_script",
"instructions"
],
"fields": [
{
@ -162,13 +166,40 @@
"fieldtype": "Select",
"label": "Footer Based On",
"options": "Image\nHTML"
},
{
"depends_on": "eval:!doc.__islocal && doc.source==='HTML'",
"fieldname": "header_script",
"fieldtype": "Code",
"label": "Header Script",
"options": "Javascript"
},
{
"depends_on": "eval:!doc.__islocal && doc.footer_source==='HTML'",
"fieldname": "footer_script",
"fieldtype": "Code",
"label": "Footer Script",
"options": "Javascript"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.header_script || doc.footer_script",
"fieldname": "scripts_section",
"fieldtype": "Section Break",
"label": "Scripts"
},
{
"fieldname": "instructions",
"fieldtype": "HTML",
"label": "Instructions",
"read_only": 1
}
],
"icon": "fa fa-font",
"idx": 1,
"links": [],
"max_attachments": 3,
"modified": "2023-12-08 15:52:37.525003",
"modified": "2023-12-21 16:19:37.525003",
"modified_by": "Administrator",
"module": "Printing",
"name": "Letter Head",

View file

@ -384,13 +384,13 @@ frappe.ui.form.PrintView = class {
this.print_wrapper.find(".preview-beta-wrapper").hide();
this.print_wrapper.find(".print-preview-wrapper").show();
const $print_format = this.print_wrapper.find("iframe");
this.$print_format_body = $print_format.contents();
this.get_print_html((out) => {
if (!out.html) {
out.html = this.get_no_preview_html();
}
const $print_format = this.print_wrapper.find("iframe");
this.$print_format_body = $print_format.contents();
this.setup_print_format_dom(out, $print_format);
const print_height = $print_format.get(0).offsetHeight;

View file

@ -121,9 +121,7 @@
</symbol>
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-external-link">
<path d="M9.75003 7.83333V9C9.75003 9.82843 9.07846 10.5 8.25003 10.5H3.25C2.42157 10.5 1.75 9.82843 1.75 9V4C1.75 3.17158 2.42151 2.50001 3.24993 2.50001C3.62327 2.5 4.02808 2.5 4.4167 2.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.75 1.5H10.25V4.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.75 5L9.75 2" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" stroke-width="0" clip-rule="evenodd" d="M4.52409 7.47575C4.3889 7.34059 4.3889 7.12141 4.52409 6.98623L9.31795 2.19237H6.69231C6.50113 2.19237 6.34615 2.03739 6.34615 1.84622C6.34615 1.65505 6.50113 1.50007 6.69231 1.50007H10.1469C10.2283 1.49846 10.3104 1.52546 10.3764 1.5811C10.452 1.64459 10.5 1.7398 10.5 1.84622V5.30773C10.5 5.4989 10.345 5.65388 10.1538 5.65388C9.9627 5.65388 9.80769 5.4989 9.80769 5.30773V2.68173L5.01362 7.47575C4.87844 7.61095 4.65927 7.61095 4.52409 7.47575ZM2.19231 3.23082C2.19231 2.6573 2.65724 2.19237 3.23077 2.19237H4.47692C4.6681 2.19237 4.82308 2.03739 4.82308 1.84622C4.82308 1.65505 4.6681 1.50007 4.47692 1.50007H3.23077C2.27489 1.50007 1.5 2.27496 1.5 3.23082V8.76924C1.5 9.72511 2.27489 10.5 3.23077 10.5H8.76923C9.7251 10.5 10.5 9.72511 10.5 8.76924V7.5231C10.5 7.33193 10.345 7.17695 10.1538 7.17695C9.9627 7.17695 9.80769 7.33193 9.80769 7.5231V8.76924C9.80769 9.34275 9.34274 9.8077 8.76923 9.8077H3.23077C2.65724 9.8077 2.19231 9.34275 2.19231 8.76924V3.23082Z" fill="var(--icon-stroke)"/>
</symbol>
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" fill="#112B42" id="icon-up">

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View file

@ -185,6 +185,11 @@ function is_filter_applied() {
}
}
function open_child_doctype() {
if (!props.field?.df?.options) return;
window.open(`/app/doctype/${props.field.df.options}`, "_blank");
}
onMounted(() => selected.value && label_input.value.focus_on_label());
</script>
@ -199,6 +204,7 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
<component
:is="component"
:df="field.df"
:is-customize-form="store.is_customize_form"
:data-fieldname="field.df.fieldname"
:data-fieldtype="field.df.fieldtype"
>
@ -216,7 +222,7 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
class="help-icon"
v-if="field.df.documentation_url"
v-html="frappe.utils.icon('help', 'sm')"
></div>
/>
</div>
</template>
<template #actions>
@ -227,7 +233,7 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
:class="is_filter_applied()"
@click="edit_filters"
>
<div v-html="frappe.utils.icon('filter', 'sm')"></div>
<div v-html="frappe.utils.icon('filter', 'sm')" />
</button>
<AddFieldButton ref="add_field_ref" :column="column" :field="field">
<div v-html="frappe.utils.icon('add', 'sm')" />
@ -240,21 +246,29 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
"
@click="move_fields_to_column"
>
<div v-html="frappe.utils.icon('move', 'sm')"></div>
<div v-html="frappe.utils.icon('move', 'sm')" />
</button>
<button
class="btn btn-xs btn-icon"
:title="__('Duplicate field')"
@click.stop="duplicate_field"
>
<div v-html="frappe.utils.icon('duplicate', 'sm')"></div>
<div v-html="frappe.utils.icon('duplicate', 'sm')" />
</button>
<button
v-if="field.df.fieldtype === 'Table' && field.df.options"
class="btn btn-xs btn-icon"
@click="open_child_doctype"
:title="__(`Edit ${field.df.options} Doctype`)"
>
<div v-html="frappe.utils.icon('external-link', 'sm')" />
</button>
<button
class="btn btn-xs btn-icon"
:title="__('Remove field')"
@click.stop="remove_field"
>
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
<div v-html="frappe.utils.icon('remove', 'sm')" />
</button>
</div>
</template>

View file

@ -2,7 +2,7 @@
import { get_table_columns, load_doctype_model } from "../../utils";
import { computedAsync } from "@vueuse/core";
const props = defineProps(["df"]);
const props = defineProps(["df", "is-customize-form"]);
let table_columns = computedAsync(async () => {
let doctype = props.df.options;
@ -13,6 +13,13 @@ let table_columns = computedAsync(async () => {
let child_doctype = frappe.get_meta(doctype);
return get_table_columns(props.df, child_doctype);
}, []);
function open_new_child_doctype_dialog() {
let is_custom = props.isCustomizeForm;
frappe.model.with_doctype("DocType").then(() => {
frappe.listview_settings["DocType"].new_doctype_dialog({ is_child: 1, is_custom });
});
}
</script>
<template>
@ -46,7 +53,15 @@ let table_columns = computedAsync(async () => {
:alt="__('Grid Empty State')"
class="grid-empty-illustration"
/>
{{ __("No Data") }}
<!-- render this button when there are no columns, which means that options is not added for the table -->
<button
class="btn btn-xs btn-secondary"
@click="open_new_child_doctype_dialog"
v-if="!table_columns.length"
>
{{ __("Create Child Doctype") }}
</button>
<p v-else>{{ __("No Data") }}</p>
</div>
<!-- description -->

View file

@ -85,8 +85,13 @@ export const useStore = defineStore("form-builder-store", () => {
async function fetch() {
doc.value = frm.value.doc;
if (doctype.value.startsWith("new-doctype-") && !doc.value.fields) {
doc.value.fields = [get_df("Data", "", __("Title"))];
if (doctype.value.startsWith("new-doctype-") && !doc.value.fields?.length) {
frappe.model.with_doctype("DocType").then(() => {
frappe.listview_settings["DocType"].new_doctype_dialog();
});
// redirect to /doctype
frappe.set_route("List", "DocType");
return;
}
if (!get_docfields.value.length) {

Some files were not shown because too many files have changed in this diff Show more