Skip to content

Commit

Permalink
Merge pull request #25963 from frappe/version-15-hotfix
Browse files Browse the repository at this point in the history
chore: release v15
  • Loading branch information
akhilnarang committed Apr 16, 2024
2 parents e51a5ef + 0e6820d commit 07a0e32
Show file tree
Hide file tree
Showing 44 changed files with 522 additions and 312 deletions.
10 changes: 5 additions & 5 deletions cypress/integration/control_dynamic_link.js
Expand Up @@ -85,7 +85,7 @@ context("Dynamic Link", () => {
//Checking if the listbox have length greater than 0
cy.get('[data-fieldname="doc_id"]')
.find(".awesomplete")
.find("li")
.find("div")
.its("length")
.should("be.gte", 0);
cy.get(".btn-modal-close").click({ force: true });
Expand All @@ -100,7 +100,7 @@ context("Dynamic Link", () => {
//Checking if the listbox have length greater than 0
cy.get('[data-fieldname="doc_id"]')
.find(".awesomplete")
.find("li")
.find("div")
.its("length")
.should("be.gte", 0);
cy.get(".btn-modal-close").click({ force: true, multiple: true });
Expand All @@ -119,7 +119,7 @@ context("Dynamic Link", () => {
//Checking if the listbox have length greater than 0
cy.get('[data-fieldname="doc_id"]')
.find(".awesomplete")
.find("li")
.find("div")
.its("length")
.should("be.gte", 0);

Expand All @@ -134,7 +134,7 @@ context("Dynamic Link", () => {
//Checking if the listbox have length greater than 0
cy.get('[data-fieldname="doc_id"]')
.find(".awesomplete")
.find("li")
.find("div")
.its("length")
.should("be.gte", 0);
cy.get_field("doc_type").clear();
Expand All @@ -143,7 +143,7 @@ context("Dynamic Link", () => {
cy.intercept("/api/method/frappe.desk.search.search_link").as("search_query");
cy.fill_field("doc_type", "System Settings", "Link", { delay: 500 });
cy.wait("@search_query");
cy.get(`[data-fieldname="doc_type"] ul:visible li:first-child`).click({
cy.get(`[data-fieldname="doc_type"] ul:visible div:first-child`).click({
scrollBehavior: false,
});

Expand Down
2 changes: 2 additions & 0 deletions frappe/auth.py
Expand Up @@ -342,6 +342,8 @@ def logout(self, arg="", user=None):
if user == frappe.session.user:
delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out")
self.clear_cookies()
if frappe.request:
self.login_as_guest()
else:
clear_sessions(user)

Expand Down
7 changes: 7 additions & 0 deletions frappe/commands/site.py
Expand Up @@ -10,6 +10,7 @@
import frappe
from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import CallbackManager


@click.command("new-site")
Expand Down Expand Up @@ -868,11 +869,13 @@ def backup(

verbose = verbose or context.verbose
exit_code = 0
rollback_callback = None

for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
rollback_callback = CallbackManager()
odb = scheduled_backup(
ignore_files=not with_files,
backup_path=backup_path,
Expand All @@ -887,12 +890,16 @@ def backup(
verbose=verbose,
force=True,
old_backup_metadata=old_backup_metadata,
rollback_callback=rollback_callback,
)
except Exception:
click.secho(
f"Backup failed for Site {site}. Database or site_config.json may be corrupted",
fg="red",
)
if rollback_callback:
rollback_callback.run()
rollback_callback = None
if verbose:
print(frappe.get_traceback(with_context=True))
exit_code = 1
Expand Down
7 changes: 5 additions & 2 deletions frappe/core/doctype/activity_log/test_activity_log.py
Expand Up @@ -8,13 +8,16 @@


class TestActivityLog(FrappeTestCase):
def setUp(self) -> None:
frappe.set_user("Administrator")

def test_activity_log(self):
# test user login log
frappe.local.form_dict = frappe._dict(
{
"cmd": "login",
"sid": "Guest",
"pwd": frappe.conf.admin_password or "admin",
"pwd": self.ADMIN_PASSWORD or "admin",
"usr": "Administrator",
}
)
Expand Down Expand Up @@ -57,7 +60,7 @@ def test_brute_security(self):
update_system_settings({"allow_consecutive_login_attempts": 3, "allow_login_after_fail": 5})

frappe.local.form_dict = frappe._dict(
{"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"}
{"cmd": "login", "sid": "Guest", "pwd": self.ADMIN_PASSWORD, "usr": "Administrator"}
)

frappe.local.request_ip = "127.0.0.1"
Expand Down
1 change: 1 addition & 0 deletions frappe/core/doctype/doctype/doctype.js
Expand Up @@ -6,6 +6,7 @@ frappe.ui.form.on("DocType", {
if (frm.is_new() && !frm.doc?.fields) {
frappe.listview_settings["DocType"].new_doctype_dialog();
}
frm.call("check_pending_migration");
},

before_save: function (frm) {
Expand Down
21 changes: 20 additions & 1 deletion frappe/core/doctype/doctype/doctype.py
Expand Up @@ -6,6 +6,7 @@
import os
import re
import shutil
from pathlib import Path
from typing import TYPE_CHECKING, Union

import frappe
Expand All @@ -32,7 +33,7 @@
from frappe.modules.import_file import get_file_path
from frappe.permissions import ALL_USER_ROLE, AUTOMATIC_ROLES, SYSTEM_USER_ROLE
from frappe.query_builder.functions import Concat
from frappe.utils import cint, flt, is_a_property, random_string
from frappe.utils import cint, flt, get_datetime, is_a_property, random_string
from frappe.website.utils import clear_cache

if TYPE_CHECKING:
Expand Down Expand Up @@ -1015,6 +1016,24 @@ def validate_name(self, name=None):

validate_route_conflict(self.doctype, self.name)

@frappe.whitelist()
def check_pending_migration(self) -> bool:
"""Checks if all migrations are applied on doctype."""
if self.is_new() or self.custom:
return

file = Path(get_file_path(frappe.scrub(self.module), self.doctype, self.name))
content = json.loads(file.read_text())
if content.get("modified") and get_datetime(self.modified) != get_datetime(content.get("modified")):
frappe.msgprint(
_(
"This doctype has pending migrations, run 'bench migrate' before modifying the doctype to avoid losing changes."
),
alert=True,
indicator="yellow",
)
return True


def validate_series(dt, autoname=None, name=None):
"""Validate if `autoname` property is correctly set."""
Expand Down
7 changes: 7 additions & 0 deletions frappe/core/doctype/prepared_report/prepared_report.py
Expand Up @@ -8,6 +8,7 @@
from rq import get_current_job

import frappe
from frappe.database.utils import dangerously_reconnect_on_connection_abort
from frappe.desk.form.load import get_attachments
from frappe.desk.query_report import generate_report_result
from frappe.model.document import Document
Expand Down Expand Up @@ -115,6 +116,7 @@ def generate_report(prepared_report):
except Exception:
instance.status = "Error"
instance.error_message = frappe.get_traceback(with_context=True)
_save_instance(instance) # we need to ensure that error gets stored

instance.report_end_time = frappe.utils.now()
instance.save(ignore_permissions=True)
Expand All @@ -126,6 +128,11 @@ def generate_report(prepared_report):
)


@dangerously_reconnect_on_connection_abort
def _save_instance(instance):
instance.save(ignore_permissions=True)


def update_job_id(prepared_report):
job = get_current_job()

Expand Down
4 changes: 2 additions & 2 deletions frappe/core/doctype/user/test_user.py
Expand Up @@ -461,10 +461,10 @@ class TestImpersonation(FrappeAPITestCase):
def test_impersonation(self):
with test_user(roles=["System Manager"], commit=True) as user:
self.post(
self.method_path("frappe.core.doctype.user.user.impersonate"),
self.method("frappe.core.doctype.user.user.impersonate"),
{"user": user.name, "reason": "test", "sid": self.sid},
)
resp = self.get(self.method_path("frappe.auth.get_logged_user"))
resp = self.get(self.method("frappe.auth.get_logged_user"))
self.assertEqual(resp.json["message"], user.name)


Expand Down
47 changes: 46 additions & 1 deletion frappe/custom/doctype/customize_form/customize_form.js
Expand Up @@ -140,6 +140,14 @@ frappe.ui.form.on("Customize Form", {
__("Actions")
);

frm.add_custom_button(
__("Trim Table"),
function () {
frm.trigger("trim_table");
},
__("Actions")
);

const is_autoname_autoincrement = frm.doc.autoname === "autoincrement";
frm.set_df_property("naming_rule", "hidden", is_autoname_autoincrement);
frm.set_df_property("autoname", "read_only", is_autoname_autoincrement);
Expand Down Expand Up @@ -194,6 +202,40 @@ frappe.ui.form.on("Customize Form", {
);
},

async trim_table(frm) {
let dropped_columns = await frappe.xcall(
"frappe.custom.doctype.customize_form.customize_form.get_orphaned_columns",
{ doctype: frm.doc.doc_type }
);

if (!dropped_columns?.length) {
frappe.toast(__("This doctype has no orphan fields to trim"));
return;
}
let msg = __(
"Warning: DATA LOSS IMMINENT! Proceeding will permanently delete following database columns from doctype {0}:",
[frm.doc.doc_type.bold()]
);
msg += "<ol>" + dropped_columns.map((col) => `<li>${col}</li>`).join("") + "</ol>";
msg += __("This action is irreversible. Do you wish to continue?");

frappe.confirm(msg, () => {
return frm.call({
doc: frm.doc,
method: "trim_table",
callback: function (r) {
if (!r.exc) {
frappe.show_alert({
message: __("Table Trimmed"),
indicator: "green",
});
frappe.customize_form.clear_locals_and_refresh(frm);
}
},
});
});
},

setup_export(frm) {
if (frappe.boot.developer_mode) {
frm.add_custom_button(
Expand All @@ -218,7 +260,10 @@ frappe.ui.form.on("Customize Form", {
fieldtype: "Check",
fieldname: "with_permissions",
label: __("Export Custom Permissions"),
default: 1,
description: __(
"Exported permissions will be force-synced on every migrate overriding any other customization."
),
default: 0,
},
],
function (data) {
Expand Down
21 changes: 21 additions & 0 deletions frappe/custom/doctype/customize_form/customize_form.py
Expand Up @@ -21,6 +21,7 @@
from frappe.model import core_doctypes_list, no_value_fields
from frappe.model.docfield import supports_translation
from frappe.model.document import Document
from frappe.model.meta import trim_table
from frappe.utils import cint


Expand Down Expand Up @@ -638,6 +639,19 @@ def reset_layout(self):
frappe.clear_cache(doctype=self.doc_type)
self.fetch_to_customize()

@frappe.whitelist()
def trim_table(self):
"""Removes database fields that don't exist in the doctype.
This may be needed as maintenance since removing a field in a DocType
doesn't automatically delete the db field.
"""
if not self.doc_type:
return

trim_table(self.doc_type, dry_run=False)
self.fetch_to_customize()

@classmethod
def allow_fieldtype_change(self, old_type: str, new_type: str) -> bool:
"""allow type change, if both old_type and new_type are in same field group.
Expand All @@ -650,6 +664,13 @@ def in_field_group(group):
return any(map(in_field_group, ALLOWED_FIELDTYPE_CHANGE))


@frappe.whitelist()
def get_orphaned_columns(doctype: str):
frappe.only_for("System Manager")
frappe.db.begin(read_only=True) # Avoid any potential bug from writing to db
return trim_table(doctype, dry_run=True)


def reset_customization(doctype):
setters = frappe.get_all(
"Property Setter",
Expand Down
2 changes: 1 addition & 1 deletion frappe/database/database.py
Expand Up @@ -1234,7 +1234,7 @@ def escape(s, percent=True):

@staticmethod
def is_column_missing(e):
return frappe.db.is_missing_column(e)
raise NotImplementedError

def get_descendants(self, doctype, name):
"""Return descendants of the group node in tree"""
Expand Down
4 changes: 4 additions & 0 deletions frappe/database/mariadb/database.py
Expand Up @@ -97,6 +97,10 @@ def is_unique_key_violation(e: pymysql.Error) -> bool:
and isinstance(e, pymysql.IntegrityError)
)

@staticmethod
def is_interface_error(e: pymysql.Error):
return isinstance(e, pymysql.InterfaceError)


class MariaDBConnectionUtil:
def get_connection(self):
Expand Down
5 changes: 5 additions & 0 deletions frappe/database/postgres/database.py
Expand Up @@ -13,6 +13,7 @@
UNIQUE_VIOLATION,
)
from psycopg2.errors import (
InterfaceError,
LockNotAvailable,
ReadOnlySqlTransaction,
SequenceGeneratorLimitExceeded,
Expand Down Expand Up @@ -116,6 +117,10 @@ def is_data_too_long(e):
def is_db_table_size_limit(e) -> bool:
return False

@staticmethod
def is_interface_error(e):
return isinstance(e, InterfaceError)


class PostgresDatabase(PostgresExceptionUtil, Database):
REGEX_CHARACTER = "~"
Expand Down

0 comments on commit 07a0e32

Please sign in to comment.