Browse Source

Merge pull request #18192 from frappe/version-14-hotfix

chore: release v14
version-14
Deepesh Garg 2 years ago
committed by GitHub
parent
commit
718bac3509
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 454 additions and 295 deletions
  1. +1
    -1
      esbuild/esbuild.js
  2. +9
    -5
      frappe/__init__.py
  3. +29
    -2
      frappe/app.py
  4. +1
    -17
      frappe/auth.py
  5. +3
    -0
      frappe/commands/site.py
  6. +4
    -4
      frappe/core/doctype/activity_log/activity_log.json
  7. +0
    -1
      frappe/core/doctype/activity_log/activity_log.py
  8. +0
    -1
      frappe/core/doctype/comment/comment.py
  9. +2
    -1
      frappe/core/doctype/custom_docperm/custom_docperm.json
  10. +1
    -1
      frappe/core/doctype/error_log/error_log.py
  11. +5
    -7
      frappe/core/doctype/file/file.json
  12. +3
    -2
      frappe/core/doctype/report/report.json
  13. +8
    -1
      frappe/core/doctype/rq_job/rq_job.py
  14. +5
    -2
      frappe/core/doctype/rq_job/test_rq_job.py
  15. +3
    -3
      frappe/core/doctype/user/user.json
  16. +3
    -7
      frappe/database/__init__.py
  17. +14
    -7
      frappe/database/database.py
  18. +4
    -0
      frappe/database/mariadb/database.py
  19. +5
    -1
      frappe/database/postgres/database.py
  20. +5
    -4
      frappe/desk/desktop.py
  21. +2
    -2
      frappe/desk/doctype/event/event.json
  22. +14
    -37
      frappe/desk/doctype/notification_log/notification_log.json
  23. +1
    -1
      frappe/desk/doctype/notification_log/notification_log.py
  24. +1
    -0
      frappe/desk/doctype/workspace/workspace.py
  25. +1
    -1
      frappe/email/doctype/notification/notification.js
  26. +4
    -0
      frappe/exceptions.py
  27. +2
    -0
      frappe/installer.py
  28. +3
    -0
      frappe/migrate.py
  29. +22
    -4
      frappe/model/document.py
  30. +6
    -1
      frappe/model/rename_doc.py
  31. +3
    -3
      frappe/model/sync.py
  32. +5
    -9
      frappe/modules/patch_handler.py
  33. +1
    -0
      frappe/patches.txt
  34. +56
    -0
      frappe/patches/v14_0/drop_unused_indexes.py
  35. +1
    -1
      frappe/public/js/frappe/form/dashboard.js
  36. +4
    -0
      frappe/public/js/frappe/form/form.js
  37. +7
    -1
      frappe/public/js/frappe/form/toolbar.js
  38. +1
    -1
      frappe/public/js/frappe/list/list_view.js
  39. +24
    -9
      frappe/public/js/frappe/list/list_view_select.js
  40. +5
    -0
      frappe/public/js/frappe/ui/toolbar/navbar.html
  41. +60
    -79
      frappe/public/js/frappe/views/kanban/kanban_view.js
  42. +18
    -17
      frappe/public/js/frappe/views/workspace/workspace.js
  43. +2
    -2
      frappe/public/scss/common/indicator.scss
  44. +7
    -2
      frappe/sessions.py
  45. +5
    -6
      frappe/social/doctype/energy_point_log/energy_point_log.json
  46. +9
    -0
      frappe/templates/includes/navbar/navbar_items.html
  47. +27
    -0
      frappe/tests/test_api.py
  48. +8
    -0
      frappe/tests/test_db.py
  49. +27
    -27
      frappe/tests/test_db_update.py
  50. +6
    -10
      frappe/utils/dashboard.py
  51. +0
    -2
      frappe/utils/fixtures.py
  52. +1
    -0
      frappe/utils/safe_exec.py
  53. +3
    -4
      frappe/website/doctype/web_page_view/web_page_view.json
  54. +4
    -1
      frappe/website/doctype/web_page_view/web_page_view.py
  55. +1
    -0
      frappe/website/doctype/website_settings/website_settings.py
  56. +2
    -1
      frappe/website/router.py
  57. +2
    -2
      frappe/www/login.html
  58. +1
    -0
      frappe/www/me.py
  59. +1
    -3
      package.json
  60. +2
    -2
      pyproject.toml

+ 1
- 1
esbuild/esbuild.js View File

@@ -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;


+ 9
- 5
frappe/__init__.py View File

@@ -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):


+ 29
- 2
frappe/app.py View File

@@ -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


+ 1
- 17
frappe/auth.py View File

@@ -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:



+ 3
- 0
frappe/commands/site.py View File

@@ -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)


+ 4
- 4
frappe/core/doctype/activity_log/activity_log.json View File

@@ -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
}
}

+ 0
- 1
frappe/core/doctype/activity_log/activity_log.py View File

@@ -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"):


+ 0
- 1
frappe/core/doctype/comment/comment.py View File

@@ -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):


+ 2
- 1
frappe/core/doctype/custom_docperm/custom_docperm.json View File

@@ -212,7 +212,8 @@
"fieldname": "parent",
"fieldtype": "Data",
"label": "Reference Document Type",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"default": "0",


+ 1
- 1
frappe/core/doctype/error_log/error_log.py View File

@@ -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()



+ 5
- 7
frappe/core/doctype/file/file.json View File

@@ -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
}

+ 3
- 2
frappe/core/doctype/report/report.json View File

@@ -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
}

+ 8
- 1
frappe/core/doctype/rq_job/rq_job.py View File

@@ -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 "",


+ 5
- 2
frappe/core/doctype/rq_job/test_rq_job.py View File

@@ -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


+ 3
- 3
frappe/core/doctype/user/user.json View File

@@ -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",


+ 3
- 7
frappe/database/__init__.py View File

@@ -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):


+ 14
- 7
frappe/database/database.py View File

@@ -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()


+ 4
- 0
frappe/database/mariadb/database.py View File

@@ -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


+ 5
- 1
frappe/database/postgres/database.py View File

@@ -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)


+ 5
- 4
frappe/desk/desktop.py View File

@@ -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



+ 2
- 2
frappe/desk/doctype/event/event.json View File

@@ -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
}
}

+ 14
- 37
frappe/desk/doctype/notification_log/notification_log.json View File

@@ -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
}
}

+ 1
- 1
frappe/desk/doctype/notification_log/notification_log.py View File

@@ -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]


+ 1
- 0
frappe/desk/doctype/workspace/workspace.py View File

@@ -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}"


+ 1
- 1
frappe/email/doctype/notification/notification.js View File

@@ -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"));


+ 4
- 0
frappe/exceptions.py View File

@@ -236,6 +236,10 @@ class QueryDeadlockError(Exception):
pass


class InReadOnlyMode(ValidationError):
http_status_code = 503 # temporarily not available


class TooManyWritesError(Exception):
pass



+ 2
- 0
frappe/installer.py View File

@@ -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)() #


+ 3
- 0
frappe/migrate.py View File

@@ -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()


+ 22
- 4
frappe/model/document.py View File

@@ -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__


+ 6
- 1
frappe/model/rename_doc.py View File

@@ -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)
)



+ 3
- 3
frappe/model/sync.py View File

@@ -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()



+ 5
- 9
frappe/modules/patch_handler.py View File

@@ -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")

+ 1
- 0
frappe/patches.txt View File

@@ -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

+ 56
- 0
frappe/patches/v14_0/drop_unused_indexes.py View File

@@ -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}")

+ 1
- 1
frappe/public/js/frappe/form/dashboard.js View File

@@ -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,


+ 4
- 0
frappe/public/js/frappe/form/form.js View File

@@ -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();
}
}
}



+ 7
- 1
frappe/public/js/frappe/form/toolbar.js View File

@@ -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,


+ 1
- 1
frappe/public/js/frappe/list/list_view.js View File

@@ -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")


+ 24
- 9
frappe/public/js/frappe/list/list_view_select.js View File

@@ -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() {


+ 5
- 0
frappe/public/js/frappe/ui/toolbar/navbar.html View File

@@ -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"


+ 60
- 79
frappe/public/js/frappe/views/kanban/kanban_view.js View File

@@ -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;
}
};

+ 18
- 17
frappe/public/js/frappe/views/workspace/workspace.js View File

@@ -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);


+ 2
- 2
frappe/public/scss/common/indicator.scss View File

@@ -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; }
}
}

+ 7
- 2
frappe/sessions.py View File

@@ -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


+ 5
- 6
frappe/social/doctype/energy_point_log/energy_point_log.json View File

@@ -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"
}
}

+ 9
- 0
frappe/templates/includes/navbar/navbar_items.html View File

@@ -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>


+ 27
- 0
frappe/tests/test_api.py View File

@@ -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")

+ 8
- 0
frappe/tests/test_db.py View File

@@ -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)


+ 27
- 27
frappe/tests/test_db_update.py View File

@@ -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)"])


+ 6
- 10
frappe/utils/dashboard.py View File

@@ -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}")


+ 0
- 2
frappe/utils/fixtures.py View File

@@ -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`"""


+ 1
- 0
frappe/utils/safe_exec.py View File

@@ -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,


+ 3
- 4
frappe/website/doctype/web_page_view/web_page_view.json View File

@@ -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"
}
}

+ 4
- 1
frappe/website/doctype/web_page_view/web_page_view.py View File

@@ -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()


+ 1
- 0
frappe/website/doctype/website_settings/website_settings.py View File

@@ -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


+ 2
- 1
frappe/website/router.py View File

@@ -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


+ 2
- 2
frappe/www/login.html View File

@@ -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>


+ 1
- 0
frappe/www/me.py View File

@@ -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

+ 1
- 3
package.json View File

@@ -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",


+ 2
- 2
pyproject.toml View File

@@ -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",


Loading…
Cancel
Save