Merge branch 'develop' into date-field-validation-fix
This commit is contained in:
commit
0ad1779bc7
1214 changed files with 18419 additions and 14751 deletions
6
.github/helper/documentation.py
vendored
6
.github/helper/documentation.py
vendored
|
|
@ -32,9 +32,9 @@ if __name__ == "__main__":
|
|||
|
||||
if response.ok:
|
||||
payload = response.json()
|
||||
title = payload.get("title", "").lower()
|
||||
head_sha = payload.get("head", {}).get("sha")
|
||||
body = payload.get("body", "").lower()
|
||||
title = (payload.get("title") or "").lower()
|
||||
head_sha = (payload.get("head") or {}).get("sha")
|
||||
body = (payload.get("body") or "").lower()
|
||||
|
||||
if title.startswith("feat") and head_sha and "no-docs" not in body:
|
||||
if docs_link_exists(body):
|
||||
|
|
|
|||
5
.github/helper/install_dependencies.sh
vendored
5
.github/helper/install_dependencies.sh
vendored
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
set -e
|
||||
|
||||
# python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
# if [[ $? != 2 ]];then
|
||||
# exit;
|
||||
# fi
|
||||
|
||||
# install wkhtmltopdf
|
||||
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
|
||||
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
|
||||
|
|
|
|||
80
.github/helper/roulette.py
vendored
80
.github/helper/roulette.py
vendored
|
|
@ -1,56 +1,68 @@
|
|||
# if the script ends with exit code 0, then no tests are run further, else all tests are run
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
def get_files_list(pr_number, repo="frappe/frappe"):
|
||||
req = urllib.request.Request(f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files")
|
||||
res = urllib.request.urlopen(req)
|
||||
dump = json.loads(res.read().decode('utf8'))
|
||||
return [change["filename"] for change in dump]
|
||||
|
||||
def get_output(command, shell=True):
|
||||
print(command)
|
||||
command = shlex.split(command)
|
||||
return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
|
||||
print(command)
|
||||
command = shlex.split(command)
|
||||
return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
|
||||
|
||||
def is_py(file):
|
||||
return file.endswith("py")
|
||||
return file.endswith("py")
|
||||
|
||||
def is_js(file):
|
||||
return file.endswith("js")
|
||||
def is_ci(file):
|
||||
return ".github" in file
|
||||
|
||||
def is_frontend_code(file):
|
||||
return file.endswith((".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts"))
|
||||
|
||||
def is_docs(file):
|
||||
regex = re.compile('\.(md|png|jpg|jpeg)$|^.github|LICENSE')
|
||||
return bool(regex.search(file))
|
||||
regex = re.compile(r'\.(md|png|jpg|jpeg|csv)$|^.github|LICENSE')
|
||||
return bool(regex.search(file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_type = os.environ.get("TYPE")
|
||||
before = os.environ.get("BEFORE")
|
||||
after = os.environ.get("AFTER")
|
||||
commit_range = before + '...' + after
|
||||
print("Build Type: {}".format(build_type))
|
||||
print("Commit Range: {}".format(commit_range))
|
||||
files_list = sys.argv[1:]
|
||||
build_type = os.environ.get("TYPE")
|
||||
pr_number = os.environ.get("PR_NUMBER")
|
||||
repo = os.environ.get("REPO_NAME")
|
||||
|
||||
try:
|
||||
files_changed = get_output("git diff --name-only {}".format(commit_range), shell=False)
|
||||
except Exception:
|
||||
sys.exit(2)
|
||||
if not files_list and pr_number:
|
||||
files_list = get_files_list(pr_number=pr_number, repo=repo)
|
||||
|
||||
if "fatal" not in files_changed:
|
||||
files_list = files_changed.split()
|
||||
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
||||
only_js_changed = len(list(filter(is_js, files_list))) == len(files_list)
|
||||
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
|
||||
if not files_list:
|
||||
print("No files' changes detected. Build is shutting")
|
||||
sys.exit(0)
|
||||
|
||||
if only_docs_changed:
|
||||
print("Only docs were updated, stopping build process.")
|
||||
sys.exit(0)
|
||||
ci_files_changed = any(f for f in files_list if is_ci(f))
|
||||
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
||||
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
|
||||
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
|
||||
|
||||
if only_js_changed and build_type == "server":
|
||||
print("Only JavaScript code was updated; Stopping Python build process.")
|
||||
sys.exit(0)
|
||||
if ci_files_changed:
|
||||
print("CI related files were updated, running all build processes.")
|
||||
|
||||
if only_py_changed and build_type == "ui":
|
||||
print("Only Python code was updated, stopping Cypress build process.")
|
||||
sys.exit(0)
|
||||
elif only_docs_changed:
|
||||
print("Only docs were updated, stopping build process.")
|
||||
sys.exit(0)
|
||||
|
||||
sys.exit(2)
|
||||
elif only_frontend_code_changed and build_type == "server":
|
||||
print("Only Frontend code was updated; Stopping Python build process.")
|
||||
sys.exit(0)
|
||||
|
||||
elif only_py_changed and build_type == "ui":
|
||||
print("Only Python code was updated, stopping Cypress build process.")
|
||||
sys.exit(0)
|
||||
|
||||
os.system('echo "::set-output name=build::strawberry"')
|
||||
|
|
|
|||
|
|
@ -98,8 +98,6 @@ rules:
|
|||
languages: [python]
|
||||
severity: WARNING
|
||||
paths:
|
||||
exclude:
|
||||
- test_*.py
|
||||
include:
|
||||
- "*/**/doctype/*"
|
||||
|
||||
|
|
|
|||
4
.github/helper/semgrep_rules/security.yml
vendored
4
.github/helper/semgrep_rules/security.yml
vendored
|
|
@ -8,10 +8,6 @@ rules:
|
|||
dynamic content. Avoid it or use safe_eval().
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
paths:
|
||||
exclude:
|
||||
- frappe/__init__.py
|
||||
- frappe/commands/utils.py
|
||||
|
||||
- id: frappe-sqli-format-strings
|
||||
patterns:
|
||||
|
|
|
|||
9
.github/helper/semgrep_rules/ux.js
vendored
Normal file
9
.github/helper/semgrep_rules/ux.js
vendored
Normal 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") }}. ');
|
||||
18
.github/helper/semgrep_rules/ux.py
vendored
18
.github/helper/semgrep_rules/ux.py
vendored
|
|
@ -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"))
|
||||
|
|
|
|||
23
.github/helper/semgrep_rules/ux.yml
vendored
23
.github/helper/semgrep_rules/ux.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
17
.github/semantic.yml
vendored
17
.github/semantic.yml
vendored
|
|
@ -11,3 +11,20 @@ allowRevertCommits: true
|
|||
|
||||
# For allowed PR types: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
|
||||
# Tool Reference: https://github.com/zeke/semantic-pull-requests
|
||||
|
||||
# By default types specified in commitizen/conventional-commit-types is used.
|
||||
# See: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
|
||||
# You can override the valid types
|
||||
types:
|
||||
- BREAKING CHANGE
|
||||
- feat
|
||||
- fix
|
||||
- docs
|
||||
- style
|
||||
- refactor
|
||||
- perf
|
||||
- test
|
||||
- build
|
||||
- ci
|
||||
- chore
|
||||
- revert
|
||||
|
|
|
|||
100
.github/workflows/patch-mariadb-tests.yml
vendored
Normal file
100
.github/workflows/patch-mariadb-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
name: Patch
|
||||
|
||||
on: [pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
|
||||
name: Patch Test
|
||||
|
||||
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
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
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
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
|
||||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
|
||||
- name: Run Patch Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
cd ~/frappe-bench/
|
||||
wget https://frappeframework.com/files/v10-frappe.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v10-frappe.sql.gz
|
||||
bench --site test_site migrate
|
||||
36
.github/workflows/semgrep.yml
vendored
36
.github/workflows/semgrep.yml
vendored
|
|
@ -1,34 +1,18 @@
|
|||
name: Semgrep
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-13-hotfix
|
||||
- version-13-pre-release
|
||||
pull_request: { }
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup semgrep
|
||||
run: |
|
||||
python -m pip install -q semgrep
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
|
||||
- name: Semgrep errors
|
||||
run: |
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
|
||||
semgrep --config="r/python.lang.correctness" --quiet --error $files
|
||||
|
||||
- name: Semgrep warnings
|
||||
run: |
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
|
||||
- uses: actions/checkout@v2
|
||||
- uses: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
.github/helper/semgrep_rules
|
||||
|
|
|
|||
24
.github/workflows/server-mariadb-tests.yml
vendored
24
.github/workflows/server-mariadb-tests.yml
vendored
|
|
@ -3,6 +3,8 @@ name: Server
|
|||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
|
@ -33,17 +35,29 @@ jobs:
|
|||
with:
|
||||
python-version: 3.7
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -53,6 +67,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -65,10 +80,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -77,6 +94,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -84,19 +102,22 @@ jobs:
|
|||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
|
||||
env:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Upload Coverage Data
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: upload-coverage-data
|
||||
run: |
|
||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
|
|
@ -113,6 +134,7 @@ jobs:
|
|||
coveralls:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
if: ${{ needs.test.steps.check-build.build == 'strawberry' }}
|
||||
container: python:3-slim
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
|
|
|
|||
18
.github/workflows/server-postgres-tests.yml
vendored
18
.github/workflows/server-postgres-tests.yml
vendored
|
|
@ -37,17 +37,29 @@ jobs:
|
|||
with:
|
||||
python-version: 3.7
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: '14'
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -57,6 +69,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -69,10 +82,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -81,6 +96,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -88,12 +104,14 @@ jobs:
|
|||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: postgres
|
||||
TYPE: server
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator
|
||||
env:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
|
|
|
|||
24
.github/workflows/ui-tests.yml
vendored
24
.github/workflows/ui-tests.yml
vendored
|
|
@ -3,6 +3,8 @@ name: UI
|
|||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
|
@ -33,17 +35,29 @@ jobs:
|
|||
with:
|
||||
python-version: 3.7
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "ui"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -53,6 +67,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -65,10 +80,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -77,6 +94,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Cache cypress binary
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache
|
||||
|
|
@ -86,6 +104,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -93,13 +112,18 @@ jobs:
|
|||
TYPE: ui
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: ui
|
||||
|
||||
- name: Site Setup
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard
|
||||
|
||||
- name: UI Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
|
||||
|
|
|
|||
14
.mergify.yml
14
.mergify.yml
|
|
@ -1,4 +1,18 @@
|
|||
pull_request_rules:
|
||||
- name: Auto-close PRs on stable branch
|
||||
conditions:
|
||||
- and:
|
||||
- author!=surajshetty3416
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
actions:
|
||||
close:
|
||||
comment:
|
||||
message: |
|
||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- status-success=Sider
|
||||
|
|
|
|||
|
|
@ -4,13 +4,10 @@
|
|||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
* @frappe/frappe-review-team
|
||||
website/ @prssanna
|
||||
web_form/ @prssanna
|
||||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @leela
|
||||
patches/ @surajshetty3416
|
||||
dashboard/ @prssanna
|
||||
email/ @leela
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
|
|
|
|||
21
README.md
21
README.md
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
BIN
cypress/fixtures/sample_image.jpg
Normal file
BIN
cypress/fixtures/sample_image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
|
|
@ -10,9 +10,9 @@ context('Awesome Bar', () => {
|
|||
});
|
||||
|
||||
it('navigates to doctype list', () => {
|
||||
cy.get('#navbar-search').type('todo', { delay: 200 });
|
||||
cy.get('#navbar-search + ul').should('be.visible');
|
||||
cy.get('#navbar-search').type('{downarrow}{enter}', { delay: 100 });
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('todo', { delay: 200 });
|
||||
cy.get('.awesomplete').findByRole('listbox').should('be.visible');
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{downarrow}{enter}', { delay: 100 });
|
||||
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
|
|
@ -20,24 +20,24 @@ context('Awesome Bar', () => {
|
|||
});
|
||||
|
||||
it('find text in doctype list', () => {
|
||||
cy.get('#navbar-search')
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('test in todo{downarrow}{enter}', { delay: 200 });
|
||||
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
cy.get('[data-original-title="Name"] > .input-with-feedback')
|
||||
cy.findByPlaceholderText('Name')
|
||||
.should('have.value', '%test%');
|
||||
});
|
||||
|
||||
it('navigates to new form', () => {
|
||||
cy.get('#navbar-search')
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('new blog post{downarrow}{enter}', { delay: 200 });
|
||||
|
||||
cy.get('.title-text:visible').should('have.text', 'New Blog Post');
|
||||
});
|
||||
|
||||
it('calculates math expressions', () => {
|
||||
cy.get('#navbar-search')
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('55 + 32{downarrow}{enter}', { delay: 200 });
|
||||
|
||||
cy.get('.modal-title').should('contain', 'Result');
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ context('Control Barcode', () => {
|
|||
it('should generate barcode on setting a value', () => {
|
||||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=barcode] input')
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.focus()
|
||||
.type('123456789')
|
||||
.blur();
|
||||
|
|
@ -37,11 +37,11 @@ context('Control Barcode', () => {
|
|||
it('should reset when input is cleared', () => {
|
||||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=barcode] input')
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.focus()
|
||||
.type('123456789')
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode] input')
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.clear()
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]')
|
||||
|
|
|
|||
50
cypress/integration/control_icon.js
Normal file
50
cypress/integration/control_icon.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
context('Control Icon', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_icon() {
|
||||
return cy.dialog({
|
||||
title: 'Icon',
|
||||
fields: [{
|
||||
label: 'Icon',
|
||||
fieldname: 'icon',
|
||||
fieldtype: 'Icon'
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it('should set icon', () => {
|
||||
get_dialog_with_icon().as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').click();
|
||||
|
||||
cy.get('.icon-picker .icon-wrapper[id=active]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'active');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('icon');
|
||||
expect(value).to.equal('active');
|
||||
});
|
||||
|
||||
cy.get('.icon-picker .icon-wrapper[id=resting]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'resting');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('icon');
|
||||
expect(value).to.equal('resting');
|
||||
});
|
||||
});
|
||||
|
||||
it('search for icon and clear search input', () => {
|
||||
let search_text = 'ed';
|
||||
cy.get('.icon-picker').findByRole('searchbox').click().type(search_text);
|
||||
cy.get('.icon-section .icon-wrapper:not(.hidden)').then(i => {
|
||||
cy.get(`.icon-section .icon-wrapper[id*='${search_text}']`).then(icons => {
|
||||
expect(i.length).to.equal(icons.length);
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('.icon-picker').findByRole('searchbox').clear().blur();
|
||||
cy.get('.icon-section .icon-wrapper').should('not.have.class', 'hidden');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -35,7 +35,7 @@ context('Control Link', () => {
|
|||
cy.wait('@search_link');
|
||||
cy.get('@input').type('todo for link', { delay: 200 });
|
||||
cy.wait('@search_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link]').findByRole('listbox').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
|
||||
cy.get('.frappe-control[data-fieldname=link] input').blur();
|
||||
cy.get('@dialog').then(dialog => {
|
||||
|
|
@ -71,7 +71,7 @@ context('Control Link', () => {
|
|||
cy.get('@input').type(todos[0]).blur();
|
||||
cy.wait('@validate_link');
|
||||
cy.get('@input').focus();
|
||||
cy.get('.frappe-control[data-fieldname=link] .link-btn')
|
||||
cy.findByTitle('Open Link')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.location('pathname').should('eq', `/app/todo/${todos[0]}`);
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ context('Control Select', () => {
|
|||
cy.get('@control').get('.select-icon').should('exist');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
|
||||
cy.get('@select').select('Option 1');
|
||||
cy.findByDisplayValue('Option 1').should('exist');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'none');
|
||||
cy.get('@select').invoke('val', '');
|
||||
cy.findByDisplayValue('Option 1').should('not.exist');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
|
||||
|
||||
|
||||
|
|
|
|||
63
cypress/integration/dashboard_links.js
Normal file
63
cypress/integration/dashboard_links.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
context('Dashboard links', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Adding a new contact, checking for the counter on the dashboard and deleting the created contact', () => {
|
||||
cy.visit('/app/contact');
|
||||
cy.clear_filters();
|
||||
|
||||
cy.visit('/app/user');
|
||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
|
||||
|
||||
//To check if initially the dashboard contains only the "Contact" link and there is no counter
|
||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
|
||||
|
||||
//Adding a new contact
|
||||
cy.get('.btn[data-doctype="Contact"]').click();
|
||||
cy.get('[data-doctype="Contact"][data-fieldname="first_name"]').type('Admin');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.visit('/app/user');
|
||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
|
||||
|
||||
//To check if the counter for contact doc is "1" after adding the contact
|
||||
cy.get('[data-doctype="Contact"] > .count').should('contain', '1');
|
||||
cy.get('[data-doctype="Contact"]').contains('Contact').click();
|
||||
|
||||
//Deleting the newly created contact
|
||||
cy.visit('/app/contact');
|
||||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click({delay: 700});
|
||||
|
||||
|
||||
//To check if the counter from the "Contact" doc link is removed
|
||||
cy.wait(700);
|
||||
cy.visit('/app/user');
|
||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
|
||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
|
||||
});
|
||||
|
||||
it('Report link in dashboard', () => {
|
||||
cy.visit('/app/user');
|
||||
cy.visit('/app/user/Administrator');
|
||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
|
||||
cy.findByText('Connections');
|
||||
cy.window()
|
||||
.its('cur_frm')
|
||||
.then(cur_frm => {
|
||||
cur_frm.dashboard.data.reports = [
|
||||
{
|
||||
'label': 'Reports',
|
||||
'items': ['Permitted Documents For User']
|
||||
}
|
||||
];
|
||||
cur_frm.dashboard.render_report_links();
|
||||
cy.get('[data-report="Permitted Documents For User"]').contains('Permitted Documents For User').click();
|
||||
cy.findByText('Permitted Documents For User');
|
||||
cy.findByPlaceholderText('User').should("have.value", "Administrator");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -62,11 +62,11 @@ context('Depends On', () => {
|
|||
it('should set the field as mandatory depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('test_field', 'Some Value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible');
|
||||
cy.hide_dialog();
|
||||
cy.fill_field('test_field', 'Random value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible');
|
||||
});
|
||||
it('should set the field as read only depending on other fields value', () => {
|
||||
|
|
@ -84,7 +84,7 @@ context('Depends On', () => {
|
|||
cy.fill_field('dependant_field', 'Some Value');
|
||||
//cy.fill_field('test_field', 'Some Other Value');
|
||||
cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').findByRole('button', {name: 'Add Row'}).click();
|
||||
cy.get('@table').find('[data-idx="1"]').as('row1');
|
||||
cy.get('@row1').find('.btn-open-row').click();
|
||||
cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid');
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ context('FileUploader', () => {
|
|||
|
||||
cy.get_open_dialog().find('.file-name').should('contain', 'example.json');
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-modal-primary').click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.statusCode').should('eq', 200);
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
|
@ -33,11 +33,11 @@ context('FileUploader', () => {
|
|||
it('should accept uploaded files', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('.btn-file-upload div:contains("Library")').click();
|
||||
cy.get('.file-filter').type('example.json');
|
||||
cy.get_open_dialog().find('.tree-label:contains("example.json")').first().click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Library'}).click();
|
||||
cy.findByPlaceholderText('Search by filename or extension').type('example.json');
|
||||
cy.get_open_dialog().findAllByText('example.json').first().click();
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.body.message')
|
||||
.should('have.property', 'file_name', 'example.json');
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
|
|
@ -46,12 +46,33 @@ context('FileUploader', () => {
|
|||
it('should accept web links', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('.btn-file-upload div:contains("Link")').click();
|
||||
cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Link'}).click();
|
||||
cy.get_open_dialog()
|
||||
.findByPlaceholderText('Attach a web link')
|
||||
.type('https://github.com', { delay: 100, force: true });
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.body.message')
|
||||
.should('have.property', 'file_url', 'https://github.com');
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
it('should allow cropping and optimization for valid images', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('.file-upload-area').attachFile('sample_image.jpg', {
|
||||
subjectType: 'drag-n-drop',
|
||||
});
|
||||
|
||||
cy.get_open_dialog().findAllByText('sample_image.jpg').should('exist');
|
||||
cy.get_open_dialog().find('.btn-crop').first().click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Crop'}).click();
|
||||
cy.get_open_dialog().findAllByRole('checkbox', {name: 'Optimize'}).should('exist');
|
||||
cy.get_open_dialog().findAllByLabelText('Optimize').first().click();
|
||||
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.statusCode').should('eq', 200);
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ context('Form', () => {
|
|||
cy.get('.primary-action').click();
|
||||
cy.wait('@form_save').its('response.statusCode').should('eq', 200);
|
||||
cy.visit('/app/todo');
|
||||
cy.wait(300);
|
||||
cy.get('.title-text').should('be.visible').and('contain', 'To Do');
|
||||
cy.get('.list-row').should('contain', 'this is a test todo');
|
||||
});
|
||||
|
|
@ -25,7 +26,7 @@ context('Form', () => {
|
|||
cy.visit('/app/contact');
|
||||
cy.add_filter();
|
||||
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true });
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.findByRole('button', {name: 'Apply Filters'}).click({ force: true });
|
||||
cy.visit('/app/contact/Test Form Contact 3');
|
||||
cy.get('.prev-doc').should('be.visible').click();
|
||||
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
|
||||
|
|
|
|||
88
cypress/integration/form_tour.js
Normal file
88
cypress/integration/form_tour.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
context('Form Tour', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/form-tour');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call("frappe.tests.ui_test_helpers.create_form_tour");
|
||||
});
|
||||
});
|
||||
|
||||
const open_test_form_tour = () => {
|
||||
cy.visit('/app/form-tour/Test Form Tour');
|
||||
cy.findByRole('button', {name: 'Show Tour'}).should('be.visible').as('show_tour');
|
||||
cy.get('@show_tour').click();
|
||||
cy.wait(500);
|
||||
cy.url().should('include', '/app/contact');
|
||||
};
|
||||
|
||||
it('jump to a form tour', open_test_form_tour);
|
||||
|
||||
it('navigates a form tour', () => {
|
||||
open_test_form_tour();
|
||||
|
||||
cy.get('.frappe-driver').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname="first_name"]').as('first_name');
|
||||
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
|
||||
cy.get('.frappe-driver').findByRole('button', {name: 'Next'}).as('next_btn');
|
||||
|
||||
// next btn shouldn't move to next step, if first name is not entered
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// after filling the field, next step should be highlighted
|
||||
cy.fill_field('first_name', 'Test Name', 'Data');
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert field is highlighted
|
||||
cy.get('.frappe-control[data-fieldname="last_name"]').as('last_name');
|
||||
cy.get('@last_name').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// after filling the field, next step should be highlighted
|
||||
cy.fill_field('last_name', 'Test Last Name', 'Data');
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert field is highlighted
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('phone_nos');
|
||||
cy.get('@phone_nos').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// move to next step
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert add row btn is highlighted
|
||||
cy.get('@phone_nos').find('.grid-add-row').as('add_row');
|
||||
cy.get('@add_row').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// add a row & move to next step
|
||||
cy.wait(500);
|
||||
cy.get('@add_row').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert table field is highlighted
|
||||
cy.get('.grid-row-open .frappe-control[data-fieldname="phone"]').as('phone');
|
||||
cy.get('@phone').should('have.class', 'driver-highlighted-element');
|
||||
// enter value in a table field
|
||||
let field = cy.fill_table_field('phone_nos', '1', 'phone', '1234567890');
|
||||
field.blur();
|
||||
|
||||
// move to collapse row step
|
||||
cy.wait(500);
|
||||
cy.get('.driver-popover-title').contains('Test Title 4').siblings().get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
// collapse row
|
||||
cy.get('.grid-row-open .grid-collapse-row').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert save btn is highlighted
|
||||
cy.get('.primary-action').should('have.class', 'driver-highlighted-element');
|
||||
cy.wait(500);
|
||||
cy.get('.frappe-driver').findByRole('button', {name: 'Save'}).should('be.visible');
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -30,12 +30,12 @@ context('Grid Pagination', () => {
|
|||
it('adds and deletes rows and changes page', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').findByRole('button', {name: 'Add Row'}).click();
|
||||
cy.get('@table').find('.grid-body .row-index').should('contain', 1001);
|
||||
cy.get('@table').find('.current-page-number').should('contain', '21');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '21');
|
||||
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({ force: true });
|
||||
cy.get('@table').find('button.grid-remove-rows').click();
|
||||
cy.get('@table').findByRole('button', {name: 'Delete'}).click();
|
||||
cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000);
|
||||
cy.get('@table').find('.current-page-number').should('contain', '20');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ context('List View Settings', () => {
|
|||
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'DocType Settings');
|
||||
|
||||
cy.get('input[data-fieldname="disable_count"]').check({ force: true });
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({ force: true });
|
||||
cy.get('button').filter(':visible').contains('Save').click();
|
||||
cy.findByLabelText('Disable Count').check({ force: true });
|
||||
cy.findByLabelText('Disable Sidebar Stats').check({ force: true });
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
|
||||
cy.reload({ force: true });
|
||||
|
||||
|
|
@ -29,8 +29,8 @@ context('List View Settings', () => {
|
|||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'DocType Settings');
|
||||
cy.get('input[data-fieldname="disable_count"]').uncheck({ force: true });
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({ force: true });
|
||||
cy.get('button').filter(':visible').contains('Save').click();
|
||||
cy.findByLabelText('Disable Count').uncheck({ force: true });
|
||||
cy.findByLabelText('Disable Sidebar Stats').uncheck({ force: true });
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ context('Login', () => {
|
|||
|
||||
it('validates password', () => {
|
||||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
it('validates email', () => {
|
||||
cy.get('#login_password').type('qwe');
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
|
|
@ -25,8 +25,8 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type('qwer');
|
||||
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.get('.btn-login:visible').contains('Invalid Login. Try again.');
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.findByRole('button', {name: 'Invalid Login. Try again.'}).should('exist');
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.location('pathname').should('eq', '/app');
|
||||
cy.window().its('frappe.session.user').should('eq', 'Administrator');
|
||||
});
|
||||
|
|
@ -60,7 +60,7 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
|
||||
// verify redirected location and url params after login
|
||||
cy.url().should('include', '/me?' + payload.toString().replace('+', '%20'));
|
||||
|
|
|
|||
14
cypress/integration/navigation.js
Normal file
14
cypress/integration/navigation.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
context('Navigation', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
it('Navigate to route with hash in document name', () => {
|
||||
cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true});
|
||||
cy.visit('/app/todo/ABC#123');
|
||||
cy.title().should('eq', 'Test this - ABC#123');
|
||||
cy.get_field('description', 'Text Editor').contains('Test this');
|
||||
cy.go('back');
|
||||
cy.title().should('eq', 'Website');
|
||||
});
|
||||
});
|
||||
|
|
@ -16,24 +16,24 @@ context('Recorder', () => {
|
|||
it('Navigate to Recorder', () => {
|
||||
cy.visit('/app');
|
||||
cy.awesomebar('recorder');
|
||||
cy.get('h3').should('contain', 'Recorder');
|
||||
cy.findByTitle('Recorder').should('exist');
|
||||
cy.url().should('include', '/recorder/detail');
|
||||
});
|
||||
|
||||
it('Recorder Empty State', () => {
|
||||
cy.get('.title-text').should('contain', 'Recorder');
|
||||
cy.findByTitle('Recorder').should('exist');
|
||||
|
||||
cy.get('.indicator-pill').should('contain', 'Inactive').should('have.class', 'red');
|
||||
|
||||
cy.get('.primary-action').should('contain', 'Start');
|
||||
cy.get('.btn-secondary').should('contain', 'Clear');
|
||||
cy.findByRole('button', {name: 'Start'}).should('exist');
|
||||
cy.findByRole('button', {name: 'Clear'}).should('exist');
|
||||
|
||||
cy.get('.msg-box').should('contain', 'Inactive');
|
||||
cy.get('.msg-box .btn-primary').should('contain', 'Start Recording');
|
||||
cy.findByRole('button', {name: 'Start Recording'}).should('exist');
|
||||
});
|
||||
|
||||
it('Recorder Start', () => {
|
||||
cy.get('.primary-action').should('contain', 'Start').click();
|
||||
cy.findByRole('button', {name: 'Start'}).click();
|
||||
cy.get('.indicator-pill').should('contain', 'Active').should('have.class', 'green');
|
||||
|
||||
cy.get('.msg-box').should('contain', 'No Requests');
|
||||
|
|
@ -46,12 +46,12 @@ context('Recorder', () => {
|
|||
cy.get('.list-count').should('contain', '20 of ');
|
||||
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.title-text').should('contain', 'Recorder');
|
||||
cy.findByTitle('Recorder').should('exist');
|
||||
cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
|
||||
});
|
||||
|
||||
it('Recorder View Request', () => {
|
||||
cy.get('.primary-action').should('contain', 'Start').click();
|
||||
cy.findByRole('button', {name: 'Start'}).click();
|
||||
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ context('Report View', () => {
|
|||
let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
|
||||
// select the cell
|
||||
cell.dblclick();
|
||||
cell.find('input[data-fieldname="enabled"]').check({ force: true });
|
||||
cell.findByRole('checkbox').check({ force: true });
|
||||
cy.get('.dt-row-0 > .dt-cell--col-5').click();
|
||||
cy.wait('@value-update');
|
||||
cy.get('@doc').then(doc => {
|
||||
|
|
|
|||
57
cypress/integration/sidebar.js
Normal file
57
cypress/integration/sidebar.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
context('Sidebar', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/doctype');
|
||||
});
|
||||
|
||||
it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => {
|
||||
cy.click_sidebar_button(0);
|
||||
|
||||
//To check if no filter is available in "Assigned To" dropdown
|
||||
cy.get('.empty-state').should('contain', 'No filters found');
|
||||
|
||||
cy.click_sidebar_button(1);
|
||||
|
||||
//To check if "Created By" dropdown contains filter
|
||||
cy.get('.group-by-item > .dropdown-item').should('contain', 'Me');
|
||||
|
||||
//Assigning a doctype to a user
|
||||
cy.click_listview_row_item(0);
|
||||
cy.get('.form-assignments > .flex > .text-muted').click();
|
||||
cy.get_field('assign_to_me', 'Check').click();
|
||||
cy.get('.modal-footer > .standard-actions > .btn-primary').click();
|
||||
cy.visit('/app/doctype');
|
||||
cy.click_sidebar_button(0);
|
||||
|
||||
//To check if filter is added in "Assigned To" dropdown after assignment
|
||||
cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').should('contain', '1');
|
||||
|
||||
//To check if there is no filter added to the listview
|
||||
cy.get('.filter-selector > .btn').should('contain', 'Filter');
|
||||
|
||||
//To add a filter to display data into the listview
|
||||
cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').click();
|
||||
|
||||
//To check if filter is applied
|
||||
cy.click_filter_button().should('contain', '1 filter');
|
||||
cy.get('.fieldname-select-area > .awesomplete > .form-control').should('have.value', 'Assigned To');
|
||||
cy.get('.condition').should('have.value', 'like');
|
||||
cy.get('.filter-field > .form-group > .input-with-feedback').should('have.value', '%Administrator%');
|
||||
|
||||
//To remove the applied filter
|
||||
cy.get('.filter-action-buttons > div > .btn-secondary').contains('Clear Filters').click();
|
||||
cy.click_filter_button();
|
||||
cy.get('.filter-selector > .btn').should('contain', 'Filter');
|
||||
|
||||
//To remove the assignment
|
||||
cy.visit('/app/doctype');
|
||||
cy.click_listview_row_item(0);
|
||||
cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click();
|
||||
cy.get('.remove-btn').click({force: true});
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-header > .modal-actions > .btn-modal-close').click();
|
||||
cy.visit('/app/doctype');
|
||||
cy.click_sidebar_button(0);
|
||||
cy.get('.empty-state').should('contain', 'No filters found');
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ context('Table MultiSelect', () => {
|
|||
cy.new_form('Assignment Rule');
|
||||
cy.fill_field('__newname', name);
|
||||
cy.fill_field('document_type', 'Blog Post');
|
||||
cy.get('.section-head').contains('Assignment Rules').scrollIntoView();
|
||||
cy.fill_field('assign_condition', 'status=="Open"', 'Code');
|
||||
cy.get('input[data-fieldname="users"]').focus().as('input');
|
||||
cy.get('input[data-fieldname="users"] + ul').should('be.visible');
|
||||
|
|
|
|||
94
cypress/integration/timeline.js
Normal file
94
cypress/integration/timeline.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import custom_submittable_doctype from '../fixtures/custom_submittable_doctype';
|
||||
|
||||
context('Timeline', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/todo');
|
||||
});
|
||||
|
||||
it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => {
|
||||
//Adding new ToDo
|
||||
cy.click_listview_primary_button('Add ToDo');
|
||||
cy.findByRole('button', {name: 'Edit in full page'}).click();
|
||||
cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true});
|
||||
cy.wait(200);
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.wait(700);
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.level-item.ellipsis').eq(0).click();
|
||||
|
||||
//To check if the comment box is initially empty and tying some text into it
|
||||
cy.get('[data-fieldname="comment"] .ql-editor').should('contain', '').type('Testing Timeline');
|
||||
|
||||
//Adding new comment
|
||||
cy.findByRole('button', {name: 'Comment'}).click();
|
||||
|
||||
//To check if the commented text is visible in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline');
|
||||
|
||||
//Editing comment
|
||||
cy.click_timeline_action_btn(0);
|
||||
cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(' 123');
|
||||
cy.click_timeline_action_btn(0);
|
||||
|
||||
//To check if the edited comment text is visible in timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
|
||||
|
||||
//Discarding comment
|
||||
cy.click_timeline_action_btn(0);
|
||||
cy.findByRole('button', {name: 'Dismiss'}).click();
|
||||
|
||||
//To check if after discarding the timeline content is same as previous
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
|
||||
|
||||
//Deleting the added comment
|
||||
cy.get('.actions > .btn > .icon').first().click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
|
||||
//Deleting the added ToDo
|
||||
cy.get('.menu-btn-group button').eq(1).click();
|
||||
cy.get('.menu-btn-group [data-label="Delete"]').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
});
|
||||
|
||||
it('Timeline should have submit and cancel activity information', () => {
|
||||
cy.visit('/app/doctype');
|
||||
|
||||
//Creating custom doctype
|
||||
cy.insert_doc('DocType', custom_submittable_doctype, true);
|
||||
|
||||
cy.visit('/app/custom-submittable-doctype');
|
||||
cy.click_listview_primary_button('Add Custom Submittable DocType');
|
||||
|
||||
//Adding a new entry for the created custom doctype
|
||||
cy.fill_field('title', 'Test');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.findByRole('button', {name: 'Submit'}).click();
|
||||
cy.visit('/app/custom-submittable-doctype');
|
||||
cy.get('.list-subject > .bold > .ellipsis').eq(0).click();
|
||||
|
||||
//To check if the submission of the documemt is visible in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Administrator submitted this document');
|
||||
cy.findByRole('button', {name: 'Cancel'}).click({delay: 900});
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
|
||||
//To check if the cancellation of the documemt is visible in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Administrator cancelled this document');
|
||||
|
||||
//Deleting the document
|
||||
cy.visit('/app/custom-submittable-doctype');
|
||||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group > .dropdown-menu > li > .grey-link').eq(7).click();
|
||||
cy.click_modal_primary_button('Yes', {force: true, delay: 700});
|
||||
|
||||
//Deleting the custom doctype
|
||||
cy.visit('/app/doctype');
|
||||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
});
|
||||
});
|
||||
70
cypress/integration/timeline_email.js
Normal file
70
cypress/integration/timeline_email.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
context('Timeline Email', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/todo');
|
||||
});
|
||||
|
||||
it('Adding new ToDo, adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => {
|
||||
//Adding new ToDo
|
||||
cy.click_listview_primary_button('Add ToDo');
|
||||
cy.get('.custom-actions:visible > .btn').contains("Edit in full page").click({delay: 500});
|
||||
cy.fill_field("description", "Test ToDo", "Text Editor");
|
||||
cy.wait(500);
|
||||
cy.get('.primary-action').contains('Save').click({force: true});
|
||||
cy.wait(700);
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject').eq(0).click();
|
||||
|
||||
//Creating a new email
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-body > :nth-child(1) > .form-layout > .form-page > :nth-child(3) > .section-body > .form-column > form > [data-fieldtype="Text Editor"] > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').type('Test Mail');
|
||||
|
||||
//Adding attachment to the email
|
||||
cy.get('.add-more-attachments > .btn').click();
|
||||
cy.get('.mt-2 > .btn > .mt-1').eq(2).click();
|
||||
cy.get('.input-group > .form-control').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
|
||||
cy.get('.btn-primary').contains('Upload').click();
|
||||
|
||||
//Sending the email
|
||||
cy.click_modal_primary_button('Send', {delay: 500});
|
||||
|
||||
//To check if the sent mail content is shown in the timeline content
|
||||
cy.get('[data-doctype="Communication"] > .timeline-content').should('contain', 'Test Mail');
|
||||
|
||||
//To check if the attachment of email is shown in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Added 72402.jpg');
|
||||
|
||||
//Deleting the sent email
|
||||
cy.get('[title="Open Communication"] > .icon').first().click({force: true});
|
||||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click();
|
||||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click();
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click();
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
|
||||
|
||||
//Removing the added attachment
|
||||
cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click();
|
||||
cy.get('.modal-footer:visible > .standard-actions > .btn-primary').contains('Yes').click();
|
||||
|
||||
//To check if the removed attachment is shown in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Removed 72402.jpg');
|
||||
cy.wait(500);
|
||||
|
||||
//To check if the discard button functionality in email is working correctly
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal-footer > .standard-actions > .btn-secondary').contains('Discard').click();
|
||||
cy.wait(500);
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.wait(500);
|
||||
cy.get_field('recipients', 'MultiSelect').should('have.text', '');
|
||||
cy.get('.modal-header:visible > .modal-actions > .btn-modal-close > .icon').click();
|
||||
|
||||
//Deleting the added ToDo
|
||||
cy.get('.menu-btn-group:visible > .btn').click();
|
||||
cy.get('.menu-btn-group:visible > .dropdown-menu > li > .dropdown-item').contains('Delete').click();
|
||||
cy.get('.modal-footer:visible > .standard-actions > .btn-primary').click();
|
||||
});
|
||||
});
|
||||
90
cypress/integration/workspace.js
Normal file
90
cypress/integration/workspace.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
context('Workspace 2.0', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Navigate to page from sidebar', () => {
|
||||
cy.visit('/app/build');
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.sidebar-item-container[item-name="Settings"]').first().click();
|
||||
cy.location('pathname').should('eq', '/app/settings');
|
||||
});
|
||||
|
||||
it('Create Private Page', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
|
||||
cy.fill_field('title', 'Test Private Page', 'Data');
|
||||
cy.fill_field('icon', 'edit', 'Icon');
|
||||
cy.get_open_dialog().find('.modal-header').click();
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
|
||||
// check if sidebar item is added in pubic section
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
cy.wait(300);
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.wait(500);
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
});
|
||||
|
||||
it('Add New Block', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click();
|
||||
cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Heading').click();
|
||||
cy.get(":focus").type('Header');
|
||||
cy.get(".ce-block:last").find('.ce-header').should('exist');
|
||||
|
||||
cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click();
|
||||
cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Text').click();
|
||||
cy.get(":focus").type('Paragraph text');
|
||||
cy.get(".ce-block:last").find('.ce-paragraph').should('exist');
|
||||
});
|
||||
|
||||
it('Delete A Block', () => {
|
||||
cy.get(".ce-block:last").find('.delete-paragraph').click();
|
||||
cy.get(".ce-block:last").find('.ce-paragraph').should('not.exist');
|
||||
});
|
||||
|
||||
it('Shrink and Expand A Block', () => {
|
||||
cy.get(".ce-block:last").find('.tune-btn').click();
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-11');
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-10');
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-9');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-10');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-11');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-12');
|
||||
});
|
||||
|
||||
it('Change Header Text Size', () => {
|
||||
cy.get('.ce-settings--opened .cdx-settings-button[data-level="3"]').click();
|
||||
cy.get(".ce-block:last").find('.widget-head h3').should('exist');
|
||||
cy.get('.ce-settings--opened .cdx-settings-button[data-level="4"]').click();
|
||||
cy.get(".ce-block:last").find('.widget-head h4').should('exist');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
});
|
||||
|
||||
it('Delete Private Page', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').find('.sidebar-item-control .delete-page').click();
|
||||
cy.wait(300);
|
||||
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import 'cypress-file-upload';
|
||||
import '@testing-library/cypress/add-commands';
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
|
|
@ -192,16 +193,16 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
|
||||
let selector = `.form-control[data-fieldname="${fieldname}"]`;
|
||||
let selector = `[data-fieldname="${fieldname}"] input:visible`;
|
||||
|
||||
if (fieldtype === 'Text Editor') {
|
||||
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
|
||||
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]:visible`;
|
||||
}
|
||||
if (fieldtype === 'Code') {
|
||||
selector = `[data-fieldname="${fieldname}"] .ace_text-input`;
|
||||
}
|
||||
|
||||
return cy.get(selector);
|
||||
return cy.get(selector).first();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
|
||||
|
|
@ -323,4 +324,30 @@ Cypress.Commands.add('clear_filters', () => {
|
|||
cy.window().its('cur_list').then(cur_list => {
|
||||
cur_list && cur_list.filter_area && cur_list.filter_area.clear();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_modal_primary_button', (btn_name) => {
|
||||
cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_sidebar_button', (btn_no) => {
|
||||
cy.get('.list-group-by-fields > .group-by-field > .btn').eq(btn_no).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_listview_row_item', (row_no) => {
|
||||
cy.get('.list-row > .level-left > .list-subject > .bold > .ellipsis').eq(row_no).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_filter_button', () => {
|
||||
cy.get('.filter-selector > .btn').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
|
||||
cy.get('.primary-action').contains(btn_name).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_timeline_action_btn', (btn_no) => {
|
||||
cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').eq(btn_no).first().click();
|
||||
});
|
||||
|
|
@ -8,6 +8,7 @@ let yargs = require("yargs");
|
|||
let cliui = require("cliui")();
|
||||
let chalk = require("chalk");
|
||||
let html_plugin = require("./frappe-html");
|
||||
let rtlcss = require('rtlcss');
|
||||
let postCssPlugin = require("esbuild-plugin-postcss2").default;
|
||||
let ignore_assets = require("./ignore-assets");
|
||||
let sass_options = require("./sass_options");
|
||||
|
|
@ -96,9 +97,9 @@ async function execute() {
|
|||
await clean_dist_folders(APPS);
|
||||
}
|
||||
|
||||
let result;
|
||||
let results;
|
||||
try {
|
||||
result = await build_assets_for_apps(APPS, FILES_TO_BUILD);
|
||||
results = await build_assets_for_apps(APPS, FILES_TO_BUILD);
|
||||
} catch (e) {
|
||||
log_error("There were some problems during build");
|
||||
log();
|
||||
|
|
@ -107,13 +108,15 @@ async function execute() {
|
|||
}
|
||||
|
||||
if (!WATCH_MODE) {
|
||||
log_built_assets(result.metafile);
|
||||
log_built_assets(results);
|
||||
console.timeEnd(TOTAL_BUILD_TIME);
|
||||
log();
|
||||
} else {
|
||||
log("Watching for changes...");
|
||||
}
|
||||
return await write_assets_json(result.metafile);
|
||||
for (const result of results) {
|
||||
await write_assets_json(result.metafile);
|
||||
}
|
||||
}
|
||||
|
||||
function build_assets_for_apps(apps, files) {
|
||||
|
|
@ -125,6 +128,8 @@ function build_assets_for_apps(apps, files) {
|
|||
let output_path = assets_path;
|
||||
|
||||
let file_map = {};
|
||||
let style_file_map = {};
|
||||
let rtl_style_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];
|
||||
|
|
@ -140,19 +145,32 @@ function build_assets_for_apps(apps, files) {
|
|||
}
|
||||
output_name = path.join(app, "dist", output_name);
|
||||
|
||||
if (Object.keys(file_map).includes(output_name)) {
|
||||
if (Object.keys(file_map).includes(output_name) || Object.keys(style_file_map).includes(output_name)) {
|
||||
log_warn(
|
||||
`Duplicate output file ${output_name} generated from ${file}`
|
||||
);
|
||||
}
|
||||
|
||||
file_map[output_name] = file;
|
||||
if ([".css", ".scss", ".less", ".sass", ".styl"].includes(extension)) {
|
||||
style_file_map[output_name] = file;
|
||||
rtl_style_file_map[output_name.replace('/css/', '/css-rtl/')] = file;
|
||||
} else {
|
||||
file_map[output_name] = file;
|
||||
}
|
||||
}
|
||||
|
||||
return build_files({
|
||||
let build = build_files({
|
||||
files: file_map,
|
||||
outdir: output_path
|
||||
});
|
||||
let style_build = build_style_files({
|
||||
files: style_file_map,
|
||||
outdir: output_path
|
||||
});
|
||||
let rtl_style_build = build_style_files({
|
||||
files: rtl_style_file_map,
|
||||
outdir: output_path,
|
||||
rtl_style: true
|
||||
});
|
||||
return Promise.all([build, style_build, rtl_style_build]);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +221,33 @@ function get_files_to_build(files) {
|
|||
}
|
||||
|
||||
function build_files({ files, outdir }) {
|
||||
return esbuild.build({
|
||||
let build_plugins = [
|
||||
html_plugin,
|
||||
vue(),
|
||||
];
|
||||
return esbuild.build(get_build_options(files, outdir, build_plugins));
|
||||
}
|
||||
|
||||
function build_style_files({ files, outdir, rtl_style=false }) {
|
||||
let plugins = [];
|
||||
if (rtl_style) {
|
||||
plugins.push(rtlcss);
|
||||
}
|
||||
|
||||
let build_plugins = [
|
||||
ignore_assets,
|
||||
postCssPlugin({
|
||||
plugins: plugins,
|
||||
sassOptions: sass_options
|
||||
})
|
||||
];
|
||||
|
||||
plugins.push(require("autoprefixer"));
|
||||
return esbuild.build(get_build_options(files, outdir, build_plugins));
|
||||
}
|
||||
|
||||
function get_build_options(files, outdir, plugins) {
|
||||
return {
|
||||
entryPoints: files,
|
||||
entryNames: "[dir]/[name].[hash]",
|
||||
outdir,
|
||||
|
|
@ -217,17 +261,9 @@ function build_files({ files, outdir }) {
|
|||
PRODUCTION ? "production" : "development"
|
||||
)
|
||||
},
|
||||
plugins: [
|
||||
html_plugin,
|
||||
ignore_assets,
|
||||
vue(),
|
||||
postCssPlugin({
|
||||
plugins: [require("autoprefixer")],
|
||||
sassOptions: sass_options
|
||||
})
|
||||
],
|
||||
plugins: plugins,
|
||||
watch: get_watch_config()
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function get_watch_config() {
|
||||
|
|
@ -258,16 +294,26 @@ function get_watch_config() {
|
|||
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
|
||||
});
|
||||
let paths = [
|
||||
path.resolve(public_path, "dist", "js"),
|
||||
path.resolve(public_path, "dist", "css"),
|
||||
path.resolve(public_path, "dist", "css-rtl")
|
||||
];
|
||||
for (let target of paths) {
|
||||
if (fs.existsSync(target)) {
|
||||
// rmdir is deprecated in node 16, this will work in both node 14 and 16
|
||||
let rmdir = fs.promises.rm || fs.promises.rmdir;
|
||||
await rmdir(target, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function log_built_assets(metafile) {
|
||||
function log_built_assets(results) {
|
||||
let outputs = {};
|
||||
for (const result of results) {
|
||||
outputs = Object.assign(outputs, result.metafile.outputs);
|
||||
}
|
||||
let column_widths = [60, 20];
|
||||
cliui.div(
|
||||
{
|
||||
|
|
@ -282,9 +328,9 @@ function log_built_assets(metafile) {
|
|||
cliui.div("");
|
||||
|
||||
let output_by_dist_path = {};
|
||||
for (let outfile in metafile.outputs) {
|
||||
for (let outfile in outputs) {
|
||||
if (outfile.endsWith(".map")) continue;
|
||||
let data = metafile.outputs[outfile];
|
||||
let data = outputs[outfile];
|
||||
outfile = path.resolve(outfile);
|
||||
outfile = path.relative(assets_path, outfile);
|
||||
let filename = path.basename(outfile);
|
||||
|
|
@ -339,16 +385,15 @@ async function write_assets_json(metafile) {
|
|||
let info = metafile.outputs[output];
|
||||
let asset_path = "/" + path.relative(sites_path, output);
|
||||
if (info.entryPoint) {
|
||||
out[path.basename(info.entryPoint)] = asset_path;
|
||||
let key = path.basename(info.entryPoint);
|
||||
if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) {
|
||||
key = `rtl_${key}`;
|
||||
}
|
||||
out[key] = asset_path;
|
||||
}
|
||||
}
|
||||
|
||||
let assets_json_path = path.resolve(
|
||||
assets_path,
|
||||
"frappe",
|
||||
"dist",
|
||||
"assets.json"
|
||||
);
|
||||
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");
|
||||
|
|
@ -483,4 +528,4 @@ function log_rebuilt_assets(prev_assets, new_assets) {
|
|||
log(" " + filename);
|
||||
}
|
||||
log();
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ if _dev_server:
|
|||
from werkzeug.local import Local, release_local
|
||||
import sys, importlib, inspect, json
|
||||
import typing
|
||||
from past.builtins import cmp
|
||||
import click
|
||||
|
||||
# Local application imports
|
||||
|
|
@ -29,6 +28,8 @@ from .exceptions import *
|
|||
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
|
||||
from .utils.lazy_loader import lazy_import
|
||||
|
||||
from frappe.query_builder import get_query_builder, patch_query_execute
|
||||
|
||||
# Lazy imports
|
||||
faker = lazy_import('faker')
|
||||
|
||||
|
|
@ -119,6 +120,7 @@ def set_user_lang(user, user_language=None):
|
|||
|
||||
# local-globals
|
||||
db = local("db")
|
||||
qb = local("qb")
|
||||
conf = local("conf")
|
||||
form = form_dict = local("form_dict")
|
||||
request = local("request")
|
||||
|
|
@ -203,8 +205,10 @@ def init(site, sites_path=None, new_site=False):
|
|||
local.form_dict = _dict()
|
||||
local.session = _dict()
|
||||
local.dev_server = _dev_server
|
||||
local.qb = get_query_builder(local.conf.db_type or "mariadb")
|
||||
|
||||
setup_module_map()
|
||||
patch_query_execute()
|
||||
|
||||
local.initialised = True
|
||||
|
||||
|
|
@ -528,16 +532,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 = []
|
||||
|
|
@ -1107,9 +1115,7 @@ def setup_module_map():
|
|||
|
||||
if not (local.app_modules and local.module_app):
|
||||
local.module_app, local.app_modules = {}, {}
|
||||
for app in get_all_apps(True):
|
||||
if app == "webnotes":
|
||||
app = "frappe"
|
||||
for app in get_all_apps(with_internal_apps=True):
|
||||
local.app_modules.setdefault(app, [])
|
||||
for module in get_module_list(app):
|
||||
module = scrub(module)
|
||||
|
|
@ -1490,7 +1496,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
|
|||
:param style: Print Format style.
|
||||
:param as_pdf: Return as PDF. Default False.
|
||||
:param password: Password to encrypt the pdf with. Default None"""
|
||||
from frappe.website.render import build_page
|
||||
from frappe.website.serve import get_response_content
|
||||
from frappe.utils.pdf import get_pdf
|
||||
|
||||
local.form_dict.doctype = doctype
|
||||
|
|
@ -1505,7 +1511,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
|
|||
options = {'password': password}
|
||||
|
||||
if not html:
|
||||
html = build_page("printview")
|
||||
html = get_response_content("printview")
|
||||
|
||||
if as_pdf:
|
||||
return get_pdf(html, output = output, options = options)
|
||||
|
|
@ -1682,7 +1688,7 @@ def get_desk_link(doctype, name):
|
|||
)
|
||||
|
||||
def bold(text):
|
||||
return '<b>{0}</b>'.format(text)
|
||||
return '<strong>{0}</strong>'.format(text)
|
||||
|
||||
def safe_eval(code, eval_globals=None, eval_locals=None):
|
||||
'''A safer `eval`'''
|
||||
|
|
@ -1693,6 +1699,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)))
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
@ -82,7 +82,7 @@ def handle():
|
|||
if frappe.local.request.method=="PUT":
|
||||
data = get_request_form_data()
|
||||
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
doc = frappe.get_doc(doctype, name, for_update=True)
|
||||
|
||||
if "flags" in data:
|
||||
del data["flags"]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -18,9 +16,9 @@ import frappe.handler
|
|||
import frappe.auth
|
||||
import frappe.api
|
||||
import frappe.utils.response
|
||||
import frappe.website.render
|
||||
from frappe.utils import get_site_name, sanitize_html
|
||||
from frappe.middlewares import StaticDataMiddleware
|
||||
from frappe.website.serve import get_response
|
||||
from frappe.utils.error import make_error_snapshot
|
||||
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
|
||||
from frappe import _
|
||||
|
|
@ -74,7 +72,7 @@ def application(request):
|
|||
response = frappe.utils.response.download_private_file(request.path)
|
||||
|
||||
elif request.method in ('GET', 'HEAD', 'POST'):
|
||||
response = frappe.website.render.render()
|
||||
response = get_response()
|
||||
|
||||
else:
|
||||
raise NotFound
|
||||
|
|
@ -191,8 +189,9 @@ def make_form_dict(request):
|
|||
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)
|
||||
|
||||
|
|
@ -267,8 +266,7 @@ def handle_exception(e):
|
|||
make_error_snapshot(e)
|
||||
|
||||
if return_as_message:
|
||||
response = frappe.website.render.render("message",
|
||||
http_status_code=http_status_code)
|
||||
response = get_response("message", http_status_code=http_status_code)
|
||||
|
||||
return response
|
||||
|
||||
|
|
|
|||
114
frappe/auth.py
114
frappe/auth.py
|
|
@ -1,35 +1,58 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See LICENSE
|
||||
from urllib.parse import quote
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import datetime
|
||||
|
||||
from frappe import _
|
||||
import frappe
|
||||
import frappe.database
|
||||
import frappe.utils
|
||||
from frappe.utils import cint, flt, get_datetime, datetime, date_diff, today
|
||||
import frappe.utils.user
|
||||
from frappe import conf
|
||||
from frappe.sessions import Session, clear_sessions, delete_session
|
||||
from frappe.modules.patch_handler import check_session_stopped
|
||||
from frappe.translate import get_lang_code
|
||||
from frappe.utils.password import check_password, delete_login_failed_cache
|
||||
from frappe import _, conf
|
||||
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.modules.patch_handler import check_session_stopped
|
||||
from frappe.sessions import Session, clear_sessions, delete_session
|
||||
from frappe.translate import get_language
|
||||
from frappe.twofactor import authenticate_for_2factor, confirm_otp_token, get_cached_user_pass, should_run_2fa
|
||||
from frappe.utils import cint, date_diff, datetime, get_datetime, today
|
||||
from frappe.utils.password import check_password
|
||||
from frappe.website.utils import get_home_page
|
||||
|
||||
from six.moves.urllib.parse import quote
|
||||
|
||||
|
||||
class HTTPRequest:
|
||||
def __init__(self):
|
||||
# Get Environment variables
|
||||
self.domain = frappe.request.host
|
||||
if self.domain and self.domain.startswith('www.'):
|
||||
self.domain = self.domain[4:]
|
||||
# set frappe.local.request_ip
|
||||
self.set_request_ip()
|
||||
|
||||
# load cookies
|
||||
self.set_cookies()
|
||||
|
||||
# set frappe.local.db
|
||||
self.connect()
|
||||
|
||||
# login and start/resume user session
|
||||
self.set_session()
|
||||
|
||||
# set request language
|
||||
self.set_lang()
|
||||
|
||||
# match csrf token from current session
|
||||
self.validate_csrf_token()
|
||||
|
||||
# write out latest cookies
|
||||
frappe.local.cookie_manager.init_cookies()
|
||||
|
||||
# check session status
|
||||
check_session_stopped()
|
||||
|
||||
@property
|
||||
def domain(self):
|
||||
if not getattr(self, "_domain", None):
|
||||
self._domain = frappe.request.host
|
||||
if self._domain and self._domain.startswith('www.'):
|
||||
self._domain = self._domain[4:]
|
||||
|
||||
return self._domain
|
||||
|
||||
def set_request_ip(self):
|
||||
if frappe.get_request_header('X-Forwarded-For'):
|
||||
frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip()
|
||||
|
||||
|
|
@ -39,37 +62,21 @@ class HTTPRequest:
|
|||
else:
|
||||
frappe.local.request_ip = '127.0.0.1'
|
||||
|
||||
# language
|
||||
self.set_lang()
|
||||
|
||||
# load cookies
|
||||
def set_cookies(self):
|
||||
frappe.local.cookie_manager = CookieManager()
|
||||
|
||||
# set db
|
||||
self.connect()
|
||||
|
||||
# login
|
||||
def set_session(self):
|
||||
frappe.local.login_manager = LoginManager()
|
||||
|
||||
if frappe.form_dict._lang:
|
||||
lang = get_lang_code(frappe.form_dict._lang)
|
||||
if lang:
|
||||
frappe.local.lang = lang
|
||||
|
||||
self.validate_csrf_token()
|
||||
|
||||
# write out latest cookies
|
||||
frappe.local.cookie_manager.init_cookies()
|
||||
|
||||
# check status
|
||||
check_session_stopped()
|
||||
|
||||
def validate_csrf_token(self):
|
||||
if frappe.local.request and frappe.local.request.method in ("POST", "PUT", "DELETE"):
|
||||
if not frappe.local.session: return
|
||||
if not frappe.local.session.data.csrf_token \
|
||||
or frappe.local.session.data.device=="mobile" \
|
||||
or frappe.conf.get('ignore_csrf', None):
|
||||
if not frappe.local.session:
|
||||
return
|
||||
if (
|
||||
not frappe.local.session.data.csrf_token
|
||||
or frappe.local.session.data.device == "mobile"
|
||||
or frappe.conf.get('ignore_csrf', None)
|
||||
):
|
||||
# not via boot
|
||||
return
|
||||
|
||||
|
|
@ -83,17 +90,18 @@ class HTTPRequest:
|
|||
frappe.throw(_("Invalid Request"), frappe.CSRFTokenError)
|
||||
|
||||
def set_lang(self):
|
||||
from frappe.translate import guess_language
|
||||
frappe.local.lang = guess_language()
|
||||
frappe.local.lang = get_language()
|
||||
|
||||
def get_db_name(self):
|
||||
"""get database name from conf"""
|
||||
return conf.db_name
|
||||
|
||||
def connect(self, ac_name = None):
|
||||
def connect(self):
|
||||
"""connect to db, from ac_name or db_name"""
|
||||
frappe.local.db = frappe.database.get_db(user = self.get_db_name(), \
|
||||
password = getattr(conf, 'db_password', ''))
|
||||
frappe.local.db = frappe.database.get_db(
|
||||
user=self.get_db_name(),
|
||||
password=getattr(conf, 'db_password', '')
|
||||
)
|
||||
|
||||
class LoginManager:
|
||||
def __init__(self):
|
||||
|
|
@ -147,7 +155,7 @@ class LoginManager:
|
|||
self.setup_boot_cache()
|
||||
self.set_user_info()
|
||||
|
||||
def get_user_info(self, resume=False):
|
||||
def get_user_info(self):
|
||||
self.info = frappe.db.get_value("User", self.user,
|
||||
["user_type", "first_name", "last_name", "user_image"], as_dict=1)
|
||||
|
||||
|
|
@ -185,11 +193,13 @@ class LoginManager:
|
|||
frappe.local.response["redirect_to"] = redirect_to
|
||||
frappe.cache().hdel('redirect_after_login', self.user)
|
||||
|
||||
|
||||
frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
|
||||
frappe.local.cookie_manager.set_cookie("user_id", self.user)
|
||||
frappe.local.cookie_manager.set_cookie("user_image", self.info.user_image or "")
|
||||
|
||||
def clear_preferred_language(self):
|
||||
frappe.local.cookie_manager.delete_cookie("preferred_language")
|
||||
|
||||
def make_session(self, resume=False):
|
||||
# start session
|
||||
frappe.local.session_obj = Session(user=self.user, resume=resume,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Assign Condition",
|
||||
"options": "PythonExpression",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -82,7 +83,8 @@
|
|||
"description": "Simple Python Expression, Example: Status in (\"Closed\", \"Cancelled\")",
|
||||
"fieldname": "unassign_condition",
|
||||
"fieldtype": "Code",
|
||||
"label": "Unassign Condition"
|
||||
"label": "Unassign Condition",
|
||||
"options": "PythonExpression"
|
||||
},
|
||||
{
|
||||
"fieldname": "assign_to_users_section",
|
||||
|
|
@ -120,7 +122,8 @@
|
|||
"description": "Simple Python Expression, Example: Status in (\"Invalid\")",
|
||||
"fieldname": "close_condition",
|
||||
"fieldtype": "Code",
|
||||
"label": "Close Condition"
|
||||
"label": "Close Condition",
|
||||
"options": "PythonExpression"
|
||||
},
|
||||
{
|
||||
"fieldname": "sb",
|
||||
|
|
@ -151,7 +154,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-20 14:47:20.662954",
|
||||
"modified": "2021-07-16 22:51:35.505575",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Assignment Rule",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -78,7 +76,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
# clear 5 assignments for first user
|
||||
# can't do a limit in "delete" since postgres does not support it
|
||||
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', owner = 'test@example.com'), limit=5):
|
||||
frappe.db.sql("delete from tabToDo where name = %s", d.name)
|
||||
frappe.db.delete("ToDo", {"name": d.name})
|
||||
|
||||
# add 5 more assignments
|
||||
for i in range(5):
|
||||
|
|
@ -179,7 +177,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
), 'owner'), 'test@example.com')
|
||||
|
||||
def check_assignment_rule_scheduling(self):
|
||||
frappe.db.sql("DELETE FROM `tabAssignment Rule`")
|
||||
frappe.db.delete("Assignment Rule")
|
||||
|
||||
days_1 = [dict(day = 'Sunday'), dict(day = 'Monday'), dict(day = 'Tuesday')]
|
||||
|
||||
|
|
@ -206,7 +204,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
), 'owner'), ['test3@example.com'])
|
||||
|
||||
def test_assignment_rule_condition(self):
|
||||
frappe.db.sql("DELETE FROM `tabAssignment Rule`")
|
||||
frappe.db.delete("Assignment Rule")
|
||||
|
||||
# Add expiry_date custom field
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
|
@ -255,7 +253,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
assignment_rule.delete()
|
||||
|
||||
def clear_assignments():
|
||||
frappe.db.sql("delete from tabToDo where reference_type = 'Note'")
|
||||
frappe.db.delete("ToDo", {"reference_type": "Note"})
|
||||
|
||||
def get_assignment_rule(days, assign=None):
|
||||
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
refresh: function(frm) {
|
||||
// auto repeat message
|
||||
if (frm.is_new()) {
|
||||
let customize_form_link = `<a href="/app/customize form">${__('Customize Form')}</a>`;
|
||||
let customize_form_link = `<a href="/app/customize-form">${__('Customize Form')}</a>`;
|
||||
frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link]));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -334,7 +333,7 @@ class AutoRepeat(Document):
|
|||
if self.reference_doctype and self.reference_document:
|
||||
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
|
||||
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
|
||||
email_ids = list(set([d.email_id for d in res]))
|
||||
email_ids = {d.email_id for d in res}
|
||||
if not email_ids:
|
||||
frappe.msgprint(_('No contacts linked to document'), alert=True)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
# -*- 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
|
||||
|
||||
class TestMilestoneTracker(unittest.TestCase):
|
||||
def test_milestone(self):
|
||||
frappe.db.sql('delete from `tabMilestone Tracker`')
|
||||
frappe.db.delete("Milestone Tracker")
|
||||
|
||||
frappe.cache().delete_key('milestone_tracker_map')
|
||||
|
||||
|
|
@ -46,5 +44,5 @@ class TestMilestoneTracker(unittest.TestCase):
|
|||
self.assertEqual(milestones[0].value, 'Closed')
|
||||
|
||||
# cleanup
|
||||
frappe.db.sql('delete from tabMilestone')
|
||||
frappe.db.delete("Milestone")
|
||||
milestone_tracker.delete()
|
||||
|
|
@ -1,22 +1,27 @@
|
|||
{
|
||||
"category": "Administration",
|
||||
"category": "",
|
||||
"charts": [],
|
||||
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"ToDo\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Note\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"File\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Assignment Rule\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Auto Repeat\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Email\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Automation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Event Streaming\", \"col\": 4}}]",
|
||||
"creation": "2020-03-02 14:53:24.980279",
|
||||
"developer_mode_only": 0,
|
||||
"disable_user_customization": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"extends": "",
|
||||
"extends_another_page": 0,
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "tool",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"is_default": 0,
|
||||
"is_standard": 0,
|
||||
"label": "Tools",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Tools",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
|
|
@ -25,6 +30,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "To Do",
|
||||
"link_count": 0,
|
||||
"link_to": "ToDo",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
|
|
@ -35,6 +41,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Calendar",
|
||||
"link_count": 0,
|
||||
"link_to": "Event",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
|
|
@ -45,6 +52,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Note",
|
||||
"link_count": 0,
|
||||
"link_to": "Note",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
|
|
@ -55,6 +63,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Files",
|
||||
"link_count": 0,
|
||||
"link_to": "File",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -65,6 +74,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Activity",
|
||||
"link_count": 0,
|
||||
"link_to": "activity",
|
||||
"link_type": "Page",
|
||||
"onboard": 0,
|
||||
|
|
@ -74,6 +84,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
|
|
@ -82,6 +93,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Newsletter",
|
||||
"link_count": 0,
|
||||
"link_to": "Newsletter",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
|
|
@ -92,6 +104,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Email Group",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -101,6 +114,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Automation",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
|
|
@ -109,6 +123,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Assignment Rule",
|
||||
"link_count": 0,
|
||||
"link_to": "Assignment Rule",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -119,6 +134,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Milestone",
|
||||
"link_count": 0,
|
||||
"link_to": "Milestone",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -129,6 +145,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Auto Repeat",
|
||||
"link_count": 0,
|
||||
"link_to": "Auto Repeat",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -138,6 +155,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Streaming",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
|
|
@ -146,6 +164,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Producer",
|
||||
"link_count": 0,
|
||||
"link_to": "Event Producer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -156,6 +175,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Consumer",
|
||||
"link_count": 0,
|
||||
"link_to": "Event Consumer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -166,6 +186,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Update Log",
|
||||
"link_count": 0,
|
||||
"link_to": "Event Update Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -176,6 +197,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Sync Log",
|
||||
"link_count": 0,
|
||||
"link_to": "Event Sync Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -186,19 +208,26 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Document Type Mapping",
|
||||
"link_count": 0,
|
||||
"link_to": "Document Type Mapping",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2020-12-01 13:38:39.950350",
|
||||
"modified": "2021-08-05 12:16:02.839180",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Tools",
|
||||
"onboarding": "",
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"pin_to_bottom": 0,
|
||||
"pin_to_top": 0,
|
||||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 26,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "ToDo",
|
||||
|
|
@ -225,5 +254,6 @@
|
|||
"link_to": "Auto Repeat",
|
||||
"type": "DocType"
|
||||
}
|
||||
]
|
||||
],
|
||||
"title": "Tools"
|
||||
}
|
||||
|
|
@ -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
|
||||
"""
|
||||
|
|
@ -75,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
|
||||
|
|
@ -110,8 +105,8 @@ def load_conf_settings(bootinfo):
|
|||
if key in conf: bootinfo[key] = conf.get(key)
|
||||
|
||||
def load_desktop_data(bootinfo):
|
||||
from frappe.desk.desktop import get_desk_sidebar_items
|
||||
bootinfo.allowed_workspaces = get_desk_sidebar_items()
|
||||
from frappe.desk.desktop import get_wspace_sidebar_items
|
||||
bootinfo.allowed_workspaces = get_wspace_sidebar_items().get('pages')
|
||||
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
|
|
@ -220,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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from io import StringIO
|
||||
from tempfile import mkdtemp, mktemp
|
||||
from distutils.spawn import find_executable
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ def build_missing_files():
|
|||
development = frappe.local.conf.developer_mode or frappe.local.dev_server
|
||||
build_mode = "development" if development else "production"
|
||||
|
||||
assets_json = frappe.read_file(frappe.get_app_path('frappe', 'public', 'dist', 'assets.json'))
|
||||
assets_json = frappe.read_file("assets/assets.json")
|
||||
if assets_json:
|
||||
assets_json = frappe.parse_json(assets_json)
|
||||
|
||||
|
|
@ -402,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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -55,7 +53,7 @@ def clear_domain_cache(user=None):
|
|||
cache.delete_value(domain_cache_keys)
|
||||
|
||||
def clear_global_cache():
|
||||
from frappe.website.render import clear_cache as clear_website_cache
|
||||
from frappe.website.utils import clear_website_cache
|
||||
|
||||
clear_doctype_cache()
|
||||
clear_website_cache()
|
||||
|
|
@ -143,18 +141,13 @@ def build_table_count_cache():
|
|||
return
|
||||
|
||||
_cache = frappe.cache()
|
||||
data = frappe.db.multisql({
|
||||
"mariadb": """
|
||||
SELECT table_name AS name,
|
||||
table_rows AS count
|
||||
FROM information_schema.tables""",
|
||||
"postgres": """
|
||||
SELECT "relname" AS name,
|
||||
"n_tup_ins" AS count
|
||||
FROM "pg_stat_all_tables"
|
||||
"""
|
||||
}, as_dict=1)
|
||||
table_name = frappe.qb.Field("table_name").as_("name")
|
||||
table_rows = frappe.qb.Field("table_rows").as_("count")
|
||||
information_schema = frappe.qb.Schema("information_schema")
|
||||
|
||||
data = (
|
||||
frappe.qb.from_(information_schema.tables).select(table_name, table_rows)
|
||||
).run(as_dict=True)
|
||||
counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data}
|
||||
_cache.set_value("information_schema:counts", counts)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
# imports - standard imports
|
||||
import json
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
import frappe
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
# imports - module imports
|
||||
from frappe.chat.util.util import (
|
||||
get_user_doc,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
# imports - standard imports
|
||||
import json
|
||||
from collections.abc import MutableMapping, MutableSequence, Sequence
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
from frappe.chat.util import filter_dict, safe_json_loads
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -103,7 +102,9 @@ def get_commands():
|
|||
from .site import commands as site_commands
|
||||
from .translate import commands as translate_commands
|
||||
from .utils import commands as utils_commands
|
||||
from .redis import commands as redis_commands
|
||||
|
||||
return list(set(scheduler_commands + site_commands + translate_commands + utils_commands))
|
||||
all_commands = scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands
|
||||
return list(set(all_commands))
|
||||
|
||||
commands = get_commands()
|
||||
|
|
|
|||
53
frappe/commands/redis.py
Normal file
53
frappe/commands/redis.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import os
|
||||
|
||||
import click
|
||||
|
||||
import frappe
|
||||
from frappe.utils.rq import RedisQueue
|
||||
from frappe.installer import update_site_config
|
||||
|
||||
@click.command('create-rq-users')
|
||||
@click.option('--set-admin-password', is_flag=True, default=False, help='Set new Redis admin(default user) password')
|
||||
@click.option('--use-rq-auth', is_flag=True, default=False, help='Enable Redis authentication for sites')
|
||||
def create_rq_users(set_admin_password=False, use_rq_auth=False):
|
||||
"""Create Redis Queue users and add to acl and app configs.
|
||||
|
||||
acl config file will be used by redis server while starting the server
|
||||
and app config is used by app while connecting to redis server.
|
||||
"""
|
||||
acl_file_path = os.path.abspath('../config/redis_queue.acl')
|
||||
|
||||
with frappe.init_site():
|
||||
acl_list, user_credentials = RedisQueue.gen_acl_list(
|
||||
set_admin_password=set_admin_password)
|
||||
|
||||
with open(acl_file_path, 'w') as f:
|
||||
f.writelines([acl+'\n' for acl in acl_list])
|
||||
|
||||
sites_path = os.getcwd()
|
||||
common_site_config_path = os.path.join(sites_path, 'common_site_config.json')
|
||||
update_site_config("rq_username", user_credentials['bench'][0], validate=False,
|
||||
site_config_path=common_site_config_path)
|
||||
update_site_config("rq_password", user_credentials['bench'][1], validate=False,
|
||||
site_config_path=common_site_config_path)
|
||||
update_site_config("use_rq_auth", use_rq_auth, validate=False,
|
||||
site_config_path=common_site_config_path)
|
||||
|
||||
click.secho('* ACL and site configs are updated with new user credentials. '
|
||||
'Please restart Redis Queue server to enable namespaces.',
|
||||
fg='green')
|
||||
|
||||
if set_admin_password:
|
||||
env_key = 'RQ_ADMIN_PASWORD'
|
||||
click.secho('* Redis admin password is successfully set up. '
|
||||
'Include below line in .bashrc file for system to use',
|
||||
fg='green')
|
||||
click.secho(f"`export {env_key}={user_credentials['default'][1]}`")
|
||||
click.secho('NOTE: Please save the admin password as you '
|
||||
'can not access redis server without the password',
|
||||
fg='yellow')
|
||||
|
||||
|
||||
commands = [
|
||||
create_rq_users
|
||||
]
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
from __future__ import unicode_literals, absolute_import, print_function
|
||||
import click
|
||||
import sys
|
||||
import frappe
|
||||
|
|
@ -173,9 +172,13 @@ def start_scheduler():
|
|||
@click.command('worker')
|
||||
@click.option('--queue', type=str)
|
||||
@click.option('--quiet', is_flag = True, default = False, help = 'Hide Log Outputs')
|
||||
def start_worker(queue, quiet = False):
|
||||
@click.option('-u', '--rq-username', default=None, help='Redis ACL user')
|
||||
@click.option('-p', '--rq-password', default=None, help='Redis ACL user password')
|
||||
def start_worker(queue, quiet = False, rq_username=None, rq_password=None):
|
||||
"""Site is used to find redis credentals.
|
||||
"""
|
||||
from frappe.utils.background_jobs import start_worker
|
||||
start_worker(queue, quiet = quiet)
|
||||
start_worker(queue, quiet = quiet, rq_username=rq_username, rq_password=rq_password)
|
||||
|
||||
@click.command('ready-for-migration')
|
||||
@click.option('--site', help='site name')
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ def install_app(context, apps):
|
|||
print("App {} is Incompatible with Site {}{}".format(app, site, err_msg))
|
||||
exit_code = 1
|
||||
except Exception as err:
|
||||
err_msg = ":\n{}".format(err if str(err) else frappe.get_traceback())
|
||||
err_msg = ": {}\n{}".format(str(err), frappe.get_traceback())
|
||||
print("An error occurred while installing {}{}".format(app, err_msg))
|
||||
exit_code = 1
|
||||
|
||||
|
|
@ -561,30 +561,54 @@ def move(dest_dir, site):
|
|||
return final_new_path
|
||||
|
||||
|
||||
@click.command('set-admin-password')
|
||||
@click.argument('admin-password')
|
||||
@click.command('set-password')
|
||||
@click.argument('user')
|
||||
@click.argument('password', required=False)
|
||||
@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
|
||||
@pass_context
|
||||
def set_admin_password(context, admin_password, logout_all_sessions=False):
|
||||
def set_password(context, user, password=None, logout_all_sessions=False):
|
||||
"Set password for a user on a site"
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
for site in context.sites:
|
||||
set_user_password(site, user, password, logout_all_sessions)
|
||||
|
||||
|
||||
@click.command('set-admin-password')
|
||||
@click.argument('admin-password', required=False)
|
||||
@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
|
||||
@pass_context
|
||||
def set_admin_password(context, admin_password=None, logout_all_sessions=False):
|
||||
"Set Administrator password for a site"
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
for site in context.sites:
|
||||
set_user_password(site, "Administrator", admin_password, logout_all_sessions)
|
||||
|
||||
|
||||
def set_user_password(site, user, password, logout_all_sessions=False):
|
||||
import getpass
|
||||
from frappe.utils.password import update_password
|
||||
|
||||
for site in context.sites:
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
|
||||
while not admin_password:
|
||||
admin_password = getpass.getpass("Administrator's password for {0}: ".format(site))
|
||||
while not password:
|
||||
password = getpass.getpass(f"{user}'s password for {site}: ")
|
||||
|
||||
frappe.connect()
|
||||
if not frappe.db.exists("User", user):
|
||||
print(f"User {user} does not exist")
|
||||
sys.exit(1)
|
||||
|
||||
update_password(user=user, pwd=password, logout_all_sessions=logout_all_sessions)
|
||||
frappe.db.commit()
|
||||
password = None
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
frappe.connect()
|
||||
update_password(user='Administrator', pwd=admin_password, logout_all_sessions=logout_all_sessions)
|
||||
frappe.db.commit()
|
||||
admin_password = None
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('set-last-active-for-user')
|
||||
@click.option('--user', help="Setup last active date for user")
|
||||
|
|
@ -729,6 +753,7 @@ commands = [
|
|||
remove_from_installed_apps,
|
||||
restore,
|
||||
run_patch,
|
||||
set_password,
|
||||
set_admin_password,
|
||||
uninstall,
|
||||
disable_user,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
|
@ -11,7 +9,14 @@ import click
|
|||
import frappe
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
from frappe.utils import get_bench_path, update_progress_bar, cint
|
||||
from frappe.utils import update_progress_bar, cint
|
||||
from frappe.coverage import CodeCoverage
|
||||
|
||||
DATA_IMPORT_DEPRECATION = click.style(
|
||||
"[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n"
|
||||
"Use `data-import` command instead to import data via 'Data Import'.",
|
||||
fg="yellow"
|
||||
)
|
||||
|
||||
|
||||
@click.command('build')
|
||||
|
|
@ -69,14 +74,14 @@ def watch(apps=None):
|
|||
def clear_cache(context):
|
||||
"Clear cache, doctype cache and defaults"
|
||||
import frappe.sessions
|
||||
import frappe.website.render
|
||||
from frappe.website.utils import clear_website_cache
|
||||
from frappe.desk.notifications import clear_notifications
|
||||
for site in context.sites:
|
||||
try:
|
||||
frappe.connect(site)
|
||||
frappe.clear_cache()
|
||||
clear_notifications()
|
||||
frappe.website.render.clear_cache()
|
||||
clear_website_cache()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
|
|
@ -86,12 +91,12 @@ def clear_cache(context):
|
|||
@pass_context
|
||||
def clear_website_cache(context):
|
||||
"Clear website cache"
|
||||
import frappe.website.render
|
||||
from frappe.website.utils import clear_website_cache
|
||||
for site in context.sites:
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
frappe.website.render.clear_cache()
|
||||
clear_website_cache()
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
|
|
@ -222,7 +227,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()
|
||||
|
|
@ -350,7 +355,8 @@ def import_doc(context, path, force=False):
|
|||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('import-csv')
|
||||
|
||||
@click.command('import-csv', help=DATA_IMPORT_DEPRECATION)
|
||||
@click.argument('path')
|
||||
@click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records')
|
||||
@click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it')
|
||||
|
|
@ -358,32 +364,8 @@ def import_doc(context, path, force=False):
|
|||
@click.option('--no-email', default=True, is_flag=True, help='Send email if applicable')
|
||||
@pass_context
|
||||
def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True):
|
||||
"Import CSV using data import"
|
||||
from frappe.core.doctype.data_import_legacy import importer
|
||||
from frappe.utils.csvutils import read_csv_content
|
||||
site = get_site(context)
|
||||
|
||||
if not os.path.exists(path):
|
||||
path = os.path.join('..', path)
|
||||
if not os.path.exists(path):
|
||||
print('Invalid path {0}'.format(path))
|
||||
sys.exit(1)
|
||||
|
||||
with open(path, 'r') as csvfile:
|
||||
content = read_csv_content(csvfile.read())
|
||||
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
|
||||
try:
|
||||
importer.upload(content, submit_after_import=submit_after_import, no_email=no_email,
|
||||
ignore_encoding_errors=ignore_encoding_errors, overwrite=not only_insert,
|
||||
via_console=True)
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
print(frappe.get_traceback())
|
||||
|
||||
frappe.destroy()
|
||||
click.secho(DATA_IMPORT_DEPRECATION)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@click.command('data-import')
|
||||
|
|
@ -504,15 +486,26 @@ frappe.db.connect()
|
|||
|
||||
|
||||
@click.command('console')
|
||||
@click.option(
|
||||
'--autoreload',
|
||||
is_flag=True,
|
||||
help="Reload changes to code automatically"
|
||||
)
|
||||
@pass_context
|
||||
def console(context):
|
||||
def console(context, autoreload=False):
|
||||
"Start ipython console for a site"
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
frappe.local.lang = frappe.db.get_default("lang")
|
||||
|
||||
import IPython
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
|
||||
terminal = InteractiveShellEmbed()
|
||||
if autoreload:
|
||||
terminal.extension_manager.load_extension("autoreload")
|
||||
terminal.run_line_magic("autoreload", "2")
|
||||
|
||||
all_apps = frappe.get_installed_apps()
|
||||
failed_to_import = []
|
||||
|
||||
|
|
@ -527,7 +520,9 @@ def console(context):
|
|||
if failed_to_import:
|
||||
print("\nFailed to import:\n{}".format(", ".join(failed_to_import)))
|
||||
|
||||
IPython.embed(display_banner="", header="", colors="neutral")
|
||||
terminal.colors = "neutral"
|
||||
terminal.display_banner = False
|
||||
terminal()
|
||||
|
||||
|
||||
@click.command('run-tests')
|
||||
|
|
@ -542,67 +537,39 @@ def console(context):
|
|||
@click.option('--skip-test-records', is_flag=True, default=False, help="Don't create test records")
|
||||
@click.option('--skip-before-tests', is_flag=True, default=False, help="Don't run before tests hook")
|
||||
@click.option('--junit-xml-output', help="Destination file path for junit xml report")
|
||||
@click.option('--failfast', is_flag=True, default=False)
|
||||
@click.option('--failfast', is_flag=True, default=False, help="Stop the test run on the first error or failure")
|
||||
@pass_context
|
||||
def run_tests(context, app=None, module=None, doctype=None, test=(), profile=False,
|
||||
coverage=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None,
|
||||
skip_test_records=False, skip_before_tests=False, failfast=False):
|
||||
|
||||
"Run tests"
|
||||
import frappe.test_runner
|
||||
tests = test
|
||||
with CodeCoverage(coverage, app):
|
||||
import frappe.test_runner
|
||||
tests = test
|
||||
site = get_site(context)
|
||||
|
||||
site = get_site(context)
|
||||
allow_tests = frappe.get_conf(site).allow_tests
|
||||
|
||||
allow_tests = frappe.get_conf(site).allow_tests
|
||||
if not (allow_tests or os.environ.get('CI')):
|
||||
click.secho('Testing is disabled for the site!', bold=True)
|
||||
click.secho('You can enable tests by entering following command:')
|
||||
click.secho('bench --site {0} set-config allow_tests true'.format(site), fg='green')
|
||||
return
|
||||
|
||||
if not (allow_tests or os.environ.get('CI')):
|
||||
click.secho('Testing is disabled for the site!', bold=True)
|
||||
click.secho('You can enable tests by entering following command:')
|
||||
click.secho('bench --site {0} set-config allow_tests true'.format(site), fg='green')
|
||||
return
|
||||
frappe.init(site=site)
|
||||
|
||||
frappe.init(site=site)
|
||||
frappe.flags.skip_before_tests = skip_before_tests
|
||||
frappe.flags.skip_test_records = skip_test_records
|
||||
|
||||
frappe.flags.skip_before_tests = skip_before_tests
|
||||
frappe.flags.skip_test_records = skip_test_records
|
||||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
|
||||
force=context.force, profile=profile, junit_xml_output=junit_xml_output,
|
||||
ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast)
|
||||
|
||||
if coverage:
|
||||
from coverage import Coverage
|
||||
if len(ret.failures) == 0 and len(ret.errors) == 0:
|
||||
ret = 0
|
||||
|
||||
# Generate coverage report only for app that is being tested
|
||||
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
|
||||
omit=[
|
||||
'*.html',
|
||||
'*.js',
|
||||
'*.xml',
|
||||
'*.css',
|
||||
'*.less',
|
||||
'*.scss',
|
||||
'*.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,
|
||||
force=context.force, profile=profile, junit_xml_output=junit_xml_output,
|
||||
ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast)
|
||||
|
||||
if coverage:
|
||||
cov.stop()
|
||||
cov.save()
|
||||
|
||||
if len(ret.failures) == 0 and len(ret.errors) == 0:
|
||||
ret = 0
|
||||
|
||||
if os.environ.get('CI'):
|
||||
sys.exit(ret)
|
||||
if os.environ.get('CI'):
|
||||
sys.exit(ret)
|
||||
|
||||
@click.command('run-parallel-tests')
|
||||
@click.option('--app', help="For App", default='frappe')
|
||||
|
|
@ -612,13 +579,14 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
|
|||
@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)
|
||||
with CodeCoverage(with_coverage, app):
|
||||
site = get_site(context)
|
||||
if use_orchestrator:
|
||||
from frappe.parallel_test_runner import ParallelTestWithOrchestrator
|
||||
ParallelTestWithOrchestrator(app, site=site)
|
||||
else:
|
||||
from frappe.parallel_test_runner import ParallelTestRunner
|
||||
ParallelTestRunner(app, site=site, build_number=build_number, total_builds=total_builds)
|
||||
|
||||
@click.command('run-ui-tests')
|
||||
@click.argument('app')
|
||||
|
|
@ -634,27 +602,29 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
|
|||
admin_password = frappe.get_conf(site).admin_password
|
||||
|
||||
# override baseUrl using env variable
|
||||
site_env = 'CYPRESS_baseUrl={}'.format(site_url)
|
||||
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
|
||||
site_env = f'CYPRESS_baseUrl={site_url}'
|
||||
password_env = f'CYPRESS_adminPassword={admin_password}' if admin_password else ''
|
||||
|
||||
os.chdir(app_base_path)
|
||||
|
||||
node_bin = subprocess.getoutput("npm bin")
|
||||
cypress_path = "{0}/cypress".format(node_bin)
|
||||
plugin_path = "{0}/../cypress-file-upload".format(node_bin)
|
||||
cypress_path = f"{node_bin}/cypress"
|
||||
plugin_path = f"{node_bin}/../cypress-file-upload"
|
||||
testing_library_path = f"{node_bin}/../@testing-library"
|
||||
|
||||
# check if cypress in path...if not, install it.
|
||||
if not (
|
||||
os.path.exists(cypress_path)
|
||||
and os.path.exists(plugin_path)
|
||||
and os.path.exists(testing_library_path)
|
||||
and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6
|
||||
):
|
||||
# install cypress
|
||||
click.secho("Installing Cypress...", fg="yellow")
|
||||
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
|
||||
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 --no-lockfile")
|
||||
|
||||
# run for headless mode
|
||||
run_or_open = 'run --browser firefox --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
|
||||
run_or_open = 'run --browser firefox --record' if headless else 'open'
|
||||
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)
|
||||
|
||||
|
|
@ -662,7 +632,7 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
|
|||
formatted_command += ' --parallel'
|
||||
|
||||
if ci_build_id:
|
||||
formatted_command += ' --ci-build-id {}'.format(ci_build_id)
|
||||
formatted_command += f' --ci-build-id {ci_build_id}'
|
||||
|
||||
click.secho("Running Cypress...", fg="yellow")
|
||||
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
|
||||
|
|
@ -760,22 +730,49 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False):
|
|||
frappe.destroy()
|
||||
|
||||
|
||||
@click.command('version')
|
||||
def get_version():
|
||||
"Show the versions of all the installed apps"
|
||||
@click.command("version")
|
||||
@click.option("-f", "--format", "output",
|
||||
type=click.Choice(["plain", "table", "json", "legacy"]), help="Output format", default="legacy")
|
||||
def get_version(output):
|
||||
"""Show the versions of all the installed apps."""
|
||||
from git import Repo
|
||||
from frappe.utils.commands import render_table
|
||||
from frappe.utils.change_log import get_app_branch
|
||||
frappe.init('')
|
||||
|
||||
for m in sorted(frappe.get_all_apps()):
|
||||
branch_name = get_app_branch(m)
|
||||
module = frappe.get_module(m)
|
||||
app_hooks = frappe.get_module(m + ".hooks")
|
||||
frappe.init("")
|
||||
data = []
|
||||
|
||||
if hasattr(app_hooks, '{0}_version'.format(branch_name)):
|
||||
print("{0} {1}".format(m, getattr(app_hooks, '{0}_version'.format(branch_name))))
|
||||
for app in sorted(frappe.get_all_apps()):
|
||||
module = frappe.get_module(app)
|
||||
app_hooks = frappe.get_module(app + ".hooks")
|
||||
repo = Repo(frappe.get_app_path(app, ".."))
|
||||
|
||||
elif hasattr(module, "__version__"):
|
||||
print("{0} {1}".format(m, module.__version__))
|
||||
app_info = frappe._dict()
|
||||
app_info.app = app
|
||||
app_info.branch = get_app_branch(app)
|
||||
app_info.commit = repo.head.object.hexsha[:7]
|
||||
app_info.version = getattr(app_hooks, f"{app_info.branch}_version", None) or module.__version__
|
||||
|
||||
data.append(app_info)
|
||||
|
||||
{
|
||||
"legacy": lambda: [
|
||||
click.echo(f"{app_info.app} {app_info.version}")
|
||||
for app_info in data
|
||||
],
|
||||
"plain": lambda: [
|
||||
click.echo(f"{app_info.app} {app_info.version} {app_info.branch} ({app_info.commit})")
|
||||
for app_info in data
|
||||
],
|
||||
"table": lambda: render_table(
|
||||
[["App", "Version", "Branch", "Commit"]] +
|
||||
[
|
||||
[app_info.app, app_info.version, app_info.branch, app_info.commit]
|
||||
for app_info in data
|
||||
]
|
||||
),
|
||||
"json": lambda: click.echo(json.dumps(data, indent=4)),
|
||||
}[output]()
|
||||
|
||||
|
||||
@click.command('rebuild-global-search')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -42,18 +39,17 @@ def get_modules_from_app(app):
|
|||
)
|
||||
|
||||
def get_all_empty_tables_by_module():
|
||||
empty_tables = set(r[0] for r in frappe.db.multisql({
|
||||
"mariadb": """
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_rows = 0 and table_schema = "{}"
|
||||
""".format(frappe.conf.db_name),
|
||||
"postgres": """
|
||||
SELECT "relname" as "table_name"
|
||||
FROM "pg_stat_all_tables"
|
||||
WHERE n_tup_ins = 0
|
||||
"""
|
||||
}))
|
||||
table_rows = frappe.qb.Field("table_rows")
|
||||
table_name = frappe.qb.Field("table_name")
|
||||
information_schema = frappe.qb.Schema("information_schema")
|
||||
|
||||
empty_tables = (
|
||||
frappe.qb.from_(information_schema.tables)
|
||||
.select(table_name)
|
||||
.where(table_rows == 0)
|
||||
).run()
|
||||
|
||||
empty_tables = {r[0] for r in empty_tables}
|
||||
|
||||
results = frappe.get_all("DocType", fields=["name", "module"])
|
||||
empty_tables_by_module = {}
|
||||
|
|
|
|||
|
|
@ -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 _
|
||||
|
|
@ -154,7 +153,7 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil
|
|||
doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"],
|
||||
distinct=True, as_list=True)
|
||||
|
||||
doctypes = tuple([d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)])
|
||||
doctypes = tuple(d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE))
|
||||
|
||||
filters.update({
|
||||
"dt": ("not in", [d[0] for d in doctypes])
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -263,7 +257,7 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
|
||||
def get_condensed_address(doc):
|
||||
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"]
|
||||
return ", ".join([doc.get(d) for d in fields if doc.get(d)])
|
||||
return ", ".join(doc.get(d) for d in fields if doc.get(d))
|
||||
|
||||
def update_preferred_address(address, field):
|
||||
frappe.db.set_value('Address', address, field, 0)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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]):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
import unittest
|
||||
|
|
|
|||
|
|
@ -1,4 +1,2 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# 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
|
||||
|
||||
|
|
@ -35,4 +29,5 @@ def make_access_log(doctype=None, document=None, method=None, file_type=None,
|
|||
doc.insert(ignore_permissions=True)
|
||||
|
||||
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
|
||||
frappe.db.commit()
|
||||
if frappe.request and frappe.request.method == 'GET':
|
||||
frappe.db.commit()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -31,10 +29,12 @@ def update_feed(doc, method=None):
|
|||
name = feed.name or doc.name
|
||||
|
||||
# delete earlier feed
|
||||
frappe.db.sql("""delete from `tabActivity Log`
|
||||
where
|
||||
reference_doctype=%s and reference_name=%s
|
||||
and link_doctype=%s""", (doctype, name,feed.link_doctype))
|
||||
frappe.db.delete("Activity Log", {
|
||||
"reference_doctype": doctype,
|
||||
"reference_name": name,
|
||||
"link_doctype": feed.link_doctype
|
||||
})
|
||||
|
||||
frappe.get_doc({
|
||||
"doctype": "Activity Log",
|
||||
"reference_doctype": doctype,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue