Merge branch 'develop' into numbercard_fix

This commit is contained in:
Maharshi Patel 2023-12-27 13:39:00 +05:30
commit 53ab3c6491
493 changed files with 5374 additions and 3635 deletions

View file

@ -40,3 +40,6 @@ f223bc02490902dfcc32892058f13f343d51fbaf
# frappe.cache() -> frappe.cache
fa6dc03cc87ad74e11609e7373078366fdcb3e1b
# Bulk refactor with sourcery
c35476256f85271fb57584eb0a26f4d9def3caf4

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

View file

@ -7,6 +7,6 @@ jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- uses: actions/labeler@v5
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View file

@ -38,7 +38,7 @@ jobs:
steps:
- name: 'Setup Environment'
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: actions/checkout@v4
@ -57,7 +57,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: pip
@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'

View file

@ -24,7 +24,7 @@ jobs:
with:
node-version: 18
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Set up bench and build assets

View file

@ -62,7 +62,7 @@ jobs:
fi
- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.10"

View file

@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Set up bench and build assets

View file

@ -83,9 +83,9 @@ jobs:
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '3.12'
- name: Check for valid Python & Merge Conflicts
run: |
@ -132,6 +132,7 @@ jobs:
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TYPE: server
DB: ${{ matrix.db }}
@ -142,6 +143,7 @@ jobs:
SITE: test_site
CI_BUILD_ID: ${{ github.run_id }}
BUILD_NUMBER: ${{ matrix.container }}
FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TOTAL_BUILDS: 2
COVERAGE_RCFILE: /home/runner/frappe-bench/apps/frappe/.coveragerc

View file

@ -65,9 +65,9 @@ jobs:
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '3.12'
- name: Check for valid Python & Merge Conflicts
run: |
@ -120,6 +120,7 @@ jobs:
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TYPE: ui
DB: mariadb

View file

@ -1,10 +1,8 @@
<div align="center">
<h1>
<br>
<a href="https://frappeframework.com">
<img src=".github/frappe-framework-logo.svg" height="50">
</a>
</h1>
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/frappe-framework-logo-dark.svg">
<img src=".github/frappe-framework-logo.svg" height="50">
</picture>
<h3>
a web framework with <a href="https://www.youtube.com/watch?v=LOjk3m0wTwg">"batteries included"</a>
</h3>
@ -71,12 +69,12 @@ Full-stack web application framework that uses Python and MariaDB on the server
1. [Code of Conduct](CODE_OF_CONDUCT.md)
1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
1. [Security Policy](SECURITY.md)
1. [Translations](https://translate.erpnext.com)
## Resources
1. [frappeframework.com](https://frappeframework.com) - Official documentation of the Frappe Framework.
1. [frappe.school](https://frappe.school) - Pick from the various courses by the maintainers or from the community.
1. [buildwithhussain.dev](https://buildwithhussain.dev) - Watch Frappe Framework being used in the wild to build world-class web apps.
## License
This repository has been released under the [MIT License](LICENSE).

View file

@ -2,7 +2,11 @@ context("Awesome Bar", () => {
before(() => {
cy.visit("/login");
cy.login();
cy.visit("/app/website");
cy.visit("/app/todo"); // Make sure ToDo filters are cleared.
cy.clear_filters();
cy.visit("/app/blog-post"); // Make sure Blog Post filters are cleared.
cy.clear_filters();
cy.visit("/app/website"); // Go to some other page.
});
beforeEach(() => {
@ -11,36 +15,61 @@ context("Awesome Bar", () => {
cy.get("@awesome_bar").type("{selectall}");
});
after(() => {
cy.visit("/app/todo"); // Make sure we're not bleeding any filters to the next spec.
cy.clear_filters();
});
it("navigates to doctype list", () => {
cy.get("@awesome_bar").type("todo");
cy.wait(100);
cy.wait(100); // Wait a bit before hitting enter.
cy.get(".awesomplete").findByRole("listbox").should("be.visible");
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text").should("contain", "To Do");
cy.location("pathname").should("eq", "/app/todo");
});
it("find text in doctype list", () => {
it("finds text in doctype list", () => {
cy.get("@awesome_bar").type("test in todo");
cy.wait(100);
cy.wait(150); // Wait a bit before hitting enter.
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text").should("contain", "To Do");
cy.wait(200);
const name_filter = cy.get('[data-original-title="ID"] > input');
name_filter.should("have.value", "%test%");
cy.clear_filters();
cy.wait(200); // Wait a bit longer before checking the filter.
cy.get('[data-original-title="ID"] > input').should("have.value", "%test%");
});
it("filter preserved, now finds something else", () => {
cy.visit("/app/todo");
cy.get(".title-text").should("contain", "To Do");
cy.wait(200); // Wait a bit longer before checking the filter.
cy.get('[data-original-title="ID"] > input').as("filter");
cy.get("@filter").should("have.value", "%test%");
cy.get("@awesome_bar").type("anothertest in todo");
cy.wait(200); // Wait a bit longer before hitting enter.
cy.get("@awesome_bar").type("{enter}");
cy.wait(200); // Wait a bit longer before checking the filter.
cy.get("@filter").should("have.value", "%anothertest%");
});
it("navigates to another doctype, filter not bleeding", () => {
cy.get("@awesome_bar").type("blog post");
cy.wait(150); // Wait a bit before hitting enter.
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text").should("contain", "Blog Post");
cy.wait(200); // Wait a bit longer before checking the filter.
cy.location("search").should("be.empty");
});
it("navigates to new form", () => {
cy.get("@awesome_bar").type("new blog post");
cy.wait(100);
cy.wait(150); // Wait a bit before hitting enter
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text:visible").should("have.text", "New Blog Post");
});
it("calculates math expressions", () => {
cy.get("@awesome_bar").type("55 + 32");
cy.wait(100);
cy.wait(150); // Wait a bit before hitting enter
cy.get("@awesome_bar").type("{downarrow}{enter}");
cy.get(".modal-title").should("contain", "Result");
cy.get(".msgprint").should("contain", "55 + 32 = 87");

View file

@ -9,6 +9,7 @@ context("Control Currency", () => {
function get_dialog_with_currency(df_options = {}) {
return cy.dialog({
title: "Currency Check",
animate: false,
fields: [
{
fieldname: fieldname,
@ -76,6 +77,7 @@ context("Control Currency", () => {
});
get_dialog_with_currency(test_case.df_options).as("dialog");
cy.wait(300);
cy.get_field(fieldname, "Currency").clear();
cy.wait(300);
cy.fill_field(fieldname, test_case.input, "Currency").blur();

View file

@ -7,6 +7,7 @@ context("Control Float", () => {
function get_dialog_with_float() {
return cy.dialog({
title: "Float Check",
animate: false,
fields: [
{
fieldname: "float_number",
@ -19,6 +20,7 @@ context("Control Float", () => {
it("check value changes", () => {
get_dialog_with_float().as("dialog");
cy.wait(300);
let data = get_data();
data.forEach((x) => {

View file

@ -152,7 +152,7 @@ context("Control Link", () => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("todo for link");
cy.get("@input").type("todo for link", { delay: 200 });
cy.wait("@search_link");
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
@ -260,7 +260,7 @@ context("Control Link", () => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("Sonstiges", { delay: 100 });
cy.get("@input").type("Sonstiges", { delay: 200 });
cy.wait("@search_link");
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });
@ -291,7 +291,7 @@ context("Control Link", () => {
cy.get(".frappe-control[data-fieldname=link] input").focus().as("input");
cy.wait("@search_link");
cy.get("@input").type("Non-Conforming", { delay: 100 });
cy.get("@input").type("Non-Conforming", { delay: 200 });
cy.wait("@search_link");
cy.get(".frappe-control[data-fieldname=link] ul").should("be.visible");
cy.get(".frappe-control[data-fieldname=link] input").type("{enter}", { delay: 100 });

View file

@ -6,6 +6,10 @@ context("Control Phone", () => {
cy.visit("/app/website");
});
afterEach(() => {
cy.clear_dialogs();
});
function get_dialog_with_phone() {
return cy.dialog({
title: "Phone",
@ -20,31 +24,37 @@ context("Control Phone", () => {
it("should set flag and data", () => {
get_dialog_with_phone().as("dialog");
cy.get(".selected-phone").click();
cy.wait(100);
cy.get(".phone-picker .phone-wrapper[id='afghanistan']").click();
cy.wait(100);
cy.get(".selected-phone .country").should("have.text", "+93");
cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/af.svg");
cy.get(".selected-phone").click();
cy.wait(100);
cy.get(".phone-picker .phone-wrapper[id='india']").click();
cy.wait(100);
cy.get(".selected-phone .country").should("have.text", "+91");
cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg");
let phone_number = "9312672712";
cy.get(".selected-phone > img").click().first();
cy.get_field("phone").first().click({ multiple: true });
cy.get_field("phone").first().click();
cy.get(".frappe-control[data-fieldname=phone]")
.findByRole("textbox")
.first()
.type(phone_number, { force: true });
.type(phone_number);
cy.get_field("phone").first().should("have.value", phone_number);
cy.get_field("phone").first().blur({ force: true });
cy.get_field("phone").first().blur();
cy.wait(100);
cy.get("@dialog").then((dialog) => {
let value = dialog.get_value("phone");
expect(value).to.equal("+91-" + phone_number);
});
});
it("case insensitive search for country and clear search", () => {
let search_text = "india";
cy.get(".selected-phone").click().first();
cy.get(".phone-picker").get(".search-phones").click().type(search_text);

View file

@ -116,50 +116,6 @@ context("Form", () => {
cy.get_field("location").should("have.value", "Bermuda");
});
it("let user undo/redo field value changes", { scrollBehavior: false }, () => {
const undo = () => cy.get("body").type("{esc}").type("{ctrl+z}").wait(500);
const redo = () => cy.get("body").type("{esc}").type("{ctrl+y}").wait(500);
cy.new_form("User");
jump_to_field("Email");
type_value("admin@example.com");
jump_to_field("Username");
type_value("admin42");
jump_to_field("Send Welcome Email");
cy.focused().uncheck();
// make a mistake
jump_to_field("Username");
type_value("admin24");
// undo behaviour
undo();
cy.get_field("username").should("have.value", "admin42");
// redo behaviour
redo();
cy.get_field("username").should("have.value", "admin24");
// undo everything & redo everything, ensure same values at the end
undo();
undo();
undo();
undo();
redo();
redo();
redo();
redo();
cy.compare_document({
username: "admin24",
email: "admin@example.com",
send_welcome_email: 0,
});
});
it("update docfield property using set_df_property in child table", () => {
cy.visit("/app/contact/Test Form Contact 1");
cy.window()

View file

@ -76,6 +76,11 @@ context("MultiSelectDialog", () => {
});
it("tests more button", () => {
cy.get_open_dialog()
.get(`.frappe-control[data-fieldname="search_term"]`)
.find('input[data-fieldname="search_term"]')
.should("exist")
.type("Test", { delay: 200 });
cy.get_open_dialog()
.get(`.frappe-control[data-fieldname="more_child_btn"]`)
.should("exist")

View file

@ -1,4 +1,4 @@
context("Permissions API", () => {
context.skip("Permissions API", () => {
before(() => {
cy.visit("/login");
cy.remove_role("frappe@example.com", "System Manager");

View file

@ -1,37 +0,0 @@
context("Theme Switcher Shortcut", () => {
before(() => {
cy.login();
cy.visit("/app");
});
beforeEach(() => {
cy.reload();
});
it("Check Toggle", () => {
cy.open_theme_dialog();
cy.get(".modal-backdrop").should("exist");
cy.intercept("POST", "/api/method/frappe.core.doctype.user.user.switch_theme").as(
"set_theme"
);
cy.findByText("Timeless Night").click();
cy.wait("@set_theme");
cy.close_theme("{ctrl+shift+g}");
cy.get(".modal-backdrop").should("not.exist");
});
it("Check Enter", () => {
cy.open_theme_dialog();
cy.intercept("POST", "/api/method/frappe.core.doctype.user.user.switch_theme").as(
"set_theme"
);
cy.findByText("Frappe Light").click();
cy.wait("@set_theme");
cy.close_theme("{enter}");
cy.get(".modal-backdrop").should("not.exist");
});
});
Cypress.Commands.add("open_theme_dialog", () => {
cy.get("body").type("{ctrl+shift+g}");
});
Cypress.Commands.add("close_theme", (shortcut_keys) => {
cy.get(".modal-header").type(shortcut_keys);
});

View file

@ -1,91 +0,0 @@
import custom_submittable_doctype from "../fixtures/custom_submittable_doctype";
context("Timeline", () => {
before(() => {
cy.visit("/login");
cy.login();
});
it("Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo", () => {
//Adding new ToDo
cy.new_form("ToDo");
cy.get('[data-fieldname="description"] .ql-editor.ql-blank')
.type("Test ToDo", { force: true })
.wait(200);
cy.get(".page-head .page-actions").findByRole("button", { name: "Save" }).click();
cy.go_to_list("ToDo");
cy.clear_filters();
cy.click_listview_row_item(0);
//To check if the comment box is initially empty and tying some text into it
cy.get('[data-fieldname="comment"] .ql-editor')
.should("contain", "")
.type("Testing Timeline");
//Adding new comment
cy.get(".comment-box").findByRole("button", { name: "Comment" }).click();
//To check if the commented text is visible in the timeline content
cy.get(".timeline-content").should("contain", "Testing Timeline");
//Editing comment
cy.click_timeline_action_btn("Edit");
cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(" 123");
cy.click_timeline_action_btn("Save");
//To check if the edited comment text is visible in timeline content
cy.get(".timeline-content").should("contain", "Testing Timeline 123");
//Discarding comment
cy.click_timeline_action_btn("Edit");
cy.click_timeline_action_btn("Dismiss");
//To check if after discarding the timeline content is same as previous
cy.get(".timeline-content").should("contain", "Testing Timeline 123");
//Deleting the added comment
cy.get(".timeline-message-box .more-actions > .action-btn").click(); //Menu button in timeline item
cy.get(".timeline-message-box .more-actions .dropdown-item")
.contains("Delete")
.click({ force: true });
cy.get_open_dialog().findByRole("button", { name: "Yes" }).click({ force: true });
cy.get(".timeline-content").should("not.contain", "Testing Timeline 123");
});
it("Timeline should have submit and cancel activity information", () => {
cy.visit("/app/doctype");
//Creating custom doctype
cy.insert_doc("DocType", custom_submittable_doctype, true);
cy.visit("/app/custom-submittable-doctype");
cy.click_listview_primary_button("Add Custom Submittable DocType");
//Adding a new entry for the created custom doctype
cy.fill_field("title", "Test");
cy.click_modal_primary_button("Save");
cy.click_modal_primary_button("Submit");
cy.visit("/app/custom-submittable-doctype");
cy.click_listview_row_item(0);
//To check if the submission of the documemt is visible in the timeline content
cy.get(".timeline-content").should("contain", "You submitted this document");
cy.get('[id="page-Custom Submittable DocType"] .page-actions')
.findByRole("button", { name: "Cancel" })
.click();
cy.get_open_dialog().findByRole("button", { name: "Yes" }).click();
//To check if the cancellation of the documemt is visible in the timeline content
cy.get(".timeline-content").should("contain", "You cancelled this document");
//Deleting the document
cy.visit("/app/custom-submittable-doctype");
cy.select_listview_row_checkbox(0);
cy.get(".page-actions").findByRole("button", { name: "Actions" }).click();
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
cy.click_modal_primary_button("Yes");
});
});

View file

@ -17,7 +17,6 @@ import inspect
import json
import os
import re
import unicodedata
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeAlias, overload
@ -43,9 +42,8 @@ from .utils.jinja import (
get_template,
render_template,
)
from .utils.lazy_loader import lazy_import
__version__ = "15.0.0-dev"
__version__ = "16.0.0-dev"
__title__ = "Frappe Framework"
controllers = {}
@ -61,6 +59,32 @@ if _dev_server:
warnings.simplefilter("always", DeprecationWarning)
warnings.simplefilter("always", PendingDeprecationWarning)
# Always initialize sentry SDK if the DSN is sent
if sentry_dsn := os.getenv("FRAPPE_SENTRY_DSN"):
import sentry_sdk
from sentry_sdk.integrations.argv import ArgvIntegration
from sentry_sdk.integrations.atexit import AtexitIntegration
from sentry_sdk.integrations.dedupe import DedupeIntegration
from sentry_sdk.integrations.excepthook import ExcepthookIntegration
from sentry_sdk.integrations.modules import ModulesIntegration
from frappe.utils.sentry import before_send
sentry_sdk.init(
dsn=sentry_dsn,
before_send=before_send,
release=__version__,
auto_enabling_integrations=False,
default_integrations=False,
integrations=[
AtexitIntegration(),
ExcepthookIntegration(),
DedupeIntegration(),
ModulesIntegration(),
ArgvIntegration(),
],
)
class _dict(dict):
"""dict like object that exposes keys as attributes"""
@ -85,7 +109,7 @@ class _dict(dict):
def _(msg: str, lang: str | None = None, context: str | None = None) -> str:
"""Returns translated string in current lang, if exists.
"""Return translated string in current lang, if exists.
Usage:
_('Change')
_('Change', context='Coins')
@ -120,8 +144,8 @@ def _(msg: str, lang: str | None = None, context: str | None = None) -> str:
return translated_string or non_translated_string
def as_unicode(text: str, encoding: str = "utf-8") -> str:
"""Convert to unicode if required"""
def as_unicode(text, encoding: str = "utf-8") -> str:
"""Convert to unicode if required."""
if isinstance(text, str):
return text
elif text is None:
@ -165,6 +189,7 @@ if TYPE_CHECKING: # pragma: no cover
from frappe.database.mariadb.database import MariaDBDatabase
from frappe.database.postgres.database import PostgresDatabase
from frappe.email.doctype.email_queue.email_queue import EmailQueue
from frappe.model.document import Document
from frappe.query_builder.builder import MariaDB, Postgres
from frappe.utils.redis_wrapper import RedisWrapper
@ -239,7 +264,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
local.jloader = None
local.cache = {}
local.form_dict = _dict()
local.preload_assets = {"style": [], "script": []}
local.preload_assets = {"style": [], "script": [], "icons": []}
local.session = _dict()
local.dev_server = _dev_server
local.qb = get_query_builder(local.conf.db_type)
@ -302,7 +327,7 @@ def connect_replica() -> bool:
def get_site_config(sites_path: str | None = None, site_path: str | None = None) -> dict[str, Any]:
"""Returns `site_config.json` combined with `sites/common_site_config.json`.
"""Return `site_config.json` combined with `sites/common_site_config.json`.
`site_config` is a set of site wide settings like database name, password, email etc."""
config = _dict()
@ -348,7 +373,7 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None)
def get_common_site_config(sites_path: str | None = None) -> dict[str, Any]:
"""Returns common site config as dictionary.
"""Return common site config as dictionary.
This is useful for:
- checking configuration which should only be allowed in common site config
@ -407,7 +432,7 @@ def setup_redis_cache_connection():
def get_traceback(with_context: bool = False) -> str:
"""Returns error traceback."""
"""Return error traceback."""
from frappe.utils import get_traceback
return get_traceback(with_context=with_context)
@ -418,7 +443,7 @@ def errprint(msg: str) -> None:
:param msg: Message."""
msg = as_unicode(msg)
if not request or (not "cmd" in local.form_dict) or conf.developer_mode:
if not request or ("cmd" not in local.form_dict) or conf.developer_mode:
print(msg)
error_log.append({"exc": msg})
@ -433,7 +458,7 @@ def log(msg: str) -> None:
:param msg: Message."""
if not request:
if conf.get("logging") or False:
if conf.get("logging"):
print(repr(msg))
debug_log.append(as_unicode(msg))
@ -450,6 +475,8 @@ def msgprint(
primary_action: str = None,
is_minimizable: bool = False,
wide: bool = False,
*,
realtime=False,
) -> None:
"""Print a message to the user (via HTTP response).
Messages are sent in the `__server_messages` property in the
@ -463,6 +490,7 @@ def msgprint(
:param primary_action: [optional] Bind a primary server/client side action.
:param is_minimizable: [optional] Allow users to minimize the modal
:param wide: [optional] Show wide modal
:param realtime: Publish message immediately using websocket.
"""
import inspect
import sys
@ -529,7 +557,10 @@ def msgprint(
if wide:
out.wide = wide
message_log.append(out)
if realtime:
publish_realtime(event="msgprint", message=out)
else:
message_log.append(out)
_raise_exception()
@ -607,7 +638,7 @@ def get_user():
def get_roles(username=None) -> list[str]:
"""Returns roles of current user."""
"""Return roles of current user."""
if not local.session or not local.session.user:
return ["Guest"]
import frappe.permissions
@ -661,7 +692,7 @@ def sendmail(
print_letterhead=False,
with_container=False,
email_read_tracker_url=None,
):
) -> Optional["EmailQueue"]:
"""Send email using user's default **Email Account** or global default **Email Account**.
@ -746,12 +777,12 @@ def sendmail(
)
# build email queue and send the email if send_now is True.
builder.process(send_now=now)
return builder.process(send_now=now)
whitelisted = []
guest_methods = []
xss_safe_methods = []
whitelisted = set()
guest_methods = set()
xss_safe_methods = set()
allowed_http_methods_for_whitelisted_func = {}
@ -790,14 +821,14 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None):
else:
fn = validate_argument_types(fn, apply_condition=in_request_or_test)
whitelisted.append(fn)
whitelisted.add(fn)
allowed_http_methods_for_whitelisted_func[fn] = methods
if allow_guest:
guest_methods.append(fn)
guest_methods.add(fn)
if xss_safe:
xss_safe_methods.append(fn)
xss_safe_methods.add(fn)
return method or fn
@ -972,8 +1003,9 @@ def has_permission(
parent_doctype=None,
):
"""
Returns True if the user has permission `ptype` for given `doctype` or `doc`
Raises `frappe.PermissionError` if user isn't permitted and `throw` is truthy
Return True if the user has permission `ptype` for given `doctype` or `doc`.
Raise `frappe.PermissionError` if user isn't permitted and `throw` is truthy
:param doctype: DocType for which permission is to be check.
:param ptype: Permission type (`read`, `write`, `create`, `submit`, `cancel`, `amend`). Default: `read`.
@ -1053,7 +1085,7 @@ def has_website_permission(doc=None, ptype="read", user=None, verbose=False, doc
def is_table(doctype: str) -> bool:
"""Returns True if `istable` property (indicating child Table) is set for given DocType."""
"""Return True if `istable` property (indicating child Table) is set for given DocType."""
def get_tables():
return db.get_values("DocType", filters={"istable": 1}, order_by=None, pluck=True)
@ -1097,7 +1129,7 @@ def new_doc(
as_dict: bool = False,
**kwargs,
) -> "Document":
"""Returns a new document of the given DocType with defaults set.
"""Return a new document of the given DocType with defaults set.
:param doctype: DocType of the new document.
:param parent_doc: [optional] add to parent document.
@ -1121,6 +1153,7 @@ def set_value(doctype, docname, fieldname, value=None):
def get_cached_doc(*args, **kwargs) -> "Document":
"""Identical to `frappe.get_doc`, but return from cache if available."""
if (key := can_cache_doc(args)) and (doc := cache.get_value(key)):
return doc
@ -1143,7 +1176,7 @@ def _set_document_in_cache(key: str, doc: "Document") -> None:
def can_cache_doc(args) -> str | None:
"""
Determine if document should be cached based on get_doc params.
Returns cache key if doc can be cached, None otherwise.
Return cache key if doc can be cached, None otherwise.
"""
if not args:
@ -1395,17 +1428,17 @@ def rename_doc(
def get_module(modulename):
"""Returns a module object for given Python module name using `importlib.import_module`."""
"""Return a module object for given Python module name using `importlib.import_module`."""
return importlib.import_module(modulename)
def scrub(txt: str) -> str:
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`."""
"""Return sluggified string. e.g. `Sales Order` becomes `sales_order`."""
return cstr(txt).replace(" ", "_").replace("-", "_").lower()
def unscrub(txt: str) -> str:
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`."""
"""Return titlified string. e.g. `sales_order` becomes `Sales Order`."""
return txt.replace("_", " ").replace("-", " ").title()
@ -1505,7 +1538,7 @@ def get_installed_apps(*, _ensure_on_bench=False) -> list[str]:
def get_doc_hooks():
"""Returns hooked methods for given doc. It will expand the dict tuple if required."""
"""Return hooked methods for given doc. Expand the dict tuple if required."""
if not hasattr(local, "doc_events_hooks"):
hooks = get_hooks("doc_events", {})
out = {}
@ -1612,7 +1645,7 @@ def setup_module_map():
def get_file_items(path, raise_not_found=False, ignore_empty_lines=True):
"""Returns items from text file as a list. Ignores empty lines."""
"""Return items from text file as a list. Ignore empty lines."""
import frappe.utils
content = read_file(path, raise_not_found=raise_not_found)
@ -1959,13 +1992,13 @@ def get_all(doctype, *args, **kwargs):
frappe.get_all("ToDo", fields=["*"], filters = [["modified", ">", "2014-01-01"]])
"""
kwargs["ignore_permissions"] = True
if not "limit_page_length" in kwargs:
if "limit_page_length" not in kwargs:
kwargs["limit_page_length"] = 0
return get_list(doctype, *args, **kwargs)
def get_value(*args, **kwargs):
"""Returns a document property or list of properties.
"""Return a document property or list of properties.
Alias for `frappe.db.get_value`
@ -1980,6 +2013,7 @@ def get_value(*args, **kwargs):
def as_json(obj: dict | list, indent=1, separators=None, ensure_ascii=True) -> str:
"""Return the JSON string representation of the given `obj`."""
from frappe.utils.response import json_handler
if separators is None:
@ -2008,11 +2042,11 @@ def as_json(obj: dict | list, indent=1, separators=None, ensure_ascii=True) -> s
def are_emails_muted():
return flags.mute_emails or cint(conf.get("mute_emails") or 0) or False
return flags.mute_emails or cint(conf.get("mute_emails"))
def get_test_records(doctype):
"""Returns list of objects from `test_records.json` in the given doctype's folder."""
"""Return list of objects from `test_records.json` in the given doctype's folder."""
from frappe.modules import get_doctype_module, get_module_path
path = os.path.join(
@ -2245,7 +2279,7 @@ log_level = None
def logger(
module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20
):
"""Returns a python logger that uses StreamHandler"""
"""Return a python logger that uses StreamHandler."""
from frappe.utils.logger import get_logger
return get_logger(
@ -2265,7 +2299,8 @@ def get_desk_link(doctype, name):
return html.format(doctype=doctype, name=name, doctype_local=_(doctype))
def bold(text):
def bold(text: str) -> str:
"""Return `text` wrapped in `<strong>` tags."""
return f"<strong>{text}</strong>"
@ -2288,7 +2323,8 @@ def get_website_settings(key):
return local.website_settings.get(key)
def get_system_settings(key):
def get_system_settings(key: str):
"""Return the value associated with the given `key` from System Settings DocType."""
if not hasattr(local, "system_settings"):
try:
local.system_settings = get_cached_doc("System Settings")
@ -2307,7 +2343,7 @@ def get_active_domains():
def get_version(doctype, name, limit=None, head=False, raise_err=True):
"""
Returns a list of version information of a given DocType.
Return a list of version information for the given DocType.
Note: Applicable only if DocType has changes tracked.

View file

@ -22,7 +22,7 @@ import frappe.rate_limiter
import frappe.recorder
import frappe.utils.response
from frappe import _
from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest, validate_auth
from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest, validate_auth # noqa
from frappe.middlewares import StaticDataMiddleware
from frappe.utils import CallbackManager, cint, get_site_name
from frappe.utils.data import escape_html
@ -179,9 +179,12 @@ def init_request(request):
raise frappe.SessionStopped("Session Stopped")
else:
frappe.connect(set_admin_as_user=False)
if request.path.startswith("/api/method/upload_file"):
from frappe.core.api.file import get_max_file_size
request.max_content_length = cint(frappe.local.conf.get("max_file_size")) or 10 * 1024 * 1024
request.max_content_length = get_max_file_size()
else:
request.max_content_length = cint(frappe.local.conf.get("max_file_size")) or 25 * 1024 * 1024
make_form_dict(request)
if request.method != "OPTIONS":

View file

@ -25,6 +25,7 @@ from frappe.website.utils import get_home_page
SAFE_HTTP_METHODS = frozenset(("GET", "HEAD", "OPTIONS"))
UNSAFE_HTTP_METHODS = frozenset(("POST", "PUT", "DELETE", "PATCH"))
MAX_PASSWORD_SIZE = 512
class HTTPRequest:
@ -96,7 +97,6 @@ class HTTPRequest:
class LoginManager:
__slots__ = ("user", "info", "full_name", "user_type", "resume")
def __init__(self):
@ -235,6 +235,9 @@ class LoginManager:
if not (user and pwd):
self.fail(_("Incomplete login details"), user=user)
if len(pwd) > MAX_PASSWORD_SIZE:
self.fail(_("Password size exceeded the maximum allowed size"), user=user)
_raw_user_name = user
user = User.find_by_credentials(user, pwd)
@ -286,7 +289,7 @@ class LoginManager:
def check_password(self, user, pwd):
"""check password"""
try:
# returns user in correct case
# return user in correct case
return check_password(user, pwd)
except frappe.AuthenticationError:
self.fail("Incorrect password", user=user)
@ -305,8 +308,8 @@ class LoginManager:
def validate_hour(self):
"""check if user is logging in during restricted hours"""
login_before = int(frappe.db.get_value("User", self.user, "login_before", ignore=True) or 0)
login_after = int(frappe.db.get_value("User", self.user, "login_after", ignore=True) or 0)
login_before = cint(frappe.db.get_value("User", self.user, "login_before", ignore=True))
login_after = cint(frappe.db.get_value("User", self.user, "login_after", ignore=True))
if not (login_before or login_after):
return

View file

@ -150,7 +150,7 @@ class AutoRepeat(Document):
def validate_auto_repeat_days(self):
auto_repeat_days = self.get_auto_repeat_days()
if not len(set(auto_repeat_days)) == len(auto_repeat_days):
if len(set(auto_repeat_days)) != len(auto_repeat_days):
repeated_days = get_repeated(auto_repeat_days)
plural = "s" if len(repeated_days) > 1 else ""
@ -297,11 +297,11 @@ class AutoRepeat(Document):
def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
"""
Returns the next schedule date for auto repeat after a recurring document has been created.
Adds required offset to the schedule_date param and returns the next schedule date.
Return the next schedule date for auto repeat after a recurring document has been created.
Add required offset to the schedule_date param and return the next schedule date.
:param schedule_date: The date when the last recurring document was created.
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
:param for_full_schedule: If True, return the immediate next schedule date, else the full schedule.
"""
if month_map.get(self.frequency):
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1

View file

@ -53,7 +53,7 @@
],
"in_create": 1,
"links": [],
"modified": "2022-08-03 12:20:55.076769",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Automation",
"name": "Milestone",
@ -74,7 +74,7 @@
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"title_field": "reference_type",
"track_changes": 1

View file

@ -35,7 +35,7 @@
}
],
"links": [],
"modified": "2022-08-03 12:20:54.955953",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Automation",
"name": "Milestone Tracker",
@ -55,7 +55,7 @@
}
],
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -122,12 +122,12 @@ def get_letter_heads():
def load_conf_settings(bootinfo):
from frappe import conf
from frappe.core.api.file import get_max_file_size
bootinfo.max_file_size = conf.get("max_file_size") or 10485760
bootinfo.max_file_size = get_max_file_size()
for key in ("developer_mode", "socketio_port", "file_watcher_port"):
if key in conf:
bootinfo[key] = conf.get(key)
if key in frappe.conf:
bootinfo[key] = frappe.conf.get(key)
def load_desktop_data(bootinfo):

View file

@ -133,10 +133,9 @@ def setup_assets(assets_archive):
return directories_created
def download_frappe_assets(verbose=True):
"""Downloads and sets up Frappe assets if they exist based on the current
commit HEAD.
Returns True if correctly setup else returns False.
def download_frappe_assets(verbose=True) -> bool:
"""Download and set up Frappe assets if they exist based on the current commit HEAD.
Return True if correctly setup else return False.
"""
frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD")
@ -407,7 +406,7 @@ def link_assets_dir(source, target, hard_link=False):
def scrub_html_template(content):
"""Returns HTML content with removed whitespace and comments"""
"""Return HTML content with removed whitespace and comments."""
# remove whitespace to a single space
content = WHITESPACE_PATTERN.sub(" ", content)
@ -418,7 +417,7 @@ def scrub_html_template(content):
def html_to_js_template(path, content):
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
"""Return HTML template content as Javascript code, by adding it to `frappe.templates`."""
return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)
)

View file

@ -10,6 +10,7 @@ import frappe.utils
from frappe import _
from frappe.desk.reportview import validate_args
from frappe.model.db_query import check_parent_permission
from frappe.model.utils import is_virtual_doctype
from frappe.utils import get_safe_filters
from frappe.utils.deprecations import deprecated
@ -37,7 +38,7 @@ def get_list(
as_dict: bool = True,
or_filters=None,
):
"""Returns a list of records by filters, fields, ordering and limit
"""Return a list of records by filters, fields, ordering and limit.
:param doctype: DocType of the data to be queried
:param fields: fields to be returned. Default is `name`
@ -73,7 +74,7 @@ def get_count(doctype, filters=None, debug=False, cache=False):
@frappe.whitelist()
def get(doctype, name=None, filters=None, parent=None):
"""Returns a document by name or filters
"""Return a document by name or filters.
:param doctype: DocType of the document to be returned
:param name: return document of this `name`
@ -96,7 +97,7 @@ def get(doctype, name=None, filters=None, parent=None):
@frappe.whitelist()
def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None):
"""Returns a value form a document
"""Return a value from a document.
:param doctype: DocType to be queried
:param fieldname: Field to be returned (default `name`)
@ -295,7 +296,7 @@ def bulk_update(docs):
@frappe.whitelist()
def has_permission(doctype, docname, perm_type="read"):
"""Returns a JSON with data whether the document has the requested permission
"""Return a JSON with data whether the document has the requested permission.
:param doctype: DocType of the document to be checked
:param docname: `name` of the document to be checked
@ -306,7 +307,7 @@ def has_permission(doctype, docname, perm_type="read"):
@frappe.whitelist()
def get_doc_permissions(doctype, docname):
"""Returns an evaluated document permissions dict like `{"read":1, "write":1}`
"""Return an evaluated document permissions dict like `{"read":1, "write":1}`.
:param doctype: DocType of the document to be evaluated
:param docname: `name` of the document to be evaluated
@ -353,7 +354,7 @@ def get_js(items):
@frappe.whitelist(allow_guest=True)
def get_time_zone():
"""Returns default time zone"""
"""Return the default time zone."""
return {"time_zone": frappe.defaults.get_defaults().get("time_zone")}
@ -431,6 +432,18 @@ def validate_link(doctype: str, docname: str, fields=None):
)
values = frappe._dict()
if is_virtual_doctype(doctype):
try:
frappe.get_doc(doctype, docname)
values.name = docname
except frappe.DoesNotExistError:
frappe.clear_last_message()
frappe.msgprint(
_("Document {0} {1} does not exist").format(frappe.bold(doctype), frappe.bold(docname)),
)
return values
values.name = frappe.db.get_value(doctype, docname, cache=True)
fields = frappe.parse_json(fields)
@ -453,8 +466,7 @@ def validate_link(doctype: str, docname: str, fields=None):
def insert_doc(doc) -> "Document":
"""Inserts document and returns parent document object with appended child document
if `doc` is child document else returns the inserted document object
"""Insert document and return parent document object with appended child document if `doc` is child document else return the inserted document object.
:param doc: doc to insert (dict)"""

View file

@ -202,7 +202,7 @@ def start_scheduler():
def start_worker(
queue, quiet=False, rq_username=None, rq_password=None, burst=False, strategy=None
):
"""Start a backgrond worker"""
"""Start a background worker"""
from frappe.utils.background_jobs import start_worker
start_worker(
@ -225,7 +225,7 @@ def start_worker(
@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs")
@click.option("--burst", is_flag=True, default=False, help="Run Worker in Burst mode.")
def start_worker_pool(queue, quiet=False, num_workers=2, burst=False):
"""Start a backgrond worker"""
"""Start a pool of background workers"""
from frappe.utils.background_jobs import start_worker_pool
start_worker_pool(

View file

@ -72,13 +72,10 @@ def new_site(
setup_db=True,
):
"Create a new site"
from frappe.installer import _new_site, extract_sql_from_archive
from frappe.installer import _new_site
frappe.init(site=site, new_site=True)
if source_sql:
source_sql = extract_sql_from_archive(source_sql)
_new_site(
db_name,
site,
@ -180,75 +177,113 @@ def _restore(
with_public_files=None,
with_private_files=None,
):
from frappe.installer import extract_files
from frappe.utils.backups import decrypt_backup, get_or_generate_backup_encryption_key
from frappe.installer import (
_new_site,
extract_files,
extract_sql_from_archive,
is_downgrade,
is_partial,
validate_database_sql,
)
from frappe.utils.backups import Backup, get_or_generate_backup_encryption_key
err, out = frappe.utils.execute_in_shell(f"file {sql_file_path}", check_exit_code=True)
if err:
click.secho("Failed to detect type of backup file", fg="red")
sys.exit(1)
_backup = Backup(sql_file_path)
try:
decompressed_file_name = extract_sql_from_archive(sql_file_path)
if is_partial(decompressed_file_name):
click.secho(
"Partial Backup file detected. You cannot use a partial file to restore a Frappe site.",
fg="red",
)
click.secho(
"Use `bench partial-restore` to restore a partial backup to an existing site.",
fg="yellow",
)
_backup.decryption_rollback()
sys.exit(1)
except UnicodeDecodeError:
_backup.decryption_rollback()
if "cipher" in out.decode().split(":")[-1].strip():
if encryption_key:
click.secho("Encrypted backup file detected. Decrypting using provided key.", fg="yellow")
_backup.backup_decryption(encryption_key)
else:
click.secho("Encrypted backup file detected. Decrypting using site config.", fg="yellow")
encryption_key = get_or_generate_backup_encryption_key()
_backup.backup_decryption(encryption_key)
# Rollback on unsuccessful decryrption
if not os.path.exists(sql_file_path):
click.secho("Decryption failed. Please provide a valid key and try again.", fg="red")
with decrypt_backup(sql_file_path, encryption_key):
# Rollback on unsuccessful decryption
if not os.path.exists(sql_file_path):
click.secho("Decryption failed. Please provide a valid key and try again.", fg="red")
sys.exit(1)
_backup.decryption_rollback()
sys.exit(1)
decompressed_file_name = extract_sql_from_archive(sql_file_path)
if is_partial(decompressed_file_name):
click.secho(
"Partial Backup file detected. You cannot use a partial file to restore a Frappe site.",
fg="red",
restore_backup(
sql_file_path,
site,
db_root_username,
db_root_password,
verbose,
install_app,
admin_password,
force,
)
click.secho(
"Use `bench partial-restore` to restore a partial backup to an existing site.",
fg="yellow",
)
_backup.decryption_rollback()
sys.exit(1)
else:
restore_backup(
sql_file_path,
site,
db_root_username,
db_root_password,
verbose,
install_app,
admin_password,
force,
)
validate_database_sql(decompressed_file_name, _raise=not force)
# Extract public and/or private files to the restored site, if user has given the path
if with_public_files:
# Decrypt data if there is a Key
if encryption_key:
with decrypt_backup(with_public_files, encryption_key):
public = extract_files(site, with_public_files)
else:
public = extract_files(site, with_public_files)
# dont allow downgrading to older versions of frappe without force
if not force and is_downgrade(decompressed_file_name, verbose=True):
# Removing temporarily created file
os.remove(public)
if with_private_files:
# Decrypt data if there is a Key
if encryption_key:
with decrypt_backup(with_private_files, encryption_key):
private = extract_files(site, with_private_files)
else:
private = extract_files(site, with_private_files)
# Removing temporarily created file
os.remove(private)
success_message = "Site {} has been restored{}".format(
site, " with files" if (with_public_files or with_private_files) else ""
)
click.secho(success_message, fg="green")
def restore_backup(
sql_file_path: str,
site,
db_root_username,
db_root_password,
verbose,
install_app,
admin_password,
force,
):
from frappe.installer import _new_site, is_downgrade, is_partial, validate_database_sql
if is_partial(sql_file_path):
click.secho(
"Partial Backup file detected. You cannot use a partial file to restore a Frappe site.",
fg="red",
)
click.secho(
"Use `bench partial-restore` to restore a partial backup to an existing site.",
fg="yellow",
)
sys.exit(1)
# Check if the backup is of an older version of frappe and the user hasn't specified force
if is_downgrade(sql_file_path, verbose=True) and not force:
warn_message = (
"This is not recommended and may lead to unexpected behaviour. "
"Do you want to continue anyway?"
)
click.confirm(warn_message, abort=True)
# Validate the sql file
validate_database_sql(sql_file_path, _raise=not force)
try:
_new_site(
frappe.conf.db_name,
@ -258,53 +293,15 @@ def _restore(
admin_password=admin_password,
verbose=verbose,
install_apps=install_app,
source_sql=decompressed_file_name,
source_sql=sql_file_path,
force=True,
db_type=frappe.conf.db_type,
)
except Exception as err:
print(err.args[1])
_backup.decryption_rollback()
sys.exit(1)
# Removing temporarily created file
if decompressed_file_name != sql_file_path:
os.remove(decompressed_file_name)
_backup.decryption_rollback()
# Extract public and/or private files to the restored site, if user has given the path
if with_public_files:
# Decrypt data if there is a Key
if encryption_key:
_backup = Backup(with_public_files)
_backup.backup_decryption(encryption_key)
if not os.path.exists(with_public_files):
_backup.decryption_rollback()
public = extract_files(site, with_public_files)
# Removing temporarily created file
os.remove(public)
_backup.decryption_rollback()
if with_private_files:
# Decrypt data if there is a Key
if encryption_key:
_backup = Backup(with_private_files)
_backup.backup_decryption(encryption_key)
if not os.path.exists(with_private_files):
_backup.decryption_rollback()
private = extract_files(site, with_private_files)
# Removing temporarily created file
os.remove(private)
_backup.decryption_rollback()
success_message = "Site {} has been restored{}".format(
site, " with files" if (with_public_files or with_private_files) else ""
)
click.secho(success_message, fg="green")
@click.command("partial-restore")
@click.argument("sql-file-path")
@ -312,38 +309,23 @@ def _restore(
@click.option("--encryption-key", help="Backup encryption key")
@pass_context
def partial_restore(context, sql_file_path, verbose, encryption_key=None):
from frappe.installer import extract_sql_from_archive, partial_restore
from frappe.utils.backups import Backup, get_or_generate_backup_encryption_key
from frappe.installer import is_partial, partial_restore
from frappe.utils.backups import decrypt_backup, get_or_generate_backup_encryption_key
if not os.path.exists(sql_file_path):
print("Invalid path", sql_file_path)
sys.exit(1)
site = get_site(context)
frappe.init(site=site)
_backup = Backup(sql_file_path)
verbose = context.verbose or verbose
frappe.init(site=site)
frappe.connect(site=site)
try:
decompressed_file_name = extract_sql_from_archive(sql_file_path)
err, out = frappe.utils.execute_in_shell(f"file {sql_file_path}", check_exit_code=True)
if err:
click.secho("Failed to detect type of backup file", fg="red")
sys.exit(1)
with open(decompressed_file_name) as f:
header = " ".join(f.readline() for _ in range(5))
# Check for full backup file
if "Partial Backup" not in header:
click.secho(
"Full backup file detected.Use `bench restore` to restore a Frappe Site.",
fg="red",
)
_backup.decryption_rollback()
sys.exit(1)
except UnicodeDecodeError:
_backup.decryption_rollback()
if "cipher" in out.decode().split(":")[-1].strip():
if encryption_key:
click.secho("Encrypted backup file detected. Decrypting using provided key.", fg="yellow")
key = encryption_key
@ -352,35 +334,30 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None):
click.secho("Encrypted backup file detected. Decrypting using site config.", fg="yellow")
key = get_or_generate_backup_encryption_key()
_backup.backup_decryption(key)
# Rollback on unsuccessful decryrption
if not os.path.exists(sql_file_path):
click.secho("Decryption failed. Please provide a valid key and try again.", fg="red")
_backup.decryption_rollback()
sys.exit(1)
decompressed_file_name = extract_sql_from_archive(sql_file_path)
with open(decompressed_file_name) as f:
header = " ".join(f.readline() for _ in range(5))
# Check for Full backup file.
if "Partial Backup" not in header:
with decrypt_backup(sql_file_path, key):
if not is_partial(sql_file_path):
click.secho(
"Full Backup file detected.Use `bench restore` to restore a Frappe Site.",
"Full backup file detected.Use `bench restore` to restore a Frappe Site.",
fg="red",
)
_backup.decryption_rollback()
sys.exit(1)
partial_restore(sql_file_path, verbose)
partial_restore(sql_file_path, verbose)
# Removing temporarily created file
_backup.decryption_rollback()
if os.path.exists(sql_file_path.rstrip(".gz")):
os.remove(sql_file_path.rstrip(".gz"))
# Rollback on unsuccessful decryption
if not os.path.exists(sql_file_path):
click.secho("Decryption failed. Please provide a valid key and try again.", fg="red")
sys.exit(1)
else:
if not is_partial(sql_file_path):
click.secho(
"Full backup file detected.Use `bench restore` to restore a Frappe Site.",
fg="red",
)
sys.exit(1)
partial_restore(sql_file_path, verbose)
frappe.destroy()
@ -413,7 +390,6 @@ def _reinstall(
verbose=False,
):
from frappe.installer import _new_site
from frappe.utils.synchronization import filelock
if not yes:
click.confirm("This will wipe your database. Are you sure you want to reinstall?", abort=True)
@ -525,6 +501,130 @@ def list_apps(context, format):
click.echo(frappe.as_json(summary_dict))
@click.command("add-database-index")
@click.option("--doctype", help="DocType on which index needs to be added")
@click.option(
"--column",
multiple=True,
help="Column to index. Multiple columns will create multi-column index in given order. To create a multiple, single column index, execute the command multiple times.",
)
@pass_context
def add_db_index(context, doctype, column):
"Adds a new DB index and creates a property setter to persist it."
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
columns = column # correct naming
for site in context.sites:
frappe.connect(site=site)
try:
frappe.db.add_index(doctype, columns)
if len(columns) == 1:
make_property_setter(
doctype,
columns[0],
property="search_index",
value="1",
property_type="Check",
for_doctype=False, # Applied on docfield
)
frappe.db.commit()
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("describe-database-table")
@click.option("--doctype", help="DocType to describe")
@click.option(
"--column",
multiple=True,
help="Explicitly fetch accurate cardinality from table data. This can be quite slow on large tables.",
)
@pass_context
def describe_database_table(context, doctype, column):
"""Describes various statistics about the table.
This is useful to build integration like
This includes:
1. Schema
2. Indexes
3. stats - total count of records
4. if column is specified then extra stats are generated for column:
Distinct values count in column
"""
import json
for site in context.sites:
frappe.connect(site=site)
try:
data = _extract_table_stats(doctype, column)
# NOTE: Do not print anything else in this to avoid clobbering the output.
print(json.dumps(data, indent=2))
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
def _extract_table_stats(doctype: str, columns: list[str]) -> dict:
from frappe.utils import cstr, get_table_name
def sql_bool(val):
return cstr(val).lower() in ("yes", "1", "true")
table = get_table_name(doctype, wrap_in_backticks=True)
schema = []
for field in frappe.db.sql(f"describe {table}", as_dict=True):
schema.append(
{
"column": field["Field"],
"type": field["Type"],
"is_nullable": sql_bool(field["Null"]),
"default": field["Default"],
}
)
def update_cardinality(column, value):
for col in schema:
if col["column"] == column:
col["cardinality"] = value
break
indexes = []
for idx in frappe.db.sql(f"show index from {table}", as_dict=True):
indexes.append(
{
"unique": not sql_bool(idx["Non_unique"]),
"cardinality": idx["Cardinality"],
"name": idx["Key_name"],
"sequence": idx["Seq_in_index"],
"nullable": sql_bool(idx["Null"]),
"column": idx["Column_name"],
"type": idx["Index_type"],
}
)
if idx["Seq_in_index"] == 1:
update_cardinality(idx["Column_name"], idx["Cardinality"])
total_rows = frappe.db.count(doctype)
# fetch accurate cardinality for columns by query. WARN: This can take a lot of time.
for column in columns:
cardinality = frappe.db.sql(f"select count(distinct {column}) from {table}")[0][0]
update_cardinality(column, cardinality)
return {
"table_name": table.strip("`"),
"total_rows": total_rows,
"schema": schema,
"indexes": indexes,
}
@click.command("add-system-manager")
@click.argument("email")
@click.option("--first-name")
@ -742,6 +842,9 @@ def use(site, sites_path="."):
)
@click.option("--verbose", default=False, is_flag=True, help="Add verbosity")
@click.option("--compress", default=False, is_flag=True, help="Compress private and public files")
@click.option(
"--old-backup-metadata", default=False, is_flag=True, help="Use older backup metadata"
)
@pass_context
def backup(
context,
@ -756,6 +859,7 @@ def backup(
compress=False,
include="",
exclude="",
old_backup_metadata=False,
):
"Backup"
@ -781,6 +885,7 @@ def backup(
compress=compress,
verbose=verbose,
force=True,
old_backup_metadata=old_backup_metadata,
)
except Exception:
click.secho(
@ -951,9 +1056,9 @@ def move(dest_dir, site):
site_dump_exists = True
count = 0
while site_dump_exists:
final_new_path = new_path + (count and str(count) or "")
final_new_path = new_path + str(count or "")
site_dump_exists = os.path.exists(final_new_path)
count = int(count or 0) + 1
count += 1
shutil.move(old_path, final_new_path)
frappe.destroy()
@ -1290,7 +1395,7 @@ def trim_database(context, dry_run, format, no_backup, yes=False):
for table_name in database_tables:
if not table_name.startswith("tab"):
continue
if not (table_name.replace("tab", "", 1) in doctype_tables or table_name in STANDARD_TABLES):
if table_name.replace("tab", "", 1) not in doctype_tables and table_name not in STANDARD_TABLES:
TABLES_TO_DROP.append(table_name)
if not TABLES_TO_DROP:
@ -1437,6 +1542,8 @@ def add_new_user(
commands = [
add_system_manager,
add_user_for_sites,
add_db_index,
describe_database_table,
backup,
drop_site,
install_app,

View file

@ -143,7 +143,7 @@ def get_preferred_address(doctype, name, preferred_key="is_primary_address"):
def get_default_address(
doctype: str, name: str | None, sort_key: str = "is_primary_address"
) -> str | None:
"""Returns default Address name for the given doctype, name"""
"""Return default Address name for the given doctype, name."""
if sort_key not in ["is_shipping_address", "is_primary_address"]:
return None
@ -228,7 +228,7 @@ def get_address_list(doctype, txt, filters, limit_start, limit_page_length=20, o
def has_website_permission(doc, ptype, user, verbose=False):
"""Returns true if there is a related lead or contact related to this document"""
"""Return True if there is a related lead or contact related to this document."""
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
if contact_name:

View file

@ -257,7 +257,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-02 12:00:27.299156",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Contact",
@ -392,7 +392,7 @@
],
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"title_field": "full_name"
}

View file

@ -50,14 +50,14 @@ class Contact(Document):
def autoname(self):
self.name = self._get_full_name()
if frappe.db.exists("Contact", self.name):
self.name = append_number_if_name_exists("Contact", self.name)
# concat party name if reqd
for link in self.links:
self.name = self.name + "-" + link.link_name.strip()
break
if frappe.db.exists("Contact", self.name):
self.name = append_number_if_name_exists("Contact", self.name)
def validate(self):
self.full_name = self._get_full_name()
self.set_primary_email()
@ -168,7 +168,7 @@ class Contact(Document):
def get_default_contact(doctype, name):
"""Returns default contact for the given doctype, name"""
"""Return default contact for the given doctype, name."""
out = frappe.db.sql(
"""select parent,
IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0)

View file

@ -15,8 +15,7 @@ def unzip_file(name: str):
@frappe.whitelist()
def get_attached_images(doctype: str, names: list[str] | str) -> frappe._dict:
"""get list of image urls attached in form
returns {name: ['image.jpg', 'image.png']}"""
"""Return list of image urls attached in form `{name: ['image.jpg', 'image.png']}`."""
if isinstance(names, str):
names = json.loads(names)

View file

@ -31,6 +31,7 @@
"additional_info",
"communication_date",
"read_receipt",
"send_after",
"column_break_14",
"sender_full_name",
"read_by_recipient",
@ -125,7 +126,7 @@
"fieldtype": "Select",
"hidden": 1,
"label": "Delivery Status",
"options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed\nError\nExpired\nSending\nRead"
"options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed\nError\nExpired\nSending\nRead\nScheduled"
},
{
"fieldname": "section_break_8",
@ -390,12 +391,17 @@
"hidden": 1,
"label": "IMAP Folder",
"read_only": 1
},
{
"fieldname": "send_after",
"fieldtype": "Datetime",
"label": "Send After"
}
],
"icon": "fa fa-comment",
"idx": 1,
"links": [],
"modified": "2023-08-29 17:20:52.541483",
"modified": "2023-11-27 20:38:27.467076",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",

View file

@ -87,6 +87,7 @@ class Communication(Document, CommunicationEmailMixin):
"Expired",
"Sending",
"Read",
"Scheduled",
]
email_account: DF.Link | None
email_status: DF.Literal["Open", "Spam", "Trash"]
@ -106,6 +107,7 @@ class Communication(Document, CommunicationEmailMixin):
reference_name: DF.DynamicLink | None
reference_owner: DF.ReadOnly | None
seen: DF.Check
send_after: DF.Datetime | None
sender: DF.Data | None
sender_full_name: DF.Data | None
sent_or_received: DF.Literal["Sent", "Received"]
@ -162,6 +164,9 @@ class Communication(Document, CommunicationEmailMixin):
self.seen = 1
self.sent_or_received = "Sent"
if not self.send_after: # Handle empty string, always set NULL
self.send_after = None
validate_email(self)
if self.communication_medium == "Email":
@ -293,7 +298,7 @@ class Communication(Document, CommunicationEmailMixin):
@staticmethod
def _get_emails_list(emails=None, exclude_displayname=False):
"""Returns list of emails from given email string.
"""Return list of emails from given email string.
* Removes duplicate mailids
* Removes display name from email address if exclude_displayname is True
@ -304,15 +309,15 @@ class Communication(Document, CommunicationEmailMixin):
return [email.lower() for email in set(emails) if email]
def to_list(self, exclude_displayname=True):
"""Returns to list."""
"""Return `to` list."""
return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname)
def cc_list(self, exclude_displayname=True):
"""Returns cc list."""
"""Return `cc` list."""
return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname)
def bcc_list(self, exclude_displayname=True):
"""Returns bcc list."""
"""Return `bcc` list."""
return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname)
def get_attachments(self):
@ -342,6 +347,9 @@ class Communication(Document, CommunicationEmailMixin):
else:
self.status = "Closed"
if self.send_after and self.is_new():
self.delivery_status = "Scheduled"
def mark_email_as_spam(self):
if (
self.communication_type == "Communication"
@ -430,7 +438,7 @@ class Communication(Document, CommunicationEmailMixin):
frappe.db.commit()
def parse_email_for_timeline_links(self):
if not frappe.db.get_value("Email Account", self.email_account, "enable_automatic_linking"):
if not frappe.db.get_value("Email Account", filters={"enable_automatic_linking": 1}):
return
for doctype, docname in parse_email([self.recipients, self.cc, self.bcc]):
@ -607,9 +615,9 @@ def parse_email(email_strings):
def get_email_without_link(email):
"""
returns email address without doctype links
returns admin@example.com for email admin+doctype+docname@example.com
"""Return email address without doctype links.
e.g. 'admin@example.com' is returned for email 'admin+doctype+docname@example.com'
"""
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
return email
@ -654,7 +662,10 @@ def update_parent_document_on_communication(doc):
def update_first_response_time(parent, communication):
if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"):
if is_system_user(communication.sender):
if (
is_system_user(communication.sender)
or frappe.get_cached_value("User", frappe.session.user, "user_type") == "System User"
):
if communication.sent_or_received == "Sent":
first_responded_on = communication.creation
if parent.meta.has_field("first_responded_on"):

View file

@ -46,6 +46,7 @@ def make(
print_letterhead=True,
email_template=None,
communication_type=None,
send_after=None,
**kwargs,
) -> dict[str, str]:
"""Make a new communication. Checks for email permissions for specified Document.
@ -64,6 +65,7 @@ def make(
:param attachments: List of File names or dicts with keys "fname" and "fcontent"
:param send_me_a_copy: Send a copy to the sender (default **False**).
:param email_template: Template which is used to compose mail .
:param send_after: Send after the given datetime.
"""
if kwargs:
from frappe.utils.commands import warn
@ -99,6 +101,7 @@ def make(
email_template=email_template,
communication_type=communication_type,
add_signature=False,
send_after=send_after,
)
@ -124,6 +127,7 @@ def _make(
email_template=None,
communication_type=None,
add_signature=True,
send_after=None,
) -> dict[str, str]:
"""Internal method to make a new communication that ignores Permission checks."""
@ -151,6 +155,7 @@ def _make(
"read_receipt": read_receipt,
"has_attachment": 1 if attachments else 0,
"communication_type": communication_type,
"send_after": send_after,
}
)
comm.flags.skip_add_signature = not add_signature
@ -186,7 +191,8 @@ def _make(
def validate_email(doc: "Communication") -> None:
"""Validate Email Addresses of Recipients and CC"""
if (
not (doc.communication_type == "Communication" and doc.communication_medium == "Email")
doc.communication_type != "Communication"
or doc.communication_medium != "Email"
or doc.flags.in_receive
):
return

View file

@ -1,6 +1,9 @@
import frappe
from frappe import _
from frappe.core.utils import get_parent_doc
from frappe.desk.doctype.notification_settings.notification_settings import (
is_email_notifications_enabled_for_type,
)
from frappe.desk.doctype.todo.todo import ToDo
from frappe.email.doctype.email_account.email_account import EmailAccount
from frappe.utils import get_formatted_email, get_url, parse_addr
@ -26,7 +29,7 @@ class CommunicationEmailMixin:
)
def get_email_with_displayname(self, email_address):
"""Returns email address after adding displayname."""
"""Return email address after adding displayname."""
display_name, email = parse_addr(email_address)
if display_name and display_name != email:
return email_address
@ -78,7 +81,12 @@ class CommunicationEmailMixin:
if doc_owner := self.get_owner():
cc.append(doc_owner)
cc = set(cc) - {self.sender_mailid}
cc.update(self.get_assignees())
assignees = set(self.get_assignees())
# Check and remove If user disabled notifications for incoming emails on assigned document.
for assignee in assignees.copy():
if not is_email_notifications_enabled_for_type(assignee, "threads_on_assigned_document"):
assignees.remove(assignee)
cc.update(assignees)
cc = set(cc) - set(self.filter_thread_notification_disbled_users(cc))
cc = cc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation))
@ -143,7 +151,7 @@ class CommunicationEmailMixin:
return self.content
def get_attach_link(self, print_format):
"""Returns public link for the attachment via `templates/emails/print_link.html`."""
"""Return public link for the attachment via `templates/emails/print_link.html`."""
return frappe.get_template("templates/emails/print_link.html").render(
{
"url": get_url(),
@ -288,8 +296,9 @@ class CommunicationEmailMixin:
"delayed": True,
"communication": self.name,
"read_receipt": self.read_receipt,
"is_notification": (self.sent_or_received == "Received" and True) or False,
"is_notification": (self.sent_or_received == "Received"),
"print_letterhead": print_letterhead,
"send_after": self.send_after,
}
def send_email(

View file

@ -136,47 +136,40 @@ frappe.ui.form.on("Data Import", {
let total_records = cint(r.message.total_records);
if (!total_records) return;
let action, message;
if (frm.doc.import_type === "Insert New Records") {
action = "imported";
} else {
action = "updated";
}
let message;
if (failed_records === 0) {
let message_args = [successful_records];
if (frm.doc.import_type === "Insert New Records") {
message =
successful_records > 1
? __("Successfully imported {0} records.", message_args)
: __("Successfully imported {0} record.", message_args);
let message_args = [action, successful_records];
if (successful_records === 1) {
message = __("Successfully {0} 1 record.", message_args);
} else {
message =
successful_records > 1
? __("Successfully updated {0} records.", message_args)
: __("Successfully updated {0} record.", message_args);
message = __("Successfully {0} {1} records.", message_args);
}
} else {
let message_args = [successful_records, total_records];
if (frm.doc.import_type === "Insert New Records") {
message =
successful_records > 1
? __(
"Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
message_args
)
: __(
"Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",
message_args
);
let message_args = [action, successful_records, total_records];
if (successful_records === 1) {
message = __(
"Successfully {0} {1} record out of {2}. Click on Export Errored Rows, fix the errors and import again.",
message_args
);
} else {
message =
successful_records > 1
? __(
"Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
message_args
)
: __(
"Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",
message_args
);
message = __(
"Successfully {0} {1} records out of {2}. Click on Export Errored Rows, fix the errors and import again.",
message_args
);
}
}
// If the job timed out, display an extra hint
if (r.message.status === "Timed Out") {
message += "<br/>" + __("Import timed out, please re-try.");
}
frm.dashboard.set_headline(message);
},
});

View file

@ -1,198 +1,198 @@
{
"actions": [],
"autoname": "format:{reference_doctype} Import on {creation}",
"beta": 1,
"creation": "2019-08-04 14:16:08.318714",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_doctype",
"import_type",
"download_template",
"import_file",
"payload_count",
"html_5",
"google_sheets_url",
"refresh_google_sheet",
"column_break_5",
"status",
"submit_after_import",
"mute_emails",
"template_options",
"import_warnings_section",
"template_warnings",
"import_warnings",
"section_import_preview",
"import_preview",
"import_log_section",
"show_failed_logs",
"import_log_preview"
],
"fields": [
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "import_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Import Type",
"options": "\nInsert New Records\nUpdate Existing Records",
"reqd": 1,
"set_only_once": 1
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "import_file",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Import File",
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
},
{
"fieldname": "import_preview",
"fieldtype": "HTML",
"label": "Import Preview"
},
{
"fieldname": "section_import_preview",
"fieldtype": "Section Break",
"label": "Preview"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "template_options",
"fieldtype": "Code",
"hidden": 1,
"label": "Template Options",
"options": "JSON",
"read_only": 1
},
{
"fieldname": "import_log_section",
"fieldtype": "Section Break",
"label": "Import Log"
},
{
"fieldname": "import_log_preview",
"fieldtype": "HTML",
"label": "Import Log Preview"
},
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"no_copy": 1,
"options": "Pending\nSuccess\nPartial Success\nError",
"read_only": 1
},
{
"fieldname": "template_warnings",
"fieldtype": "Code",
"hidden": 1,
"label": "Template Warnings",
"options": "JSON"
},
{
"default": "0",
"fieldname": "submit_after_import",
"fieldtype": "Check",
"label": "Submit After Import",
"set_only_once": 1
},
{
"fieldname": "import_warnings_section",
"fieldtype": "Section Break",
"label": "Import File Errors and Warnings"
},
{
"fieldname": "import_warnings",
"fieldtype": "HTML",
"label": "Import Warnings"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "download_template",
"fieldtype": "Button",
"label": "Download Template"
},
{
"default": "1",
"fieldname": "mute_emails",
"fieldtype": "Check",
"label": "Don't Send Emails",
"set_only_once": 1
},
{
"default": "0",
"fieldname": "show_failed_logs",
"fieldtype": "Check",
"label": "Show Failed Logs"
},
{
"depends_on": "eval:!doc.__islocal && !doc.import_file",
"fieldname": "html_5",
"fieldtype": "HTML",
"options": "<h5 class=\"text-muted uppercase\">Or</h5>"
},
{
"depends_on": "eval:!doc.__islocal && !doc.import_file\n",
"description": "Must be a publicly accessible Google Sheets URL",
"fieldname": "google_sheets_url",
"fieldtype": "Data",
"label": "Import from Google Sheets",
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
},
{
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)",
"fieldname": "refresh_google_sheet",
"fieldtype": "Button",
"label": "Refresh Google Sheet"
},
{
"fieldname": "payload_count",
"fieldtype": "Int",
"hidden": 1,
"label": "Payload Count",
"read_only": 1
}
],
"hide_toolbar": 1,
"links": [],
"modified": "2022-02-14 10:08:37.624914",
"modified_by": "Administrator",
"module": "Core",
"name": "Data Import",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
"actions": [],
"autoname": "format:{reference_doctype} Import on {creation}",
"beta": 1,
"creation": "2019-08-04 14:16:08.318714",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_doctype",
"import_type",
"download_template",
"import_file",
"payload_count",
"html_5",
"google_sheets_url",
"refresh_google_sheet",
"column_break_5",
"status",
"submit_after_import",
"mute_emails",
"template_options",
"import_warnings_section",
"template_warnings",
"import_warnings",
"section_import_preview",
"import_preview",
"import_log_section",
"show_failed_logs",
"import_log_preview"
],
"fields": [
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "import_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Import Type",
"options": "\nInsert New Records\nUpdate Existing Records",
"reqd": 1,
"set_only_once": 1
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "import_file",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Import File",
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
},
{
"fieldname": "import_preview",
"fieldtype": "HTML",
"label": "Import Preview"
},
{
"fieldname": "section_import_preview",
"fieldtype": "Section Break",
"label": "Preview"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "template_options",
"fieldtype": "Code",
"hidden": 1,
"label": "Template Options",
"options": "JSON",
"read_only": 1
},
{
"fieldname": "import_log_section",
"fieldtype": "Section Break",
"label": "Import Log"
},
{
"fieldname": "import_log_preview",
"fieldtype": "HTML",
"label": "Import Log Preview"
},
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"no_copy": 1,
"options": "Pending\nSuccess\nPartial Success\nError\nTimed Out",
"read_only": 1
},
{
"fieldname": "template_warnings",
"fieldtype": "Code",
"hidden": 1,
"label": "Template Warnings",
"options": "JSON"
},
{
"default": "0",
"fieldname": "submit_after_import",
"fieldtype": "Check",
"label": "Submit After Import",
"set_only_once": 1
},
{
"fieldname": "import_warnings_section",
"fieldtype": "Section Break",
"label": "Import File Errors and Warnings"
},
{
"fieldname": "import_warnings",
"fieldtype": "HTML",
"label": "Import Warnings"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "download_template",
"fieldtype": "Button",
"label": "Download Template"
},
{
"default": "1",
"fieldname": "mute_emails",
"fieldtype": "Check",
"label": "Don't Send Emails",
"set_only_once": 1
},
{
"default": "0",
"fieldname": "show_failed_logs",
"fieldtype": "Check",
"label": "Show Failed Logs"
},
{
"depends_on": "eval:!doc.__islocal && !doc.import_file",
"fieldname": "html_5",
"fieldtype": "HTML",
"options": "<h5 class=\"text-muted uppercase\">Or</h5>"
},
{
"depends_on": "eval:!doc.__islocal && !doc.import_file\n",
"description": "Must be a publicly accessible Google Sheets URL",
"fieldname": "google_sheets_url",
"fieldtype": "Data",
"label": "Import from Google Sheets",
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
},
{
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)",
"fieldname": "refresh_google_sheet",
"fieldtype": "Button",
"label": "Refresh Google Sheet"
},
{
"fieldname": "payload_count",
"fieldtype": "Int",
"hidden": 1,
"label": "Payload Count",
"read_only": 1
}
],
"hide_toolbar": 1,
"links": [],
"modified": "2023-12-15 12:45:49.452834",
"modified_by": "Administrator",
"module": "Core",
"name": "Data Import",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -3,6 +3,8 @@
import os
from rq.timeouts import JobTimeoutException
import frappe
from frappe import _
from frappe.core.doctype.data_import.exporter import Exporter
@ -32,11 +34,13 @@ class DataImport(Document):
payload_count: DF.Int
reference_doctype: DF.Link
show_failed_logs: DF.Check
status: DF.Literal["Pending", "Success", "Partial Success", "Error"]
status: DF.Literal["Pending", "Success", "Partial Success", "Error", "Timed Out"]
submit_after_import: DF.Check
template_options: DF.Code | None
template_warnings: DF.Code | None
# end: auto-generated types
def validate(self):
doc_before_save = self.get_doc_before_save()
if (
@ -136,6 +140,9 @@ def start_import(data_import):
try:
i = Importer(data_import.reference_doctype, data_import=data_import)
i.import_data()
except JobTimeoutException:
frappe.db.rollback()
data_import.db_set("status", "Timed Out")
except Exception:
frappe.db.rollback()
data_import.db_set("status", "Error")
@ -190,6 +197,9 @@ def download_import_log(data_import_name):
def get_import_status(data_import_name):
import_status = {}
data_import = frappe.get_doc("Data Import", data_import_name)
import_status["status"] = data_import.status
logs = frappe.get_all(
"Data Import Log",
fields=["count(*) as count", "success"],

View file

@ -20,13 +20,14 @@ frappe.listview_settings["Data Import"] = {
Success: "green",
"In Progress": "orange",
Error: "red",
"Timed Out": "orange",
};
let status = doc.status;
if (imports_in_progress.includes(doc.name)) {
status = "In Progress";
}
if (status == "Pending") {
if (status === "Pending") {
status = "Not Started";
}

View file

@ -144,8 +144,7 @@ class Exporter:
value = doc.get(df.fieldname, None)
if df.fieldtype == "Duration":
value = flt(value or 0)
value = format_duration(value, df.hide_days)
value = format_duration(flt(value), df.hide_days)
row[i] = value
return rows

View file

@ -179,7 +179,7 @@ class Importer:
log_index += 1
if not self.data_import.status == "Partial Success":
if self.data_import.status != "Partial Success":
self.data_import.db_set("status", "Partial Success")
# commit after every successful import
@ -514,8 +514,8 @@ class ImportFile:
def parse_next_row_for_import(self, data):
"""
Parses rows that make up a doc. A doc maybe built from a single row or multiple rows.
Returns the doc, rows, and data without the rows.
Parse rows that make up a doc. A doc maybe built from a single row or multiple rows.
Return the doc, rows, and data without the rows.
"""
doctypes = self.header.doctypes

View file

@ -27,6 +27,14 @@ class DeletedDocument(Document):
# end: auto-generated types
pass
@staticmethod
def clear_old_logs(days=180):
from frappe.query_builder import Interval
from frappe.query_builder.functions import Now
table = frappe.qb.DocType("Deleted Document")
frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
@frappe.whitelist()
def restore(name, alert=True):

View file

@ -118,9 +118,10 @@ class DocField(Document):
width: DF.Data | None
# end: auto-generated types
def get_link_doctype(self):
"""Returns the Link doctype for the docfield (if applicable)
if fieldtype is Link: Returns "options"
if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table
"""Return the Link doctype for the `docfield` (if applicable).
* If fieldtype is Link: Return "options".
* If fieldtype is Table MultiSelect: Return "options" of the Link field in the Child Table.
"""
if self.fieldtype == "Link":
return self.options

View file

@ -3,7 +3,7 @@
frappe.ui.form.on("DocType", {
onload: function (frm) {
if (frm.is_new()) {
if (frm.is_new() && !frm.doc?.fields) {
frappe.listview_settings["DocType"].new_doctype_dialog();
}
},

View file

@ -68,6 +68,7 @@
"column_break_51",
"email_append_to",
"sender_field",
"sender_name_field",
"subject_field",
"sb2",
"permissions",
@ -520,7 +521,7 @@
"depends_on": "email_append_to",
"fieldname": "sender_field",
"fieldtype": "Data",
"label": "Sender Field",
"label": "Sender Email Field",
"mandatory_depends_on": "email_append_to"
},
{
@ -661,6 +662,12 @@
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"depends_on": "email_append_to",
"fieldname": "sender_name_field",
"fieldtype": "Data",
"label": "Sender Name Field"
}
],
"icon": "fa fa-bolt",
@ -743,7 +750,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2023-11-01 16:45:14.960949",
"modified": "2023-12-01 18:37:16.799471",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -162,6 +162,7 @@ class DocType(Document):
route: DF.Data | None
search_fields: DF.Data | None
sender_field: DF.Data | None
sender_name_field: DF.Data | None
show_name_in_global_search: DF.Check
show_preview_popup: DF.Check
show_title_field_in_link: DF.Check
@ -177,6 +178,7 @@ class DocType(Document):
translated_doctype: DF.Check
website_search_field: DF.Data | None
# end: auto-generated types
def validate(self):
"""Validate DocType before saving.
@ -290,7 +292,7 @@ class DocType(Document):
if not [d.fieldname for d in self.fields if d.in_list_view]:
cnt = 0
for d in self.fields:
if d.reqd and not d.hidden and not d.fieldtype in not_allowed_in_list_view:
if d.reqd and not d.hidden and d.fieldtype not in not_allowed_in_list_view:
d.in_list_view = 1
cnt += 1
if cnt == 4:
@ -305,7 +307,7 @@ class DocType(Document):
def check_indexing_for_dashboard_links(self):
"""Enable indexing for outgoing links used in dashboard"""
for d in self.fields:
if d.fieldtype == "Link" and not (d.unique or d.search_index):
if d.fieldtype == "Link" and not d.unique and not d.search_index:
referred_as_link = frappe.db.exists(
"DocType Link",
{"parent": d.options, "link_doctype": self.name, "link_fieldname": d.fieldname},
@ -412,7 +414,7 @@ class DocType(Document):
if self.has_web_view:
# route field must be present
if not "route" in [d.fieldname for d in self.fields]:
if "route" not in [d.fieldname for d in self.fields]:
frappe.throw(_('Field "route" is mandatory for Web Views'), title="Missing Field")
# clear website cache
@ -984,7 +986,7 @@ class DocType(Document):
add_column(self.name, "parentfield", "Data")
def get_max_idx(self):
"""Returns the highest `idx`"""
"""Return the highest `idx`."""
max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""", self.name)
return max_idx and max_idx[0][0] or 0
@ -1265,7 +1267,7 @@ def validate_fields(meta):
),
WrongOptionsDoctypeLinkError,
)
elif not (options == d.options):
elif options != d.options:
frappe.throw(
_("{0}: Options {1} must be the same as doctype name {2} for the field {3}").format(
docname, d.options, options, d.label
@ -1513,7 +1515,7 @@ def validate_fields(meta):
def check_table_multiselect_option(docfield):
"""check if the doctype provided in Option has atleast 1 Link field"""
if not docfield.fieldtype == "Table MultiSelect":
if docfield.fieldtype != "Table MultiSelect":
return
doctype = docfield.options
@ -1579,7 +1581,7 @@ def validate_fields(meta):
title=_("Invalid Option"),
)
if not (meta.is_virtual == child_doctype_meta.is_virtual):
if meta.is_virtual != child_doctype_meta.is_virtual:
error_msg = " should be virtual." if meta.is_virtual else " cannot be virtual."
frappe.throw(
_("Child Table {0} for field {1}" + error_msg).format(
@ -1666,22 +1668,12 @@ def validate_permissions_for_doctype(doctype, for_remove=False, alert=False):
def clear_permissions_cache(doctype):
from frappe.cache_manager import clear_user_cache
frappe.clear_cache(doctype=doctype)
delete_notification_count_for(doctype)
for user in frappe.db.sql_list(
"""
SELECT
DISTINCT `tabHas Role`.`parent`
FROM
`tabHas Role`,
`tabDocPerm`
WHERE `tabDocPerm`.`parent` = %s
AND `tabDocPerm`.`role` = `tabHas Role`.`role`
AND `tabHas Role`.`parenttype` = 'User'
""",
doctype,
):
frappe.clear_cache(user=user)
clear_user_cache()
def validate_permissions(doctype, for_remove=False, alert=False):
@ -1891,7 +1883,7 @@ def check_email_append_to(doc):
if doc.sender_field and not sender_field:
frappe.throw(_("Select a valid Sender Field for creating documents from Email"))
if not sender_field.options == "Email":
if sender_field.options != "Email":
frappe.throw(_("Sender Field should have Email in options"))

View file

@ -786,6 +786,7 @@ def new_doctype(
depends_on: str = "",
fields: list[dict] | None = None,
custom: bool = True,
default: str | None = None,
**kwargs,
):
if not name:
@ -803,6 +804,7 @@ def new_doctype(
"fieldname": "some_fieldname",
"fieldtype": "Data",
"unique": unique,
"default": default,
"depends_on": depends_on,
}
],

View file

@ -35,6 +35,7 @@
{
"fieldname": "prefix",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Prefix",
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"",
"reqd": 1
@ -44,6 +45,7 @@
"description": "Warning: Updating counter may lead to document name conflicts if not done properly",
"fieldname": "counter",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Counter",
"no_copy": 1
},
@ -78,6 +80,7 @@
"description": "Rules with higher priority number will be applied first.",
"fieldname": "priority",
"fieldtype": "Int",
"in_standard_filter": 1,
"label": "Priority"
},
{
@ -87,7 +90,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-04-24 15:14:32.054272",
"modified": "2023-11-21 11:58:25.712375",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule",
@ -107,7 +110,7 @@
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_field": "priority",
"sort_order": "DESC",
"states": [],
"title_field": "document_type",

View file

@ -0,0 +1,3 @@
frappe.listview_settings["Document Naming Rule"] = {
hide_name_column: true,
};

View file

@ -4,6 +4,7 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document
from frappe.utils import cint
class Domain(Document):
@ -28,7 +29,7 @@ class Domain(Document):
self.setup_properties()
self.set_values()
if not int(frappe.defaults.get_defaults().setup_complete or 0):
if not cint(frappe.defaults.get_defaults().setup_complete):
# if setup not complete, setup desktop etc.
self.setup_sidebar_items()
self.set_default_portal_role()

View file

@ -21,7 +21,7 @@ class DomainSettings(Document):
active_domains = [d.domain for d in self.active_domains]
added = False
for d in domains:
if not d in active_domains:
if d not in active_domains:
self.append("active_domains", dict(domain=d))
added = True

View file

@ -32,7 +32,7 @@ def deduplicate_dynamic_links(doc):
links, duplicate = [], False
for l in doc.links or []:
t = (l.link_doctype, l.link_name)
if not t in links:
if t not in links:
links.append(t)
else:
duplicate = True

View file

@ -70,7 +70,7 @@
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2023-08-23 14:20:15.343339",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Core",
"name": "Error Log",
@ -89,7 +89,7 @@
}
],
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"title_field": "method"
}

View file

@ -189,7 +189,7 @@
"icon": "fa fa-file",
"idx": 1,
"links": [],
"modified": "2023-08-02 09:43:51.178012",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Core",
"name": "File",
@ -217,7 +217,7 @@
}
],
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"title_field": "file_name",
"track_changes": 1

View file

@ -544,7 +544,7 @@ class File(Document):
return self._content
def get_full_path(self):
"""Returns file path from given file name"""
"""Return file path using the set file name."""
file_path = self.file_url or self.file_name
@ -705,7 +705,7 @@ class File(Document):
return has_permission(self, "read")
def get_extension(self):
"""returns split filename and extension"""
"""Split and return filename and extension for the set `file_name`."""
return os.path.splitext(self.file_name)
def create_attachment_record(self):

View file

@ -184,7 +184,7 @@ def remove_file_by_url(file_url: str, doctype: str = None, name: str = None) ->
def get_content_hash(content: bytes | str) -> str:
if isinstance(content, str):
content = content.encode()
return hashlib.md5(content).hexdigest() # nosec
return hashlib.md5(content, usedforsecurity=False).hexdigest() # nosec
def generate_file_name(name: str, suffix: str | None = None, is_private: bool = False) -> str:

View file

@ -10,20 +10,6 @@ from frappe.model.document import Document
from frappe.utils import cint
from frappe.utils.caching import site_cache
DEFAULT_LOGTYPES_RETENTION = {
"Error Log": 30,
"Activity Log": 90,
"Email Queue": 30,
"Scheduled Job Log": 90,
"Route History": 90,
"Submission Queue": 30,
"Prepared Report": 30,
"Webhook Request Log": 30,
"Integration Request": 90,
"Unhandled Email": 30,
"Reminder": 30,
}
@runtime_checkable
class LogType(Protocol):
@ -81,12 +67,14 @@ class LogSettings(Document):
def add_default_logtypes(self):
existing_logtypes = {d.ref_doctype for d in self.logs_to_clear}
added_logtypes = set()
for logtype, retention in DEFAULT_LOGTYPES_RETENTION.items():
default_logtypes_retention = frappe.get_hooks("default_log_clearing_doctypes", {})
for logtype, retentions in default_logtypes_retention.items():
if logtype not in existing_logtypes and _supports_log_clearing(logtype):
if not frappe.db.exists("DocType", logtype):
continue
self.append("logs_to_clear", {"ref_doctype": logtype, "days": cint(retention)})
self.append("logs_to_clear", {"ref_doctype": logtype, "days": cint(retentions[-1])})
added_logtypes.add(logtype)
if added_logtypes:

View file

@ -123,7 +123,7 @@
"link_fieldname": "module"
}
],
"modified": "2022-01-03 13:56:52.817954",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Def",
@ -160,7 +160,7 @@
],
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -47,7 +47,7 @@ class ModuleDef(Document):
if not frappe.local.module_app.get(frappe.scrub(self.name)):
with open(frappe.get_app_path(self.app_name, "modules.txt")) as f:
content = f.read()
if not self.name in content.splitlines():
if self.name not in content.splitlines():
modules = list(filter(None, content.splitlines()))
modules.append(self.name)

View file

@ -102,7 +102,7 @@
"icon": "fa fa-file",
"idx": 1,
"links": [],
"modified": "2023-10-22 22:41:25.568952",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Core",
"name": "Page",
@ -129,7 +129,7 @@
}
],
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -118,7 +118,7 @@ class Page(Document):
shutil.rmtree(dir_path, ignore_errors=True)
def is_permitted(self):
"""Returns true if Has Role is not set or the user is allowed."""
"""Return True if `Has Role` is not set or the user is allowed."""
from frappe.utils import has_common
allowed = [

View file

@ -4,7 +4,7 @@
import frappe
from frappe.model.document import Document
from frappe.recorder import get as get_recorder_data
from frappe.utils import cint, evaluate_filters, make_filter_dict
from frappe.utils import cint, evaluate_filters
class Recorder(Document):
@ -27,6 +27,7 @@ class Recorder(Document):
sql_queries: DF.Table[RecorderQuery]
time: DF.Datetime | None
time_in_queries: DF.Float
# end: auto-generated types
def load_from_db(self):
@ -38,7 +39,7 @@ class Recorder(Document):
@staticmethod
def get_list(args):
start = cint(args.get("start")) or 0
start = cint(args.get("start"))
page_length = cint(args.get("page_length")) or 20
requests = Recorder.get_filtered_requests(args)[start : start + page_length]

View file

@ -62,6 +62,8 @@
"reqd": 1
},
{
"fetch_from": "ref_doctype.module",
"fetch_if_empty": 1,
"fieldname": "module",
"fieldtype": "Link",
"label": "Module",

View file

@ -45,6 +45,7 @@ class Report(Document):
report_script: DF.Code | None
report_type: DF.Literal["Report Builder", "Query Report", "Script Report", "Custom Report"]
roles: DF.Table[HasRole]
# end: auto-generated types
def validate(self):
"""only administrator can save standard report"""
@ -86,7 +87,8 @@ class Report(Document):
if (
self.is_standard == "Yes"
and not cint(getattr(frappe.local.conf, "developer_mode", 0))
and not (frappe.flags.in_migrate or frappe.flags.in_patch)
and not frappe.flags.in_migrate
and not frappe.flags.in_patch
):
frappe.throw(_("You are not allowed to delete Standard Report"))
delete_custom_role("report", self.name)
@ -103,7 +105,7 @@ class Report(Document):
self.set("roles", roles)
def is_permitted(self):
"""Returns true if Has Role is not set or the user is allowed."""
"""Return True if `Has Role` is not set or the user is allowed."""
from frappe.utils import has_common
allowed = [
@ -129,7 +131,7 @@ class Report(Document):
if frappe.flags.in_import:
return
if self.is_standard == "Yes" and (frappe.local.conf.get("developer_mode") or 0) == 1:
if self.is_standard == "Yes" and frappe.conf.developer_mode:
export_to_files(
record_list=[["Report", self.name]], record_module=self.module, create_init=True
)
@ -155,7 +157,6 @@ class Report(Document):
def execute_script_report(self, filters):
# save the timestamp to automatically set to prepared
threshold = 15
res = []
start_time = datetime.datetime.now()
@ -183,7 +184,7 @@ class Report(Document):
def execute_script(self, filters):
# server script
loc = {"filters": frappe._dict(filters), "data": None, "result": None}
safe_exec(self.report_script, None, loc)
safe_exec(self.report_script, None, loc, script_filename=f"Report {self.name}")
if loc["data"]:
return loc["data"]
else:
@ -382,7 +383,7 @@ class Report(Document):
def is_prepared_report_enabled(report):
return cint(frappe.db.get_value("Report", report, "prepared_report")) or 0
return cint(frappe.db.get_value("Report", report, "prepared_report"))
def get_report_module_dotted_path(module, report_name):

View file

@ -148,7 +148,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-08-05 18:33:27.694065",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Core",
"name": "Role",
@ -169,7 +169,7 @@
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"track_changes": 1,
"translated_doctype": 1

View file

@ -80,12 +80,23 @@ class Role(Document):
if frappe.flags.in_install:
return
if self.has_value_changed("desk_access"):
for user_name in get_users(self.name):
user = frappe.get_doc("User", user_name)
user_type = user.user_type
user.set_system_user()
if user_type != user.user_type:
user.save()
self.update_user_type_on_change()
def update_user_type_on_change(self):
"""When desk access changes, all the users that have this role need to be re-evaluated"""
users_with_role = get_users(self.name)
# perf: Do not re-evaluate users who already have same desk access that this role permits.
role_user_type = "System User" if self.desk_access else "Website User"
users_with_same_user_type = frappe.get_all("User", {"user_type": role_user_type}, pluck="name")
for user_name in set(users_with_role) - set(users_with_same_user_type):
user = frappe.get_doc("User", user_name)
user_type = user.user_type
user.set_system_user()
if user_type != user.user_type:
user.save()
def get_info_based_on_role(role, field="email", ignore_permissions=False):

View file

@ -1,6 +1,8 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# License: MIT. See LICENSE
from collections import defaultdict
import frappe
from frappe.model.document import Document
@ -24,9 +26,24 @@ class RoleProfile(Document):
def on_update(self):
"""Changes in role_profile reflected across all its user"""
users = frappe.get_all("User", filters={"role_profile_name": self.name})
roles = [role.role for role in self.roles]
for d in users:
user = frappe.get_doc("User", d)
user.set("roles", [])
user.add_roles(*roles)
has_role = frappe.qb.DocType("Has Role")
user = frappe.qb.DocType("User")
all_current_roles = (
frappe.qb.from_(user)
.join(has_role)
.on(user.name == has_role.parent)
.where(user.role_profile_name == self.name)
.select(user.name, has_role.role)
).run()
user_roles = defaultdict(set)
for user, role in all_current_roles:
user_roles[user].add(role)
role_profile_roles = {role.role for role in self.roles}
for user, roles in user_roles.items():
if roles != role_profile_roles:
user = frappe.get_doc("User", user)
user.roles = []
user.add_roles(*role_profile_roles)

View file

@ -59,6 +59,7 @@ class RQJob(Document):
]
time_taken: DF.Duration | None
timeout: DF.Duration | None
# end: auto-generated types
def load_from_db(self):
try:
@ -79,7 +80,7 @@ class RQJob(Document):
@staticmethod
def get_list(args):
start = cint(args.get("start")) or 0
start = cint(args.get("start"))
page_length = cint(args.get("page_length")) or 20
order_desc = "desc" in args.get("order_by", "")
@ -151,6 +152,12 @@ def serialize_job(job: Job) -> frappe._dict:
if matches := re.match(r"<function (?P<func_name>.*) at 0x.*>", job_name):
job_name = matches.group("func_name")
exc_info = None
# Get exc_string from the job result if it exists
if job_result := job.latest_result():
exc_info = job_result.exc_string
return frappe._dict(
name=job.id,
job_id=job.id,
@ -160,7 +167,7 @@ def serialize_job(job: Job) -> frappe._dict:
started_at=convert_utc_to_system_timezone(job.started_at) if job.started_at else "",
ended_at=convert_utc_to_system_timezone(job.ended_at) if job.ended_at else "",
time_taken=(job.ended_at - job.started_at).total_seconds() if job.ended_at else "",
exc_info=job.exc_info,
exc_info=exc_info,
arguments=frappe.as_json(job.kwargs),
timeout=job.timeout,
creation=convert_utc_to_system_timezone(job.created_at),

View file

@ -4,6 +4,7 @@
import datetime
from contextlib import suppress
import pytz
from rq import Worker
import frappe
@ -33,6 +34,7 @@ class RQWorker(Document):
total_working_time: DF.Duration | None
utilization_percent: DF.Percent
worker_name: DF.Data | None
# end: auto-generated types
def load_from_db(self):
@ -46,7 +48,7 @@ class RQWorker(Document):
@staticmethod
def get_list(args):
start = cint(args.get("start")) or 0
start = cint(args.get("start"))
page_length = cint(args.get("page_length")) or 20
workers = get_workers()
@ -105,5 +107,7 @@ def serialize_worker(worker: Worker) -> frappe._dict:
def compute_utilization(worker: Worker) -> float:
with suppress(Exception):
total_time = (datetime.datetime.utcnow() - worker.birth_date).total_seconds()
total_time = (
datetime.datetime.now(pytz.UTC) - worker.birth_date.replace(tzinfo=pytz.UTC)
).total_seconds()
return worker.total_working_time / total_time * 100

View file

@ -2,7 +2,8 @@
# License: MIT. See LICENSE
import json
from datetime import datetime
from datetime import datetime, timedelta
from random import randint
import click
from croniter import croniter
@ -110,7 +111,12 @@ class ScheduledJobType(Document):
# immediately, even when it's meant to be daily.
# A dynamic fallback like current time might miss the scheduler interval and job will never start.
last_execution = get_datetime(self.last_execution or self.creation)
return croniter(self.cron_format, last_execution).get_next(datetime)
next_execution = croniter(self.cron_format, last_execution).get_next(datetime)
jitter = 0
if self.frequency in ("Hourly Long", "Daily Long"):
jitter = randint(1, 600)
return next_execution + timedelta(seconds=jitter)
def execute(self):
self.scheduler_log = None

View file

@ -128,14 +128,14 @@ class ServerScript(Document):
frappe.msgprint(str(e), title=_("Compilation warning"))
def execute_method(self) -> dict:
"""Specific to API endpoint Server Scripts
"""Specific to API endpoint Server Scripts.
Raises:
frappe.DoesNotExistError: If self.script_type is not API
frappe.PermissionError: If self.allow_guest is unset for API accessed by Guest user
Raise:
frappe.DoesNotExistError: If self.script_type is not API.
frappe.PermissionError: If self.allow_guest is unset for API accessed by Guest user.
Returns:
dict: Evaluates self.script with frappe.utils.safe_exec.safe_exec and returns the flags set in it's safe globals
Return:
dict: Evaluate self.script with frappe.utils.safe_exec.safe_exec and return the flags set in its safe globals.
"""
if self.enable_rate_limit:
@ -155,7 +155,12 @@ class ServerScript(Document):
Args:
doc (Document): Executes script with for a certain document's events
"""
safe_exec(self.script, _locals={"doc": doc}, restrict_commit_rollback=True)
safe_exec(
self.script,
_locals={"doc": doc},
restrict_commit_rollback=True,
script_filename=self.name,
)
def execute_scheduled_method(self):
"""Specific to Scheduled Jobs via Server Scripts
@ -166,30 +171,28 @@ class ServerScript(Document):
if self.script_type != "Scheduler Event":
raise frappe.DoesNotExistError
safe_exec(self.script)
safe_exec(self.script, script_filename=self.name)
def get_permission_query_conditions(self, user: str) -> list[str]:
"""Specific to Permission Query Server Scripts
"""Specific to Permission Query Server Scripts.
Args:
user (str): Takes user email to execute script and return list of conditions
user (str): Take user email to execute script and return list of conditions.
Returns:
list: Returns list of conditions defined by rules in self.script
Return:
list: Return list of conditions defined by rules in self.script.
"""
locals = {"user": user, "conditions": ""}
safe_exec(self.script, None, locals)
safe_exec(self.script, None, locals, script_filename=self.name)
if locals["conditions"]:
return locals["conditions"]
@frappe.whitelist()
def get_autocompletion_items(self):
"""Generates a list of a autocompletion strings from the context dict
"""Generate a list of autocompletion strings from the context dict
that is used while executing a Server Script.
Returns:
list: Returns list of autocompletion items.
For e.g., ["frappe.utils.cint", "frappe.get_all", ...]
e.g., ["frappe.utils.cint", "frappe.get_all", ...]
"""
def get_keys(obj):
@ -278,7 +281,7 @@ def execute_api_server_script(script=None, *args, **kwargs):
raise frappe.PermissionError
# output can be stored in flags
_globals, _locals = safe_exec(script.script)
_globals, _locals = safe_exec(script.script, script_filename=script.name)
return _globals.frappe.flags

View file

@ -23,7 +23,7 @@ EVENT_MAP = {
def run_server_script_for_doc_event(doc, event):
# run document event method
if not event in EVENT_MAP:
if event not in EVENT_MAP:
return
if frappe.flags.in_install:

View file

@ -0,0 +1 @@
Log of SMS sent via SMS Center.

View file

View file

@ -0,0 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("SMS Log", {
refresh: function (frm) {},
});

View file

@ -0,0 +1,371 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "SYS-SMS-.#####",
"beta": 0,
"creation": "2012-03-27 14:36:47",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 0,
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sender_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sender Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sent_on",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sent On",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break0",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "50%"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sec_break1",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"options": "Simple",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "no_of_requested_sms",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "No of Requested SMS",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "requested_numbers",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Requested Numbers",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "50%"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "no_of_sent_sms",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "No of Sent SMS",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sent_to",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sent To",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-mobile-phone",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 16:15:40.898889",
"modified_by": "Administrator",
"module": "Core",
"name": "SMS Log",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -0,0 +1,26 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from frappe.model.document import Document
class SMSLog(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
message: DF.SmallText | None
no_of_requested_sms: DF.Int
no_of_sent_sms: DF.Int
requested_numbers: DF.Code | None
sender_name: DF.Data | None
sent_on: DF.Date | None
sent_to: DF.Code | None
# end: auto-generated types
pass

View file

@ -0,0 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
# test_records = frappe.get_test_records('SMS Log')
class TestSMSLog(unittest.TestCase):
pass

View file

@ -46,7 +46,7 @@ def validate_receiver_nos(receiver_list):
@frappe.whitelist()
def get_contact_number(contact_name, ref_doctype, ref_name):
"returns mobile number of the contact"
"Return mobile number of the given contact."
number = frappe.db.sql(
"""select mobile_no, phone from tabContact
where name=%s

View file

@ -32,10 +32,25 @@ frappe.ui.form.on("System Settings", {
frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0);
}
},
on_update: function (frm) {
if (frappe.boot.time_zone && frappe.boot.time_zone.system !== frm.doc.time_zone) {
// Clear cache after saving to refresh the values of boot.
frappe.ui.toolbar.clear_cache();
after_save: function (frm) {
/**
* Checks whether the effective value has changed.
*
* @param {Array.<string>} - Tuple with new fallback, previous fallback and
* optionally an override value.
* @returns {boolean} - Whether the resulting value has effectively changed
*/
const has_effectively_changed = ([new_fallback, prev_fallback, override = undefined]) =>
!override && prev_fallback !== new_fallback;
const attr_tuples = [
[frm.doc.language, frappe.boot.sysdefaults.language, frappe.boot.user.language],
[frm.doc.rounding_method, frappe.boot.sysdefaults.rounding_method], // no user override.
];
if (attr_tuples.some(has_effectively_changed)) {
frappe.msgprint(__("Refreshing..."));
window.location.reload();
}
},
first_day_of_the_week(frm) {

View file

@ -23,38 +23,23 @@
"float_precision",
"currency_precision",
"rounding_method",
"sec_backup_limit",
"backup_limit",
"encrypt_backup",
"background_workers",
"enable_scheduler",
"dormant_days",
"permissions",
"apply_strict_user_permissions",
"column_break_21",
"allow_guests_to_upload_files",
"force_web_capture_mode_for_uploads",
"allow_older_web_view_links",
"security_tab",
"security",
"session_expiry",
"document_share_key_expiry",
"column_break_13",
"column_break_txqh",
"deny_multiple_sessions",
"disable_user_pass_login",
"login_methods_section",
"allow_login_using_mobile_number",
"allow_login_using_user_name",
"disable_user_pass_login",
"column_break_uhqk",
"login_with_email_link",
"login_with_email_link_expiry",
"allow_error_traceback",
"strip_exif_metadata_from_uploaded_images",
"allow_older_web_view_links",
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
"reset_password_link_expiry_duration",
"password_reset_limit",
"column_break_31",
"enable_password_policy",
"minimum_password_score",
"brute_force_security",
"allow_consecutive_login_attempts",
"column_break_34",
@ -66,6 +51,16 @@
"two_factor_method",
"lifespan_qrcode_image",
"otp_issuer_name",
"password_tab",
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
"reset_password_link_expiry_duration",
"password_reset_limit",
"column_break_31",
"enable_password_policy",
"minimum_password_score",
"email_tab",
"email",
"email_footer_address",
"email_retry_limit",
@ -75,17 +70,31 @@
"attach_view_link",
"welcome_email_template",
"reset_password_template",
"prepared_report_section",
"max_auto_email_report_per_user",
"files_tab",
"files_section",
"max_file_size",
"allow_guests_to_upload_files",
"force_web_capture_mode_for_uploads",
"strip_exif_metadata_from_uploaded_images",
"column_break_uqma",
"allowed_file_extensions",
"updates_tab",
"system_updates_section",
"disable_system_update_notification",
"disable_change_log_notification",
"backups_tab",
"sec_backup_limit",
"backup_limit",
"encrypt_backup",
"advanced_tab",
"prepared_report_section",
"max_auto_email_report_per_user",
"background_workers",
"enable_scheduler",
"dormant_days",
"telemetry_section",
"enable_telemetry",
"files_section",
"max_file_size",
"column_break_uqma",
"allowed_file_extensions"
"allow_error_traceback",
"enable_telemetry"
],
"fields": [
{
@ -126,7 +135,6 @@
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "date_and_number_format",
"fieldtype": "Section Break",
"label": "Date and Number Format"
@ -171,10 +179,8 @@
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"collapsible": 1,
"fieldname": "sec_backup_limit",
"fieldtype": "Section Break",
"label": "Backups"
"fieldtype": "Section Break"
},
{
"default": "3",
@ -184,7 +190,6 @@
"label": "Number of Backups"
},
{
"collapsible": 1,
"fieldname": "background_workers",
"fieldtype": "Section Break",
"label": "Background Workers"
@ -198,7 +203,6 @@
"label": "Enable Scheduled Jobs"
},
{
"collapsible": 1,
"fieldname": "permissions",
"fieldtype": "Section Break",
"label": "Permissions"
@ -211,10 +215,8 @@
"label": "Apply Strict User Permissions"
},
{
"collapsible": 1,
"fieldname": "security",
"fieldtype": "Section Break",
"label": "Security"
"fieldtype": "Section Break"
},
{
"default": "170:00",
@ -223,10 +225,6 @@
"fieldtype": "Data",
"label": "Session Expiry (idle timeout)"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Note: Multiple sessions will be allowed in case of mobile device",
@ -255,7 +253,6 @@
"label": "Show Full Error and Allow Reporting of Issues to the Developer"
},
{
"collapsible": 1,
"fieldname": "password_settings",
"fieldtype": "Section Break",
"label": "Password"
@ -286,7 +283,6 @@
"options": "2\n3\n4"
},
{
"collapsible": 1,
"fieldname": "brute_force_security",
"fieldtype": "Section Break",
"label": "Brute Force Security"
@ -309,7 +305,6 @@
"label": "Allow Login After Fail"
},
{
"collapsible": 1,
"fieldname": "two_factor_authentication",
"fieldtype": "Section Break",
"label": "Two Factor Authentication"
@ -338,6 +333,7 @@
},
{
"default": "OTP App",
"depends_on": "enable_two_factor_auth",
"description": "Choose authentication method to be used by all users",
"fieldname": "two_factor_method",
"fieldtype": "Select",
@ -345,7 +341,7 @@
"options": "OTP App\nSMS\nEmail"
},
{
"depends_on": "eval:doc.two_factor_method == \"OTP App\"",
"depends_on": "eval:doc.enable_two_factor_auth && doc.two_factor_method == \"OTP App\"",
"description": "Time in seconds to retain QR code image on server. Min:<strong>240</strong>",
"fieldname": "lifespan_qrcode_image",
"fieldtype": "Int",
@ -359,10 +355,8 @@
"label": "OTP Issuer Name"
},
{
"collapsible": 1,
"fieldname": "email",
"fieldtype": "Section Break",
"label": "Email"
"fieldtype": "Section Break"
},
{
"description": "Your organization name and address for the email footer.",
@ -430,7 +424,6 @@
"label": "Include Web View Link in Email"
},
{
"collapsible": 1,
"fieldname": "prepared_report_section",
"fieldtype": "Section Break",
"label": "Reports"
@ -456,10 +449,8 @@
"label": "Encrypt Backups"
},
{
"collapsible": 1,
"fieldname": "system_updates_section",
"fieldtype": "Section Break",
"label": "System Updates"
"fieldtype": "Section Break"
},
{
"default": "0",
@ -547,7 +538,6 @@
"label": "Disable Document Sharing"
},
{
"collapsible": 1,
"fieldname": "telemetry_section",
"fieldtype": "Section Break",
"label": "Telemetry"
@ -578,10 +568,8 @@
"label": "Force Web Capture Mode for Uploads"
},
{
"collapsible": 1,
"fieldname": "files_section",
"fieldtype": "Section Break",
"label": "Files"
"fieldtype": "Section Break"
},
{
"fieldname": "max_file_size",
@ -598,12 +586,60 @@
"fieldname": "allowed_file_extensions",
"fieldtype": "Small Text",
"label": "Allowed File Extensions"
},
{
"fieldname": "security_tab",
"fieldtype": "Tab Break",
"label": "Login"
},
{
"fieldname": "email_tab",
"fieldtype": "Tab Break",
"label": "Email"
},
{
"fieldname": "files_tab",
"fieldtype": "Tab Break",
"label": "Files"
},
{
"fieldname": "updates_tab",
"fieldtype": "Tab Break",
"label": "Updates"
},
{
"fieldname": "backups_tab",
"fieldtype": "Tab Break",
"label": "Backups"
},
{
"fieldname": "advanced_tab",
"fieldtype": "Tab Break",
"label": "Advanced"
},
{
"fieldname": "password_tab",
"fieldtype": "Tab Break",
"label": "Password"
},
{
"fieldname": "column_break_txqh",
"fieldtype": "Column Break"
},
{
"fieldname": "login_methods_section",
"fieldtype": "Section Break",
"label": "Login Methods"
},
{
"fieldname": "column_break_uhqk",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2023-10-17 16:12:28.145496",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@ -619,7 +655,7 @@
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -97,8 +97,8 @@ class SystemSettings(Document):
def validate(self):
from frappe.twofactor import toggle_two_factor_auth
enable_password_policy = cint(self.enable_password_policy) and True or False
minimum_password_score = cint(getattr(self, "minimum_password_score", 0)) or 0
enable_password_policy = cint(self.enable_password_policy)
minimum_password_score = cint(getattr(self, "minimum_password_score", 0))
if enable_password_policy and minimum_password_score <= 0:
frappe.throw(_("Please select Minimum Password Score"))
elif not enable_password_policy:
@ -195,7 +195,7 @@ def update_last_reset_password_date():
def load():
from frappe.utils.momentjs import get_all_timezones
if not "System Manager" in frappe.get_roles():
if "System Manager" not in frappe.get_roles():
frappe.throw(_("Not permitted"), frappe.PermissionError)
all_defaults = frappe.db.get_defaults()

View file

@ -114,22 +114,6 @@ frappe.ui.form.on("User", {
return;
}
const hasChanged = (doc_attr, boot_attr) => {
return doc_attr && boot_attr && doc_attr !== boot_attr;
};
if (
doc.name === frappe.session.user &&
!doc.__unsaved &&
frappe.all_timezones &&
(hasChanged(doc.language, frappe.boot.user.language) ||
hasChanged(doc.time_zone, frappe.boot.time_zone.user) ||
hasChanged(doc.desk_theme, frappe.boot.user.desk_theme))
) {
frappe.msgprint(__("Refreshing..."));
window.location.reload();
}
frm.toggle_display(["sb1", "sb3", "modules_access"], false);
if (!frm.is_new()) {
@ -335,10 +319,31 @@ frappe.ui.form.on("User", {
},
});
},
on_update: function (frm) {
if (frappe.boot.time_zone && frappe.boot.time_zone.user !== frm.doc.time_zone) {
// Clear cache after saving to refresh the values of boot.
frappe.ui.toolbar.clear_cache();
after_save: function (frm) {
/**
* Checks whether the effective value has changed.
*
* @param {Array.<string>} - Tuple with new override, previous override,
* and optionally fallback.
* @returns {boolean} - Whether the resulting value has effectively changed
*/
const has_effectively_changed = ([new_override, prev_override, fallback = undefined]) => {
const prev_effective = prev_override || fallback;
const new_effective = new_override || fallback;
return new_override !== undefined && prev_effective !== new_effective;
};
const doc = frm.doc;
const boot = frappe.boot;
const attr_tuples = [
[doc.language, boot.user.language, boot.sysdefaults.language],
[doc.time_zone, boot.time_zone.user, boot.time_zone.system],
[doc.desk_theme, boot.user.desk_theme], // No system default.
];
if (doc.name === frappe.session.user && attr_tuples.some(has_effectively_changed)) {
frappe.msgprint(__("Refreshing..."));
window.location.reload();
}
},
});

View file

@ -9,6 +9,7 @@ import frappe.defaults
import frappe.permissions
import frappe.share
from frappe import STANDARD_USERS, _, msgprint, throw
from frappe.auth import MAX_PASSWORD_SIZE
from frappe.core.doctype.user_type.user_type import user_linked_with_permission_on_doctype
from frappe.desk.doctype.notification_settings.notification_settings import (
create_notification_settings,
@ -229,7 +230,7 @@ class User(Document):
frappe.cache.delete_key("users_for_mentions")
def has_website_permission(self, ptype, user, verbose=False):
"""Returns true if current user is the session user"""
"""Return True if current user is the session user."""
return self.name == frappe.session.user
def set_full_name(self):
@ -685,7 +686,7 @@ class User(Document):
)
def get_blocked_modules(self):
"""Returns list of modules blocked for that user"""
"""Return list of modules blocked for that user."""
return [d.module for d in self.block_modules] if self.block_modules else []
def validate_user_email_inbox(self):
@ -823,6 +824,9 @@ def update_password(
old_password (str, optional): Old password. Defaults to None.
"""
if len(new_password) > MAX_PASSWORD_SIZE:
frappe.throw(_("Password size exceeded the maximum allowed size."))
result = test_password_strength(new_password)
feedback = result.get("feedback", None)
@ -872,7 +876,7 @@ def test_password_strength(
"Arguments `key` and `old_password` are deprecated in function `test_password_strength`."
)
enable_password_policy = frappe.get_system_settings("enable_password_policy") or 0
enable_password_policy = frappe.get_system_settings("enable_password_policy")
if not enable_password_policy:
return {}
@ -885,7 +889,7 @@ def test_password_strength(
if new_password:
result = _test_password_strength(new_password, user_inputs=user_data)
password_policy_validation_passed = False
minimum_password_score = cint(frappe.get_system_settings("minimum_password_score")) or 0
minimum_password_score = cint(frappe.get_system_settings("minimum_password_score"))
# score should be greater than 0 and minimum_password_score
if result.get("score") and result.get("score") >= minimum_password_score:
@ -1079,7 +1083,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
def get_total_users():
"""Returns total no. of system users"""
"""Return total number of system users."""
return flt(
frappe.db.sql(
"""SELECT SUM(`simultaneous_sessions`)
@ -1114,7 +1118,7 @@ def get_system_users(exclude_users: Iterable[str] | str | None = None, limit: in
def get_active_users():
"""Returns No. of system users who logged in, in the last 3 days"""
"""Return number of system users who logged in, in the last 3 days."""
return frappe.db.sql(
"""select count(*) from `tabUser`
where enabled = 1 and user_type != 'Website User'
@ -1127,12 +1131,12 @@ def get_active_users():
def get_website_users():
"""Returns total no. of website users"""
"""Return total number of website users."""
return frappe.db.count("User", filters={"enabled": True, "user_type": "Website User"})
def get_active_website_users():
"""Returns No. of website users who logged in, in the last 3 days"""
"""Return number of website users who logged in, in the last 3 days."""
return frappe.db.sql(
"""select count(*) from `tabUser`
where enabled = 1 and user_type = 'Website User'
@ -1223,27 +1227,31 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
contact_name = get_contact_name(user.email)
if not contact_name:
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": user.first_name,
"last_name": user.last_name,
"user": user.name,
"gender": user.gender,
}
)
try:
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": user.first_name,
"last_name": user.last_name,
"user": user.name,
"gender": user.gender,
}
)
if user.email:
contact.add_email(user.email, is_primary=True)
if user.email:
contact.add_email(user.email, is_primary=True)
if user.phone:
contact.add_phone(user.phone, is_primary_phone=True)
if user.phone:
contact.add_phone(user.phone, is_primary_phone=True)
if user.mobile_no:
contact.add_phone(user.mobile_no, is_primary_mobile_no=True)
contact.insert(
ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory
)
if user.mobile_no:
contact.add_phone(user.mobile_no, is_primary_mobile_no=True)
contact.insert(
ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory
)
except frappe.DuplicateEntryError:
pass
else:
contact = frappe.get_doc("Contact", contact_name)
contact.first_name = user.first_name

View file

@ -173,7 +173,7 @@ def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len,
def get_permitted_documents(doctype):
"""Returns permitted documents from the given doctype for the session user"""
"""Return permitted documents from the given doctype for the session user."""
# sort permissions in a way to make the first permission in the list to be default
user_perm_list = sorted(
get_user_permissions().get(doctype, []), key=lambda x: x.get("is_default"), reverse=True

View file

@ -31,6 +31,7 @@ class UserType(Document):
user_doctypes: DF.Table[UserDocumentType]
user_id_field: DF.Literal
user_type_modules: DF.Table[UserTypeModule]
# end: auto-generated types
def validate(self):
self.set_modules()
@ -140,7 +141,7 @@ class UserType(Document):
for row in self.user_doctypes:
docperm = add_role_permissions(row.document_type, self.role)
values = {perm: row.get(perm) or 0 for perm in perms}
values = {perm: row.get(perm, default=0) for perm in perms}
for perm in ["print", "email", "share"]:
values[perm] = 1

View file

@ -54,7 +54,7 @@
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2022-08-03 12:20:53.929691",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Core",
"name": "Version",
@ -74,7 +74,7 @@
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"title_field": "docname",
"track_changes": 1

View file

@ -17,7 +17,7 @@ def get_notification_config():
def get_things_todo(as_list=False):
"""Returns a count of incomplete todos"""
"""Return a count of incomplete ToDos."""
data = frappe.get_list(
"ToDo",
fields=["name", "description"] if as_list else "count(*)",
@ -35,7 +35,7 @@ def get_things_todo(as_list=False):
def get_todays_events(as_list: bool = False):
"""Returns a count of todays events in calendar"""
"""Return a count of today's events in calendar."""
from frappe.desk.doctype.event.event import get_events
from frappe.utils import nowdate

View file

@ -109,8 +109,10 @@ def add(parent, role, permlevel):
@frappe.whitelist()
def update(doctype, role, permlevel, ptype, value=None, if_owner=0):
"""Update role permission params
def update(
doctype: str, role: str, permlevel: int, ptype: str, value=None, if_owner=0
) -> str | None:
"""Update role permission params.
Args:
doctype (str): Name of the DocType to update params for
@ -119,8 +121,8 @@ def update(doctype, role, permlevel, ptype, value=None, if_owner=0):
ptype (str): permission type, example "read", "delete", etc.
value (None, optional): value for ptype, None indicates False
Returns:
str: Refresh flag is permission is updated successfully
Return:
str: Refresh flag if permission is updated successfully
"""
def clear_cache():

View file

@ -7,7 +7,7 @@ import frappe
def get_parent_doc(doc):
"""Returns document of `reference_doctype`, `reference_doctype`"""
"""Return document of `reference_doctype`, `reference_doctype`."""
if not hasattr(doc, "parent_doc"):
if doc.reference_doctype and doc.reference_name:
doc.parent_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
@ -38,8 +38,7 @@ def set_timeline_doc(doc):
def find(list_of_dict, match_function):
"""Returns a dict in a list of dicts on matching the conditions
provided in match function
"""Return a dict in a list of dicts on matching the conditions provided in match function.
Usage:
list_of_dict = [{'name': 'Suraj'}, {'name': 'Aditya'}]
@ -54,8 +53,7 @@ def find(list_of_dict, match_function):
def find_all(list_of_dict, match_function):
"""Returns all matching dicts in a list of dicts.
Uses matching function to filter out the dicts
"""Return all matching dicts in a list of dicts. Uses matching function to filter out the dicts.
Usage:
colored_shapes = [
@ -86,6 +84,7 @@ def ljust_list(_list, length, fill_word=None):
return _list
def html2text(html, strip_links=False, wrap=True):
def html2text(html: str, strip_links=False, wrap=True) -> str:
"""Return the given `html` as markdown text."""
strip = ["a"] if strip_links else None
return md(html, heading_style="ATX", strip=strip, wrap=wrap)

View file

@ -77,7 +77,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-04-12 12:48:15.717985",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Custom",
"name": "Client Script",
@ -108,7 +108,7 @@
}
],
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -457,7 +457,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-25 06:55:10.713382",
"modified": "2023-12-08 15:52:37.525003",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
@ -488,7 +488,7 @@
],
"search_fields": "dt,label,fieldtype,options",
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -357,7 +357,7 @@ def rename_fieldname(custom_field: str, fieldname: str):
if field.is_system_generated:
frappe.throw(_("System Generated Fields can not be renamed"))
if frappe.db.has_column(parent_doctype, fieldname):
frappe.throw(_("Can not rename as fieldname {0} is already present on DocType."))
frappe.throw(_("Can not rename as column {0} is already present on DocType.").format(fieldname))
if old_fieldname == new_fieldname:
frappe.msgprint(_("Old and new fieldnames are same."), alert=True)
return

View file

@ -84,7 +84,7 @@ frappe.ui.form.on("Customize Form", {
if (!in_list(["Table", "Table MultiSelect"], f.fieldtype)) return;
frm.add_custom_button(
f.options,
__(f.options),
() => frm.set_value("doc_type", f.options),
__("Customize Child Table")
);
@ -97,7 +97,7 @@ frappe.ui.form.on("Customize Form", {
if (frm.doc.doc_type) {
frappe.model.with_doctype(frm.doc.doc_type).then(() => {
frm.page.set_title(__("Customize Form - {0}", [frm.doc.doc_type]));
frm.page.set_title(__("Customize Form - {0}", [__(frm.doc.doc_type)]));
frappe.customize_form.set_primary_action(frm);
frm.add_custom_button(

View file

@ -46,6 +46,7 @@
"column_break_26",
"email_append_to",
"sender_field",
"sender_name_field",
"subject_field",
"section_break_8",
"sort_field",
@ -219,7 +220,7 @@
"depends_on": "email_append_to",
"fieldname": "sender_field",
"fieldtype": "Data",
"label": "Sender Field",
"label": "Sender Email Field",
"mandatory_depends_on": "email_append_to"
},
{
@ -392,6 +393,12 @@
"fieldname": "details_tab",
"fieldtype": "Tab Break",
"label": "Details"
},
{
"depends_on": "email_append_to",
"fieldname": "sender_name_field",
"fieldtype": "Data",
"label": "Sender Name Field"
}
],
"hide_toolbar": 1,
@ -400,7 +407,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-11-16 11:23:06.427432",
"modified": "2023-12-01 18:18:23.086134",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",

View file

@ -72,6 +72,7 @@ class CustomizeForm(Document):
quick_entry: DF.Check
search_fields: DF.Data | None
sender_field: DF.Data | None
sender_name_field: DF.Data | None
show_preview_popup: DF.Check
show_title_field_in_link: DF.Check
sort_field: DF.Literal
@ -83,6 +84,7 @@ class CustomizeForm(Document):
track_views: DF.Check
translated_doctype: DF.Check
# end: auto-generated types
def on_update(self):
frappe.db.delete("Singles", {"doctype": "Customize Form"})
frappe.db.delete("Customize Form Field")

Some files were not shown because too many files have changed in this diff Show more