Merge branch 'develop' into add-using-cached-build-flag
This commit is contained in:
commit
2b3ab02d79
43 changed files with 1886 additions and 1556 deletions
3
.github/workflows/linters.yml
vendored
3
.github/workflows/linters.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
26
.github/workflows/pre-commit.yml
vendored
Normal file
26
.github/workflows/pre-commit.yml
vendored
Normal 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
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const chalk = require("chalk");
|
||||
let bench_path;
|
||||
if (process.env.FRAPPE_BENCH_ROOT) {
|
||||
bench_path = process.env.FRAPPE_BENCH_ROOT;
|
||||
} else {
|
||||
const frappe_path = path.resolve(__dirname, "..");
|
||||
bench_path = path.resolve(frappe_path, "..", "..");
|
||||
}
|
||||
|
||||
const frappe_path = path.resolve(__dirname, "..");
|
||||
const bench_path = path.resolve(frappe_path, "..", "..");
|
||||
const sites_path = path.resolve(bench_path, "sites");
|
||||
const apps_path = path.resolve(bench_path, "apps");
|
||||
const sites_path = path.resolve(bench_path, "sites");
|
||||
const assets_path = path.resolve(sites_path, "assets");
|
||||
const app_list = get_apps_list();
|
||||
|
||||
|
|
|
|||
|
|
@ -269,6 +269,10 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
|
|||
|
||||
local.initialised = True
|
||||
|
||||
# Set the user as database name if not set in config
|
||||
if local.conf and local.conf.db_name is not None and local.conf.db_user is None:
|
||||
local.conf.db_user = local.conf.db_name
|
||||
|
||||
|
||||
def connect(
|
||||
site: str | None = None, db_name: str | None = None, set_admin_as_user: bool = True
|
||||
|
|
@ -287,7 +291,7 @@ def connect(
|
|||
local.db = get_db(
|
||||
host=local.conf.db_host,
|
||||
port=local.conf.db_port,
|
||||
user=db_name or local.conf.db_name,
|
||||
user=local.conf.db_user or db_name,
|
||||
password=None,
|
||||
)
|
||||
if set_admin_as_user:
|
||||
|
|
@ -300,12 +304,12 @@ 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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
@ -1058,7 +1061,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 +1343,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.
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@ class TestContact(FrappeTestCase):
|
|||
|
||||
def test_check_default_phone_and_mobile(self):
|
||||
phones = [
|
||||
{"phone": "+91 0000000000", "is_primary_phone": 0, "is_primary_mobile_no": 0},
|
||||
{"phone": "+91 0000000001", "is_primary_phone": 0, "is_primary_mobile_no": 0},
|
||||
{"phone": "+91 0000000002", "is_primary_phone": 1, "is_primary_mobile_no": 0},
|
||||
{"phone": "+91 0000000003", "is_primary_phone": 0, "is_primary_mobile_no": 1},
|
||||
{"phone": "+91 0000000010", "is_primary_phone": 0, "is_primary_mobile_no": 0},
|
||||
{"phone": "+91 0000000011", "is_primary_phone": 0, "is_primary_mobile_no": 0},
|
||||
{"phone": "+91 0000000012", "is_primary_phone": 1, "is_primary_mobile_no": 0},
|
||||
{"phone": "+91 0000000013", "is_primary_phone": 0, "is_primary_mobile_no": 1},
|
||||
]
|
||||
contact = create_contact("Phone", "Mr", phones=phones)
|
||||
|
||||
self.assertEqual(contact.phone, "+91 0000000002")
|
||||
self.assertEqual(contact.mobile_no, "+91 0000000003")
|
||||
self.assertEqual(contact.phone, "+91 0000000012")
|
||||
self.assertEqual(contact.mobile_no, "+91 0000000013")
|
||||
|
||||
def test_get_full_name(self):
|
||||
self.assertEqual(get_full_name(first="John"), "John")
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ def create_linked_contact(link_list, address):
|
|||
}
|
||||
)
|
||||
contact.add_email("test_contact@example.com", is_primary=True)
|
||||
contact.add_phone("+91 0000000000", is_primary_phone=True)
|
||||
contact.add_phone("+91 0000000020", is_primary_phone=True)
|
||||
|
||||
for name in link_list:
|
||||
contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})
|
||||
|
|
@ -105,7 +105,7 @@ class TestAddressesAndContacts(FrappeTestCase):
|
|||
"_Test First Name",
|
||||
"_Test Last Name",
|
||||
"_Test Address-Billing",
|
||||
"+91 0000000000",
|
||||
"+91 0000000020",
|
||||
"",
|
||||
"test_contact@example.com",
|
||||
1,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@
|
|||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"default": "2",
|
||||
"fieldname": "simultaneous_sessions",
|
||||
"fieldtype": "Int",
|
||||
"label": "Simultaneous Sessions"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -36,21 +36,17 @@ def bootstrap_database(db_name, verbose=None, source_sql=None):
|
|||
return frappe.database.mariadb.setup_db.bootstrap_database(db_name, 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):
|
||||
|
|
|
|||
|
|
@ -40,12 +40,13 @@ 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')
|
||||
MULTI_WORD_PATTERN = re.compile(r'([`"])(tab([A-Z]\w+)( [A-Z]\w+)+)\1')
|
||||
|
||||
SQL_ITERATOR_BATCH_SIZE = 100
|
||||
|
||||
|
||||
class Database:
|
||||
"""
|
||||
|
|
@ -79,7 +80,7 @@ class Database:
|
|||
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.user = user or frappe.conf.db_user or frappe.conf.db_name
|
||||
self.cur_db_name = frappe.conf.db_name
|
||||
self._conn = None
|
||||
|
||||
|
|
@ -102,8 +103,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
|
||||
|
|
@ -175,6 +176,8 @@ class Database:
|
|||
:param pluck: Get the plucked field only.
|
||||
:param explain: Print `EXPLAIN` in error log.
|
||||
:param as_iterator: Returns iterator over results instead of fetching all results at once.
|
||||
This should be used with unbuffered cursor as default cursors used by pymysql and postgres
|
||||
buffer the results internally. See `Database.unbuffered_cursor`.
|
||||
Examples:
|
||||
|
||||
# return customer names as dicts
|
||||
|
|
@ -276,12 +279,10 @@ class Database:
|
|||
if not self._cursor.description:
|
||||
return ()
|
||||
|
||||
last_result = self._transform_result(self._cursor.fetchall())
|
||||
if as_iterator:
|
||||
return self._return_as_iterator(
|
||||
last_result, pluck=pluck, as_dict=as_dict, as_list=as_list, update=update
|
||||
)
|
||||
return self._return_as_iterator(pluck=pluck, as_dict=as_dict, as_list=as_list, update=update)
|
||||
|
||||
last_result = self._transform_result(self._cursor.fetchall())
|
||||
if pluck:
|
||||
last_result = [r[0] for r in last_result]
|
||||
self._clean_up()
|
||||
|
|
@ -300,24 +301,25 @@ class Database:
|
|||
self._clean_up()
|
||||
return last_result
|
||||
|
||||
def _return_as_iterator(self, result, *, pluck, as_dict, as_list, update):
|
||||
if pluck:
|
||||
for row in result:
|
||||
yield row[0]
|
||||
def _return_as_iterator(self, *, pluck, as_dict, as_list, update):
|
||||
while result := self._transform_result(self._cursor.fetchmany(SQL_ITERATOR_BATCH_SIZE)):
|
||||
if pluck:
|
||||
for row in result:
|
||||
yield row[0]
|
||||
|
||||
elif as_dict:
|
||||
keys = [column[0] for column in self._cursor.description]
|
||||
for row in result:
|
||||
row = frappe._dict(zip(keys, row))
|
||||
if update:
|
||||
row.update(update)
|
||||
yield row
|
||||
elif as_dict:
|
||||
keys = [column[0] for column in self._cursor.description]
|
||||
for row in result:
|
||||
row = frappe._dict(zip(keys, row))
|
||||
if update:
|
||||
row.update(update)
|
||||
yield row
|
||||
|
||||
elif as_list:
|
||||
for row in result:
|
||||
yield list(row)
|
||||
else:
|
||||
frappe.throw(_("`as_iterator` only works with `as_list=True` or `as_dict=True`"))
|
||||
elif as_list:
|
||||
for row in result:
|
||||
yield list(row)
|
||||
else:
|
||||
frappe.throw(_("`as_iterator` only works with `as_list=True` or `as_dict=True`"))
|
||||
|
||||
self._clean_up()
|
||||
|
||||
|
|
@ -781,7 +783,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(
|
||||
|
|
@ -1344,6 +1346,22 @@ class Database:
|
|||
def rename_column(self, doctype: str, old_column_name: str, new_column_name: str):
|
||||
raise NotImplementedError
|
||||
|
||||
@contextmanager
|
||||
def unbuffered_cursor(self):
|
||||
"""Context manager to temporarily use unbuffered cursor.
|
||||
|
||||
Using this with `as_iterator=True` provides O(1) memory usage while reading large result sets.
|
||||
|
||||
NOTE: You MUST do entire result set processing in the context, otherwise underlying cursor
|
||||
will be switched and you'll not get complete results.
|
||||
|
||||
Usage:
|
||||
with frappe.db.unbuffered_cursor():
|
||||
for row in frappe.db.sql("query with huge result", as_iterator=True):
|
||||
continue # Do some processing.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@contextmanager
|
||||
def savepoint(catch: type | tuple[type, ...] = Exception):
|
||||
|
|
|
|||
|
|
@ -18,6 +18,20 @@ class DbManager:
|
|||
password_predicate = f" IDENTIFIED BY '{password}'" if password else ""
|
||||
self.db.sql(f"CREATE USER '{user}'@'{host}'{password_predicate}")
|
||||
|
||||
def does_user_exist(self, username: str, host: str | None = None) -> bool:
|
||||
return (
|
||||
self.db.sql(
|
||||
f"SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '{username}' and "
|
||||
f"host = '{host or self.get_current_host()}')"
|
||||
)[0][0]
|
||||
== 1
|
||||
)
|
||||
|
||||
def set_user_password(self, username: str, password: str, host: str | None = None) -> None:
|
||||
self.db.sql(
|
||||
f"SET PASSWORD FOR '{username}'@'{host or self.get_current_host()}' = PASSWORD('{password}')"
|
||||
)
|
||||
|
||||
def delete_user(self, target, host=None):
|
||||
host = host or self.get_current_host()
|
||||
self.db.sql(f"DROP USER IF EXISTS '{target}'@'{host}'")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import re
|
||||
from contextlib import contextmanager
|
||||
|
||||
import pymysql
|
||||
from pymysql.constants import ER, FIELD_TYPE
|
||||
|
|
@ -525,3 +526,15 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
|
|||
|
||||
if est_row_size:
|
||||
return int(est_row_size[0][0])
|
||||
|
||||
@contextmanager
|
||||
def unbuffered_cursor(self):
|
||||
from pymysql.cursors import SSCursor
|
||||
|
||||
try:
|
||||
original_cursor = self._cursor
|
||||
new_cursor = self._cursor = self._conn.cursor(SSCursor)
|
||||
yield
|
||||
finally:
|
||||
self._cursor = original_cursor
|
||||
new_cursor.close()
|
||||
|
|
|
|||
|
|
@ -26,42 +26,51 @@ 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"] = "%"
|
||||
|
||||
if dbman.does_user_exist(db_user):
|
||||
print("User exists", db_user)
|
||||
dbman.set_user_password(db_user, frappe.conf.db_password, **dbman_kwargs)
|
||||
if verbose:
|
||||
print("Re-used existing user %s" % db_user)
|
||||
else:
|
||||
dbman.create_user(db_user, frappe.conf.db_password, **dbman_kwargs)
|
||||
if verbose:
|
||||
print("Created user %s" % 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):
|
||||
|
|
@ -96,14 +105,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 +160,24 @@ 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"
|
||||
if not frappe.flags.root_login:
|
||||
frappe.flags.root_login = "root"
|
||||
|
||||
if not root_password:
|
||||
root_password = frappe.conf.get("root_password") or None
|
||||
if not frappe.flags.root_password:
|
||||
frappe.flags.root_password = frappe.conf.get("root_password") or None
|
||||
|
||||
if not root_password:
|
||||
root_password = getpass.getpass("MySQL root password: ")
|
||||
if not frappe.flags.root_password:
|
||||
import getpass
|
||||
|
||||
frappe.flags.root_password = getpass.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,
|
||||
)
|
||||
|
||||
return frappe.local.flags.root_connection
|
||||
|
|
|
|||
|
|
@ -7,19 +7,25 @@ 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()
|
||||
|
||||
|
||||
|
|
@ -49,42 +55,40 @@ 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
|
||||
if not frappe.flags.root_login:
|
||||
frappe.flags.root_login = frappe.conf.get("root_login") or None
|
||||
|
||||
if not root_login:
|
||||
root_login = input("Enter postgres super user: ")
|
||||
if not frappe.flags.root_login:
|
||||
frappe.flags.root_login = input("Enter postgres super user: ")
|
||||
|
||||
if not root_password:
|
||||
root_password = frappe.conf.get("root_password") or None
|
||||
if not frappe.flags.root_password:
|
||||
frappe.flags.root_password = frappe.conf.get("root_password") or None
|
||||
|
||||
if not root_password:
|
||||
if not frappe.flags.root_password:
|
||||
from getpass import getpass
|
||||
|
||||
root_password = getpass("Postgres super user password: ")
|
||||
frappe.flags.root_password = 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,
|
||||
)
|
||||
|
||||
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}")
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
"fieldtype": "Select",
|
||||
"in_global_search": 1,
|
||||
"label": "Repeat On",
|
||||
"options": "\nDaily\nWeekly\nMonthly\nYearly"
|
||||
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly"
|
||||
},
|
||||
{
|
||||
"depends_on": "repeat_this_event",
|
||||
|
|
@ -295,7 +295,7 @@
|
|||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-23 10:33:15.685368",
|
||||
"modified": "2024-01-11 07:11:17.467503",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Event",
|
||||
|
|
@ -336,4 +336,4 @@
|
|||
"track_changes": 1,
|
||||
"track_seen": 1,
|
||||
"track_views": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ from frappe.utils import (
|
|||
format_datetime,
|
||||
get_datetime_str,
|
||||
getdate,
|
||||
month_diff,
|
||||
now_datetime,
|
||||
nowdate,
|
||||
)
|
||||
|
|
@ -62,7 +63,7 @@ class Event(Document):
|
|||
google_meet_link: DF.Data | None
|
||||
monday: DF.Check
|
||||
pulled_from_google_calendar: DF.Check
|
||||
repeat_on: DF.Literal["", "Daily", "Weekly", "Monthly", "Yearly"]
|
||||
repeat_on: DF.Literal["", "Daily", "Weekly", "Monthly", "Quarterly", "Half Yearly", "Yearly"]
|
||||
repeat_this_event: DF.Check
|
||||
repeat_till: DF.Date | None
|
||||
saturday: DF.Check
|
||||
|
|
@ -392,6 +393,62 @@ def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[
|
|||
|
||||
remove_events.append(e)
|
||||
|
||||
if e.repeat_on == "Half Yearly":
|
||||
# creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27
|
||||
year, month = start.split("-", maxsplit=2)[:2]
|
||||
date = f"{year}-{month}-" + event_start.split("-", maxsplit=3)[2]
|
||||
|
||||
# last day of month issue, start from prev month!
|
||||
try:
|
||||
getdate(date)
|
||||
except Exception:
|
||||
date = date.split("-")
|
||||
date = date[0] + "-" + str(cint(date[1]) - 1) + "-" + date[2]
|
||||
|
||||
start_from = date
|
||||
for i in range(int(date_diff(end, start) / 30) + 3):
|
||||
diff = month_diff(date, event_start) - 1
|
||||
if diff % 6 != 0:
|
||||
continue
|
||||
if (
|
||||
getdate(date) >= getdate(start)
|
||||
and getdate(date) <= getdate(end)
|
||||
and getdate(date) <= getdate(repeat)
|
||||
and getdate(date) >= getdate(event_start)
|
||||
):
|
||||
add_event(e, date)
|
||||
|
||||
date = add_months(start_from, i + 1)
|
||||
remove_events.append(e)
|
||||
|
||||
if e.repeat_on == "Quarterly":
|
||||
# creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27
|
||||
year, month = start.split("-", maxsplit=2)[:2]
|
||||
date = f"{year}-{month}-" + event_start.split("-", maxsplit=3)[2]
|
||||
|
||||
# last day of month issue, start from prev month!
|
||||
try:
|
||||
getdate(date)
|
||||
except Exception:
|
||||
date = date.split("-")
|
||||
date = date[0] + "-" + str(cint(date[1]) - 1) + "-" + date[2]
|
||||
|
||||
start_from = date
|
||||
for i in range(int(date_diff(end, start) / 30) + 3):
|
||||
diff = month_diff(date, event_start) - 1
|
||||
if diff % 3 != 0:
|
||||
continue
|
||||
if (
|
||||
getdate(date) >= getdate(start)
|
||||
and getdate(date) <= getdate(end)
|
||||
and getdate(date) <= getdate(repeat)
|
||||
and getdate(date) >= getdate(event_start)
|
||||
):
|
||||
add_event(e, date)
|
||||
|
||||
date = add_months(start_from, i + 1)
|
||||
remove_events.append(e)
|
||||
|
||||
if e.repeat_on == "Monthly":
|
||||
# creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27
|
||||
year, month = start.split("-", maxsplit=2)[:2]
|
||||
|
|
|
|||
|
|
@ -136,3 +136,77 @@ class TestEvent(FrappeTestCase):
|
|||
|
||||
ev_list3 = get_events("2015-02-01", "2015-02-01", "Administrator", for_reminder=True)
|
||||
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list3))))
|
||||
|
||||
def test_quaterly_repeat(self):
|
||||
ev = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Event",
|
||||
"subject": "_Test Event",
|
||||
"starts_on": "2023-02-17",
|
||||
"repeat_till": "2024-02-17",
|
||||
"event_type": "Public",
|
||||
"repeat_this_event": 1,
|
||||
"repeat_on": "Quarterly",
|
||||
}
|
||||
)
|
||||
ev.insert()
|
||||
# Test Quaterly months
|
||||
ev_list = get_events("2023-02-17", "2023-02-17", "Administrator", for_reminder=True)
|
||||
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list))))
|
||||
|
||||
ev_list1 = get_events("2023-05-17", "2023-05-17", "Administrator", for_reminder=True)
|
||||
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list1))))
|
||||
|
||||
ev_list2 = get_events("2023-08-17", "2023-08-17", "Administrator", for_reminder=True)
|
||||
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list2))))
|
||||
|
||||
ev_list3 = get_events("2023-11-17", "2023-11-17", "Administrator", for_reminder=True)
|
||||
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list3))))
|
||||
|
||||
# Test before event start date and after event end date
|
||||
ev_list4 = get_events("2022-11-17", "2022-11-17", "Administrator", for_reminder=True)
|
||||
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||
|
||||
ev_list4 = get_events("2024-02-17", "2024-02-17", "Administrator", for_reminder=True)
|
||||
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||
|
||||
# Test months that aren't part of the quarterly cycle
|
||||
ev_list4 = get_events("2023-12-17", "2023-12-17", "Administrator", for_reminder=True)
|
||||
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||
|
||||
ev_list4 = get_events("2023-03-17", "2023-03-17", "Administrator", for_reminder=True)
|
||||
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||
|
||||
def test_half_yearly_repeat(self):
|
||||
ev = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Event",
|
||||
"subject": "_Test Event",
|
||||
"starts_on": "2023-02-17",
|
||||
"repeat_till": "2024-02-17",
|
||||
"event_type": "Public",
|
||||
"repeat_this_event": 1,
|
||||
"repeat_on": "Half Yearly",
|
||||
}
|
||||
)
|
||||
ev.insert()
|
||||
# Test Half Yearly months
|
||||
ev_list = get_events("2023-02-17", "2023-02-17", "Administrator", for_reminder=True)
|
||||
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list))))
|
||||
|
||||
ev_list1 = get_events("2023-08-17", "2023-08-17", "Administrator", for_reminder=True)
|
||||
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list1))))
|
||||
|
||||
# Test before event start date and after event end date
|
||||
ev_list4 = get_events("2022-08-17", "2022-08-17", "Administrator", for_reminder=True)
|
||||
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||
|
||||
ev_list4 = get_events("2024-02-17", "2024-02-17", "Administrator", for_reminder=True)
|
||||
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||
|
||||
# Test months that aren't part of the half yearly cycle
|
||||
ev_list4 = get_events("2023-12-17", "2023-12-17", "Administrator", for_reminder=True)
|
||||
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||
|
||||
ev_list4 = get_events("2023-05-17", "2023-05-17", "Administrator", for_reminder=True)
|
||||
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-07-04 09:42:52.425440",
|
||||
"modified": "2024-01-17 15:37:31.605278",
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ def _new_site(
|
|||
db_type=None,
|
||||
db_host=None,
|
||||
db_port=None,
|
||||
db_user=None,
|
||||
setup_db=True,
|
||||
):
|
||||
"""Install a new Frappe site"""
|
||||
|
|
@ -97,6 +98,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 +137,7 @@ def install_db(
|
|||
db_type=None,
|
||||
db_host=None,
|
||||
db_port=None,
|
||||
db_user=None,
|
||||
no_mariadb_socket=False,
|
||||
setup=True,
|
||||
):
|
||||
|
|
@ -156,6 +159,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
|
||||
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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 & Services\" > \"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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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", ""))
|
||||
|
|
|
|||
|
|
@ -52,7 +52,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 +110,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,
|
||||
|
|
|
|||
2763
frappe/locale/de.po
2763
frappe/locale/de.po
File diff suppressed because it is too large
Load diff
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -49,9 +49,6 @@ frappe.ui.form.Control = class BaseControl {
|
|||
if (this.df.get_status) {
|
||||
return this.df.get_status(this);
|
||||
}
|
||||
if (this.df.is_virtual) {
|
||||
return "Read";
|
||||
}
|
||||
|
||||
if (
|
||||
(!this.doctype && !this.docname) ||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ frappe.ui.form.add_options = function (input, options_list, sort) {
|
|||
|
||||
let options = options_list.map((raw_option) => parse_option(raw_option));
|
||||
if (sort) {
|
||||
options = options.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options = options.sort((a, b) => cstr(a.label).localeCompare(cstr(b.label)));
|
||||
}
|
||||
|
||||
options
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class FormTimeline extends BaseTimeline {
|
|||
.append(
|
||||
`
|
||||
<div class="d-flex align-items-center show-all-activity">
|
||||
<span style="color: var(--text-light); margin:0px 6px;">Show all activity</span>
|
||||
<span style="color: var(--text-light); margin:0px 6px;">${__("Show all activity")}</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox">
|
||||
<span class="slider round"></span>
|
||||
|
|
|
|||
|
|
@ -739,15 +739,17 @@ class FilterArea {
|
|||
this.standard_filters_wrapper = this.list_view.page.page_form.find(
|
||||
".standard-filter-section"
|
||||
);
|
||||
let fields = [
|
||||
{
|
||||
let fields = [];
|
||||
|
||||
if (!this.list_view.settings.hide_name_filter) {
|
||||
fields.push({
|
||||
fieldtype: "Data",
|
||||
label: "ID",
|
||||
condition: "like",
|
||||
fieldname: "name",
|
||||
onchange: () => this.refresh_list_view(),
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if (this.list_view.custom_filter_configs) {
|
||||
this.list_view.custom_filter_configs.forEach((config) => {
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ $.extend(frappe.perm, {
|
|||
|
||||
if (!perm) {
|
||||
let is_hidden = df && (cint(df.hidden) || cint(df.hidden_due_to_dependency));
|
||||
let is_read_only = df && cint(df.read_only);
|
||||
let is_read_only = df && (cint(df.read_only) || cint(df.is_virtual));
|
||||
return is_hidden ? "None" : is_read_only ? "Read" : "Write";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -317,7 +317,9 @@ frappe.search.AwesomeBar = class AwesomeBar {
|
|||
var route = frappe.get_route();
|
||||
if (route[0] === "List" && txt.indexOf(" in") === -1) {
|
||||
// search in title field
|
||||
var meta = frappe.get_meta(frappe.container.page.list_view.doctype);
|
||||
const doctype = frappe.container.page?.list_view?.doctype;
|
||||
if (!doctype) return;
|
||||
var meta = frappe.get_meta(doctype);
|
||||
var search_field = meta.title_field || "name";
|
||||
var options = {};
|
||||
options[search_field] = ["like", "%" + txt + "%"];
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
/* global gapi:false, google:false */
|
||||
export default class GoogleDrivePicker {
|
||||
constructor({ pickerCallback, enabled, appId, developerKey, clientId } = {}) {
|
||||
constructor({ pickerCallback, enabled, appId, clientId } = {}) {
|
||||
this.scope = "https://www.googleapis.com/auth/drive.file";
|
||||
this.pickerApiLoaded = false;
|
||||
this.enabled = enabled;
|
||||
this.appId = appId;
|
||||
this.pickerCallback = pickerCallback;
|
||||
this.developerKey = developerKey;
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +44,6 @@ export default class GoogleDrivePicker {
|
|||
createPicker(access_token) {
|
||||
this.view = new google.picker.View(google.picker.ViewId.DOCS);
|
||||
this.picker = new google.picker.PickerBuilder()
|
||||
.setDeveloperKey(this.developerKey)
|
||||
.setAppId(this.appId)
|
||||
.setOAuthToken(access_token)
|
||||
.addView(this.view)
|
||||
|
|
|
|||
|
|
@ -59,11 +59,12 @@ def get_sessions_to_clear(user=None, keep_current=False):
|
|||
offset = 0
|
||||
if user == frappe.session.user:
|
||||
simultaneous_sessions = frappe.db.get_value("User", user, "simultaneous_sessions") or 1
|
||||
offset = simultaneous_sessions - 1
|
||||
offset = simultaneous_sessions
|
||||
|
||||
session = frappe.qb.DocType("Sessions")
|
||||
session_id = frappe.qb.from_(session).where(session.user == user)
|
||||
if keep_current:
|
||||
offset = max(0, offset - 1)
|
||||
session_id = session_id.where(session.sid != frappe.session.sid)
|
||||
|
||||
query = (
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ def add_user(email, password, username=None, mobile_no=None):
|
|||
dict(doctype="User", email=email, first_name=first_name, username=username, mobile_no=mobile_no)
|
||||
).insert()
|
||||
user.new_password = password
|
||||
user.simultaneous_sessions = 1
|
||||
user.add_roles("System Manager")
|
||||
frappe.db.commit()
|
||||
|
||||
|
|
@ -212,12 +213,12 @@ class TestSessionExpirty(FrappeAPITestCase):
|
|||
seconds_elapsed = expiry_in * step / 100
|
||||
|
||||
time_now = add_to_date(session_created, seconds=seconds_elapsed, as_string=True)
|
||||
with patch("frappe.utils.now", return_value=time_now):
|
||||
with self.freeze_time(time_now):
|
||||
data = s.get_session_data_from_db()
|
||||
self.assertEqual(data.user, "Administrator")
|
||||
|
||||
# 1% higher should immediately expire
|
||||
time_now = add_to_date(session_created, seconds=expiry_in * 1.01, as_string=True)
|
||||
with patch("frappe.utils.now", return_value=time_now):
|
||||
time_of_expiry = add_to_date(session_created, seconds=expiry_in * 1.01, as_string=True)
|
||||
with self.freeze_time(time_of_expiry):
|
||||
self.assertIn(sid, get_expired_sessions())
|
||||
self.assertFalse(s.get_session_data_from_db())
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import gzip
|
|||
import importlib
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import shlex
|
||||
import string
|
||||
import subprocess
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
|
|
@ -511,6 +513,84 @@ class TestCommands(BaseTestCommands):
|
|||
|
||||
self.assertEqual(conf[key], value)
|
||||
|
||||
def test_different_db_username(self):
|
||||
site = frappe.generate_hash()
|
||||
user = "".join(secrets.choice(string.ascii_letters) for _ in range(8))
|
||||
password = frappe.generate_hash()
|
||||
kwargs = {
|
||||
"new_site": site,
|
||||
"admin_password": frappe.conf.admin_password,
|
||||
"root_password": frappe.conf.root_password or "",
|
||||
"db_type": frappe.conf.db_type,
|
||||
"db_user": user,
|
||||
"db_password": password,
|
||||
"db_root_username": frappe.conf.root_login,
|
||||
}
|
||||
self.execute(
|
||||
"bench new-site {new_site} --force --verbose "
|
||||
"--admin-password {admin_password} "
|
||||
"--db-root-password {root_password} "
|
||||
"--db-type {db_type} "
|
||||
"--db-user {db_user} "
|
||||
"--db-password {db_password}",
|
||||
kwargs,
|
||||
)
|
||||
self.assertEqual(self.returncode, 0)
|
||||
self.execute("bench --site {new_site} show-config --format json", kwargs)
|
||||
self.assertEqual(self.returncode, 0)
|
||||
config = json.loads(self.stdout)
|
||||
self.assertEqual(config[site]["db_user"], user)
|
||||
self.assertEqual(config[site]["db_password"], password)
|
||||
self.execute(
|
||||
"bench drop-site {new_site} --force --db-root-username {db_root_username} --db-root-password {root_password}",
|
||||
kwargs,
|
||||
)
|
||||
self.assertEqual(self.returncode, 0)
|
||||
|
||||
def test_existing_db_username(self):
|
||||
site = frappe.generate_hash()
|
||||
user = "".join(secrets.choice(string.ascii_letters) for _ in range(8))
|
||||
if frappe.conf.db_type == "mariadb":
|
||||
from frappe.database.mariadb.setup_db import get_root_connection
|
||||
|
||||
root_conn = get_root_connection()
|
||||
root_conn.sql(f"CREATE USER '{user}'@'localhost'")
|
||||
else:
|
||||
from frappe.database.postgres.setup_db import get_root_connection
|
||||
|
||||
root_conn = get_root_connection()
|
||||
root_conn.sql(f"CREATE USER {user}")
|
||||
password = frappe.generate_hash()
|
||||
kwargs = {
|
||||
"new_site": site,
|
||||
"admin_password": frappe.conf.admin_password,
|
||||
"root_password": frappe.conf.root_password,
|
||||
"db_type": frappe.conf.db_type,
|
||||
"db_user": user,
|
||||
"db_password": password,
|
||||
"db_root_username": frappe.conf.root_login,
|
||||
}
|
||||
self.execute(
|
||||
"bench new-site {new_site} --force --verbose "
|
||||
"--admin-password {admin_password} "
|
||||
"--db-root-password {root_password} "
|
||||
"--db-type {db_type} "
|
||||
"--db-user {db_user} "
|
||||
"--db-password {db_password}",
|
||||
kwargs,
|
||||
)
|
||||
self.assertEqual(self.returncode, 0)
|
||||
self.execute("bench --site {new_site} show-config --format json", kwargs)
|
||||
self.assertEqual(self.returncode, 0)
|
||||
config = json.loads(self.stdout)
|
||||
self.assertEqual(config[site]["db_user"], user)
|
||||
self.assertEqual(config[site]["db_password"], password)
|
||||
self.execute(
|
||||
"bench drop-site {new_site} --force --db-root-username {db_root_username} --db-root-password {root_password}",
|
||||
kwargs,
|
||||
)
|
||||
self.assertEqual(self.returncode, 0)
|
||||
|
||||
|
||||
class TestBackups(BaseTestCommands):
|
||||
backup_map = {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import frappe
|
|||
from frappe.core.utils import find
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.database import savepoint
|
||||
from frappe.database.database import Database, get_query_execution_timeout
|
||||
from frappe.database.database import get_query_execution_timeout
|
||||
from frappe.database.utils import FallBackDateTimeStr
|
||||
from frappe.query_builder import Field
|
||||
from frappe.query_builder.functions import Concat_ws
|
||||
|
|
@ -1007,3 +1007,8 @@ class TestSqlIterator(FrappeTestCase):
|
|||
list(frappe.db.sql(query, as_list=True, as_iterator=True)),
|
||||
msg=f"{query=} results not same as iterator",
|
||||
)
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
def test_unbuffered_cursor(self):
|
||||
with frappe.db.unbuffered_cursor():
|
||||
self.test_db_sql_iterator()
|
||||
|
|
|
|||
|
|
@ -193,3 +193,7 @@ class TestPerformance(FrappeTestCase):
|
|||
result = frappe.db.sql(query, **kwargs)
|
||||
self.assertEqual(sys.getrefcount(result), 2) # Note: This always returns +1
|
||||
self.assertFalse(gc.get_referrers(result))
|
||||
|
||||
def test_no_cyclic_references(self):
|
||||
doc = frappe.get_doc("User", "Administrator")
|
||||
self.assertEqual(sys.getrefcount(doc), 2) # Note: This always returns +1
|
||||
|
|
|
|||
|
|
@ -261,12 +261,12 @@ def has_gravatar(email: str) -> str:
|
|||
|
||||
gravatar_url = get_gravatar_url(email, "404")
|
||||
try:
|
||||
res = requests.get(gravatar_url)
|
||||
res = requests.get(gravatar_url, timeout=5)
|
||||
if res.status_code == 200:
|
||||
return gravatar_url
|
||||
else:
|
||||
return ""
|
||||
except requests.exceptions.ConnectionError:
|
||||
except requests.exceptions.RequestException:
|
||||
return ""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import contextlib
|
|||
# imports - standard imports
|
||||
import gzip
|
||||
import os
|
||||
import sys
|
||||
from calendar import timegm
|
||||
from datetime import datetime
|
||||
from glob import glob
|
||||
|
|
@ -225,6 +226,9 @@ class BackupGenerator:
|
|||
"""
|
||||
Encrypt all the backups created using gpg.
|
||||
"""
|
||||
if which("gpg") is None:
|
||||
click.secho("Please install `gpg` and ensure its available in your PATH", fg="red")
|
||||
sys.exit(1)
|
||||
paths = (self.backup_path_db, self.backup_path_files, self.backup_path_private_files)
|
||||
for path in paths:
|
||||
if os.path.exists(path):
|
||||
|
|
@ -492,7 +496,7 @@ def fetch_latest_backups(partial=False) -> dict:
|
|||
frappe.only_for("System Manager")
|
||||
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,
|
||||
|
|
@ -559,7 +563,7 @@ def new_backup(
|
|||
delete_temp_backups()
|
||||
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,
|
||||
|
|
@ -640,6 +644,9 @@ def get_or_generate_backup_encryption_key():
|
|||
|
||||
@contextlib.contextmanager
|
||||
def decrypt_backup(file_path: str, passphrase: str):
|
||||
if which("gpg") is None:
|
||||
click.secho("Please install `gpg` and ensure its available in your PATH", fg="red")
|
||||
sys.exit(1)
|
||||
if not os.path.exists(file_path):
|
||||
print("Invalid path: ", file_path)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -107,13 +107,13 @@ def capture_exception(message: str | None = None) -> None:
|
|||
return
|
||||
try:
|
||||
hub = Hub.current
|
||||
if frappe.request:
|
||||
with hub.configure_scope() as scope:
|
||||
if (
|
||||
os.getenv("ENABLE_SENTRY_DB_MONITORING") is None
|
||||
or os.getenv("SENTRY_TRACING_SAMPLE_RATE") is None
|
||||
):
|
||||
set_scope(scope)
|
||||
with hub.configure_scope() as scope:
|
||||
if (
|
||||
os.getenv("ENABLE_SENTRY_DB_MONITORING") is None
|
||||
or os.getenv("SENTRY_TRACING_SAMPLE_RATE") is None
|
||||
):
|
||||
set_scope(scope)
|
||||
if frappe.request:
|
||||
evt_processor = _make_wsgi_event_processor(frappe.request.environ, False)
|
||||
scope.add_event_processor(evt_processor)
|
||||
if frappe.request.is_json:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const redis = require("@redis/client");
|
||||
const bench_path = path.resolve(__dirname, "..", "..");
|
||||
let bench_path;
|
||||
if (process.env.FRAPPE_BENCH_ROOT) {
|
||||
bench_path = process.env.FRAPPE_BENCH_ROOT;
|
||||
} else {
|
||||
bench_path = path.resolve(__dirname, "..", "..");
|
||||
}
|
||||
|
||||
const dns = require("dns");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue