Merge branch 'develop' into add-using-cached-build-flag

This commit is contained in:
Ankush Menat 2024-01-18 10:51:04 +05:30 committed by GitHub
commit 2b3ab02d79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1886 additions and 1556 deletions

View file

@ -51,7 +51,7 @@ jobs:
python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER
linter: linter:
name: 'Frappe Linter' name: 'Semgrep Rules'
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
@ -61,7 +61,6 @@ jobs:
with: with:
python-version: '3.10' python-version: '3.10'
cache: pip cache: pip
- uses: pre-commit/action@v3.0.0
- name: Download Semgrep rules - name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-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
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

@ -1,11 +1,16 @@
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const chalk = require("chalk"); 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 apps_path = path.resolve(bench_path, "apps");
const sites_path = path.resolve(bench_path, "sites");
const assets_path = path.resolve(sites_path, "assets"); const assets_path = path.resolve(sites_path, "assets");
const app_list = get_apps_list(); const app_list = get_apps_list();

View file

@ -269,6 +269,10 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
local.initialised = True 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( def connect(
site: str | None = None, db_name: str | None = None, set_admin_as_user: bool = True 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( local.db = get_db(
host=local.conf.db_host, host=local.conf.db_host,
port=local.conf.db_port, port=local.conf.db_port,
user=db_name or local.conf.db_name, user=local.conf.db_user or db_name,
password=None, password=None,
) )
if set_admin_as_user: 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"): if local and hasattr(local, "replica_db") and hasattr(local, "primary_db"):
return False return False
user = local.conf.db_name user = local.conf.db_user
password = local.conf.db_password password = local.conf.db_password
port = local.conf.replica_db_port port = local.conf.replica_db_port
if local.conf.different_credentials_for_replica: 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 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, user=user, password=password, port=port)

View file

@ -53,6 +53,7 @@ from frappe.exceptions import SiteNotSpecifiedError
default=True, default=True,
help="Create user and database in mariadb/postgres; only bootstrap if false", 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( def new_site(
site, site,
db_root_username=None, db_root_username=None,
@ -68,6 +69,7 @@ def new_site(
db_type=None, db_type=None,
db_host=None, db_host=None,
db_port=None, db_port=None,
db_user=None,
set_default=False, set_default=False,
setup_db=True, setup_db=True,
): ):
@ -91,6 +93,7 @@ def new_site(
db_type=db_type, db_type=db_type,
db_host=db_host, db_host=db_host,
db_port=db_port, db_port=db_port,
db_user=db_user,
setup_db=setup_db, setup_db=setup_db,
) )
@ -319,7 +322,7 @@ def restore_backup(
) )
except Exception as err: except Exception as err:
print(err.args[1]) print(err)
sys.exit(1) sys.exit(1)
@ -1058,7 +1061,11 @@ def _drop_site(
sys.exit(1) sys.exit(1)
click.secho("Dropping site database and user", fg="green") 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( archived_sites_path = archived_sites_path or os.path.join(
frappe.utils.get_bench_path(), "archived", "sites" 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") @click.option("--no-backup", is_flag=True, default=False, help="Do not backup the table")
@pass_context @pass_context
def clear_log_table(context, doctype, days, no_backup): def clear_log_table(context, doctype, days, no_backup):
"""If any logtype table grows too large then clearing it with DELETE query """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 is not feasible in reasonable time. This command copies recent data to new
table and replaces current table with new smaller table. table and replaces current table with new smaller table.

View file

@ -23,15 +23,15 @@ class TestContact(FrappeTestCase):
def test_check_default_phone_and_mobile(self): def test_check_default_phone_and_mobile(self):
phones = [ phones = [
{"phone": "+91 0000000000", "is_primary_phone": 0, "is_primary_mobile_no": 0}, {"phone": "+91 0000000010", "is_primary_phone": 0, "is_primary_mobile_no": 0},
{"phone": "+91 0000000001", "is_primary_phone": 0, "is_primary_mobile_no": 0}, {"phone": "+91 0000000011", "is_primary_phone": 0, "is_primary_mobile_no": 0},
{"phone": "+91 0000000002", "is_primary_phone": 1, "is_primary_mobile_no": 0}, {"phone": "+91 0000000012", "is_primary_phone": 1, "is_primary_mobile_no": 0},
{"phone": "+91 0000000003", "is_primary_phone": 0, "is_primary_mobile_no": 1}, {"phone": "+91 0000000013", "is_primary_phone": 0, "is_primary_mobile_no": 1},
] ]
contact = create_contact("Phone", "Mr", phones=phones) contact = create_contact("Phone", "Mr", phones=phones)
self.assertEqual(contact.phone, "+91 0000000002") self.assertEqual(contact.phone, "+91 0000000012")
self.assertEqual(contact.mobile_no, "+91 0000000003") self.assertEqual(contact.mobile_no, "+91 0000000013")
def test_get_full_name(self): def test_get_full_name(self):
self.assertEqual(get_full_name(first="John"), "John") self.assertEqual(get_full_name(first="John"), "John")

View file

@ -76,7 +76,7 @@ def create_linked_contact(link_list, address):
} }
) )
contact.add_email("test_contact@example.com", is_primary=True) 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: for name in link_list:
contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name}) contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})
@ -105,7 +105,7 @@ class TestAddressesAndContacts(FrappeTestCase):
"_Test First Name", "_Test First Name",
"_Test Last Name", "_Test Last Name",
"_Test Address-Billing", "_Test Address-Billing",
"+91 0000000000", "+91 0000000020",
"", "",
"test_contact@example.com", "test_contact@example.com",
1, 1,

View file

@ -4,13 +4,10 @@
import copy import copy
import json import json
import os import os
# imports - standard imports
import re import re
import shutil import shutil
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING, Union
# imports - module imports
import frappe import frappe
from frappe import _ from frappe import _
from frappe.cache_manager import clear_controller_cache, clear_user_cache 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_illegal_characters(d.fieldname)
check_invalid_fieldnames(meta.get("name"), d.fieldname) check_invalid_fieldnames(meta.get("name"), d.fieldname)
check_unique_fieldname(meta.get("name"), d.fieldname)
check_fieldname_length(d.fieldname) check_fieldname_length(d.fieldname)
check_hidden_and_mandatory(meta.get("name"), d) check_hidden_and_mandatory(meta.get("name"), d)
check_unique_and_text(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) validate_data_field_type(d)
if not frappe.flags.in_migrate: if not frappe.flags.in_migrate:
check_unique_fieldname(meta.get("name"), d.fieldname)
check_link_table_options(meta.get("name"), d) check_link_table_options(meta.get("name"), d)
check_illegal_mandatory(meta.get("name"), d) check_illegal_mandatory(meta.get("name"), d)
check_dynamic_link_options(d) check_dynamic_link_options(d)

View file

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

View file

@ -8,7 +8,7 @@ import frappe
def get_parent_doc(doc): def get_parent_doc(doc):
"""Return document of `reference_doctype`, `reference_doctype`.""" """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: if doc.reference_doctype and doc.reference_name:
doc.parent_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name) doc.parent_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
else: else:

View file

@ -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) 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 import frappe
if frappe.conf.db_type == "postgres": if frappe.conf.db_type == "postgres":
import frappe.database.postgres.setup_db import frappe.database.postgres.setup_db
return frappe.database.postgres.setup_db.drop_user_and_database( return frappe.database.postgres.setup_db.drop_user_and_database(db_name, db_user)
db_name, root_login, root_password
)
else: else:
import frappe.database.mariadb.setup_db import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.drop_user_and_database( return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, db_user)
db_name, root_login, root_password
)
def get_db(host=None, user=None, password=None, port=None): def get_db(host=None, user=None, password=None, port=None):

View file

@ -40,12 +40,13 @@ if TYPE_CHECKING:
from pymysql.connections import Connection as MariadbConnection from pymysql.connections import Connection as MariadbConnection
from pymysql.cursors import Cursor as MariadbCursor from pymysql.cursors import Cursor as MariadbCursor
IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE) IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE)
INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*") INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*")
SINGLE_WORD_PATTERN = re.compile(r'([`"]?)(tab([A-Z]\w+))\1') 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') MULTI_WORD_PATTERN = re.compile(r'([`"])(tab([A-Z]\w+)( [A-Z]\w+)+)\1')
SQL_ITERATOR_BATCH_SIZE = 100
class Database: class Database:
""" """
@ -79,7 +80,7 @@ class Database:
self.setup_type_map() self.setup_type_map()
self.host = host or frappe.conf.db_host self.host = host or frappe.conf.db_host
self.port = port or frappe.conf.db_port 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.cur_db_name = frappe.conf.db_name
self._conn = None self._conn = None
@ -102,8 +103,8 @@ class Database:
self.before_rollback = CallbackManager() self.before_rollback = CallbackManager()
self.after_rollback = CallbackManager() self.after_rollback = CallbackManager()
# self.db_type: str # self.db_type: str
# self.last_query (lazy) attribute of last sql query executed # self.last_query (lazy) attribute of last sql query executed
def setup_type_map(self): def setup_type_map(self):
pass pass
@ -175,6 +176,8 @@ class Database:
:param pluck: Get the plucked field only. :param pluck: Get the plucked field only.
:param explain: Print `EXPLAIN` in error log. :param explain: Print `EXPLAIN` in error log.
:param as_iterator: Returns iterator over results instead of fetching all results at once. :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: Examples:
# return customer names as dicts # return customer names as dicts
@ -276,12 +279,10 @@ class Database:
if not self._cursor.description: if not self._cursor.description:
return () return ()
last_result = self._transform_result(self._cursor.fetchall())
if as_iterator: if as_iterator:
return self._return_as_iterator( return self._return_as_iterator(pluck=pluck, as_dict=as_dict, as_list=as_list, update=update)
last_result, pluck=pluck, as_dict=as_dict, as_list=as_list, update=update
)
last_result = self._transform_result(self._cursor.fetchall())
if pluck: if pluck:
last_result = [r[0] for r in last_result] last_result = [r[0] for r in last_result]
self._clean_up() self._clean_up()
@ -300,24 +301,25 @@ class Database:
self._clean_up() self._clean_up()
return last_result return last_result
def _return_as_iterator(self, result, *, pluck, as_dict, as_list, update): def _return_as_iterator(self, *, pluck, as_dict, as_list, update):
if pluck: while result := self._transform_result(self._cursor.fetchmany(SQL_ITERATOR_BATCH_SIZE)):
for row in result: if pluck:
yield row[0] for row in result:
yield row[0]
elif as_dict: elif as_dict:
keys = [column[0] for column in self._cursor.description] keys = [column[0] for column in self._cursor.description]
for row in result: for row in result:
row = frappe._dict(zip(keys, row)) row = frappe._dict(zip(keys, row))
if update: if update:
row.update(update) row.update(update)
yield row yield row
elif as_list: elif as_list:
for row in result: for row in result:
yield list(row) yield list(row)
else: else:
frappe.throw(_("`as_iterator` only works with `as_list=True` or `as_dict=True`")) frappe.throw(_("`as_iterator` only works with `as_list=True` or `as_dict=True`"))
self._clean_up() self._clean_up()
@ -781,7 +783,7 @@ class Database:
Example: Example:
# Update the `deny_multiple_sessions` field in System Settings DocType. # 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( 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): def rename_column(self, doctype: str, old_column_name: str, new_column_name: str):
raise NotImplementedError 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 @contextmanager
def savepoint(catch: type | tuple[type, ...] = Exception): def savepoint(catch: type | tuple[type, ...] = Exception):

View file

@ -18,6 +18,20 @@ class DbManager:
password_predicate = f" IDENTIFIED BY '{password}'" if password else "" 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 '{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): def delete_user(self, target, host=None):
host = host or self.get_current_host() host = host or self.get_current_host()
self.db.sql(f"DROP USER IF EXISTS '{target}'@'{host}'") self.db.sql(f"DROP USER IF EXISTS '{target}'@'{host}'")

View file

@ -1,4 +1,5 @@
import re import re
from contextlib import contextmanager
import pymysql import pymysql
from pymysql.constants import ER, FIELD_TYPE from pymysql.constants import ER, FIELD_TYPE
@ -525,3 +526,15 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
if est_row_size: if est_row_size:
return int(est_row_size[0][0]) 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()

View file

@ -26,42 +26,51 @@ def get_mariadb_version(version_string: str = ""):
def setup_database(force, verbose, no_mariadb_socket=False): def setup_database(force, verbose, no_mariadb_socket=False):
frappe.local.session = frappe._dict({"user": "Administrator"}) frappe.local.session = frappe._dict({"user": "Administrator"})
db_user = frappe.conf.db_user
db_name = frappe.local.conf.db_name 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 = DbManager(root_conn)
dbman_kwargs = {} dbman_kwargs = {}
if no_mariadb_socket: if no_mariadb_socket:
dbman_kwargs["host"] = "%" 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()): if force or (db_name not in dbman.get_database_list()):
dbman.delete_user(db_name, **dbman_kwargs)
dbman.drop_database(db_name) dbman.drop_database(db_name)
else: else:
raise Exception(f"Database {db_name} already exists") 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) dbman.create_database(db_name)
if verbose: if verbose:
print("Created database %s" % db_name) 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() dbman.flush_privileges()
if verbose: 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 # close root connection
root_conn.close() root_conn.close()
def drop_user_and_database(db_name, root_login, root_password): def drop_user_and_database(
frappe.local.db = get_root_connection(root_login, root_password) db_name,
db_user,
):
frappe.local.db = get_root_connection()
dbman = DbManager(frappe.local.db) dbman = DbManager(frappe.local.db)
dbman.drop_database(db_name) dbman.drop_database(db_name)
dbman.delete_user(db_name, host="%") dbman.delete_user(db_user, host="%")
dbman.delete_user(db_name) dbman.delete_user(db_user)
def bootstrap_database(db_name, verbose, source_sql=None): 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: if not source_sql:
source_sql = os.path.join(os.path.dirname(__file__), "framework_mariadb.sql") source_sql = os.path.join(os.path.dirname(__file__), "framework_mariadb.sql")
DbManager(frappe.local.db).restore_database( 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: if verbose:
print("Imported from database %s" % source_sql) print("Imported from database %s" % source_sql)
def check_database_settings(): def check_database_settings():
check_compatible_versions() check_compatible_versions()
# Check each expected value vs. actuals: # Check each expected value vs. actuals:
@ -152,24 +160,24 @@ def check_compatible_versions():
) )
def get_root_connection(root_login, root_password): def get_root_connection():
import getpass
if not frappe.local.flags.root_connection: if not frappe.local.flags.root_connection:
if not root_login: if not frappe.flags.root_login:
root_login = "root" frappe.flags.root_login = "root"
if not root_password: if not frappe.flags.root_password:
root_password = frappe.conf.get("root_password") or None frappe.flags.root_password = frappe.conf.get("root_password") or None
if not root_password: if not frappe.flags.root_password:
root_password = getpass.getpass("MySQL root password: ") import getpass
frappe.flags.root_password = getpass.getpass("MySQL root password: ")
frappe.local.flags.root_connection = frappe.database.get_db( frappe.local.flags.root_connection = frappe.database.get_db(
host=frappe.conf.db_host, host=frappe.conf.db_host,
port=frappe.conf.db_port, port=frappe.conf.db_port,
user=root_login, user=frappe.flags.root_login,
password=root_password, password=frappe.flags.root_password,
) )
return frappe.local.flags.root_connection return frappe.local.flags.root_connection

View file

@ -7,19 +7,25 @@ from frappe.utils import cint
def setup_database(): 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.commit()
root_conn.sql("end") root_conn.sql("end")
root_conn.sql(f"DROP DATABASE IF EXISTS `{frappe.conf.db_name}`") 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}`") # If user exists, just update password
root_conn.sql(f"CREATE user {frappe.conf.db_name} password '{frappe.conf.db_password}'") if root_conn.sql(f"SELECT 1 FROM pg_roles WHERE rolname='{frappe.conf.db_user}'"):
root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(frappe.conf.db_name)) 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): if psql_version := root_conn.sql("SELECT VERSION()", as_dict=True):
version_string = psql_version[0].get("version") or "PostgreSQL 14" version_string = psql_version[0].get("version") or "PostgreSQL 14"
major_version = cint(re.split(r"[\w\.]", version_string)[1]) major_version = cint(re.split(r"[\w\.]", version_string)[1])
if major_version > 15: 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() root_conn.close()
@ -49,42 +55,40 @@ def import_db_from_sql(source_sql=None, verbose=False):
if not source_sql: if not source_sql:
source_sql = os.path.join(os.path.dirname(__file__), "framework_postgres.sql") source_sql = os.path.join(os.path.dirname(__file__), "framework_postgres.sql")
DbManager(frappe.local.db).restore_database( 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: if verbose:
print("Imported from database %s" % source_sql) 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 frappe.local.flags.root_connection:
if not root_login: if not frappe.flags.root_login:
root_login = frappe.conf.get("root_login") or None frappe.flags.root_login = frappe.conf.get("root_login") or None
if not root_login: if not frappe.flags.root_login:
root_login = input("Enter postgres super user: ") frappe.flags.root_login = input("Enter postgres super user: ")
if not root_password: if not frappe.flags.root_password:
root_password = frappe.conf.get("root_password") or None 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 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( frappe.local.flags.root_connection = frappe.database.get_db(
host=frappe.conf.db_host, host=frappe.conf.db_host,
port=frappe.conf.db_port, port=frappe.conf.db_port,
user=root_login, user=frappe.flags.root_login,
password=root_password, password=frappe.flags.root_password,
) )
return frappe.local.flags.root_connection return frappe.local.flags.root_connection
def drop_user_and_database(db_name, root_login, root_password): def drop_user_and_database(db_name, db_user):
root_conn = get_root_connection( root_conn = get_root_connection()
frappe.flags.root_login or root_login, frappe.flags.root_password or root_password
)
root_conn.commit() root_conn.commit()
root_conn.sql( root_conn.sql(
"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s", "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("end")
root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}") 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

@ -123,7 +123,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_global_search": 1, "in_global_search": 1,
"label": "Repeat On", "label": "Repeat On",
"options": "\nDaily\nWeekly\nMonthly\nYearly" "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly"
}, },
{ {
"depends_on": "repeat_this_event", "depends_on": "repeat_this_event",
@ -295,7 +295,7 @@
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2023-06-23 10:33:15.685368", "modified": "2024-01-11 07:11:17.467503",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Desk", "module": "Desk",
"name": "Event", "name": "Event",
@ -336,4 +336,4 @@
"track_changes": 1, "track_changes": 1,
"track_seen": 1, "track_seen": 1,
"track_views": 1 "track_views": 1
} }

View file

@ -21,6 +21,7 @@ from frappe.utils import (
format_datetime, format_datetime,
get_datetime_str, get_datetime_str,
getdate, getdate,
month_diff,
now_datetime, now_datetime,
nowdate, nowdate,
) )
@ -62,7 +63,7 @@ class Event(Document):
google_meet_link: DF.Data | None google_meet_link: DF.Data | None
monday: DF.Check monday: DF.Check
pulled_from_google_calendar: 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_this_event: DF.Check
repeat_till: DF.Date | None repeat_till: DF.Date | None
saturday: DF.Check saturday: DF.Check
@ -392,6 +393,62 @@ def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[
remove_events.append(e) 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": if e.repeat_on == "Monthly":
# creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27 # creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27
year, month = start.split("-", maxsplit=2)[:2] year, month = start.split("-", maxsplit=2)[:2]

View file

@ -136,3 +136,77 @@ class TestEvent(FrappeTestCase):
ev_list3 = get_events("2015-02-01", "2015-02-01", "Administrator", for_reminder=True) 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)))) 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))))

View file

@ -82,7 +82,7 @@
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-07-04 09:42:52.425440", "modified": "2024-01-17 15:37:31.605278",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Geo", "module": "Geo",
"name": "Currency", "name": "Currency",
@ -102,6 +102,10 @@
"share": 1, "share": 1,
"write": 1 "write": 1
}, },
{
"read": 1,
"role": "Accounts Manager"
},
{ {
"read": 1, "read": 1,
"role": "Accounts User" "role": "Accounts User"

View file

@ -49,6 +49,7 @@ def _new_site(
db_type=None, db_type=None,
db_host=None, db_host=None,
db_port=None, db_port=None,
db_user=None,
setup_db=True, setup_db=True,
): ):
"""Install a new Frappe site""" """Install a new Frappe site"""
@ -97,6 +98,7 @@ def _new_site(
db_type=db_type, db_type=db_type,
db_host=db_host, db_host=db_host,
db_port=db_port, db_port=db_port,
db_user=db_user,
no_mariadb_socket=no_mariadb_socket, no_mariadb_socket=no_mariadb_socket,
setup=setup_db, setup=setup_db,
) )
@ -135,6 +137,7 @@ def install_db(
db_type=None, db_type=None,
db_host=None, db_host=None,
db_port=None, db_port=None,
db_user=None,
no_mariadb_socket=False, no_mariadb_socket=False,
setup=True, setup=True,
): ):
@ -156,6 +159,7 @@ def install_db(
db_type=db_type, db_type=db_type,
db_host=db_host, db_host=db_host,
db_port=db_port, db_port=db_port,
db_user=db_user,
) )
frappe.flags.in_install_db = True frappe.flags.in_install_db = True
@ -533,11 +537,23 @@ def init_singles():
def make_conf( 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 site = frappe.local.site
make_site_config( 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 sites_path = frappe.local.sites_path
frappe.destroy() frappe.destroy()
@ -545,7 +561,13 @@ def make_conf(
def make_site_config( 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)) frappe.create_folder(os.path.join(frappe.local.site_path))
site_file = get_site_config_path() site_file = get_site_config_path()
@ -563,6 +585,8 @@ def make_site_config(
if db_port: if db_port:
site_config["db_port"] = db_port site_config["db_port"] = db_port
site_config["db_user"] = db_user or db_name
with open(site_file, "w") as f: with open(site_file, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True)) f.write(json.dumps(site_config, indent=1, sort_keys=True))

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>", "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", "fieldname": "api_key",
"fieldtype": "Data", "fieldtype": "Data",
"label": "API Key", "label": "API Key"
"mandatory_depends_on": "google_drive_picker_enabled"
}, },
{ {
"depends_on": "enable", "depends_on": "enable",
@ -76,7 +75,7 @@
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-06-29 18:26:07.094851", "modified": "2024-01-16 13:19:22.365362",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Integrations", "module": "Integrations",
"name": "Google Settings", "name": "Google Settings",
@ -96,5 +95,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View file

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

View file

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

View file

@ -52,7 +52,7 @@ def get_latest_backup_file(with_files=False):
odb = BackupGenerator( odb = BackupGenerator(
frappe.conf.db_name, frappe.conf.db_name,
frappe.conf.db_name, frappe.conf.db_user,
frappe.conf.db_password, frappe.conf.db_password,
db_host=frappe.conf.db_host, db_host=frappe.conf.db_host,
db_port=frappe.conf.db_port, db_port=frappe.conf.db_port,
@ -110,7 +110,7 @@ def generate_files_backup():
backup = BackupGenerator( backup = BackupGenerator(
frappe.conf.db_name, frappe.conf.db_name,
frappe.conf.db_name, frappe.conf.db_user,
frappe.conf.db_password, frappe.conf.db_password,
db_host=frappe.conf.db_host, db_host=frappe.conf.db_host,
db_port=frappe.conf.db_port, db_port=frappe.conf.db_port,

File diff suppressed because it is too large Load diff

View file

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

View file

@ -49,9 +49,6 @@ frappe.ui.form.Control = class BaseControl {
if (this.df.get_status) { if (this.df.get_status) {
return this.df.get_status(this); return this.df.get_status(this);
} }
if (this.df.is_virtual) {
return "Read";
}
if ( if (
(!this.doctype && !this.docname) || (!this.doctype && !this.docname) ||

View file

@ -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)); let options = options_list.map((raw_option) => parse_option(raw_option));
if (sort) { 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 options

View file

@ -66,7 +66,7 @@ class FormTimeline extends BaseTimeline {
.append( .append(
` `
<div class="d-flex align-items-center show-all-activity"> <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"> <label class="switch">
<input type="checkbox"> <input type="checkbox">
<span class="slider round"></span> <span class="slider round"></span>

View file

@ -739,15 +739,17 @@ class FilterArea {
this.standard_filters_wrapper = this.list_view.page.page_form.find( this.standard_filters_wrapper = this.list_view.page.page_form.find(
".standard-filter-section" ".standard-filter-section"
); );
let fields = [ let fields = [];
{
if (!this.list_view.settings.hide_name_filter) {
fields.push({
fieldtype: "Data", fieldtype: "Data",
label: "ID", label: "ID",
condition: "like", condition: "like",
fieldname: "name", fieldname: "name",
onchange: () => this.refresh_list_view(), onchange: () => this.refresh_list_view(),
}, });
]; }
if (this.list_view.custom_filter_configs) { if (this.list_view.custom_filter_configs) {
this.list_view.custom_filter_configs.forEach((config) => { this.list_view.custom_filter_configs.forEach((config) => {

View file

@ -193,7 +193,7 @@ $.extend(frappe.perm, {
if (!perm) { if (!perm) {
let is_hidden = df && (cint(df.hidden) || cint(df.hidden_due_to_dependency)); 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"; return is_hidden ? "None" : is_read_only ? "Read" : "Write";
} }

View file

@ -317,7 +317,9 @@ frappe.search.AwesomeBar = class AwesomeBar {
var route = frappe.get_route(); var route = frappe.get_route();
if (route[0] === "List" && txt.indexOf(" in") === -1) { if (route[0] === "List" && txt.indexOf(" in") === -1) {
// search in title field // 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 search_field = meta.title_field || "name";
var options = {}; var options = {};
options[search_field] = ["like", "%" + txt + "%"]; options[search_field] = ["like", "%" + txt + "%"];

View file

@ -1,12 +1,11 @@
/* global gapi:false, google:false */ /* global gapi:false, google:false */
export default class GoogleDrivePicker { 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.scope = "https://www.googleapis.com/auth/drive.file";
this.pickerApiLoaded = false; this.pickerApiLoaded = false;
this.enabled = enabled; this.enabled = enabled;
this.appId = appId; this.appId = appId;
this.pickerCallback = pickerCallback; this.pickerCallback = pickerCallback;
this.developerKey = developerKey;
this.clientId = clientId; this.clientId = clientId;
} }
@ -45,7 +44,6 @@ export default class GoogleDrivePicker {
createPicker(access_token) { createPicker(access_token) {
this.view = new google.picker.View(google.picker.ViewId.DOCS); this.view = new google.picker.View(google.picker.ViewId.DOCS);
this.picker = new google.picker.PickerBuilder() this.picker = new google.picker.PickerBuilder()
.setDeveloperKey(this.developerKey)
.setAppId(this.appId) .setAppId(this.appId)
.setOAuthToken(access_token) .setOAuthToken(access_token)
.addView(this.view) .addView(this.view)

View file

@ -59,11 +59,12 @@ def get_sessions_to_clear(user=None, keep_current=False):
offset = 0 offset = 0
if user == frappe.session.user: if user == frappe.session.user:
simultaneous_sessions = frappe.db.get_value("User", user, "simultaneous_sessions") or 1 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 = frappe.qb.DocType("Sessions")
session_id = frappe.qb.from_(session).where(session.user == user) session_id = frappe.qb.from_(session).where(session.user == user)
if keep_current: if keep_current:
offset = max(0, offset - 1)
session_id = session_id.where(session.sid != frappe.session.sid) session_id = session_id.where(session.sid != frappe.session.sid)
query = ( query = (

View file

@ -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) dict(doctype="User", email=email, first_name=first_name, username=username, mobile_no=mobile_no)
).insert() ).insert()
user.new_password = password user.new_password = password
user.simultaneous_sessions = 1
user.add_roles("System Manager") user.add_roles("System Manager")
frappe.db.commit() frappe.db.commit()
@ -212,12 +213,12 @@ class TestSessionExpirty(FrappeAPITestCase):
seconds_elapsed = expiry_in * step / 100 seconds_elapsed = expiry_in * step / 100
time_now = add_to_date(session_created, seconds=seconds_elapsed, as_string=True) 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() data = s.get_session_data_from_db()
self.assertEqual(data.user, "Administrator") self.assertEqual(data.user, "Administrator")
# 1% higher should immediately expire # 1% higher should immediately expire
time_now = add_to_date(session_created, seconds=expiry_in * 1.01, as_string=True) time_of_expiry = add_to_date(session_created, seconds=expiry_in * 1.01, as_string=True)
with patch("frappe.utils.now", return_value=time_now): with self.freeze_time(time_of_expiry):
self.assertIn(sid, get_expired_sessions()) self.assertIn(sid, get_expired_sessions())
self.assertFalse(s.get_session_data_from_db()) self.assertFalse(s.get_session_data_from_db())

View file

@ -6,7 +6,9 @@ import gzip
import importlib import importlib
import json import json
import os import os
import secrets
import shlex import shlex
import string
import subprocess import subprocess
import unittest import unittest
from contextlib import contextmanager from contextlib import contextmanager
@ -511,6 +513,84 @@ class TestCommands(BaseTestCommands):
self.assertEqual(conf[key], value) 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): class TestBackups(BaseTestCommands):
backup_map = { backup_map = {

View file

@ -11,7 +11,7 @@ import frappe
from frappe.core.utils import find from frappe.core.utils import find
from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.database import savepoint 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.database.utils import FallBackDateTimeStr
from frappe.query_builder import Field from frappe.query_builder import Field
from frappe.query_builder.functions import Concat_ws 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)), list(frappe.db.sql(query, as_list=True, as_iterator=True)),
msg=f"{query=} results not same as iterator", 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()

View file

@ -193,3 +193,7 @@ class TestPerformance(FrappeTestCase):
result = frappe.db.sql(query, **kwargs) result = frappe.db.sql(query, **kwargs)
self.assertEqual(sys.getrefcount(result), 2) # Note: This always returns +1 self.assertEqual(sys.getrefcount(result), 2) # Note: This always returns +1
self.assertFalse(gc.get_referrers(result)) 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

View file

@ -261,12 +261,12 @@ def has_gravatar(email: str) -> str:
gravatar_url = get_gravatar_url(email, "404") gravatar_url = get_gravatar_url(email, "404")
try: try:
res = requests.get(gravatar_url) res = requests.get(gravatar_url, timeout=5)
if res.status_code == 200: if res.status_code == 200:
return gravatar_url return gravatar_url
else: else:
return "" return ""
except requests.exceptions.ConnectionError: except requests.exceptions.RequestException:
return "" return ""

View file

@ -5,6 +5,7 @@ import contextlib
# imports - standard imports # imports - standard imports
import gzip import gzip
import os import os
import sys
from calendar import timegm from calendar import timegm
from datetime import datetime from datetime import datetime
from glob import glob from glob import glob
@ -225,6 +226,9 @@ class BackupGenerator:
""" """
Encrypt all the backups created using gpg. 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) paths = (self.backup_path_db, self.backup_path_files, self.backup_path_private_files)
for path in paths: for path in paths:
if os.path.exists(path): if os.path.exists(path):
@ -492,7 +496,7 @@ def fetch_latest_backups(partial=False) -> dict:
frappe.only_for("System Manager") frappe.only_for("System Manager")
odb = BackupGenerator( odb = BackupGenerator(
frappe.conf.db_name, frappe.conf.db_name,
frappe.conf.db_name, frappe.conf.db_user,
frappe.conf.db_password, frappe.conf.db_password,
db_host=frappe.conf.db_host, db_host=frappe.conf.db_host,
db_port=frappe.conf.db_port, db_port=frappe.conf.db_port,
@ -559,7 +563,7 @@ def new_backup(
delete_temp_backups() delete_temp_backups()
odb = BackupGenerator( odb = BackupGenerator(
frappe.conf.db_name, frappe.conf.db_name,
frappe.conf.db_name, frappe.conf.db_user,
frappe.conf.db_password, frappe.conf.db_password,
db_host=frappe.conf.db_host, db_host=frappe.conf.db_host,
db_port=frappe.conf.db_port, db_port=frappe.conf.db_port,
@ -640,6 +644,9 @@ def get_or_generate_backup_encryption_key():
@contextlib.contextmanager @contextlib.contextmanager
def decrypt_backup(file_path: str, passphrase: str): 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): if not os.path.exists(file_path):
print("Invalid path: ", file_path) print("Invalid path: ", file_path)
return return

View file

@ -107,13 +107,13 @@ def capture_exception(message: str | None = None) -> None:
return return
try: try:
hub = Hub.current hub = Hub.current
if frappe.request: with hub.configure_scope() as scope:
with hub.configure_scope() as scope: if (
if ( os.getenv("ENABLE_SENTRY_DB_MONITORING") is None
os.getenv("ENABLE_SENTRY_DB_MONITORING") is None or os.getenv("SENTRY_TRACING_SAMPLE_RATE") is None
or os.getenv("SENTRY_TRACING_SAMPLE_RATE") is None ):
): set_scope(scope)
set_scope(scope) if frappe.request:
evt_processor = _make_wsgi_event_processor(frappe.request.environ, False) evt_processor = _make_wsgi_event_processor(frappe.request.environ, False)
scope.add_event_processor(evt_processor) scope.add_event_processor(evt_processor)
if frappe.request.is_json: if frappe.request.is_json:

View file

@ -1,7 +1,12 @@
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const redis = require("@redis/client"); 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"); const dns = require("dns");