Merge branch 'develop' into fix-link-selector

This commit is contained in:
Suraj Shetty 2021-08-31 09:24:52 +05:30 committed by GitHub
commit 552dc47e90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 675 additions and 151 deletions

View file

@ -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

View file

@ -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

View file

@ -1,56 +1,72 @@
# 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)
# this is a push build, run all builds
if not pr_number:
os.system('echo "::set-output name=build::strawberry"')
sys.exit(0)
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)
files_list = files_list or get_files_list(pr_number=pr_number, repo=repo)
if only_docs_changed:
print("Only docs were updated, stopping build process.")
sys.exit(0)
if not files_list:
print("No files' changes detected. Build is shutting")
sys.exit(0)
if only_js_changed and build_type == "server":
print("Only JavaScript code was updated; Stopping Python 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_py_changed and build_type == "ui":
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)
if ci_files_changed:
print("CI related files were updated, running all build processes.")
sys.exit(2)
elif only_docs_changed:
print("Only docs were updated, stopping build process.")
sys.exit(0)
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"')

View file

@ -2,6 +2,11 @@ name: Patch
on: [pull_request, workflow_dispatch]
concurrency:
group: patch-mariadb-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
@ -26,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
@ -39,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
@ -51,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 }}
@ -63,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 }}
@ -70,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

View file

@ -6,6 +6,11 @@ on:
push:
branches: [ develop ]
concurrency:
group: server-mariadb-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
@ -35,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
@ -55,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
@ -67,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 }}
@ -79,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 }}
@ -86,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}
@ -111,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

View file

@ -4,6 +4,10 @@ on:
pull_request:
workflow_dispatch:
concurrency:
group: server-postgres-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
@ -37,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
@ -57,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
@ -69,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 }}
@ -81,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 }}
@ -88,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 }}

View file

@ -6,6 +6,10 @@ on:
push:
branches: [ develop ]
concurrency:
group: ui-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
@ -35,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
@ -55,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
@ -67,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 }}
@ -79,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
@ -88,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 }}
@ -95,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

View file

@ -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

View file

@ -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");
});
});
});

View file

@ -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');
});
});
});

View file

@ -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();
});
});

View file

@ -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"));
});
});
}

View file

@ -313,8 +313,16 @@ 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'''
@ -531,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"])
@ -838,22 +875,6 @@ def unzip_file(name):
files = file_obj.unzip()
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):

View file

@ -440,6 +440,7 @@ class TestFile(unittest.TestCase):
}).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'
@ -569,3 +570,68 @@ class TestFileUtils(unittest.TestCase):
from frappe.core.doctype.file.file import create_new_folder
folder = create_new_folder('test_folder', 'Home')
self.assertTrue(folder.is_folder)
class TestFileOptimization(unittest.TestCase):
def test_optimize_file(self):
file_path = frappe.get_app_path("frappe", "tests/data/sample_image_for_optimization.jpg")
with open(file_path, "rb") as f:
file_content = f.read()
test_file = frappe.get_doc({
"doctype": "File",
"file_name": "sample_image_for_optimization.jpg",
"content": file_content
}).insert()
original_size = test_file.file_size
original_content_hash = test_file.content_hash
test_file.optimize_file()
optimized_size = test_file.file_size
updated_content_hash = test_file.content_hash
self.assertLess(optimized_size, original_size)
self.assertNotEqual(original_content_hash, updated_content_hash)
test_file.delete()
def test_optimize_svg(self):
file_path = frappe.get_app_path("frappe", "tests/data/sample_svg.svg")
with open(file_path, "rb") as f:
file_content = f.read()
test_file = frappe.get_doc({
"doctype": "File",
"file_name": "sample_svg.svg",
"content": file_content
}).insert()
self.assertRaises(TypeError, test_file.optimize_file)
test_file.delete()
def test_optimize_textfile(self):
test_file = frappe.get_doc({
"doctype": "File",
"file_name": "sample_text.txt",
"content": "Text files cannot be optimized"
}).insert()
self.assertRaises(NotImplementedError, test_file.optimize_file)
test_file.delete()
def test_optimize_folder(self):
test_folder = frappe.get_doc("File", "Home/Attachments")
self.assertRaises(TypeError, test_folder.optimize_file)
def test_revert_optimized_file_on_rollback(self):
file_path = frappe.get_app_path("frappe", "tests/data/sample_image_for_optimization.jpg")
with open(file_path, "rb") as f:
file_content = f.read()
test_file = frappe.get_doc({
"doctype": "File",
"file_name": "sample_image_for_optimization.jpg",
"content": file_content
}).insert()
image_path = test_file.get_full_path()
size_before_optimization = os.stat(image_path).st_size
test_file.optimize_file()
frappe.db.rollback()
size_after_rollback = os.stat(image_path).st_size
self.assertEqual(size_before_optimization, size_after_rollback)
test_file.delete()

View file

@ -12,19 +12,20 @@ class TestWorkspace(unittest.TestCase):
frappe.db.delete("DocType", {"module": "Test Module"})
frappe.delete_doc("Module Def", "Test Module")
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")
# 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)
# workspace.insert(ignore_if_duplicate = True)
cards = workspace.get_link_groups()
# cards = workspace.get_link_groups()
if frappe.get_system_settings('country') == "France":
self.assertEqual(len(cards), 2)
else:
self.assertEqual(len(cards), 1)
# 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({
@ -91,4 +92,4 @@ def create_doctype(doctype_name, module):
'permissions': [
{'role': 'System Manager'}
]
}).insert(ignore_if_duplicate = True)
}).insert(ignore_if_duplicate = True)

View file

@ -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):

View file

@ -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():

View file

@ -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())

View file

@ -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.

View file

@ -567,7 +567,7 @@
<path d="M6.45466 8.81824L4.47873 10.7942C3.85205 11.4211 3.5 12.2713 3.5 13.1577C3.5 14.0442 3.85205 14.8943 4.47873 15.5213V15.5213C5.10568 16.148 5.95584 16.5 6.84229 16.5C7.72874 16.5 8.5789 16.148 9.20584 15.5213L11.1818 13.5453" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol viewBox="0 0 24 24" fill="none" id="icon-scan" xmlns="http://www.w3.org/2000/svg">
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"
stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/>
</symbol>
<symbol viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-dashboard">

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View file

@ -13,6 +13,13 @@ frappe.data_import.DataExporter = class DataExporter {
this.dialog = new frappe.ui.Dialog({
title: __('Export Data'),
fields: [
{
fieldtype: 'Select',
fieldname: 'file_type',
label: __('File Type'),
options: ['Excel', 'CSV'],
default: 'CSV'
},
{
fieldtype: 'Select',
fieldname: 'export_records',
@ -45,13 +52,6 @@ frappe.data_import.DataExporter = class DataExporter {
fieldname: 'filter_area',
depends_on: doc => doc.export_records === 'by_filter'
},
{
fieldtype: 'Select',
fieldname: 'file_type',
label: __('File Type'),
options: ['Excel', 'CSV'],
default: 'CSV'
},
{
fieldtype: 'Section Break'
},
@ -141,7 +141,7 @@ frappe.data_import.DataExporter = class DataExporter {
let for_insert = this.exporting_for === 'Insert New Records';
let section_title = for_insert ? __('Select Fields To Insert') : __('Select Fields To Update');
let $select_all_buttons = $(`
<div>
<div class="mb-3">
<h6 class="form-section-heading uppercase">${section_title}</h6>
<button class="btn btn-default btn-xs" data-action="select_all">
${__('Select All')}

View file

@ -28,7 +28,7 @@ export default class FileUploader {
}
if (attach_doc_image) {
restrictions.allowed_file_types = ['.jpg', '.jpeg', '.png'];
restrictions.allowed_file_types = ['image/jpeg', 'image/png'];
}
this.$fileuploader = new Vue({
@ -70,8 +70,10 @@ export default class FileUploader {
this.uploader.$watch('hide_dialog_footer', (hide_dialog_footer) => {
if (hide_dialog_footer) {
this.dialog && this.dialog.footer.addClass('hide');
this.dialog.$wrapper.data('bs.modal')._config.backdrop = 'static';
} else {
this.dialog && this.dialog.footer.removeClass('hide');
this.dialog.$wrapper.data('bs.modal')._config.backdrop = true;
}
});

View file

@ -9,12 +9,7 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex
this.ace_editor_target = $('<div class="ace-editor-target"></div>')
.appendTo(this.input_area);
this.expanded = false;
this.$expand_button = $(`<button class="btn btn-xs btn-default">${this.get_button_label()}</button>`).click(() => {
this.expanded = !this.expanded;
this.refresh_height();
this.toggle_label();
}).appendTo(this.$input_wrapper);
// styling
this.ace_editor_target.addClass('border rounded');
this.ace_editor_target.css('height', 300);
@ -22,6 +17,21 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex
// initialize
const ace = window.ace;
this.editor = ace.edit(this.ace_editor_target.get(0));
if (this.df.max_lines || this.df.min_lines) {
if (this.df.max_lines)
this.editor.setOption("maxLines", this.df.max_lines);
if (this.df.min_lines)
this.editor.setOption("minLines", this.df.min_lines);
} else {
this.expanded = false;
this.$expand_button = $(`<button class="btn btn-xs btn-default">${this.get_button_label()}</button>`).click(() => {
this.expanded = !this.expanded;
this.refresh_height();
this.toggle_label();
}).appendTo(this.$input_wrapper);
}
this.editor.setTheme('ace/theme/tomorrow');
this.editor.setOption("showPrintMargin", false);
this.editor.setOption("wrap", this.df.wrap);

View file

@ -36,4 +36,9 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
$tp.$secondsText.prev().css('display', 'none');
}
}
get_model_value() {
let value = super.get_model_value();
return frappe.datetime.get_datetime_as_string(value);
}
};

View file

@ -283,21 +283,13 @@ frappe.ui.form.Dashboard = class FormDashboard {
$(frappe.render_template('form_links', this.data))
.appendTo(transactions_area_body);
if (this.data.reports && this.data.reports.length) {
$(frappe.render_template('report_links', this.data))
.appendTo(transactions_area_body);
}
this.render_report_links();
// bind links
transactions_area_body.find(".badge-link").on('click', function() {
me.open_document_list($(this).closest('.document-link'));
});
// bind reports
transactions_area_body.find(".report-link").on('click', function() {
me.open_report($(this).parent());
});
// bind open notifications
transactions_area_body.find('.open-notification').on('click', function() {
me.open_document_list($(this).parent(), true);
@ -311,6 +303,18 @@ frappe.ui.form.Dashboard = class FormDashboard {
this.data_rendered = true;
}
render_report_links() {
let parent = this.transactions_area;
if (this.data.reports && this.data.reports.length) {
$(frappe.render_template('report_links', this.data))
.appendTo(parent);
// bind reports
parent.find(".report-link").on('click', (e) => {
this.open_report($(e.target).parent());
});
}
}
open_report($link) {
let report = $link.attr('data-report');
@ -318,6 +322,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
? (this.data.non_standard_fieldnames[report] || this.data.fieldname)
: this.data.fieldname;
frappe.provide('frappe.route_options');
frappe.route_options[fieldname] = this.frm.doc.name;
frappe.set_route("query-report", report);
}

View file

@ -261,4 +261,14 @@ export default class BulkOperations {
});
dialog.show();
}
export(doctype, docnames) {
frappe.require('data_import_tools.bundle.js', () => {
const data_exporter = new frappe.data_import.DataExporter(doctype, 'Insert New Records');
data_exporter.dialog.set_value('export_records', 'by_filter');
data_exporter.filter_group.add_filters_to_filter_group(
[[doctype, "name", "in", docnames, false]]
);
});
}
}

View file

@ -367,6 +367,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
if (
!this.settings.hide_name_column &&
this.meta.title_field &&
this.meta.title_field !== 'name'
) {
this.columns.push({
@ -867,8 +868,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
filters: this.get_filters_for_args()
}).then(total_count => {
this.total_count = total_count || current_count;
this.count_without_children = count_without_children !== current_count ? count_without_children : undefined;
let str = __('{0} of {1}', [current_count, this.total_count]);
if (count_without_children !== current_count) {
if (this.count_without_children) {
str = __('{0} of {1} ({2} rows with children)', [count_without_children, this.total_count, current_count]);
}
return str;
@ -1730,11 +1732,25 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
};
};
const bulk_export = () => {
return {
label: __("Export"),
action: () => {
const docnames = this.get_checked_items(true);
bulk_operations.export(doctype, docnames);
},
standard: true
};
};
// bulk edit
if (has_editable_fields(doctype)) {
actions_menu_items.push(bulk_edit());
}
actions_menu_items.push(bulk_export());
// bulk assignment
actions_menu_items.push(bulk_assignment());

View file

@ -1,5 +1,5 @@
<template>
<div>
<div v-cloak @drop.prevent="import_data" @dragover.prevent>
<div class="page-form">
<div class="filter-list">
<div class="tag-filters-area">
@ -46,7 +46,7 @@
</div>
<div class="result-list">
<div class="list-row-container" v-for="(request, index) in paginated(sorted(filtered(requests)))" :key="index" @click="route_to_request_detail(request.uuid)">
<div class="list-row-container" v-for="(request, index) in paginated(sorted(filtered(requests)))" :key="index" @click="route_to_request_detail(request)">
<div class="level list-row small">
<div class="level-left ellipsis">
<div class="list-row-col ellipsis list-subject level ">
@ -71,15 +71,16 @@
</div>
<div v-if="requests.length == 0" class="no-result text-muted flex justify-center align-center" style="">
<div class="msg-box no-border" v-if="status.status == 'Inactive'" >
<p>{{ __("Recorder is Inactive") }}</p>
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="start()">{{ __("Start Recording") }}</button></p>
<p>{{ __("Recorder is Inactive.") }}</p>
<p>{{ __("Start recording or drag & drop a previously exported data file to view it.") }}</p>
</div>
<div class="msg-box no-border" v-if="status.status == 'Active'" >
<p>{{ __("No Requests found") }}</p>
<p>{{ __("Go make some noise") }}</p>
</div>
</div>
<div v-if="requests.length != 0" class="list-paging-area">
<div v-else class="list-paging-area">
<div class="row">
<div class="col-xs-6">
<div class="btn-group btn-group-paging">
@ -144,7 +145,7 @@ export default {
frappe.set_route("recorder");
this.clear();
});
this.$root.page.add_menu_item("Export data", () => this.export_data());
},
computed: {
pages: function() {
@ -239,8 +240,36 @@ export default {
});
}
},
route_to_request_detail(id) {
this.$router.push({name: 'request-detail', params: {id}});
route_to_request_detail(request) {
this.$router.push({name: 'request-detail', params: {request, id: request.uuid}});
},
export_data: function() {
if (!this.requests) {
return;
}
frappe.call("frappe.recorder.export_data")
.then((r) => {
const data = r.message;
const filename = `${data[0]['uuid']}..${data[data.length -1]['uuid']}.json`
const el = document.createElement('a');
el.setAttribute('href', 'data:application/json,' + encodeURIComponent(JSON.stringify(data)));
el.setAttribute('download', filename);
el.click();
});
},
import_data: function(e) {
if (this.requests.length > 0) {
// don't replace existing capture
return;
}
const request_file = e.dataTransfer.files[0];
const file_reader = new FileReader();
file_reader.readAsText(request_file, 'UTF-8');
file_reader.onload = ({target: {result}}) => {
this.requests = JSON.parse(result);
}
}
}
};

View file

@ -283,14 +283,21 @@ export default {
label: __('Recorder'),
route: '/app/recorder'
});
frappe.call({
method: "frappe.recorder.get",
args: {
uuid: this.$route.params.id
}
}).then( r => {
this.request = r.message
});
}
const request = this.$route.params.request;
if (request.headers || request.form_dict || request.calls) {
// complete request data passed as parameter.
this.request = request;
} else {
frappe.call({
method: "frappe.recorder.get",
args: {
uuid: request.uuid
}
}).then( r => {
this.request = r.message
});
}
},
};
</script>

View file

@ -854,6 +854,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
};
if (this.raw_data.add_total_row) {
this.$page.find('.layout-main-section').css('--report-total-height', '310px');
}
if (this.report_settings.get_datatable_options) {
datatable_options = this.report_settings.get_datatable_options(datatable_options);
}

View file

@ -1401,7 +1401,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
];
if (this.total_count > args.page_length) {
if (this.total_count > this.count_without_children || args.page_length) {
fields.push({
fieldtype: 'Check',
fieldname: 'export_all_rows',

View file

@ -115,7 +115,7 @@
margin-bottom: var(--margin-sm);
font-weight: var(--text-bold);
}
.row:first-child {
.form-documents:first-of-type .row:first-child {
.form-link-title {
margin-top: 0;
}

View file

@ -84,8 +84,9 @@
margin-bottom: 10px;
}
.layout-main-section .frappe-card {
.layout-main-section {
--report-filter-height: 0px;
--report-total-height: 275px;
}
.report-wrapper {
@ -95,7 +96,7 @@
height: calc(100vh - var(--report-filter-height) - 205px);
.dt-scrollable {
height: calc(100vh - var(--report-filter-height) - 275px);
height: calc(100vh - var(--report-filter-height) - var(--report-total-height));
}
}
}

View file

@ -181,6 +181,13 @@ def get(uuid=None, *args, **kwargs):
return result
@frappe.whitelist()
@do_not_record
@administrator_only
def export_data(*args, **kwargs):
return list(frappe.cache().hgetall(RECORDER_REQUEST_HASH).values())
@frappe.whitelist()
@do_not_record
@administrator_only

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>Artboard</title>
<desc>Created with Sketch.</desc>
<g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="frappe" transform="translate(3.000000, 1.000000)" fill="#0089FF" fill-rule="nonzero">
<polygon id="Path" points="9.360932 0 0 0 0 2.46232 9.360932 2.46232"></polygon>
<polygon id="Path" points="0 6.281996 0 14 2.98788 14 2.98788 8.74846 8.740172 8.74846 8.740172 6.281996"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 751 B

View file

@ -227,3 +227,28 @@ class TestDocument(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Currency", d.name), d.name)
frappe.delete_doc_if_exists("Currency", "Frappe Coin", 1)
def test_get_formatted(self):
frappe.get_doc({
'doctype': 'DocType',
'name': 'Test Formatted',
'module': 'Custom',
'custom': 1,
'fields': [
{'label': 'Currency', 'fieldname': 'currency', 'reqd': 1, 'fieldtype': 'Currency'},
]
}).insert()
frappe.delete_doc_if_exists("Currency", "INR", 1)
d = frappe.get_doc({
'doctype': 'Currency',
'currency_name': 'INR',
'symbol': '',
}).insert()
d = frappe.get_doc({
'doctype': 'Test Formatted',
'currency': 100000
})
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')

View file

@ -92,6 +92,9 @@ class TestFmtMoney(unittest.TestCase):
self.assertEqual(fmt_money(1000.456), "1.000,456")
frappe.db.set_default("currency_precision", "")
def test_custom_fmt_money_format(self):
self.assertEqual(fmt_money(100000, format="#,###.##"), '100,000.00')
if __name__=="__main__":
frappe.connect()
unittest.main()

View file

@ -17,9 +17,9 @@ class TestFormatter(unittest.TestCase):
frappe.db.set_default("currency", 'INR')
# if currency field is not passed then default currency should be used.
self.assertEqual(format(100, df, doc), '₹ 100.00')
self.assertEqual(format(100000, df, doc, format="#,###.##"), '₹ 100,000.00')
doc.currency = 'USD'
self.assertEqual(format(100, df, doc), "$ 100.00")
self.assertEqual(format(100000, df, doc, format="#,###.##"), "$ 100,000.00")
frappe.db.set_default("currency", None)

View file

@ -3,8 +3,7 @@
import unittest
import frappe
from frappe.desk.search import search_link
from frappe.desk.search import search_widget
from frappe.desk.search import search_link, search_widget, get_names_for_mentions
class TestSearch(unittest.TestCase):
@ -47,6 +46,23 @@ class TestSearch(unittest.TestCase):
search_link, 'DocType', 'Customer', query=None, filters=None,
page_length=20, searchfield=';')
def test_only_enabled_in_mention(self):
email = 'test_disabled_user_in_mentions@example.com'
frappe.delete_doc('User', email)
if not frappe.db.exists('User', email):
user = frappe.new_doc('User')
user.update({
'email' : email,
'first_name' : email.split("@")[0],
'enabled' : False,
'allowed_in_mentions' : True,
})
# saved when roles are added
user.add_roles('System Manager',)
names_for_mention = [user.get('id') for user in get_names_for_mentions('')]
self.assertNotIn(email, names_for_mention)
def test_link_field_order(self):
# Making a request to the search_link with the tree doctype
search_link(doctype=self.tree_doctype_name, txt='all', query=None,

View file

@ -61,6 +61,18 @@ def create_todo_records():
"description": "this is fourth todo"
}).insert()
@frappe.whitelist()
def create_communication_records():
if frappe.db.get_all('Communication', {'subject': 'Test Form Communication 1'}):
return
frappe.get_doc({
"doctype": "Communication",
"recipients": "test@gmail.com",
"subject": "Test Form Communication 1",
"communication_date": frappe.utils.now_datetime(),
}).insert()
@frappe.whitelist()
def setup_workflow():
from frappe.workflow.doctype.workflow.test_workflow import create_todo_workflow

View file

@ -7,7 +7,7 @@ from frappe.utils import formatdate, fmt_money, flt, cstr, cint, format_datetime
from frappe.model.meta import get_field_currency, get_field_precision
import re
def format_value(value, df=None, doc=None, currency=None, translated=False):
def format_value(value, df=None, doc=None, currency=None, translated=False, format=None):
'''Format value based on given fieldtype, document reference, currency reference.
If docfield info (df) is not given, it will try and guess based on the datatype of the value'''
if isinstance(df, str):
@ -56,7 +56,7 @@ def format_value(value, df=None, doc=None, currency=None, translated=False):
elif df.get("fieldtype") == "Currency":
default_currency = frappe.db.get_default("currency")
currency = currency or get_field_currency(df, doc) or default_currency
return fmt_money(value, precision=get_field_precision(df, doc), currency=currency)
return fmt_money(value, precision=get_field_precision(df, doc), currency=currency, format=format)
elif df.get("fieldtype") == "Float":
precision = get_field_precision(df, doc)