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
|
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
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 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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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}'")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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))))
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>",
|
"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",
|
"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
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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", ""))
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
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
|
# 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:
|
||||||
|
|
|
||||||
|
|
@ -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) ||
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 + "%"];
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 = (
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue