Merge branch 'develop' into google_drive_picker

This commit is contained in:
Raffael Meyer 2021-06-03 15:01:50 +02:00 committed by GitHub
commit 7f7e8ac36a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
1240 changed files with 10891 additions and 122739 deletions

View file

@ -80,6 +80,7 @@
"validate_email": true,
"validate_name": true,
"validate_phone": true,
"validate_url": true,
"get_number_format": true,
"format_number": true,
"format_currency": true,
@ -135,7 +136,6 @@
"PhotoSwipeUI_Default": true,
"fluxify": true,
"io": true,
"QUnit": true,
"JsBarcode": true,
"L": true,
"Chart": true,
@ -149,6 +149,7 @@
"before": true,
"beforeEach": true,
"qz": true,
"localforage": true
"localforage": true,
"extend_cscript": true
}
}

View file

@ -29,4 +29,5 @@ ignore =
B950,
W191,
max-line-length = 200
max-line-length = 200
exclude=.github/helper/semgrep_rules

12
.git-blame-ignore-revs Normal file
View file

@ -0,0 +1,12 @@
# Since version 2.23 (released in August 2019), git-blame has a feature
# to ignore or bypass certain commits.
#
# This file contains a list of commits that are not likely what you
# are looking for in a blame, such as mass reformatting or renaming.
# You can set this file as a default ignore file for blame by running
# the following command.
#
# $ git config blame.ignoreRevsFile .git-blame-ignore-revs
# Replace use of Class.extend with native JS class
fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85

View file

@ -18,7 +18,7 @@ def is_js(file):
return file.endswith("js")
def is_docs(file):
regex = re.compile('\.(md|png|jpg|jpeg)$|^.github|LICENSE')
regex = re.compile(r'\.(md|png|jpg|jpeg)$|^.github|LICENSE')
return bool(regex.search(file))

View file

@ -4,25 +4,61 @@ from frappe import _, flt
from frappe.model.document import Document
# ruleid: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
# ruleid: frappe-modifying-after-submit
self.status = 'Submitted'
def on_submit(self): # noqa
if flt(self.per_billed) < 100:
self.update_billing_status()
else:
# todook: frappe-modifying-after-submit
self.status = "Completed"
self.db_set("status", "Completed")
class TestDoc(Document):
pass
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
self.status = 'Submitted'
self.db_set('status', 'Submitted')
def validate(self):
#ruleid: frappe-modifying-child-tables-while-iterating
for item in self.child_table:
if item.value < 0:
self.remove(item)
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
x = "y"
self.status = x
self.db_set('status', x)
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
x = "y"
self.status = x
self.save()
# ruleid: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
def tainted_method(self):
self.status = "uptate"
# ok: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
def tainted_method(self):
self.status = "update"
self.db_set("status", "update")
# ok: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
self.save()
def tainted_method(self):
self.status = "uptate"

View file

@ -35,3 +35,10 @@ __('You have' + 'subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers' +
'in your mailing list', [subscribers.length])
// ok: frappe-translation-js-splitting
__("Ctrl+Enter to add comment")
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers \
in your mailing list', [subscribers.length])

View file

@ -51,3 +51,11 @@ _(f"what" + f"this is also not cool")
_("")
# ruleid: frappe-translation-empty-string
_('')
class Test:
# ok: frappe-translation-python-splitting
def __init__(
args
):
pass

View file

@ -42,10 +42,10 @@ rules:
- id: frappe-translation-python-splitting
pattern-either:
- pattern: _(...) + ... + _(...)
- pattern: _(...) + _(...)
- pattern: _("..." + "...")
- pattern-regex: '_\([^\)]*\\\s*' # lines broken by `\`
- pattern-regex: '_\(\s*\n' # line breaks allowed by python for using ( )
- pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
- pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
@ -54,8 +54,8 @@ rules:
- id: frappe-translation-js-splitting
pattern-either:
- pattern-regex: '__\([^\)]*[\+\\]\s*'
- pattern: __('...' + '...')
- pattern-regex: '__\([^\)]*[\\]\s+'
- pattern: __('...' + '...', ...)
- pattern: __('...') + __('...')
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.

9
.github/helper/semgrep_rules/ux.js vendored Normal file
View file

@ -0,0 +1,9 @@
// ok: frappe-missing-translate-function-js
frappe.msgprint('{{ _("Both login and password required") }}');
// ruleid: frappe-missing-translate-function-js
frappe.msgprint('What');
// ok: frappe-missing-translate-function-js
frappe.throw(' {{ _("Both login and password required") }}. ');

View file

@ -2,30 +2,30 @@ import frappe
from frappe import msgprint, throw, _
# ruleid: frappe-missing-translate-function
# ruleid: frappe-missing-translate-function-python
throw("Error Occured")
# ruleid: frappe-missing-translate-function
# ruleid: frappe-missing-translate-function-python
frappe.throw("Error Occured")
# ruleid: frappe-missing-translate-function
# ruleid: frappe-missing-translate-function-python
frappe.msgprint("Useful message")
# ruleid: frappe-missing-translate-function
# ruleid: frappe-missing-translate-function-python
msgprint("Useful message")
# ok: frappe-missing-translate-function
# ok: frappe-missing-translate-function-python
translatedmessage = _("Hello")
# ok: frappe-missing-translate-function
# ok: frappe-missing-translate-function-python
throw(translatedmessage)
# ok: frappe-missing-translate-function
# ok: frappe-missing-translate-function-python
msgprint(translatedmessage)
# ok: frappe-missing-translate-function
# ok: frappe-missing-translate-function-python
msgprint(_("Helpful message"))
# ok: frappe-missing-translate-function
# ok: frappe-missing-translate-function-python
frappe.throw(_("Error occured"))

View file

@ -1,15 +1,30 @@
rules:
- id: frappe-missing-translate-function
- id: frappe-missing-translate-function-python
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(_("..."), ...)
- pattern-not: frappe.msgprint(__("..."), ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(_("..."), ...)
- pattern-not: frappe.throw(__("..."), ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [python, javascript, json]
languages: [python]
severity: ERROR
- id: frappe-missing-translate-function-js
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(__("..."), ...)
# ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
- pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(__("..."), ...)
# ignore microtemplating
- pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [javascript]
severity: ERROR

View file

@ -15,11 +15,11 @@ jobs:
path: 'frappe'
- uses: actions/setup-node@v1
with:
python-version: '12.x'
node-version: 14
- uses: actions/setup-python@v2
with:
python-version: '3.6'
- name: Set up bench for current push
- name: Set up bench and build assets
run: |
npm install -g yarn
pip3 install -U frappe-bench
@ -29,7 +29,7 @@ jobs:
- name: Package assets
run: |
mkdir -p $GITHUB_WORKSPACE/build
tar -cvpzf $GITHUB_WORKSPACE/build/$GITHUB_SHA.tar.gz ./frappe-bench/sites/assets/js ./frappe-bench/sites/assets/css
tar -cvpzf $GITHUB_WORKSPACE/build/$GITHUB_SHA.tar.gz ./frappe-bench/sites/assets/frappe/dist
- name: Publish assets to S3
uses: jakejarvis/s3-sync-action@master

View file

@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: '3.6'
- name: Set up bench for current push
- name: Set up bench and build assets
run: |
npm install -g yarn
pip3 install -U frappe-bench
@ -32,7 +32,7 @@ jobs:
- name: Package assets
run: |
mkdir -p $GITHUB_WORKSPACE/build
tar -cvpzf $GITHUB_WORKSPACE/build/assets.tar.gz ./frappe-bench/sites/assets/js ./frappe-bench/sites/assets/css
tar -cvpzf $GITHUB_WORKSPACE/build/assets.tar.gz ./frappe-bench/sites/assets/frappe/dist
- name: Get release
id: get_release

View file

@ -4,6 +4,8 @@ on:
pull_request:
branches:
- develop
- version-13-hotfix
- version-13-pre-release
jobs:
semgrep:
name: Frappe Linter

View file

@ -0,0 +1,131 @@
name: Server
on:
pull_request:
workflow_dispatch:
push:
branches: [ develop ]
jobs:
test:
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
container: [1, 2]
name: Python Unit Tests (MariaDB)
services:
mysql:
image: mariadb:10.3
env:
MYSQL_ALLOW_EMPTY_PASSWORD: YES
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.7
- uses: actions/setup-node@v2
with:
node-version: 14
check-latest: true
- name: Add to Hosts
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
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Dependencies
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
TYPE: server
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: server
- name: Run Tests
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
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true
coveralls:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
steps:
- name: Clone
uses: actions/checkout@v2
- name: Coveralls Finished
run: |
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -0,0 +1,100 @@
name: Server
on:
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
container: [1, 2]
name: Python Unit Tests (Postgres)
services:
postgres:
image: postgres:12.4
env:
POSTGRES_PASSWORD: travis
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Clone
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.7
- uses: actions/setup-node@v2
with:
node-version: '14'
check-latest: true
- name: Add to Hosts
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
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Dependencies
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
TYPE: server
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: postgres
TYPE: server
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator
env:
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io

View file

@ -1,10 +1,10 @@
name: CI
name: UI
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
workflow_dispatch:
push:
branches: [ develop ]
jobs:
test:
@ -13,23 +13,9 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- DB: "mariadb"
TYPE: "server"
JOB_NAME: "Python MariaDB"
RUN_COMMAND: bench --site test_site run-tests --coverage
containers: [1, 2]
- DB: "postgres"
TYPE: "server"
JOB_NAME: "Python PostgreSQL"
RUN_COMMAND: bench --site test_site run-tests --coverage
- DB: "mariadb"
TYPE: "ui"
JOB_NAME: "UI MariaDB"
RUN_COMMAND: bench --site test_site run-ui-tests frappe --headless
name: ${{ matrix.JOB_NAME }}
name: UI Tests (Cypress)
services:
mysql:
@ -40,18 +26,6 @@ jobs:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
postgres:
image: postgres:12.4
env:
POSTGRES_PASSWORD: travis
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Clone
uses: actions/checkout@v2
@ -63,7 +37,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: '12'
node-version: 14
check-latest: true
- name: Add to Hosts
@ -105,7 +79,6 @@ jobs:
${{ runner.os }}-yarn-
- name: Cache cypress binary
if: matrix.TYPE == 'ui'
uses: actions/cache@v2
with:
path: ~/.cache
@ -119,40 +92,16 @@ jobs:
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
TYPE: ${{ matrix.TYPE }}
TYPE: ui
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: ${{ matrix.DB }}
TYPE: ${{ matrix.TYPE }}
DB: mariadb
TYPE: ui
- name: Run Set-Up
if: matrix.TYPE == 'ui'
- name: Site Setup
run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard
env:
DB: ${{ matrix.DB }}
TYPE: ${{ matrix.TYPE }}
- name: Setup tmate session
if: contains(github.event.pull_request.labels.*.name, 'debug-gha')
uses: mxschmitt/action-tmate@v3
- name: Run Tests
run: cd ~/frappe-bench/ && ${{ matrix.RUN_COMMAND }}
env:
DB: ${{ matrix.DB }}
TYPE: ${{ matrix.TYPE }}
- name: Coverage
if: matrix.TYPE == 'server'
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip install coveralls==2.2.0
pip install coverage==4.5.4
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_SERVICE_NAME: github
- name: UI Tests
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID

1
.gitignore vendored
View file

@ -9,6 +9,7 @@ locale
dist/
# build/
frappe/docs/current
frappe/public/dist
.vscode
node_modules
.kdev4/

View file

@ -3,9 +3,12 @@ pull_request_rules:
conditions:
- status-success=Sider
- status-success=Semantic Pull Request
- status-success=Python MariaDB
- status-success=Python PostgreSQL
- status-success=UI MariaDB
- status-success=Python Unit Tests (MariaDB) (1)
- status-success=Python Unit Tests (MariaDB) (2)
- status-success=Python Unit Tests (Postgres) (1)
- status-success=Python Unit Tests (Postgres) (2)
- status-success=UI Tests (Cypress) (1)
- status-success=UI Tests (Cypress) (2)
- status-success=security/snyk (frappe)
- label!=dont-merge
- label!=squash
@ -16,9 +19,12 @@ pull_request_rules:
- name: Automatic squash on CI success and review
conditions:
- status-success=Sider
- status-success=Python MariaDB
- status-success=Python PostgreSQL
- status-success=UI MariaDB
- status-success=Python Unit Tests (MariaDB) (1)
- status-success=Python Unit Tests (MariaDB) (2)
- status-success=Python Unit Tests (Postgres) (1)
- status-success=Python Unit Tests (Postgres) (2)
- status-success=UI Tests (Cypress) (1)
- status-success=UI Tests (Cypress) (2)
- status-success=security/snyk (frappe)
- label!=dont-merge
- label=squash

View file

@ -14,18 +14,21 @@
</div>
<div align="center">
<a href="https://github.com/frappe/frappe/actions/workflows/ci-tests.yml">
<img src="https://github.com/frappe/frappe/actions/workflows/ci-tests.yml/badge.svg?branch=develop">
</a>
<a href='https://frappeframework.com/docs'>
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/>
</a>
<a href="https://github.com/frappe/frappe/actions/workflows/server-mariadb-tests.yml">
<img src="https://github.com/frappe/frappe/actions/workflows/server-mariadb-tests.yml/badge.svg">
</a>
<a href="https://github.com/frappe/frappe/actions/workflows/ui-tests.yml">
<img src="https://github.com/frappe/frappe/actions/workflows/ui-tests.yml/badge.svg?branch=develop">
</a>
<a href='https://frappeframework.com/docs'>
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/>
</a>
<a href='https://www.codetriage.com/frappe/frappe'>
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
</a>
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'>
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'>
</a>
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'>
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'>
</a>
</div>

View file

@ -0,0 +1,65 @@
export default {
name: 'Validation Test',
custom: 1,
actions: [],
creation: '2019-03-15 06:29:07.215072',
doctype: 'DocType',
editable_grid: 1,
engine: 'InnoDB',
fields: [
{
fieldname: 'email',
fieldtype: 'Data',
label: 'Email',
options: 'Email'
},
{
fieldname: 'URL',
fieldtype: 'Data',
label: 'URL',
options: 'URL'
},
{
fieldname: 'Phone',
fieldtype: 'Data',
label: 'Phone',
options: 'Phone'
},
{
fieldname: 'person_name',
fieldtype: 'Data',
label: 'Person Name',
options: 'Name'
},
{
fieldname: 'read_only_url',
fieldtype: 'Data',
label: 'Read Only URL',
options: 'URL',
read_only: '1',
default: 'https://frappe.io'
}
],
issingle: 1,
links: [],
modified: '2021-04-19 14:40:53.127615',
modified_by: 'Administrator',
module: 'Custom',
owner: 'Administrator',
permissions: [
{
create: 1,
delete: 1,
email: 1,
print: 1,
read: 1,
role: 'System Manager',
share: 1,
write: 1
}
],
quick_entry: 1,
sort_field: 'modified',
sort_order: 'ASC',
track_changes: 1
};

View file

@ -0,0 +1,43 @@
import data_field_validation_doctype from '../fixtures/data_field_validation_doctype';
const doctype_name = data_field_validation_doctype.name;
context('Data Field Input Validation in New Form', () => {
before(() => {
cy.login();
cy.visit('/app/website');
return cy.insert_doc('DocType', data_field_validation_doctype, true);
});
function validateField(fieldname, invalid_value, valid_value) {
// Invalid, should have has-error class
cy.get_field(fieldname).clear().type(invalid_value).blur();
cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('have.class', 'has-error');
// Valid value, should not have has-error class
cy.get_field(fieldname).clear().type(valid_value);
cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('not.have.class', 'has-error');
}
describe('Data Field Options', () => {
it('should validate email address', () => {
cy.new_form(doctype_name);
validateField('email', 'captian', 'hello@test.com');
});
it('should validate URL', () => {
validateField('url', 'jkl', 'https://frappe.io');
validateField('url', 'abcd.com', 'http://google.com/home');
validateField('url', '&&http://google.uae', 'gopher://frappe.io');
validateField('url', 'ftt2:://google.in?q=news', 'ftps2://frappe.io/__/#home');
validateField('url', 'ftt2://', 'ntps://localhost'); // For intranet URLs
});
it('should validate phone number', () => {
validateField('phone', 'america', '89787878');
});
it('should validate name', () => {
validateField('person_name', ' 777Hello', 'James Bond');
});
});
});

View file

@ -50,7 +50,7 @@ context('Recorder', () => {
cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
});
it.only('Recorder View Request', () => {
it('Recorder View Request', () => {
cy.get('.primary-action').should('contain', 'Start').click();
cy.visit('/app/List/DocType/List');

View file

@ -0,0 +1,43 @@
import data_field_validation_doctype from '../fixtures/data_field_validation_doctype';
const doctype_name = data_field_validation_doctype.name;
context('URL Data Field Input', () => {
before(() => {
cy.login();
cy.visit('/app/website');
return cy.insert_doc('DocType', data_field_validation_doctype, true);
});
describe('URL Data Field Input ', () => {
it('should not show URL link button without focus', () => {
cy.new_form(doctype_name);
cy.get_field('url').clear().type('https://frappe.io');
cy.get_field('url').blur().wait(500);
cy.get('.link-btn').should('not.be.visible');
});
it('should show URL link button on focus', () => {
cy.get_field('url').focus().wait(500);
cy.get('.link-btn').should('be.visible');
});
it('should not show URL link button for invalid URL', () => {
cy.get_field('url').clear().type('fuzzbuzz');
cy.get('.link-btn').should('not.be.visible');
});
it('should have valid URL link with target _blank', () => {
cy.get_field('url').clear().type('https://frappe.io');
cy.get('.link-btn .btn-open').should('have.attr', 'href', 'https://frappe.io');
cy.get('.link-btn .btn-open').should('have.attr', 'target', '_blank');
});
it('should inject anchor tag in read-only URL data field', () => {
cy.get('[data-fieldname="read_only_url"]')
.find('a')
.should('have.attr', 'target', '_blank');
});
});
});

481
esbuild/esbuild.js Normal file
View file

@ -0,0 +1,481 @@
/* eslint-disable no-console */
let path = require("path");
let fs = require("fs");
let glob = require("fast-glob");
let esbuild = require("esbuild");
let vue = require("esbuild-vue");
let yargs = require("yargs");
let cliui = require("cliui")();
let chalk = require("chalk");
let html_plugin = require("./frappe-html");
let postCssPlugin = require("esbuild-plugin-postcss2").default;
let ignore_assets = require("./ignore-assets");
let sass_options = require("./sass_options");
let {
app_list,
assets_path,
apps_path,
sites_path,
get_app_path,
get_public_path,
log,
log_warn,
log_error,
bench_path,
get_redis_subscriber
} = require("./utils");
let argv = yargs
.usage("Usage: node esbuild [options]")
.option("apps", {
type: "string",
description: "Run build for specific apps"
})
.option("skip_frappe", {
type: "boolean",
description: "Skip building frappe assets"
})
.option("files", {
type: "string",
description: "Run build for specified bundles"
})
.option("watch", {
type: "boolean",
description: "Run in watch mode and rebuild on file changes"
})
.option("production", {
type: "boolean",
description: "Run build in production mode"
})
.option("run-build-command", {
type: "boolean",
description: "Run build command for apps"
})
.example(
"node esbuild --apps frappe,erpnext",
"Run build only for frappe and erpnext"
)
.example(
"node esbuild --files frappe/website.bundle.js,frappe/desk.bundle.js",
"Run build only for specified bundles"
)
.version(false).argv;
const APPS = (!argv.apps ? app_list : argv.apps.split(",")).filter(
app => !(argv.skip_frappe && app == "frappe")
);
const FILES_TO_BUILD = argv.files ? argv.files.split(",") : [];
const WATCH_MODE = Boolean(argv.watch);
const PRODUCTION = Boolean(argv.production);
const RUN_BUILD_COMMAND = !WATCH_MODE && Boolean(argv["run-build-command"]);
const TOTAL_BUILD_TIME = `${chalk.black.bgGreen(" DONE ")} Total Build Time`;
const NODE_PATHS = [].concat(
// node_modules of apps directly importable
app_list
.map(app => path.resolve(get_app_path(app), "../node_modules"))
.filter(fs.existsSync),
// import js file of any app if you provide the full path
app_list
.map(app => path.resolve(get_app_path(app), ".."))
.filter(fs.existsSync)
);
execute()
.then(() => RUN_BUILD_COMMAND && run_build_command_for_apps(APPS))
.catch(e => console.error(e));
if (WATCH_MODE) {
// listen for open files in editor event
open_in_editor();
}
async function execute() {
console.time(TOTAL_BUILD_TIME);
if (!FILES_TO_BUILD.length) {
await clean_dist_folders(APPS);
}
let result;
try {
result = await build_assets_for_apps(APPS, FILES_TO_BUILD);
} catch (e) {
log_error("There were some problems during build");
log();
log(chalk.dim(e.stack));
return;
}
if (!WATCH_MODE) {
log_built_assets(result.metafile);
console.timeEnd(TOTAL_BUILD_TIME);
log();
} else {
log("Watching for changes...");
}
return await write_assets_json(result.metafile);
}
function build_assets_for_apps(apps, files) {
let { include_patterns, ignore_patterns } = files.length
? get_files_to_build(files)
: get_all_files_to_build(apps);
return glob(include_patterns, { ignore: ignore_patterns }).then(files => {
let output_path = assets_path;
let file_map = {};
for (let file of files) {
let relative_app_path = path.relative(apps_path, file);
let app = relative_app_path.split(path.sep)[0];
let extension = path.extname(file);
let output_name = path.basename(file, extension);
if (
[".css", ".scss", ".less", ".sass", ".styl"].includes(extension)
) {
output_name = path.join("css", output_name);
} else if ([".js", ".ts"].includes(extension)) {
output_name = path.join("js", output_name);
}
output_name = path.join(app, "dist", output_name);
if (Object.keys(file_map).includes(output_name)) {
log_warn(
`Duplicate output file ${output_name} generated from ${file}`
);
}
file_map[output_name] = file;
}
return build_files({
files: file_map,
outdir: output_path
});
});
}
function get_all_files_to_build(apps) {
let include_patterns = [];
let ignore_patterns = [];
for (let app of apps) {
let public_path = get_public_path(app);
include_patterns.push(
path.resolve(
public_path,
"**",
"*.bundle.{js,ts,css,sass,scss,less,styl}"
)
);
ignore_patterns.push(
path.resolve(public_path, "node_modules"),
path.resolve(public_path, "dist")
);
}
return {
include_patterns,
ignore_patterns
};
}
function get_files_to_build(files) {
// files: ['frappe/website.bundle.js', 'erpnext/main.bundle.js']
let include_patterns = [];
let ignore_patterns = [];
for (let file of files) {
let [app, bundle] = file.split("/");
let public_path = get_public_path(app);
include_patterns.push(path.resolve(public_path, "**", bundle));
ignore_patterns.push(
path.resolve(public_path, "node_modules"),
path.resolve(public_path, "dist")
);
}
return {
include_patterns,
ignore_patterns
};
}
function build_files({ files, outdir }) {
return esbuild.build({
entryPoints: files,
entryNames: "[dir]/[name].[hash]",
outdir,
sourcemap: true,
bundle: true,
metafile: true,
minify: PRODUCTION,
nodePaths: NODE_PATHS,
define: {
"process.env.NODE_ENV": JSON.stringify(
PRODUCTION ? "production" : "development"
)
},
plugins: [
html_plugin,
ignore_assets,
vue(),
postCssPlugin({
plugins: [require("autoprefixer")],
sassOptions: sass_options
})
],
watch: get_watch_config()
});
}
function get_watch_config() {
if (WATCH_MODE) {
return {
async onRebuild(error, result) {
if (error) {
log_error("There was an error during rebuilding changes.");
log();
log(chalk.dim(error.stack));
notify_redis({ error });
} else {
let {
assets_json,
prev_assets_json
} = await write_assets_json(result.metafile);
if (prev_assets_json) {
log_rebuilt_assets(prev_assets_json, assets_json);
}
notify_redis({ success: true });
}
}
};
}
return null;
}
async function clean_dist_folders(apps) {
for (let app of apps) {
let public_path = get_public_path(app);
await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
recursive: true
});
await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
recursive: true
});
}
}
function log_built_assets(metafile) {
let column_widths = [60, 20];
cliui.div(
{
text: chalk.cyan.bold("File"),
width: column_widths[0]
},
{
text: chalk.cyan.bold("Size"),
width: column_widths[1]
}
);
cliui.div("");
let output_by_dist_path = {};
for (let outfile in metafile.outputs) {
if (outfile.endsWith(".map")) continue;
let data = metafile.outputs[outfile];
outfile = path.resolve(outfile);
outfile = path.relative(assets_path, outfile);
let filename = path.basename(outfile);
let dist_path = outfile.replace(filename, "");
output_by_dist_path[dist_path] = output_by_dist_path[dist_path] || [];
output_by_dist_path[dist_path].push({
name: filename,
size: (data.bytes / 1000).toFixed(2) + " Kb"
});
}
for (let dist_path in output_by_dist_path) {
let files = output_by_dist_path[dist_path];
cliui.div({
text: dist_path,
width: column_widths[0]
});
for (let i in files) {
let file = files[i];
let branch = "";
if (i < files.length - 1) {
branch = "├─ ";
} else {
branch = "└─ ";
}
let color = file.name.endsWith(".js") ? "green" : "blue";
cliui.div(
{
text: branch + chalk[color]("" + file.name),
width: column_widths[0]
},
{
text: file.size,
width: column_widths[1]
}
);
}
cliui.div("");
}
log(cliui.toString());
}
// to store previous build's assets.json for comparison
let prev_assets_json;
let curr_assets_json;
async function write_assets_json(metafile) {
prev_assets_json = curr_assets_json;
let out = {};
for (let output in metafile.outputs) {
let info = metafile.outputs[output];
let asset_path = "/" + path.relative(sites_path, output);
if (info.entryPoint) {
out[path.basename(info.entryPoint)] = asset_path;
}
}
let assets_json_path = path.resolve(assets_path, "assets.json");
let assets_json;
try {
assets_json = await fs.promises.readFile(assets_json_path, "utf-8");
} catch (error) {
assets_json = "{}";
}
assets_json = JSON.parse(assets_json);
// update with new values
assets_json = Object.assign({}, assets_json, out);
curr_assets_json = assets_json;
await fs.promises.writeFile(
assets_json_path,
JSON.stringify(assets_json, null, 4)
);
await update_assets_json_in_cache(assets_json);
return {
assets_json,
prev_assets_json
};
}
function update_assets_json_in_cache(assets_json) {
// update assets_json cache in redis, so that it can be read directly by python
return new Promise(resolve => {
let client = get_redis_subscriber("redis_cache");
// handle error event to avoid printing stack traces
client.on("error", _ => {
log_warn("Cannot connect to redis_cache to update assets_json");
});
client.set("assets_json", JSON.stringify(assets_json), err => {
client.unref();
resolve();
});
});
}
function run_build_command_for_apps(apps) {
let cwd = process.cwd();
let { execSync } = require("child_process");
for (let app of apps) {
if (app === "frappe") continue;
let root_app_path = path.resolve(get_app_path(app), "..");
let package_json = path.resolve(root_app_path, "package.json");
if (fs.existsSync(package_json)) {
let { scripts } = require(package_json);
if (scripts && scripts.build) {
log("\nRunning build command for", chalk.bold(app));
process.chdir(root_app_path);
execSync("yarn build", { encoding: "utf8", stdio: "inherit" });
}
}
}
process.chdir(cwd);
}
async function notify_redis({ error, success }) {
// notify redis which in turns tells socketio to publish this to browser
let subscriber = get_redis_subscriber("redis_socketio");
subscriber.on("error", _ => {
log_warn("Cannot connect to redis_socketio for browser events");
});
let payload = null;
if (error) {
let formatted = await esbuild.formatMessages(error.errors, {
kind: "error",
terminalWidth: 100
});
let stack = error.stack.replace(new RegExp(bench_path, "g"), "");
payload = {
error,
formatted,
stack
};
}
if (success) {
payload = {
success: true
};
}
subscriber.publish(
"events",
JSON.stringify({
event: "build_event",
message: payload
})
);
}
function open_in_editor() {
let subscriber = get_redis_subscriber("redis_socketio");
subscriber.on("error", _ => {
log_warn("Cannot connect to redis_socketio for open_in_editor events");
});
subscriber.on("message", (event, file) => {
if (event === "open_in_editor") {
file = JSON.parse(file);
let file_path = path.resolve(file.file);
log("Opening file in editor:", file_path);
let launch = require("launch-editor");
launch(`${file_path}:${file.line}:${file.column}`);
}
});
subscriber.subscribe("open_in_editor");
}
function log_rebuilt_assets(prev_assets, new_assets) {
let added_files = [];
let old_files = Object.values(prev_assets);
let new_files = Object.values(new_assets);
for (let filepath of new_files) {
if (!old_files.includes(filepath)) {
added_files.push(filepath);
}
}
log(
chalk.yellow(
`${new Date().toLocaleTimeString()}: Compiled ${
added_files.length
} files...`
)
);
for (let filepath of added_files) {
let filename = path.basename(filepath);
log(" " + filename);
}
log();
}

43
esbuild/frappe-html.js Normal file
View file

@ -0,0 +1,43 @@
module.exports = {
name: "frappe-html",
setup(build) {
let path = require("path");
let fs = require("fs/promises");
build.onResolve({ filter: /\.html$/ }, args => {
return {
path: path.join(args.resolveDir, args.path),
namespace: "frappe-html"
};
});
build.onLoad({ filter: /.*/, namespace: "frappe-html" }, args => {
let filepath = args.path;
let filename = path.basename(filepath).split(".")[0];
return fs
.readFile(filepath, "utf-8")
.then(content => {
content = scrub_html_template(content);
return {
contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`
};
})
.catch(() => {
return {
contents: "",
warnings: [
{
text: `There was an error importing ${filepath}`
}
]
};
});
});
}
};
function scrub_html_template(content) {
content = content.replace(/`/g, "\\`");
return content;
}

11
esbuild/ignore-assets.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = {
name: "frappe-ignore-asset",
setup(build) {
build.onResolve({ filter: /^\/assets\// }, args => {
return {
path: args.path,
external: true
};
});
}
};

1
esbuild/index.js Normal file
View file

@ -0,0 +1 @@
require("./esbuild");

29
esbuild/sass_options.js Normal file
View file

@ -0,0 +1,29 @@
let path = require("path");
let { get_app_path, app_list } = require("./utils");
let node_modules_path = path.resolve(
get_app_path("frappe"),
"..",
"node_modules"
);
let app_paths = app_list
.map(get_app_path)
.map(app_path => path.resolve(app_path, ".."));
module.exports = {
includePaths: [node_modules_path, ...app_paths],
importer: function(url) {
if (url.startsWith("~")) {
// strip ~ so that it can resolve from node_modules
url = url.slice(1);
}
if (url.endsWith(".css")) {
// strip .css from end of path
url = url.slice(0, -4);
}
// normal file, let it go
return {
file: url
};
}
};

145
esbuild/utils.js Normal file
View file

@ -0,0 +1,145 @@
const path = require("path");
const fs = require("fs");
const chalk = require("chalk");
const frappe_path = path.resolve(__dirname, "..");
const bench_path = path.resolve(frappe_path, "..", "..");
const sites_path = path.resolve(bench_path, "sites");
const apps_path = path.resolve(bench_path, "apps");
const assets_path = path.resolve(sites_path, "assets");
const app_list = get_apps_list();
const app_paths = app_list.reduce((out, app) => {
out[app] = path.resolve(apps_path, app, app);
return out;
}, {});
const public_paths = app_list.reduce((out, app) => {
out[app] = path.resolve(app_paths[app], "public");
return out;
}, {});
const public_js_paths = app_list.reduce((out, app) => {
out[app] = path.resolve(app_paths[app], "public/js");
return out;
}, {});
const bundle_map = app_list.reduce((out, app) => {
const public_js_path = public_js_paths[app];
if (fs.existsSync(public_js_path)) {
const all_files = fs.readdirSync(public_js_path);
const js_files = all_files.filter(file => file.endsWith(".js"));
for (let js_file of js_files) {
const filename = path.basename(js_file).split(".")[0];
out[path.join(app, "js", filename)] = path.resolve(
public_js_path,
js_file
);
}
}
return out;
}, {});
const get_public_path = app => public_paths[app];
const get_build_json_path = app =>
path.resolve(get_public_path(app), "build.json");
function get_build_json(app) {
try {
return require(get_build_json_path(app));
} catch (e) {
// build.json does not exist
return null;
}
}
function delete_file(path) {
if (fs.existsSync(path)) {
fs.unlinkSync(path);
}
}
function run_serially(tasks) {
let result = Promise.resolve();
tasks.forEach(task => {
if (task) {
result = result.then ? result.then(task) : Promise.resolve();
}
});
return result;
}
const get_app_path = app => app_paths[app];
function get_apps_list() {
return fs
.readFileSync(path.resolve(sites_path, "apps.txt"), {
encoding: "utf-8"
})
.split("\n")
.filter(Boolean);
}
function get_cli_arg(name) {
let args = process.argv.slice(2);
let arg = `--${name}`;
let index = args.indexOf(arg);
let value = null;
if (index != -1) {
value = true;
}
if (value && args[index + 1]) {
value = args[index + 1];
}
return value;
}
function log_error(message, badge = "ERROR") {
badge = chalk.white.bgRed(` ${badge} `);
console.error(`${badge} ${message}`); // eslint-disable-line no-console
}
function log_warn(message, badge = "WARN") {
badge = chalk.black.bgYellowBright(` ${badge} `);
console.warn(`${badge} ${message}`); // eslint-disable-line no-console
}
function log(...args) {
console.log(...args); // eslint-disable-line no-console
}
function get_redis_subscriber(kind) {
// get redis subscriber that aborts after 10 connection attempts
let { get_redis_subscriber: get_redis } = require("../node_utils");
return get_redis(kind, {
retry_strategy: function(options) {
// abort after 10 connection attempts
if (options.attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 100, 2000);
}
});
}
module.exports = {
app_list,
bench_path,
assets_path,
sites_path,
apps_path,
bundle_map,
get_public_path,
get_build_json_path,
get_build_json,
get_app_path,
delete_file,
run_serially,
get_cli_arg,
log,
log_warn,
log_error,
get_redis_subscriber
};

View file

@ -10,13 +10,17 @@ be used to build database driven apps.
Read the documentation: https://frappeframework.com/docs
"""
from __future__ import unicode_literals, print_function
import os, warnings
_dev_server = os.environ.get('DEV_SERVER', False)
if _dev_server:
warnings.simplefilter('always', DeprecationWarning)
warnings.simplefilter('always', PendingDeprecationWarning)
from six import iteritems, binary_type, text_type, string_types, PY2
from werkzeug.local import Local, release_local
import os, sys, importlib, inspect, json
import sys, importlib, inspect, json
import typing
from past.builtins import cmp
import click
# Local application imports
@ -27,13 +31,6 @@ from .utils.lazy_loader import lazy_import
# Lazy imports
faker = lazy_import('faker')
# Harmless for Python 3
# For Python 2 set default encoding to utf-8
if PY2:
reload(sys)
sys.setdefaultencoding("utf-8")
__version__ = '14.0.0-dev'
__title__ = "Frappe Framework"
@ -97,14 +94,14 @@ def _(msg, lang=None, context=None):
def as_unicode(text, encoding='utf-8'):
'''Convert to unicode if required'''
if isinstance(text, text_type):
if isinstance(text, str):
return text
elif text==None:
return ''
elif isinstance(text, binary_type):
return text_type(text, encoding)
elif isinstance(text, bytes):
return str(text, encoding)
else:
return text_type(text)
return str(text)
def get_lang_dict(fortype, name=None):
"""Returns the translated language dict for the given type and name.
@ -204,7 +201,7 @@ def init(site, sites_path=None, new_site=False):
local.meta_cache = {}
local.form_dict = _dict()
local.session = _dict()
local.dev_server = os.environ.get('DEV_SERVER', False)
local.dev_server = _dev_server
setup_module_map()
@ -530,16 +527,20 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
if not delayed:
now = True
from frappe.email import queue
queue.send(recipients=recipients, sender=sender,
from frappe.email.doctype.email_queue.email_queue import QueueBuilder
builder = QueueBuilder(recipients=recipients, sender=sender,
subject=subject, message=message, text_content=text_content,
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, add_unsubscribe_link=add_unsubscribe_link,
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message,
attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to,
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately,
communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification,
communication=communication, read_receipt=read_receipt, is_notification=is_notification,
inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container)
# build email queue and send the email if send_now is True.
builder.process(send_now=now)
whitelisted = []
guest_methods = []
xss_safe_methods = []
@ -597,7 +598,7 @@ def is_whitelisted(method):
# strictly sanitize form_dict
# escapes html characters like <> except for predefined tags like a, b, ul etc.
for key, value in form_dict.items():
if isinstance(value, string_types):
if isinstance(value, str):
form_dict[key] = sanitize_html(value)
def read_only():
@ -721,7 +722,7 @@ def has_website_permission(doc=None, ptype='read', user=None, verbose=False, doc
user = session.user
if doc:
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = get_doc(doctype, doc)
doctype = doc.doctype
@ -790,7 +791,7 @@ def set_value(doctype, docname, fieldname, value=None):
return frappe.client.set_value(doctype, docname, fieldname, value)
def get_cached_doc(*args, **kwargs):
if args and len(args) > 1 and isinstance(args[1], text_type):
if args and len(args) > 1 and isinstance(args[1], str):
key = get_document_cache_key(args[0], args[1])
# local cache
doc = local.document_cache.get(key)
@ -821,7 +822,7 @@ def clear_document_cache(doctype, name):
def get_cached_value(doctype, name, fieldname, as_dict=False):
doc = get_cached_doc(doctype, name)
if isinstance(fieldname, string_types):
if isinstance(fieldname, str):
if as_dict:
throw('Cannot make dict for single fieldname')
return doc.get(fieldname)
@ -1027,7 +1028,7 @@ def get_doc_hooks():
if not hasattr(local, 'doc_events_hooks'):
hooks = get_hooks('doc_events', {})
out = {}
for key, value in iteritems(hooks):
for key, value in hooks.items():
if isinstance(key, tuple):
for doctype in key:
append_hook(out, doctype, value)
@ -1144,7 +1145,7 @@ def get_file_json(path):
def read_file(path, raise_not_found=False):
"""Open a file and return its content as Unicode."""
if isinstance(path, text_type):
if isinstance(path, str):
path = path.encode("utf-8")
if os.path.exists(path):
@ -1167,7 +1168,7 @@ def get_attr(method_string):
def call(fn, *args, **kwargs):
"""Call a function and match arguments."""
if isinstance(fn, string_types):
if isinstance(fn, str):
fn = get_attr(fn)
newargs = get_newargs(fn, kwargs)
@ -1178,13 +1179,9 @@ def get_newargs(fn, kwargs):
if hasattr(fn, 'fnargs'):
fnargs = fn.fnargs
else:
try:
fnargs, varargs, varkw, defaults = inspect.getargspec(fn)
except ValueError:
fnargs = inspect.getfullargspec(fn).args
varargs = inspect.getfullargspec(fn).varargs
varkw = inspect.getfullargspec(fn).varkw
defaults = inspect.getfullargspec(fn).defaults
fnargs = inspect.getfullargspec(fn).args
fnargs.extend(inspect.getfullargspec(fn).kwonlyargs)
varkw = inspect.getfullargspec(fn).varkw
newargs = {}
for a in kwargs:
@ -1626,6 +1623,12 @@ def enqueue(*args, **kwargs):
import frappe.utils.background_jobs
return frappe.utils.background_jobs.enqueue(*args, **kwargs)
def task(**task_kwargs):
def decorator_task(f):
f.enqueue = lambda **fun_kwargs: enqueue(f, **task_kwargs, **fun_kwargs)
return f
return decorator_task
def enqueue_doc(*args, **kwargs):
'''
Enqueue method to be executed using a background worker
@ -1693,6 +1696,23 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
"round": round
}
UNSAFE_ATTRIBUTES = {
# Generator Attributes
"gi_frame", "gi_code",
# Coroutine Attributes
"cr_frame", "cr_code", "cr_origin",
# Async Generator Attributes
"ag_code", "ag_frame",
# Traceback Attributes
"tb_frame", "tb_next",
# Format Attributes
"format", "format_map",
}
for attribute in UNSAFE_ATTRIBUTES:
if attribute in code:
throw('Illegal rule {0}. Cannot use "{1}"'.format(bold(code), attribute))
if '__' in code:
throw('Illegal rule {0}. Cannot use "__"'.format(bold(code)))

View file

@ -1,6 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import base64
import binascii
import json
@ -11,6 +10,7 @@ import frappe.client
import frappe.handler
from frappe import _
from frappe.utils.response import build_response
from frappe.utils.data import sbool
def handle():
@ -108,25 +108,40 @@ def handle():
elif doctype:
if frappe.local.request.method == "GET":
if frappe.local.form_dict.get('fields'):
frappe.local.form_dict['fields'] = json.loads(frappe.local.form_dict['fields'])
frappe.local.form_dict.setdefault('limit_page_length', 20)
frappe.local.response.update({
"data": frappe.call(
frappe.client.get_list,
doctype,
**frappe.local.form_dict
)
})
# set fields for frappe.get_list
if frappe.local.form_dict.get("fields"):
frappe.local.form_dict["fields"] = json.loads(frappe.local.form_dict["fields"])
# set limit of records for frappe.get_list
frappe.local.form_dict.setdefault(
"limit_page_length",
frappe.local.form_dict.limit or frappe.local.form_dict.limit_page_length or 20,
)
# convert strings to native types - only as_dict and debug accept bool
for param in ["as_dict", "debug"]:
param_val = frappe.local.form_dict.get(param)
if param_val is not None:
frappe.local.form_dict[param] = sbool(param_val)
# evaluate frappe.get_list
data = frappe.call(frappe.client.get_list, doctype, **frappe.local.form_dict)
# set frappe.get_list result to response
frappe.local.response.update({"data": data})
if frappe.local.request.method == "POST":
# fetch data from from dict
data = get_request_form_data()
data.update({
"doctype": doctype
})
frappe.local.response.update({
"data": frappe.get_doc(data).insert().as_dict()
})
data.update({"doctype": doctype})
# insert document from request data
doc = frappe.get_doc(data).insert()
# set response data
frappe.local.response.update({"data": doc.as_dict()})
# commit for POST requests
frappe.db.commit()
else:
raise frappe.DoesNotExistError

View file

@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import os
from six import iteritems
import logging
from werkzeug.local import LocalManager
@ -99,17 +97,7 @@ def application(request):
frappe.monitor.stop(response)
frappe.recorder.dump()
if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger:
frappe.logger("frappe.web", allow_site=frappe.local.site).info({
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),
"scheme": getattr(request, "scheme", "NOTFOUND"),
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
log_request(request, response)
process_response(response)
frappe.destroy()
@ -137,6 +125,19 @@ def init_request(request):
if request.method != "OPTIONS":
frappe.local.http_request = frappe.auth.HTTPRequest()
def log_request(request, response):
if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger:
frappe.logger("frappe.web", allow_site=frappe.local.site).info({
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),
"scheme": getattr(request, "scheme", "NOTFOUND"),
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
def process_response(response):
if not response:
return
@ -185,11 +186,12 @@ def make_form_dict(request):
args = request.form or request.args
if not isinstance(args, dict):
frappe.throw("Invalid request arguments")
frappe.throw(_("Invalid request arguments"))
try:
frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \
for k, v in iteritems(args) })
frappe.local.form_dict = frappe._dict({
k: v[0] if isinstance(v, (list, tuple)) else v for k, v in args.items()
})
except IndexError:
frappe.local.form_dict = frappe._dict(args)
@ -201,12 +203,20 @@ def handle_exception(e):
response = None
http_status_code = getattr(e, "http_status_code", 500)
return_as_message = False
accept_header = frappe.get_request_header("Accept") or ""
respond_as_json = (
frappe.get_request_header('Accept')
and (frappe.local.is_ajax or 'application/json' in accept_header)
or (
frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")
)
)
if frappe.conf.get('developer_mode'):
# don't fail silently
print(frappe.get_traceback())
if frappe.get_request_header('Accept') and (frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept')):
if respond_as_json:
# handle ajax responses first
# if the request is ajax, send back the trace or error message
response = frappe.utils.response.report_error(http_status_code)
@ -286,8 +296,9 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
_sites_path = sites_path
from werkzeug.serving import run_simple
patch_werkzeug_reloader()
if profile:
if profile or os.environ.get('USE_PROFILER'):
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls'))
if not os.environ.get('NO_STATICS'):
@ -316,3 +327,23 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
use_debugger=not in_test_env,
use_evalex=not in_test_env,
threaded=not no_threading)
def patch_werkzeug_reloader():
"""
This function monkey patches Werkzeug reloader to ignore reloading files in
the __pycache__ directory.
To be deprecated when upgrading to Werkzeug 2.
"""
from werkzeug._reloader import WatchdogReloaderLoop
trigger_reload = WatchdogReloaderLoop.trigger_reload
def custom_trigger_reload(self, filename):
if os.path.basename(os.path.dirname(filename)) == "__pycache__":
return
return trigger_reload(self, filename)
WatchdogReloaderLoop.trigger_reload = custom_trigger_reload

View file

@ -1,9 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import datetime
from frappe import _
import frappe
import frappe.database
@ -19,8 +16,7 @@ from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,
confirm_otp_token, get_cached_user_pass)
from frappe.website.utils import get_home_page
from six.moves.urllib.parse import quote
from urllib.parse import quote
class HTTPRequest:

View file

@ -2,8 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.desk.form import assign_to

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import random_string

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

View file

@ -103,7 +103,7 @@ frappe.ui.form.on('Auto Repeat', {
frappe.auto_repeat.render_schedule = function(frm) {
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
frm.call("get_auto_repeat_schedule").then(r => {
frm.dashboard.wrapper.empty();
frm.dashboard.reset();
frm.dashboard.add_section(
frappe.render_template("auto_repeat_schedule", {
schedule_details: r.message || []

View file

@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from datetime import timedelta

View file

@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Auto Repeat", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Auto Repeat
() => frappe.tests.make('Auto Repeat', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
import frappe
@ -173,7 +171,7 @@ class TestAutoRepeat(unittest.TestCase):
fields=['docstatus'],
limit=1
)
self.assertEquals(docnames[0].docstatus, 1)
self.assertEqual(docnames[0].docstatus, 1)
def make_auto_repeat(**args):

View file

@ -2,7 +2,6 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

View file

@ -2,8 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
#import frappe
import unittest

View file

@ -2,8 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import frappe.cache_manager

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import frappe.cache_manager
import unittest

View file

@ -1,10 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
from six import iteritems, text_type
"""
bootstrap client session
"""
@ -42,8 +37,6 @@ def get_bootinfo():
bootinfo.user_info = get_user_info()
bootinfo.sid = frappe.session['sid']
bootinfo.user_groups = frappe.get_all('User Group', pluck="name")
bootinfo.modules = {}
bootinfo.module_list = []
load_desktop_data(bootinfo)
@ -77,7 +70,7 @@ def get_bootinfo():
frappe.get_attr(method)(bootinfo)
if bootinfo.lang:
bootinfo.lang = text_type(bootinfo.lang)
bootinfo.lang = str(bootinfo.lang)
bootinfo.versions = {k: v['version'] for k, v in get_versions().items()}
bootinfo.error_report_email = frappe.conf.error_report_email
@ -222,7 +215,7 @@ def load_translations(bootinfo):
messages[name] = frappe._(name)
# only untranslated
messages = {k:v for k, v in iteritems(messages) if k!=v}
messages = {k: v for k, v in messages.items() if k!=v}
bootinfo["__messages"] = messages

View file

@ -1,14 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import print_function, unicode_literals
import os
import re
import json
import shutil
import warnings
import tempfile
import subprocess
from io import StringIO
from tempfile import mkdtemp, mktemp
from distutils.spawn import find_executable
import frappe
@ -16,8 +14,9 @@ from frappe.utils.minify import JavascriptMinify
import click
import psutil
from six import iteritems, text_type
from six.moves.urllib.parse import urlparse
from urllib.parse import urlparse
from simple_chalk import green
from semantic_version import Version
timestamps = {}
@ -39,35 +38,36 @@ def download_file(url, prefix):
def build_missing_files():
# check which files dont exist yet from the build.json and tell build.js to build only those!
'''Check which files dont exist yet from the assets.json and run build for those files'''
missing_assets = []
current_asset_files = []
frappe_build = os.path.join("..", "apps", "frappe", "frappe", "public", "build.json")
for type in ["css", "js"]:
current_asset_files.extend(
[
"{0}/{1}".format(type, name)
for name in os.listdir(os.path.join(sites_path, "assets", type))
]
)
folder = os.path.join(sites_path, "assets", "frappe", "dist", type)
current_asset_files.extend(os.listdir(folder))
with open(frappe_build) as f:
all_asset_files = json.load(f).keys()
development = frappe.local.conf.developer_mode or frappe.local.dev_server
build_mode = "development" if development else "production"
for asset in all_asset_files:
if asset.replace("concat:", "") not in current_asset_files:
missing_assets.append(asset)
assets_json = frappe.read_file("assets/assets.json")
if assets_json:
assets_json = frappe.parse_json(assets_json)
if missing_assets:
from subprocess import check_call
from shlex import split
for bundle_file, output_file in assets_json.items():
if not output_file.startswith('/assets/frappe'):
continue
click.secho("\nBuilding missing assets...\n", fg="yellow")
command = split(
"node rollup/build.js --files {0} --no-concat".format(",".join(missing_assets))
)
check_call(command, cwd=os.path.join("..", "apps", "frappe"))
if os.path.basename(output_file) not in current_asset_files:
missing_assets.append(bundle_file)
if missing_assets:
click.secho("\nBuilding missing assets...\n", fg="yellow")
files_to_build = ["frappe/" + name for name in missing_assets]
bundle(build_mode, files=files_to_build)
else:
# no assets.json, run full build
bundle(build_mode, apps="frappe")
def get_assets_link(frappe_head):
@ -75,8 +75,8 @@ def get_assets_link(frappe_head):
from requests import head
tag = getoutput(
"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
" refs/tags/,,' -e 's/\^{}//'"
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
r" refs/tags/,,' -e 's/\^{}//'"
% frappe_head
)
@ -97,9 +97,7 @@ def download_frappe_assets(verbose=True):
commit HEAD.
Returns True if correctly setup else returns False.
"""
from simple_chalk import green
from subprocess import getoutput
from tempfile import mkdtemp
assets_setup = False
frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD")
@ -166,7 +164,7 @@ def symlink(target, link_name, overwrite=False):
# Create link to target with temporary filename
while True:
temp_link_name = tempfile.mktemp(dir=link_dir)
temp_link_name = mktemp(dir=link_dir)
# os.* functions mimic as closely as possible system functions
# The POSIX symlink() returns EEXIST if link_name already exists
@ -193,7 +191,8 @@ def symlink(target, link_name, overwrite=False):
def setup():
global app_paths
global app_paths, assets_path
pymodules = []
for app in frappe.get_all_apps(True):
try:
@ -201,51 +200,54 @@ def setup():
except ImportError:
pass
app_paths = [os.path.dirname(pymodule.__file__) for pymodule in pymodules]
assets_path = os.path.join(frappe.local.sites_path, "assets")
def get_node_pacman():
exec_ = find_executable("yarn")
if exec_:
return exec_
raise ValueError("Yarn not found")
def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False, skip_frappe=False):
def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, verbose=False, skip_frappe=False, files=None):
"""concat / minify js files"""
setup()
make_asset_dirs(make_copy=make_copy, restore=restore)
make_asset_dirs(hard_link=hard_link)
pacman = get_node_pacman()
mode = "build" if no_compress else "production"
command = "{pacman} run {mode}".format(pacman=pacman, mode=mode)
mode = "production" if mode == "production" else "build"
command = "yarn run {mode}".format(mode=mode)
if app:
command += " --app {app}".format(app=app)
if apps:
command += " --apps {apps}".format(apps=apps)
if skip_frappe:
command += " --skip_frappe"
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
check_yarn()
if files:
command += " --files {files}".format(files=','.join(files))
command += " --run-build-command"
check_node_executable()
frappe_app_path = frappe.get_app_path("frappe", "..")
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
def watch(no_compress):
def watch(apps=None):
"""watch and rebuild if necessary"""
setup()
pacman = get_node_pacman()
command = "yarn run watch"
if apps:
command += " --apps {apps}".format(apps=apps)
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
check_yarn()
check_node_executable()
frappe_app_path = frappe.get_app_path("frappe", "..")
frappe.commands.popen("{pacman} run watch".format(pacman=pacman),
cwd=frappe_app_path, env=get_node_env())
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
def check_yarn():
def check_node_executable():
node_version = Version(subprocess.getoutput('node -v')[1:])
warn = '⚠️ '
if node_version.major < 14:
click.echo(f"{warn} Please update your node version to 14")
if not find_executable("yarn"):
print("Please install yarn using below command and try again.\nnpm install -g yarn")
click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn")
click.echo()
def get_node_env():
node_env = {
@ -266,75 +268,109 @@ def get_safe_max_old_space_size():
return safe_max_old_space_size
def make_asset_dirs(make_copy=False, restore=False):
# don't even think of making assets_path absolute - rm -rf ahead.
assets_path = os.path.join(frappe.local.sites_path, "assets")
def generate_assets_map():
symlinks = {}
for dir_path in [os.path.join(assets_path, "js"), os.path.join(assets_path, "css")]:
if not os.path.exists(dir_path):
os.makedirs(dir_path)
for app_name in frappe.get_all_apps():
app_doc_path = None
for app_name in frappe.get_all_apps(True):
pymodule = frappe.get_module(app_name)
app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__))
symlinks = []
app_public_path = os.path.join(app_base_path, "public")
# app/public > assets/app
symlinks.append([app_public_path, os.path.join(assets_path, app_name)])
# app/node_modules > assets/app/node_modules
if os.path.exists(os.path.abspath(app_public_path)):
symlinks.append(
[
os.path.join(app_base_path, "..", "node_modules"),
os.path.join(assets_path, app_name, "node_modules"),
]
)
app_node_modules_path = os.path.join(app_base_path, "..", "node_modules")
app_docs_path = os.path.join(app_base_path, "docs")
app_www_docs_path = os.path.join(app_base_path, "www", "docs")
app_doc_path = None
if os.path.isdir(os.path.join(app_base_path, "docs")):
app_assets = os.path.abspath(app_public_path)
app_node_modules = os.path.abspath(app_node_modules_path)
# {app}/public > assets/{app}
if os.path.isdir(app_assets):
symlinks[app_assets] = os.path.join(assets_path, app_name)
# {app}/node_modules > assets/{app}/node_modules
if os.path.isdir(app_node_modules):
symlinks[app_node_modules] = os.path.join(assets_path, app_name, "node_modules")
# {app}/docs > assets/{app}_docs
if os.path.isdir(app_docs_path):
app_doc_path = os.path.join(app_base_path, "docs")
elif os.path.isdir(os.path.join(app_base_path, "www", "docs")):
elif os.path.isdir(app_www_docs_path):
app_doc_path = os.path.join(app_base_path, "www", "docs")
if app_doc_path:
symlinks.append([app_doc_path, os.path.join(assets_path, app_name + "_docs")])
app_docs = os.path.abspath(app_doc_path)
symlinks[app_docs] = os.path.join(assets_path, app_name + "_docs")
for source, target in symlinks:
source = os.path.abspath(source)
if os.path.exists(source):
if restore:
if os.path.exists(target):
if os.path.islink(target):
os.unlink(target)
else:
shutil.rmtree(target)
shutil.copytree(source, target)
elif make_copy:
if os.path.exists(target):
warnings.warn("Target {target} already exists.".format(target=target))
else:
shutil.copytree(source, target)
else:
if os.path.exists(target):
if os.path.islink(target):
os.unlink(target)
else:
shutil.rmtree(target)
try:
symlink(source, target, overwrite=True)
except OSError:
print("Cannot link {} to {}".format(source, target))
else:
# warnings.warn('Source {source} does not exist.'.format(source = source))
pass
return symlinks
def setup_assets_dirs():
for dir_path in (os.path.join(assets_path, x) for x in ("js", "css")):
os.makedirs(dir_path, exist_ok=True)
def clear_broken_symlinks():
for path in os.listdir(assets_path):
path = os.path.join(assets_path, path)
if os.path.islink(path) and not os.path.exists(path):
os.remove(path)
def unstrip(message: str) -> str:
"""Pads input string on the right side until the last available column in the terminal
"""
_len = len(message)
try:
max_str = os.get_terminal_size().columns
except Exception:
max_str = 80
if _len < max_str:
_rem = max_str - _len
else:
_rem = max_str % _len
return f"{message}{' ' * _rem}"
def make_asset_dirs(hard_link=False):
setup_assets_dirs()
clear_broken_symlinks()
symlinks = generate_assets_map()
for source, target in symlinks.items():
start_message = unstrip(f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}")
fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}")
# Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes
try:
print(start_message, end="\r")
link_assets_dir(source, target, hard_link=hard_link)
except Exception:
print(fail_message, end="\r")
print(unstrip(f"{green('')} Application Assets Linked") + "\n")
def link_assets_dir(source, target, hard_link=False):
if not os.path.exists(source):
return
if os.path.exists(target):
if os.path.islink(target):
os.unlink(target)
else:
shutil.rmtree(target)
if hard_link:
shutil.copytree(source, target, dirs_exist_ok=True)
else:
symlink(source, target, overwrite=True)
def build(no_compress=False, verbose=False):
assets_path = os.path.join(frappe.local.sites_path, "assets")
for target, sources in iteritems(get_build_maps()):
for target, sources in get_build_maps().items():
pack(os.path.join(assets_path, target), sources, no_compress, verbose)
@ -348,7 +384,7 @@ def get_build_maps():
if os.path.exists(path):
with open(path) as f:
try:
for target, sources in iteritems(json.loads(f.read())):
for target, sources in (json.loads(f.read() or "{}")).items():
# update app path
source_paths = []
for source in sources:
@ -366,8 +402,6 @@ def get_build_maps():
def pack(target, sources, no_compress, verbose):
from six import StringIO
outtype, outtxt = target.split(".")[-1], ""
jsm = JavascriptMinify()
@ -381,7 +415,7 @@ def pack(target, sources, no_compress, verbose):
timestamps[f] = os.path.getmtime(f)
try:
with open(f, "r") as sourcefile:
data = text_type(sourcefile.read(), "utf-8", errors="ignore")
data = str(sourcefile.read(), "utf-8", errors="ignore")
extn = f.rsplit(".", 1)[1]
@ -396,7 +430,7 @@ def pack(target, sources, no_compress, verbose):
jsm.minify(tmpin, tmpout)
minified = tmpout.getvalue()
if minified:
outtxt += text_type(minified or "", "utf-8").strip("\n") + ";"
outtxt += str(minified or "", "utf-8").strip("\n") + ";"
if verbose:
print("{0}: {1}k".format(f, int(len(minified) / 1024)))
@ -426,16 +460,16 @@ def html_to_js_template(path, content):
def scrub_html_template(content):
"""Returns HTML content with removed whitespace and comments"""
# remove whitespace to a single space
content = re.sub("\s+", " ", content)
content = re.sub(r"\s+", " ", content)
# strip comments
content = re.sub("(<!--.*?-->)", "", content)
content = re.sub(r"(<!--.*?-->)", "", content)
return content.replace("'", "\'")
def files_dirty():
for target, sources in iteritems(get_build_maps()):
for target, sources in get_build_maps().items():
for f in sources:
if ":" in f:
f, suffix = f.split(":")

View file

@ -1,8 +1,6 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, json
from frappe.model.document import Document
from frappe.desk.notifications import (delete_notification_count_for,
@ -13,6 +11,8 @@ common_default_keys = ["__default", "__global"]
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map',
'milestone_tracker_map', 'event_consumer_document_type_map')
bench_cache_keys = ('assets_json',)
global_cache_keys = ("app_hooks", "installed_apps", 'all_apps',
"app_modules", "module_app", "system_settings",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
@ -58,6 +58,7 @@ def clear_global_cache():
clear_doctype_cache()
clear_website_cache()
frappe.cache().delete_value(global_cache_keys)
frappe.cache().delete_value(bench_cache_keys)
frappe.setup_module_map()
def clear_defaults_cache(user=None):

View file

@ -0,0 +1,32 @@
# Version 13.2.0 Release Notes
### Features & Enhancements
- Add option to mention a group of users ([#12844](https://github.com/frappe/frappe/pull/12844))
- Copy DocType / documents across sites ([#12872](https://github.com/frappe/frappe/pull/12872))
- Scheduler log in notifications ([#1135](https://github.com/frappe/frappe/pull/1135))
- Add Enable/Disable Webhook via Check Field ([#12842](https://github.com/frappe/frappe/pull/12842))
- Allow query/custom reports to save custom data in the json field ([#12534](https://github.com/frappe/frappe/pull/12534))
### Fixes
- Load server translations in boot (backport #12848) ([#12852](https://github.com/frappe/frappe/pull/12852))
- Allow to override dashboard chart properties type/color ([#12846](https://github.com/frappe/frappe/pull/12846))
- Multi-column paste in grid ([#12861](https://github.com/frappe/frappe/pull/12861))
- Add log_error and FrappeClient to restricted python ([#12857](https://github.com/frappe/frappe/pull/12857))
- Redirect Web Form user directly to success URL, if no amount is due ([#12661](https://github.com/frappe/frappe/pull/12661))
- Attachment pill lock icon redirects to File ([#12864](https://github.com/frappe/frappe/pull/12864))
- Redirect Web Form user directly to success URL, if no amount is due (backport #12661) ([#12856](https://github.com/frappe/frappe/pull/12856))
- Remove events to redraw charts ([#12973](https://github.com/frappe/frappe/pull/12973))
- Don't allow user to remove/change data source file in data import ([#12827](https://github.com/frappe/frappe/pull/12827))
- Load server translations in boot ([#12848](https://github.com/frappe/frappe/pull/12848))
- Newly created Workspace not being accessible unless a shortcut u… ([#12866](https://github.com/frappe/frappe/pull/12866))
- Currency labels in grids ([#12974](https://github.com/frappe/frappe/pull/12974))
- Handle error while session start ([#12933](https://github.com/frappe/frappe/pull/12933))
- Add field type check in custom field validation ([#12858](https://github.com/frappe/frappe/pull/12858))
- Make language select optional and fix breakpoint issues ([#12860](https://github.com/frappe/frappe/pull/12860))
- Form Dashboard reference link ([#12945](https://github.com/frappe/frappe/pull/12945))
- Invalid HTML generated by the base template ([#12953](https://github.com/frappe/frappe/pull/12953))
- Default values were not triggering change event ([#12975](https://github.com/frappe/frappe/pull/12975))
- Make strings translatable ([#12877](https://github.com/frappe/frappe/pull/12877))
- Added build-message-files command ([#12950](https://github.com/frappe/frappe/pull/12950))

View file

@ -0,0 +1,49 @@
# Version 13.3.0 Release Notes
### Features & Enhancements
- Deletion Steps in Data Deletion Tool ([#13124](https://github.com/frappe/frappe/pull/13124))
- Format Option for list-apps in bench CLI ([#13125](https://github.com/frappe/frappe/pull/13125))
- Add password fieldtype option for Web Form ([#13093](https://github.com/frappe/frappe/pull/13093))
- Add simple __repr__ for DocTypes ([#13151](https://github.com/frappe/frappe/pull/13151))
- Switch theme with left/right keys ([#13077](https://github.com/frappe/frappe/pull/13077))
- sourceURL for injected javascript ([#13022](https://github.com/frappe/frappe/pull/13022))
### Fixes
- Decode uri before importing file via weblink ([#13026](https://github.com/frappe/frappe/pull/13026))
- Respond to /api requests as JSON by default ([#13053](https://github.com/frappe/frappe/pull/13053))
- Disabled checkbox should be disabled ([#13021](https://github.com/frappe/frappe/pull/13021))
- Moving Site folder across different FileSystems failed ([#13038](https://github.com/frappe/frappe/pull/13038))
- Freeze screen till the background request is complete ([#13078](https://github.com/frappe/frappe/pull/13078))
- Added conditional rendering for content field in split section w… ([#13075](https://github.com/frappe/frappe/pull/13075))
- Show delete button on portal if user has permission to delete document ([#13149](https://github.com/frappe/frappe/pull/13149))
- Dont disable dialog scroll on focusing a Link/Autocomplete field ([#13119](https://github.com/frappe/frappe/pull/13119))
- Typo in RecorderDetail.vue ([#13086](https://github.com/frappe/frappe/pull/13086))
- Error for bench drop-site. Added missing import. ([#13064](https://github.com/frappe/frappe/pull/13064))
- Report column context ([#13090](https://github.com/frappe/frappe/pull/13090))
- Different service name for push and pull request events ([#13094](https://github.com/frappe/frappe/pull/13094))
- Moving Site folder across different FileSystems failed ([#13033](https://github.com/frappe/frappe/pull/13033))
- Consistent checkboxes on all browsers ([#13042](https://github.com/frappe/frappe/pull/13042))
- Changed shorcut widgets color picker to dropdown ([#13073](https://github.com/frappe/frappe/pull/13073))
- Error while exporting reports with duration field ([#13118](https://github.com/frappe/frappe/pull/13118))
- Add margin to download backup card ([#13079](https://github.com/frappe/frappe/pull/13079))
- Move mention list generation logic to server-side ([#13074](https://github.com/frappe/frappe/pull/13074))
- Make strings translatable ([#13046](https://github.com/frappe/frappe/pull/13046))
- Don't evaluate dynamic properties to check if conflicts exist ([#13186](https://github.com/frappe/frappe/pull/13186))
- Add __ function in vue global for translation in recorder ([#13089](https://github.com/frappe/frappe/pull/13089))
- Make strings translatable ([#13076](https://github.com/frappe/frappe/pull/13076))
- Show config in bench CLI ([#13128](https://github.com/frappe/frappe/pull/13128))
- Add breadcrumbs for list view ([#13091](https://github.com/frappe/frappe/pull/13091))
- Do not skip data in save while using shortcut ([#13182](https://github.com/frappe/frappe/pull/13182))
- Use docfields from options if no docfields are returned from meta ([#13188](https://github.com/frappe/frappe/pull/13188))
- Disable reloading files in `__pycache__` directory ([#13109](https://github.com/frappe/frappe/pull/13109))
- RTL stylesheet route to load RTL style on demand. ([#13007](https://github.com/frappe/frappe/pull/13007))
- Do not show messsage when exception is handled ([#13111](https://github.com/frappe/frappe/pull/13111))
- Replace parseFloat by Number ([#13082](https://github.com/frappe/frappe/pull/13082))
- Add margin to download backup card ([#13050](https://github.com/frappe/frappe/pull/13050))
- Translate report column labels ([#13083](https://github.com/frappe/frappe/pull/13083))
- Grid row color picker field not working ([#13040](https://github.com/frappe/frappe/pull/13040))
- Improve oauthlib implementation ([#13045](https://github.com/frappe/frappe/pull/13045))
- Replace filter_by like with full text filter ([#13126](https://github.com/frappe/frappe/pull/13126))
- Focus jumps to first field ([#13067](https://github.com/frappe/frappe/pull/13067))

View file

@ -1,4 +1,4 @@
from __future__ import unicode_literals
import frappe
from frappe import _

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - standard imports
import json

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - module imports
from frappe.model.document import Document
from frappe import _

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - module imports
from frappe.model.document import Document
from frappe import _

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - module imports
from frappe.model.document import Document
import frappe

View file

@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - module imports
from frappe.chat.util.util import (
get_user_doc,

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - standard imports
import unittest
@ -9,7 +7,6 @@ from frappe.chat.util import (
safe_json_loads
)
import frappe
import six
class TestChatUtil(unittest.TestCase):
def test_safe_json_loads(self):
@ -20,7 +17,7 @@ class TestChatUtil(unittest.TestCase):
self.assertEqual(type(number), float)
string = safe_json_loads("foobar")
self.assertEqual(type(string), six.text_type)
self.assertEqual(type(string), str)
array = safe_json_loads('[{ "foo": "bar" }]')
self.assertEqual(type(array), list)

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
# imports - standard imports
import json
from collections.abc import MutableMapping, MutableSequence, Sequence

View file

@ -1,4 +1,4 @@
from __future__ import unicode_literals
import frappe
from frappe.chat.util import filter_dict, safe_json_loads

View file

@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
import frappe.model
@ -11,7 +9,6 @@ from frappe.utils import get_safe_filters
from frappe.desk.reportview import validate_args
from frappe.model.db_query import check_parent_permission
from six import iteritems, string_types, integer_types
'''
Handle RESTful requests that are mapped to the `/api/resource` route.
@ -86,7 +83,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
frappe.throw(_("No permission for {0}").format(doctype), frappe.PermissionError)
filters = get_safe_filters(filters)
if isinstance(filters, string_types):
if isinstance(filters, str):
filters = {"name": filters}
try:
@ -135,7 +132,7 @@ def set_value(doctype, name, fieldname, value=None):
if not value:
values = fieldname
if isinstance(fieldname, string_types):
if isinstance(fieldname, str):
try:
values = json.loads(fieldname)
except ValueError:
@ -161,7 +158,7 @@ def insert(doc=None):
'''Insert a document
:param doc: JSON or dict object to be inserted'''
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = json.loads(doc)
if doc.get("parent") and doc.get("parenttype"):
@ -179,7 +176,7 @@ def insert_many(docs=None):
'''Insert multiple documents
:param docs: JSON or list of dict objects to be inserted in one request'''
if isinstance(docs, string_types):
if isinstance(docs, str):
docs = json.loads(docs)
out = []
@ -205,7 +202,7 @@ def save(doc):
'''Update (save) an existing document
:param doc: JSON or dict object with the properties of the document to be updated'''
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = json.loads(doc)
doc = frappe.get_doc(doc)
@ -228,7 +225,7 @@ def submit(doc):
'''Submit a document
:param doc: JSON or dict object to be submitted remotely'''
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = json.loads(doc)
doc = frappe.get_doc(doc)
@ -266,7 +263,7 @@ def make_width_property_setter(doc):
'''Set width Property Setter
:param doc: Property Setter document with `width` property'''
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = json.loads(doc)
if doc["doctype"]=="Property Setter" and doc["property"]=="width":
frappe.get_doc(doc).insert(ignore_permissions = True)
@ -280,7 +277,7 @@ def bulk_update(docs):
failed_docs = []
for doc in docs:
try:
ddoc = {key: val for key, val in iteritems(doc) if key not in ['doctype', 'docname']}
ddoc = {key: val for key, val in doc.items() if key not in ['doctype', 'docname']}
doctype = doc['doctype']
docname = doc['docname']
doc = frappe.get_doc(doctype, docname)

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, absolute_import, print_function
import sys
import click
import cProfile
@ -10,7 +9,7 @@ import frappe
import frappe.utils
import subprocess # nosec
from functools import wraps
from six import StringIO
from io import StringIO
from os import environ
click.disable_unicode_literals_warning = True
@ -28,6 +27,10 @@ def pass_context(f):
except frappe.exceptions.SiteNotSpecifiedError as e:
click.secho(str(e), fg='yellow')
sys.exit(1)
except frappe.exceptions.IncorrectSitePath:
site = ctx.obj.get("sites", "")[0]
click.secho(f'Site {site} does not exist!', fg='yellow')
sys.exit(1)
if profile:
pr.disable()

View file

@ -1,4 +1,3 @@
from __future__ import unicode_literals, absolute_import, print_function
import click
import sys
import frappe

View file

@ -1,6 +1,7 @@
# imports - standard imports
import os
import sys
import shutil
# imports - third party imports
import click
@ -202,10 +203,13 @@ def install_app(context, apps):
@click.command("list-apps")
@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
@pass_context
def list_apps(context):
def list_apps(context, format):
"List apps in site"
summary_dict = {}
def fix_whitespaces(text):
if site == context.sites[-1]:
text = text.rstrip()
@ -234,18 +238,23 @@ def list_apps(context):
]
applications_summary = "\n".join(installed_applications)
summary = f"{site_title}\n{applications_summary}\n"
summary_dict[site] = [app.app_name for app in apps]
else:
applications_summary = "\n".join(frappe.get_installed_apps())
installed_applications = frappe.get_installed_apps()
applications_summary = "\n".join(installed_applications)
summary = f"{site_title}\n{applications_summary}\n"
summary_dict[site] = installed_applications
summary = fix_whitespaces(summary)
if applications_summary and summary:
if format == "text" and applications_summary and summary:
print(summary)
frappe.destroy()
if format == "json":
click.echo(frappe.as_json(summary_dict))
@click.command('add-system-manager')
@click.argument('email')
@ -547,7 +556,7 @@ def move(dest_dir, site):
site_dump_exists = os.path.exists(final_new_path)
count = int(count or 0) + 1
os.rename(old_path, final_new_path)
shutil.move(old_path, final_new_path)
frappe.destroy()
return final_new_path

View file

@ -1,4 +1,3 @@
from __future__ import unicode_literals, absolute_import, print_function
import click
from frappe.commands import pass_context, get_site
from frappe.exceptions import SiteNotSpecifiedError

View file

@ -16,33 +16,52 @@ from frappe.utils import get_bench_path, update_progress_bar, cint
@click.command('build')
@click.option('--app', help='Build assets for app')
@click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking')
@click.option('--restore', is_flag=True, default=False, help='Copy the files instead of symlinking with force')
@click.option('--apps', help='Build assets for specific apps')
@click.option('--hard-link', is_flag=True, default=False, help='Copy the files instead of symlinking')
@click.option('--make-copy', is_flag=True, default=False, help='[DEPRECATED] Copy the files instead of symlinking')
@click.option('--restore', is_flag=True, default=False, help='[DEPRECATED] Copy the files instead of symlinking with force')
@click.option('--production', is_flag=True, default=False, help='Build assets in production mode')
@click.option('--verbose', is_flag=True, default=False, help='Verbose')
@click.option('--force', is_flag=True, default=False, help='Force build assets instead of downloading available')
def build(app=None, make_copy=False, restore=False, verbose=False, force=False):
"Minify + concatenate JS and CSS files, build translations"
import frappe.build
def build(app=None, apps=None, hard_link=False, make_copy=False, restore=False, production=False, verbose=False, force=False):
"Compile JS and CSS source files"
from frappe.build import bundle, download_frappe_assets
frappe.init('')
# don't minify in developer_mode for faster builds
no_compress = frappe.local.conf.developer_mode or False
if not apps and app:
apps = app
# dont try downloading assets if force used, app specified or running via CI
if not (force or app or os.environ.get('CI')):
if not (force or apps or os.environ.get('CI')):
# skip building frappe if assets exist remotely
skip_frappe = frappe.build.download_frappe_assets(verbose=verbose)
skip_frappe = download_frappe_assets(verbose=verbose)
else:
skip_frappe = False
frappe.build.bundle(no_compress, app=app, make_copy=make_copy, restore=restore, verbose=verbose, skip_frappe=skip_frappe)
# don't minify in developer_mode for faster builds
development = frappe.local.conf.developer_mode or frappe.local.dev_server
mode = "development" if development else "production"
if production:
mode = "production"
if make_copy or restore:
hard_link = make_copy or restore
click.secho(
"bench build: --make-copy and --restore options are deprecated in favour of --hard-link",
fg="yellow",
)
bundle(mode, apps=apps, hard_link=hard_link, verbose=verbose, skip_frappe=skip_frappe)
@click.command('watch')
def watch():
"Watch and concatenate JS and CSS files as and when they change"
import frappe.build
@click.option('--apps', help='Watch assets for specific apps')
def watch(apps=None):
"Watch and compile JS and CSS files as and when they change"
from frappe.build import watch
frappe.init('')
frappe.build.watch(True)
watch(apps)
@click.command('clear-cache')
@ -96,22 +115,54 @@ def destroy_all_sessions(context, reason=None):
raise SiteNotSpecifiedError
@click.command('show-config')
@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
@pass_context
def show_config(context):
"print configuration file"
print("\t\033[92m{:<50}\033[0m \033[92m{:<15}\033[0m".format('Config','Value'))
sites_path = os.path.join(frappe.utils.get_bench_path(), 'sites')
site_path = context.sites[0]
configuration = frappe.get_site_config(sites_path=sites_path, site_path=site_path)
print_config(configuration)
def show_config(context, format):
"Print configuration file to STDOUT in speified format"
if not context.sites:
raise SiteNotSpecifiedError
def print_config(config):
for conf, value in config.items():
if isinstance(value, dict):
print_config(value)
else:
print("\t{:<50} {:<15}".format(conf, value))
sites_config = {}
sites_path = os.getcwd()
from frappe.utils.commands import render_table
def transform_config(config, prefix=None):
prefix = f"{prefix}." if prefix else ""
site_config = []
for conf, value in config.items():
if isinstance(value, dict):
site_config += transform_config(value, prefix=f"{prefix}{conf}")
else:
log_value = json.dumps(value) if isinstance(value, list) else value
site_config += [[f"{prefix}{conf}", log_value]]
return site_config
for site in context.sites:
frappe.init(site)
if len(context.sites) != 1 and format == "text":
if context.sites.index(site) != 0:
click.echo()
click.secho(f"Site {site}", fg="yellow")
configuration = frappe.get_site_config(sites_path=sites_path, site_path=site)
if format == "text":
data = transform_config(configuration)
data.insert(0, ['Config','Value'])
render_table(data)
if format == "json":
sites_config[site] = configuration
frappe.destroy()
if format == "json":
click.echo(frappe.as_json(sites_config))
@click.command('reset-perms')
@ -171,7 +222,7 @@ def execute(context, method, args=None, kwargs=None, profile=False):
if profile:
import pstats
from six import StringIO
from io import StringIO
pr.disable()
s = StringIO()
@ -470,6 +521,7 @@ def console(context):
locals()[app] = __import__(app)
except ModuleNotFoundError:
failed_to_import.append(app)
all_apps.remove(app)
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
if failed_to_import:
@ -520,7 +572,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
cov = Coverage(source=[source_path], omit=[
omit=[
'*.html',
'*.js',
'*.xml',
@ -530,7 +582,12 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
'*.vue',
'*/doctype/*/*_dashboard.py',
'*/patches/*'
])
]
if not app or app == 'frappe':
omit.append('*/commands/*')
cov = Coverage(source=[source_path], omit=omit)
cov.start()
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
@ -547,12 +604,29 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
if os.environ.get('CI'):
sys.exit(ret)
@click.command('run-parallel-tests')
@click.option('--app', help="For App", default='frappe')
@click.option('--build-number', help="Build number", default=1)
@click.option('--total-builds', help="Total number of builds", default=1)
@click.option('--with-coverage', is_flag=True, help="Build coverage file")
@click.option('--use-orchestrator', is_flag=True, help="Use orchestrator to run parallel tests")
@pass_context
def run_parallel_tests(context, app, build_number, total_builds, with_coverage=False, use_orchestrator=False):
site = get_site(context)
if use_orchestrator:
from frappe.parallel_test_runner import ParallelTestWithOrchestrator
ParallelTestWithOrchestrator(app, site=site, with_coverage=with_coverage)
else:
from frappe.parallel_test_runner import ParallelTestRunner
ParallelTestRunner(app, site=site, build_number=build_number, total_builds=total_builds, with_coverage=with_coverage)
@click.command('run-ui-tests')
@click.argument('app')
@click.option('--headless', is_flag=True, help="Run UI Test in headless mode")
@click.option('--parallel', is_flag=True, help="Run UI Test in parallel mode")
@click.option('--ci-build-id')
@pass_context
def run_ui_tests(context, app, headless=False):
def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
"Run UI tests"
site = get_site(context)
app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..'))
@ -584,6 +658,12 @@ def run_ui_tests(context, app, headless=False):
command = '{site_env} {password_env} {cypress} {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
if parallel:
formatted_command += ' --parallel'
if ci_build_id:
formatted_command += ' --ci-build-id {}'.format(ci_build_id)
click.secho("Running Cypress...", fg="yellow")
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
@ -652,20 +732,27 @@ def make_app(destination, app_name):
@click.command('set-config')
@click.argument('key')
@click.argument('value')
@click.option('-g', '--global', 'global_', is_flag = True, default = False, help = 'Set Global Site Config')
@click.option('--as-dict', is_flag=True, default=False)
@click.option('-g', '--global', 'global_', is_flag=True, default=False, help='Set value in bench config')
@click.option('-p', '--parse', is_flag=True, default=False, help='Evaluate as Python Object')
@click.option('--as-dict', is_flag=True, default=False, help='Legacy: Evaluate as Python Object')
@pass_context
def set_config(context, key, value, global_ = False, as_dict=False):
def set_config(context, key, value, global_=False, parse=False, as_dict=False):
"Insert/Update a value in site_config.json"
from frappe.installer import update_site_config
import ast
if as_dict:
from frappe.utils.commands import warn
warn("--as-dict will be deprecated in v14. Use --parse instead", category=PendingDeprecationWarning)
parse = as_dict
if parse:
import ast
value = ast.literal_eval(value)
if global_:
sites_path = os.getcwd() # big assumption.
sites_path = os.getcwd()
common_site_config_path = os.path.join(sites_path, 'common_site_config.json')
update_site_config(key, value, validate = False, site_config_path = common_site_config_path)
update_site_config(key, value, validate=False, site_config_path=common_site_config_path)
else:
for site in context.sites:
frappe.init(site=site)
@ -722,50 +809,6 @@ def rebuild_global_search(context, static_pages=False):
if not context.sites:
raise SiteNotSpecifiedError
@click.command('auto-deploy')
@click.argument('app')
@click.option('--migrate', is_flag=True, default=False, help='Migrate after pulling')
@click.option('--restart', is_flag=True, default=False, help='Restart after migration')
@click.option('--remote', default='upstream', help='Remote, default is "upstream"')
@pass_context
def auto_deploy(context, app, migrate=False, restart=False, remote='upstream'):
'''Pull and migrate sites that have new version'''
from frappe.utils.gitutils import get_app_branch
from frappe.utils import get_sites
branch = get_app_branch(app)
app_path = frappe.get_app_path(app)
# fetch
subprocess.check_output(['git', 'fetch', remote, branch], cwd = app_path)
# get diff
if subprocess.check_output(['git', 'diff', '{0}..{1}/{0}'.format(branch, remote)], cwd = app_path):
print('Updates found for {0}'.format(app))
if app=='frappe':
# run bench update
import shlex
subprocess.check_output(shlex.split('bench update --no-backup'), cwd = '..')
else:
updated = False
subprocess.check_output(['git', 'pull', '--rebase', remote, branch],
cwd = app_path)
# find all sites with that app
for site in get_sites():
frappe.init(site)
if app in frappe.get_installed_apps():
print('Updating {0}'.format(site))
updated = True
subprocess.check_output(['bench', '--site', site, 'clear-cache'], cwd = '..')
if migrate:
subprocess.check_output(['bench', '--site', site, 'migrate'], cwd = '..')
frappe.destroy()
if updated or restart:
subprocess.check_output(['bench', 'restart'], cwd = '..')
else:
print('No Updates')
commands = [
build,
@ -796,5 +839,6 @@ commands = [
watch,
bulk_rename,
add_to_email_queue,
rebuild_global_search
rebuild_global_search,
run_parallel_tests
]

View file

@ -1,6 +1,3 @@
from __future__ import unicode_literals
import json
from six import iteritems
import frappe
from frappe import _
from frappe.desk.moduleview import (get_data, get_onboard_items, config_exists, get_module_link_items_from_list)

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe import _

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import throw, _
@ -10,15 +9,10 @@ from frappe.utils import cstr
from frappe.model.document import Document
from jinja2 import TemplateSyntaxError
from frappe.utils.user import is_website_user
from frappe.model.naming import make_autoname
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
from six import iteritems, string_types
from past.builtins import cmp
from frappe.contacts.address_and_contact import set_link_title
import functools
class Address(Document):
def __setup__(self):
@ -112,10 +106,13 @@ def get_default_address(doctype, name, sort_key='is_primary_address'):
WHERE
dl.parent = addr.name and dl.link_doctype = %s and
dl.link_name = %s and ifnull(addr.disabled, 0) = 0
""" %(sort_key, '%s', '%s'), (doctype, name))
""" %(sort_key, '%s', '%s'), (doctype, name), as_dict=True)
if out:
return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0]
for contact in out:
if contact.get(sort_key):
return contact.name
return out[0].name
else:
return None
@ -141,7 +138,7 @@ def get_territory_from_address(address):
if not address:
return
if isinstance(address, string_types):
if isinstance(address, str):
address = frappe.get_cached_doc("Address", address)
territory = None
@ -174,14 +171,11 @@ def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20,
def has_website_permission(doc, ptype, user, verbose=False):
"""Returns true if there is a related lead or contact related to this document"""
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
return contact.has_common_link(doc)
lead_name = frappe.db.get_value("Lead", {"email_id": frappe.session.user})
if lead_name:
return doc.has_link('Lead', lead_name)
return False
def get_address_templates(address):
@ -214,7 +208,7 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
condition = ""
meta = frappe.get_meta("Address")
for fieldname, value in iteritems(filters):
for fieldname, value in filters.items():
if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS:
condition += " and {field}={value}".format(
field=fieldname,

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe, unittest
from frappe.contacts.doctype.address.address import get_address_display

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import cint

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe, unittest
class TestAddressTemplate(unittest.TestCase):
@ -42,4 +40,4 @@ class TestAddressTemplate(unittest.TestCase):
"doctype": "Address Template",
"country": 'Brazil',
"template": template
}).insert()
}).insert()

View file

@ -1,18 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import cstr, has_gravatar, cint
from frappe.utils import cstr, has_gravatar
from frappe import _
from frappe.model.document import Document
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
from six import iteritems
from past.builtins import cmp
from frappe.model.naming import append_number_if_name_exists
from frappe.contacts.address_and_contact import set_link_title
import functools
class Contact(Document):
def autoname(self):
@ -120,7 +115,7 @@ class Contact(Document):
if len(is_primary) > 1:
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname))))
primary_number_exists = False
primary_number_exists = False
for d in self.phone_nos:
if d.get(field_name) == 1:
primary_number_exists = True
@ -140,10 +135,13 @@ def get_default_contact(doctype, name):
where
dl.link_doctype=%s and
dl.link_name=%s and
dl.parenttype = "Contact"''', (doctype, name))
dl.parenttype = "Contact"''', (doctype, name), as_dict=True)
if out:
return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(cint(y[1]), cint(x[1]))))[0][0]
for contact in out:
if contact.is_primary_contact:
return contact.parent
return out[0].parent
else:
return None

View file

@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Contact", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Contact
() => frappe.tests.make('Contact', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View file

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.exceptions import ValidationError
test_dependencies = ['Contact', 'Salutation']
class TestContact(unittest.TestCase):
@ -52,4 +51,4 @@ def create_contact(name, salutation, emails=None, phones=None, save=True):
if save:
doc.insert()
return doc
return doc

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe.model.document import Document
class Gender(Document):

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestGender(unittest.TestCase):

View file

@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe.model.document import Document
class Salutation(Document):

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestSalutation(unittest.TestCase):

View file

@ -1,8 +1,5 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from six import iteritems
import frappe
from frappe import _
@ -58,7 +55,7 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name):
reference_details = get_reference_details(reference_doctype, "Address", reference_list, reference_details)
reference_details = get_reference_details(reference_doctype, "Contact", reference_list, reference_details)
for reference_name, details in iteritems(reference_details):
for reference_name, details in reference_details.items():
addresses = details.get("address", [])
contacts = details.get("contact", [])
if not any([addresses, contacts]):

View file

@ -1,4 +1,4 @@
from __future__ import unicode_literals
import frappe
import frappe.defaults
import unittest

View file

@ -1,4 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

View file

@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

View file

@ -3,8 +3,6 @@
# For license information, please see license.txt
# imports - standard imports
from __future__ import unicode_literals
# imports - module imports
import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe import _
from frappe.utils import get_fullname, now
from frappe.model.document import Document

View file

@ -1,13 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
from __future__ import unicode_literals
import frappe
import frappe.permissions
from frappe.utils import get_fullname
from frappe import _
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from six import string_types
def update_feed(doc, method=None):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
@ -23,7 +21,7 @@ def update_feed(doc, method=None):
feed = doc.get_feed()
if feed:
if isinstance(feed, string_types):
if isinstance(feed, str):
feed = {"subject": feed}
feed = frappe._dict(feed)

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
import time
@ -65,12 +63,12 @@ class TestActivityLog(unittest.TestCase):
frappe.local.login_manager = LoginManager()
auth_log = self.get_auth_log()
self.assertEquals(auth_log.status, 'Success')
self.assertEqual(auth_log.status, 'Success')
# test user logout log
frappe.local.login_manager.logout()
auth_log = self.get_auth_log(operation='Logout')
self.assertEquals(auth_log.status, 'Success')
self.assertEqual(auth_log.status, 'Success')
# test invalid login
frappe.form_dict.update({ 'pwd': 'password' })
@ -90,4 +88,5 @@ class TestActivityLog(unittest.TestCase):
def update_system_settings(args):
doc = frappe.get_doc('System Settings')
doc.update(args)
doc.flags.ignore_mandatory = 1
doc.save()

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals, absolute_import
import frappe
from frappe import _
import json

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe, json
import unittest

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, absolute_import
from collections import Counter
import frappe
from frappe import _
@ -13,7 +12,7 @@ from frappe.utils.bot import BotReply
from frappe.utils import parse_addr
from frappe.core.doctype.comment.comment import update_comment_in_doc
from email.utils import parseaddr
from six.moves.urllib.parse import unquote
from urllib.parse import unquote
from frappe.utils.user import is_system_user
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
@ -21,9 +20,11 @@ from frappe.automation.doctype.assignment_rule.assignment_rule import apply as a
exclude_from_linked_with = True
class Communication(Document):
"""Communication represents an external communication like Email.
"""
no_feed_on_delete = True
DOCTYPE = 'Communication'
"""Communication represents an external communication like Email."""
def onload(self):
"""create email flag queue"""
if self.communication_type == "Communication" and self.communication_medium == "Email" \
@ -149,6 +150,23 @@ class Communication(Document):
self.email_status = "Spam"
@classmethod
def find(cls, name, ignore_error=False):
try:
return frappe.get_doc(cls.DOCTYPE, name)
except frappe.DoesNotExistError:
if ignore_error:
return
raise
@classmethod
def find_one_by_filters(cls, *, order_by=None, **kwargs):
name = frappe.db.get_value(cls.DOCTYPE, kwargs, order_by=order_by)
return cls.find(name) if name else None
def update_db(self, **kwargs):
frappe.db.set_value(self.DOCTYPE, self.name, kwargs)
def set_sender_full_name(self):
if not self.sender_full_name and self.sender:
if self.sender == "Administrator":
@ -485,4 +503,4 @@ def set_avg_response_time(parent, communication):
response_times.append(response_time)
if response_times:
avg_response_time = sum(response_times) / len(response_times)
parent.db_set("avg_response_time", avg_response_time)
parent.db_set("avg_response_time", avg_response_time)

View file

@ -1,9 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, absolute_import
from six.moves import range
from six import string_types
import frappe
import json
from email.utils import formataddr
@ -77,7 +74,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
comm.save(ignore_permissions=True)
if isinstance(attachments, string_types):
if isinstance(attachments, str):
attachments = json.loads(attachments)
# if not committed, delayed task doesn't find the communication
@ -249,11 +246,11 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
"name":doc.reference_name, "print_format":print_format, "html":print_html})
if attachments:
if isinstance(attachments, string_types):
if isinstance(attachments, str):
attachments = json.loads(attachments)
for a in attachments:
if isinstance(a, string_types):
if isinstance(a, str):
# is it a filename?
try:
# check for both filename and file id
@ -272,22 +269,13 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
doc.attachments.append(a)
def set_incoming_outgoing_accounts(doc):
doc.incoming_email_account = doc.outgoing_email_account = None
from frappe.email.doctype.email_account.email_account import EmailAccount
incoming_email_account = EmailAccount.find_incoming(
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype)
doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None
if not doc.incoming_email_account and doc.sender:
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"email_id": doc.sender, "enable_incoming": 1}, "email_id")
if not doc.incoming_email_account and doc.reference_doctype:
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"append_to": doc.reference_doctype, }, "email_id")
if not doc.incoming_email_account:
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"default_incoming": 1, "enable_incoming": 1}, "email_id")
doc.outgoing_email_account = frappe.email.smtp.get_outgoing_email_account(raise_exception_not_set=False,
append_to=doc.doctype, sender=doc.sender)
doc.outgoing_email_account = EmailAccount.find_outgoing(
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype)
if doc.sent_or_received == "Sent":
doc.db_set("email_account", doc.outgoing_email_account.name)
@ -364,7 +352,7 @@ def add_attachments(name, attachments):
'''Add attachments to the given Communication'''
# loop through attachments
for a in attachments:
if isinstance(a, string_types):
if isinstance(a, str):
attach = frappe.db.get_value("File", {"name":a},
["file_name", "file_url", "is_private"], as_dict=1)

View file

@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Communication", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Communication
() => frappe.tests.make('Communication', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View file

@ -1,10 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from six.moves.urllib.parse import quote
from urllib.parse import quote
test_records = frappe.get_test_records('Communication')

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

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