diff --git a/.editorconfig b/.editorconfig
index 24f122a8d4..d76f67cd7f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,6 +9,6 @@ trim_trailing_whitespace = true
charset = utf-8
# python, js indentation settings
-[{*.py,*.js}]
+[{*.py,*.js,*.vue}]
indent_style = tab
indent_size = 4
diff --git a/.github/helper/install_dependencies.sh b/.github/helper/install_dependencies.sh
index 9be8519d85..d16f5b62ad 100644
--- a/.github/helper/install_dependencies.sh
+++ b/.github/helper/install_dependencies.sh
@@ -2,11 +2,6 @@
set -e
-# python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
-# if [[ $? != 2 ]];then
-# exit;
-# fi
-
# install wkhtmltopdf
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
diff --git a/.github/helper/roulette.py b/.github/helper/roulette.py
index ea4f07b9f7..dd3081822e 100644
--- a/.github/helper/roulette.py
+++ b/.github/helper/roulette.py
@@ -1,56 +1,68 @@
-# if the script ends with exit code 0, then no tests are run further, else all tests are run
+import json
import os
import re
import shlex
import subprocess
import sys
+import urllib.request
+def get_files_list(pr_number, repo="frappe/frappe"):
+ req = urllib.request.Request(f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files")
+ res = urllib.request.urlopen(req)
+ dump = json.loads(res.read().decode('utf8'))
+ return [change["filename"] for change in dump]
+
def get_output(command, shell=True):
- print(command)
- command = shlex.split(command)
- return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
+ print(command)
+ command = shlex.split(command)
+ return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
def is_py(file):
- return file.endswith("py")
+ return file.endswith("py")
-def is_js(file):
- return file.endswith("js")
+def is_ci(file):
+ return ".github" in file
+
+def is_frontend_code(file):
+ return file.lower().endswith((".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue"))
def is_docs(file):
- regex = re.compile(r'\.(md|png|jpg|jpeg)$|^.github|LICENSE')
- return bool(regex.search(file))
+ regex = re.compile(r'\.(md|png|jpg|jpeg|csv)$|^.github|LICENSE')
+ return bool(regex.search(file))
if __name__ == "__main__":
- build_type = os.environ.get("TYPE")
- before = os.environ.get("BEFORE")
- after = os.environ.get("AFTER")
- commit_range = before + '...' + after
- print("Build Type: {}".format(build_type))
- print("Commit Range: {}".format(commit_range))
+ files_list = sys.argv[1:]
+ build_type = os.environ.get("TYPE")
+ pr_number = os.environ.get("PR_NUMBER")
+ repo = os.environ.get("REPO_NAME")
- try:
- files_changed = get_output("git diff --name-only {}".format(commit_range), shell=False)
- except Exception:
- sys.exit(2)
+ if not files_list and pr_number:
+ files_list = get_files_list(pr_number=pr_number, repo=repo)
- if "fatal" not in files_changed:
- files_list = files_changed.split()
- only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
- only_js_changed = len(list(filter(is_js, files_list))) == len(files_list)
- only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
+ if not files_list:
+ print("No files' changes detected. Build is shutting")
+ sys.exit(0)
- if only_docs_changed:
- print("Only docs were updated, stopping build process.")
- sys.exit(0)
+ ci_files_changed = any(f for f in files_list if is_ci(f))
+ only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
+ only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
+ only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
- if only_js_changed and build_type == "server":
- print("Only JavaScript code was updated; Stopping Python build process.")
- sys.exit(0)
+ if ci_files_changed:
+ print("CI related files were updated, running all build processes.")
- if only_py_changed and build_type == "ui":
- print("Only Python code was updated, stopping Cypress build process.")
- sys.exit(0)
+ elif only_docs_changed:
+ print("Only docs were updated, stopping build process.")
+ sys.exit(0)
- sys.exit(2)
+ elif only_frontend_code_changed and build_type == "server":
+ print("Only Frontend code was updated; Stopping Python build process.")
+ sys.exit(0)
+
+ elif only_py_changed and build_type == "ui":
+ print("Only Python code was updated, stopping Cypress build process.")
+ sys.exit(0)
+
+ os.system('echo "::set-output name=build::strawberry"')
diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml
index 82be4d06b5..0dd4cd51d8 100644
--- a/.github/workflows/patch-mariadb-tests.yml
+++ b/.github/workflows/patch-mariadb-tests.yml
@@ -1,11 +1,11 @@
name: Patch
-on:
- pull_request:
- paths-ignore:
- - '**.js'
- - '**.md'
- workflow_dispatch:
+on: [pull_request, workflow_dispatch]
+
+
+concurrency:
+ group: patch-mariadb-develop-${{ github.event.number }}
+ cancel-in-progress: true
jobs:
test:
@@ -31,10 +31,21 @@ jobs:
with:
python-version: 3.7
+ - name: Check if build should be run
+ id: check-build
+ run: |
+ python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
+ env:
+ TYPE: "server"
+ PR_NUMBER: ${{ github.event.number }}
+ REPO_NAME: ${{ github.repository }}
+
- name: Add to Hosts
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2
with:
path: ~/.cache/pip
@@ -44,6 +55,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2
env:
cache-name: cache-node-modules
@@ -56,10 +68,12 @@ jobs:
${{ runner.os }}-
- name: Get yarn cache directory path
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -68,6 +82,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Install Dependencies
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
@@ -75,12 +90,14 @@ jobs:
TYPE: server
- name: Install
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: server
- name: Run Patch Tests
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: |
cd ~/frappe-bench/
wget https://frappeframework.com/files/v10-frappe.sql.gz
diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml
index 8d5bd690a1..fb6e56037c 100644
--- a/.github/workflows/server-mariadb-tests.yml
+++ b/.github/workflows/server-mariadb-tests.yml
@@ -2,15 +2,14 @@ name: Server
on:
pull_request:
- paths-ignore:
- - '**.js'
- - '**.md'
workflow_dispatch:
push:
branches: [ develop ]
- paths-ignore:
- - '**.js'
- - '**.md'
+
+concurrency:
+ group: server-mariadb-develop-${{ github.event.number }}
+ cancel-in-progress: true
+
jobs:
test:
@@ -41,17 +40,29 @@ jobs:
with:
python-version: 3.7
+ - name: Check if build should be run
+ id: check-build
+ run: |
+ python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
+ env:
+ TYPE: "server"
+ PR_NUMBER: ${{ github.event.number }}
+ REPO_NAME: ${{ github.repository }}
+
- uses: actions/setup-node@v2
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
with:
node-version: 14
check-latest: true
- name: Add to Hosts
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: |
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
- name: Cache pip
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2
with:
path: ~/.cache/pip
@@ -61,6 +72,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2
env:
cache-name: cache-node-modules
@@ -73,10 +85,12 @@ jobs:
${{ runner.os }}-
- name: Get yarn cache directory path
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -85,6 +99,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Install Dependencies
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
@@ -92,18 +107,22 @@ jobs:
TYPE: server
- name: Install
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: server
- name: Run Tests
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
env:
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Upload Coverage Data
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
+ id: upload-coverage-data
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
@@ -117,16 +136,29 @@ jobs:
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true
+ - run: echo ${{ steps.check-build.outputs.build }} > guess-the-fruit.txt
+ - uses: actions/upload-artifact@v1
+ with:
+ name: fruit
+ path: guess-the-fruit.txt
+
coveralls:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
steps:
+ - uses: actions/download-artifact@v1
+ with:
+ name: fruit
+ - run: echo "WILDCARD=$(cat fruit/guess-the-fruit.txt)" >> $GITHUB_ENV
+
- name: Clone
+ if: ${{ env.WILDCARD == 'strawberry' }}
uses: actions/checkout@v2
- name: Coveralls Finished
+ if: ${{ env.WILDCARD == 'strawberry' }}
run: |
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml
index 8c97c7f84b..1539e8c2d5 100644
--- a/.github/workflows/server-postgres-tests.yml
+++ b/.github/workflows/server-postgres-tests.yml
@@ -2,11 +2,12 @@ name: Server
on:
pull_request:
- paths-ignore:
- - '**.js'
- - '**.md'
workflow_dispatch:
+concurrency:
+ group: server-postgres-develop-${{ github.event.number }}
+ cancel-in-progress: true
+
jobs:
test:
runs-on: ubuntu-18.04
@@ -40,17 +41,29 @@ jobs:
with:
python-version: 3.7
+ - name: Check if build should be run
+ id: check-build
+ run: |
+ python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
+ env:
+ TYPE: "server"
+ PR_NUMBER: ${{ github.event.number }}
+ REPO_NAME: ${{ github.repository }}
+
- uses: actions/setup-node@v2
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
with:
node-version: '14'
check-latest: true
- name: Add to Hosts
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: |
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
- name: Cache pip
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2
with:
path: ~/.cache/pip
@@ -60,6 +73,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2
env:
cache-name: cache-node-modules
@@ -72,10 +86,12 @@ jobs:
${{ runner.os }}-
- name: Get yarn cache directory path
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -84,6 +100,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Install Dependencies
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
@@ -91,12 +108,14 @@ jobs:
TYPE: server
- name: Install
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: postgres
TYPE: server
- name: Run Tests
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator
env:
CI_BUILD_ID: ${{ github.run_id }}
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index d76e5e77ea..2a55546ec4 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -2,12 +2,14 @@ name: UI
on:
pull_request:
- paths-ignore:
- - '**.md'
workflow_dispatch:
push:
branches: [ develop ]
+concurrency:
+ group: ui-develop-${{ github.event.number }}
+ cancel-in-progress: true
+
jobs:
test:
runs-on: ubuntu-18.04
@@ -37,17 +39,29 @@ jobs:
with:
python-version: 3.7
+ - name: Check if build should be run
+ id: check-build
+ run: |
+ python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
+ env:
+ TYPE: "ui"
+ PR_NUMBER: ${{ github.event.number }}
+ REPO_NAME: ${{ github.repository }}
+
- uses: actions/setup-node@v2
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
with:
node-version: 14
check-latest: true
- name: Add to Hosts
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: |
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
- name: Cache pip
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2
with:
path: ~/.cache/pip
@@ -57,6 +71,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2
env:
cache-name: cache-node-modules
@@ -69,10 +84,12 @@ jobs:
${{ runner.os }}-
- name: Get yarn cache directory path
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -81,6 +98,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Cache cypress binary
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2
with:
path: ~/.cache
@@ -90,6 +108,7 @@ jobs:
${{ runner.os }}-
- name: Install Dependencies
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
@@ -97,15 +116,18 @@ jobs:
TYPE: ui
- name: Install
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: ui
- name: Site Setup
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard
- name: UI Tests
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID
env:
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
diff --git a/.mergify.yml b/.mergify.yml
index 1a81a28594..8c7a7dc95d 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -1,9 +1,11 @@
pull_request_rules:
- name: Auto-close PRs on stable branch
conditions:
- - or:
- - base=version-13
- - base=version-12
+ - and:
+ - author!=surajshetty3416
+ - or:
+ - base=version-13
+ - base=version-12
actions:
close:
comment:
diff --git a/CODEOWNERS b/CODEOWNERS
index 2dff157294..30cdb4d64d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -7,10 +7,13 @@
templates/ @surajshetty3416
www/ @surajshetty3416
integrations/ @leela
-patches/ @surajshetty3416
+patches/ @surajshetty3416 @gavindsouza
email/ @leela
event_streaming/ @ruchamahabal
data_import* @netchampfaris
core/ @surajshetty3416
+database @gavindsouza
+model @gavindsouza
requirements.txt @gavindsouza
commands/ @gavindsouza
+workspace @shariquerik
diff --git a/cypress/integration/dashboard_links.js b/cypress/integration/dashboard_links.js
new file mode 100644
index 0000000000..b77965ee1a
--- /dev/null
+++ b/cypress/integration/dashboard_links.js
@@ -0,0 +1,63 @@
+context('Dashboard links', () => {
+ before(() => {
+ cy.visit('/login');
+ cy.login();
+ });
+
+ it('Adding a new contact, checking for the counter on the dashboard and deleting the created contact', () => {
+ cy.visit('/app/contact');
+ cy.clear_filters();
+
+ cy.visit('/app/user');
+ cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
+
+ //To check if initially the dashboard contains only the "Contact" link and there is no counter
+ cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
+
+ //Adding a new contact
+ cy.get('.btn[data-doctype="Contact"]').click();
+ cy.get('[data-doctype="Contact"][data-fieldname="first_name"]').type('Admin');
+ cy.findByRole('button', {name: 'Save'}).click();
+ cy.visit('/app/user');
+ cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
+
+ //To check if the counter for contact doc is "1" after adding the contact
+ cy.get('[data-doctype="Contact"] > .count').should('contain', '1');
+ cy.get('[data-doctype="Contact"]').contains('Contact').click();
+
+ //Deleting the newly created contact
+ cy.visit('/app/contact');
+ cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
+ cy.findByRole('button', {name: 'Actions'}).click();
+ cy.get('.actions-btn-group [data-label="Delete"]').click();
+ cy.findByRole('button', {name: 'Yes'}).click({delay: 700});
+
+
+ //To check if the counter from the "Contact" doc link is removed
+ cy.wait(700);
+ cy.visit('/app/user');
+ cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
+ cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
+ });
+
+ it('Report link in dashboard', () => {
+ cy.visit('/app/user');
+ cy.visit('/app/user/Administrator');
+ cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
+ cy.findByText('Connections');
+ cy.window()
+ .its('cur_frm')
+ .then(cur_frm => {
+ cur_frm.dashboard.data.reports = [
+ {
+ 'label': 'Reports',
+ 'items': ['Permitted Documents For User']
+ }
+ ];
+ cur_frm.dashboard.render_report_links();
+ cy.get('[data-report="Permitted Documents For User"]').contains('Permitted Documents For User').click();
+ cy.findByText('Permitted Documents For User');
+ cy.findByPlaceholderText('User').should("have.value", "Administrator");
+ });
+ });
+});
diff --git a/cypress/integration/datetime_field_form_validation.js b/cypress/integration/datetime_field_form_validation.js
new file mode 100644
index 0000000000..66fdde6863
--- /dev/null
+++ b/cypress/integration/datetime_field_form_validation.js
@@ -0,0 +1,19 @@
+context('Datetime Field Validation', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app/communication');
+ cy.window().its('frappe').then(frappe => {
+ frappe.call("frappe.tests.ui_test_helpers.create_communication_records");
+ });
+ });
+
+ // validating datetime field value when value is set from backend and get validated on form load.
+ it('datetime field form validation', () => {
+ cy.visit('/app/communication');
+ cy.get('a[title="Test Form Communication 1"]').invoke('attr', 'data-name')
+ .then((name) => {
+ cy.visit(`/app/communication/${name}`);
+ cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red');
+ });
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/folder_navigation.js b/cypress/integration/folder_navigation.js
new file mode 100644
index 0000000000..1b7c02d98c
--- /dev/null
+++ b/cypress/integration/folder_navigation.js
@@ -0,0 +1,79 @@
+context('Folder Navigation', () => {
+ before(() => {
+ cy.visit('/login');
+ cy.login();
+ cy.visit('/app/file');
+ });
+
+ it('Adding Folders', () => {
+ //Adding filter to go into the home folder
+ cy.get('.filter-selector > .btn').findByText('1 filter').click();
+ cy.findByRole('button', {name: 'Clear Filters'}).click();
+ cy.get('.filter-action-buttons > .text-muted').findByText('+ Add a Filter').click();
+ cy.get('.fieldname-select-area > .awesomplete > .form-control').type('Fol{enter}');
+ cy.get('.filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback').type('Home{enter}');
+ cy.get('.filter-action-buttons > div > .btn-primary').findByText('Apply Filters').click();
+
+ //Adding folder (Test Folder)
+ cy.get('.menu-btn-group > .btn').click();
+ cy.get('.menu-btn-group [data-label="New Folder"]').click();
+ cy.get('form > [data-fieldname="value"]').type('Test Folder');
+ cy.findByRole('button', {name: 'Create'}).click();
+ });
+
+ it('Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct', () => {
+ //Navigating inside the Attachments folder
+ cy.get('[title="Attachments"] > span').click();
+
+ //To check if the URL formed after visiting the attachments folder is correct
+ cy.location('pathname').should('eq', '/app/file/view/home/Attachments');
+ cy.visit('/app/file/view/home/Attachments');
+
+ //Adding folder inside the attachments folder
+ cy.get('.menu-btn-group > .btn').click();
+ cy.get('.menu-btn-group [data-label="New Folder"]').click();
+ cy.get('form > [data-fieldname="value"]').type('Test Folder');
+ cy.findByRole('button', {name: 'Create'}).click();
+
+ //Navigating inside the added folder in the Attachments folder
+ cy.get('[title="Test Folder"] > span').click();
+
+ //To check if the URL is correct after visiting the Test Folder
+ cy.location('pathname').should('eq', '/app/file/view/home/Attachments/Test%20Folder');
+ cy.visit('/app/file/view/home/Attachments/Test%20Folder');
+
+ //Adding a file inside the Test Folder
+ cy.findByRole('button', {name: 'Add File'}).eq(0).click({force: true});
+ cy.get('.file-uploader').findByText('Link').click();
+ cy.get('.input-group > .form-control').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
+ cy.findByRole('button', {name: 'Upload'}).click();
+
+ //To check if the added file is present in the Test Folder
+ cy.get('span.level-item > span').should('contain', 'Test Folder');
+ cy.get('.list-row-container').eq(0).should('contain.text', '72402.jpg');
+ cy.get('.list-row-checkbox').eq(0).click();
+
+ //Deleting the added file from the Test folder
+ cy.findByRole('button', {name: 'Actions'}).click();
+ cy.get('.actions-btn-group [data-label="Delete"]').click();
+ cy.wait(700);
+ cy.findByRole('button', {name: 'Yes'}).click();
+ cy.wait(700);
+
+ //Deleting the Test Folder
+ cy.visit('/app/file/view/home/Attachments');
+ cy.get('.list-row-checkbox').eq(0).click();
+ cy.findByRole('button', {name: 'Actions'}).click();
+ cy.get('.actions-btn-group [data-label="Delete"]').click();
+ cy.findByRole('button', {name: 'Yes'}).click();
+ });
+
+ it('Deleting Test Folder from the home', () => {
+ //Deleting the Test Folder added in the home directory
+ cy.visit('/app/file/view/home');
+ cy.get('.level-left > .list-subject > .list-row-checkbox').eq(0).click({force: true, delay: 500});
+ cy.findByRole('button', {name: 'Actions'}).click();
+ cy.get('.actions-btn-group [data-label="Delete"]').click();
+ cy.findByRole('button', {name: 'Yes'}).click();
+ });
+});
diff --git a/cypress/integration/form_tour.js b/cypress/integration/form_tour.js
index d2d39679a8..ab7ada9034 100644
--- a/cypress/integration/form_tour.js
+++ b/cypress/integration/form_tour.js
@@ -20,10 +20,10 @@ context('Form Tour', () => {
it('navigates a form tour', () => {
open_test_form_tour();
- cy.get('#driver-popover-item').should('be.visible');
+ cy.get('.frappe-driver').should('be.visible');
cy.get('.frappe-control[data-fieldname="first_name"]').as('first_name');
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
- cy.get('#driver-popover-item').findByRole('button', {name: 'Next'}).as('next_btn');
+ cy.get('.frappe-driver').findByRole('button', {name: 'Next'}).as('next_btn');
// next btn shouldn't move to next step, if first name is not entered
cy.get('@next_btn').click();
@@ -68,13 +68,13 @@ context('Form Tour', () => {
cy.get('.grid-row-open .frappe-control[data-fieldname="phone"]').as('phone');
cy.get('@phone').should('have.class', 'driver-highlighted-element');
// enter value in a table field
- cy.fill_table_field('phone_nos', '1', 'phone', '1234567890');
+ let field = cy.fill_table_field('phone_nos', '1', 'phone', '1234567890');
+ field.blur();
// move to collapse row step
cy.wait(500);
- cy.get('@next_btn').click();
+ cy.get('.driver-popover-title').contains('Test Title 4').siblings().get('@next_btn').click();
cy.wait(500);
-
// collapse row
cy.get('.grid-row-open .grid-collapse-row').click();
cy.wait(500);
@@ -82,7 +82,7 @@ context('Form Tour', () => {
// assert save btn is highlighted
cy.get('.primary-action').should('have.class', 'driver-highlighted-element');
cy.wait(500);
- cy.get('#driver-popover-item').findByRole('button', {name: 'Save'}).should('be.visible');
+ cy.get('.frappe-driver').findByRole('button', {name: 'Save'}).should('be.visible');
});
});
diff --git a/cypress/integration/timeline_email.js b/cypress/integration/timeline_email.js
index e5b3ebeb7c..82af24e822 100644
--- a/cypress/integration/timeline_email.js
+++ b/cypress/integration/timeline_email.js
@@ -8,14 +8,13 @@ context('Timeline Email', () => {
it('Adding new ToDo, adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => {
//Adding new ToDo
cy.click_listview_primary_button('Add ToDo');
- cy.get('.custom-actions > .btn').trigger('click', {delay: 500});
- cy.get('.row > .section-body > .form-column > form > .frappe-control > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').eq(0).type('Test ToDo', {force: true});
+ cy.get('.custom-actions:visible > .btn').contains("Edit in full page").click({delay: 500});
+ cy.fill_field("description", "Test ToDo", "Text Editor");
cy.wait(500);
- //cy.click_listview_primary_button('Save');
cy.get('.primary-action').contains('Save').click({force: true});
cy.wait(700);
cy.visit('/app/todo');
- cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
+ cy.get('.list-row > .level-left > .list-subject').eq(0).click();
//Creating a new email
cy.get('.timeline-actions > .btn').click();
@@ -47,7 +46,7 @@ context('Timeline Email', () => {
//Removing the added attachment
cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click();
- cy.get('.modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').contains('Yes').click();
+ cy.get('.modal-footer:visible > .standard-actions > .btn-primary').contains('Yes').click();
//To check if the removed attachment is shown in the timeline content
cy.get('.timeline-content').should('contain', 'Removed 72402.jpg');
@@ -55,17 +54,17 @@ context('Timeline Email', () => {
//To check if the discard button functionality in email is working correctly
cy.get('.timeline-actions > .btn').click();
- cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
+ cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
cy.get('.modal-footer > .standard-actions > .btn-secondary').contains('Discard').click();
cy.wait(500);
cy.get('.timeline-actions > .btn').click();
cy.wait(500);
cy.get_field('recipients', 'MultiSelect').should('have.text', '');
- cy.get('.modal.show > .modal-dialog > .modal-content > .modal-header > .modal-actions > .btn-modal-close > .icon').click();
+ cy.get('.modal-header:visible > .modal-actions > .btn-modal-close > .icon').click();
//Deleting the added ToDo
- cy.get('#page-ToDo > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click();
- cy.get('.menu-btn-group > .dropdown-menu > li > .grey-link').eq(17).click();
- cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click();
+ cy.get('.menu-btn-group:visible > .btn').click();
+ cy.get('.menu-btn-group:visible > .dropdown-menu > li > .dropdown-item').contains('Delete').click();
+ cy.get('.modal-footer:visible > .standard-actions > .btn-primary').click();
});
});
diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js
index f18e48aadc..65586366e6 100644
--- a/cypress/integration/workspace.js
+++ b/cypress/integration/workspace.js
@@ -36,12 +36,12 @@ context('Workspace 2.0', () => {
cy.get('.codex-editor__redactor .ce-block');
cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click();
cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Heading').click();
- cy.get(".ce-block:last").find('h2').click({force: true}).type('Header');
+ cy.get(":focus").type('Header');
cy.get(".ce-block:last").find('.ce-header').should('exist');
cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click();
cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Text').click();
- cy.get(".ce-block:last").find('.ce-paragraph').click({force: true}).type('Paragraph text');
+ cy.get(":focus").type('Paragraph text');
cy.get(".ce-block:last").find('.ce-paragraph').should('exist');
});
diff --git a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py
index e287b83965..dfefd091fb 100644
--- a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py
+++ b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py
@@ -76,7 +76,7 @@ class TestAutoAssign(unittest.TestCase):
# clear 5 assignments for first user
# can't do a limit in "delete" since postgres does not support it
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', owner = 'test@example.com'), limit=5):
- frappe.db.sql("delete from tabToDo where name = %s", d.name)
+ frappe.db.delete("ToDo", {"name": d.name})
# add 5 more assignments
for i in range(5):
@@ -177,7 +177,7 @@ class TestAutoAssign(unittest.TestCase):
), 'owner'), 'test@example.com')
def check_assignment_rule_scheduling(self):
- frappe.db.sql("DELETE FROM `tabAssignment Rule`")
+ frappe.db.delete("Assignment Rule")
days_1 = [dict(day = 'Sunday'), dict(day = 'Monday'), dict(day = 'Tuesday')]
@@ -204,7 +204,7 @@ class TestAutoAssign(unittest.TestCase):
), 'owner'), ['test3@example.com'])
def test_assignment_rule_condition(self):
- frappe.db.sql("DELETE FROM `tabAssignment Rule`")
+ frappe.db.delete("Assignment Rule")
# Add expiry_date custom field
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
@@ -253,7 +253,7 @@ class TestAutoAssign(unittest.TestCase):
assignment_rule.delete()
def clear_assignments():
- frappe.db.sql("delete from tabToDo where reference_type = 'Note'")
+ frappe.db.delete("ToDo", {"reference_type": "Note"})
def get_assignment_rule(days, assign=None):
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1')
diff --git a/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py b/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py
index 21b2779018..1683e94827 100644
--- a/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py
+++ b/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py
@@ -7,7 +7,7 @@ import unittest
class TestMilestoneTracker(unittest.TestCase):
def test_milestone(self):
- frappe.db.sql('delete from `tabMilestone Tracker`')
+ frappe.db.delete("Milestone Tracker")
frappe.cache().delete_key('milestone_tracker_map')
@@ -44,5 +44,5 @@ class TestMilestoneTracker(unittest.TestCase):
self.assertEqual(milestones[0].value, 'Closed')
# cleanup
- frappe.db.sql('delete from tabMilestone')
+ frappe.db.delete("Milestone")
milestone_tracker.delete()
\ No newline at end of file
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index be8304e45d..b0151106db 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -486,15 +486,26 @@ frappe.db.connect()
@click.command('console')
+@click.option(
+ '--autoreload',
+ is_flag=True,
+ help="Reload changes to code automatically"
+)
@pass_context
-def console(context):
+def console(context, autoreload=False):
"Start ipython console for a site"
site = get_site(context)
frappe.init(site=site)
frappe.connect()
frappe.local.lang = frappe.db.get_default("lang")
- import IPython
+ from IPython.terminal.embed import InteractiveShellEmbed
+
+ terminal = InteractiveShellEmbed()
+ if autoreload:
+ terminal.extension_manager.load_extension("autoreload")
+ terminal.run_line_magic("autoreload", "2")
+
all_apps = frappe.get_installed_apps()
failed_to_import = []
@@ -509,7 +520,9 @@ def console(context):
if failed_to_import:
print("\nFailed to import:\n{}".format(", ".join(failed_to_import)))
- IPython.embed(display_banner="", header="", colors="neutral")
+ terminal.colors = "neutral"
+ terminal.display_banner = False
+ terminal()
@click.command('run-tests')
@@ -524,7 +537,7 @@ def console(context):
@click.option('--skip-test-records', is_flag=True, default=False, help="Don't create test records")
@click.option('--skip-before-tests', is_flag=True, default=False, help="Don't run before tests hook")
@click.option('--junit-xml-output', help="Destination file path for junit xml report")
-@click.option('--failfast', is_flag=True, default=False)
+@click.option('--failfast', is_flag=True, default=False, help="Stop the test run on the first error or failure")
@pass_context
def run_tests(context, app=None, module=None, doctype=None, test=(), profile=False,
coverage=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None,
diff --git a/frappe/core/doctype/access_log/access_log.py b/frappe/core/doctype/access_log/access_log.py
index 2ea014f981..82db450b4a 100644
--- a/frappe/core/doctype/access_log/access_log.py
+++ b/frappe/core/doctype/access_log/access_log.py
@@ -29,4 +29,5 @@ def make_access_log(doctype=None, document=None, method=None, file_type=None,
doc.insert(ignore_permissions=True)
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
- frappe.db.commit()
+ if frappe.request and frappe.request.method == 'GET':
+ frappe.db.commit()
diff --git a/frappe/core/doctype/comment/test_comment.py b/frappe/core/doctype/comment/test_comment.py
index 13db92e7a8..12fe027fba 100644
--- a/frappe/core/doctype/comment/test_comment.py
+++ b/frappe/core/doctype/comment/test_comment.py
@@ -30,7 +30,7 @@ class TestComment(unittest.TestCase):
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
test_blog = make_test_blog()
- frappe.db.sql("delete from `tabComment` where reference_doctype = 'Blog Post'")
+ frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
from frappe.templates.includes.comments.comments import add_comment
add_comment('Good comment with 10 chars', 'test@test.com', 'Good Tester',
@@ -41,7 +41,7 @@ class TestComment(unittest.TestCase):
reference_name = test_blog.name
))[0].published, 1)
- frappe.db.sql("delete from `tabComment` where reference_doctype = 'Blog Post'")
+ frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor',
'Blog Post', test_blog.name, test_blog.route)
diff --git a/frappe/core/doctype/feedback/test_feedback.py b/frappe/core/doctype/feedback/test_feedback.py
index 2a96d86874..c7551420c3 100644
--- a/frappe/core/doctype/feedback/test_feedback.py
+++ b/frappe/core/doctype/feedback/test_feedback.py
@@ -9,7 +9,7 @@ class TestFeedback(unittest.TestCase):
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
test_blog = make_test_blog()
- frappe.db.sql("delete from `tabFeedback` where reference_doctype = 'Blog Post'")
+ frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
from frappe.templates.includes.feedback.feedback import add_feedback, update_feedback
feedback = add_feedback('Blog Post', test_blog.name, 5, 'New feedback')
@@ -22,6 +22,6 @@ class TestFeedback(unittest.TestCase):
self.assertEqual(updated_feedback.feedback, 'Updated feedback')
self.assertEqual(updated_feedback.rating, 6)
- frappe.db.sql("delete from `tabFeedback` where reference_doctype = 'Blog Post'")
+ frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
test_blog.delete()
\ No newline at end of file
diff --git a/frappe/core/doctype/file/file.js b/frappe/core/doctype/file/file.js
index bc0cc17553..d40328d3cd 100644
--- a/frappe/core/doctype/file/file.js
+++ b/frappe/core/doctype/file/file.js
@@ -29,15 +29,8 @@ frappe.ui.form.on("File", "refresh", function(frm) {
if (is_optimizable) {
frm.add_custom_button(__("Optimize"), function() {
frappe.show_alert(__("Optimizing image..."));
- frappe.call({
- method: "frappe.core.doctype.file.file.optimize_saved_image",
- args: {
- doc_name: frm.doc.name,
- },
- callback: function() {
- frappe.show_alert(__("Image optimized"));
- frappe.set_route("List", "File");
- }
+ frm.call("optimize_file").then(() => {
+ frappe.show_alert(__("Image optimized"));
});
});
}
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index e79b2bd761..36ff67ce7c 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -21,11 +21,11 @@ import zipfile
import requests
import requests.exceptions
from PIL import Image, ImageFile, ImageOps
-from io import StringIO
+from io import BytesIO
from urllib.parse import quote, unquote
import frappe
-from frappe import _, conf
+from frappe import _, conf, safe_decode
from frappe.model.document import Document
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
from frappe.utils.image import strip_exif_data, optimize_image
@@ -257,8 +257,7 @@ class File(Document):
with open(get_files_path(file_name, is_private=self.is_private), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
- frappe.msgprint(_("File {0} does not exist").format(self.file_url))
- raise
+ frappe.throw(_("File {0} does not exist").format(self.file_url))
def on_trash(self):
if self.is_home_folder or self.is_attachments_folder:
@@ -270,16 +269,12 @@ class File(Document):
def make_thumbnail(self, set_as_thumbnail=True, width=300, height=300, suffix="small", crop=False):
if self.file_url:
- if self.file_url.startswith("/files"):
- try:
+ try:
+ if self.file_url.startswith(("/files", "/private/files")):
image, filename, extn = get_local_image(self.file_url)
- except IOError:
- return
-
- else:
- try:
+ else:
image, filename, extn = get_web_image(self.file_url)
- except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError):
+ except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError):
return
size = width, height
@@ -289,16 +284,13 @@ class File(Document):
image.thumbnail(size, Image.ANTIALIAS)
thumbnail_url = filename + "_" + suffix + "." + extn
-
path = os.path.abspath(frappe.get_site_path("public", thumbnail_url.lstrip("/")))
try:
image.save(path)
-
if set_as_thumbnail:
self.db_set("thumbnail_url", thumbnail_url)
- self.db_set("thumbnail_url", thumbnail_url)
except IOError:
frappe.msgprint(_("Unable to write file format for {0}").format(path))
return
@@ -321,17 +313,23 @@ class File(Document):
self.delete_file_data_content(only_thumbnail=True)
def on_rollback(self):
- self.flags.on_rollback = True
- self.on_trash()
+ # if original_content flag is set, this rollback should revert the file to its original state
+ if self.flags.original_content:
+ file_path = self.get_full_path()
+ with open(file_path, "wb+") as f:
+ f.write(self.flags.original_content)
+
+ # following condition is only executed when an insert has been rolledback
+ else:
+ self.flags.on_rollback = True
+ self.on_trash()
def unzip(self):
'''Unzip current file and replace it by its children'''
- if not ".zip" in self.file_name:
- frappe.msgprint(_("Not a zip file"))
- return
+ if not self.file_url.endswith(".zip"):
+ frappe.throw(_("{0} is not a zip file").format(self.file_name))
- zip_path = frappe.get_site_path(self.file_url.strip('/'))
- base_url = os.path.dirname(self.file_url)
+ zip_path = self.get_full_path()
files = []
with zipfile.ZipFile(zip_path) as z:
@@ -359,10 +357,6 @@ class File(Document):
return files
- def get_file_url(self):
- data = frappe.db.get_value("File", self.file_data_name, ["file_name", "file_url"], as_dict=True)
- return data.file_url or data.file_name
-
def exists_on_disk(self):
exists = os.path.exists(self.get_full_path())
return exists
@@ -431,47 +425,6 @@ class File(Document):
return get_files_path(self.file_name, is_private=self.is_private)
- def get_file_doc(self):
- '''returns File object (Document) from given parameters or form_dict'''
- r = frappe.form_dict
-
- if self.file_url is None: self.file_url = r.file_url
- if self.file_name is None: self.file_name = r.file_name
- if self.attached_to_doctype is None: self.attached_to_doctype = r.doctype
- if self.attached_to_name is None: self.attached_to_name = r.docname
- if self.attached_to_field is None: self.attached_to_field = r.docfield
- if self.folder is None: self.folder = r.folder
- if self.is_private is None: self.is_private = r.is_private
-
- if r.filedata:
- file_doc = self.save_uploaded()
-
- elif r.file_url:
- file_doc = self.save()
-
- return file_doc
-
-
- def save_uploaded(self):
- self.content = self.get_uploaded_content()
- if self.content:
- return self.save()
- else:
- raise Exception
-
- def get_uploaded_content(self):
- # should not be unicode when reading a file, hence using frappe.form
- if 'filedata' in frappe.form_dict:
- if "," in frappe.form_dict.filedata:
- frappe.form_dict.filedata = frappe.form_dict.filedata.rsplit(",", 1)[1]
- frappe.uploaded_content = base64.b64decode(frappe.form_dict.filedata)
- return frappe.uploaded_content
- elif self.content:
- return self.content
- frappe.msgprint(_('No file attached'))
- return None
-
-
def save_file(self, content=None, decode=False, ignore_existing_file_check=False):
file_exists = False
self.content = content
@@ -539,14 +492,6 @@ class File(Document):
'file_url': self.file_url
}
- def get_file_data_from_hash(self):
- for name in frappe.db.sql_list("select name from `tabFile` where content_hash=%s and is_private=%s",
- (self.content_hash, self.is_private)):
- b = frappe.get_doc('File', name)
- return {k: b.get(k) for k in frappe.get_hooks()['write_file_keys']}
- return False
-
-
def check_max_file_size(self):
max_file_size = get_max_file_size()
file_size = len(self.content)
@@ -594,6 +539,35 @@ class File(Document):
if self.file_url:
self.is_private = cint(self.file_url.startswith('/private'))
+ @frappe.whitelist()
+ def optimize_file(self):
+ if self.is_folder:
+ raise TypeError('Folders cannot be optimized')
+
+ content_type = mimetypes.guess_type(self.file_name)[0]
+ is_local_image = content_type.startswith('image/') and self.file_size > 0
+ is_svg = content_type == 'image/svg+xml'
+
+ if not is_local_image:
+ raise NotImplementedError('Only local image files can be optimized')
+
+ if is_svg:
+ raise TypeError('Optimization of SVG images is not supported')
+
+ content = self.get_content()
+ file_path = self.get_full_path()
+ optimized_content = optimize_image(content, content_type)
+
+ with open(file_path, 'wb+') as f:
+ f.write(optimized_content)
+
+ self.file_size = len(optimized_content)
+ self.content_hash = get_content_hash(optimized_content)
+ # if rolledback, revert back to original
+ self.flags.original_content = content
+ frappe.local.rollback_observers.append(self)
+ self.save()
+
def on_doctype_update():
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])
@@ -621,7 +595,8 @@ def create_new_folder(file_name, folder):
file.file_name = file_name
file.is_folder = 1
file.folder = folder
- file.insert()
+ file.insert(ignore_if_duplicate=True)
+ return file
@frappe.whitelist()
def move_file(file_list, new_parent, old_parent):
@@ -672,7 +647,7 @@ def get_local_image(file_url):
try:
image = Image.open(file_path)
except IOError:
- frappe.msgprint(_("Unable to read file format for {0}").format(file_url), raise_exception=True)
+ frappe.throw(_("Unable to read file format for {0}").format(file_url))
content = None
@@ -704,7 +679,7 @@ def get_web_image(file_url):
raise
try:
- image = Image.open(StringIO(frappe.safe_decode(r.content)))
+ image = Image.open(BytesIO(r.content))
except Exception as e:
frappe.msgprint(_("Image link '{0}' is not valid").format(file_url), raise_exception=e)
@@ -740,48 +715,12 @@ def delete_file(path):
os.remove(path)
-def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_delete=False, delete_permanently=False):
- """Remove file and File entry"""
- file_name = None
- if not (attached_to_doctype and attached_to_name):
- attached = frappe.db.get_value("File", fid,
- ["attached_to_doctype", "attached_to_name", "file_name"])
- if attached:
- attached_to_doctype, attached_to_name, file_name = attached
-
- ignore_permissions, comment = False, None
- if attached_to_doctype and attached_to_name and not from_delete:
- doc = frappe.get_doc(attached_to_doctype, attached_to_name)
- ignore_permissions = doc.has_permission("write") or False
- if frappe.flags.in_web_form:
- ignore_permissions = True
- if not file_name:
- file_name = frappe.db.get_value("File", fid, "file_name")
- comment = doc.add_comment("Attachment Removed", _("Removed {0}").format(file_name))
- frappe.delete_doc("File", fid, ignore_permissions=ignore_permissions, delete_permanently=delete_permanently)
-
- return comment
def get_max_file_size():
return cint(conf.get('max_file_size')) or 10485760
-def remove_all(dt, dn, from_delete=False, delete_permanently=False):
- """remove all files in a transaction"""
- try:
- for fid in frappe.db.sql_list("""select name from `tabFile` where
- attached_to_doctype=%s and attached_to_name=%s""", (dt, dn)):
- if from_delete:
- # If deleting a doc, directly delete files
- frappe.delete_doc("File", fid, ignore_permissions=True, delete_permanently=delete_permanently)
- else:
- # Removes file and adds a comment in the document it is attached to
- remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn,
- from_delete=from_delete, delete_permanently=delete_permanently)
- except Exception as e:
- if e.args[0]!=1054: raise # (temp till for patched)
-
def has_permission(doc, ptype=None, user=None):
has_access = False
@@ -827,6 +766,7 @@ def remove_file_by_url(file_url, doctype=None, name=None):
fid = frappe.db.get_value("File", {"file_url": file_url})
if fid:
+ from frappe.utils.file_manager import remove_file
return remove_file(fid=fid)
@@ -886,15 +826,13 @@ def extract_images_from_html(doc, content):
if b"," in content:
content = content.split(b",")[1]
content = base64.b64decode(content)
-
+
content = optimize_image(content, mtype)
if "filename=" in headers:
filename = headers.split("filename=")[-1]
+ filename = safe_decode(filename).split(";")[0]
- # decode filename
- if not isinstance(filename, str):
- filename = str(filename, 'utf-8')
else:
filename = get_random_filename(content_type=mtype)
@@ -922,12 +860,9 @@ def extract_images_from_html(doc, content):
return content
-def get_random_filename(extn=None, content_type=None):
- if extn:
- if not extn.startswith("."):
- extn = "." + extn
-
- elif content_type:
+def get_random_filename(content_type=None):
+ extn = None
+ if content_type:
extn = mimetypes.guess_extension(content_type)
return random_string(7) + (extn or "")
@@ -938,24 +873,8 @@ def unzip_file(name):
'''Unzip the given file and make file records for each of the extracted files'''
file_obj = frappe.get_doc('File', name)
files = file_obj.unzip()
- return len(files)
+ return files
-@frappe.whitelist()
-def optimize_saved_image(doc_name):
- file_doc = frappe.get_doc('File', doc_name)
- content = file_doc.get_content()
- content_type = mimetypes.guess_type(file_doc.file_name)[0]
-
- optimized_content = optimize_image(content, content_type)
-
- file_path = get_files_path(is_private=file_doc.is_private)
- file_path = os.path.join(file_path.encode('utf-8'), file_doc.file_name.encode('utf-8'))
- with open(file_path, 'wb+') as f:
- f.write(optimized_content)
-
- file_doc.file_size = len(optimized_content)
- file_doc.content_hash = get_content_hash(optimized_content)
- file_doc.save()
@frappe.whitelist()
def get_attached_images(doctype, names):
@@ -979,13 +898,6 @@ def get_attached_images(doctype, names):
return out
-@frappe.whitelist()
-def validate_filename(filename):
- from frappe.utils import now_datetime
- timestamp = now_datetime().strftime(" %Y-%m-%d %H:%M:%S")
- fname = get_file_name(filename, timestamp)
- return fname
-
@frappe.whitelist()
def get_files_in_folder(folder, start=0, page_length=20):
start = cint(start)
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index 649010c468..18ad95eeba 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -2,11 +2,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import base64
+import json
import frappe
import os
import unittest
from frappe import _
-from frappe.core.doctype.file.file import move_file, get_files_in_folder
+from frappe.core.doctype.file.file import get_attached_images, move_file, get_files_in_folder, unzip_file
from frappe.utils import get_files_path
# test_records = frappe.get_test_records('File')
@@ -365,6 +366,81 @@ class TestFile(unittest.TestCase):
file1.file_url = '/private/files/parent_dir2.txt'
file1.save()
+ def test_file_url_validation(self):
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'logo',
+ "file_url": 'https://frappe.io/files/frappe.png'
+ })
+
+ self.assertIsNone(test_file.validate())
+
+ # bad path
+ test_file.file_url = "/usr/bin/man"
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "URL must start with http:// or https://", test_file.validate)
+
+ test_file.file_url = None
+ test_file.file_name = "/usr/bin/man"
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, "There is some problem with the file url", test_file.validate)
+
+ test_file.file_url = None
+ test_file.file_name = "_file"
+ self.assertRaisesRegex(IOError, "does not exist", test_file.validate)
+
+ test_file.file_url = None
+ test_file.file_name = "/private/files/_file"
+ self.assertRaisesRegex(IOError, "does not exist", test_file.validate)
+
+ def test_make_thumbnail(self):
+ # test web image
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'logo',
+ "file_url": frappe.utils.get_url('/_test/assets/image.jpg'),
+ }).insert(ignore_permissions=True)
+
+ test_file.make_thumbnail()
+ self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg')
+
+ # test local image
+ test_file.db_set('thumbnail_url', None)
+ test_file.reload()
+ test_file.file_url = "/files/image_small.jpg"
+ test_file.make_thumbnail(suffix="xs", crop=True)
+ self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg')
+
+ frappe.clear_messages()
+ test_file.db_set('thumbnail_url', None)
+ test_file.reload()
+ test_file.file_url = frappe.utils.get_url('unknown.jpg')
+ test_file.make_thumbnail(suffix="xs")
+ self.assertEqual(json.loads(frappe.message_log[0]), {"message": f"File '{frappe.utils.get_url('unknown.jpg')}' not found"})
+ self.assertEquals(test_file.thumbnail_url, None)
+
+ def test_file_unzip(self):
+ file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip')
+ public_file_path = frappe.get_site_path('public', 'files')
+ try:
+ import shutil
+ shutil.copy(file_path, public_file_path)
+ except Exception:
+ pass
+
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_url": '/files/file.zip',
+ }).insert(ignore_permissions=True)
+
+ self.assertListEqual([file.file_name for file in unzip_file(test_file.name)],
+ ['css_asset.css', 'image.jpg', 'js_asset.min.js'])
+
+ test_file = frappe.get_doc({
+ "doctype": "File",
+ "file_url": frappe.utils.get_url('/_test/assets/image.jpg'),
+ }).insert(ignore_permissions=True)
+ self.assertRaisesRegex(frappe.exceptions.ValidationError, 'not a zip file', test_file.unzip)
+
+
class TestAttachment(unittest.TestCase):
test_doctype = 'Test For Attachment'
@@ -469,3 +545,93 @@ class TestAttachmentsAccess(unittest.TestCase):
frappe.set_user('Administrator')
frappe.db.rollback()
+
+
+class TestFileUtils(unittest.TestCase):
+ def test_extract_images_from_doc(self):
+ # with filename in data URI
+ todo = frappe.get_doc({
+ "doctype": "ToDo",
+ "description": 'Test
'
+ }).insert()
+ self.assertTrue(frappe.db.exists("File", {"attached_to_name": todo.name}))
+ self.assertIn('
', todo.description)
+ self.assertListEqual(get_attached_images('ToDo', [todo.name])[todo.name], ['/files/pix.png'])
+
+ # without filename in data URI
+ todo = frappe.get_doc({
+ "doctype": "ToDo",
+ "description": 'Test '
+ }).insert()
+ filename = frappe.db.exists("File", {"attached_to_name": todo.name})
+ self.assertIn(f'
@@ -227,6 +209,7 @@ class TestUser(unittest.TestCase):
self.assertEqual(extract_mentions(comment)[0], "test_user@example.com")
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
+ frappe.delete_doc("User Group", "Team")
doc = frappe.get_doc({
'doctype': 'User Group',
'name': 'Team',
@@ -236,14 +219,18 @@ class TestUser(unittest.TestCase):
'user': 'test1@example.com'
}]
})
- doc.insert(ignore_if_duplicate=True)
+
+ doc.insert()
comment = '''
Your OTP secret on {} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.
'.format(otp_issuer or "Frappe Framework"), - 'delayed':False, - 'retry':3 - } - enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, is_async=True, job_name=None, now=False, **email_args) - return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login.")) - else: - return frappe.throw(_("OTP secret can only be reset by the Administrator.")) - def throttle_user_creation(): if frappe.flags.in_import: return @@ -1150,15 +966,6 @@ def get_module_profile(module_profile): module_profile = frappe.get_doc('Module Profile', {'module_profile_name': module_profile}) return module_profile.get('block_modules') -def update_roles(role_profile): - users = frappe.get_all('User', filters={'role_profile_name': role_profile}) - role_profile = frappe.get_doc('Role Profile', role_profile) - roles = [role.role for role in role_profile.roles] - for d in users: - user = frappe.get_doc('User', d) - user.set('roles', []) - user.add_roles(*roles) - def create_contact(user, ignore_links=False, ignore_mandatory=False): from frappe.contacts.doctype.contact.contact import get_contact_name if user.name in ["Administrator", "Guest"]: return @@ -1217,18 +1024,18 @@ def generate_keys(user): :param user: str """ - if "System Manager" in frappe.get_roles(): - user_details = frappe.get_doc("User", user) - api_secret = frappe.generate_hash(length=15) - # if api key is not set generate api key - if not user_details.api_key: - api_key = frappe.generate_hash(length=15) - user_details.api_key = api_key - user_details.api_secret = api_secret - user_details.save() + frappe.only_for("System Manager") + user_details = frappe.get_doc("User", user) + api_secret = frappe.generate_hash(length=15) + # if api key is not set generate api key + if not user_details.api_key: + api_key = frappe.generate_hash(length=15) + user_details.api_key = api_key + user_details.api_secret = api_secret + user_details.save() + + return {"api_secret": api_secret} - return {"api_secret": api_secret} - frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) @frappe.whitelist() def switch_theme(theme): diff --git a/frappe/desk/doctype/event/test_event.py b/frappe/desk/doctype/event/test_event.py index 77211946a9..8f56d11da3 100644 --- a/frappe/desk/doctype/event/test_event.py +++ b/frappe/desk/doctype/event/test_event.py @@ -14,7 +14,7 @@ test_records = frappe.get_test_records('Event') class TestEvent(unittest.TestCase): def setUp(self): - frappe.db.sql('delete from tabEvent') + frappe.db.delete("Event") make_test_objects('Event', reset=True) self.test_records = frappe.get_test_records('Event') diff --git a/frappe/desk/doctype/note/test_note.py b/frappe/desk/doctype/note/test_note.py index 1bb1730357..3207fa9b8d 100644 --- a/frappe/desk/doctype/note/test_note.py +++ b/frappe/desk/doctype/note/test_note.py @@ -8,9 +8,9 @@ test_records = frappe.get_test_records('Note') class TestNote(unittest.TestCase): def insert_note(self): - frappe.db.sql('delete from tabVersion') - frappe.db.sql('delete from tabNote') - frappe.db.sql('delete from `tabNote Seen By`') + frappe.db.delete("Version") + frappe.db.delete("Note") + frappe.db.delete("Note Seen By") return frappe.get_doc(dict(doctype='Note', title='test note', content='test note content')).insert() diff --git a/frappe/desk/doctype/notification_log/test_notification_log.py b/frappe/desk/doctype/notification_log/test_notification_log.py index af4dee8df3..bedb10b495 100644 --- a/frappe/desk/doctype/notification_log/test_notification_log.py +++ b/frappe/desk/doctype/notification_log/test_notification_log.py @@ -2,6 +2,7 @@ # Copyright (c) 2019, Frappe Technologies and Contributors # See license.txt import frappe +from frappe.core.doctype.user.user import get_system_users from frappe.desk.form.assign_to import add as assign_task import unittest @@ -54,7 +55,4 @@ def get_todo(): return frappe.get_cached_doc('ToDo', res[0].name) def get_user(): - users = frappe.db.get_all('User', - filters={'name': ('not in', ['Administrator', 'Guest'])}, - fields='name', limit=1) - return users[0].name + return get_system_users(limit=1)[0] diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js index c7eac39490..48dd2ba108 100644 --- a/frappe/desk/doctype/system_console/system_console.js +++ b/frappe/desk/doctype/system_console/system_console.js @@ -5,7 +5,7 @@ frappe.ui.form.on('System Console', { onload: function(frm) { frappe.ui.keys.add_shortcut({ shortcut: 'shift+enter', - action: () => frm.execute_action('Execute'), + action: () => frm.page.btn_primary.trigger('click'), page: frm.page, description: __('Execute Console script'), ignore_inputs: true, @@ -14,8 +14,11 @@ frappe.ui.form.on('System Console', { refresh: function(frm) { frm.disable_save(); - frm.page.set_primary_action(__("Execute"), () => { - frm.execute_action('Execute'); + frm.page.set_primary_action(__("Execute"), $btn => { + $btn.text(__('Executing...')); + return frm.execute_action("Execute").then(() => { + $btn.text(__('Execute')); + }); }); } }); diff --git a/frappe/desk/doctype/tag/test_tag.py b/frappe/desk/doctype/tag/test_tag.py index 6eb7219c26..b9c6e0b744 100644 --- a/frappe/desk/doctype/tag/test_tag.py +++ b/frappe/desk/doctype/tag/test_tag.py @@ -6,7 +6,7 @@ from frappe.desk.doctype.tag.tag import add_tag class TestTag(unittest.TestCase): def setUp(self) -> None: - frappe.db.sql("DELETE from `tabTag`") + frappe.db.delete("Tag") frappe.db.sql("UPDATE `tabDocType` set _user_tags=''") def test_tag_count_query(self): diff --git a/frappe/desk/doctype/todo/test_todo.py b/frappe/desk/doctype/todo/test_todo.py index b38e4a059a..f6371c5921 100644 --- a/frappe/desk/doctype/todo/test_todo.py +++ b/frappe/desk/doctype/todo/test_todo.py @@ -14,7 +14,7 @@ class TestToDo(unittest.TestCase): todo = frappe.get_doc(dict(doctype='ToDo', description='test todo', assigned_by='Administrator')).insert() - frappe.db.sql('delete from `tabDeleted Document`') + frappe.db.delete("Deleted Document") todo.delete() deleted = frappe.get_doc('Deleted Document', dict(deleted_doctype=todo.doctype, deleted_name=todo.name)) @@ -27,7 +27,7 @@ class TestToDo(unittest.TestCase): frappe.db.get_value('User', todo.assigned_by, 'full_name')) def test_fetch_setup(self): - frappe.db.sql('delete from tabToDo') + frappe.db.delete("ToDo") todo_meta = frappe.get_doc('DocType', 'ToDo') todo_meta.get('fields', dict(fieldname='assigned_by_full_name'))[0].fetch_from = '' @@ -104,8 +104,8 @@ class TestToDo(unittest.TestCase): clear_permissions_cache('ToDo') frappe.db.rollback() -def test_fetch_if_empty(self): - frappe.db.sql('delete from tabToDo') + def test_fetch_if_empty(self): + frappe.db.delete("ToDo") # Allow user changes todo_meta = frappe.get_doc('DocType', 'ToDo') @@ -122,9 +122,8 @@ def test_fetch_if_empty(self): self.assertEqual(todo.assigned_by_full_name, 'Admin') # Overwrite user changes - todo_meta = frappe.get_doc('DocType', 'ToDo') - todo_meta.get('fields', dict(fieldname='assigned_by_full_name'))[0].fetch_if_empty = 0 - todo_meta.save() + todo.meta.get('fields', dict(fieldname='assigned_by_full_name'))[0].fetch_if_empty = 0 + todo.meta.save() todo.reload() todo.save() diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 09297b4e5e..754b94cdcb 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -29,8 +29,15 @@ class ToDo(Document): else: # NOTE the previous value is only available in validate method if self.get_db_value("status") != self.status: + if self.owner == frappe.session.user: + removal_message = frappe._("{0} removed their assignment.").format( + get_fullname(frappe.session.user)) + else: + removal_message = frappe._("Assignment of {0} removed by {1}").format( + get_fullname(self.owner), get_fullname(frappe.session.user)) + self._assignment = { - "text": frappe._("Assignment closed by {0}").format(get_fullname(frappe.session.user)), + "text": removal_message, "comment_type": "Assignment Completed" } diff --git a/frappe/desk/doctype/workspace/test_workspace.py b/frappe/desk/doctype/workspace/test_workspace.py index 619b3608eb..f13a136c20 100644 --- a/frappe/desk/doctype/workspace/test_workspace.py +++ b/frappe/desk/doctype/workspace/test_workspace.py @@ -1,8 +1,95 @@ # -*- coding: utf-8 -*- # Copyright (c) 2020, Frappe Technologies and Contributors # See license.txt -# import frappe +import frappe import unittest - class TestWorkspace(unittest.TestCase): - pass + def setUp(self): + create_module("Test Module") + + def tearDown(self): + frappe.db.delete("Workspace", {"module": "Test Module"}) + frappe.db.delete("DocType", {"module": "Test Module"}) + frappe.delete_doc("Module Def", "Test Module") + + # TODO: FIX ME - flaky test!!! + # def test_workspace_with_cards_specific_to_a_country(self): + # workspace = create_workspace() + # insert_card(workspace, "Card Label 1", "DocType 1", "DocType 2", "France") + # insert_card(workspace, "Card Label 2", "DocType A", "DocType B") + + # workspace.insert(ignore_if_duplicate = True) + + # cards = workspace.get_link_groups() + + # if frappe.get_system_settings('country') == "France": + # self.assertEqual(len(cards), 2) + # else: + # self.assertEqual(len(cards), 1) + +def create_module(module_name): + module = frappe.get_doc({ + "doctype": "Module Def", + "module_name": module_name, + "app_name": "frappe" + }) + module.insert(ignore_if_duplicate = True) + + return module + +def create_workspace(**args): + workspace = frappe.new_doc("Workspace") + args = frappe._dict(args) + + workspace.name = args.name or "Test Workspace" + workspace.label = args.label or "Test Workspace" + workspace.category = args.category or "Modules" + workspace.is_standard = args.is_standard or 1 + workspace.module = "Test Module" + + return workspace + +def insert_card(workspace, card_label, doctype1, doctype2, country=None): + workspace.append("links", { + "type": "Card Break", + "label": card_label, + "only_for": country + }) + + create_doctype(doctype1, "Test Module") + workspace.append("links", { + "type": "Link", + "label": doctype1, + "only_for": country, + "link_type": "DocType", + "link_to": doctype1 + }) + + create_doctype(doctype2, "Test Module") + workspace.append("links", { + "type": "Link", + "label": doctype2, + "only_for": country, + "link_type": "DocType", + "link_to": doctype2 + }) + +def create_doctype(doctype_name, module): + frappe.get_doc({ + 'doctype': 'DocType', + 'name': doctype_name, + 'module': module, + 'custom': 1, + 'autoname': 'field:title', + 'fields': [ + {'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, + {'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'}, + {'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'}, + {'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'}, + {'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, + {'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'} + ], + 'permissions': [ + {'role': 'System Manager'} + ] + }).insert(ignore_if_duplicate = True) diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index e2ae38faf1..020f3153df 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -28,7 +28,6 @@ "pin_to_bottom", "hide_custom", "public", - "content_section", "content", "section_break_2", "charts_label", @@ -39,6 +38,7 @@ "section_break_18", "cards_label", "links", + "roles_section", "roles" ], "fields": [ @@ -46,6 +46,7 @@ "fieldname": "label", "fieldtype": "Data", "label": "Name", + "reqd": 1, "unique": 1 }, { @@ -232,21 +233,18 @@ { "fieldname": "title", "fieldtype": "Data", - "label": "Title" + "label": "Title", + "reqd": 1 }, { "fieldname": "parent_page", "fieldtype": "Data", "label": "Parent Page" }, - { - "fieldname": "content_section", - "fieldtype": "Section Break", - "label": "Content" - }, { "fieldname": "content", "fieldtype": "Long Text", + "hidden": 1, "label": "Content" }, { @@ -259,10 +257,15 @@ "fieldtype": "Table", "label": "Roles", "options": "Has Role" + }, + { + "fieldname": "roles_section", + "fieldtype": "Section Break", + "label": "Roles" } ], "links": [], - "modified": "2021-08-05 11:49:09.028243", + "modified": "2021-08-19 12:51:00.233017", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 0821ae03c4..31bb551330 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -17,6 +17,12 @@ class Workspace(Document): frappe.throw(_("You need to be in developer mode to edit this document")) validate_route_conflict(self.doctype, self.name) + try: + if not isinstance(loads(self.content), list): + raise + except Exception: + frappe.throw(_("Content data shoud be a list")) + duplicate_exists = frappe.db.exists("Workspace", { "name": ["!=", self.name], 'is_default': 1, 'extends': self.extends }) @@ -56,7 +62,7 @@ class Workspace(Document): for link in self.links: link = link.as_dict() if link.type == "Card Break": - if card_links and (not current_card.only_for or current_card.only_for == frappe.get_system_settings('country')): + if card_links and (not current_card['only_for'] or current_card['only_for'] == frappe.get_system_settings('country')): current_card['links'] = card_links cards.append(current_card) diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index bfceee6ea2..d7ac940d21 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -5,7 +5,7 @@ import frappe, json import frappe.desk.form.meta import frappe.desk.form.load from frappe.desk.form.document_follow import follow_document -from frappe.utils.file_manager import extract_images_from_html +from frappe.core.doctype.file.file import extract_images_from_html from frappe import _ diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index b42c9c89a0..610eaf466a 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -391,7 +391,7 @@ def handle_duration_fieldtype_values(result, columns): return result -def build_xlsx_data(columns, data, visible_idx, include_indentation): +def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visible_idx=False): result = [[]] column_widths = [] @@ -407,7 +407,7 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation): # build table from result for row_idx, row in enumerate(data.result): # only pick up rows that are visible in the report - if row_idx in visible_idx: + if ignore_visible_idx or row_idx in visible_idx: row_data = [] if isinstance(row, dict): for col_idx, column in enumerate(data.columns): diff --git a/frappe/desk/search.py b/frappe/desk/search.py index f9b65fc98e..9a5e7533d1 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -265,6 +265,7 @@ def get_users_for_mentions(): 'name': ['not in', ('Administrator', 'Guest')], 'allowed_in_mentions': True, 'user_type': 'System User', + 'enabled': True, }) def get_user_groups(): diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index f30279e308..ccfff594b7 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -13,6 +13,7 @@ from frappe.utils import (format_time, get_link_to_form, get_url_to_report, from frappe.model.naming import append_number_if_name_exists from frappe.utils.csvutils import to_csv from frappe.utils.xlsxutils import make_xlsx +from frappe.desk.query_report import build_xlsx_data max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 @@ -99,13 +100,21 @@ class AutoEmailReport(Document): return self.get_html_table(columns, data) elif self.format == 'XLSX': - spreadsheet_data = self.get_spreadsheet_data(columns, data) - xlsx_file = make_xlsx(spreadsheet_data, "Auto Email Report") + report_data = frappe._dict() + report_data['columns'] = columns + report_data['result'] = data + + xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True) + xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths) return xlsx_file.getvalue() elif self.format == 'CSV': - spreadsheet_data = self.get_spreadsheet_data(columns, data) - return to_csv(spreadsheet_data) + report_data = frappe._dict() + report_data['columns'] = columns + report_data['result'] = data + + xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True) + return to_csv(xlsx_data) else: frappe.throw(_('Invalid Output Format')) @@ -126,18 +135,6 @@ class AutoEmailReport(Document): 'edit_report_settings': get_link_to_form('Auto Email Report', self.name) }) - @staticmethod - def get_spreadsheet_data(columns, data): - out = [[_(df.label) for df in columns], ] - for row in data: - new_row = [] - out.append(new_row) - for df in columns: - if df.fieldname not in row: continue - new_row.append(frappe.format(row[df.fieldname], df, row)) - - return out - def get_file_name(self): return "{0}.{1}".format(self.report.replace(" ", "-").replace("/", "-"), self.format.lower()) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index ecd59f42bb..fb7349adba 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -137,8 +137,6 @@ class EmailAccount(Document): def on_update(self): """Check there is only one default of each type.""" - from frappe.core.doctype.user.user import setup_user_email_inbox - self.check_automatic_linking_email_account() self.there_must_be_only_one_default() setup_user_email_inbox(email_account=self.name, awaiting_password=self.awaiting_password, @@ -532,8 +530,6 @@ class EmailAccount(Document): def on_trash(self): """Clear communications where email account is linked""" - from frappe.core.doctype.user.user import remove_user_email_inbox - frappe.db.sql("update `tabCommunication` set email_account='' where email_account=%s", self.name) remove_user_email_inbox(email_account=self.name) @@ -724,3 +720,84 @@ def get_max_email_uid(email_account): else: max_uid = cint(result[0].get("uid", 0)) + 1 return max_uid + + +def setup_user_email_inbox(email_account, awaiting_password, email_id, enable_outgoing): + """ setup email inbox for user """ + from frappe.core.doctype.user.user import ask_pass_update + + def add_user_email(user): + user = frappe.get_doc("User", user) + row = user.append("user_emails", {}) + + row.email_id = email_id + row.email_account = email_account + row.awaiting_password = awaiting_password or 0 + row.enable_outgoing = enable_outgoing or 0 + + user.save(ignore_permissions=True) + + update_user_email_settings = False + if not all([email_account, email_id]): + return + + user_names = frappe.db.get_values("User", {"email": email_id}, as_dict=True) + if not user_names: + return + + for user in user_names: + user_name = user.get("name") + + # check if inbox is alreay configured + user_inbox = frappe.db.get_value("User Email", { + "email_account": email_account, + "parent": user_name + }, ["name"]) or None + + if not user_inbox: + add_user_email(user_name) + else: + # update awaiting password for email account + update_user_email_settings = True + + if update_user_email_settings: + frappe.db.sql("""UPDATE `tabUser Email` SET awaiting_password = %(awaiting_password)s, + enable_outgoing = %(enable_outgoing)s WHERE email_account = %(email_account)s""", { + "email_account": email_account, + "enable_outgoing": enable_outgoing, + "awaiting_password": awaiting_password or 0 + }) + else: + users = " and ".join([frappe.bold(user.get("name")) for user in user_names]) + frappe.msgprint(_("Enabled email inbox for user {0}").format(users)) + ask_pass_update() + +def remove_user_email_inbox(email_account): + """ remove user email inbox settings if email account is deleted """ + if not email_account: + return + + users = frappe.get_all("User Email", filters={ + "email_account": email_account + }, fields=["parent as name"]) + + for user in users: + doc = frappe.get_doc("User", user.get("name")) + to_remove = [row for row in doc.user_emails if row.email_account == email_account] + [doc.remove(row) for row in to_remove] + + doc.save(ignore_permissions=True) + +@frappe.whitelist(allow_guest=False) +def set_email_password(email_account, user, password): + account = frappe.get_doc("Email Account", email_account) + if account.awaiting_password: + account.awaiting_password = 0 + account.password = password + try: + account.save(ignore_permissions=True) + except Exception: + frappe.db.rollback() + return False + + return True \ No newline at end of file diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py index 35cacac45a..da03a5959e 100644 --- a/frappe/email/doctype/email_account/test_email_account.py +++ b/frappe/email/doctype/email_account/test_email_account.py @@ -34,8 +34,8 @@ class TestEmailAccount(unittest.TestCase): def setUp(self): frappe.flags.mute_emails = False frappe.flags.sent_mail = None - frappe.db.sql('delete from `tabEmail Queue`') - frappe.db.sql('delete from `tabUnhandled Email`') + frappe.db.delete("Email Queue") + frappe.db.delete("Unhandled Email") def get_test_mail(self, fname): with open(os.path.join(os.path.dirname(__file__), "test_mails", fname), "r") as f: @@ -60,7 +60,7 @@ class TestEmailAccount(unittest.TestCase): comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"}) comm.db_set("creation", datetime.now() - timedelta(seconds = 30 * 60)) - frappe.db.sql("DELETE FROM `tabEmail Queue`") + frappe.db.delete("Email Queue") notify_unreplied() self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": comm.reference_doctype, "reference_name": comm.reference_name, "status":"Not Sent"})) @@ -183,7 +183,7 @@ class TestEmailAccount(unittest.TestCase): def test_threading_by_message_id(self): cleanup() - frappe.db.sql("""delete from `tabEmail Queue`""") + frappe.db.delete("Email Queue") # reference document for testing event = frappe.get_doc(dict(doctype='Event', subject='test-message')).insert() @@ -242,8 +242,8 @@ class TestInboundMail(unittest.TestCase): def setUp(self): cleanup() - frappe.db.sql('delete from `tabEmail Queue`') - frappe.db.sql('delete from `tabToDo`') + frappe.db.delete("Email Queue") + frappe.db.delete("ToDo") def get_test_mail(self, fname): with open(os.path.join(os.path.dirname(__file__), "test_mails", fname), "r") as f: diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 667d0fb34c..a118240488 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -165,6 +165,7 @@ class Newsletter(WebsiteGenerator): sender = self.send_from or frappe.utils.get_formatted_email(self.owner) args = {"message": message, "name": self.name} + is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes) frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test frappe.sendmail( @@ -184,7 +185,7 @@ class Newsletter(WebsiteGenerator): args=args, ) - frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test + frappe.db.auto_commit_on_many_writes = is_auto_commit_set def get_message(self) -> str: if self.content_type == "HTML": diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py index d6358ccbbe..2629050c1b 100644 --- a/frappe/email/doctype/notification/test_notification.py +++ b/frappe/email/doctype/notification/test_notification.py @@ -9,7 +9,7 @@ test_dependencies = ["User", "Notification"] class TestNotification(unittest.TestCase): def setUp(self): - frappe.db.sql("""delete from `tabEmail Queue`""") + frappe.db.delete("Email Queue") frappe.set_user("test@example.com") if not frappe.db.exists('Notification', {'name': 'ToDo Status Update'}, 'name'): @@ -50,7 +50,7 @@ class TestNotification(unittest.TestCase): self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Communication", "reference_name": communication.name, "status":"Not Sent"})) - frappe.db.sql("""delete from `tabEmail Queue`""") + frappe.db.delete("Email Queue") communication.reload() communication.content = "test 2" @@ -189,9 +189,9 @@ class TestNotification(unittest.TestCase): def test_cc_jinja(self): - frappe.db.sql("""delete from `tabUser` where email='test_jinja@example.com'""") - frappe.db.sql("""delete from `tabEmail Queue`""") - frappe.db.sql("""delete from `tabEmail Queue Recipient`""") + frappe.db.delete("User", {"email": "test_jinja@example.com"}) + frappe.db.delete("Email Queue") + frappe.db.delete("Email Queue Recipient") test_user = frappe.new_doc("User") test_user.name = 'test_jinja' @@ -205,9 +205,9 @@ class TestNotification(unittest.TestCase): self.assertTrue(frappe.db.get_value("Email Queue Recipient", {"recipient": "test_jinja@example.com"})) - frappe.db.sql("""delete from `tabUser` where email='test_jinja@example.com'""") - frappe.db.sql("""delete from `tabEmail Queue`""") - frappe.db.sql("""delete from `tabEmail Queue Recipient`""") + frappe.db.delete("User", {"email": "test_jinja@example.com"}) + frappe.db.delete("Email Queue") + frappe.db.delete("Email Queue Recipient") def test_notification_to_assignee(self): todo = frappe.new_doc('ToDo') diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py index b77e311f7e..1470f666a1 100644 --- a/frappe/integrations/doctype/webhook/test_webhook.py +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -11,9 +11,9 @@ class TestWebhook(unittest.TestCase): @classmethod def setUpClass(cls): # delete any existing webhooks - frappe.db.sql("DELETE FROM tabWebhook") + frappe.db.delete("Webhook") # Delete existing logs if any - frappe.db.sql("DELETE FROM `tabWebhook Request Log`") + frappe.db.delete("Webhook Request Log") # create test webhooks cls.create_sample_webhooks() @@ -46,7 +46,7 @@ class TestWebhook(unittest.TestCase): @classmethod def tearDownClass(cls): # delete any existing webhooks - frappe.db.sql("DELETE FROM tabWebhook") + frappe.db.delete("Webhook") def setUp(self): # retrieve or create a User webhook for `after_insert` @@ -168,7 +168,7 @@ class TestWebhook(unittest.TestCase): def test_webhook_req_log_creation(self): if not frappe.db.get_value('User', 'user2@integration.webhooks.test.com'): user = frappe.get_doc({ - 'doctype': 'User', + 'doctype': 'User', 'email': 'user2@integration.webhooks.test.com', 'first_name': 'user2' }).insert() diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 1e3ef53fbd..5a204caf70 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -874,7 +874,7 @@ class BaseDocument(object): return self._precision[cache_key][fieldname] - def get_formatted(self, fieldname, doc=None, currency=None, absolute_value=False, translated=False): + def get_formatted(self, fieldname, doc=None, currency=None, absolute_value=False, translated=False, format=None): from frappe.utils.formatters import format_value df = self.meta.get_field(fieldname) @@ -898,7 +898,7 @@ class BaseDocument(object): if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)): val = abs(self.get(fieldname)) - return format_value(val, df=df, doc=doc, currency=currency) + return format_value(val, df=df, doc=doc, currency=currency, format=format) def is_print_hide(self, fieldname, df=None, for_print=True): """Returns true if fieldname is to be hidden for print. diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index fbbf1a4852..9ce74054e7 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -10,7 +10,7 @@ import frappe.model.meta from frappe import _ from frappe import get_module_path from frappe.model.dynamic_links import get_dynamic_link_map -from frappe.core.doctype.file.file import remove_all +from frappe.utils.file_manager import remove_all from frappe.utils.password import delete_all_passwords_for from frappe.model.naming import revert_series_if_last from frappe.utils.global_search import delete_for_document @@ -190,7 +190,7 @@ def delete_from_table(doctype, name, ignore_doctypes, doc): # delete from child tables for t in list(set(tables)): if t not in ignore_doctypes: - frappe.db.sql("delete from `tab%s` where parenttype=%s and parent = %s" % (t, '%s', '%s'), (doctype, name)) + frappe.db.delete(t, {"parenttype": doctype, "parent": name}) def update_flags(doc, flags=None, ignore_permissions=False): if ignore_permissions: @@ -323,9 +323,10 @@ def delete_dynamic_links(doctype, name): def delete_references(doctype, reference_doctype, reference_name, reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'): - frappe.db.sql('''delete from `tab{0}` - where {1}=%s and {2}=%s'''.format(doctype, reference_doctype_field, reference_name_field), # nosec - (reference_doctype, reference_name)) + frappe.db.delete(doctype, { + reference_doctype_field: reference_doctype, + reference_name_field: reference_name + }) def clear_references(doctype, reference_doctype, reference_name, reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'): diff --git a/frappe/model/document.py b/frappe/model/document.py index ee12fd89e0..37549e2001 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -385,8 +385,7 @@ class Document(BaseDocument): [self.name, self.doctype, fieldname] + rows) if len(deleted_rows) > 0: # delete rows that do not match the ones in the document - frappe.db.sql("""delete from `tab{0}` where name in ({1})""".format(df.options, - ','.join(['%s'] * len(deleted_rows))), tuple(row[0] for row in deleted_rows)) + frappe.db.delete(df.options, {"name": ("in", tuple(row[0] for row in deleted_rows))}) else: # no rows found, delete all rows diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 0f3e57a5a0..ed2a839dc1 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -114,8 +114,7 @@ def sync_customizations_for_doctype(data, folder): doc.db_insert() if custom_doctype != 'Custom Field': - frappe.db.sql('delete from `tab{0}` where `{1}` =%s'.format( - custom_doctype, doctype_fieldname), doc_type) + frappe.db.delete(custom_doctype, {doctype_fieldname: doc_type}) for d in data[key]: _insert(d) diff --git a/frappe/patches.txt b/frappe/patches.txt index 87919b0247..41ca1a1724 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -182,4 +182,4 @@ frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty frappe.patches.v14_0.drop_data_import_legacy frappe.patches.v14_0.rename_cancelled_documents -frappe.patches.v14_0.update_workspace2 +frappe.patches.v14_0.update_workspace2 # 25.08.2021 diff --git a/frappe/patches/v14_0/update_workspace2.py b/frappe/patches/v14_0/update_workspace2.py index 2d7eb4cc76..c212faee76 100644 --- a/frappe/patches/v14_0/update_workspace2.py +++ b/frappe/patches/v14_0/update_workspace2.py @@ -50,11 +50,11 @@ def create_content(doc): return content def update_wspace(doc, seq, content): - if not doc.is_standard and not doc.public: + if not doc.title and not doc.content and not doc.is_standard and not doc.public: doc.sequence_id = seq + 1 doc.content = json.dumps(content) doc.public = 0 - doc.title = doc.extends + doc.title = doc.extends or doc.label doc.extends = '' doc.category = '' doc.onboarding = '' diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg index f216374526..b2f1428967 100644 --- a/frappe/public/icons/timeless/symbol-defs.svg +++ b/frappe/public/icons/timeless/symbol-defs.svg @@ -567,7 +567,7 @@{{ __("Recorder is Inactive") }}
+{{ __("Recorder is Inactive.") }}
+{{ __("Start recording or drag & drop a previously exported data file to view it.") }}
{{ __("No Requests found") }}
{{ __("Go make some noise") }}
Your OTP secret on {0} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.
').format(otp_issuer or "Frappe Framework"), + 'delayed':False, + 'retry':3 + } + enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, is_async=True, job_name=None, now=False, **email_args) + return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login.")) + else: + return frappe.throw(_("OTP secret can only be reset by the Administrator.")) \ No newline at end of file diff --git a/frappe/utils/csvutils.py b/frappe/utils/csvutils.py index 734d68fe8a..69b7f6f2d3 100644 --- a/frappe/utils/csvutils.py +++ b/frappe/utils/csvutils.py @@ -6,16 +6,7 @@ import json import csv import requests from io import StringIO -from frappe.utils import encode, cstr, cint, flt, comma_or - -def read_csv_content_from_uploaded_file(ignore_encoding=False): - if getattr(frappe, "uploaded_file", None): - with open(frappe.uploaded_file, "r") as upfile: - fcontent = upfile.read() - else: - _file = frappe.new_doc("File") - fcontent = _file.get_uploaded_content() - return read_csv_content(fcontent, ignore_encoding) +from frappe.utils import cstr, cint, flt, comma_or def read_csv_content_from_attached_file(doc): fileid = frappe.get_all("File", fields = ["name"], filters = {"attached_to_doctype": doc.doctype, diff --git a/frappe/utils/file_manager.py b/frappe/utils/file_manager.py index 7efdff299b..b1e088d641 100644 --- a/frappe/utils/file_manager.py +++ b/frappe/utils/file_manager.py @@ -213,28 +213,22 @@ def write_file(content, fname, is_private=0): return get_files_path(fname, is_private=is_private) -def remove_all(dt, dn, from_delete=False): +def remove_all(dt, dn, from_delete=False, delete_permanently=False): """remove all files in a transaction""" try: for fid in frappe.db.sql_list("""select name from `tabFile` where attached_to_doctype=%s and attached_to_name=%s""", (dt, dn)): - remove_file(fid, dt, dn, from_delete) + if from_delete: + # If deleting a doc, directly delete files + frappe.delete_doc("File", fid, ignore_permissions=True, delete_permanently=delete_permanently) + else: + # Removes file and adds a comment in the document it is attached to + remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn, + from_delete=from_delete, delete_permanently=delete_permanently) except Exception as e: if e.args[0]!=1054: raise # (temp till for patched) - -def remove_file_by_url(file_url, doctype=None, name=None): - if doctype and name: - fid = frappe.db.get_value("File", {"file_url": file_url, - "attached_to_doctype": doctype, "attached_to_name": name}) - else: - fid = frappe.db.get_value("File", {"file_url": file_url}) - - if fid: - return remove_file(fid) - - -def remove_file(fid, attached_to_doctype=None, attached_to_name=None, from_delete=False): +def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_delete=False, delete_permanently=False): """Remove file and File entry""" file_name = None if not (attached_to_doctype and attached_to_name): @@ -252,8 +246,7 @@ def remove_file(fid, attached_to_doctype=None, attached_to_name=None, from_delet if not file_name: file_name = frappe.db.get_value("File", fid, "file_name") comment = doc.add_comment("Attachment Removed", _("Removed {0}").format(file_name)) - - frappe.delete_doc("File", fid, ignore_permissions=ignore_permissions) + frappe.delete_doc("File", fid, ignore_permissions=ignore_permissions, delete_permanently=delete_permanently) return comment @@ -372,76 +365,6 @@ def download_file(file_url): frappe.local.response.filecontent = filedata frappe.local.response.type = "download" -def extract_images_from_doc(doc, fieldname): - content = doc.get(fieldname) - content = extract_images_from_html(doc, content) - if frappe.flags.has_dataurl: - doc.set(fieldname, content) - - -def extract_images_from_html(doc, content): - frappe.flags.has_dataurl = False - - def _save_file(match): - data = match.group(1) - data = data.split("data:")[1] - headers, content = data.split(",") - mtype = headers.split(";")[0] - - if isinstance(content, str): - content = content.encode("utf-8") - if b"," in content: - content = content.split(b",")[1] - content = base64.b64decode(content) - - content = optimize_image(content, mtype) - - if "filename=" in headers: - filename = headers.split("filename=")[-1] - - # decode filename - if not isinstance(filename, str): - filename = str(filename, 'utf-8') - else: - filename = get_random_filename(content_type=mtype) - - doctype = doc.parenttype if doc.parent else doc.doctype - name = doc.parent or doc.name - - if doc.doctype == "Comment": - doctype = doc.reference_doctype - name = doc.reference_name - - # TODO fix this - file_url = save_file(filename, content, doctype, name, decode=False).get("file_url") - if not frappe.flags.has_dataurl: - frappe.flags.has_dataurl = True - - return '