浏览代码

Merge branch 'develop' into fix-number-format-issues

version-14
Mohammad Hasnain 3 年前
父节点
当前提交
01fb128912
共有 100 个文件被更改,包括 1485 次插入510 次删除
  1. +1
    -1
      .editorconfig
  2. +3
    -0
      .git-blame-ignore-revs
  3. +3
    -3
      .github/helper/documentation.py
  4. +0
    -5
      .github/helper/install_dependencies.sh
  5. +56
    -40
      .github/helper/roulette.py
  6. +0
    -2
      .github/helper/semgrep_rules/frappe_correctness.yml
  7. +0
    -4
      .github/helper/semgrep_rules/security.yml
  8. +17
    -0
      .github/semantic.yml
  9. +22
    -0
      .github/workflows/patch-mariadb-tests.yml
  10. +10
    -26
      .github/workflows/semgrep.yml
  11. +31
    -31
      .github/workflows/server-mariadb-tests.yml
  12. +34
    -1
      .github/workflows/server-postgres-tests.yml
  13. +26
    -0
      .github/workflows/ui-tests.yml
  14. +16
    -0
      .mergify.yml
  15. +4
    -4
      CODEOWNERS
  16. +1
    -1
      LICENSE
  17. +2
    -2
      README.md
  18. +13
    -0
      codecov.yml
  19. 二进制
      cypress/fixtures/sample_image.jpg
  20. +8
    -3
      cypress/integration/api.js
  21. +7
    -7
      cypress/integration/awesome_bar.js
  22. +3
    -3
      cypress/integration/control_barcode.js
  23. +50
    -0
      cypress/integration/control_icon.js
  24. +2
    -2
      cypress/integration/control_link.js
  25. +2
    -0
      cypress/integration/control_select.js
  26. +63
    -0
      cypress/integration/dashboard_links.js
  27. +19
    -0
      cypress/integration/datetime_field_form_validation.js
  28. +3
    -3
      cypress/integration/depends_on.js
  29. +29
    -8
      cypress/integration/file_uploader.js
  30. +79
    -0
      cypress/integration/folder_navigation.js
  31. +2
    -1
      cypress/integration/form.js
  32. +88
    -0
      cypress/integration/form_tour.js
  33. +2
    -2
      cypress/integration/grid_pagination.js
  34. +2
    -2
      cypress/integration/list_view.js
  35. +6
    -6
      cypress/integration/list_view_settings.js
  36. +6
    -6
      cypress/integration/login.js
  37. +14
    -0
      cypress/integration/navigation.js
  38. +8
    -8
      cypress/integration/recorder.js
  39. +1
    -1
      cypress/integration/report_view.js
  40. +56
    -0
      cypress/integration/sidebar.js
  41. +1
    -0
      cypress/integration/table_multiselect.js
  42. +94
    -0
      cypress/integration/timeline.js
  43. +70
    -0
      cypress/integration/timeline_email.js
  44. +90
    -0
      cypress/integration/workspace.js
  45. +37
    -6
      cypress/support/commands.js
  46. +81
    -31
      esbuild/esbuild.js
  47. +13
    -4
      frappe/__init__.py
  48. +2
    -2
      frappe/api.py
  49. +4
    -5
      frappe/app.py
  50. +63
    -49
      frappe/auth.py
  51. +6
    -3
      frappe/automation/doctype/assignment_rule/assignment_rule.json
  52. +1
    -1
      frappe/automation/doctype/assignment_rule/assignment_rule.py
  53. +5
    -5
      frappe/automation/doctype/assignment_rule/test_assignment_rule.py
  54. +1
    -1
      frappe/automation/doctype/assignment_rule_day/assignment_rule_day.py
  55. +1
    -1
      frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py
  56. +1
    -1
      frappe/automation/doctype/auto_repeat/auto_repeat.js
  57. +2
    -2
      frappe/automation/doctype/auto_repeat/auto_repeat.py
  58. +1
    -1
      frappe/automation/doctype/auto_repeat/test_auto_repeat.py
  59. +1
    -1
      frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py
  60. +1
    -1
      frappe/automation/doctype/milestone/milestone.py
  61. +1
    -1
      frappe/automation/doctype/milestone/test_milestone.py
  62. +1
    -1
      frappe/automation/doctype/milestone_tracker/milestone_tracker.py
  63. +3
    -3
      frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py
  64. +34
    -4
      frappe/automation/workspace/tools/tools.json
  65. +3
    -3
      frappe/boot.py
  66. +1
    -1
      frappe/build.py
  67. +8
    -13
      frappe/cache_manager.py
  68. +1
    -1
      frappe/chat/doctype/chat_token/chat_token.py
  69. +1
    -1
      frappe/client.py
  70. +4
    -2
      frappe/commands/__init__.py
  71. +53
    -0
      frappe/commands/redis.py
  72. +6
    -2
      frappe/commands/scheduler.py
  73. +41
    -16
      frappe/commands/site.py
  74. +113
    -123
      frappe/commands/utils.py
  75. +11
    -12
      frappe/config/__init__.py
  76. +3
    -3
      frappe/contacts/address_and_contact.py
  77. +2
    -2
      frappe/contacts/doctype/address/address.py
  78. +1
    -1
      frappe/contacts/doctype/address/test_address.py
  79. +1
    -1
      frappe/contacts/doctype/address_template/address_template.py
  80. +1
    -1
      frappe/contacts/doctype/address_template/test_address_template.py
  81. +2
    -2
      frappe/contacts/doctype/contact/contact.py
  82. +1
    -1
      frappe/contacts/doctype/contact/test_contact.py
  83. +1
    -1
      frappe/contacts/doctype/contact_email/contact_email.py
  84. +1
    -1
      frappe/contacts/doctype/contact_phone/contact_phone.py
  85. +1
    -1
      frappe/contacts/doctype/gender/gender.py
  86. +1
    -1
      frappe/contacts/doctype/gender/test_gender.py
  87. +1
    -1
      frappe/contacts/doctype/salutation/salutation.py
  88. +1
    -1
      frappe/contacts/doctype/salutation/test_salutation.py
  89. +1
    -1
      frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py
  90. +1
    -1
      frappe/core/__init__.py
  91. +1
    -1
      frappe/core/doctype/__init__.py
  92. +3
    -6
      frappe/core/doctype/access_log/access_log.py
  93. +1
    -1
      frappe/core/doctype/access_log/test_access_log.py
  94. +1
    -1
      frappe/core/doctype/activity_log/activity_log.py
  95. +7
    -5
      frappe/core/doctype/activity_log/feed.py
  96. +1
    -1
      frappe/core/doctype/activity_log/test_activity_log.py
  97. +1
    -1
      frappe/core/doctype/block_module/block_module.py
  98. +2
    -2
      frappe/core/doctype/comment/comment.py
  99. +3
    -3
      frappe/core/doctype/comment/test_comment.py
  100. +1
    -1
      frappe/core/doctype/communication/__init__.py

+ 1
- 1
.editorconfig 查看文件

@@ -9,6 +9,6 @@ trim_trailing_whitespace = true
charset = utf-8 charset = utf-8


# python, js indentation settings # python, js indentation settings
[{*.py,*.js}]
[{*.py,*.js,*.vue}]
indent_style = tab indent_style = tab
indent_size = 4 indent_size = 4

+ 3
- 0
.git-blame-ignore-revs 查看文件

@@ -10,3 +10,6 @@


# Replace use of Class.extend with native JS class # Replace use of Class.extend with native JS class
fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85 fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85

# Updating license headers
34460265554242a8d05fb09f049033b1117e1a2b

+ 3
- 3
.github/helper/documentation.py 查看文件

@@ -32,9 +32,9 @@ if __name__ == "__main__":


if response.ok: if response.ok:
payload = response.json() 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 title.startswith("feat") and head_sha and "no-docs" not in body:
if docs_link_exists(body): if docs_link_exists(body):


+ 0
- 5
.github/helper/install_dependencies.sh 查看文件

@@ -2,11 +2,6 @@


set -e set -e


# python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
# if [[ $? != 2 ]];then
# exit;
# fi

# install wkhtmltopdf # install wkhtmltopdf
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz 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 tar -xf /tmp/wkhtmltox.tar.xz -C /tmp


+ 56
- 40
.github/helper/roulette.py 查看文件

@@ -1,56 +1,72 @@
# if the script ends with exit code 0, then no tests are run further, else all tests are run
import json
import os import os
import re import re
import shlex import shlex
import subprocess import subprocess
import sys 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): 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): def is_py(file):
return file.endswith("py")
return file.endswith("py")

def is_ci(file):
return ".github" in file


def is_js(file):
return file.endswith("js")
def is_frontend_code(file):
return file.lower().endswith((".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue"))


def is_docs(file): def is_docs(file):
regex = re.compile(r'\.(md|png|jpg|jpeg)$|^.github|LICENSE')
return bool(regex.search(file))
regex = re.compile(r'\.(md|png|jpg|jpeg|csv)$|^.github|LICENSE')
return bool(regex.search(file))




if __name__ == "__main__": 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))

try:
files_changed = get_output("git diff --name-only {}".format(commit_range), shell=False)
except Exception:
sys.exit(2)

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 only_docs_changed:
print("Only docs were updated, stopping build process.")
sys.exit(0)

if only_js_changed and build_type == "server":
print("Only JavaScript code was updated; Stopping Python build process.")
sys.exit(0)

if only_py_changed and build_type == "ui":
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)

sys.exit(2)
files_list = sys.argv[1:]
build_type = os.environ.get("TYPE")
pr_number = os.environ.get("PR_NUMBER")
repo = os.environ.get("REPO_NAME")

# this is a push build, run all builds
if not pr_number:
os.system('echo "::set-output name=build::strawberry"')
sys.exit(0)

files_list = files_list or get_files_list(pr_number=pr_number, repo=repo)

if not files_list:
print("No files' changes detected. Build is shutting")
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 ci_files_changed:
print("CI related files were updated, running all build processes.")

elif only_docs_changed:
print("Only docs were updated, stopping build process.")
sys.exit(0)

elif only_frontend_code_changed and build_type == "server":
print("Only Frontend code was updated; Stopping Python build process.")
sys.exit(0)

elif only_py_changed and build_type == "ui":
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)

os.system('echo "::set-output name=build::strawberry"')

+ 0
- 2
.github/helper/semgrep_rules/frappe_correctness.yml 查看文件

@@ -98,8 +98,6 @@ rules:
languages: [python] languages: [python]
severity: WARNING severity: WARNING
paths: paths:
exclude:
- test_*.py
include: include:
- "*/**/doctype/*" - "*/**/doctype/*"




+ 0
- 4
.github/helper/semgrep_rules/security.yml 查看文件

@@ -8,10 +8,6 @@ rules:
dynamic content. Avoid it or use safe_eval(). dynamic content. Avoid it or use safe_eval().
languages: [python] languages: [python]
severity: ERROR severity: ERROR
paths:
exclude:
- frappe/__init__.py
- frappe/commands/utils.py


- id: frappe-sqli-format-strings - id: frappe-sqli-format-strings
patterns: patterns:


+ 17
- 0
.github/semantic.yml 查看文件

@@ -11,3 +11,20 @@ allowRevertCommits: true


# For allowed PR types: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json # 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 # 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

+ 22
- 0
.github/workflows/patch-mariadb-tests.yml 查看文件

@@ -2,6 +2,11 @@ name: Patch


on: [pull_request, workflow_dispatch] on: [pull_request, workflow_dispatch]



concurrency:
group: patch-mariadb-develop-${{ github.event.number }}
cancel-in-progress: true

jobs: jobs:
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
@@ -26,10 +31,21 @@ jobs:
with: with:
python-version: 3.7 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 - name: Add to Hosts
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts


- name: Cache pip - name: Cache pip
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache/pip path: ~/.cache/pip
@@ -39,6 +55,7 @@ jobs:
${{ runner.os }}- ${{ runner.os }}-


- name: Cache node modules - name: Cache node modules
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2 uses: actions/cache@v2
env: env:
cache-name: cache-node-modules cache-name: cache-node-modules
@@ -51,10 +68,12 @@ jobs:
${{ runner.os }}- ${{ runner.os }}-


- name: Get yarn cache directory path - name: Get yarn cache directory path
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "::set-output name=dir::$(yarn cache dir)"


- uses: actions/cache@v2 - uses: actions/cache@v2
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -63,6 +82,7 @@ jobs:
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-


- name: Install Dependencies - name: Install Dependencies
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env: env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
@@ -70,12 +90,14 @@ jobs:
TYPE: server TYPE: server


- name: Install - name: Install
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env: env:
DB: mariadb DB: mariadb
TYPE: server TYPE: server


- name: Run Patch Tests - name: Run Patch Tests
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: | run: |
cd ~/frappe-bench/ cd ~/frappe-bench/
wget https://frappeframework.com/files/v10-frappe.sql.gz wget https://frappeframework.com/files/v10-frappe.sql.gz


+ 10
- 26
.github/workflows/semgrep.yml 查看文件

@@ -1,34 +1,18 @@
name: Semgrep name: Semgrep


on: on:
pull_request:
branches:
- develop
- version-13-hotfix
- version-13-pre-release
pull_request: { }

jobs: jobs:
semgrep: semgrep:
name: Frappe Linter name: Frappe Linter
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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

+ 31
- 31
.github/workflows/server-mariadb-tests.yml 查看文件

@@ -6,6 +6,11 @@ on:
push: push:
branches: [ develop ] branches: [ develop ]


concurrency:
group: server-mariadb-develop-${{ github.event.number }}
cancel-in-progress: true


jobs: jobs:
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
@@ -35,17 +40,29 @@ jobs:
with: with:
python-version: 3.7 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 - uses: actions/setup-node@v2
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
with: with:
node-version: 14 node-version: 14
check-latest: true check-latest: true


- name: Add to Hosts - name: Add to Hosts
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: | run: |
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts 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 echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts


- name: Cache pip - name: Cache pip
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache/pip path: ~/.cache/pip
@@ -55,6 +72,7 @@ jobs:
${{ runner.os }}- ${{ runner.os }}-


- name: Cache node modules - name: Cache node modules
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2 uses: actions/cache@v2
env: env:
cache-name: cache-node-modules cache-name: cache-node-modules
@@ -67,10 +85,12 @@ jobs:
${{ runner.os }}- ${{ runner.os }}-


- name: Get yarn cache directory path - name: Get yarn cache directory path
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "::set-output name=dir::$(yarn cache dir)"


- uses: actions/cache@v2 - uses: actions/cache@v2
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -79,6 +99,7 @@ jobs:
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-


- name: Install Dependencies - name: Install Dependencies
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env: env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
@@ -86,45 +107,24 @@ jobs:
TYPE: server TYPE: server


- name: Install - name: Install
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env: env:
DB: mariadb DB: mariadb
TYPE: server TYPE: server


- name: Run Tests - 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 run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
env: env:
CI_BUILD_ID: ${{ github.run_id }} CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io ORCHESTRATOR_URL: http://test-orchestrator.frappe.io


- name: Upload Coverage Data
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true

coveralls:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
steps:
- name: Clone
uses: actions/checkout@v2

- name: Coveralls Finished
run: |
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload coverage data
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: codecov/codecov-action@v2
with:
name: MariaDB
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true

+ 34
- 1
.github/workflows/server-postgres-tests.yml 查看文件

@@ -3,6 +3,12 @@ name: Server
on: on:
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
push:
branches: [ develop ]

concurrency:
group: server-postgres-develop-${{ github.event.number }}
cancel-in-progress: true


jobs: jobs:
test: test:
@@ -37,17 +43,29 @@ jobs:
with: with:
python-version: 3.7 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 - uses: actions/setup-node@v2
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
with: with:
node-version: '14' node-version: '14'
check-latest: true check-latest: true


- name: Add to Hosts - name: Add to Hosts
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: | run: |
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts 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 echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts


- name: Cache pip - name: Cache pip
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache/pip path: ~/.cache/pip
@@ -57,6 +75,7 @@ jobs:
${{ runner.os }}- ${{ runner.os }}-


- name: Cache node modules - name: Cache node modules
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2 uses: actions/cache@v2
env: env:
cache-name: cache-node-modules cache-name: cache-node-modules
@@ -69,10 +88,12 @@ jobs:
${{ runner.os }}- ${{ runner.os }}-


- name: Get yarn cache directory path - name: Get yarn cache directory path
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "::set-output name=dir::$(yarn cache dir)"


- uses: actions/cache@v2 - uses: actions/cache@v2
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -81,6 +102,7 @@ jobs:
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-


- name: Install Dependencies - name: Install Dependencies
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env: env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
@@ -88,13 +110,24 @@ jobs:
TYPE: server TYPE: server


- name: Install - name: Install
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env: env:
DB: postgres DB: postgres
TYPE: server TYPE: server


- name: Run Tests - name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
env: env:
CI_BUILD_ID: ${{ github.run_id }} CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io ORCHESTRATOR_URL: http://test-orchestrator.frappe.io

- name: Upload coverage data
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: codecov/codecov-action@v2
with:
name: Postgres
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true

+ 26
- 0
.github/workflows/ui-tests.yml 查看文件

@@ -6,6 +6,10 @@ on:
push: push:
branches: [ develop ] branches: [ develop ]


concurrency:
group: ui-develop-${{ github.event.number }}
cancel-in-progress: true

jobs: jobs:
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
@@ -35,17 +39,29 @@ jobs:
with: with:
python-version: 3.7 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 - uses: actions/setup-node@v2
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
with: with:
node-version: 14 node-version: 14
check-latest: true check-latest: true


- name: Add to Hosts - name: Add to Hosts
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: | run: |
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts 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 echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts


- name: Cache pip - name: Cache pip
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache/pip path: ~/.cache/pip
@@ -55,6 +71,7 @@ jobs:
${{ runner.os }}- ${{ runner.os }}-


- name: Cache node modules - name: Cache node modules
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2 uses: actions/cache@v2
env: env:
cache-name: cache-node-modules cache-name: cache-node-modules
@@ -67,10 +84,12 @@ jobs:
${{ runner.os }}- ${{ runner.os }}-


- name: Get yarn cache directory path - name: Get yarn cache directory path
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "::set-output name=dir::$(yarn cache dir)"


- uses: actions/cache@v2 - uses: actions/cache@v2
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
id: yarn-cache id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -79,6 +98,7 @@ jobs:
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-


- name: Cache cypress binary - name: Cache cypress binary
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.cache path: ~/.cache
@@ -88,6 +108,7 @@ jobs:
${{ runner.os }}- ${{ runner.os }}-


- name: Install Dependencies - name: Install Dependencies
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env: env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
@@ -95,13 +116,18 @@ jobs:
TYPE: ui TYPE: ui


- name: Install - name: Install
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env: env:
DB: mariadb DB: mariadb
TYPE: ui TYPE: ui


- name: Site Setup - 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 run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard


- name: UI Tests - 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 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

+ 16
- 0
.mergify.yml 查看文件

@@ -1,4 +1,20 @@
pull_request_rules: pull_request_rules:
- name: Auto-close PRs on stable branch
conditions:
- and:
- and:
- author!=surajshetty3416
- author!=gavindsouza
- 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 - name: Automatic merge on CI success and review
conditions: conditions:
- status-success=Sider - status-success=Sider


+ 4
- 4
CODEOWNERS 查看文件

@@ -4,16 +4,16 @@
# the repo. Unless a later match takes precedence, # the repo. Unless a later match takes precedence,


* @frappe/frappe-review-team * @frappe/frappe-review-team
website/ @prssanna
web_form/ @prssanna
templates/ @surajshetty3416 templates/ @surajshetty3416
www/ @surajshetty3416 www/ @surajshetty3416
integrations/ @leela integrations/ @leela
patches/ @surajshetty3416
dashboard/ @prssanna
patches/ @surajshetty3416 @gavindsouza
email/ @leela email/ @leela
event_streaming/ @ruchamahabal event_streaming/ @ruchamahabal
data_import* @netchampfaris data_import* @netchampfaris
core/ @surajshetty3416 core/ @surajshetty3416
database @gavindsouza
model @gavindsouza
requirements.txt @gavindsouza requirements.txt @gavindsouza
commands/ @gavindsouza commands/ @gavindsouza
workspace @shariquerik

+ 1
- 1
LICENSE 查看文件

@@ -1,6 +1,6 @@
The MIT License The MIT License


Copyright (c) 2016-2018 Frappe Technologies Pvt. Ltd. <developers@frappe.io>
Copyright (c) 2016-2021 Frappe Technologies Pvt. Ltd. <developers@frappe.io>


Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal


+ 2
- 2
README.md 查看文件

@@ -26,8 +26,8 @@
<a href='https://www.codetriage.com/frappe/frappe'> <a href='https://www.codetriage.com/frappe/frappe'>
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'> <img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
</a> </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 href="https://codecov.io/gh/frappe/frappe">
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/>
</a> </a>
</div> </div>




+ 13
- 0
codecov.yml 查看文件

@@ -0,0 +1,13 @@
codecov:
require_ci_to_pass: yes

coverage:
status:
project:
default:
target: auto
threshold: 0.5%

comment:
layout: "diff"
require_changes: true

二进制
cypress/fixtures/sample_image.jpg 查看文件

之前 之后
宽度: 1920  |  高度: 1281  |  大小: 244 KiB

+ 8
- 3
cypress/integration/api.js 查看文件

@@ -31,8 +31,13 @@ context('API Resources', () => {
}); });


it('Removes the Comments', () => { it('Removes the Comments', () => {
cy.get_list('Comment').then(body => body.data.forEach(comment => {
cy.remove_doc('Comment', comment.name);
}));
cy.get_list('Comment').then(body => {
let comment_names = [];
body.data.map(comment => comment_names.push(comment.name));
comment_names = [...new Set(comment_names)]; // remove duplicates
comment_names.forEach((comment_name) => {
cy.remove_doc('Comment', comment_name);
});
});
}); });
}); });

+ 7
- 7
cypress/integration/awesome_bar.js 查看文件

@@ -10,9 +10,9 @@ context('Awesome Bar', () => {
}); });


it('navigates to doctype list', () => { 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'); cy.get('.title-text').should('contain', 'To Do');


@@ -20,24 +20,24 @@ context('Awesome Bar', () => {
}); });


it('find text in doctype list', () => { 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 }); .type('test in todo{downarrow}{enter}', { delay: 200 });


cy.get('.title-text').should('contain', 'To Do'); cy.get('.title-text').should('contain', 'To Do');


cy.get('[data-original-title="Name"] > .input-with-feedback')
cy.findByPlaceholderText('Name')
.should('have.value', '%test%'); .should('have.value', '%test%');
}); });


it('navigates to new form', () => { 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 }); .type('new blog post{downarrow}{enter}', { delay: 200 });


cy.get('.title-text:visible').should('have.text', 'New Blog Post'); cy.get('.title-text:visible').should('have.text', 'New Blog Post');
}); });


it('calculates math expressions', () => { it('calculates math expressions', () => {
cy.get('#navbar-search')
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
.type('55 + 32{downarrow}{enter}', { delay: 200 }); .type('55 + 32{downarrow}{enter}', { delay: 200 });


cy.get('.modal-title').should('contain', 'Result'); cy.get('.modal-title').should('contain', 'Result');


+ 3
- 3
cypress/integration/control_barcode.js 查看文件

@@ -20,7 +20,7 @@ context('Control Barcode', () => {
it('should generate barcode on setting a value', () => { it('should generate barcode on setting a value', () => {
get_dialog_with_barcode().as('dialog'); get_dialog_with_barcode().as('dialog');


cy.get('.frappe-control[data-fieldname=barcode] input')
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
.focus() .focus()
.type('123456789') .type('123456789')
.blur(); .blur();
@@ -37,11 +37,11 @@ context('Control Barcode', () => {
it('should reset when input is cleared', () => { it('should reset when input is cleared', () => {
get_dialog_with_barcode().as('dialog'); get_dialog_with_barcode().as('dialog');


cy.get('.frappe-control[data-fieldname=barcode] input')
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
.focus() .focus()
.type('123456789') .type('123456789')
.blur(); .blur();
cy.get('.frappe-control[data-fieldname=barcode] input')
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
.clear() .clear()
.blur(); .blur();
cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]') cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]')


+ 50
- 0
cypress/integration/control_icon.js 查看文件

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

});

+ 2
- 2
cypress/integration/control_link.js 查看文件

@@ -35,7 +35,7 @@ context('Control Link', () => {
cy.wait('@search_link'); cy.wait('@search_link');
cy.get('@input').type('todo for link', { delay: 200 }); cy.get('@input').type('todo for link', { delay: 200 });
cy.wait('@search_link'); 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').type('{enter}', { delay: 100 });
cy.get('.frappe-control[data-fieldname=link] input').blur(); cy.get('.frappe-control[data-fieldname=link] input').blur();
cy.get('@dialog').then(dialog => { cy.get('@dialog').then(dialog => {
@@ -71,7 +71,7 @@ context('Control Link', () => {
cy.get('@input').type(todos[0]).blur(); cy.get('@input').type(todos[0]).blur();
cy.wait('@validate_link'); cy.wait('@validate_link');
cy.get('@input').focus(); cy.get('@input').focus();
cy.get('.frappe-control[data-fieldname=link] .link-btn')
cy.findByTitle('Open Link')
.should('be.visible') .should('be.visible')
.click(); .click();
cy.location('pathname').should('eq', `/app/todo/${todos[0]}`); cy.location('pathname').should('eq', `/app/todo/${todos[0]}`);


+ 2
- 0
cypress/integration/control_select.js 查看文件

@@ -24,8 +24,10 @@ context('Control Select', () => {
cy.get('@control').get('.select-icon').should('exist'); cy.get('@control').get('.select-icon').should('exist');
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block'); cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
cy.get('@select').select('Option 1'); cy.get('@select').select('Option 1');
cy.findByDisplayValue('Option 1').should('exist');
cy.get('@control').get('.placeholder').should('have.css', 'display', 'none'); cy.get('@control').get('.placeholder').should('have.css', 'display', 'none');
cy.get('@select').invoke('val', ''); cy.get('@select').invoke('val', '');
cy.findByDisplayValue('Option 1').should('not.exist');
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block'); cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');






+ 63
- 0
cypress/integration/dashboard_links.js 查看文件

@@ -0,0 +1,63 @@
context('Dashboard links', () => {
before(() => {
cy.visit('/login');
cy.login();
});

it('Adding a new contact, checking for the counter on the dashboard and deleting the created contact', () => {
cy.visit('/app/contact');
cy.clear_filters();

cy.visit('/app/user');
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();

//To check if initially the dashboard contains only the "Contact" link and there is no counter
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');

//Adding a new contact
cy.get('.btn[data-doctype="Contact"]').click();
cy.get('[data-doctype="Contact"][data-fieldname="first_name"]').type('Admin');
cy.findByRole('button', {name: 'Save'}).click();
cy.visit('/app/user');
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();

//To check if the counter for contact doc is "1" after adding the contact
cy.get('[data-doctype="Contact"] > .count').should('contain', '1');
cy.get('[data-doctype="Contact"]').contains('Contact').click();

//Deleting the newly created contact
cy.visit('/app/contact');
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.findByRole('button', {name: 'Yes'}).click({delay: 700});


//To check if the counter from the "Contact" doc link is removed
cy.wait(700);
cy.visit('/app/user');
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
});

it('Report link in dashboard', () => {
cy.visit('/app/user');
cy.visit('/app/user/Administrator');
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
cy.findByText('Connections');
cy.window()
.its('cur_frm')
.then(cur_frm => {
cur_frm.dashboard.data.reports = [
{
'label': 'Reports',
'items': ['Permitted Documents For User']
}
];
cur_frm.dashboard.render_report_links();
cy.get('[data-report="Permitted Documents For User"]').contains('Permitted Documents For User').click();
cy.findByText('Permitted Documents For User');
cy.findByPlaceholderText('User').should("have.value", "Administrator");
});
});
});

+ 19
- 0
cypress/integration/datetime_field_form_validation.js 查看文件

@@ -0,0 +1,19 @@
// TODO: Enable this again
// currently this is flaky possibly because of different timezone in CI

// context('Datetime Field Validation', () => {
// before(() => {
// cy.login();
// cy.visit('/app/communication');
// });

// it('datetime field form validation', () => {
// // validating datetime field value when value is set from backend and get validated on form load.
// cy.window().its('frappe').then(frappe => {
// return frappe.xcall("frappe.tests.ui_test_helpers.create_communication_record");
// }).then(doc => {
// cy.visit(`/app/communication/${doc.name}`);
// cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red');
// });
// });
// });

+ 3
- 3
cypress/integration/depends_on.js 查看文件

@@ -62,11 +62,11 @@ context('Depends On', () => {
it('should set the field as mandatory depending on other fields value', () => { it('should set the field as mandatory depending on other fields value', () => {
cy.new_form('Test Depends On'); cy.new_form('Test Depends On');
cy.fill_field('test_field', 'Some Value'); 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.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible');
cy.hide_dialog(); cy.hide_dialog();
cy.fill_field('test_field', 'Random value'); 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'); 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', () => { 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('dependant_field', 'Some Value');
//cy.fill_field('test_field', 'Some Other Value'); //cy.fill_field('test_field', 'Some Other Value');
cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table'); 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('@table').find('[data-idx="1"]').as('row1');
cy.get('@row1').find('.btn-open-row').click(); cy.get('@row1').find('.btn-open-row').click();
cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid'); cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid');


+ 29
- 8
cypress/integration/file_uploader.js 查看文件

@@ -25,7 +25,7 @@ context('FileUploader', () => {


cy.get_open_dialog().find('.file-name').should('contain', 'example.json'); cy.get_open_dialog().find('.file-name').should('contain', 'example.json');
cy.intercept('POST', '/api/method/upload_file').as('upload_file'); 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.wait('@upload_file').its('response.statusCode').should('eq', 200);
cy.get('.modal:visible').should('not.exist'); cy.get('.modal:visible').should('not.exist');
}); });
@@ -33,11 +33,11 @@ context('FileUploader', () => {
it('should accept uploaded files', () => { it('should accept uploaded files', () => {
open_upload_dialog(); 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.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') cy.wait('@upload_file').its('response.body.message')
.should('have.property', 'file_name', 'example.json'); .should('have.property', 'file_name', 'example.json');
cy.get('.modal:visible').should('not.exist'); cy.get('.modal:visible').should('not.exist');
@@ -46,12 +46,33 @@ context('FileUploader', () => {
it('should accept web links', () => { it('should accept web links', () => {
open_upload_dialog(); 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.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') cy.wait('@upload_file').its('response.body.message')
.should('have.property', 'file_url', 'https://github.com'); .should('have.property', 'file_url', 'https://github.com');
cy.get('.modal:visible').should('not.exist'); 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');
});
}); });

+ 79
- 0
cypress/integration/folder_navigation.js 查看文件

@@ -0,0 +1,79 @@
context('Folder Navigation', () => {
before(() => {
cy.visit('/login');
cy.login();
cy.visit('/app/file');
});

it('Adding Folders', () => {
//Adding filter to go into the home folder
cy.get('.filter-selector > .btn').findByText('1 filter').click();
cy.findByRole('button', {name: 'Clear Filters'}).click();
cy.get('.filter-action-buttons > .text-muted').findByText('+ Add a Filter').click();
cy.get('.fieldname-select-area > .awesomplete > .form-control').type('Fol{enter}');
cy.get('.filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback').type('Home{enter}');
cy.get('.filter-action-buttons > div > .btn-primary').findByText('Apply Filters').click();

//Adding folder (Test Folder)
cy.get('.menu-btn-group > .btn').click();
cy.get('.menu-btn-group [data-label="New Folder"]').click();
cy.get('form > [data-fieldname="value"]').type('Test Folder');
cy.findByRole('button', {name: 'Create'}).click();
});

it('Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct', () => {
//Navigating inside the Attachments folder
cy.get('[title="Attachments"] > span').click();

//To check if the URL formed after visiting the attachments folder is correct
cy.location('pathname').should('eq', '/app/file/view/home/Attachments');
cy.visit('/app/file/view/home/Attachments');

//Adding folder inside the attachments folder
cy.get('.menu-btn-group > .btn').click();
cy.get('.menu-btn-group [data-label="New Folder"]').click();
cy.get('form > [data-fieldname="value"]').type('Test Folder');
cy.findByRole('button', {name: 'Create'}).click();

//Navigating inside the added folder in the Attachments folder
cy.get('[title="Test Folder"] > span').click();

//To check if the URL is correct after visiting the Test Folder
cy.location('pathname').should('eq', '/app/file/view/home/Attachments/Test%20Folder');
cy.visit('/app/file/view/home/Attachments/Test%20Folder');

//Adding a file inside the Test Folder
cy.findByRole('button', {name: 'Add File'}).eq(0).click({force: true});
cy.get('.file-uploader').findByText('Link').click();
cy.get('.input-group > .form-control').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
cy.findByRole('button', {name: 'Upload'}).click();

//To check if the added file is present in the Test Folder
cy.get('span.level-item > span').should('contain', 'Test Folder');
cy.get('.list-row-container').eq(0).should('contain.text', '72402.jpg');
cy.get('.list-row-checkbox').eq(0).click();

//Deleting the added file from the Test folder
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.wait(700);
cy.findByRole('button', {name: 'Yes'}).click();
cy.wait(700);

//Deleting the Test Folder
cy.visit('/app/file/view/home/Attachments');
cy.get('.list-row-checkbox').eq(0).click();
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.findByRole('button', {name: 'Yes'}).click();
});

it('Deleting Test Folder from the home', () => {
//Deleting the Test Folder added in the home directory
cy.visit('/app/file/view/home');
cy.get('.level-left > .list-subject > .list-row-checkbox').eq(0).click({force: true, delay: 500});
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.findByRole('button', {name: 'Yes'}).click();
});
});

+ 2
- 1
cypress/integration/form.js 查看文件

@@ -18,6 +18,7 @@ context('Form', () => {
cy.get('.primary-action').click(); cy.get('.primary-action').click();
cy.wait('@form_save').its('response.statusCode').should('eq', 200); cy.wait('@form_save').its('response.statusCode').should('eq', 200);
cy.visit('/app/todo'); cy.visit('/app/todo');
cy.wait(300);
cy.get('.title-text').should('be.visible').and('contain', 'To Do'); cy.get('.title-text').should('be.visible').and('contain', 'To Do');
cy.get('.list-row').should('contain', 'this is a test todo'); cy.get('.list-row').should('contain', 'this is a test todo');
}); });
@@ -25,7 +26,7 @@ context('Form', () => {
cy.visit('/app/contact'); cy.visit('/app/contact');
cy.add_filter(); cy.add_filter();
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true }); 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.visit('/app/contact/Test Form Contact 3');
cy.get('.prev-doc').should('be.visible').click(); cy.get('.prev-doc').should('be.visible').click();
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible'); cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');


+ 88
- 0
cypress/integration/form_tour.js 查看文件

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

});
});

+ 2
- 2
cypress/integration/grid_pagination.js 查看文件

@@ -30,12 +30,12 @@ context('Grid Pagination', () => {
it('adds and deletes rows and changes page', () => { it('adds and deletes rows and changes page', () => {
cy.visit('/app/contact/Test Contact'); cy.visit('/app/contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); 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('.grid-body .row-index').should('contain', 1001);
cy.get('@table').find('.current-page-number').should('contain', '21'); cy.get('@table').find('.current-page-number').should('contain', '21');
cy.get('@table').find('.total-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('.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('.grid-body .row-index').last().should('contain', 1000);
cy.get('@table').find('.current-page-number').should('contain', '20'); cy.get('@table').find('.current-page-number').should('contain', '20');
cy.get('@table').find('.total-page-number').should('contain', '20'); cy.get('@table').find('.total-page-number').should('contain', '20');


+ 2
- 2
cypress/integration/list_view.js 查看文件

@@ -7,11 +7,11 @@ context('List View', () => {
}); });
}); });
it('enables "Actions" button', () => { it('enables "Actions" button', () => {
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete'];
const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete'];
cy.go_to_list('ToDo'); cy.go_to_list('ToDo');
cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true }); cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true });
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => {
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 9).each((el, index) => {
cy.wrap(el).contains(actions[index]); cy.wrap(el).contains(actions[index]);
}).then((elements) => { }).then((elements) => {
cy.intercept({ cy.intercept({


+ 6
- 6
cypress/integration/list_view_settings.js 查看文件

@@ -17,9 +17,9 @@ context('List View Settings', () => {
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click(); cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
cy.get('.modal-dialog').should('contain', 'DocType Settings'); 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 }); cy.reload({ force: true });


@@ -29,8 +29,8 @@ context('List View Settings', () => {
cy.get('.menu-btn-group button').click({ force: true }); cy.get('.menu-btn-group button').click({ force: true });
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click(); cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
cy.get('.modal-dialog').should('contain', 'DocType Settings'); 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();
}); });
}); });

+ 6
- 6
cypress/integration/login.js 查看文件

@@ -11,13 +11,13 @@ context('Login', () => {


it('validates password', () => { it('validates password', () => {
cy.get('#login_email').type('Administrator'); cy.get('#login_email').type('Administrator');
cy.get('.btn-login:visible').click();
cy.findByRole('button', {name: 'Login'}).click();
cy.location('pathname').should('eq', '/login'); cy.location('pathname').should('eq', '/login');
}); });


it('validates email', () => { it('validates email', () => {
cy.get('#login_password').type('qwe'); cy.get('#login_password').type('qwe');
cy.get('.btn-login:visible').click();
cy.findByRole('button', {name: 'Login'}).click();
cy.location('pathname').should('eq', '/login'); cy.location('pathname').should('eq', '/login');
}); });


@@ -25,8 +25,8 @@ context('Login', () => {
cy.get('#login_email').type('Administrator'); cy.get('#login_email').type('Administrator');
cy.get('#login_password').type('qwer'); 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'); cy.location('pathname').should('eq', '/login');
}); });


@@ -34,7 +34,7 @@ context('Login', () => {
cy.get('#login_email').type('Administrator'); cy.get('#login_email').type('Administrator');
cy.get('#login_password').type(Cypress.config('adminPassword')); 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.location('pathname').should('eq', '/app');
cy.window().its('frappe.session.user').should('eq', 'Administrator'); cy.window().its('frappe.session.user').should('eq', 'Administrator');
}); });
@@ -60,7 +60,7 @@ context('Login', () => {
cy.get('#login_email').type('Administrator'); cy.get('#login_email').type('Administrator');
cy.get('#login_password').type(Cypress.config('adminPassword')); 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 // verify redirected location and url params after login
cy.url().should('include', '/me?' + payload.toString().replace('+', '%20')); cy.url().should('include', '/me?' + payload.toString().replace('+', '%20'));


+ 14
- 0
cypress/integration/navigation.js 查看文件

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

+ 8
- 8
cypress/integration/recorder.js 查看文件

@@ -16,24 +16,24 @@ context('Recorder', () => {
it('Navigate to Recorder', () => { it('Navigate to Recorder', () => {
cy.visit('/app'); cy.visit('/app');
cy.awesomebar('recorder'); cy.awesomebar('recorder');
cy.get('h3').should('contain', 'Recorder');
cy.findByTitle('Recorder').should('exist');
cy.url().should('include', '/recorder/detail'); cy.url().should('include', '/recorder/detail');
}); });


it('Recorder Empty State', () => { 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('.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').should('contain', 'Inactive');
cy.get('.msg-box .btn-primary').should('contain', 'Start Recording');
cy.findByRole('button', {name: 'Start Recording'}).should('exist');
}); });


it('Recorder Start', () => { 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('.indicator-pill').should('contain', 'Active').should('have.class', 'green');


cy.get('.msg-box').should('contain', 'No Requests'); cy.get('.msg-box').should('contain', 'No Requests');
@@ -46,12 +46,12 @@ context('Recorder', () => {
cy.get('.list-count').should('contain', '20 of '); cy.get('.list-count').should('contain', '20 of ');


cy.visit('/app/recorder'); 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'); cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
}); });


it('Recorder View Request', () => { 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.visit('/app/List/DocType/List');
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');


+ 1
- 1
cypress/integration/report_view.js 查看文件

@@ -23,7 +23,7 @@ context('Report View', () => {
let cell = cy.get('.dt-row-0 > .dt-cell--col-4'); let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
// select the cell // select the cell
cell.dblclick(); 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.get('.dt-row-0 > .dt-cell--col-5').click();
cy.wait('@value-update'); cy.wait('@value-update');
cy.get('@doc').then(doc => { cy.get('@doc').then(doc => {


+ 56
- 0
cypress/integration/sidebar.js 查看文件

@@ -0,0 +1,56 @@
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("Assigned To");

//To check if no filter is available in "Assigned To" dropdown
cy.get('.empty-state').should('contain', 'No filters found');

cy.click_sidebar_button("Created By");

//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("Assigned To");

//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%');
cy.click_filter_button();

//To remove the applied filter
cy.clear_filters();

//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.hide_dialog();
cy.visit('/app/doctype');
cy.click_sidebar_button("Assigned To");
cy.get('.empty-state').should('contain', 'No filters found');
});
});

+ 1
- 0
cypress/integration/table_multiselect.js 查看文件

@@ -9,6 +9,7 @@ context('Table MultiSelect', () => {
cy.new_form('Assignment Rule'); cy.new_form('Assignment Rule');
cy.fill_field('__newname', name); cy.fill_field('__newname', name);
cy.fill_field('document_type', 'Blog Post'); cy.fill_field('document_type', 'Blog Post');
cy.get('.section-head').contains('Assignment Rules').scrollIntoView();
cy.fill_field('assign_condition', 'status=="Open"', 'Code'); cy.fill_field('assign_condition', 'status=="Open"', 'Code');
cy.get('input[data-fieldname="users"]').focus().as('input'); cy.get('input[data-fieldname="users"]').focus().as('input');
cy.get('input[data-fieldname="users"] + ul').should('be.visible'); cy.get('input[data-fieldname="users"] + ul').should('be.visible');


+ 94
- 0
cypress/integration/timeline.js 查看文件

@@ -0,0 +1,94 @@
import custom_submittable_doctype from '../fixtures/custom_submittable_doctype';

context('Timeline', () => {
before(() => {
cy.visit('/login');
cy.login();
});

it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => {
//Adding new ToDo
cy.visit('/app/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("Edit");
cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(' 123');
cy.click_timeline_action_btn("Save");

//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("Edit");
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 > .dropdown-item').contains("Delete").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
- 0
cypress/integration/timeline_email.js 查看文件

@@ -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
- 0
cypress/integration/workspace.js 查看文件

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

});

+ 37
- 6
cypress/support/commands.js 查看文件

@@ -1,4 +1,5 @@
import 'cypress-file-upload'; import 'cypress-file-upload';
import '@testing-library/cypress/add-commands';
// *********************************************** // ***********************************************
// This example commands.js shows you how to // This example commands.js shows you how to
// create various custom commands and overwrite // create various custom commands and overwrite
@@ -186,22 +187,22 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') { if (fieldtype === 'Select') {
cy.get('@input').select(value); cy.get('@input').select(value);
} else { } else {
cy.get('@input').type(value, {waitForAnimations: false, force: true});
cy.get('@input').type(value, {waitForAnimations: false, force: true, delay: 100});
} }
return cy.get('@input'); return cy.get('@input');
}); });


Cypress.Commands.add('get_field', (fieldname, 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') { if (fieldtype === 'Text Editor') {
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]:visible`;
} }
if (fieldtype === 'Code') { if (fieldtype === 'Code') {
selector = `[data-fieldname="${fieldname}"] .ace_text-input`; 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') => { Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
@@ -251,7 +252,8 @@ Cypress.Commands.add('new_form', doctype => {
}); });


Cypress.Commands.add('go_to_list', doctype => { Cypress.Commands.add('go_to_list', doctype => {
cy.visit(`/app/list/${doctype}/list`);
let dt_in_route = doctype.toLowerCase().replace(/ /g, '-');
cy.visit(`/app/${dt_in_route}`);
}); });


Cypress.Commands.add('clear_cache', () => { Cypress.Commands.add('clear_cache', () => {
@@ -315,7 +317,11 @@ Cypress.Commands.add('add_filter', () => {
}); });


Cypress.Commands.add('clear_filters', () => { Cypress.Commands.add('clear_filters', () => {
cy.get('.filter-section .filter-button').click();
cy.intercept({
method: 'POST',
url: 'api/method/frappe.model.utils.user_settings.save'
}).as('filter-saved');
cy.get('.filter-section .filter-button').click({force: true});
cy.wait(300); cy.wait(300);
cy.get('.filter-popover').should('exist'); cy.get('.filter-popover').should('exist');
cy.get('.filter-popover').find('.clear-filters').click(); cy.get('.filter-popover').find('.clear-filters').click();
@@ -323,4 +329,29 @@ Cypress.Commands.add('clear_filters', () => {
cy.window().its('cur_list').then(cur_list => { cy.window().its('cur_list').then(cur_list => {
cur_list && cur_list.filter_area && cur_list.filter_area.clear(); cur_list && cur_list.filter_area && cur_list.filter_area.clear();
}); });
cy.wait('@filter-saved');
});

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_name) => {
cy.get('.list-group-by-fields .list-link > a').contains(btn_name).click({force: true});
});

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_name) => {
cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').contains(btn_name).click();
});

+ 81
- 31
esbuild/esbuild.js 查看文件

@@ -8,6 +8,7 @@ let yargs = require("yargs");
let cliui = require("cliui")(); let cliui = require("cliui")();
let chalk = require("chalk"); let chalk = require("chalk");
let html_plugin = require("./frappe-html"); let html_plugin = require("./frappe-html");
let rtlcss = require('rtlcss');
let postCssPlugin = require("esbuild-plugin-postcss2").default; let postCssPlugin = require("esbuild-plugin-postcss2").default;
let ignore_assets = require("./ignore-assets"); let ignore_assets = require("./ignore-assets");
let sass_options = require("./sass_options"); let sass_options = require("./sass_options");
@@ -96,9 +97,9 @@ async function execute() {
await clean_dist_folders(APPS); await clean_dist_folders(APPS);
} }


let result;
let results;
try { try {
result = await build_assets_for_apps(APPS, FILES_TO_BUILD);
results = await build_assets_for_apps(APPS, FILES_TO_BUILD);
} catch (e) { } catch (e) {
log_error("There were some problems during build"); log_error("There were some problems during build");
log(); log();
@@ -107,13 +108,15 @@ async function execute() {
} }


if (!WATCH_MODE) { if (!WATCH_MODE) {
log_built_assets(result.metafile);
log_built_assets(results);
console.timeEnd(TOTAL_BUILD_TIME); console.timeEnd(TOTAL_BUILD_TIME);
log(); log();
} else { } else {
log("Watching for changes..."); 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) { function build_assets_for_apps(apps, files) {
@@ -125,6 +128,8 @@ function build_assets_for_apps(apps, files) {
let output_path = assets_path; let output_path = assets_path;


let file_map = {}; let file_map = {};
let style_file_map = {};
let rtl_style_file_map = {};
for (let file of files) { for (let file of files) {
let relative_app_path = path.relative(apps_path, file); let relative_app_path = path.relative(apps_path, file);
let app = relative_app_path.split(path.sep)[0]; 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); 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( log_warn(
`Duplicate output file ${output_name} generated from ${file}` `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, files: file_map,
outdir: output_path 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 }) { 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, entryPoints: files,
entryNames: "[dir]/[name].[hash]", entryNames: "[dir]/[name].[hash]",
outdir, outdir,
@@ -217,17 +261,9 @@ function build_files({ files, outdir }) {
PRODUCTION ? "production" : "development" PRODUCTION ? "production" : "development"
) )
}, },
plugins: [
html_plugin,
ignore_assets,
vue(),
postCssPlugin({
plugins: [require("autoprefixer")],
sassOptions: sass_options
})
],
plugins: plugins,
watch: get_watch_config() watch: get_watch_config()
});
};
} }


function get_watch_config() { function get_watch_config() {
@@ -258,16 +294,26 @@ function get_watch_config() {
async function clean_dist_folders(apps) { async function clean_dist_folders(apps) {
for (let app of apps) { for (let app of apps) {
let public_path = get_public_path(app); 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]; let column_widths = [60, 20];
cliui.div( cliui.div(
{ {
@@ -282,9 +328,9 @@ function log_built_assets(metafile) {
cliui.div(""); cliui.div("");


let output_by_dist_path = {}; let output_by_dist_path = {};
for (let outfile in metafile.outputs) {
for (let outfile in outputs) {
if (outfile.endsWith(".map")) continue; if (outfile.endsWith(".map")) continue;
let data = metafile.outputs[outfile];
let data = outputs[outfile];
outfile = path.resolve(outfile); outfile = path.resolve(outfile);
outfile = path.relative(assets_path, outfile); outfile = path.relative(assets_path, outfile);
let filename = path.basename(outfile); let filename = path.basename(outfile);
@@ -339,7 +385,11 @@ async function write_assets_json(metafile) {
let info = metafile.outputs[output]; let info = metafile.outputs[output];
let asset_path = "/" + path.relative(sites_path, output); let asset_path = "/" + path.relative(sites_path, output);
if (info.entryPoint) { 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;
} }
} }


@@ -478,4 +528,4 @@ function log_rebuilt_assets(prev_assets, new_assets) {
log(" " + filename); log(" " + filename);
} }
log(); log();
}
}

+ 13
- 4
frappe/__init__.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE
""" """
Frappe - Low Code Open Source Framework in Python and JS Frappe - Low Code Open Source Framework in Python and JS


@@ -28,6 +28,8 @@ from .exceptions import *
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader) from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
from .utils.lazy_loader import lazy_import from .utils.lazy_loader import lazy_import


from frappe.query_builder import get_query_builder, patch_query_execute

# Lazy imports # Lazy imports
faker = lazy_import('faker') faker = lazy_import('faker')


@@ -118,6 +120,7 @@ def set_user_lang(user, user_language=None):


# local-globals # local-globals
db = local("db") db = local("db")
qb = local("qb")
conf = local("conf") conf = local("conf")
form = form_dict = local("form_dict") form = form_dict = local("form_dict")
request = local("request") request = local("request")
@@ -137,7 +140,11 @@ lang = local("lang")
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from frappe.database.mariadb.database import MariaDBDatabase from frappe.database.mariadb.database import MariaDBDatabase
from frappe.database.postgres.database import PostgresDatabase from frappe.database.postgres.database import PostgresDatabase
from pypika import Query

db: typing.Union[MariaDBDatabase, PostgresDatabase] db: typing.Union[MariaDBDatabase, PostgresDatabase]
qb: Query

# end: static analysis hack # end: static analysis hack


def init(site, sites_path=None, new_site=False): def init(site, sites_path=None, new_site=False):
@@ -202,8 +209,10 @@ def init(site, sites_path=None, new_site=False):
local.form_dict = _dict() local.form_dict = _dict()
local.session = _dict() local.session = _dict()
local.dev_server = _dev_server local.dev_server = _dev_server
local.qb = get_query_builder(local.conf.db_type or "mariadb")


setup_module_map() setup_module_map()
patch_query_execute()


local.initialised = True local.initialised = True


@@ -1491,7 +1500,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
:param style: Print Format style. :param style: Print Format style.
:param as_pdf: Return as PDF. Default False. :param as_pdf: Return as PDF. Default False.
:param password: Password to encrypt the pdf with. Default None""" :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 from frappe.utils.pdf import get_pdf


local.form_dict.doctype = doctype local.form_dict.doctype = doctype
@@ -1506,7 +1515,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
options = {'password': password} options = {'password': password}


if not html: if not html:
html = build_page("printview")
html = get_response_content("printview")


if as_pdf: if as_pdf:
return get_pdf(html, output = output, options = options) return get_pdf(html, output = output, options = options)
@@ -1683,7 +1692,7 @@ def get_desk_link(doctype, name):
) )


def bold(text): 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): def safe_eval(code, eval_globals=None, eval_locals=None):
'''A safer `eval`''' '''A safer `eval`'''


+ 2
- 2
frappe/api.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE
import base64 import base64
import binascii import binascii
import json import json
@@ -82,7 +82,7 @@ def handle():
if frappe.local.request.method=="PUT": if frappe.local.request.method=="PUT":
data = get_request_form_data() data = get_request_form_data()


doc = frappe.get_doc(doctype, name)
doc = frappe.get_doc(doctype, name, for_update=True)


if "flags" in data: if "flags" in data:
del data["flags"] del data["flags"]


+ 4
- 5
frappe/app.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE


import os import os
import logging import logging
@@ -16,9 +16,9 @@ import frappe.handler
import frappe.auth import frappe.auth
import frappe.api import frappe.api
import frappe.utils.response import frappe.utils.response
import frappe.website.render
from frappe.utils import get_site_name, sanitize_html from frappe.utils import get_site_name, sanitize_html
from frappe.middlewares import StaticDataMiddleware from frappe.middlewares import StaticDataMiddleware
from frappe.website.serve import get_response
from frappe.utils.error import make_error_snapshot from frappe.utils.error import make_error_snapshot
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe import _ from frappe import _
@@ -72,7 +72,7 @@ def application(request):
response = frappe.utils.response.download_private_file(request.path) response = frappe.utils.response.download_private_file(request.path)


elif request.method in ('GET', 'HEAD', 'POST'): elif request.method in ('GET', 'HEAD', 'POST'):
response = frappe.website.render.render()
response = get_response()


else: else:
raise NotFound raise NotFound
@@ -266,8 +266,7 @@ def handle_exception(e):
make_error_snapshot(e) make_error_snapshot(e)


if return_as_message: 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 return response




+ 63
- 49
frappe/auth.py 查看文件

@@ -1,71 +1,82 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import datetime
from frappe import _
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See LICENSE
from urllib.parse import quote
import frappe import frappe
import frappe.database import frappe.database
import frappe.utils import frappe.utils
from frappe.utils import cint, flt, get_datetime, datetime, date_diff, today
import frappe.utils.user 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.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 frappe.website.utils import get_home_page
from urllib.parse import quote




class HTTPRequest: class HTTPRequest:
def __init__(self): def __init__(self):
# Get Environment variables
self.domain = frappe.request.host
if self.domain and self.domain.startswith('www.'):
self.domain = self.domain[4:]

if frappe.get_request_header('X-Forwarded-For'):
frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip()

elif frappe.get_request_header('REMOTE_ADDR'):
frappe.local.request_ip = frappe.get_request_header('REMOTE_ADDR')

else:
frappe.local.request_ip = '127.0.0.1'

# language
self.set_lang()
# set frappe.local.request_ip
self.set_request_ip()


# load cookies # load cookies
frappe.local.cookie_manager = CookieManager()
self.set_cookies()


# set db
# set frappe.local.db
self.connect() self.connect()


# login
frappe.local.login_manager = LoginManager()
# login and start/resume user session
self.set_session()


if frappe.form_dict._lang:
lang = get_lang_code(frappe.form_dict._lang)
if lang:
frappe.local.lang = lang
# set request language
self.set_lang()


# match csrf token from current session
self.validate_csrf_token() self.validate_csrf_token()


# write out latest cookies # write out latest cookies
frappe.local.cookie_manager.init_cookies() frappe.local.cookie_manager.init_cookies()


# check status
# check session status
check_session_stopped() 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()

elif frappe.get_request_header('REMOTE_ADDR'):
frappe.local.request_ip = frappe.get_request_header('REMOTE_ADDR')

else:
frappe.local.request_ip = '127.0.0.1'

def set_cookies(self):
frappe.local.cookie_manager = CookieManager()

def set_session(self):
frappe.local.login_manager = LoginManager()

def validate_csrf_token(self): def validate_csrf_token(self):
if frappe.local.request and frappe.local.request.method in ("POST", "PUT", "DELETE"): 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 # not via boot
return return


@@ -79,17 +90,18 @@ class HTTPRequest:
frappe.throw(_("Invalid Request"), frappe.CSRFTokenError) frappe.throw(_("Invalid Request"), frappe.CSRFTokenError)


def set_lang(self): 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): def get_db_name(self):
"""get database name from conf""" """get database name from conf"""
return conf.db_name return conf.db_name


def connect(self, ac_name = None):
def connect(self):
"""connect to db, from ac_name or db_name""" """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: class LoginManager:
def __init__(self): def __init__(self):
@@ -143,7 +155,7 @@ class LoginManager:
self.setup_boot_cache() self.setup_boot_cache()
self.set_user_info() 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, self.info = frappe.db.get_value("User", self.user,
["user_type", "first_name", "last_name", "user_image"], as_dict=1) ["user_type", "first_name", "last_name", "user_image"], as_dict=1)


@@ -181,11 +193,13 @@ class LoginManager:
frappe.local.response["redirect_to"] = redirect_to frappe.local.response["redirect_to"] = redirect_to
frappe.cache().hdel('redirect_after_login', self.user) 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("full_name", self.full_name)
frappe.local.cookie_manager.set_cookie("user_id", self.user) frappe.local.cookie_manager.set_cookie("user_id", self.user)
frappe.local.cookie_manager.set_cookie("user_image", self.info.user_image or "") 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): def make_session(self, resume=False):
# start session # start session
frappe.local.session_obj = Session(user=self.user, resume=resume, frappe.local.session_obj = Session(user=self.user, resume=resume,


+ 6
- 3
frappe/automation/doctype/assignment_rule/assignment_rule.json 查看文件

@@ -72,6 +72,7 @@
"fieldtype": "Code", "fieldtype": "Code",
"in_list_view": 1, "in_list_view": 1,
"label": "Assign Condition", "label": "Assign Condition",
"options": "PythonExpression",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -82,7 +83,8 @@
"description": "Simple Python Expression, Example: Status in (\"Closed\", \"Cancelled\")", "description": "Simple Python Expression, Example: Status in (\"Closed\", \"Cancelled\")",
"fieldname": "unassign_condition", "fieldname": "unassign_condition",
"fieldtype": "Code", "fieldtype": "Code",
"label": "Unassign Condition"
"label": "Unassign Condition",
"options": "PythonExpression"
}, },
{ {
"fieldname": "assign_to_users_section", "fieldname": "assign_to_users_section",
@@ -120,7 +122,8 @@
"description": "Simple Python Expression, Example: Status in (\"Invalid\")", "description": "Simple Python Expression, Example: Status in (\"Invalid\")",
"fieldname": "close_condition", "fieldname": "close_condition",
"fieldtype": "Code", "fieldtype": "Code",
"label": "Close Condition"
"label": "Close Condition",
"options": "PythonExpression"
}, },
{ {
"fieldname": "sb", "fieldname": "sb",
@@ -151,7 +154,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-10-20 14:47:20.662954",
"modified": "2021-07-16 22:51:35.505575",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Automation", "module": "Automation",
"name": "Assignment Rule", "name": "Assignment Rule",


+ 1
- 1
frappe/automation/doctype/assignment_rule/assignment_rule.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 5
- 5
frappe/automation/doctype/assignment_rule/test_assignment_rule.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
# License: MIT. See LICENSE
import frappe import frappe
import unittest import unittest
from frappe.utils import random_string from frappe.utils import random_string
@@ -76,7 +76,7 @@ class TestAutoAssign(unittest.TestCase):
# clear 5 assignments for first user # clear 5 assignments for first user
# can't do a limit in "delete" since postgres does not support it # 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): 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 # add 5 more assignments
for i in range(5): for i in range(5):
@@ -177,7 +177,7 @@ class TestAutoAssign(unittest.TestCase):
), 'owner'), 'test@example.com') ), 'owner'), 'test@example.com')


def check_assignment_rule_scheduling(self): 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')] days_1 = [dict(day = 'Sunday'), dict(day = 'Monday'), dict(day = 'Tuesday')]


@@ -204,7 +204,7 @@ class TestAutoAssign(unittest.TestCase):
), 'owner'), ['test3@example.com']) ), 'owner'), ['test3@example.com'])


def test_assignment_rule_condition(self): def test_assignment_rule_condition(self):
frappe.db.sql("DELETE FROM `tabAssignment Rule`")
frappe.db.delete("Assignment Rule")


# Add expiry_date custom field # Add expiry_date custom field
from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.custom_field.custom_field import create_custom_field
@@ -253,7 +253,7 @@ class TestAutoAssign(unittest.TestCase):
assignment_rule.delete() assignment_rule.delete()


def clear_assignments(): 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): def get_assignment_rule(days, assign=None):
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1') frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1')


+ 1
- 1
frappe/automation/doctype/assignment_rule_day/assignment_rule_day.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 1
- 1
frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 1
- 1
frappe/automation/doctype/auto_repeat/auto_repeat.js 查看文件

@@ -30,7 +30,7 @@ frappe.ui.form.on('Auto Repeat', {
refresh: function(frm) { refresh: function(frm) {
// auto repeat message // auto repeat message
if (frm.is_new()) { 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])); frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link]));
} }




+ 2
- 2
frappe/automation/doctype/auto_repeat/auto_repeat.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


import frappe import frappe
from frappe import _ from frappe import _
@@ -333,7 +333,7 @@ class AutoRepeat(Document):
if self.reference_doctype and self.reference_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_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']) 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: if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True) frappe.msgprint(_('No contacts linked to document'), alert=True)
else: else:


+ 1
- 1
frappe/automation/doctype/auto_repeat/test_auto_repeat.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors # Copyright (c) 2018, Frappe Technologies and Contributors
# See license.txt
# License: MIT. See LICENSE
import unittest import unittest


import frappe import frappe


+ 1
- 1
frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors # Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 1
- 1
frappe/automation/doctype/milestone/milestone.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 1
- 1
frappe/automation/doctype/milestone/test_milestone.py 查看文件

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




+ 1
- 1
frappe/automation/doctype/milestone_tracker/milestone_tracker.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 3
- 3
frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py 查看文件

@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
# License: MIT. See LICENSE
import frappe import frappe
import frappe.cache_manager import frappe.cache_manager
import unittest import unittest


class TestMilestoneTracker(unittest.TestCase): class TestMilestoneTracker(unittest.TestCase):
def test_milestone(self): def test_milestone(self):
frappe.db.sql('delete from `tabMilestone Tracker`')
frappe.db.delete("Milestone Tracker")


frappe.cache().delete_key('milestone_tracker_map') frappe.cache().delete_key('milestone_tracker_map')


@@ -44,5 +44,5 @@ class TestMilestoneTracker(unittest.TestCase):
self.assertEqual(milestones[0].value, 'Closed') self.assertEqual(milestones[0].value, 'Closed')


# cleanup # cleanup
frappe.db.sql('delete from tabMilestone')
frappe.db.delete("Milestone")
milestone_tracker.delete() milestone_tracker.delete()

+ 34
- 4
frappe/automation/workspace/tools/tools.json 查看文件

@@ -1,22 +1,27 @@
{ {
"category": "Administration",
"category": "",
"charts": [], "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", "creation": "2020-03-02 14:53:24.980279",
"developer_mode_only": 0, "developer_mode_only": 0,
"disable_user_customization": 0, "disable_user_customization": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace", "doctype": "Workspace",
"extends": "",
"extends_another_page": 0, "extends_another_page": 0,
"for_user": "",
"hide_custom": 0, "hide_custom": 0,
"icon": "tool", "icon": "tool",
"idx": 0, "idx": 0,
"is_standard": 1,
"is_default": 0,
"is_standard": 0,
"label": "Tools", "label": "Tools",
"links": [ "links": [
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Tools", "label": "Tools",
"link_count": 0,
"onboard": 0, "onboard": 0,
"type": "Card Break" "type": "Card Break"
}, },
@@ -25,6 +30,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "To Do", "label": "To Do",
"link_count": 0,
"link_to": "ToDo", "link_to": "ToDo",
"link_type": "DocType", "link_type": "DocType",
"onboard": 1, "onboard": 1,
@@ -35,6 +41,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Calendar", "label": "Calendar",
"link_count": 0,
"link_to": "Event", "link_to": "Event",
"link_type": "DocType", "link_type": "DocType",
"onboard": 1, "onboard": 1,
@@ -45,6 +52,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Note", "label": "Note",
"link_count": 0,
"link_to": "Note", "link_to": "Note",
"link_type": "DocType", "link_type": "DocType",
"onboard": 1, "onboard": 1,
@@ -55,6 +63,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Files", "label": "Files",
"link_count": 0,
"link_to": "File", "link_to": "File",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
@@ -65,6 +74,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Activity", "label": "Activity",
"link_count": 0,
"link_to": "activity", "link_to": "activity",
"link_type": "Page", "link_type": "Page",
"onboard": 0, "onboard": 0,
@@ -74,6 +84,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Email", "label": "Email",
"link_count": 0,
"onboard": 0, "onboard": 0,
"type": "Card Break" "type": "Card Break"
}, },
@@ -82,6 +93,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Newsletter", "label": "Newsletter",
"link_count": 0,
"link_to": "Newsletter", "link_to": "Newsletter",
"link_type": "DocType", "link_type": "DocType",
"onboard": 1, "onboard": 1,
@@ -92,6 +104,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Email Group", "label": "Email Group",
"link_count": 0,
"link_to": "Email Group", "link_to": "Email Group",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
@@ -101,6 +114,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Automation", "label": "Automation",
"link_count": 0,
"onboard": 0, "onboard": 0,
"type": "Card Break" "type": "Card Break"
}, },
@@ -109,6 +123,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Assignment Rule", "label": "Assignment Rule",
"link_count": 0,
"link_to": "Assignment Rule", "link_to": "Assignment Rule",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
@@ -119,6 +134,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Milestone", "label": "Milestone",
"link_count": 0,
"link_to": "Milestone", "link_to": "Milestone",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
@@ -129,6 +145,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Auto Repeat", "label": "Auto Repeat",
"link_count": 0,
"link_to": "Auto Repeat", "link_to": "Auto Repeat",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
@@ -138,6 +155,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Event Streaming", "label": "Event Streaming",
"link_count": 0,
"onboard": 0, "onboard": 0,
"type": "Card Break" "type": "Card Break"
}, },
@@ -146,6 +164,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Event Producer", "label": "Event Producer",
"link_count": 0,
"link_to": "Event Producer", "link_to": "Event Producer",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
@@ -156,6 +175,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Event Consumer", "label": "Event Consumer",
"link_count": 0,
"link_to": "Event Consumer", "link_to": "Event Consumer",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
@@ -166,6 +186,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Event Update Log", "label": "Event Update Log",
"link_count": 0,
"link_to": "Event Update Log", "link_to": "Event Update Log",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
@@ -176,6 +197,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Event Sync Log", "label": "Event Sync Log",
"link_count": 0,
"link_to": "Event Sync Log", "link_to": "Event Sync Log",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
@@ -186,19 +208,26 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Document Type Mapping", "label": "Document Type Mapping",
"link_count": 0,
"link_to": "Document Type Mapping", "link_to": "Document Type Mapping",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
} }
], ],
"modified": "2020-12-01 13:38:39.950350",
"modified": "2021-08-05 12:16:02.839180",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Automation", "module": "Automation",
"name": "Tools", "name": "Tools",
"onboarding": "",
"owner": "Administrator", "owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0, "pin_to_bottom": 0,
"pin_to_top": 0, "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
"sequence_id": 26,
"shortcuts": [ "shortcuts": [
{ {
"label": "ToDo", "label": "ToDo",
@@ -225,5 +254,6 @@
"link_to": "Auto Repeat", "link_to": "Auto Repeat",
"type": "DocType" "type": "DocType"
} }
]
],
"title": "Tools"
} }

+ 3
- 3
frappe/boot.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE
""" """
bootstrap client session bootstrap client session
""" """
@@ -105,8 +105,8 @@ def load_conf_settings(bootinfo):
if key in conf: bootinfo[key] = conf.get(key) if key in conf: bootinfo[key] = conf.get(key)


def load_desktop_data(bootinfo): 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.module_page_map = get_controller("Workspace").get_module_page_map()
bootinfo.dashboards = frappe.get_all("Dashboard") bootinfo.dashboards = frappe.get_all("Dashboard")




+ 1
- 1
frappe/build.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE
import os import os
import re import re
import json import json


+ 8
- 13
frappe/cache_manager.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE


import frappe, json import frappe, json
from frappe.model.document import Document from frappe.model.document import Document
@@ -53,7 +53,7 @@ def clear_domain_cache(user=None):
cache.delete_value(domain_cache_keys) cache.delete_value(domain_cache_keys)


def clear_global_cache(): 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_doctype_cache()
clear_website_cache() clear_website_cache()
@@ -141,18 +141,13 @@ def build_table_count_cache():
return return


_cache = frappe.cache() _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} counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data}
_cache.set_value("information_schema:counts", counts) _cache.set_value("information_schema:counts", counts)




+ 1
- 1
frappe/chat/doctype/chat_token/chat_token.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors # Copyright (c) 2018, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 1
- 1
frappe/client.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE
import frappe import frappe
from frappe import _ from frappe import _
import frappe.model import frappe.model


+ 4
- 2
frappe/commands/__init__.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE


import sys import sys
import click import click
@@ -102,7 +102,9 @@ def get_commands():
from .site import commands as site_commands from .site import commands as site_commands
from .translate import commands as translate_commands from .translate import commands as translate_commands
from .utils import commands as utils_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() commands = get_commands()

+ 53
- 0
frappe/commands/redis.py 查看文件

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

+ 6
- 2
frappe/commands/scheduler.py 查看文件

@@ -172,9 +172,13 @@ def start_scheduler():
@click.command('worker') @click.command('worker')
@click.option('--queue', type=str) @click.option('--queue', type=str)
@click.option('--quiet', is_flag = True, default = False, help = 'Hide Log Outputs') @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 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.command('ready-for-migration')
@click.option('--site', help='site name') @click.option('--site', help='site name')


+ 41
- 16
frappe/commands/site.py 查看文件

@@ -193,7 +193,7 @@ def install_app(context, apps):
print("App {} is Incompatible with Site {}{}".format(app, site, err_msg)) print("App {} is Incompatible with Site {}{}".format(app, site, err_msg))
exit_code = 1 exit_code = 1
except Exception as err: 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)) print("An error occurred while installing {}{}".format(app, err_msg))
exit_code = 1 exit_code = 1


@@ -561,30 +561,54 @@ def move(dest_dir, site):
return final_new_path return final_new_path




@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_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.command('set-admin-password')
@click.argument('admin-password')
@click.argument('admin-password', required=False)
@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False) @click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
@pass_context @pass_context
def set_admin_password(context, admin_password, logout_all_sessions=False):
def set_admin_password(context, admin_password=None, logout_all_sessions=False):
"Set Administrator password for a site" "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 import getpass
from frappe.utils.password import update_password 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.command('set-last-active-for-user')
@click.option('--user', help="Setup last active date for user") @click.option('--user', help="Setup last active date for user")
@@ -729,6 +753,7 @@ commands = [
remove_from_installed_apps, remove_from_installed_apps,
restore, restore,
run_patch, run_patch,
set_password,
set_admin_password, set_admin_password,
uninstall, uninstall,
disable_user, disable_user,


+ 113
- 123
frappe/commands/utils.py 查看文件

@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

import json import json
import os import os
import subprocess import subprocess
@@ -11,7 +9,14 @@ import click
import frappe import frappe
from frappe.commands import get_site, pass_context from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError 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') @click.command('build')
@@ -69,14 +74,14 @@ def watch(apps=None):
def clear_cache(context): def clear_cache(context):
"Clear cache, doctype cache and defaults" "Clear cache, doctype cache and defaults"
import frappe.sessions import frappe.sessions
import frappe.website.render
from frappe.website.utils import clear_website_cache
from frappe.desk.notifications import clear_notifications from frappe.desk.notifications import clear_notifications
for site in context.sites: for site in context.sites:
try: try:
frappe.connect(site) frappe.connect(site)
frappe.clear_cache() frappe.clear_cache()
clear_notifications() clear_notifications()
frappe.website.render.clear_cache()
clear_website_cache()
finally: finally:
frappe.destroy() frappe.destroy()
if not context.sites: if not context.sites:
@@ -86,12 +91,12 @@ def clear_cache(context):
@pass_context @pass_context
def clear_website_cache(context): def clear_website_cache(context):
"Clear website cache" "Clear website cache"
import frappe.website.render
from frappe.website.utils import clear_website_cache
for site in context.sites: for site in context.sites:
try: try:
frappe.init(site=site) frappe.init(site=site)
frappe.connect() frappe.connect()
frappe.website.render.clear_cache()
clear_website_cache()
finally: finally:
frappe.destroy() frappe.destroy()
if not context.sites: if not context.sites:
@@ -350,7 +355,8 @@ def import_doc(context, path, force=False):
if not context.sites: if not context.sites:
raise SiteNotSpecifiedError raise SiteNotSpecifiedError


@click.command('import-csv')

@click.command('import-csv', help=DATA_IMPORT_DEPRECATION)
@click.argument('path') @click.argument('path')
@click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records') @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') @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') @click.option('--no-email', default=True, is_flag=True, help='Send email if applicable')
@pass_context @pass_context
def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True): 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') @click.command('data-import')
@@ -504,15 +486,26 @@ frappe.db.connect()




@click.command('console') @click.command('console')
@click.option(
'--autoreload',
is_flag=True,
help="Reload changes to code automatically"
)
@pass_context @pass_context
def console(context):
def console(context, autoreload=False):
"Start ipython console for a site" "Start ipython console for a site"
site = get_site(context) site = get_site(context)
frappe.init(site=site) frappe.init(site=site)
frappe.connect() frappe.connect()
frappe.local.lang = frappe.db.get_default("lang") 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() all_apps = frappe.get_installed_apps()
failed_to_import = [] failed_to_import = []


@@ -527,7 +520,9 @@ def console(context):
if failed_to_import: if failed_to_import:
print("\nFailed to import:\n{}".format(", ".join(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') @click.command('run-tests')
@@ -542,74 +537,39 @@ def console(context):
@click.option('--skip-test-records', is_flag=True, default=False, help="Don't create test records") @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('--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('--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 @pass_context
def run_tests(context, app=None, module=None, doctype=None, test=(), profile=False, 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, coverage=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None,
skip_test_records=False, skip_before_tests=False, failfast=False): 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

if coverage:
from coverage import Coverage

# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
incl = [
'*.py',
]
omit = [
'*.js',
'*.xml',
'*.pyc',
'*.css',
'*.less',
'*.scss',
'*.vue',
'*.html',
'*/test_*',
'*/node_modules/*',
'*/doctype/*/*_dashboard.py',
'*/patches/*',
]

if not app or app == 'frappe':
omit.append('*/tests/*')
omit.append('*/commands/*')

cov = Coverage(source=[source_path], omit=omit, include=incl)
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)
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 len(ret.failures) == 0 and len(ret.errors) == 0:
ret = 0

if os.environ.get('CI'):
sys.exit(ret)


@click.command('run-parallel-tests') @click.command('run-parallel-tests')
@click.option('--app', help="For App", default='frappe') @click.option('--app', help="For App", default='frappe')
@@ -619,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") @click.option('--use-orchestrator', is_flag=True, help="Use orchestrator to run parallel tests")
@pass_context @pass_context
def run_parallel_tests(context, app, build_number, total_builds, with_coverage=False, use_orchestrator=False): 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.command('run-ui-tests')
@click.argument('app') @click.argument('app')
@@ -641,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 admin_password = frappe.get_conf(site).admin_password


# override baseUrl using env variable # 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) os.chdir(app_base_path)


node_bin = subprocess.getoutput("npm bin") 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. # check if cypress in path...if not, install it.
if not ( if not (
os.path.exists(cypress_path) os.path.exists(cypress_path)
and os.path.exists(plugin_path) and os.path.exists(plugin_path)
and os.path.exists(testing_library_path)
and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6 and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6
): ):
# install cypress # install cypress
click.secho("Installing Cypress...", fg="yellow") 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 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}' 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) formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)


@@ -669,7 +632,7 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
formatted_command += ' --parallel' formatted_command += ' --parallel'


if ci_build_id: 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") click.secho("Running Cypress...", fg="yellow")
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True) frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
@@ -767,22 +730,49 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False):
frappe.destroy() 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 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")

if hasattr(app_hooks, '{0}_version'.format(branch_name)):
print("{0} {1}".format(m, getattr(app_hooks, '{0}_version'.format(branch_name))))


elif hasattr(module, "__version__"):
print("{0} {1}".format(m, module.__version__))
frappe.init("")
data = []

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, ".."))

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') @click.command('rebuild-global-search')


+ 11
- 12
frappe/config/__init__.py 查看文件

@@ -39,18 +39,17 @@ def get_modules_from_app(app):
) )


def get_all_empty_tables_by_module(): 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"]) results = frappe.get_all("DocType", fields=["name", "module"])
empty_tables_by_module = {} empty_tables_by_module = {}


+ 3
- 3
frappe/contacts/address_and_contact.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE


import frappe import frappe


@@ -153,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"], doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"],
distinct=True, as_list=True) 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({ filters.update({
"dt": ("not in", [d[0] for d in doctypes]) "dt": ("not in", [d[0] for d in doctypes])


+ 2
- 2
frappe/contacts/doctype/address/address.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors # Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


import frappe import frappe


@@ -257,7 +257,7 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):


def get_condensed_address(doc): def get_condensed_address(doc):
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"] 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): def update_preferred_address(address, field):
frappe.db.set_value('Address', address, field, 0) frappe.db.set_value('Address', address, field, 0)

+ 1
- 1
frappe/contacts/doctype/address/test_address.py 查看文件

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




+ 1
- 1
frappe/contacts/doctype/address_template/address_template.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors # Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 1
- 1
frappe/contacts/doctype/address_template/test_address_template.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
# License: MIT. See LICENSE
import frappe, unittest import frappe, unittest


class TestAddressTemplate(unittest.TestCase): class TestAddressTemplate(unittest.TestCase):


+ 2
- 2
frappe/contacts/doctype/contact/contact.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe import frappe
from frappe.utils import cstr, has_gravatar from frappe.utils import cstr, has_gravatar
from frappe import _ from frappe import _


+ 1
- 1
frappe/contacts/doctype/contact/test_contact.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors # Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
# License: MIT. See LICENSE
import frappe import frappe
import unittest import unittest




+ 1
- 1
frappe/contacts/doctype/contact_email/contact_email.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 1
- 1
frappe/contacts/doctype/contact_phone/contact_phone.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


# import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 1
- 1
frappe/contacts/doctype/gender/gender.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors # Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


from frappe.model.document import Document from frappe.model.document import Document




+ 1
- 1
frappe/contacts/doctype/gender/test_gender.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors # Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
# License: MIT. See LICENSE
import unittest import unittest


class TestGender(unittest.TestCase): class TestGender(unittest.TestCase):


+ 1
- 1
frappe/contacts/doctype/salutation/salutation.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors # Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


from frappe.model.document import Document from frappe.model.document import Document




+ 1
- 1
frappe/contacts/doctype/salutation/test_salutation.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors # Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
# License: MIT. See LICENSE
import unittest import unittest


class TestSalutation(unittest.TestCase): class TestSalutation(unittest.TestCase):


+ 1
- 1
frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE
import frappe import frappe
from frappe import _ from frappe import _




+ 1
- 1
frappe/core/__init__.py 查看文件

@@ -1,2 +1,2 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE

+ 1
- 1
frappe/core/doctype/__init__.py 查看文件

@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE



+ 3
- 6
frappe/core/doctype/access_log/access_log.py 查看文件

@@ -1,9 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt

# imports - standard imports
# imports - module imports
# License: MIT. See LICENSE
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document


@@ -33,4 +29,5 @@ def make_access_log(doctype=None, document=None, method=None, file_type=None,
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True)


# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` # `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()

+ 1
- 1
frappe/core/doctype/access_log/test_access_log.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
# License: MIT. See LICENSE


# imports - standard imports # imports - standard imports
import unittest import unittest


+ 1
- 1
frappe/core/doctype/activity_log/activity_log.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors # Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


from frappe import _ from frappe import _
from frappe.utils import get_fullname, now from frappe.utils import get_fullname, now


+ 7
- 5
frappe/core/doctype/activity_log/feed.py 查看文件

@@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
# License: MIT. See LICENSE


import frappe import frappe
import frappe.permissions import frappe.permissions
@@ -29,10 +29,12 @@ def update_feed(doc, method=None):
name = feed.name or doc.name name = feed.name or doc.name


# delete earlier feed # 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({ frappe.get_doc({
"doctype": "Activity Log", "doctype": "Activity Log",
"reference_doctype": doctype, "reference_doctype": doctype,


+ 1
- 1
frappe/core/doctype/activity_log/test_activity_log.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors # Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
# License: MIT. See LICENSE
import frappe import frappe
import unittest import unittest
import time import time


+ 1
- 1
frappe/core/doctype/block_module/block_module.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE


import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document


+ 2
- 2
frappe/core/doctype/comment/comment.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors # Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
# License: MIT. See LICENSE
import frappe import frappe
from frappe import _ from frappe import _
import json import json
@@ -9,7 +9,7 @@ from frappe.core.doctype.user.user import extract_mentions
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\ from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\
get_title, get_title_html get_title, get_title_html
from frappe.utils import get_fullname from frappe.utils import get_fullname
from frappe.website.render import clear_cache
from frappe.website.utils import clear_cache
from frappe.database.schema import add_column from frappe.database.schema import add_column
from frappe.exceptions import ImplicitCommitError from frappe.exceptions import ImplicitCommitError




+ 3
- 3
frappe/core/doctype/comment/test_comment.py 查看文件

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


@@ -30,7 +30,7 @@ class TestComment(unittest.TestCase):
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
test_blog = make_test_blog() test_blog = make_test_blog()


frappe.db.sql("delete from `tabComment` where reference_doctype = 'Blog Post'")
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})


from frappe.templates.includes.comments.comments import add_comment from frappe.templates.includes.comments.comments import add_comment
add_comment('Good comment with 10 chars', 'test@test.com', 'Good Tester', add_comment('Good comment with 10 chars', 'test@test.com', 'Good Tester',
@@ -41,7 +41,7 @@ class TestComment(unittest.TestCase):
reference_name = test_blog.name reference_name = test_blog.name
))[0].published, 1) ))[0].published, 1)


frappe.db.sql("delete from `tabComment` where reference_doctype = 'Blog Post'")
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})


add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor', add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor',
'Blog Post', test_blog.name, test_blog.route) 'Blog Post', test_blog.name, test_blog.route)


+ 1
- 1
frappe/core/doctype/communication/__init__.py 查看文件

@@ -1,3 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# License: MIT. See LICENSE



部分文件因为文件数量过多而无法显示

正在加载...
取消
保存