@@ -103,7 +103,7 @@ async function execute() { | |||
log_error("There were some problems during build"); | |||
log(); | |||
log(chalk.dim(e.stack)); | |||
if (process.env.CI) { | |||
if (process.env.CI || PRODUCTION) { | |||
process.kill(process.pid); | |||
} | |||
return; | |||
@@ -203,6 +203,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False) -> None: | |||
"mute_emails": False, | |||
"has_dataurl": False, | |||
"new_site": new_site, | |||
"read_only": False, | |||
} | |||
) | |||
local.rollback_observers = [] | |||
@@ -284,9 +285,7 @@ def connect_replica(): | |||
user = local.conf.replica_db_name | |||
password = local.conf.replica_db_password | |||
local.replica_db = get_db( | |||
host=local.conf.replica_host, user=user, password=password, port=port, read_only=True | |||
) | |||
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password, port=port) | |||
# swap db connections | |||
local.primary_db = local.db | |||
@@ -2225,13 +2224,18 @@ def log_error(title=None, message=None, reference_doctype=None, reference_name=N | |||
title = title or "Error" | |||
traceback = as_unicode(traceback or get_traceback(with_context=True)) | |||
return get_doc( | |||
error_log = get_doc( | |||
doctype="Error Log", | |||
error=traceback, | |||
method=title, | |||
reference_doctype=reference_doctype, | |||
reference_name=reference_name, | |||
).insert(ignore_permissions=True) | |||
) | |||
if flags.read_only: | |||
error_log.deferred_insert() | |||
else: | |||
return error_log.insert(ignore_permissions=True) | |||
def get_desk_link(doctype, name): | |||
@@ -115,9 +115,12 @@ def init_request(request): | |||
# site does not exist | |||
raise NotFound | |||
if frappe.local.conf.get("maintenance_mode"): | |||
if frappe.local.conf.maintenance_mode: | |||
frappe.connect() | |||
raise frappe.SessionStopped("Session Stopped") | |||
if frappe.local.conf.allow_reads_during_maintenance: | |||
setup_read_only_mode() | |||
else: | |||
raise frappe.SessionStopped("Session Stopped") | |||
else: | |||
frappe.connect(set_admin_as_user=False) | |||
@@ -129,6 +132,24 @@ def init_request(request): | |||
frappe.local.http_request = frappe.auth.HTTPRequest() | |||
def setup_read_only_mode(): | |||
"""During maintenance_mode reads to DB can still be performed to reduce downtime. This | |||
function sets up read only mode | |||
- Setting global flag so other pages, desk and database can know that we are in read only mode. | |||
- Setup read only database access either by: | |||
- Connecting to read replica if one exists | |||
- Or setting up read only SQL transactions. | |||
""" | |||
frappe.flags.read_only = True | |||
# If replica is available then just connect replica, else setup read only transaction. | |||
if frappe.conf.read_from_replica: | |||
frappe.connect_replica() | |||
else: | |||
frappe.db.begin(read_only=True) | |||
def log_request(request, response): | |||
if hasattr(frappe.local, "conf") and frappe.local.conf.enable_frappe_logger: | |||
frappe.logger("frappe.web", allow_site=frappe.local.site).info( | |||
@@ -230,6 +251,12 @@ def handle_exception(e): | |||
or (frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")) | |||
) | |||
if not frappe.session.user: | |||
# If session creation fails then user won't be unset. This causes a lot of code that | |||
# assumes presence of this to fail. Session creation fails => guest or expired login | |||
# usually. | |||
frappe.session.user = "Guest" | |||
if respond_as_json: | |||
# handle ajax responses first | |||
# if the request is ajax, send back the trace or error message | |||
@@ -6,7 +6,7 @@ import frappe | |||
import frappe.database | |||
import frappe.utils | |||
import frappe.utils.user | |||
from frappe import _, conf | |||
from frappe import _ | |||
from frappe.core.doctype.activity_log.activity_log import add_authentication_log | |||
from frappe.modules.patch_handler import check_session_stopped | |||
from frappe.sessions import Session, clear_sessions, delete_session | |||
@@ -30,9 +30,6 @@ class HTTPRequest: | |||
# load cookies | |||
self.set_cookies() | |||
# set frappe.local.db | |||
self.connect() | |||
# login and start/resume user session | |||
self.set_session() | |||
@@ -45,9 +42,6 @@ class HTTPRequest: | |||
# write out latest cookies | |||
frappe.local.cookie_manager.init_cookies() | |||
# check session status | |||
check_session_stopped() | |||
@property | |||
def domain(self): | |||
if not getattr(self, "_domain", None): | |||
@@ -97,16 +91,6 @@ class HTTPRequest: | |||
def set_lang(self): | |||
frappe.local.lang = get_language() | |||
def get_db_name(self): | |||
"""get database name from conf""" | |||
return conf.db_name | |||
def connect(self): | |||
"""connect to db, from ac_name or db_name""" | |||
frappe.local.db = frappe.database.get_db( | |||
user=self.get_db_name(), password=getattr(conf, "db_password", "") | |||
) | |||
class LoginManager: | |||
@@ -420,6 +420,9 @@ def install_app(context, apps, force=False): | |||
print(f"An error occurred while installing {app}{err_msg}") | |||
exit_code = 1 | |||
if not exit_code: | |||
frappe.db.commit() | |||
frappe.destroy() | |||
sys.exit(exit_code) | |||
@@ -102,8 +102,7 @@ | |||
"fetch_from": "reference_name.owner", | |||
"fieldname": "reference_owner", | |||
"fieldtype": "Read Only", | |||
"label": "Reference Owner", | |||
"search_index": 1 | |||
"label": "Reference Owner" | |||
}, | |||
{ | |||
"fieldname": "column_break_14", | |||
@@ -154,7 +153,7 @@ | |||
"icon": "fa fa-comment", | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2021-10-25 11:43:57.504565", | |||
"modified": "2022-09-13 15:19:42.474114", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Activity Log", | |||
@@ -181,6 +180,7 @@ | |||
"search_fields": "subject", | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"title_field": "subject", | |||
"track_seen": 1 | |||
} | |||
} |
@@ -37,7 +37,6 @@ def on_doctype_update(): | |||
"""Add indexes in `tabActivity Log`""" | |||
frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"]) | |||
frappe.db.add_index("Activity Log", ["timeline_doctype", "timeline_name"]) | |||
frappe.db.add_index("Activity Log", ["link_doctype", "link_name"]) | |||
def add_authentication_log(subject, user, operation="Login", status="Success"): | |||
@@ -60,7 +60,6 @@ class Comment(Document): | |||
def on_doctype_update(): | |||
frappe.db.add_index("Comment", ["reference_doctype", "reference_name"]) | |||
frappe.db.add_index("Comment", ["link_doctype", "link_name"]) | |||
def update_comment_in_doc(doc): | |||
@@ -212,7 +212,8 @@ | |||
"fieldname": "parent", | |||
"fieldtype": "Data", | |||
"label": "Reference Document Type", | |||
"read_only": 1 | |||
"read_only": 1, | |||
"search_index": 1 | |||
}, | |||
{ | |||
"default": "0", | |||
@@ -9,7 +9,7 @@ from frappe.query_builder.functions import Now | |||
class ErrorLog(Document): | |||
def onload(self): | |||
if not self.seen: | |||
if not self.seen and not frappe.flags.read_only: | |||
self.db_set("seen", 1, update_modified=0) | |||
frappe.db.commit() | |||
@@ -64,8 +64,7 @@ | |||
"fieldname": "is_home_folder", | |||
"fieldtype": "Check", | |||
"hidden": 1, | |||
"label": "Is Home Folder", | |||
"search_index": 1 | |||
"label": "Is Home Folder" | |||
}, | |||
{ | |||
"default": "0", | |||
@@ -125,8 +124,7 @@ | |||
"in_standard_filter": 1, | |||
"label": "Attached To DocType", | |||
"options": "DocType", | |||
"read_only": 1, | |||
"search_index": 1 | |||
"read_only": 1 | |||
}, | |||
{ | |||
"fieldname": "column_break_10", | |||
@@ -136,8 +134,7 @@ | |||
"fieldname": "attached_to_name", | |||
"fieldtype": "Data", | |||
"label": "Attached To Name", | |||
"read_only": 1, | |||
"search_index": 1 | |||
"read_only": 1 | |||
}, | |||
{ | |||
"fieldname": "attached_to_field", | |||
@@ -175,7 +172,7 @@ | |||
"icon": "fa fa-file", | |||
"idx": 1, | |||
"links": [], | |||
"modified": "2020-06-28 12:21:30.772386", | |||
"modified": "2022-09-13 15:50:15.508250", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "File", | |||
@@ -210,6 +207,7 @@ | |||
], | |||
"sort_field": "modified", | |||
"sort_order": "ASC", | |||
"states": [], | |||
"title_field": "file_name", | |||
"track_changes": 1 | |||
} |
@@ -128,7 +128,6 @@ | |||
"fieldtype": "Section Break" | |||
}, | |||
{ | |||
"depends_on": "eval:doc.is_standard == 'Yes'", | |||
"fieldname": "roles", | |||
"fieldtype": "Table", | |||
"label": "Roles", | |||
@@ -192,10 +191,11 @@ | |||
"idx": 1, | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2020-08-17 16:49:28.474274", | |||
"modified": "2022-09-15 13:37:24.531848", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Report", | |||
"naming_rule": "By fieldname", | |||
"owner": "Administrator", | |||
"permissions": [ | |||
{ | |||
@@ -242,5 +242,6 @@ | |||
"show_name_in_global_search": 1, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"track_changes": 1 | |||
} |
@@ -2,6 +2,7 @@ | |||
# For license information, please see license.txt | |||
import functools | |||
import re | |||
from rq.command import send_stop_job_command | |||
from rq.job import Job | |||
@@ -113,12 +114,18 @@ class RQJob(Document): | |||
def serialize_job(job: Job) -> frappe._dict: | |||
modified = job.last_heartbeat or job.ended_at or job.started_at or job.created_at | |||
job_name = job.kwargs.get("kwargs", {}).get("job_type") or str(job.kwargs.get("job_name")) | |||
# function objects have this repr: '<function functionname at 0xmemory_address >' | |||
# This regex just removes unnecessary things around it. | |||
if matches := re.match(r"<function (?P<func_name>.*) at 0x.*>", job_name): | |||
job_name = matches.group("func_name") | |||
return frappe._dict( | |||
name=job.id, | |||
job_id=job.id, | |||
queue=job.origin.rsplit(":", 1)[1], | |||
job_name=job.kwargs.get("kwargs", {}).get("job_type") or str(job.kwargs.get("job_name")), | |||
job_name=job_name, | |||
status=job.get_status(), | |||
started_at=convert_utc_to_user_timezone(job.started_at) if job.started_at else "", | |||
ended_at=convert_utc_to_user_timezone(job.ended_at) if job.ended_at else "", | |||
@@ -29,11 +29,9 @@ class TestRQJob(FrappeTestCase): | |||
def test_serialization(self): | |||
job = frappe.enqueue(method=self.BG_JOB, queue="short") | |||
rq_job = frappe.get_doc("RQ Job", job.id) | |||
self.assertEqual(job, rq_job.job) | |||
self.assertDocumentEqual( | |||
{ | |||
"name": job.id, | |||
@@ -46,6 +44,11 @@ class TestRQJob(FrappeTestCase): | |||
) | |||
self.check_status(job, "finished") | |||
def test_func_obj_serialization(self): | |||
job = frappe.enqueue(method=test_func, queue="short") | |||
rq_job = frappe.get_doc("RQ Job", job.id) | |||
self.assertEqual(rq_job.job_name, "test_func") | |||
def test_get_list_filtering(self): | |||
# Check failed job clearning and filtering | |||
@@ -126,7 +126,7 @@ | |||
{ | |||
"fieldname": "middle_name", | |||
"fieldtype": "Data", | |||
"label": "Middle Name (Optional)", | |||
"label": "Middle Name", | |||
"oldfieldname": "middle_name", | |||
"oldfieldtype": "Data" | |||
}, | |||
@@ -492,7 +492,7 @@ | |||
{ | |||
"description": "Restrict user from this IP address only. Multiple IP addresses can be added by separating with commas. Also accepts partial IP addresses like (111.111.111)", | |||
"fieldname": "restrict_ip", | |||
"fieldtype": "Data", | |||
"fieldtype": "Small Text", | |||
"label": "Restrict IP", | |||
"permlevel": 1 | |||
}, | |||
@@ -722,7 +722,7 @@ | |||
"link_fieldname": "user" | |||
} | |||
], | |||
"modified": "2022-05-25 01:00:51.345319", | |||
"modified": "2022-09-19 16:05:46.485242", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "User", | |||
@@ -39,21 +39,17 @@ def drop_user_and_database(db_name, root_login=None, root_password=None): | |||
) | |||
def get_db(host=None, user=None, password=None, port=None, read_only=False): | |||
def get_db(host=None, user=None, password=None, port=None): | |||
import frappe | |||
if frappe.conf.db_type == "postgres": | |||
import frappe.database.postgres.database | |||
return frappe.database.postgres.database.PostgresDatabase( | |||
host, user, password, port=port, read_only=read_only | |||
) | |||
return frappe.database.postgres.database.PostgresDatabase(host, user, password, port=port) | |||
else: | |||
import frappe.database.mariadb.database | |||
return frappe.database.mariadb.database.MariaDBDatabase( | |||
host, user, password, port=port, read_only=read_only | |||
) | |||
return frappe.database.mariadb.database.MariaDBDatabase(host, user, password, port=port) | |||
def setup_help_database(help_db_name): | |||
@@ -82,14 +82,12 @@ class Database: | |||
ac_name=None, | |||
use_default=0, | |||
port=None, | |||
read_only=False, | |||
): | |||
self.setup_type_map() | |||
self.host = host or frappe.conf.db_host or "127.0.0.1" | |||
self.port = port or frappe.conf.db_port or "" | |||
self.user = user or frappe.conf.db_name | |||
self.db_name = frappe.conf.db_name | |||
self.read_only = read_only # Uses READ ONLY connection if set | |||
self._conn = None | |||
if ac_name: | |||
@@ -217,6 +215,15 @@ class Database: | |||
elif self.is_timedout(e): | |||
raise frappe.QueryTimeoutError(e) from e | |||
elif self.is_read_only_mode_error(e): | |||
frappe.throw( | |||
_( | |||
"Site is running in read only mode, this action can not be performed right now. Please try again later." | |||
), | |||
title=_("In Read Only Mode"), | |||
exc=frappe.InReadOnlyMode, | |||
) | |||
# TODO: added temporarily | |||
elif self.db_type == "postgres": | |||
traceback.print_stack() | |||
@@ -957,8 +964,10 @@ class Database: | |||
return defaults.get(frappe.scrub(key)) | |||
def begin(self): | |||
self.sql("START TRANSACTION") | |||
def begin(self, *, read_only=False): | |||
read_only = read_only or frappe.flags.read_only | |||
mode = "READ ONLY" if read_only else "" | |||
self.sql(f"START TRANSACTION {mode}") | |||
def commit(self): | |||
"""Commit current transaction. Calls SQL `COMMIT`.""" | |||
@@ -966,9 +975,7 @@ class Database: | |||
frappe.call(method[0], *(method[1] or []), **(method[2] or {})) | |||
self.sql("commit") | |||
if self.db_type == "postgres": | |||
# Postgres requires explicitly starting new transaction | |||
self.begin() | |||
self.begin() # explicitly start a new transaction | |||
frappe.local.rollback_observers = [] | |||
self.flush_realtime_log() | |||
@@ -32,6 +32,10 @@ class MariaDBExceptionUtil: | |||
def is_timedout(e: pymysql.Error) -> bool: | |||
return e.args[0] == ER.LOCK_WAIT_TIMEOUT | |||
@staticmethod | |||
def is_read_only_mode_error(e: pymysql.Error) -> bool: | |||
return e.args[0] == 1792 | |||
@staticmethod | |||
def is_table_missing(e: pymysql.Error) -> bool: | |||
return e.args[0] == ER.NO_SUCH_TABLE | |||
@@ -12,7 +12,7 @@ from psycopg2.errorcodes import ( | |||
UNDEFINED_TABLE, | |||
UNIQUE_VIOLATION, | |||
) | |||
from psycopg2.errors import SequenceGeneratorLimitExceeded, SyntaxError | |||
from psycopg2.errors import ReadOnlySqlTransaction, SequenceGeneratorLimitExceeded, SyntaxError | |||
from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ | |||
import frappe | |||
@@ -55,6 +55,10 @@ class PostgresExceptionUtil: | |||
# http://initd.org/psycopg/docs/extensions.html?highlight=datatype#psycopg2.extensions.QueryCanceledError | |||
return isinstance(e, psycopg2.extensions.QueryCanceledError) | |||
@staticmethod | |||
def is_read_only_mode_error(e) -> bool: | |||
return isinstance(e, ReadOnlySqlTransaction) | |||
@staticmethod | |||
def is_syntax_error(e): | |||
return isinstance(e, SyntaxError) | |||
@@ -291,12 +291,13 @@ class Workspace: | |||
quick_lists = self.doc.quick_lists | |||
for item in quick_lists: | |||
new_item = item.as_dict().copy() | |||
if self.is_item_allowed(item.document_type, "doctype"): | |||
new_item = item.as_dict().copy() | |||
# Translate label | |||
new_item["label"] = _(item.label) if item.label else _(item.document_type) | |||
# Translate label | |||
new_item["label"] = _(item.label) if item.label else _(item.document_type) | |||
items.append(new_item) | |||
items.append(new_item) | |||
return items | |||
@@ -221,7 +221,7 @@ | |||
"in_list_view": 1, | |||
"in_standard_filter": 1, | |||
"label": "Status", | |||
"options": "Open\nClosed" | |||
"options": "Open\nCompleted\nClosed" | |||
}, | |||
{ | |||
"collapsible": 1, | |||
@@ -318,4 +318,4 @@ | |||
"track_changes": 1, | |||
"track_seen": 1, | |||
"track_views": 1 | |||
} | |||
} |
@@ -22,18 +22,14 @@ | |||
"fieldname": "subject", | |||
"fieldtype": "Text", | |||
"in_list_view": 1, | |||
"label": "Subject", | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"label": "Subject" | |||
}, | |||
{ | |||
"fieldname": "for_user", | |||
"fieldtype": "Link", | |||
"hidden": 1, | |||
"label": "For User", | |||
"options": "User", | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"options": "User" | |||
}, | |||
{ | |||
"fieldname": "type", | |||
@@ -42,36 +38,26 @@ | |||
"in_list_view": 1, | |||
"in_standard_filter": 1, | |||
"label": "Type", | |||
"options": "Mention\nEnergy Point\nAssignment\nShare\nAlert", | |||
"search_index": 1, | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"options": "Mention\nEnergy Point\nAssignment\nShare\nAlert" | |||
}, | |||
{ | |||
"fieldname": "email_content", | |||
"fieldtype": "Text Editor", | |||
"label": "Message", | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"label": "Message" | |||
}, | |||
{ | |||
"fieldname": "document_type", | |||
"fieldtype": "Link", | |||
"hidden": 1, | |||
"label": "Document Type", | |||
"options": "DocType", | |||
"search_index": 1, | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"options": "DocType" | |||
}, | |||
{ | |||
"fieldname": "document_name", | |||
"fieldtype": "Data", | |||
"hidden": 1, | |||
"label": "Document Link", | |||
"search_index": 1, | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"search_index": 1 | |||
}, | |||
{ | |||
"fieldname": "from_user", | |||
@@ -79,9 +65,7 @@ | |||
"hidden": 1, | |||
"label": "From User", | |||
"options": "User", | |||
"search_index": 1, | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"search_index": 1 | |||
}, | |||
{ | |||
"default": "0", | |||
@@ -89,38 +73,30 @@ | |||
"fieldtype": "Check", | |||
"hidden": 1, | |||
"ignore_user_permissions": 1, | |||
"label": "Read", | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"label": "Read" | |||
}, | |||
{ | |||
"fieldname": "open_reference_document", | |||
"fieldtype": "Button", | |||
"label": "Open Reference Document", | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"label": "Open Reference Document" | |||
}, | |||
{ | |||
"fieldname": "attached_file", | |||
"fieldtype": "Code", | |||
"hidden": 1, | |||
"label": "Attached File", | |||
"options": "JSON", | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"options": "JSON" | |||
}, | |||
{ | |||
"fieldname": "attachment_link", | |||
"fieldtype": "HTML", | |||
"label": "Attachment Link", | |||
"show_days": 1, | |||
"show_seconds": 1 | |||
"label": "Attachment Link" | |||
} | |||
], | |||
"hide_toolbar": 1, | |||
"in_create": 1, | |||
"links": [], | |||
"modified": "2021-10-25 17:26:09.703215", | |||
"modified": "2022-09-13 16:08:48.153934", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "Notification Log", | |||
@@ -138,6 +114,7 @@ | |||
], | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"title_field": "subject", | |||
"track_seen": 1 | |||
} | |||
} |
@@ -136,7 +136,7 @@ def get_email_header(doc): | |||
@frappe.whitelist() | |||
def get_notification_logs(limit=20): | |||
notification_logs = frappe.db.get_list( | |||
"Notification Log", fields=["*"], limit=limit, order_by="creation desc" | |||
"Notification Log", fields=["*"], limit=limit, order_by="modified desc" | |||
) | |||
users = [log.from_user for log in notification_logs] | |||
@@ -266,6 +266,7 @@ def duplicate_page(page_name, new_page): | |||
doc.public = new_page.get("is_public") | |||
doc.for_user = "" | |||
doc.label = doc.title | |||
doc.module = "" | |||
if not doc.public: | |||
doc.for_user = doc.for_user or frappe.session.user | |||
doc.label = f"{doc.title}-{doc.for_user}" | |||
@@ -175,7 +175,7 @@ frappe.ui.form.on("Notification", { | |||
notification: frm.doc.name, | |||
}, | |||
callback: function (r) { | |||
if (r.message) { | |||
if (r.message && r.message.length > 0) { | |||
frappe.msgprint(r.message); | |||
} else { | |||
frappe.msgprint(__("No alerts for today")); | |||
@@ -236,6 +236,10 @@ class QueryDeadlockError(Exception): | |||
pass | |||
class InReadOnlyMode(ValidationError): | |||
http_status_code = 503 # temporarily not available | |||
class TooManyWritesError(Exception): | |||
pass | |||
@@ -11,6 +11,7 @@ import click | |||
import frappe | |||
from frappe.defaults import _clear_cache | |||
from frappe.utils import cint, is_git_url | |||
from frappe.utils.dashboard import sync_dashboards | |||
def _is_scheduler_enabled() -> bool: | |||
@@ -301,6 +302,7 @@ def install_app(name, verbose=False, set_as_patched=True, force=False): | |||
sync_jobs() | |||
sync_fixtures(name) | |||
sync_customizations(name) | |||
sync_dashboards(name) | |||
for after_sync in app_hooks.after_sync or []: | |||
frappe.get_attr(after_sync)() # | |||
@@ -13,6 +13,7 @@ from frappe.cache_manager import clear_global_cache | |||
from frappe.core.doctype.language.language import sync_languages | |||
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs | |||
from frappe.database.schema import add_column | |||
from frappe.deferred_insert import save_to_db as flush_deferred_inserts | |||
from frappe.desk.notifications import clear_notifications | |||
from frappe.modules.patch_handler import PatchType | |||
from frappe.modules.utils import sync_customizations | |||
@@ -123,6 +124,7 @@ class SiteMigration: | |||
* Sync in-Desk Module Dashboards | |||
* Sync customizations: Custom Fields, Property Setters, Custom Permissions | |||
* Sync Frappe's internal language master | |||
* Flush deferred inserts made during maintenance mode. | |||
* Sync Portal Menu Items | |||
* Sync Installed Applications Version History | |||
* Execute `after_migrate` hooks | |||
@@ -132,6 +134,7 @@ class SiteMigration: | |||
sync_dashboards() | |||
sync_customizations() | |||
sync_languages() | |||
flush_deferred_inserts() | |||
frappe.get_single("Portal Settings").sync_menu() | |||
frappe.get_single("Installed Applications").update_versions() | |||
@@ -1365,7 +1365,7 @@ class Document(BaseDocument): | |||
if not user: | |||
user = frappe.session.user | |||
if self.meta.track_seen: | |||
if self.meta.track_seen and not frappe.flags.read_only: | |||
_seen = self.get("_seen") or [] | |||
_seen = frappe.parse_json(_seen) | |||
@@ -1380,15 +1380,19 @@ class Document(BaseDocument): | |||
user = frappe.session.user | |||
if hasattr(self.meta, "track_views") and self.meta.track_views: | |||
frappe.get_doc( | |||
view_log = frappe.get_doc( | |||
{ | |||
"doctype": "View Log", | |||
"viewed_by": frappe.session.user, | |||
"reference_doctype": self.doctype, | |||
"reference_name": self.name, | |||
} | |||
).insert(ignore_permissions=True) | |||
frappe.local.flags.commit = True | |||
) | |||
if frappe.flags.read_only: | |||
view_log.deferred_insert() | |||
else: | |||
view_log.insert(ignore_permissions=True) | |||
frappe.local.flags.commit = True | |||
def log_error(self, title=None, message=None): | |||
"""Helper function to create an Error Log""" | |||
@@ -1535,6 +1539,20 @@ class Document(BaseDocument): | |||
return DocTags(self.doctype).get_tags(self.name).split(",")[1:] | |||
def deferred_insert(self) -> None: | |||
"""Push the document to redis temporarily and insert later. | |||
WARN: This doesn't guarantee insertion as redis can be restarted | |||
before data is flushed to database. | |||
""" | |||
from frappe.deferred_insert import deferred_insert | |||
self.set_user_and_timestamp() | |||
doc = self.get_valid_dict(convert_dates_to_str=True, ignore_virtual=True) | |||
deferred_insert(doctype=self.doctype, records=doc) | |||
def __repr__(self): | |||
name = self.name or "unsaved" | |||
doctype = self.__class__.__name__ | |||
@@ -527,7 +527,12 @@ def get_select_fields(old: str, new: str) -> list[dict]: | |||
standard_fields = ( | |||
frappe.qb.from_(df) | |||
.select(df.parent, df.fieldname, st_issingle) | |||
.where((df.parent != new) & (df.fieldtype == "Select") & (df.options.like(f"%{old}%"))) | |||
.where( | |||
(df.parent != new) | |||
& (df.fieldname != "fieldtype") | |||
& (df.fieldtype == "Select") | |||
& (df.options.like(f"%{old}%")) | |||
) | |||
.run(as_dict=True) | |||
) | |||
@@ -8,17 +8,17 @@ import os | |||
import frappe | |||
from frappe.modules.import_file import import_file_by_path | |||
from frappe.modules.patch_handler import block_user | |||
from frappe.modules.patch_handler import _patch_mode | |||
from frappe.utils import update_progress_bar | |||
def sync_all(force=0, reset_permissions=False): | |||
block_user(True) | |||
_patch_mode(True) | |||
for app in frappe.get_installed_apps(): | |||
sync_for(app, force, reset_permissions=reset_permissions) | |||
block_user(False) | |||
_patch_mode(False) | |||
frappe.clear_cache() | |||
@@ -154,7 +154,7 @@ def run_single(patchmodule=None, method=None, methodargs=None, force=False): | |||
def execute_patch(patchmodule, method=None, methodargs=None): | |||
"""execute the patch""" | |||
block_user(True) | |||
_patch_mode(True) | |||
if patchmodule.startswith("execute:"): | |||
has_patch_file = False | |||
@@ -197,7 +197,7 @@ def execute_patch(patchmodule, method=None, methodargs=None): | |||
else: | |||
frappe.db.commit() | |||
end_time = time.time() | |||
block_user(False) | |||
_patch_mode(False) | |||
print(f"Success: Done in {round(end_time - start_time, 3)}s") | |||
return True | |||
@@ -216,18 +216,14 @@ def executed(patchmodule): | |||
return frappe.db.get_value("Patch Log", {"patch": patchmodule}) | |||
def block_user(block, msg=None): | |||
def _patch_mode(enable): | |||
"""stop/start execution till patch is run""" | |||
frappe.local.flags.in_patch = block | |||
frappe.db.begin() | |||
if not msg: | |||
msg = "Patches are being executed in the system. Please try again in a few moments." | |||
frappe.db.set_global("__session_status", block and "stop" or None) | |||
frappe.db.set_global("__session_status_message", block and msg or None) | |||
frappe.local.flags.in_patch = enable | |||
frappe.db.commit() | |||
def check_session_stopped(): | |||
"""This function is deprecated. Use maintenance_mode in site config instead.""" | |||
if frappe.db.get_global("__session_status") == "stop": | |||
frappe.msgprint(frappe.db.get_global("__session_status_message")) | |||
raise frappe.SessionStopped("Session Stopped") |
@@ -212,3 +212,4 @@ frappe.patches.v14_0.delete_data_migration_tool | |||
frappe.patches.v14_0.set_suspend_email_queue_default | |||
frappe.patches.v14_0.different_encryption_key | |||
frappe.patches.v14_0.update_multistep_webforms | |||
frappe.patches.v14_0.drop_unused_indexes |
@@ -0,0 +1,56 @@ | |||
""" | |||
This patch just drops some known indexes which aren't being used anymore or never were used. | |||
""" | |||
import click | |||
import frappe | |||
UNUSED_INDEXES = [ | |||
("Comment", ["link_doctype", "link_name"]), | |||
("Activity Log", ["link_doctype", "link_name"]), | |||
] | |||
def execute(): | |||
if frappe.db.db_type == "postgres": | |||
return | |||
db_tables = frappe.db.get_tables(cached=False) | |||
# All parent indexes | |||
parent_doctypes = frappe.get_all( | |||
"DocType", | |||
{"istable": 0, "is_virtual": 0, "issingle": 0}, | |||
pluck="name", | |||
) | |||
db_tables = frappe.db.get_tables(cached=False) | |||
for doctype in parent_doctypes: | |||
table = f"tab{doctype}" | |||
if table not in db_tables: | |||
continue | |||
_drop_index_if_exists(table, "parent") | |||
# Unused composite indexes | |||
for doctype, index_fields in UNUSED_INDEXES: | |||
table = f"tab{doctype}" | |||
index_name = frappe.db.get_index_name(index_fields) | |||
if table not in db_tables: | |||
continue | |||
_drop_index_if_exists(table, index_name) | |||
def _drop_index_if_exists(table: str, index: str): | |||
if not frappe.db.has_index(table, index): | |||
click.echo(f"- Skipped {index} index for {table} because it doesn't exist") | |||
return | |||
try: | |||
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`") | |||
except Exception as e: | |||
frappe.log_error("Failed to drop index") | |||
click.secho(f"x Failed to drop index {index} from {table}\n {str(e)}", fg="red") | |||
return | |||
click.echo(f"✓ dropped {index} index from {table}") |
@@ -568,7 +568,7 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
this.chart_area.body.empty(); | |||
$.extend(args, { | |||
type: "line", | |||
colors: ["green"], | |||
colors: args.colors || ["green"], | |||
truncateLegends: 1, | |||
axisOptions: { | |||
shortenYAxisNumbers: 1, | |||
@@ -445,6 +445,10 @@ frappe.ui.form.Form = class FrappeForm { | |||
.toggleClass("cancelled-form", this.doc.docstatus === 2); | |||
this.show_conflict_message(); | |||
if (frappe.boot.read_only) { | |||
this.disable_form(); | |||
} | |||
} | |||
} | |||
@@ -197,10 +197,16 @@ frappe.ui.form.Toolbar = class Toolbar { | |||
// check if docname is updatable | |||
if (me.can_rename()) { | |||
let label = __("New Name"); | |||
if (me.frm.meta.autoname && me.frm.meta.autoname.startsWith("field:")) { | |||
let fieldname = me.frm.meta.autoname.split(":")[1]; | |||
label = __("New {0}", [me.frm.get_docfield(fieldname).label]); | |||
} | |||
fields.push( | |||
...[ | |||
{ | |||
label: __("New Name"), | |||
label: label, | |||
fieldname: "name", | |||
fieldtype: "Data", | |||
reqd: 1, | |||
@@ -215,7 +215,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
} | |||
set_primary_action() { | |||
if (this.can_create) { | |||
if (this.can_create && !frappe.boot.read_only) { | |||
const doctype_name = __(frappe.router.doctype_layout) || __(this.doctype); | |||
// Better style would be __("Add {0}", [doctype_name], "Primary action in list view") | |||
@@ -248,19 +248,34 @@ frappe.views.ListViewSelect = class ListViewSelect { | |||
} | |||
setup_kanban_boards() { | |||
function fetch_kanban_board(doctype) { | |||
frappe.db.get_value( | |||
"Kanban Board", | |||
{ reference_doctype: doctype }, | |||
"name", | |||
(board) => { | |||
if (!$.isEmptyObject(board)) { | |||
frappe.set_route("list", doctype, "kanban", board.name); | |||
} else { | |||
frappe.views.KanbanView.show_kanban_dialog(doctype); | |||
} | |||
} | |||
); | |||
} | |||
const last_opened_kanban = | |||
frappe.model.user_settings[this.doctype]["Kanban"]?.last_kanban_board; | |||
if (!last_opened_kanban) { | |||
return frappe.views.KanbanView.show_kanban_dialog(this.doctype, true); | |||
fetch_kanban_board(this.doctype); | |||
} else { | |||
frappe.db.exists("Kanban Board", last_opened_kanban).then((exists) => { | |||
if (exists) { | |||
frappe.set_route("list", this.doctype, "kanban", last_opened_kanban); | |||
} else { | |||
fetch_kanban_board(this.doctype); | |||
} | |||
}); | |||
} | |||
frappe.db.exists("Kanban Board", last_opened_kanban).then((exists) => { | |||
if (exists) { | |||
frappe.set_route("list", this.doctype, "kanban", last_opened_kanban); | |||
} else { | |||
frappe.views.KanbanView.show_kanban_dialog(this.doctype, true); | |||
} | |||
}); | |||
} | |||
get_calendars() { | |||
@@ -6,6 +6,11 @@ | |||
<ul class="nav navbar-nav d-none d-sm-flex" id="navbar-breadcrumbs"></ul> | |||
<div class="collapse navbar-collapse justify-content-end"> | |||
<form class="form-inline fill-width justify-content-end" role="search" onsubmit="return false;"> | |||
{% if (frappe.boot.read_only) { %} | |||
<span class="indicator-pill yellow no-indicator-dot" title="{%= __("Your site is getting upgraded.") %}"> | |||
{%= __("Read Only Mode") %} | |||
</span> | |||
{% } %} | |||
<div class="input-group search-bar text-muted hidden"> | |||
<input | |||
id="navbar-search" | |||
@@ -35,12 +35,28 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { | |||
this.card_meta = this.get_card_meta(); | |||
this.page_length = 0; | |||
this.menu_items.push({ | |||
label: __("Save filters"), | |||
action: () => { | |||
this.save_kanban_board_filters(); | |||
}, | |||
}); | |||
this.menu_items.push( | |||
...[ | |||
{ | |||
label: __("Save filters"), | |||
action: () => { | |||
this.save_kanban_board_filters(); | |||
}, | |||
}, | |||
{ | |||
label: __("Delete Kanban Board"), | |||
action: () => { | |||
frappe.confirm("Are you sure you want to proceed?", () => { | |||
frappe.db.delete_doc("Kanban Board", this.board_name).then(() => { | |||
frappe.show_alert(`Kanban Board ${this.board_name} deleted.`); | |||
frappe.set_route("List", this.doctype, "List"); | |||
}); | |||
}); | |||
}, | |||
}, | |||
] | |||
); | |||
return this.get_board(); | |||
}); | |||
} | |||
@@ -234,13 +250,9 @@ frappe.views.KanbanView.get_kanbans = function (doctype) { | |||
} | |||
}; | |||
frappe.views.KanbanView.show_kanban_dialog = function (doctype, show_existing) { | |||
let dialog = null; | |||
frappe.views.KanbanView.get_kanbans(doctype).then((kanbans) => { | |||
dialog = new_kanban_dialog(kanbans, show_existing); | |||
dialog.show(); | |||
}); | |||
frappe.views.KanbanView.show_kanban_dialog = function (doctype) { | |||
let dialog = new_kanban_dialog(); | |||
dialog.show(); | |||
function make_kanban_board(board_name, field_name, project) { | |||
return frappe.call({ | |||
@@ -262,79 +274,74 @@ frappe.views.KanbanView.show_kanban_dialog = function (doctype, show_existing) { | |||
}); | |||
} | |||
function new_kanban_dialog(kanbans, show_existing) { | |||
function new_kanban_dialog() { | |||
/* Kanban dialog can show either "Save" or "Customize Form" option depending if any Select fields exist in the DocType for Kanban creation | |||
*/ | |||
if (dialog) return dialog; | |||
const dialog_fields = get_fields_for_dialog( | |||
kanbans.map((kanban) => kanban.name), | |||
show_existing | |||
); | |||
const select_fields = frappe.get_meta(doctype).fields.filter((df) => { | |||
return df.fieldtype === "Select" && df.fieldname !== "kanban_column"; | |||
}); | |||
const dialog_fields = get_fields_for_dialog(select_fields); | |||
const to_save = select_fields.length > 0; | |||
const primary_action_label = to_save ? __("Save") : __("Customize Form"); | |||
const dialog_title = to_save ? __("New Kanban Board") : __("No Select Field Found"); | |||
let primary_action = () => { | |||
if (to_save) { | |||
const values = dialog.get_values(); | |||
if (!values.selected_kanban || values.selected_kanban == "Create New Board") { | |||
make_kanban_board(values.board_name, values.field_name, values.project).then( | |||
() => dialog.hide(), | |||
(err) => frappe.msgprint(err) | |||
); | |||
} else { | |||
frappe.set_route( | |||
kanbans.find((kanban) => kanban.name == values.selected_kanban).route | |||
); | |||
} | |||
make_kanban_board(values.board_name, values.field_name, values.project).then( | |||
() => dialog.hide(), | |||
(err) => frappe.msgprint(err) | |||
); | |||
} else { | |||
frappe.set_route("Form", "Customize Form", { doc_type: doctype }); | |||
} | |||
}; | |||
dialog = new frappe.ui.Dialog({ | |||
title: __("New Kanban Board"), | |||
return new frappe.ui.Dialog({ | |||
title: dialog_title, | |||
fields: dialog_fields, | |||
primary_action_label, | |||
primary_action, | |||
}); | |||
return dialog; | |||
} | |||
function get_fields_for_dialog(kanban_options, show_existing = false) { | |||
kanban_options.push("Create New Board"); | |||
const select_fields = frappe.get_meta(doctype).fields.filter((df) => { | |||
return df.fieldtype === "Select" && df.fieldname !== "kanban_column"; | |||
}); | |||
function get_fields_for_dialog(select_fields) { | |||
if (!select_fields.length) { | |||
return [ | |||
{ | |||
fieldtype: "HTML", | |||
options: ` | |||
<div> | |||
<p class="text-medium"> | |||
${__( | |||
'No fields found that can be used as a Kanban Column. Use the Customize Form to add a Custom Field of type "Select".' | |||
)} | |||
</p> | |||
</div> | |||
`, | |||
}, | |||
]; | |||
} | |||
let fields = [ | |||
{ | |||
fieldtype: "Select", | |||
fieldname: "selected_kanban", | |||
label: __("Choose Kanban Board"), | |||
reqd: 1, | |||
depends_on: `eval: ${show_existing}`, | |||
mandatory_depends_on: `eval: ${show_existing}`, | |||
options: kanban_options, | |||
default: kanban_options[0], | |||
}, | |||
{ | |||
fieldname: "new_kanban_board_sb", | |||
fieldtype: "Section Break", | |||
depends_on: `eval: !${show_existing} || doc.selected_kanban == "Create New Board"`, | |||
}, | |||
{ | |||
fieldtype: "Data", | |||
fieldname: "board_name", | |||
label: __("Kanban Board Name"), | |||
mandatory_depends_on: 'eval: doc.selected_kanban == "Create New Board"', | |||
reqd: 1, | |||
description: ["Note", "ToDo"].includes(doctype) | |||
? __("This Kanban Board will be private") | |||
: "", | |||
}, | |||
{ | |||
fieldtype: "Select", | |||
fieldname: "field_name", | |||
label: __("Columns based on"), | |||
options: select_fields.map((df) => ({ label: df.label, value: df.fieldname })), | |||
default: select_fields[0], | |||
reqd: 1, | |||
}, | |||
]; | |||
if (doctype === "Task") { | |||
@@ -346,32 +353,6 @@ frappe.views.KanbanView.show_kanban_dialog = function (doctype, show_existing) { | |||
}); | |||
} | |||
if (select_fields.length > 0) { | |||
fields.push({ | |||
fieldtype: "Select", | |||
fieldname: "field_name", | |||
label: __("Columns based on"), | |||
options: select_fields.map((df) => ({ label: df.label, value: df.fieldname })), | |||
default: select_fields[0], | |||
mandatory_depends_on: 'eval: doc.selected_kanban == "Create New Board"', | |||
}); | |||
} else { | |||
fields = [ | |||
{ | |||
fieldtype: "HTML", | |||
options: ` | |||
<div> | |||
<p class="text-medium"> | |||
${__( | |||
'No fields found that can be used as a Kanban Column. Use the Customize Form to add a Custom Field of type "Select".' | |||
)} | |||
</p> | |||
</div> | |||
`, | |||
}, | |||
]; | |||
} | |||
return fields; | |||
} | |||
}; |
@@ -390,18 +390,17 @@ frappe.views.Workspace = class Workspace { | |||
this.clear_page_actions(); | |||
current_page.is_editable && | |||
this.page.set_secondary_action(__("Edit"), async () => { | |||
if (!this.editor || !this.editor.readOnly) return; | |||
this.is_read_only = false; | |||
await this.editor.readOnly.toggle(); | |||
this.editor.isReady.then(() => { | |||
this.initialize_editorjs_undo(); | |||
this.setup_customization_buttons(current_page); | |||
this.show_sidebar_actions(); | |||
this.make_blocks_sortable(); | |||
}); | |||
this.page.set_secondary_action(__("Edit"), async () => { | |||
if (!this.editor || !this.editor.readOnly) return; | |||
this.is_read_only = false; | |||
await this.editor.readOnly.toggle(); | |||
this.editor.isReady.then(() => { | |||
this.initialize_editorjs_undo(); | |||
this.setup_customization_buttons(current_page); | |||
this.show_sidebar_actions(); | |||
this.make_blocks_sortable(); | |||
}); | |||
}); | |||
this.page.add_inner_button(__("Create Workspace"), () => { | |||
this.initialize_new_page(); | |||
@@ -794,7 +793,11 @@ frappe.views.Workspace = class Workspace { | |||
duplicate_page(page) { | |||
var me = this; | |||
let parent_pages = this.get_parent_pages(page); | |||
let new_page = { ...page }; | |||
if (!this.has_access && new_page.public) { | |||
new_page.public = 0; | |||
} | |||
let parent_pages = this.get_parent_pages({ public: new_page.public }); | |||
const d = new frappe.ui.Dialog({ | |||
title: __("Create Duplicate"), | |||
fields: [ | |||
@@ -809,14 +812,14 @@ frappe.views.Workspace = class Workspace { | |||
fieldtype: "Select", | |||
fieldname: "parent", | |||
options: parent_pages, | |||
default: page.parent_page, | |||
default: new_page.parent_page, | |||
}, | |||
{ | |||
label: __("Public"), | |||
fieldtype: "Check", | |||
fieldname: "is_public", | |||
depends_on: `eval:${this.has_access}`, | |||
default: page.public, | |||
default: new_page.public, | |||
onchange: function () { | |||
d.set_df_property( | |||
"parent", | |||
@@ -832,7 +835,7 @@ frappe.views.Workspace = class Workspace { | |||
label: __("Icon"), | |||
fieldtype: "Icon", | |||
fieldname: "icon", | |||
default: page.icon, | |||
default: new_page.icon, | |||
}, | |||
], | |||
primary_action_label: __("Duplicate"), | |||
@@ -854,8 +857,6 @@ frappe.views.Workspace = class Workspace { | |||
}, | |||
}); | |||
let new_page = { ...page }; | |||
new_page.title = values.title; | |||
new_page.public = values.is_public || 0; | |||
new_page.name = values.title + (new_page.public ? "" : "-" + frappe.session.user); | |||
@@ -48,7 +48,7 @@ | |||
height: 24px; | |||
} | |||
.indicator-pill::before, | |||
.indicator-pill:not(.no-indicator-dot)::before, | |||
.indicator-pill-right::after { | |||
content:''; | |||
display: inline-table; | |||
@@ -179,4 +179,4 @@ | |||
@keyframes blink { | |||
50% { opacity: 0.5; } | |||
} | |||
} |
@@ -90,6 +90,11 @@ def get_sessions_to_clear(user=None, keep_current=False, device=None): | |||
def delete_session(sid=None, user=None, reason="Session Expired"): | |||
from frappe.core.doctype.activity_log.feed import logout_feed | |||
if frappe.flags.read_only: | |||
# This isn't manually initated logout, most likely user's cookies were expired in such case | |||
# we should just ignore it till database is back up again. | |||
return | |||
frappe.cache().hdel("session", sid) | |||
frappe.cache().hdel("last_db_session_update", sid) | |||
if sid and not user: | |||
@@ -179,6 +184,7 @@ def get(): | |||
bootinfo.notes = get_unseen_notes() | |||
bootinfo.assets_json = get_assets_json() | |||
bootinfo.read_only = bool(frappe.flags.read_only) | |||
for hook in frappe.get_hooks("extend_bootinfo"): | |||
frappe.get_attr(hook)(bootinfo=bootinfo) | |||
@@ -407,7 +413,7 @@ class Session: | |||
# database persistence is secondary, don't update it too often | |||
updated_in_db = False | |||
if force or (time_diff is None) or (time_diff > 600): | |||
if (force or (time_diff is None) or (time_diff > 600)) and not frappe.flags.read_only: | |||
# update sessions table | |||
frappe.db.sql( | |||
"""update `tabSessions` set sessiondata=%s, | |||
@@ -426,7 +432,6 @@ class Session: | |||
updated_in_db = True | |||
# set in memcache | |||
frappe.cache().hset("session", self.sid, self.data) | |||
return updated_in_db | |||
@@ -36,8 +36,7 @@ | |||
"in_standard_filter": 1, | |||
"label": "Type", | |||
"options": "Auto\nAppreciation\nCriticism\nReview\nRevert", | |||
"read_only": 1, | |||
"search_index": 1 | |||
"read_only": 1 | |||
}, | |||
{ | |||
"fieldname": "rule", | |||
@@ -54,8 +53,7 @@ | |||
"in_standard_filter": 1, | |||
"label": "Reference Document Type", | |||
"options": "DocType", | |||
"read_only": 1, | |||
"search_index": 1 | |||
"read_only": 1 | |||
}, | |||
{ | |||
"fieldname": "reference_name", | |||
@@ -112,7 +110,7 @@ | |||
], | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2021-10-25 17:25:40.477044", | |||
"modified": "2022-09-13 16:10:37.747013", | |||
"modified_by": "Administrator", | |||
"module": "Social", | |||
"name": "Energy Point Log", | |||
@@ -131,5 +129,6 @@ | |||
], | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"title_field": "user" | |||
} | |||
} |
@@ -89,6 +89,15 @@ | |||
</div> | |||
{% endif %} | |||
{% if read_only_mode %} | |||
<div | |||
class="indicator-pill yellow no-indicator-dot align-self-center" | |||
title="{{ _("This site is in read only mode, full functionality will be restored soon.") }}" | |||
> | |||
{{ _("Read Only Mode") }} | |||
</div> | |||
{% endif %} | |||
{% include "templates/includes/navbar/navbar_login.html" %} | |||
</ul> | |||
@@ -9,6 +9,7 @@ from semantic_version import Version | |||
from werkzeug.test import TestResponse | |||
import frappe | |||
from frappe.installer import update_site_config | |||
from frappe.tests.utils import FrappeTestCase | |||
from frappe.utils import get_site_url, get_test_client | |||
@@ -269,3 +270,29 @@ class TestMethodAPI(FrappeAPITestCase): | |||
self.assertEqual(response.json["message"], "Administrator") | |||
authorization_token = None | |||
class TestReadOnlyMode(FrappeAPITestCase): | |||
"""During migration if read only mode can be enabled. | |||
Test if reads work well and writes are blocked""" | |||
REQ_PATH = "/api/resource/ToDo" | |||
@classmethod | |||
def setUpClass(cls): | |||
super().setUpClass() | |||
update_site_config("allow_reads_during_maintenance", 1) | |||
cls.addClassCleanup(update_site_config, "maintenance_mode", 0) | |||
# XXX: this has potential to crumble rest of the test suite. | |||
update_site_config("maintenance_mode", 1) | |||
def test_reads(self): | |||
response = self.get(self.REQ_PATH, {"sid": self.sid}) | |||
self.assertEqual(response.status_code, 200) | |||
self.assertIsInstance(response.json, dict) | |||
self.assertIsInstance(response.json["data"], list) | |||
def test_blocked_writes(self): | |||
response = self.post(self.REQ_PATH, {"description": frappe.mock("paragraph"), "sid": self.sid}) | |||
self.assertEqual(response.status_code, 503) | |||
self.assertEqual(response.json["exc_type"], "InReadOnlyMode") |
@@ -460,6 +460,14 @@ class TestDB(FrappeTestCase): | |||
# recover transaction to continue other tests | |||
raise Exception | |||
def test_read_only_errors(self): | |||
frappe.db.rollback() | |||
frappe.db.begin(read_only=True) | |||
self.addCleanup(frappe.db.rollback) | |||
with self.assertRaises(frappe.InReadOnlyMode): | |||
frappe.db.set_value("User", "Administrator", "full_name", "Haxor") | |||
def test_exists(self): | |||
dt, dn = "User", "Administrator" | |||
self.assertEqual(frappe.db.exists(dt, dn, cache=True), dn) | |||
@@ -40,46 +40,46 @@ class TestDBUpdate(FrappeTestCase): | |||
frappe.reload_doctype("User", force=True) | |||
frappe.model.meta.trim_tables("User") | |||
make_property_setter(doctype, "restrict_ip", "unique", "1", "Int") | |||
make_property_setter(doctype, "middle_name", "unique", "1", "Check") | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertTrue(restrict_ip_in_table.unique) | |||
middle_name_in_table = get_table_column("User", "middle_name") | |||
self.assertTrue(middle_name_in_table.unique) | |||
make_property_setter(doctype, "restrict_ip", "unique", "0", "Int") | |||
make_property_setter(doctype, "middle_name", "unique", "0", "Check") | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertFalse(restrict_ip_in_table.unique) | |||
middle_name_in_table = get_table_column("User", "middle_name") | |||
self.assertFalse(middle_name_in_table.unique) | |||
make_property_setter(doctype, "restrict_ip", "search_index", "1", "Int") | |||
make_property_setter(doctype, "middle_name", "search_index", "1", "Check") | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertTrue(restrict_ip_in_table.index) | |||
middle_name_in_table = get_table_column("User", "middle_name") | |||
self.assertTrue(middle_name_in_table.index) | |||
make_property_setter(doctype, "restrict_ip", "search_index", "0", "Int") | |||
make_property_setter(doctype, "middle_name", "search_index", "0", "Check") | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertFalse(restrict_ip_in_table.index) | |||
middle_name_in_table = get_table_column("User", "middle_name") | |||
self.assertFalse(middle_name_in_table.index) | |||
make_property_setter(doctype, "restrict_ip", "search_index", "1", "Int") | |||
make_property_setter(doctype, "restrict_ip", "unique", "1", "Int") | |||
make_property_setter(doctype, "middle_name", "search_index", "1", "Check") | |||
make_property_setter(doctype, "middle_name", "unique", "1", "Check") | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertTrue(restrict_ip_in_table.index) | |||
self.assertTrue(restrict_ip_in_table.unique) | |||
middle_name_in_table = get_table_column("User", "middle_name") | |||
self.assertTrue(middle_name_in_table.index) | |||
self.assertTrue(middle_name_in_table.unique) | |||
make_property_setter(doctype, "restrict_ip", "search_index", "1", "Int") | |||
make_property_setter(doctype, "restrict_ip", "unique", "0", "Int") | |||
make_property_setter(doctype, "middle_name", "search_index", "1", "Check") | |||
make_property_setter(doctype, "middle_name", "unique", "0", "Check") | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertTrue(restrict_ip_in_table.index) | |||
self.assertFalse(restrict_ip_in_table.unique) | |||
middle_name_in_table = get_table_column("User", "middle_name") | |||
self.assertTrue(middle_name_in_table.index) | |||
self.assertFalse(middle_name_in_table.unique) | |||
make_property_setter(doctype, "restrict_ip", "search_index", "0", "Int") | |||
make_property_setter(doctype, "restrict_ip", "unique", "1", "Int") | |||
make_property_setter(doctype, "middle_name", "search_index", "0", "Check") | |||
make_property_setter(doctype, "middle_name", "unique", "1", "Check") | |||
frappe.db.updatedb(doctype) | |||
restrict_ip_in_table = get_table_column("User", "restrict_ip") | |||
self.assertFalse(restrict_ip_in_table.index) | |||
self.assertTrue(restrict_ip_in_table.unique) | |||
middle_name_in_table = get_table_column("User", "middle_name") | |||
self.assertFalse(middle_name_in_table.index) | |||
self.assertTrue(middle_name_in_table.unique) | |||
# explicitly make a text index | |||
frappe.db.add_index(doctype, ["email_signature(200)"]) | |||
@@ -64,9 +64,10 @@ def generate_and_cache_results(args, function, cache_key, chart): | |||
else: | |||
raise | |||
frappe.db.set_value( | |||
"Dashboard Chart", args.chart_name, "last_synced_on", frappe.utils.now(), update_modified=False | |||
) | |||
if not frappe.flags.read_only: | |||
frappe.db.set_value( | |||
"Dashboard Chart", args.chart_name, "last_synced_on", frappe.utils.now(), update_modified=False | |||
) | |||
return results | |||
@@ -84,13 +85,8 @@ def get_dashboards_with_link(docname, doctype): | |||
def sync_dashboards(app=None): | |||
"""Import, overwrite fixtures from `[app]/fixtures`""" | |||
if not cint(frappe.db.get_single_value("System Settings", "setup_complete")): | |||
return | |||
if app: | |||
apps = [app] | |||
else: | |||
apps = frappe.get_installed_apps() | |||
"""Import, overwrite dashboards from `[app]/[app]_dashboard`""" | |||
apps = [app] if app else frappe.get_installed_apps() | |||
for app_name in apps: | |||
print(f"Updating Dashboard for {app_name}") | |||
@@ -25,8 +25,6 @@ def sync_fixtures(app=None): | |||
frappe.flags.in_fixtures = False | |||
frappe.db.commit() | |||
def import_custom_scripts(app): | |||
"""Import custom scripts from `[app]/fixtures/custom_scripts`""" | |||
@@ -146,6 +146,7 @@ def get_safe_globals(): | |||
), | |||
make_get_request=frappe.integrations.utils.make_get_request, | |||
make_post_request=frappe.integrations.utils.make_post_request, | |||
make_put_request=frappe.integrations.utils.make_put_request, | |||
socketio_port=frappe.conf.socketio_port, | |||
get_hooks=get_hooks, | |||
enqueue=safe_enqueue, | |||
@@ -25,14 +25,12 @@ | |||
"fieldname": "referrer", | |||
"fieldtype": "Data", | |||
"label": "Referrer", | |||
"search_index": 1, | |||
"set_only_once": 1 | |||
}, | |||
{ | |||
"fieldname": "browser", | |||
"fieldtype": "Data", | |||
"label": "Browser", | |||
"search_index": 1, | |||
"set_only_once": 1 | |||
}, | |||
{ | |||
@@ -59,7 +57,7 @@ | |||
], | |||
"in_create": 1, | |||
"links": [], | |||
"modified": "2021-10-25 14:11:24.718770", | |||
"modified": "2022-09-13 15:38:25.401797", | |||
"modified_by": "Administrator", | |||
"module": "Website", | |||
"name": "Web Page View", | |||
@@ -82,5 +80,6 @@ | |||
"read_only": 1, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"title_field": "path" | |||
} | |||
} |
@@ -37,7 +37,10 @@ def make_view_log(path, referrer=None, browser=None, version=None, url=None, use | |||
view.is_unique = is_unique | |||
try: | |||
view.insert(ignore_permissions=True) | |||
if frappe.flags.read_only: | |||
view.deferred_insert() | |||
else: | |||
view.insert(ignore_permissions=True) | |||
except Exception: | |||
if frappe.message_log: | |||
frappe.message_log.pop() | |||
@@ -191,6 +191,7 @@ def get_website_settings(context=None): | |||
if settings.splash_image: | |||
context["splash_image"] = settings.splash_image | |||
context.read_only_mode = frappe.flags.read_only | |||
context.boot = get_boot_data() | |||
return context | |||
@@ -5,7 +5,8 @@ import io | |||
import os | |||
import re | |||
from werkzeug.routing import Map, NotFound, Rule | |||
from werkzeug.exceptions import NotFound | |||
from werkzeug.routing import Map, Rule | |||
import frappe | |||
from frappe.website.utils import extract_title, get_frontmatter | |||
@@ -108,7 +108,7 @@ | |||
{%- endif -%} | |||
</form> | |||
</div> | |||
{%- if not disable_signup -%} | |||
{%- if not disable_signup and not disable_user_pass_login -%} | |||
<div class="text-center sign-up-message"> | |||
{{ _("Don't have an account?") }} | |||
<a href="#signup">{{ _("Sign up") }}</a> | |||
@@ -124,7 +124,7 @@ | |||
{{ email_login_body() }} | |||
</form> | |||
</div> | |||
{%- if not disable_signup -%} | |||
{%- if not disable_signup and not disable_user_pass_login -%} | |||
<div class="text-center sign-up-message"> | |||
{{ _("Don't have an account?") }} | |||
<a href="#signup">{{ _("Sign up") }}</a> | |||
@@ -13,3 +13,4 @@ def get_context(context): | |||
frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError) | |||
context.current_user = frappe.get_doc("User", frappe.session.user) | |||
context.show_sidebar = True |
@@ -61,9 +61,7 @@ | |||
"vue": "2.6.14", | |||
"vue-router": "^2.0.0", | |||
"vuedraggable": "^2.24.3", | |||
"vuex": "3" | |||
}, | |||
"devDependencies": { | |||
"vuex": "3", | |||
"@frappe/esbuild-plugin-postcss2": "^0.1.3", | |||
"@vue/component-compiler": "^4.2.4", | |||
"autoprefixer": "10", | |||
@@ -22,7 +22,7 @@ dependencies = [ | |||
"PyYAML~=5.4.1", | |||
"RestrictedPython~=5.1", | |||
"WeasyPrint==52.5", | |||
"Werkzeug~=2.1.2", | |||
"Werkzeug~=2.2.2", | |||
"Whoosh~=2.7.4", | |||
"beautifulsoup4~=4.9.3", | |||
"bleach-allowlist~=1.0.3", | |||
@@ -53,7 +53,7 @@ dependencies = [ | |||
"pyasn1~=0.4.8", | |||
"pycryptodome~=3.10.1", | |||
"pyotp~=2.6.0", | |||
"pypng~=0.0.20", | |||
"pypng~=0.20220715.0", | |||
"python-dateutil~=2.8.1", | |||
"pytz==2022.1", | |||
"rauth~=0.7.3", | |||