Merge branch 'develop' of https://github.com/frappe/frappe into ft/add-apply-module-export-filter-on-export-customization

This commit is contained in:
KerollesFathy 2025-11-19 16:56:46 +00:00
commit c56a725b53
366 changed files with 30392 additions and 10163 deletions

View file

@ -96,6 +96,7 @@
"hljs": true,
"Awesomplete": true,
"Sortable": true,
"gemoji": true,
"Showdown": true,
"Taggle": true,
"Gantt": true,

View file

@ -100,10 +100,16 @@ runs:
run: |
# Install System Dependencies
start_time=$(date +%s)
curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash
curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version="mariadb-11.8.5"
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt -qq update
sudo apt -qq remove mysql-server mysql-client
sudo apt -qq install libcups2-dev redis-server mariadb-client libmariadb-dev
sudo apt -qq remove -y postgresql-client postgresql-client-16 postgresql-client-common
sudo apt -qq install libcups2-dev redis-server mariadb-client libmariadb-dev postgresql-client-18 libpq-dev
echo "/usr/lib/postgresql/18/bin" >> $GITHUB_PATH
wget -q -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
sudo apt install /tmp/wkhtmltox.deb
@ -169,6 +175,14 @@ runs:
mariadb --host 127.0.0.1 --port 3306 -u root -p${{ inputs.db-root-password }} -e "FLUSH PRIVILEGES";
fi
if [ "$DB" == "postgres" ]; then
export PGPASSWORD='travis'
psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres
psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres
psql -h 127.0.0.1 -p 5432 -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE test_frappe TO test_frappe;"
unset PGPASSWORD
fi
- shell: bash -e {0}
run: |
# Install App(s)

18
.github/helper/db/postgres.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
"db_host": "127.0.0.1",
"db_port": 5432,
"db_name": "test_frappe",
"db_password": "test_frappe",
"db_type": "postgres",
"allow_tests": true,
"auto_email_id": "test@example.com",
"mail_server": "localhost",
"mail_port": 2525,
"mail_login": "test@example.com",
"mail_password": "test",
"admin_password": "admin",
"root_login": "postgres",
"root_password": "travis",
"host_name": "http://test_site:8000",
"server_script_enabled": true
}

View file

@ -9,7 +9,7 @@ on:
python-version:
required: false
type: string
default: '3.13'
default: '3.14'
node-version:
required: false
type: number
@ -22,6 +22,10 @@ on:
required: false
type: boolean
default: false
enable-postgres:
required: false
type: boolean
default: true
enable-coverage:
required: false
type: boolean
@ -62,9 +66,20 @@ jobs:
strategy:
fail-fast: false
matrix:
db: ${{ fromJson(inputs.enable-sqlite && '["mariadb", "sqlite"]' || '["mariadb"]') }}
db: ${{ fromJson(inputs.enable-sqlite && (inputs.enable-postgres && '["mariadb", "postgres", "sqlite"]' || '["mariadb", "sqlite"]') || (inputs.enable-postgres && '["mariadb", "postgres"]' || '["mariadb"]')) }}
index: ${{ fromJson(needs.gen-idx-integration.outputs.indices) }}
services:
postgres:
image: postgres:18.0
ports:
- 5432:5432
env:
POSTGRES_PASSWORD: travis
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 3
mariadb:
image: mariadb:11.8
ports:

View file

@ -5,7 +5,7 @@ on:
python-version:
required: false
type: string
default: '3.13.0'
default: '3.14.0'
jobs:
typecheck:

View file

@ -9,7 +9,7 @@ on:
python-version:
required: false
type: string
default: '3.13'
default: '3.14'
node-version:
required: false
type: number

View file

@ -25,7 +25,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
python-version: "3.14"
- name: Run script to update POT file
run: |

View file

@ -41,7 +41,7 @@ jobs:
- name: 'Setup Environment'
uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- uses: actions/checkout@v5
- name: Validate Docs
@ -60,7 +60,7 @@ jobs:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
cache: pip
- name: Download Semgrep rules
@ -78,7 +78,7 @@ jobs:
steps:
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- uses: actions/checkout@v5
@ -106,6 +106,6 @@ jobs:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
cache: pip
- uses: pre-commit/action@v3.0.1

View file

@ -26,7 +26,7 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- name: Set up bench and build assets
run: |
npm install -g yarn

View file

@ -19,7 +19,7 @@ jobs:
node-version: 22
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- name: Set up bench and build assets
run: |
npm install -g yarn

View file

@ -74,7 +74,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'
- name: Setup Node
uses: actions/setup-node@v6

View file

@ -44,6 +44,7 @@ jobs:
name: Tests
uses: ./.github/workflows/_base-server-tests.yml
with:
enable-postgres: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }} # This enables PostgreSQL to run tests
enable-sqlite: false # This will test against both MariaDB and SQLite if enabled
parallel-runs: 2
enable-coverage: ${{ github.event_name != 'pull_request' }}

View file

@ -36,5 +36,9 @@ module.exports = defineConfig({
testIsolation: false,
baseUrl: "http://test_site_ui:8000",
specPattern: ["./cypress/integration/*.js", "**/ui_test_*.js"],
excludeSpecPattern: [
"./cypress/integration/workspace.js",
"./cypress/integration/workspace_blocks.js",
],
},
});

View file

@ -2,11 +2,11 @@ context("Awesome Bar", () => {
before(() => {
cy.visit("/login");
cy.login();
cy.visit("/app/todo"); // Make sure ToDo filters are cleared.
cy.visit("/desk/todo"); // Make sure ToDo filters are cleared.
cy.clear_filters();
cy.visit("/app/web-page"); // Make sure Blog Post filters are cleared.
cy.visit("/desk/web-page"); // Make sure Blog Post filters are cleared.
cy.clear_filters();
cy.visit("/app/build"); // Go to some other page.
cy.visit("/desk/build"); // Go to some other page.
});
beforeEach(() => {
@ -18,7 +18,7 @@ context("Awesome Bar", () => {
});
after(() => {
cy.visit("/app/todo"); // Make sure we're not bleeding any filters to the next spec.
cy.visit("/desk/todo"); // Make sure we're not bleeding any filters to the next spec.
cy.clear_filters();
});
@ -28,7 +28,7 @@ context("Awesome Bar", () => {
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");
cy.location("pathname").should("eq", "/desk/todo");
});
it("finds text in doctype list", () => {
@ -40,7 +40,7 @@ context("Awesome Bar", () => {
cy.get('[data-original-title="ID"]:visible > input').should("have.value", "%test%");
// filter preserved, now finds something else
cy.visit("/app/todo");
cy.visit("/desk/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"]:visible > input').as("filter");

View file

@ -1,7 +1,7 @@
context("Attach Control", () => {
before(() => {
cy.login();
cy.visit("/app/doctype");
cy.visit("/desk/doctype");
return cy
.window()
.its("frappe")
@ -166,7 +166,7 @@ context("Attach Control", () => {
context("Attach Control with Failed Document Save", () => {
before(() => {
cy.login();
cy.visit("/app/doctype");
cy.visit("/desk/doctype");
return cy
.window()
.its("frappe")

View file

@ -1,7 +1,7 @@
context("Control Autocomplete", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
cy.wait(4000);
});

View file

@ -1,7 +1,7 @@
context("Control Barcode", () => {
beforeEach(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_barcode() {

View file

@ -1,7 +1,7 @@
context("Control Color", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_color() {

View file

@ -3,7 +3,7 @@ context("Control Currency", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_currency(df_options = {}) {

View file

@ -1,7 +1,7 @@
context("Data Control", () => {
before(() => {
cy.login();
cy.visit("/app/doctype");
cy.visit("/desk/doctype");
return cy
.window()
.its("frappe")
@ -39,7 +39,7 @@ context("Data Control", () => {
});
it("check custom formatters", () => {
cy.visit(`/app/doctype/User`);
cy.visit(`/desk/doctype/User`);
cy.get(
'[data-fieldname="fields"] .grid-row[data-idx="3"] [data-fieldname="fieldtype"] .static-area'
).should("have.text", "Section Break");
@ -49,7 +49,10 @@ context("Data Control", () => {
cy.new_form("Test Data Control");
//Checking the URL for the new form of the doctype
cy.location("pathname").should("contains", "/app/test-data-control/new-test-data-control");
cy.location("pathname").should(
"contains",
"/desk/test-data-control/new-test-data-control"
);
cy.get(".title-text").should("have.text", "New Test Data Control");
cy.get('.frappe-control[data-fieldname="name1"]')
.find("label")
@ -130,7 +133,7 @@ context("Data Control", () => {
//Checking if the fields contains the data which has been filled in
cy.location("pathname").should(
"not.contains",
"/app/test-data-control/new-test-data-control"
"/desk/test-data-control/new-test-data-control"
);
cy.get_field("name1").should("have.value", "Komal");
cy.get_field("email").should("have.value", "komal@test.com");

View file

@ -1,7 +1,7 @@
context("Date Control", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
});
function get_dialog(date_field_options) {

View file

@ -1,7 +1,7 @@
context("Date Range Control", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
});
function get_dialog() {

View file

@ -1,7 +1,7 @@
context("Control Duration", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_duration(hide_days = 0, hide_seconds = 0) {

View file

@ -1,7 +1,7 @@
context("Dynamic Link", () => {
before(() => {
cy.login();
cy.visit("/app/doctype");
cy.visit("/desk/doctype");
return cy
.window()
.its("frappe")
@ -107,7 +107,7 @@ context("Dynamic Link", () => {
});
it("Creating a dynamic link and verifying it", () => {
cy.visit("/app/test-dynamic-link");
cy.visit("/desk/test-dynamic-link");
//Clicking on the Document ID field
cy.get_field("doc_type").clear();

View file

@ -1,7 +1,7 @@
context("Control Float", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_float() {

View file

@ -1,7 +1,7 @@
context("Control Icon", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_icon() {

View file

@ -1,11 +1,11 @@
context("Control Link", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
beforeEach(() => {
cy.visit("/app/website");
cy.visit("/desk/website");
cy.create_records({
doctype: "ToDo",
description: "this is a test todo for link",
@ -120,7 +120,7 @@ context("Control Link", () => {
cy.get("@input").trigger("mouseover");
cy.get(".frappe-control[data-fieldname=link] .btn-open")
.should("be.visible")
.should("have.attr", "href", `/app/todo/${todos[0]}`);
.should("have.attr", "href", `/desk/todo/${todos[0]}`);
});
});
@ -176,7 +176,7 @@ context("Control Link", () => {
it("should update dependant fields (via fetch_from)", () => {
cy.get("@todos").then((todos) => {
cy.visit(`/app/todo/${todos[0]}`);
cy.visit(`/desk/todo/${todos[0]}`);
cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link");
cy.intercept("/api/method/frappe.client.validate_link*").as("validate_link");

View file

@ -1,11 +1,11 @@
context("Control Markdown Editor", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
});
it("should allow inserting images by drag and drop", () => {
cy.visit("/app/web-page/new");
cy.visit("/desk/web-page/new");
cy.fill_field("content_type", "Markdown", "Select");
cy.get_field("main_section_md", "Markdown Editor").selectFile(
"cypress/fixtures/sample_image.jpg",

View file

@ -3,7 +3,7 @@ import doctype_with_phone from "../fixtures/doctype_with_phone";
context("Control Phone", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
afterEach(() => {
@ -68,13 +68,13 @@ context("Control Phone", () => {
});
it("existing document should render phone field with data", () => {
cy.visit("/app/doctype");
cy.visit("/desk/doctype");
cy.insert_doc("DocType", doctype_with_phone, true);
cy.clear_cache();
// Creating custom doctype
cy.insert_doc("DocType", doctype_with_phone, true);
cy.visit("/app/doctype-with-phone");
cy.visit("/desk/doctype-with-phone");
cy.click_listview_primary_button("Add Doctype With Phone");
// create a record

View file

@ -1,7 +1,7 @@
context("Control Rating", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_rating() {

View file

@ -1,7 +1,7 @@
context("Control Select", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
function get_dialog_with_select() {

View file

@ -40,7 +40,7 @@ describe(
() => {
before(() => {
cy.login();
cy.visit(`/app/note/new`);
cy.visit(`/desk/note/new`);
// close the sidebar cause default is expanded
cy.get(".body-sidebar .collapse-sidebar-link").click();
});

View file

@ -1,7 +1,7 @@
context("Customize Form", () => {
before(() => {
cy.login();
cy.visit("/app/customize-form");
cy.visit("/desk/customize-form");
});
it("Changing to naming rule should update autoname", () => {
cy.fill_field("doc_type", "ToDo", "Link").blur();

View file

@ -1,7 +1,7 @@
describe("Dashboard view", { scrollBehavior: false }, () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
});
it("should load", () => {

View file

@ -1,7 +1,7 @@
context("Dashboard Chart", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
it("Check filter populate for child table doctype", () => {

View file

@ -24,7 +24,7 @@ context("Dashboard links", () => {
});
it("Adding a new contact, checking for the counter on the dashboard and deleting the created contact", () => {
cy.visit("/app/contact");
cy.visit("/desk/contact");
cy.clear_filters();
cy.visit(`/app/user/${cy.config("testUser")}`);
@ -48,7 +48,7 @@ context("Dashboard links", () => {
cy.get('[data-doctype="Contact"]').contains("Contact").click();
//Deleting the newly created contact
cy.visit("/app/contact");
cy.visit("/desk/contact");
cy.get(".list-subject > .select-like > .list-row-checkbox").eq(0).click({ force: true });
cy.findByRole("button", { name: "Actions" }).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
@ -56,7 +56,7 @@ context("Dashboard links", () => {
//To check if the counter from the "Contact" doc link is removed
cy.wait(700);
cy.visit("/app/user");
cy.visit("/desk/user");
cy.get(".list-row-col > .level-item > .ellipsis").eq(0).click({ force: true });
cy.get('[data-doctype="Contact"]').should("contain", "Contact");
});

View file

@ -4,7 +4,7 @@ const doctype_name = data_field_validation_doctype.name;
context("Data Field Input Validation in New Form", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy.insert_doc("DocType", data_field_validation_doctype, true);
});

View file

@ -4,7 +4,7 @@ const doctype_name = datetime_doctype.name;
context("Control Date, Time and DateTime", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy.insert_doc("DocType", datetime_doctype, true);
});

View file

@ -1,7 +1,7 @@
context("Depends On", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy
.window()
.its("frappe")

View file

@ -4,7 +4,7 @@ context("FileUploader", () => {
});
beforeEach(() => {
cy.visit("/app");
cy.visit("/desk");
cy.wait(2000); // workspace can load async and clear active dialog
});

View file

@ -17,7 +17,7 @@ const type_value = (value) => {
context("Form", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy
.window()
.its("frappe")
@ -28,11 +28,11 @@ context("Form", () => {
beforeEach(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
it("create a new form", () => {
cy.visit("/app/todo/new");
cy.visit("/desk/todo/new");
cy.get_field("description", "Text Editor")
.type("this is a test todo", { force: true })
.wait(1000);
@ -51,7 +51,7 @@ context("Form", () => {
});
it("navigates between documents with child table list filters applied", () => {
cy.visit("/app/contact");
cy.visit("/desk/contact");
cy.clear_filters();
cy.get('.standard-filter-section [data-fieldname="name"] input')
@ -60,7 +60,7 @@ context("Form", () => {
cy.click_listview_row_item_with_text("Test Form Contact 3");
// clear filters
cy.visit("/app/contact");
cy.visit("/desk/contact");
cy.clear_filters();
});
@ -70,7 +70,7 @@ context("Form", () => {
let valid_email = "user@email.com";
let expectBackgroundColor = "rgb(255, 245, 245)";
cy.visit("/app/contact/new");
cy.visit("/desk/contact/new");
cy.fill_field("company_name", "Test Company");
cy.get('.frappe-control[data-fieldname="email_ids"]').as("table");
@ -105,7 +105,7 @@ context("Form", () => {
});
it("update docfield property using set_df_property in child table", () => {
cy.visit("/app/contact/Test Form Contact 1");
cy.visit("/desk/contact/Test Form Contact 1");
cy.window()
.its("cur_frm")
.then((frm) => {

View file

@ -3,18 +3,18 @@ const doctype_name = form_builder_doctype.name;
context("Form Builder", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
return cy.insert_doc("DocType", form_builder_doctype, true);
});
it("Open Form Builder for Web Form Doctype/Customize Form", () => {
// doctype
cy.visit("/app/doctype/Web Form");
cy.visit("/desk/doctype/Web Form");
cy.findByRole("tab", { name: "Form" }).click();
cy.get(".form-builder-container").should("exist");
// customize form
cy.visit("/app/customize-form?doc_type=Web%20Form");
cy.visit("/desk/customize-form?doc_type=Web%20Form");
cy.findByRole("tab", { name: "Form" }).click();
cy.get(".form-builder-container").should("exist");
});
@ -264,7 +264,7 @@ context("Form Builder", () => {
cy.findByRole("button", { name: "Save" }).click({ force: true });
cy.visit("/app/form-builder-doctype/new");
cy.visit("/desk/form-builder-doctype/new");
cy.get("[data-fieldname='data3'] .clearfix label").should("have.text", "New Title");
});

View file

@ -3,7 +3,7 @@ const doctype_name = doctype_with_tab_break.name;
context("Form Tab Break", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy.insert_doc("DocType", doctype_with_tab_break, true);
});
it("Should switch tab and open correct tabs on validation error", () => {

View file

@ -1,7 +1,7 @@
context.skip("Form Tour", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
return cy
.window()
.its("frappe")
@ -11,11 +11,11 @@ context.skip("Form Tour", () => {
});
const open_test_form_tour = () => {
cy.visit("/app/form-tour/Test Form Tour");
cy.visit("/desk/form-tour/Test Form Tour");
cy.findByRole("button", { name: "Show Tour" }).should("be.visible").as("show_tour");
cy.get("@show_tour").click();
cy.wait(500);
cy.url().should("include", "/app/contact");
cy.url().should("include", "/desk/contact");
};
it("jump to a form tour", open_test_form_tour);

View file

@ -1,11 +1,11 @@
context("Grid", () => {
beforeEach(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy
.window()
.its("frappe")
@ -16,7 +16,7 @@ context("Grid", () => {
});
});
it("update docfield property using update_docfield_property", () => {
cy.visit("/app/contact/Test Contact");
cy.visit("/desk/contact/Test Contact");
cy.window()
.its("cur_frm")
.then((frm) => {
@ -40,7 +40,7 @@ context("Grid", () => {
});
});
it("update docfield property using toggle_display", () => {
cy.visit("/app/contact/Test Contact");
cy.visit("/desk/contact/Test Contact");
cy.window()
.its("cur_frm")
.then((frm) => {
@ -64,7 +64,7 @@ context("Grid", () => {
});
});
it("update docfield property using toggle_enable", () => {
cy.visit("/app/contact/Test Contact");
cy.visit("/desk/contact/Test Contact");
cy.window()
.its("cur_frm")
.then((frm) => {
@ -88,7 +88,7 @@ context("Grid", () => {
});
});
it("update docfield property using toggle_reqd", () => {
cy.visit("/app/contact/Test Contact");
cy.visit("/desk/contact/Test Contact");
cy.window()
.its("cur_frm")
.then((frm) => {

View file

@ -1,7 +1,7 @@
context("Grid Configuration", () => {
beforeEach(() => {
cy.login();
cy.visit("/app/website-settings");
cy.visit("/desk/website-settings");
});
it("Set user wise grid settings", () => {
cy.findByRole("tab", { name: "Navbar" }).click();

View file

@ -1,11 +1,11 @@
context("Grid Pagination", () => {
beforeEach(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy
.window()
.its("frappe")
@ -16,14 +16,14 @@ context("Grid Pagination", () => {
});
});
it("creates pages for child table", () => {
cy.visit("/app/contact/Test Contact");
cy.visit("/desk/contact/Test Contact");
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
cy.get("@table").find(".current-page-number").should("have.value", "1");
cy.get("@table").find(".total-page-number").should("contain", "20");
cy.get("@table").find(".grid-body .grid-row").should("have.length", 50);
});
it("goes to the next and previous page", () => {
cy.visit("/app/contact/Test Contact");
cy.visit("/desk/contact/Test Contact");
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
cy.get("@table").find(".next-page").click();
cy.get("@table").find(".current-page-number").should("have.value", "2");
@ -36,7 +36,7 @@ context("Grid Pagination", () => {
cy.get("@table").find(".grid-body .grid-row").first().should("have.attr", "data-idx", "1");
});
it("adds and deletes rows and changes page", () => {
cy.visit("/app/contact/Test Contact");
cy.visit("/desk/contact/Test Contact");
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
cy.get("@table").findByRole("button", { name: "Add Row" }).click();
cy.get("@table").find(".grid-body .row-index").should("contain", 1001);
@ -49,7 +49,7 @@ context("Grid Pagination", () => {
cy.get("@table").find(".total-page-number").should("contain", "20");
});
it("go to specific page, use up and down arrow, type characters, 0 page and more than existing page", () => {
cy.visit("/app/contact/Test Contact");
cy.visit("/desk/contact/Test Contact");
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
cy.get("@table").find(".current-page-number").focus().clear().type("17").blur();
cy.get("@table").find(".grid-body .row-index").should("contain", 801);

View file

@ -7,7 +7,7 @@ context("Grid Search", () => {
before(() => {
cy.visit("/login");
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
cy.insert_doc("DocType", child_table_doctype, true);
cy.insert_doc("DocType", child_table_doctype_1, true);
cy.insert_doc("DocType", doctype_with_child_table, true);
@ -40,7 +40,7 @@ context("Grid Search", () => {
});
});
cy.visit(`/app/doctype-with-child-table/Test Grid Search`);
cy.visit(`/desk/doctype-with-child-table/Test Grid Search`);
cy.get('.frappe-control[data-fieldname="child_table_1"]').as("table");
cy.get("@table").find(".grid-row-check:last").click();
@ -49,7 +49,7 @@ context("Grid Search", () => {
});
it("test search field for different fieldtypes", () => {
cy.visit(`/app/doctype-with-child-table/Test Grid Search`);
cy.visit(`/desk/doctype-with-child-table/Test Grid Search`);
cy.get('.frappe-control[data-fieldname="child_table_1"]').as("table");

View file

@ -1,11 +1,11 @@
context("Kanban Board", () => {
before(() => {
cy.login("frappe@example.com");
cy.visit("/app");
cy.visit("/desk");
});
it("Create ToDo Kanban", () => {
cy.visit("/app/todo");
cy.visit("/desk/todo");
cy.get(".page-actions .custom-btn-group button").click();
cy.get(".page-actions .custom-btn-group ul.dropdown-menu li").contains("Kanban").click();
@ -33,7 +33,7 @@ context("Kanban Board", () => {
});
it("Add and Remove fields", () => {
cy.visit("/app/todo/view/kanban/ToDo Kanban");
cy.visit("/desk/todo/view/kanban/ToDo Kanban");
cy.intercept(
"POST",
@ -110,7 +110,7 @@ context("Kanban Board", () => {
cy.switch_to_user(not_system_manager);
cy.visit("/app/todo/view/kanban/Admin Kanban");
cy.visit("/desk/todo/view/kanban/Admin Kanban");
// Menu button should be hidden (dropdown for 'Save Filters' and 'Delete Kanban Board')
cy.get(".no-list-sidebar .menu-btn-group .btn-default[data-original-title='Menu']").should(

View file

@ -1,7 +1,7 @@
context("List Paging", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy
.window()
.its("frappe")
@ -11,7 +11,7 @@ context("List Paging", () => {
});
it("test load more with count selection buttons", () => {
cy.visit("/app/todo/view/report");
cy.visit("/desk/todo/view/report");
cy.clear_filters();
cy.get(".list-paging-area .list-count").should("contain.text", "20 of");

View file

@ -1,7 +1,7 @@
context("List View", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy
.window()
.its("frappe")

View file

@ -1,17 +1,17 @@
context("List View Settings", () => {
beforeEach(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
it("Default settings", () => {
cy.visit("/app/List/DocType/List");
cy.visit("/desk/List/DocType/List");
cy.clear_filters();
cy.get(".list-count").should("contain", "20 of");
cy.get(".list-stats").should("contain", "Tags");
});
it("disable count and sidebar stats then verify", () => {
cy.wait(300);
cy.visit("/app/List/DocType/List");
cy.visit("/desk/List/DocType/List");
cy.clear_filters();
cy.wait(300);
cy.get(".list-count").should("contain", "20 of");

View file

@ -36,7 +36,7 @@ context("Login", () => {
cy.get("#login_password").type(Cypress.env("adminPassword"));
cy.findByRole("button", { name: "Login" }).click();
cy.location("pathname").should("match", /^\/app/);
cy.location("pathname").should("match", /^\/desk/);
cy.window().its("frappe.session.user").should("eq", "Administrator");
});

View file

@ -1,7 +1,7 @@
context("MultiSelectDialog", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
const contact_template = {
doctype: "Contact",
first_name: "Test",

View file

@ -2,7 +2,7 @@ context("Navigation", () => {
before(() => {
cy.visit("/login");
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
it("Navigate to route with hash in document name", () => {
cy.insert_doc(
@ -15,14 +15,14 @@ context("Navigation", () => {
},
true
);
cy.visit(`/app/client-script/${encodeURIComponent("ABC#123")}`);
cy.visit(`/desk/client-script/${encodeURIComponent("ABC#123")}`);
cy.title().should("eq", "ABC#123");
cy.go("back");
cy.title().should("eq", "Website");
});
it("Navigate to previous page after login", () => {
cy.visit("/app/todo");
cy.visit("/desk/todo");
cy.get(".page-head").findByTitle("To Do").should("be.visible");
cy.clear_filters();
cy.call("logout");
@ -31,6 +31,6 @@ context("Navigation", () => {
cy.location("pathname").should("eq", "/login");
cy.login();
cy.reload().as("reload");
cy.location("pathname").should("eq", "/app/todo");
cy.location("pathname").should("eq", "/desk/todo");
});
});

View file

@ -1,7 +1,7 @@
context("Number Card", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
it("Check filter populate for child table doctype", () => {

View file

@ -2,11 +2,11 @@ context.skip("Permissions API", () => {
before(() => {
cy.visit("/login");
cy.remove_role("frappe@example.com", "System Manager");
cy.visit("/app");
cy.visit("/desk");
});
it("Checks permissions via `has_perm` for Kanban Board DocType", () => {
cy.visit("/app/kanban-board/view/list");
cy.visit("/desk/kanban-board/view/list");
cy.window()
.its("frappe")
.then((frappe) => {
@ -20,7 +20,7 @@ context.skip("Permissions API", () => {
});
it("Checks permissions via `get_perm` for Kanban Board DocType", () => {
cy.visit("/app/kanban-board/view/list");
cy.visit("/desk/kanban-board/view/list");
cy.window()
.its("frappe")
.then((frappe) => {

View file

@ -1,7 +1,7 @@
context("Query Report", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
cy.insert_doc(
"Report",
{
@ -19,7 +19,7 @@ context("Query Report", () => {
});
it("add custom column in report", () => {
cy.visit("/app/query-report/Permitted Documents For User");
cy.visit("/desk/query-report/Permitted Documents For User");
cy.get(".page-form.flex", { timeout: 60000 })
.should("have.length", 1)
@ -77,12 +77,12 @@ context("Query Report", () => {
.findByRole("button", { name: "Submit" })
.click({ timeout: 1000, force: true });
cy.visit("/app/query-report/" + report);
cy.visit("/desk/query-report/" + report);
cy.get(".datatable").should("exist");
};
it("test multi level query report", () => {
cy.visit("/app/query-report/Test ToDo Report");
cy.visit("/desk/query-report/Test ToDo Report");
cy.get(".datatable").should("exist");
save_report_and_open("Test ToDo Report 1", " 1");

View file

@ -4,7 +4,7 @@ const doctype_name = custom_submittable_doctype.name;
context("Report View", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
cy.insert_doc("DocType", custom_submittable_doctype, true);
cy.clear_cache();
cy.insert_doc(

View file

@ -1,7 +1,7 @@
context("Rounding behaviour", () => {
before(() => {
cy.login();
cy.visit("/app/");
cy.visit("/desk/");
});
it("Commercial Rounding", () => {

View file

@ -1,4 +1,4 @@
const list_view = "/app/todo";
const list_view = "/desk/todo";
// test round trip with filter types

View file

@ -38,7 +38,7 @@ context("Sidebar", () => {
before(() => {
cy.visit("/");
cy.login();
cy.visit("/app");
cy.visit("/desk");
return cy
.window()
.its("frappe")
@ -87,7 +87,7 @@ context("Sidebar", () => {
description: "Sidebar Attachment ToDo",
}).then((todo) => {
let todo_name = todo.message.name;
cy.visit("/app/todo");
cy.visit("/desk/todo");
cy.click_sidebar_button("Assigned To");
//To check if no filter is available in "Assigned To" dropdown
@ -99,7 +99,7 @@ context("Sidebar", () => {
cy.get_field("assign_to_me", "Check").click();
cy.wait(1000);
cy.get(".modal-footer > .standard-actions > .btn-primary").click();
cy.visit("/app/todo");
cy.visit("/desk/todo");
cy.click_sidebar_button("Assigned To");
//To check if filter is added in "Assigned To" dropdown after assignment
@ -136,7 +136,7 @@ context("Sidebar", () => {
cy.get(".assignments > .avatar-group > .avatar > .avatar-frame").click();
cy.get(".remove-btn").click({ force: true });
cy.hide_dialog();
cy.visit("/app/todo");
cy.visit("/desk/todo");
cy.click_sidebar_button("Assigned To");
cy.get(".empty-state").should("contain", "No filters found");
});

View file

@ -4,7 +4,7 @@ context("Realtime updates", () => {
});
beforeEach(() => {
cy.visit("/app/todo");
cy.visit("/desk/todo");
// required because immediately after load socket is still connecting.
// Not a huge deal breaker in prod.
cy.wait(500);

View file

@ -5,7 +5,7 @@ const doctype_name = data_field_validation_doctype.name;
context("URL Data Field Input", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
return cy.insert_doc("DocType", data_field_validation_doctype, true);
});

View file

@ -1,7 +1,7 @@
context("Utils", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
});
function run_util(name, ...args) {

View file

@ -1,11 +1,11 @@
context("View", () => {
before(() => {
cy.login();
cy.visit("/app/website");
cy.visit("/desk/website");
});
it("Route to ToDo List View", () => {
cy.visit("/app/todo/view/list");
cy.visit("/desk/todo/view/list");
cy.wait(500);
cy.window()
.its("cur_list")
@ -15,7 +15,7 @@ context("View", () => {
});
it("Route to ToDo Report View", () => {
cy.visit("/app/todo/view/report");
cy.visit("/desk/todo/view/report");
cy.wait(500);
cy.window()
.its("cur_list")
@ -25,7 +25,7 @@ context("View", () => {
});
it("Route to ToDo Dashboard View", () => {
cy.visit("/app/todo/view/dashboard");
cy.visit("/desk/todo/view/dashboard");
cy.wait(500);
cy.window()
.its("cur_list")
@ -35,7 +35,7 @@ context("View", () => {
});
it("Route to ToDo Gantt View", () => {
cy.visit("/app/todo/view/gantt");
cy.visit("/desk/todo/view/gantt");
cy.wait(500);
cy.window()
.its("cur_list")
@ -46,7 +46,7 @@ context("View", () => {
it("Route to ToDo Kanban View", () => {
cy.call("frappe.tests.ui_test_helpers.create_kanban").then(() => {
cy.visit("/app/note/view/kanban/_Note _Kanban");
cy.visit("/desk/note/view/kanban/_Note _Kanban");
cy.wait(500);
cy.window()
.its("cur_list")
@ -57,7 +57,7 @@ context("View", () => {
});
it("Route to ToDo Calendar View", () => {
cy.visit("/app/todo/view/calendar");
cy.visit("/desk/todo/view/calendar");
cy.wait(500);
cy.window()
.its("cur_list")
@ -68,7 +68,7 @@ context("View", () => {
it("Route to Custom Tree View", () => {
cy.call("frappe.tests.ui_test_helpers.setup_tree_doctype").then(() => {
cy.visit("/app/custom-tree/view/tree");
cy.visit("/desk/custom-tree/view/tree");
cy.wait(500);
cy.window()
.its("cur_tree")
@ -137,7 +137,7 @@ context("View", () => {
it("Route to default view from app/{doctype}", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => {
cy.visit("/app/event");
cy.visit("/desk/event");
cy.wait(500);
cy.window()
.its("cur_list")
@ -149,7 +149,7 @@ context("View", () => {
it("Route to default view from app/{doctype}/view", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => {
cy.visit("/app/event/view");
cy.visit("/desk/event/view");
cy.wait(500);
cy.window()
.its("cur_list")
@ -164,7 +164,7 @@ context("View", () => {
view: "Report",
force_reroute: true,
}).then(() => {
cy.visit("/app/event");
cy.visit("/desk/event");
cy.wait(500);
cy.window()
.its("cur_list")
@ -179,7 +179,7 @@ context("View", () => {
view: "Report",
force_reroute: true,
}).then(() => {
cy.visit("/app/event/view");
cy.visit("/desk/event/view");
cy.wait(500);
cy.window()
.its("cur_list")
@ -194,7 +194,7 @@ context("View", () => {
view: "Report",
force_reroute: true,
}).then(() => {
cy.visit("/app/event/view/list");
cy.visit("/desk/event/view/list");
cy.wait(500);
cy.window()
.its("cur_list")
@ -206,17 +206,17 @@ context("View", () => {
it("Validate Route History for Default View", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => {
cy.visit("/app/event");
cy.visit("/app/event/view/list");
cy.location("pathname").should("eq", "/app/event/view/list");
cy.visit("/desk/event");
cy.visit("/desk/event/view/list");
cy.location("pathname").should("eq", "/desk/event/view/list");
cy.go("back");
cy.location("pathname").should("eq", "/app/event");
cy.location("pathname").should("eq", "/desk/event");
});
});
it("Route to Form", () => {
const test_user = cy.config("testUser");
cy.visit(`/app/user/${test_user}`);
cy.visit(`/desk/user/${test_user}`);
cy.window()
.its("cur_frm")
.then((frm) => {
@ -225,7 +225,7 @@ context("View", () => {
});
it("Route to Website Workspace", () => {
cy.visit("/app/website");
cy.visit("/desk/website");
cy.get(".title-text").should("contain", "Website");
});
});

View file

@ -1,7 +1,7 @@
context("Web Form", () => {
before(() => {
cy.login("Administrator");
cy.visit("/app/");
cy.visit("/desk/");
return cy
.window()
.its("frappe")
@ -11,7 +11,7 @@ context("Web Form", () => {
});
it("Create Web Form", () => {
cy.visit("/app/web-form/new");
cy.visit("/desk/web-form/new");
cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
@ -40,7 +40,11 @@ context("Web Form", () => {
cy.url().should("include", "/note/new");
cy.fill_field("title", "Guest Note 1");
cy.get(".web-form-actions button").contains("Save").click();
cy.window()
.its("__")
.then((__) => {
cy.get(".web-form-actions button").contains(__("Save")).click();
});
cy.url().should("include", "/note/new");
@ -51,7 +55,7 @@ context("Web Form", () => {
it("Login Required", () => {
cy.call("logout");
cy.login("Administrator");
cy.visit("/app/web-form/note");
cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
cy.get('input[data-fieldname="login_required"]').check({ force: true });
@ -70,7 +74,7 @@ context("Web Form", () => {
it("Show List", () => {
cy.login("Administrator");
cy.visit("/app/web-form/note");
cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
cy.get(".section-head").contains("List Settings").click();
@ -84,7 +88,7 @@ context("Web Form", () => {
});
it("Show Custom List Title", () => {
cy.visit("/app/web-form/note");
cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
@ -107,7 +111,7 @@ context("Web Form", () => {
cy.get(".web-list-table thead th").contains("Sr.");
cy.get(".web-list-table thead th").contains("Title");
cy.visit("/app/web-form/note");
cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
@ -156,7 +160,7 @@ context("Web Form", () => {
});
it("Custom Breadcrumbs", () => {
cy.visit("/app/web-form/note");
cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Customization" }).click();
cy.fill_field("breadcrumbs", '[{"label": _("Notes"), "route":"note"}]', "Code");
@ -188,7 +192,7 @@ context("Web Form", () => {
});
it("Edit Mode", () => {
cy.visit("/app/web-form/note");
cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
cy.get('input[data-fieldname="allow_edit"]').check();
@ -212,7 +216,7 @@ context("Web Form", () => {
});
it("Allow Multiple Response", () => {
cy.visit("/app/web-form/note");
cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
cy.get('input[data-fieldname="allow_multiple"]').check();
@ -230,7 +234,7 @@ context("Web Form", () => {
});
it("Allow Delete", () => {
cy.visit("/app/web-form/note");
cy.visit("/desk/web-form/note");
cy.findByRole("tab", { name: "Settings" }).click();
cy.get('input[data-fieldname="allow_delete"]').check();

View file

@ -2,16 +2,23 @@ context("Workspace 2.0", () => {
before(() => {
cy.visit("/login");
cy.login();
return cy
.window()
.its("frappe")
.then((frappe) => {
return frappe.xcall("frappe.tests.ui_test_helpers.empty_my_workspaces");
});
});
it("Navigate to page from sidebar", () => {
cy.visit("/app/build");
cy.visit("/desk/build");
cy.get(".codex-editor__redactor .ce-block");
cy.get('.sidebar-item-container[item-title="Website"]').first().click();
cy.location("pathname").should("eq", "/app/website");
cy.get('.sidebar-item-container[item-name="Page"]').first().click();
cy.location("pathname").should("eq", "/desk/page");
});
it("Create Private Page", () => {
cy.visit("/desk/website");
cy.intercept({
method: "POST",
url: "api/method/frappe.desk.doctype.workspace.workspace.new_page",
@ -27,61 +34,16 @@ context("Workspace 2.0", () => {
cy.get_open_dialog().find(".btn-primary").click();
// check if sidebar item is added in pubic section
cy.get('.sidebar-item-container[item-title="Test Private Page"]').should(
"have.attr",
"item-public",
"0"
);
cy.get('.sidebar-item-container[item-name="Test Private Page"]');
cy.wait(300);
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.wait(300);
cy.get('.sidebar-item-container[item-title="Test Private Page"]').should(
"have.attr",
"item-public",
"0"
);
cy.wait("@new_page");
});
it("Create Child Page", () => {
cy.intercept({
method: "POST",
url: "api/method/frappe.desk.doctype.workspace.workspace.new_page",
}).as("new_page");
cy.get(".codex-editor__redactor .ce-block");
cy.get(".btn-new-workspace").click();
cy.fill_field("title", "Test Child Page", "Data");
cy.fill_field("parent", "Test Private Page", "Select");
cy.fill_field("type", "Workspace", "Select");
cy.get_open_dialog().find(".modal-header").click();
cy.wait(300);
cy.get_open_dialog().find(".btn-primary").click();
// check if sidebar item is added in pubic section
cy.get('.sidebar-item-container[item-title="Test Child Page"]').should(
"have.attr",
"item-public",
"0"
);
cy.wait(300);
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.wait(300);
cy.get('.sidebar-item-container[item-title="Test Child Page"]').should(
"have.attr",
"item-public",
"0"
);
cy.get('.sidebar-item-container[item-name="Test Private Page"]');
cy.wait("@new_page");
});
it("Add New Block", () => {
cy.get('.sidebar-item-container[item-title="Test Private Page"]').as("sidebar-item");
cy.get("@sidebar-item").find(".standard-sidebar-item").first().click({ force: true });
cy.get(".btn-edit-workspace").click({ force: true });
cy.get(".ce-block").click().type("{enter}");

View file

@ -1,7 +1,7 @@
context("Workspace Blocks", () => {
before(() => {
cy.login();
cy.visit("/app");
cy.visit("/desk");
return cy
.window()
.its("frappe")
@ -11,12 +11,13 @@ context("Workspace Blocks", () => {
});
it("Create Test Page", () => {
cy.remove_doc("Workspace", `Test Block Page-${Cypress.config("testUser")}`, true);
cy.intercept({
method: "POST",
url: "api/method/frappe.desk.doctype.workspace.workspace.new_page",
}).as("new_page");
cy.visit("/app/website");
cy.visit("/desk/website");
cy.get(".codex-editor__redactor .ce-block");
cy.get(".btn-new-workspace").click();
cy.fill_field("title", "Test Block Page", "Data");
@ -25,19 +26,11 @@ context("Workspace Blocks", () => {
cy.get_open_dialog().find(".btn-primary").click();
// check if sidebar item is added in private section
cy.get('.sidebar-item-container[item-title="Test Block Page"]').should(
"have.attr",
"item-public",
"0"
);
cy.get('.sidebar-item-container[item-name="Test Block Page"]');
cy.wait(300);
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.wait(300);
cy.get('.sidebar-item-container[item-title="Test Block Page"]').should(
"have.attr",
"item-public",
"0"
);
cy.get('.sidebar-item-container[item-name="Test Block Page"]');
cy.wait("@new_page");
});

View file

@ -125,7 +125,7 @@ Cypress.Commands.add("get_doc", (doctype, name) => {
});
});
Cypress.Commands.add("remove_doc", (doctype, name) => {
Cypress.Commands.add("remove_doc", (doctype, name, ignore_missing) => {
return cy
.window()
.its("frappe.csrf_token")
@ -138,9 +138,9 @@ Cypress.Commands.add("remove_doc", (doctype, name) => {
Accept: "application/json",
"X-Frappe-CSRF-Token": csrf_token,
},
failOnStatusCode: !ignore_missing,
})
.then((res) => {
expect(res.status).eq(202);
return res.body;
});
});

View file

@ -1355,9 +1355,9 @@ def get_list(doctype, *args, **kwargs):
# filter as a list of lists
frappe.get_list("ToDo", fields="*", filters = [["modified", ">", "2014-01-01"]])
"""
import frappe.model.db_query
import frappe.model.qb_query
return frappe.model.db_query.DatabaseQuery(doctype).execute(*args, **kwargs)
return frappe.model.qb_query.DatabaseQuery(doctype).execute(*args, **kwargs)
def get_all(doctype, *args, **kwargs):

View file

@ -78,12 +78,13 @@ def read_doc(doctype: str, name: str):
doc = frappe.get_doc(doctype, name)
doc.check_permission("read")
doc.apply_fieldlevel_read_permissions()
doc_dict = doc.as_dict()
if sbool(frappe.form_dict.get("expand_links")):
doc_dict = doc.as_dict()
get_values_for_link_and_dynamic_link_fields(doc_dict)
get_values_for_table_and_multiselect_fields(doc_dict)
return doc_dict
return doc_dict
return doc
def get_values_for_link_and_dynamic_link_fields(doc_dict):

View file

@ -79,9 +79,9 @@ def get_default_path():
return get_route(user_default_app)
if len(_apps) == 1:
return _apps[0].get("route") or "/apps"
return _apps[0].get("route") or "/desk"
elif is_desk_apps(_apps):
return "/app"
return "/desk"
return "/apps"

View file

@ -201,7 +201,7 @@ class LoginManager:
frappe.local.cookie_manager.set_cookie("system_user", "yes", deduplicate=True)
if not resume:
frappe.local.response["message"] = "Logged In"
frappe.local.response["home_page"] = get_default_path() or "/app"
frappe.local.response["home_page"] = get_default_path() or "/desk"
if not resume:
frappe.response["full_name"] = self.full_name

View file

@ -85,14 +85,17 @@ frappe.ui.form.on("Auto Repeat", {
},
preview_message: function (frm) {
if (frm.is_dirty()) {
frappe.msgprint(__("Please save the form before previewing the message"));
return;
}
if (frm.doc.message) {
frappe.call({
method: "frappe.automation.doctype.auto_repeat.auto_repeat.generate_message_preview",
type: "POST",
args: {
reference_dt: frm.doc.reference_doctype,
reference_doc: frm.doc.reference_document,
subject: frm.doc.subject,
message: frm.doc.message,
name: frm.doc.name,
},
callback: function (r) {
if (r.message) {

View file

@ -605,14 +605,15 @@ def update_reference(docname: str, reference: str):
return "success" # backward compatbility
@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
@frappe.whitelist(methods=["POST"])
def generate_message_preview(name: str):
frappe.has_permission("Auto Repeat", "write", throw=True)
doc = frappe.get_doc(reference_dt, reference_doc)
auto_repeat = frappe.get_doc("Auto Repeat", str(name))
doc = frappe.get_doc(auto_repeat.reference_doctype, auto_repeat.reference_document)
doc.check_permission()
subject_preview = _("Please add a subject to your email")
msg_preview = frappe.render_template(message, {"doc": doc})
if subject:
subject_preview = frappe.render_template(subject, {"doc": doc})
msg_preview = frappe.render_template(auto_repeat.message, {"doc": doc})
if auto_repeat.subject:
subject_preview = frappe.render_template(auto_repeat.subject, {"doc": doc})
return {"message": msg_preview, "subject": subject_preview}

View file

@ -1,345 +0,0 @@
{
"charts": [],
"content": "[{\"id\":\"sR-UFcO7II\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Import Data\",\"col\":3}},{\"id\":\"IkcVmgWb3z\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"id\":\"6wir-jZFRE\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"id\":\"45a1jzQkTm\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"id\":\"EgtURZsoiF\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"0yceBIfhHM\",\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"id\":\"42WbBA9rpj\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"wE9n7TIrAc\",\"type\":\"card\",\"data\":{\"card_name\":\"Alerts and Notifications\",\"col\":4}},{\"id\":\"7_U7_xCOos\",\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"id\":\"3imoh2oqsJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"SlYKJZj5r3\",\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}}]",
"creation": "2020-03-02 14:53:24.980279",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
"is_hidden": 0,
"label": "Tools",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Automation",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Assignment Rule",
"link_count": 0,
"link_to": "Assignment Rule",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Milestone",
"link_count": 0,
"link_to": "Milestone",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Auto Repeat",
"link_count": 0,
"link_to": "Auto Repeat",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Tools",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "To Do",
"link_count": 0,
"link_to": "ToDo",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Calendar",
"link_count": 0,
"link_to": "Event",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Note",
"link_count": 0,
"link_to": "Note",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Files",
"link_count": 0,
"link_to": "File",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Printing",
"link_count": 4,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Print Format Builder",
"link_count": 0,
"link_to": "print-format-builder",
"link_type": "Page",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Print Format Builder (New)",
"link_count": 0,
"link_to": "print-format-builder-beta",
"link_type": "Page",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Print Settings",
"link_count": 0,
"link_to": "Print Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Print Heading",
"link_count": 0,
"link_to": "Print Heading",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Alerts and Notifications",
"link_count": 3,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Notification",
"link_count": 0,
"link_to": "Notification",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Auto Email Report",
"link_count": 0,
"link_to": "Auto Email Report",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Notification Settings",
"link_count": 0,
"link_to": "Notification Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"description": "Manage your data",
"hidden": 0,
"is_query_report": 0,
"label": "Data",
"link_count": 5,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Import Data",
"link_count": 0,
"link_to": "Data Import",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Export Data",
"link_count": 0,
"link_to": "Data Export",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Bulk Update",
"link_count": 0,
"link_to": "Bulk Update",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Download Backups",
"link_count": 0,
"link_to": "backups",
"link_type": "Page",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Deleted Documents",
"link_count": 0,
"link_to": "Deleted Document",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Email",
"link_count": 4,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Email Account",
"link_count": 0,
"link_to": "Email Account",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Email Domain",
"link_count": 0,
"link_to": "Email Domain",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Email Template",
"link_count": 0,
"link_to": "Email Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Email Group",
"link_count": 0,
"link_to": "Email Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2025-06-27 11:39:44.392114",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 1.0,
"shortcuts": [
{
"color": "Grey",
"doc_view": "List",
"label": "Import Data",
"link_to": "Data Import",
"type": "DocType"
},
{
"doc_view": "",
"label": "ToDo",
"link_to": "ToDo",
"stats_filter": "[[\"ToDo\",\"status\",\"=\",\"Open\",false]]",
"type": "DocType"
},
{
"label": "File",
"link_to": "File",
"type": "DocType"
},
{
"label": "Assignment Rule",
"link_to": "Assignment Rule",
"type": "DocType"
}
],
"title": "Tools"
}

View file

@ -14,6 +14,7 @@ from frappe.core.doctype.installed_applications.installed_applications import (
)
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
from frappe.desk.doctype.changelog_feed.changelog_feed import get_changelog_feed_items
from frappe.desk.doctype.desktop_icon.desktop_icon import get_desktop_icons
from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle
@ -41,6 +42,7 @@ def get_bootinfo():
# user
get_user(bootinfo)
# desktop icon info
# system info
bootinfo.sitename = frappe.local.site
@ -55,6 +57,7 @@ def get_bootinfo():
bootinfo.modules = {}
bootinfo.module_list = []
load_desktop_data(bootinfo)
bootinfo.desktop_icons = get_desktop_icons(bootinfo=bootinfo)
bootinfo.letter_heads = get_letter_heads()
bootinfo.active_domains = frappe.get_active_domains()
bootinfo.all_domains = [d.get("name") for d in frappe.get_all("Domain")]
@ -148,8 +151,12 @@ def load_conf_settings(bootinfo):
def load_desktop_data(bootinfo):
from frappe.desk.desktop import get_workspace_sidebar_items
bootinfo.sidebar_pages = get_workspace_sidebar_items()
allowed_pages = [d.name for d in bootinfo.sidebar_pages.get("pages")]
bootinfo.workspaces = get_workspace_sidebar_items()
bootinfo.show_app_icons_as_folder = frappe.db.get_single_value(
"Desktop Settings", "show_app_icons_as_folder"
)
bootinfo.workspace_sidebar_item = get_sidebar_items()
allowed_pages = [d.name for d in bootinfo.workspaces.get("pages")]
bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces()
bootinfo.dashboards = frappe.get_all("Dashboard")
bootinfo.app_data = []
@ -196,7 +203,7 @@ def load_desktop_data(bootinfo):
frappe.get_hooks("app_home", app_name=app_name)
and frappe.get_hooks("app_home", app_name=app_name)[0]
)
or (workspaces and "/app/" + frappe.utils.slug(workspaces[0]))
or (workspaces and "/desk/" + frappe.utils.slug(workspaces[0]))
or "",
app_logo_url=app_info.get("logo")
or frappe.get_hooks("app_logo_url", app_name=app_name)
@ -360,7 +367,7 @@ def add_home_page(bootinfo, docs):
bootinfo["home_page"] = page.name
except (frappe.DoesNotExistError, frappe.PermissionError):
frappe.clear_last_message()
bootinfo["home_page"] = "Workspaces"
bootinfo["home_page"] = "desktop"
def add_timezone_info(bootinfo):
@ -518,3 +525,68 @@ def get_sentry_dsn():
return
return os.getenv("FRAPPE_SENTRY_DSN")
def get_sidebar_items():
sidebars = frappe.get_all(
"Workspace Sidebar", fields=["name", "header_icon"], filters={"name": ["not like", "%My Workspaces%"]}
)
add_user_specific_sidebar(sidebars)
sidebar_items = {}
for s in sidebars:
w = frappe.get_doc("Workspace Sidebar", s["name"])
sidebar_items[s["name"].lower()] = {
"label": s["name"],
"items": [],
"header_icon": s["header_icon"],
"module": w.module,
}
for si in w.items:
workspace_sidebar = {
"label": si.label,
"link_to": si.link_to,
"link_type": si.link_type,
"type": si.type,
"icon": si.icon,
"child": si.child,
"collapsible": si.collapsible,
"indent": si.indent,
"keep_closed": si.keep_closed,
"display_depends_on": si.display_depends_on,
"url": si.url,
"show_arrow": si.show_arrow,
"filters": si.filters,
"route_options": si.route_options,
}
if si.link_type == "Report" and si.link_to:
report_type, ref_doctype = frappe.db.get_value(
"Report", si.link_to, ["report_type", "ref_doctype"]
)
workspace_sidebar["report"] = {
"report_type": report_type,
"ref_doctype": ref_doctype,
}
if (
"My Workspaces" in s["name"]
or si.type == "Section Break"
or w.is_item_allowed(si.link_to, si.link_type)
):
sidebar_items[s["name"].lower()]["items"].append(workspace_sidebar)
old_name = f"my workspaces-{frappe.session.user}"
if old_name in sidebar_items:
sidebar_items["my workspaces"] = sidebar_items.pop(old_name)
return sidebar_items
def add_user_specific_sidebar(sidebars):
try:
my_workspace_for_user = frappe.get_doc("Workspace Sidebar", f"My Workspaces-{frappe.session.user}")
sidebars.append(
{"name": my_workspace_for_user.name, "header_icon": my_workspace_for_user.header_icon}
)
except frappe.DoesNotExistError:
my_workspace = frappe.get_doc("Workspace Sidebar", "My Workspaces")
sidebars.append({"name": my_workspace.name, "header_icon": my_workspace.header_icon})

View file

@ -1584,6 +1584,33 @@ def bypass_patch(context: CliCtxObj, patch_name: str, yes: bool):
frappe.destroy()
@click.command("create-desktop-icons-and-sidebar")
@pass_context
def create_icons_and_sidebar(context: CliCtxObj):
"""Create desktop icons and workspace sidebars."""
from frappe.desk.doctype.desktop_icon.desktop_icon import create_desktop_icons
from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import (
create_workspace_sidebar_for_workspaces,
)
if not context.sites:
raise SiteNotSpecifiedError
for site in context.sites:
frappe.init(site)
frappe.connect()
try:
print("Creating Desktop Icons")
create_desktop_icons()
print("Creating Workspace Sidebars")
create_workspace_sidebar_for_workspaces()
# Saving it in a command need it
frappe.db.commit() # nosemgrep
except Exception as e:
print(f"Error creating icons {site}: {e}")
finally:
frappe.destroy()
commands = [
add_system_manager,
add_user_for_sites,
@ -1620,4 +1647,5 @@ commands = [
trim_database,
clear_log_table,
bypass_patch,
create_icons_and_sidebar,
]

View file

@ -469,11 +469,10 @@ class TestCommands(BaseTestCommands):
self.assertEqual(check_password("Administrator", original_password), "Administrator")
@skipIf(
not (frappe.conf.root_password and frappe.conf.admin_password and frappe.conf.db_type == "mariadb"),
not (frappe.conf.root_password and frappe.conf.admin_password and frappe.conf.db_type != "sqlite"),
"DB Root password and Admin password not set in config",
)
def test_bench_drop_site_should_archive_site(self):
# TODO: Make this test postgres compatible
site = TEST_SITE
self.execute(
@ -499,7 +498,7 @@ class TestCommands(BaseTestCommands):
self.assertTrue(os.path.exists(archive_directory))
@skipIf(
not (frappe.conf.root_password and frappe.conf.admin_password and frappe.conf.db_type == "mariadb"),
not (frappe.conf.root_password and frappe.conf.admin_password and frappe.conf.db_type != "sqlite"),
"DB Root password and Admin password not set in config",
)
def test_force_install_app(self):
@ -656,10 +655,10 @@ class TestBackups(BaseTestCommands):
except OSError:
pass
@run_only_if(db_type_is.MARIADB)
def test_backup_no_options(self):
"""Take a backup without any options"""
before_backup = fetch_latest_backups(partial=True)
time.sleep(1)
self.execute("bench --site {site} backup")
after_backup = fetch_latest_backups(partial=True)
@ -1003,9 +1002,11 @@ class TestDBCli(BaseTestCommands):
self.execute("bench --site {site} db-console", kwargs={"cmd_input": cmd_input})
self.assertEqual(self.returncode, 0)
@run_only_if(db_type_is.MARIADB)
def test_db_cli_with_sql(self):
self.execute("bench --site {site} db-console -e 'select 1'")
if frappe.db.db_type == "postgres":
self.execute("bench --site {site} db-console -c 'select 1'")
elif frappe.db.db_type == "mariadb":
self.execute("bench --site {site} db-console -e 'select 1'")
self.assertEqual(self.returncode, 0)
self.assertIn("1", self.stdout)

View file

@ -437,6 +437,7 @@ def run_parallel_tests(
@click.option("--parallel", is_flag=True, help="Run UI Test in parallel mode")
@click.option("--with-coverage", is_flag=True, help="Generate coverage report")
@click.option("--browser", default="chrome", help="Browser to run tests in")
@click.option("--spec", help="Spec file to run")
@click.option("--ci-build-id")
@pass_context
def run_ui_tests(
@ -448,6 +449,7 @@ def run_ui_tests(
browser="chrome",
ci_build_id=None,
cypressargs=None,
spec=None,
):
"Run UI tests"
site = get_site(context)
@ -494,6 +496,8 @@ def run_ui_tests(
# run for headless mode
run_or_open = f"run --browser {browser}" if headless else "open"
if headless and spec:
run_or_open += f" --spec {spec}"
formatted_command = f"{site_env} {password_env} {coverage_env} {cypress_path} {run_or_open}"
if os.environ.get("CYPRESS_RECORD_KEY"):

View file

@ -310,7 +310,7 @@ def get_address_display_list(doctype: str, name: str) -> list[dict]:
["Dynamic Link", "parenttype", "=", "Address"],
],
fields=["*"],
order_by="is_primary_address DESC, `tabAddress`.creation ASC",
order_by="is_primary_address DESC, creation ASC",
)
for a in address_list:
a["display"] = get_address_display(a)

View file

@ -486,7 +486,7 @@ def get_contact_display_list(doctype: str, name: str) -> list[dict]:
["Dynamic Link", "parenttype", "=", "Contact"],
],
fields=["*"],
order_by="is_primary_contact DESC, `tabContact`.creation ASC",
order_by="is_primary_contact DESC, creation ASC",
)
for contact in contact_list:

View file

@ -0,0 +1,34 @@
{
"based_on": "creation",
"chart_name": "Background Job Activity",
"chart_type": "Count",
"creation": "2025-09-08 11:56:13.469137",
"currency": "",
"docstatus": 0,
"doctype": "Dashboard Chart",
"document_type": "Scheduled Job Type",
"dynamic_filters_json": "[]",
"filters_json": "[]",
"group_by_type": "Count",
"idx": 0,
"is_public": 0,
"is_standard": 1,
"last_synced_on": "2025-10-30 21:36:33.646973",
"modified": "2025-10-30 21:37:11.340673",
"modified_by": "Administrator",
"module": "Core",
"name": "Background Job Activity",
"number_of_groups": 0,
"owner": "Administrator",
"parent_document_type": "",
"roles": [],
"show_values_over_chart": 0,
"source": "",
"time_interval": "Daily",
"timeseries": 1,
"timespan": "Last Year",
"type": "Line",
"use_report_chart": 0,
"value_based_on": "",
"y_axis": []
}

View file

@ -50,7 +50,7 @@ class TestActivityLog(IntegrationTestCase):
"user": "Administrator",
"operation": operation,
},
order_by="`creation` DESC",
order_by="creation DESC",
)
name = names[0]

View file

@ -34,9 +34,10 @@
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-05-21 17:09:55.054044",
"modified": "2025-10-29 11:26:28.177653",
"modified_by": "Administrator",
"module": "Core",
"name": "API Request Log",

View file

@ -236,7 +236,7 @@ def get_import_status(data_import_name: str):
import_status = {"status": data_import.status}
logs = frappe.get_all(
"Data Import Log",
fields=["count(*) as count", "success"],
fields=[{"COUNT": "*", "as": "count"}, "success"],
filters={"data_import": data_import_name},
group_by="success",
)

View file

@ -165,9 +165,9 @@ class Exporter:
filters = self.export_filters
if self.meta.is_nested_set():
order_by = f"`tab{self.doctype}`.`lft` ASC"
order_by = "lft ASC"
else:
order_by = f"`tab{self.doctype}`.`creation` DESC"
order_by = "creation DESC"
parent_fields = [format_column_name(df) for df in self.fields if df.parent == self.doctype]
parent_data = frappe.db.get_list(

View file

@ -153,24 +153,25 @@ class TestDocType(IntegrationTestCase):
def test_all_depends_on_fields_conditions(self):
import re
docfields = frappe.get_all(
"DocField",
or_filters={
"ifnull(depends_on, '')": ("!=", ""),
"ifnull(collapsible_depends_on, '')": ("!=", ""),
"ifnull(mandatory_depends_on, '')": ("!=", ""),
"ifnull(read_only_depends_on, '')": ("!=", ""),
},
fields=[
"parent",
"depends_on",
"collapsible_depends_on",
"mandatory_depends_on",
"read_only_depends_on",
"fieldname",
"fieldtype",
],
DocField = frappe.qb.DocType("DocField")
docfields_query = (
frappe.qb.from_(DocField)
.select(
DocField.parent,
DocField.depends_on,
DocField.collapsible_depends_on,
DocField.mandatory_depends_on,
DocField.read_only_depends_on,
DocField.fieldname,
)
.where(
(DocField.depends_on != "")
| (DocField.collapsible_depends_on != "")
| (DocField.mandatory_depends_on != "")
| (DocField.read_only_depends_on != "")
)
)
docfields = docfields_query.run(as_dict=True)
pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+'
for field in docfields:
@ -840,7 +841,20 @@ class TestDocType(IntegrationTestCase):
],
).insert(ignore_if_duplicate=True)
decimal_field_type = frappe.db.get_column_type(doctype.name, "decimal_field")
self.assertIn("(30,3)", decimal_field_type.lower())
if frappe.db.db_type == "postgres":
result = frappe.db.sql(
"""
SELECT numeric_precision, numeric_scale
FROM information_schema.columns
WHERE lower(table_name) = lower(%s)
AND column_name = %s
""",
(f"tab{doctype.name}", "decimal_field"),
)
length, precision = result[0]
self.assertEqual((length, precision), (30, 3))
elif frappe.db.db_type == "mariadb":
self.assertIn("(30,3)", decimal_field_type.lower())
def test_decimal_field_precision_exceeds_length(self):
doctype = new_doctype(

View file

@ -56,7 +56,15 @@ class InstalledApplications(Document):
},
)
self.save()
try:
savepoint = "update_installed_apps"
frappe.db.savepoint(savepoint)
self.save()
except frappe.db.DataError:
frappe.db.rollback(save_point=savepoint)
# Tolerate primary key change on versions during migrate
self.save(ignore_version=True)
frappe.clear_cache(doctype="System Settings")
frappe.db.set_single_value("System Settings", "setup_complete", frappe.is_setup_complete())

View file

@ -13,6 +13,7 @@ frappe.ui.form.on("Permission Inspector", {
ref_doctype(frm) {
frm.doc.docname = ""; // Usually doctype change invalidates docname
call_debug(frm);
frm.trigger("add_custom_perm_types");
},
user: call_debug,
permission_type: call_debug,
@ -21,4 +22,21 @@ frappe.ui.form.on("Permission Inspector", {
frm.call("debug");
}
},
add_custom_perm_types(frm) {
if (!frm.doc.ref_doctype) return;
const doctype_ptype_map = frm.doc.__onload.doctype_ptype_map;
if (!Object.keys(doctype_ptype_map).length) return;
const standard_options = frm.meta.fields.find(
(f) => f.fieldname === "permission_type"
).options;
const custom_options = doctype_ptype_map[frm.doc.ref_doctype]?.join("\n");
frm.set_df_property(
"permission_type",
"options",
`${standard_options}\n${custom_options}`
);
},
});

View file

@ -37,6 +37,11 @@ class PermissionInspector(Document):
user: DF.Link
# end: auto-generated types
def onload(self):
from frappe.core.doctype.permission_type.permission_type import get_doctype_ptype_map
self.set_onload("doctype_ptype_map", get_doctype_ptype_map())
@frappe.whitelist()
def debug(self):
if not (self.ref_doctype and self.user):

View file

@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Permission Type", {
// refresh(frm) {
// },
// });

View file

@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2025-07-28 13:12:03.573433",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"perm_type",
"doc_type"
],
"fields": [
{
"fieldname": "perm_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Permission Type",
"reqd": 1
},
{
"fieldname": "doc_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Applies To (DocType)",
"options": "DocType",
"reqd": 1
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-11-13 16:17:58.536849",
"modified_by": "Administrator",
"module": "Core",
"name": "Permission Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
}
],
"read_only": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View file

@ -0,0 +1,135 @@
# Copyright (c) 2025, Frappe Technologies and contributors
# For license information, please see license.txt
from collections import defaultdict
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.modules.export_file import delete_folder
from frappe.utils.caching import site_cache
# doctypes where custom fields for permission types will be created
CUSTOM_FIELD_TARGET = ["Custom DocPerm", "DocPerm", "DocShare"]
class PermissionType(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
doc_type: DF.Link
perm_type: DF.Data
# end: auto-generated types
def autoname(self):
self.name = f"{frappe.scrub(self.doc_type)}_{frappe.scrub(self.perm_type)}"
def before_insert(self):
self.perm_type = frappe.scrub(self.perm_type)
def validate(self):
from frappe.permissions import std_rights
if self.perm_type in std_rights:
frappe.throw(
_("Permission Type '{0}' is reserved. Please choose another name.").format(self.perm_type)
)
def can_write(self):
return (
frappe.conf.developer_mode
or frappe.flags.in_migrate
or frappe.flags.in_install
or frappe.flags.in_test
)
def should_export(self):
return (
frappe.conf.developer_mode
and not frappe.flags.in_migrate
and not frappe.flags.in_install
and not frappe.flags.in_test
)
def get_folder_path(self):
app = frappe.get_doctype_app(self.doc_type)
folder = frappe.get_app_source_path(app, app, "permission_types")
return folder
def on_update(self):
if not self.can_write():
frappe.throw(_("Creation of this document is only permitted in developer mode."))
for target in CUSTOM_FIELD_TARGET:
self.create_custom_field(target)
if self.should_export():
from frappe.modules.export_file import export_to_files
module = frappe.db.get_value("DocType", self.doc_type, "module")
export_to_files(record_list=[["Permission Type", self.name]], record_module=module)
def before_export(self, export_doc):
del export_doc["idx"]
del export_doc["docstatus"]
for key in list(export_doc.keys()):
if key.startswith("_"):
del export_doc[key]
def create_custom_field(self, target):
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
if not self.custom_field_exists(target):
field = "share_doctype" if target == "DocShare" else "parent"
depends_on = f"eval:doc.{field} == '{self.doc_type}'"
create_custom_field(
target,
{
"fieldname": self.perm_type,
"label": frappe.unscrub(self.perm_type),
"fieldtype": "Check",
"insert_after": "append",
"depends_on": depends_on,
},
)
def on_trash(self):
if not self.can_write():
frappe.throw(_("Deletion of this document is only permitted in developer mode."))
for target in CUSTOM_FIELD_TARGET:
self.delete_custom_field(target)
if self.should_export():
module = frappe.db.get_value("DocType", self.doc_type, "module")
delete_folder(module, "Permission Type", self.name)
def delete_custom_field(self, target):
if name := self.custom_field_exists(target):
frappe.delete_doc("Custom Field", name)
def custom_field_exists(self, target):
return frappe.db.exists(
"Custom Field",
{
"fieldname": self.perm_type,
"dt": target,
},
)
@site_cache
def get_doctype_ptype_map():
ptypes = frappe.get_all("Permission Type", fields=["perm_type", "doc_type"], order_by="perm_type")
doctype_ptype_map = defaultdict(list)
for pt in ptypes:
if pt.perm_type not in doctype_ptype_map[pt.doc_type]:
doctype_ptype_map[pt.doc_type].append(pt.perm_type)
return dict(doctype_ptype_map)

View file

@ -0,0 +1,103 @@
# Copyright (c) 2025, Frappe Technologies and Contributors
# See license.txt
import frappe
from frappe.permissions import update_permission_property
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestPermissionType(IntegrationTestCase):
"""
Integration tests for PermissionType.
Use this class for testing interactions between multiple components.
"""
def test_approve_ptype_on_blog_post(self):
"""Test that custom permission types are applied correctly."""
user_role = "Website Manager"
doc_type = "Web Page"
ptype_name = "approve"
user = self._create_test_user("test_approve_permission@example.com", user_role)
ptype_doc = self._create_permission_type(ptype_name, doc_type)
try:
self._verify_custom_fields_created(ptype_doc, doc_type)
self._verify_user_lacks_permission(doc_type, ptype_name, user.name)
update_permission_property(
doctype=doc_type, role=user_role, permlevel=0, ptype=ptype_name, value=1
)
self._verify_user_has_permission(doc_type, ptype_name, user.name)
update_permission_property(
doctype=doc_type, role=user_role, permlevel=0, ptype=ptype_name, value=0
)
finally:
frappe.delete_doc("User", user.name, force=True)
frappe.delete_doc("Permission Type", ptype_doc.name, force=True)
def test_permission_type_creation_reserved_name(self):
"""Test that permission types with reserved names are rejected."""
doc = frappe.get_doc(
{
"doctype": "Permission Type",
"perm_type": "read",
"doc_type": "ToDo",
"module": "Core",
}
)
with self.assertRaises(frappe.exceptions.ValidationError):
doc.insert()
def _create_test_user(self, email, role):
"""Create a test user with the specified role."""
user = frappe.new_doc("User")
user.email = email
user.first_name = email.split("@", 1)[0]
user.insert(ignore_if_duplicate=True)
user.reload()
user.add_roles(role)
return user
def _create_permission_type(self, name, doc_type):
"""Create a permission type for the specified doctype."""
ptype_doc = frappe.get_doc(
{
"doctype": "Permission Type",
"perm_type": name,
"doc_type": doc_type,
"module": "Core",
}
)
ptype_doc.insert(ignore_if_duplicate=True)
ptype_doc.reload()
return ptype_doc
def _verify_custom_fields_created(self, ptype_doc, doc_type):
"""Verify that custom fields are created for the permission type."""
for target in ["Custom DocPerm", "DocPerm", "DocShare"]:
custom_field = frappe.get_doc("Custom Field", {"dt": target, "fieldname": ptype_doc.perm_type})
self.assertEqual(custom_field.dt, target)
self.assertEqual(custom_field.fieldname, ptype_doc.perm_type)
self.assertEqual(custom_field.fieldtype, "Check")
self.assertIn(doc_type, custom_field.depends_on)
def _verify_user_lacks_permission(self, doc_type, ptype_name, user_name):
"""Verify that user does not have the specified permission type."""
self.assertFalse(frappe.has_permission(doc_type, ptype=ptype_name, user=user_name))
def _verify_user_has_permission(self, doc_type, ptype_name, user_name):
"""Verify that user has the specified permission type."""
self.assertTrue(frappe.has_permission(doc_type, ptype=ptype_name, user=user_name))

View file

@ -63,9 +63,12 @@ class TestPreparedReport(IntegrationTestCase):
self.assertEqual(len(prepared_data["result"]), len(generated_data["result"]))
self.assertEqual(len(prepared_data), len(generated_data))
@run_only_if(db_type_is.MARIADB)
def test_start_status_and_kill_jobs(self):
with test_report(report_type="Query Report", query="select sleep(10)") as report:
if frappe.db.db_type == "postgres":
query = "select pg_sleep(5)"
elif frappe.db.db_type == "mariadb":
query = "select sleep(5)"
with test_report(report_type="Query Report", query=query) as report:
doc = self.create_prepared_report(report.name)
self.wait_for_status(doc, "Started")
job_id = doc.job_id

View file

@ -36,6 +36,7 @@
"read_only": 1
}
],
"grid_page_length": 50,
"links": [
{
"link_doctype": "User",
@ -43,7 +44,7 @@
"table_fieldname": "role_profiles"
}
],
"modified": "2024-03-23 16:03:37.104710",
"modified": "2025-11-15 20:00:18.844434",
"modified_by": "Administrator",
"module": "Core",
"name": "Role Profile",
@ -74,9 +75,11 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "role_profile",
"track_changes": 1
}
"track_changes": 1,
"translated_doctype": 1
}

View file

@ -176,6 +176,13 @@ class TestRQJob(IntegrationTestCase):
if frappe.conf.use_mysqlclient:
# TEMP: Add extra allowance for running two connectors, this should be rolled back before v16
LAST_MEASURED_USAGE += 2
# Observed higher usage on 3.14. Temporarily raising the limit
from sys import version_info
if version_info >= (3, 14):
LAST_MEASURED_USAGE += 5
self.assertLessEqual(rss, LAST_MEASURED_USAGE * 1.05, msg)
def test_clear_failed_jobs(self):
@ -184,7 +191,7 @@ class TestRQJob(IntegrationTestCase):
jobs = [frappe.enqueue(method=self.BG_JOB, queue="short", fail=True) for _ in range(limit * 2)]
self.check_status(jobs[-1], "failed")
self.assertLessEqual(RQJob.get_count(filters=[["RQ Job", "status", "=", "failed"]]), limit * 1.1)
self.assertLessEqual(RQJob.get_count(filters=[["RQ Job", "status", "=", "failed"]]), limit * 1.2)
def test_func(fail=False, sleep=0):

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