Merge branch 'develop' into numbercard_fix
This commit is contained in:
commit
53ab3c6491
493 changed files with 5374 additions and 3635 deletions
|
|
@ -40,3 +40,6 @@ f223bc02490902dfcc32892058f13f343d51fbaf
|
|||
|
||||
# frappe.cache() -> frappe.cache
|
||||
fa6dc03cc87ad74e11609e7373078366fdcb3e1b
|
||||
|
||||
# Bulk refactor with sourcery
|
||||
c35476256f85271fb57584eb0a26f4d9def3caf4
|
||||
|
|
|
|||
5
.github/frappe-framework-logo-dark.svg
vendored
Normal file
5
.github/frappe-framework-logo-dark.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.9 KiB |
2
.github/workflows/labeller.yml
vendored
2
.github/workflows/labeller.yml
vendored
|
|
@ -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 }}"
|
||||
|
|
|
|||
6
.github/workflows/linters.yml
vendored
6
.github/workflows/linters.yml
vendored
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
2
.github/workflows/on_release.yml
vendored
2
.github/workflows/on_release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/patch-mariadb-tests.yml
vendored
2
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
|
|
|
|||
2
.github/workflows/publish-assets-develop.yml
vendored
2
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
.github/workflows/server-tests.yml
vendored
6
.github/workflows/server-tests.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
5
.github/workflows/ui-tests.yml
vendored
5
.github/workflows/ui-tests.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
context("Permissions API", () => {
|
||||
context.skip("Permissions API", () => {
|
||||
before(() => {
|
||||
cy.visit("/login");
|
||||
cy.remove_role("frappe@example.com", "System Manager");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
frappe.listview_settings["Document Naming Rule"] = {
|
||||
hide_name_column: true,
|
||||
};
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@
|
|||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "ref_doctype.module",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
1
frappe/core/doctype/sms_log/README.md
Normal file
1
frappe/core/doctype/sms_log/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Log of SMS sent via SMS Center.
|
||||
0
frappe/core/doctype/sms_log/__init__.py
Normal file
0
frappe/core/doctype/sms_log/__init__.py
Normal file
6
frappe/core/doctype/sms_log/sms_log.js
Normal file
6
frappe/core/doctype/sms_log/sms_log.js
Normal 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) {},
|
||||
});
|
||||
371
frappe/core/doctype/sms_log/sms_log.json
Normal file
371
frappe/core/doctype/sms_log/sms_log.json
Normal 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
|
||||
}
|
||||
26
frappe/core/doctype/sms_log/sms_log.py
Normal file
26
frappe/core/doctype/sms_log/sms_log.py
Normal 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
|
||||
10
frappe/core/doctype/sms_log/test_sms_log.py
Normal file
10
frappe/core/doctype/sms_log/test_sms_log.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue