Merge branch 'develop' of github.com:frappe/frappe into multi_timezone_support
This commit is contained in:
commit
87b1e9af00
1280 changed files with 30563 additions and 21216 deletions
|
|
@ -9,6 +9,6 @@ trim_trailing_whitespace = true
|
|||
charset = utf-8
|
||||
|
||||
# python, js indentation settings
|
||||
[{*.py,*.js}]
|
||||
[{*.py,*.js,*.vue}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
|
|
|||
|
|
@ -10,3 +10,6 @@
|
|||
|
||||
# Replace use of Class.extend with native JS class
|
||||
fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85
|
||||
|
||||
# Updating license headers
|
||||
34460265554242a8d05fb09f049033b1117e1a2b
|
||||
|
|
|
|||
6
.github/helper/documentation.py
vendored
6
.github/helper/documentation.py
vendored
|
|
@ -32,9 +32,9 @@ if __name__ == "__main__":
|
|||
|
||||
if response.ok:
|
||||
payload = response.json()
|
||||
title = payload.get("title", "").lower()
|
||||
head_sha = payload.get("head", {}).get("sha")
|
||||
body = payload.get("body", "").lower()
|
||||
title = (payload.get("title") or "").lower()
|
||||
head_sha = (payload.get("head") or {}).get("sha")
|
||||
body = (payload.get("body") or "").lower()
|
||||
|
||||
if title.startswith("feat") and head_sha and "no-docs" not in body:
|
||||
if docs_link_exists(body):
|
||||
|
|
|
|||
4
.github/helper/install.sh
vendored
4
.github/helper/install.sh
vendored
|
|
@ -17,6 +17,7 @@ if [ "$TYPE" == "server" ]; then
|
|||
fi
|
||||
|
||||
if [ "$DB" == "mariadb" ];then
|
||||
sudo apt install mariadb-client-10.3
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ if [ "$TYPE" == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; f
|
|||
if [ "$TYPE" == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi
|
||||
|
||||
if [ "$TYPE" == "ui" ]; then bench setup requirements --node; fi
|
||||
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
||||
|
||||
# install node-sass which is required for website theme test
|
||||
cd ./apps/frappe || exit
|
||||
|
|
@ -58,4 +60,4 @@ cd ../..
|
|||
bench start &
|
||||
bench --site test_site reinstall --yes
|
||||
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi
|
||||
bench build --app frappe
|
||||
CI=Yes bench build --app frappe
|
||||
|
|
|
|||
5
.github/helper/install_dependencies.sh
vendored
5
.github/helper/install_dependencies.sh
vendored
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
set -e
|
||||
|
||||
# python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
# if [[ $? != 2 ]];then
|
||||
# exit;
|
||||
# fi
|
||||
|
||||
# install wkhtmltopdf
|
||||
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
|
||||
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
|
||||
|
|
|
|||
84
.github/helper/roulette.py
vendored
84
.github/helper/roulette.py
vendored
|
|
@ -1,56 +1,72 @@
|
|||
# if the script ends with exit code 0, then no tests are run further, else all tests are run
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
def get_files_list(pr_number, repo="frappe/frappe"):
|
||||
req = urllib.request.Request(f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files")
|
||||
res = urllib.request.urlopen(req)
|
||||
dump = json.loads(res.read().decode('utf8'))
|
||||
return [change["filename"] for change in dump]
|
||||
|
||||
def get_output(command, shell=True):
|
||||
print(command)
|
||||
command = shlex.split(command)
|
||||
return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
|
||||
print(command)
|
||||
command = shlex.split(command)
|
||||
return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
|
||||
|
||||
def is_py(file):
|
||||
return file.endswith("py")
|
||||
return file.endswith("py")
|
||||
|
||||
def is_js(file):
|
||||
return file.endswith("js")
|
||||
def is_ci(file):
|
||||
return ".github" in file
|
||||
|
||||
def is_frontend_code(file):
|
||||
return file.lower().endswith((".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue"))
|
||||
|
||||
def is_docs(file):
|
||||
regex = re.compile(r'\.(md|png|jpg|jpeg)$|^.github|LICENSE')
|
||||
return bool(regex.search(file))
|
||||
regex = re.compile(r'\.(md|png|jpg|jpeg|csv)$|^.github|LICENSE')
|
||||
return bool(regex.search(file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_type = os.environ.get("TYPE")
|
||||
before = os.environ.get("BEFORE")
|
||||
after = os.environ.get("AFTER")
|
||||
commit_range = before + '...' + after
|
||||
print("Build Type: {}".format(build_type))
|
||||
print("Commit Range: {}".format(commit_range))
|
||||
files_list = sys.argv[1:]
|
||||
build_type = os.environ.get("TYPE")
|
||||
pr_number = os.environ.get("PR_NUMBER")
|
||||
repo = os.environ.get("REPO_NAME")
|
||||
|
||||
try:
|
||||
files_changed = get_output("git diff --name-only {}".format(commit_range), shell=False)
|
||||
except Exception:
|
||||
sys.exit(2)
|
||||
# this is a push build, run all builds
|
||||
if not pr_number:
|
||||
os.system('echo "::set-output name=build::strawberry"')
|
||||
sys.exit(0)
|
||||
|
||||
if "fatal" not in files_changed:
|
||||
files_list = files_changed.split()
|
||||
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
||||
only_js_changed = len(list(filter(is_js, files_list))) == len(files_list)
|
||||
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
|
||||
files_list = files_list or get_files_list(pr_number=pr_number, repo=repo)
|
||||
|
||||
if only_docs_changed:
|
||||
print("Only docs were updated, stopping build process.")
|
||||
sys.exit(0)
|
||||
if not files_list:
|
||||
print("No files' changes detected. Build is shutting")
|
||||
sys.exit(0)
|
||||
|
||||
if only_js_changed and build_type == "server":
|
||||
print("Only JavaScript code was updated; Stopping Python build process.")
|
||||
sys.exit(0)
|
||||
ci_files_changed = any(f for f in files_list if is_ci(f))
|
||||
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
||||
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
|
||||
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
|
||||
|
||||
if only_py_changed and build_type == "ui":
|
||||
print("Only Python code was updated, stopping Cypress build process.")
|
||||
sys.exit(0)
|
||||
if ci_files_changed:
|
||||
print("CI related files were updated, running all build processes.")
|
||||
|
||||
sys.exit(2)
|
||||
elif only_docs_changed:
|
||||
print("Only docs were updated, stopping build process.")
|
||||
sys.exit(0)
|
||||
|
||||
elif only_frontend_code_changed and build_type == "server":
|
||||
print("Only Frontend code was updated; Stopping Python build process.")
|
||||
sys.exit(0)
|
||||
|
||||
elif only_py_changed and build_type == "ui":
|
||||
print("Only Python code was updated, stopping Cypress build process.")
|
||||
sys.exit(0)
|
||||
|
||||
os.system('echo "::set-output name=build::strawberry"')
|
||||
|
|
|
|||
38
.github/helper/semgrep_rules/README.md
vendored
38
.github/helper/semgrep_rules/README.md
vendored
|
|
@ -1,38 +0,0 @@
|
|||
# Semgrep linting
|
||||
|
||||
## What is semgrep?
|
||||
Semgrep or "semantic grep" is language agnostic static analysis tool. In simple terms semgrep is syntax-aware `grep`, so unlike regex it doesn't get confused by different ways of writing same thing or whitespaces or code split in multiple lines etc.
|
||||
|
||||
Example:
|
||||
|
||||
To check if a translate function is using f-string or not the regex would be `r"_\(\s*f[\"']"` while equivalent rule in semgrep would be `_(f"...")`. As semgrep knows grammer of language it takes care of unnecessary whitespace, type of quotation marks etc.
|
||||
|
||||
You can read more such examples in `.github/helper/semgrep_rules` directory.
|
||||
|
||||
# Why/when to use this?
|
||||
We want to maintain quality of contributions, at the same time remembering all the good practices can be pain to deal with while evaluating contributions. Using semgrep if you can translate "best practice" into a rule then it can automate the task for us.
|
||||
|
||||
## Running locally
|
||||
|
||||
Install semgrep using homebrew `brew install semgrep` or pip `pip install semgrep`.
|
||||
|
||||
To run locally use following command:
|
||||
|
||||
`semgrep --config=.github/helper/semgrep_rules [file/folder names]`
|
||||
|
||||
## Testing
|
||||
semgrep allows testing the tests. Refer to this page: https://semgrep.dev/docs/writing-rules/testing-rules/
|
||||
|
||||
When writing new rules you should write few positive and few negative cases as shown in the guide and current tests.
|
||||
|
||||
To run current tests: `semgrep --test --test-ignore-todo .github/helper/semgrep_rules`
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
If you are new to Semgrep read following pages to get started on writing/modifying rules:
|
||||
|
||||
- https://semgrep.dev/docs/getting-started/
|
||||
- https://semgrep.dev/docs/writing-rules/rule-syntax
|
||||
- https://semgrep.dev/docs/writing-rules/pattern-examples/
|
||||
- https://semgrep.dev/docs/writing-rules/rule-ideas/#common-use-cases
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import frappe
|
||||
from frappe import _, flt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
# ruleid: frappe-modifying-but-not-comitting
|
||||
def on_submit(self):
|
||||
if self.value_of_goods == 0:
|
||||
frappe.throw(_('Value of goods cannot be 0'))
|
||||
self.status = 'Submitted'
|
||||
|
||||
|
||||
# ok: frappe-modifying-but-not-comitting
|
||||
def on_submit(self):
|
||||
if self.value_of_goods == 0:
|
||||
frappe.throw(_('Value of goods cannot be 0'))
|
||||
self.status = 'Submitted'
|
||||
self.db_set('status', 'Submitted')
|
||||
|
||||
# ok: frappe-modifying-but-not-comitting
|
||||
def on_submit(self):
|
||||
if self.value_of_goods == 0:
|
||||
frappe.throw(_('Value of goods cannot be 0'))
|
||||
x = "y"
|
||||
self.status = x
|
||||
self.db_set('status', x)
|
||||
|
||||
|
||||
# ok: frappe-modifying-but-not-comitting
|
||||
def on_submit(self):
|
||||
x = "y"
|
||||
self.status = x
|
||||
self.save()
|
||||
|
||||
# ruleid: frappe-modifying-but-not-comitting-other-method
|
||||
class DoctypeClass(Document):
|
||||
def on_submit(self):
|
||||
self.good_method()
|
||||
self.tainted_method()
|
||||
|
||||
def tainted_method(self):
|
||||
self.status = "uptate"
|
||||
|
||||
|
||||
# ok: frappe-modifying-but-not-comitting-other-method
|
||||
class DoctypeClass(Document):
|
||||
def on_submit(self):
|
||||
self.good_method()
|
||||
self.tainted_method()
|
||||
|
||||
def tainted_method(self):
|
||||
self.status = "update"
|
||||
self.db_set("status", "update")
|
||||
|
||||
# ok: frappe-modifying-but-not-comitting-other-method
|
||||
class DoctypeClass(Document):
|
||||
def on_submit(self):
|
||||
self.good_method()
|
||||
self.tainted_method()
|
||||
self.save()
|
||||
|
||||
def tainted_method(self):
|
||||
self.status = "uptate"
|
||||
135
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
135
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
|
|
@ -1,135 +0,0 @@
|
|||
# This file specifies rules for correctness according to how frappe doctype data model works.
|
||||
|
||||
rules:
|
||||
- id: frappe-modifying-but-not-comitting
|
||||
patterns:
|
||||
- pattern: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
- pattern-not: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
...
|
||||
self.db_set(..., self.$ATTR, ...)
|
||||
- pattern-not: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = $SOME_VAR
|
||||
...
|
||||
self.db_set(..., $SOME_VAR, ...)
|
||||
- pattern-not: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = $SOME_VAR
|
||||
...
|
||||
self.save()
|
||||
- metavariable-regex:
|
||||
metavariable: '$ATTR'
|
||||
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
|
||||
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
|
||||
- metavariable-regex:
|
||||
metavariable: "$METHOD"
|
||||
regex: "(on_submit|on_cancel)"
|
||||
message: |
|
||||
DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-modifying-but-not-comitting-other-method
|
||||
patterns:
|
||||
- pattern: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
- pattern-not: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
...
|
||||
self.db_set(..., self.$ATTR, ...)
|
||||
- pattern-not: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = $SOME_VAR
|
||||
...
|
||||
self.db_set(..., $SOME_VAR, ...)
|
||||
- pattern-not: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
self.save()
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
- metavariable-regex:
|
||||
metavariable: "$METHOD"
|
||||
regex: "(on_submit|on_cancel)"
|
||||
message: |
|
||||
self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-print-function-in-doctypes
|
||||
pattern: print(...)
|
||||
message: |
|
||||
Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement.
|
||||
languages: [python]
|
||||
severity: WARNING
|
||||
paths:
|
||||
exclude:
|
||||
- test_*.py
|
||||
include:
|
||||
- "*/**/doctype/*"
|
||||
|
||||
- id: frappe-modifying-child-tables-while-iterating
|
||||
pattern-either:
|
||||
- pattern: |
|
||||
for $ROW in self.$TABLE:
|
||||
...
|
||||
self.remove(...)
|
||||
- pattern: |
|
||||
for $ROW in self.$TABLE:
|
||||
...
|
||||
self.append(...)
|
||||
message: |
|
||||
Child table being modified while iterating on it.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
paths:
|
||||
include:
|
||||
- "*/**/doctype/*"
|
||||
|
||||
- id: frappe-same-key-assigned-twice
|
||||
pattern-either:
|
||||
- pattern: |
|
||||
{..., $X: $A, ..., $X: $B, ...}
|
||||
- pattern: |
|
||||
dict(..., ($X, $A), ..., ($X, $B), ...)
|
||||
- pattern: |
|
||||
_dict(..., ($X, $A), ..., ($X, $B), ...)
|
||||
message: |
|
||||
key `$X` is uselessly assigned twice. This could be a potential bug.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
6
.github/helper/semgrep_rules/security.py
vendored
6
.github/helper/semgrep_rules/security.py
vendored
|
|
@ -1,6 +0,0 @@
|
|||
def function_name(input):
|
||||
# ruleid: frappe-codeinjection-eval
|
||||
eval(input)
|
||||
|
||||
# ok: frappe-codeinjection-eval
|
||||
eval("1 + 1")
|
||||
29
.github/helper/semgrep_rules/security.yml
vendored
29
.github/helper/semgrep_rules/security.yml
vendored
|
|
@ -1,29 +0,0 @@
|
|||
rules:
|
||||
- id: frappe-codeinjection-eval
|
||||
patterns:
|
||||
- pattern-not: eval("...")
|
||||
- pattern: eval(...)
|
||||
message: |
|
||||
Detected the use of eval(). eval() can be dangerous if used to evaluate
|
||||
dynamic content. Avoid it or use safe_eval().
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
paths:
|
||||
exclude:
|
||||
- frappe/__init__.py
|
||||
- frappe/commands/utils.py
|
||||
|
||||
- id: frappe-sqli-format-strings
|
||||
patterns:
|
||||
- pattern-inside: |
|
||||
@frappe.whitelist()
|
||||
def $FUNC(...):
|
||||
...
|
||||
- pattern-either:
|
||||
- pattern: frappe.db.sql("..." % ...)
|
||||
- pattern: frappe.db.sql(f"...", ...)
|
||||
- pattern: frappe.db.sql("...".format(...), ...)
|
||||
message: |
|
||||
Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
|
||||
languages: [python]
|
||||
severity: WARNING
|
||||
44
.github/helper/semgrep_rules/translate.js
vendored
44
.github/helper/semgrep_rules/translate.js
vendored
|
|
@ -1,44 +0,0 @@
|
|||
// ruleid: frappe-translation-empty-string
|
||||
__("")
|
||||
// ruleid: frappe-translation-empty-string
|
||||
__('')
|
||||
|
||||
// ok: frappe-translation-js-formatting
|
||||
__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name]);
|
||||
|
||||
// ruleid: frappe-translation-js-formatting
|
||||
__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`);
|
||||
|
||||
// ok: frappe-translation-js-formatting
|
||||
__('This is fine');
|
||||
|
||||
|
||||
// ok: frappe-translation-trailing-spaces
|
||||
__('This is fine');
|
||||
|
||||
// ruleid: frappe-translation-trailing-spaces
|
||||
__(' this is not ok ');
|
||||
// ruleid: frappe-translation-trailing-spaces
|
||||
__('this is not ok ');
|
||||
// ruleid: frappe-translation-trailing-spaces
|
||||
__(' this is not ok');
|
||||
|
||||
// ok: frappe-translation-js-splitting
|
||||
__('You have {0} subscribers in your mailing list.', [subscribers.length])
|
||||
|
||||
// todoruleid: frappe-translation-js-splitting
|
||||
__('You have') + subscribers.length + __('subscribers in your mailing list.')
|
||||
|
||||
// ruleid: frappe-translation-js-splitting
|
||||
__('You have' + 'subscribers in your mailing list.')
|
||||
|
||||
// ruleid: frappe-translation-js-splitting
|
||||
__('You have {0} subscribers' +
|
||||
'in your mailing list', [subscribers.length])
|
||||
|
||||
// ok: frappe-translation-js-splitting
|
||||
__("Ctrl+Enter to add comment")
|
||||
|
||||
// ruleid: frappe-translation-js-splitting
|
||||
__('You have {0} subscribers \
|
||||
in your mailing list', [subscribers.length])
|
||||
61
.github/helper/semgrep_rules/translate.py
vendored
61
.github/helper/semgrep_rules/translate.py
vendored
|
|
@ -1,61 +0,0 @@
|
|||
# Examples taken from https://frappeframework.com/docs/user/en/translations
|
||||
# This file is used for testing the tests.
|
||||
|
||||
from frappe import _
|
||||
|
||||
full_name = "Jon Doe"
|
||||
# ok: frappe-translation-python-formatting
|
||||
_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)
|
||||
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_('Welcome %s, get started with ERPNext in just a few clicks.' % full_name)
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})
|
||||
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))
|
||||
|
||||
|
||||
subscribers = ["Jon", "Doe"]
|
||||
# ok: frappe-translation-python-formatting
|
||||
_('You have {0} subscribers in your mailing list.').format(len(subscribers))
|
||||
|
||||
# ruleid: frappe-translation-python-splitting
|
||||
_('You have') + len(subscribers) + _('subscribers in your mailing list.')
|
||||
|
||||
# ruleid: frappe-translation-python-splitting
|
||||
_('You have {0} subscribers \
|
||||
in your mailing list').format(len(subscribers))
|
||||
|
||||
# ok: frappe-translation-python-splitting
|
||||
_('You have {0} subscribers') \
|
||||
+ 'in your mailing list'
|
||||
|
||||
# ruleid: frappe-translation-trailing-spaces
|
||||
msg = _(" You have {0} pending invoice ")
|
||||
# ruleid: frappe-translation-trailing-spaces
|
||||
msg = _("You have {0} pending invoice ")
|
||||
# ruleid: frappe-translation-trailing-spaces
|
||||
msg = _(" You have {0} pending invoice")
|
||||
|
||||
# ok: frappe-translation-trailing-spaces
|
||||
msg = ' ' + _("You have {0} pending invoices") + ' '
|
||||
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_(f"can not format like this - {subscribers}")
|
||||
# ruleid: frappe-translation-python-splitting
|
||||
_(f"what" + f"this is also not cool")
|
||||
|
||||
|
||||
# ruleid: frappe-translation-empty-string
|
||||
_("")
|
||||
# ruleid: frappe-translation-empty-string
|
||||
_('')
|
||||
|
||||
|
||||
class Test:
|
||||
# ok: frappe-translation-python-splitting
|
||||
def __init__(
|
||||
args
|
||||
):
|
||||
pass
|
||||
64
.github/helper/semgrep_rules/translate.yml
vendored
64
.github/helper/semgrep_rules/translate.yml
vendored
|
|
@ -1,64 +0,0 @@
|
|||
rules:
|
||||
- id: frappe-translation-empty-string
|
||||
pattern-either:
|
||||
- pattern: _("")
|
||||
- pattern: __("")
|
||||
message: |
|
||||
Empty string is useless for translation.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python, javascript, json]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-trailing-spaces
|
||||
pattern-either:
|
||||
- pattern: _("=~/(^[ \t]+|[ \t]+$)/")
|
||||
- pattern: __("=~/(^[ \t]+|[ \t]+$)/")
|
||||
message: |
|
||||
Trailing or leading whitespace not allowed in translate strings.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python, javascript, json]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-python-formatting
|
||||
pattern-either:
|
||||
- pattern: _("..." % ...)
|
||||
- pattern: _("...".format(...))
|
||||
- pattern: _(f"...")
|
||||
message: |
|
||||
Only positional formatters are allowed and formatting should not be done before translating.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-js-formatting
|
||||
patterns:
|
||||
- pattern: __(`...`)
|
||||
- pattern-not: __("...")
|
||||
message: |
|
||||
Template strings are not allowed for text formatting.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [javascript, json]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-python-splitting
|
||||
pattern-either:
|
||||
- pattern: _(...) + _(...)
|
||||
- pattern: _("..." + "...")
|
||||
- pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
|
||||
- pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
|
||||
message: |
|
||||
Do not split strings inside translate function. Do not concatenate using translate functions.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-js-splitting
|
||||
pattern-either:
|
||||
- pattern-regex: '__\([^\)]*[\\]\s+'
|
||||
- pattern: __('...' + '...', ...)
|
||||
- pattern: __('...') + __('...')
|
||||
message: |
|
||||
Do not split strings inside translate function. Do not concatenate using translate functions.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [javascript, json]
|
||||
severity: ERROR
|
||||
9
.github/helper/semgrep_rules/ux.js
vendored
9
.github/helper/semgrep_rules/ux.js
vendored
|
|
@ -1,9 +0,0 @@
|
|||
|
||||
// ok: frappe-missing-translate-function-js
|
||||
frappe.msgprint('{{ _("Both login and password required") }}');
|
||||
|
||||
// ruleid: frappe-missing-translate-function-js
|
||||
frappe.msgprint('What');
|
||||
|
||||
// ok: frappe-missing-translate-function-js
|
||||
frappe.throw(' {{ _("Both login and password required") }}. ');
|
||||
31
.github/helper/semgrep_rules/ux.py
vendored
31
.github/helper/semgrep_rules/ux.py
vendored
|
|
@ -1,31 +0,0 @@
|
|||
import frappe
|
||||
from frappe import msgprint, throw, _
|
||||
|
||||
|
||||
# ruleid: frappe-missing-translate-function-python
|
||||
throw("Error Occured")
|
||||
|
||||
# ruleid: frappe-missing-translate-function-python
|
||||
frappe.throw("Error Occured")
|
||||
|
||||
# ruleid: frappe-missing-translate-function-python
|
||||
frappe.msgprint("Useful message")
|
||||
|
||||
# ruleid: frappe-missing-translate-function-python
|
||||
msgprint("Useful message")
|
||||
|
||||
|
||||
# ok: frappe-missing-translate-function-python
|
||||
translatedmessage = _("Hello")
|
||||
|
||||
# ok: frappe-missing-translate-function-python
|
||||
throw(translatedmessage)
|
||||
|
||||
# ok: frappe-missing-translate-function-python
|
||||
msgprint(translatedmessage)
|
||||
|
||||
# ok: frappe-missing-translate-function-python
|
||||
msgprint(_("Helpful message"))
|
||||
|
||||
# ok: frappe-missing-translate-function-python
|
||||
frappe.throw(_("Error occured"))
|
||||
30
.github/helper/semgrep_rules/ux.yml
vendored
30
.github/helper/semgrep_rules/ux.yml
vendored
|
|
@ -1,30 +0,0 @@
|
|||
rules:
|
||||
- id: frappe-missing-translate-function-python
|
||||
pattern-either:
|
||||
- patterns:
|
||||
- pattern: frappe.msgprint("...", ...)
|
||||
- pattern-not: frappe.msgprint(_("..."), ...)
|
||||
- patterns:
|
||||
- pattern: frappe.throw("...", ...)
|
||||
- pattern-not: frappe.throw(_("..."), ...)
|
||||
message: |
|
||||
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-missing-translate-function-js
|
||||
pattern-either:
|
||||
- patterns:
|
||||
- pattern: frappe.msgprint("...", ...)
|
||||
- pattern-not: frappe.msgprint(__("..."), ...)
|
||||
# ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
|
||||
- pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
|
||||
- patterns:
|
||||
- pattern: frappe.throw("...", ...)
|
||||
- pattern-not: frappe.throw(__("..."), ...)
|
||||
# ignore microtemplating
|
||||
- pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
|
||||
message: |
|
||||
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
|
||||
languages: [javascript]
|
||||
severity: ERROR
|
||||
17
.github/semantic.yml
vendored
17
.github/semantic.yml
vendored
|
|
@ -11,3 +11,20 @@ allowRevertCommits: true
|
|||
|
||||
# For allowed PR types: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
|
||||
# Tool Reference: https://github.com/zeke/semantic-pull-requests
|
||||
|
||||
# By default types specified in commitizen/conventional-commit-types is used.
|
||||
# See: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
|
||||
# You can override the valid types
|
||||
types:
|
||||
- BREAKING CHANGE
|
||||
- feat
|
||||
- fix
|
||||
- docs
|
||||
- style
|
||||
- refactor
|
||||
- perf
|
||||
- test
|
||||
- build
|
||||
- ci
|
||||
- chore
|
||||
- revert
|
||||
|
|
|
|||
32
.github/try-on-f-cloud-button.svg
vendored
Normal file
32
.github/try-on-f-cloud-button.svg
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
|
||||
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>
|
||||
<path d="M41.6982 35.5H45.0129V28.7109C45.0129 27.2344 46.0866 26.2188 47.5494 26.2188C48.0085 26.2188 48.6388 26.2969 48.95 26.3984V23.4453C48.6543 23.375 48.2419 23.3281 47.9074 23.3281C46.5691 23.3281 45.472 24.1094 45.0362 25.5938H44.9117V23.5H41.6982V35.5Z" fill="white"/>
|
||||
<path d="M52.8331 40C55.2996 40 56.6068 38.7344 57.2837 36.7969L61.9289 23.5156L58.4197 23.5L55.9221 32.3125H55.7976L53.3233 23.5H49.8374L54.1247 35.8437L53.9302 36.3516C53.4944 37.4766 52.6619 37.5312 51.4947 37.1719L50.7478 39.6562C51.2224 39.8594 51.9927 40 52.8331 40Z" fill="white"/>
|
||||
<path d="M73.6142 35.7344C77.2401 35.7344 79.4966 33.2422 79.4966 29.5469C79.4966 25.8281 77.2401 23.3438 73.6142 23.3438C69.9883 23.3438 67.7319 25.8281 67.7319 29.5469C67.7319 33.2422 69.9883 35.7344 73.6142 35.7344ZM73.6298 33.1562C71.9569 33.1562 71.101 31.6171 71.101 29.5233C71.101 27.4296 71.9569 25.8827 73.6298 25.8827C75.2715 25.8827 76.1274 27.4296 76.1274 29.5233C76.1274 31.6171 75.2715 33.1562 73.6298 33.1562Z" fill="white"/>
|
||||
<path d="M84.7253 28.5625C84.7331 27.0156 85.6512 26.1094 86.9895 26.1094C88.3201 26.1094 89.1215 26.9844 89.1137 28.4531V35.5H92.4284V27.8594C92.4284 25.0625 90.7945 23.3438 88.3046 23.3438C86.5306 23.3438 85.2466 24.2187 84.7097 25.6172H84.5697V23.5H81.4106V35.5H84.7253V28.5625Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M102.429 19.5H113.429V22.3141H102.429V19.5ZM102.429 35.5V26.6794H112.699V29.4982H105.94V35.5H102.429Z" fill="white"/>
|
||||
<path d="M131.584 24.9625C131.09 21.5057 128.345 19.5 124.785 19.5C120.589 19.5 117.429 22.463 117.429 27.4924C117.429 32.5142 120.55 35.4848 124.785 35.4848C128.604 35.4848 131.137 33.0916 131.584 30.1211L128.651 30.1059C128.282 31.9293 126.745 32.9549 124.824 32.9549C122.22 32.9549 120.354 31.0632 120.354 27.4924C120.354 23.9824 122.204 22.0299 124.832 22.0299C126.784 22.0299 128.314 23.1011 128.651 24.9625H131.584Z" fill="white"/>
|
||||
<path d="M136.409 19.7124H133.571V35.2718H136.409V19.7124Z" fill="white"/>
|
||||
<path d="M144.031 35.5001C147.56 35.5001 149.803 33.0917 149.803 29.483C149.803 25.8667 147.56 23.4507 144.031 23.4507C140.502 23.4507 138.259 25.8667 138.259 29.483C138.259 33.0917 140.502 35.5001 144.031 35.5001ZM144.047 33.2969C142.094 33.2969 141.137 31.6103 141.137 29.4754C141.137 27.3406 142.094 25.6312 144.047 25.6312C145.968 25.6312 146.925 27.3406 146.925 29.4754C146.925 31.6103 145.968 33.2969 144.047 33.2969Z" fill="white"/>
|
||||
<path d="M159.338 30.3641C159.338 32.1419 158.028 33.0232 156.773 33.0232C155.409 33.0232 154.499 32.0887 154.499 30.6072V23.6025H151.66V31.0327C151.66 33.8361 153.307 35.4239 155.675 35.4239C157.479 35.4239 158.749 34.5046 159.298 33.1979H159.424V35.272H162.176V23.6025H159.338V30.3641Z" fill="white"/>
|
||||
<path d="M169.014 35.4769C171.084 35.4769 172.017 34.2841 172.464 33.4332H172.637V35.2718H175.429V19.7124H172.582V25.532H172.464C172.033 24.6887 171.147 23.4503 169.022 23.4503C166.238 23.4503 164.05 25.5624 164.05 29.4522C164.05 33.2965 166.175 35.4769 169.014 35.4769ZM169.806 33.2205C167.931 33.2205 166.943 31.6251 166.943 29.437C166.943 27.2642 167.916 25.7067 169.806 25.7067C171.633 25.7067 172.637 27.173 172.637 29.437C172.637 31.701 171.617 33.2205 169.806 33.2205Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="0" y="0" width="201" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
2
.github/workflows/docker-release.yml
vendored
2
.github/workflows/docker-release.yml
vendored
|
|
@ -12,4 +12,4 @@ jobs:
|
|||
- name: curl
|
||||
run: |
|
||||
apk add curl bash
|
||||
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests
|
||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}'
|
||||
|
|
|
|||
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.6
|
||||
python-version: 3.7
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v2
|
||||
|
|
|
|||
45
.github/workflows/patch-mariadb-tests.yml
vendored
45
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -2,9 +2,14 @@ name: Patch
|
|||
|
||||
on: [pull_request, workflow_dispatch]
|
||||
|
||||
|
||||
concurrency:
|
||||
group: patch-mariadb-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: Patch Test
|
||||
|
||||
|
|
@ -24,12 +29,23 @@ jobs:
|
|||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -39,6 +55,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -51,10 +68,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -63,6 +82,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -70,14 +90,35 @@ jobs:
|
|||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
|
||||
- name: Run Patch Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
cd ~/frappe-bench/
|
||||
wget https://frappeframework.com/files/v10-frappe.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v10-frappe.sql.gz
|
||||
|
||||
source env/bin/activate
|
||||
cd apps/frappe/
|
||||
git remote set-url upstream https://github.com/frappe/frappe.git
|
||||
|
||||
for version in $(seq 12 13)
|
||||
do
|
||||
echo "Updating to v$version"
|
||||
branch_name="version-$version-hotfix"
|
||||
git fetch --depth 1 upstream $branch_name:$branch_name
|
||||
|
||||
git checkout -q -f $branch_name
|
||||
pip install -q -r requirements.txt
|
||||
bench --site test_site migrate
|
||||
done
|
||||
|
||||
echo "Updating to last commit"
|
||||
git checkout -q -f "$GITHUB_SHA"
|
||||
bench setup requirements --python
|
||||
bench --site test_site migrate
|
||||
|
|
|
|||
2
.github/workflows/publish-assets-develop.yml
vendored
2
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
node-version: 14
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.6'
|
||||
python-version: '3.9'
|
||||
- name: Set up bench and build assets
|
||||
run: |
|
||||
npm install -g yarn
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
python-version: '12.x'
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.6'
|
||||
python-version: '3.9'
|
||||
- name: Set up bench and build assets
|
||||
run: |
|
||||
npm install -g yarn
|
||||
|
|
|
|||
36
.github/workflows/semgrep.yml
vendored
36
.github/workflows/semgrep.yml
vendored
|
|
@ -1,34 +1,22 @@
|
|||
name: Semgrep
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-13-hotfix
|
||||
- version-13-pre-release
|
||||
pull_request: { }
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup semgrep
|
||||
run: |
|
||||
python -m pip install -q semgrep
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- 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: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
./frappe-semgrep-rules/rules
|
||||
|
|
|
|||
67
.github/workflows/server-mariadb-tests.yml
vendored
67
.github/workflows/server-mariadb-tests.yml
vendored
|
|
@ -6,9 +6,14 @@ on:
|
|||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
concurrency:
|
||||
group: server-mariadb-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -33,19 +38,31 @@ jobs:
|
|||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -55,6 +72,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -67,10 +85,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -79,6 +99,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -86,45 +107,25 @@ jobs:
|
|||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
|
||||
env:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Upload Coverage Data
|
||||
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
|
||||
flags: server
|
||||
40
.github/workflows/server-postgres-tests.yml
vendored
40
.github/workflows/server-postgres-tests.yml
vendored
|
|
@ -3,10 +3,16 @@ name: Server
|
|||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
concurrency:
|
||||
group: server-postgres-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -35,19 +41,31 @@ jobs:
|
|||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: '14'
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -57,6 +75,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -69,10 +88,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -81,6 +102,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -88,13 +110,25 @@ jobs:
|
|||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: postgres
|
||||
TYPE: server
|
||||
|
||||
- name: Run Tests
|
||||
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:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
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
|
||||
flags: server
|
||||
|
|
|
|||
22
.github/workflows/translation_linter.yml
vendored
22
.github/workflows/translation_linter.yml
vendored
|
|
@ -1,22 +0,0 @@
|
|||
name: Frappe Linter
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-12-hotfix
|
||||
- version-11-hotfix
|
||||
jobs:
|
||||
check_translation:
|
||||
name: Translation Syntax Check
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.6
|
||||
- name: Validating Translation Syntax
|
||||
run: |
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
python $GITHUB_WORKSPACE/.github/helper/translation.py $files
|
||||
56
.github/workflows/ui-tests.yml
vendored
56
.github/workflows/ui-tests.yml
vendored
|
|
@ -6,9 +6,13 @@ on:
|
|||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
concurrency:
|
||||
group: ui-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -33,19 +37,31 @@ jobs:
|
|||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "ui"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -55,6 +71,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -67,10 +84,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -79,6 +98,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Cache cypress binary
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache
|
||||
|
|
@ -88,6 +108,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -95,13 +116,42 @@ jobs:
|
|||
TYPE: ui
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: ui
|
||||
|
||||
- name: Instrument Source Code
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/apps/frappe/ && npx nyc instrument -x 'frappe/public/dist/**' -x 'frappe/public/js/lib/**' -x '**/*.bundle.js' --compact=false --in-place frappe
|
||||
|
||||
- name: Build
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench build --apps frappe
|
||||
|
||||
- name: Site Setup
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard
|
||||
|
||||
- name: UI Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
|
||||
|
||||
- name: Check If Coverage Report Exists
|
||||
id: check_coverage
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "/home/runner/frappe-bench/apps/frappe/.cypress-coverage/clover.xml"
|
||||
|
||||
- name: Upload Coverage Data
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' && steps.check_coverage.outputs.files_exists == 'true' }}
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: Cypress
|
||||
fail_ci_if_error: true
|
||||
directory: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/
|
||||
verbose: true
|
||||
flags: ui-tests
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -67,6 +67,7 @@ coverage.xml
|
|||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
.cypress-coverage
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
|
|
|||
16
.mergify.yml
16
.mergify.yml
|
|
@ -1,4 +1,20 @@
|
|||
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
|
||||
conditions:
|
||||
- status-success=Sider
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@
|
|||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
* @frappe/frappe-review-team
|
||||
website/ @prssanna
|
||||
web_form/ @prssanna
|
||||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @leela
|
||||
patches/ @surajshetty3416
|
||||
dashboard/ @prssanna
|
||||
patches/ @surajshetty3416 @gavindsouza
|
||||
email/ @leela
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
database @gavindsouza
|
||||
model @gavindsouza
|
||||
requirements.txt @gavindsouza
|
||||
query_builder/ @gavindsouza
|
||||
commands/ @gavindsouza
|
||||
workspace @shariquerik
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
31
README.md
31
README.md
|
|
@ -26,8 +26,8 @@
|
|||
<a href='https://www.codetriage.com/frappe/frappe'>
|
||||
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
|
||||
</a>
|
||||
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'>
|
||||
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'>
|
||||
<a href="https://codecov.io/gh/frappe/frappe">
|
||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj&flag=server"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
@ -35,25 +35,36 @@
|
|||
|
||||
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com)
|
||||
|
||||
### Table of Contents
|
||||
* [Installation](https://frappeframework.com/docs/user/en/installation)
|
||||
* [Documentation](https://frappeframework.com/docs)
|
||||
<div align="center">
|
||||
<a href="https://frappecloud.com/deploy?apps=frappe&source=frappe_readme">
|
||||
<img src=".github/try-on-f-cloud-button.svg" height="40">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
* [Installation](#installation)
|
||||
* [Contributing](#contributing)
|
||||
* [Resources](#resources)
|
||||
* [License](#license)
|
||||
|
||||
### Installation
|
||||
## Installation
|
||||
|
||||
* [Install via Docker](https://github.com/frappe/frappe_docker)
|
||||
* [Install via Frappe Bench](https://github.com/frappe/bench)
|
||||
* [Offical Documentation](https://frappeframework.com/docs/user/en/installation)
|
||||
* [Managed Hosting on Frappe Cloud](https://frappecloud.com/deploy?apps=frappe&source=frappe_readme)
|
||||
|
||||
## Contributing
|
||||
|
||||
1. [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
|
||||
1. [Security Policy](SECURITY.md)
|
||||
1. [Translations](https://translate.erpnext.com)
|
||||
|
||||
### Website
|
||||
## Resources
|
||||
|
||||
For details and documentation, see the website
|
||||
[https://frappeframework.com](https://frappeframework.com)
|
||||
1. [frappeframework.com](https://frappeframework.com) - Official documentation of the Frappe Framework.
|
||||
1. [frappe.school](https://frappe.school) - Pick from the various courses by the maintainers or from the community.
|
||||
|
||||
### License
|
||||
## License
|
||||
This repository has been released under the [MIT License](LICENSE).
|
||||
|
|
|
|||
27
codecov.yml
Normal file
27
codecov.yml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
codecov:
|
||||
require_ci_to_pass: yes
|
||||
|
||||
coverage:
|
||||
status:
|
||||
patch: off
|
||||
project:
|
||||
default: false
|
||||
server:
|
||||
target: auto
|
||||
threshold: 0.5%
|
||||
flags:
|
||||
- server
|
||||
|
||||
comment:
|
||||
layout: "diff, flags"
|
||||
require_changes: true
|
||||
|
||||
flags:
|
||||
server:
|
||||
paths:
|
||||
- ".*\\.py"
|
||||
carryforward: true
|
||||
ui-tests:
|
||||
paths:
|
||||
- ".*\\.js"
|
||||
carryforward: true
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000,
|
||||
"video": true,
|
||||
"videoUploadOnPasses": false,
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 2
|
||||
|
|
|
|||
54
cypress/fixtures/doctype_with_tab_break.js
Normal file
54
cypress/fixtures/doctype_with_tab_break.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
export default {
|
||||
name: 'Form With Tab Break',
|
||||
custom: 1,
|
||||
actions: [],
|
||||
doctype: 'DocType',
|
||||
engine: 'InnoDB',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'username',
|
||||
fieldtype: 'Data',
|
||||
label: 'Name',
|
||||
options: 'Name'
|
||||
},
|
||||
{
|
||||
fieldname: 'tab',
|
||||
fieldtype: 'Tab Break',
|
||||
label: 'Tab 2',
|
||||
},
|
||||
{
|
||||
fieldname: 'Phone',
|
||||
fieldtype: 'Data',
|
||||
label: 'Phone',
|
||||
options: 'Phone',
|
||||
reqd: 1
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
"group": "Profile",
|
||||
"link_doctype": "Contact",
|
||||
"link_fieldname": "user"
|
||||
},
|
||||
],
|
||||
modified_by: 'Administrator',
|
||||
module: 'Custom',
|
||||
owner: 'Administrator',
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1
|
||||
}
|
||||
],
|
||||
quick_entry: 1,
|
||||
autoname: "format: Test-{####}",
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
BIN
cypress/fixtures/sample_image.jpg
Normal file
BIN
cypress/fixtures/sample_image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
|
|
@ -31,8 +31,13 @@ context('API Resources', () => {
|
|||
});
|
||||
|
||||
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,12 +7,13 @@ context('Awesome Bar', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
cy.get('.navbar .navbar-home').click();
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').clear();
|
||||
});
|
||||
|
||||
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: 700 });
|
||||
cy.get('.awesomplete').findByRole('listbox').should('be.visible');
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{downarrow}{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
|
|
@ -20,25 +21,25 @@ context('Awesome Bar', () => {
|
|||
});
|
||||
|
||||
it('find text in doctype list', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('test in todo{downarrow}{enter}', { delay: 200 });
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('test in todo{downarrow}{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
cy.get('[data-original-title="Name"] > .input-with-feedback')
|
||||
cy.findByPlaceholderText('Name')
|
||||
.should('have.value', '%test%');
|
||||
});
|
||||
|
||||
it('navigates to new form', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('new blog post{downarrow}{enter}', { delay: 200 });
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('new blog post{downarrow}{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.title-text:visible').should('have.text', 'New Blog Post');
|
||||
});
|
||||
|
||||
it('calculates math expressions', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('55 + 32{downarrow}{enter}', { delay: 200 });
|
||||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)')
|
||||
.type('55 + 32{downarrow}{enter}', { delay: 700 });
|
||||
|
||||
cy.get('.modal-title').should('contain', 'Result');
|
||||
cy.get('.msgprint').should('contain', '55 + 32 = 87');
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ context('Control Barcode', () => {
|
|||
it('should generate barcode on setting a value', () => {
|
||||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=barcode] input')
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.focus()
|
||||
.type('123456789')
|
||||
.blur();
|
||||
|
|
@ -37,11 +37,11 @@ context('Control Barcode', () => {
|
|||
it('should reset when input is cleared', () => {
|
||||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=barcode] input')
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.focus()
|
||||
.type('123456789')
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode] input')
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.clear()
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]')
|
||||
|
|
|
|||
|
|
@ -33,12 +33,13 @@ context('Control Duration', () => {
|
|||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('duration');
|
||||
expect(value).to.equal(3889800);
|
||||
cy.hide_dialog();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide days or seconds according to duration options', () => {
|
||||
get_dialog_with_duration(1, 1).as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first();
|
||||
cy.get('.duration-input[data-duration=days]').should('not.be.visible');
|
||||
cy.get('.duration-input[data-duration=seconds]').should('not.be.visible');
|
||||
});
|
||||
|
|
|
|||
93
cypress/integration/control_float.js
Normal file
93
cypress/integration/control_float.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
context("Control Float", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
});
|
||||
|
||||
function get_dialog_with_float() {
|
||||
return cy.dialog({
|
||||
title: "Float Check",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "float_number",
|
||||
fieldtype: "Float",
|
||||
Label: "Float"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
it("check value changes", () => {
|
||||
get_dialog_with_float().as("dialog");
|
||||
|
||||
let data = get_data();
|
||||
data.forEach(x => {
|
||||
cy.window()
|
||||
.its("frappe")
|
||||
.then(frappe => {
|
||||
frappe.boot.sysdefaults.number_format = x.number_format;
|
||||
});
|
||||
x.values.forEach(d => {
|
||||
cy.get_field("float_number", "Float").clear();
|
||||
cy.fill_field("float_number", d.input, "Float").blur();
|
||||
cy.get_field("float_number", "Float").should(
|
||||
"have.value",
|
||||
d.blur_expected
|
||||
);
|
||||
|
||||
cy.get_field("float_number", "Float").focus();
|
||||
cy.get_field("float_number", "Float").blur();
|
||||
cy.get_field("float_number", "Float").focus();
|
||||
cy.get_field("float_number", "Float").should(
|
||||
"have.value",
|
||||
d.focus_expected
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function get_data() {
|
||||
return [
|
||||
{
|
||||
number_format: "#.###,##",
|
||||
values: [
|
||||
{
|
||||
input: "364.87,334",
|
||||
blur_expected: "36.487,334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "36487,334",
|
||||
blur_expected: "36.487,334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "100",
|
||||
blur_expected: "100,000",
|
||||
focus_expected: "100"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
number_format: "#,###.##",
|
||||
values: [
|
||||
{
|
||||
input: "364,87.334",
|
||||
blur_expected: "36,487.334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "36487.334",
|
||||
blur_expected: "36,487.334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "100",
|
||||
blur_expected: "100.000",
|
||||
focus_expected: "100"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
50
cypress/integration/control_icon.js
Normal file
50
cypress/integration/control_icon.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
context('Control Icon', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_icon() {
|
||||
return cy.dialog({
|
||||
title: 'Icon',
|
||||
fields: [{
|
||||
label: 'Icon',
|
||||
fieldname: 'icon',
|
||||
fieldtype: 'Icon'
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it('should set icon', () => {
|
||||
get_dialog_with_icon().as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').click();
|
||||
|
||||
cy.get('.icon-picker .icon-wrapper[id=active]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'active');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('icon');
|
||||
expect(value).to.equal('active');
|
||||
});
|
||||
|
||||
cy.get('.icon-picker .icon-wrapper[id=resting]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'resting');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('icon');
|
||||
expect(value).to.equal('resting');
|
||||
});
|
||||
});
|
||||
|
||||
it('search for icon and clear search input', () => {
|
||||
let search_text = 'ed';
|
||||
cy.get('.icon-picker').findByRole('searchbox').click().type(search_text);
|
||||
cy.get('.icon-section .icon-wrapper:not(.hidden)').then(i => {
|
||||
cy.get(`.icon-section .icon-wrapper[id*='${search_text}']`).then(icons => {
|
||||
expect(i.length).to.equal(icons.length);
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('.icon-picker').findByRole('searchbox').clear().blur();
|
||||
cy.get('.icon-section .icon-wrapper').should('not.have.class', 'hidden');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -35,7 +35,7 @@ context('Control Link', () => {
|
|||
cy.wait('@search_link');
|
||||
cy.get('@input').type('todo for link', { delay: 200 });
|
||||
cy.wait('@search_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link]').findByRole('listbox').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
|
||||
cy.get('.frappe-control[data-fieldname=link] input').blur();
|
||||
cy.get('@dialog').then(dialog => {
|
||||
|
|
@ -49,7 +49,7 @@ context('Control Link', () => {
|
|||
it('should unset invalid value', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=link] input')
|
||||
.type('invalid value', { delay: 100 })
|
||||
|
|
@ -61,7 +61,7 @@ context('Control Link', () => {
|
|||
it('should route to form on arrow click', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
|
||||
cy.get('@todos').then(todos => {
|
||||
|
|
@ -71,10 +71,25 @@ context('Control Link', () => {
|
|||
cy.get('@input').type(todos[0]).blur();
|
||||
cy.wait('@validate_link');
|
||||
cy.get('@input').focus();
|
||||
cy.get('.frappe-control[data-fieldname=link] .link-btn')
|
||||
cy.findByTitle('Open Link')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.location('pathname').should('eq', `/app/todo/${todos[0]}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch valid value', () => {
|
||||
cy.get('@todos').then(todos => {
|
||||
cy.visit(`/app/todo/${todos[0]}`);
|
||||
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by] input').focus().as('input');
|
||||
cy.get('@input').type('Administrator', {delay: 100}).blur();
|
||||
cy.wait('@validate_link');
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', 'Administrator'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ context('Control Select', () => {
|
|||
cy.get('@control').get('.select-icon').should('exist');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
|
||||
cy.get('@select').select('Option 1');
|
||||
cy.findByDisplayValue('Option 1').should('exist');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'none');
|
||||
cy.get('@select').invoke('val', '');
|
||||
cy.findByDisplayValue('Option 1').should('not.exist');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
|
||||
|
||||
|
||||
|
|
|
|||
22
cypress/integration/dashboard_chart.js
Normal file
22
cypress/integration/dashboard_chart.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
context('Dashboard Chart', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Check filter populate for child table doctype', () => {
|
||||
cy.visit('/app/dashboard-chart/new-dashboard-chart-1');
|
||||
cy.get('[data-fieldname="parent_document_type"]').should('have.css', 'display', 'none');
|
||||
|
||||
cy.get_field('document_type', 'Link');
|
||||
cy.fill_field('document_type', 'Workspace Link', 'Link').focus().blur();
|
||||
cy.get_field('document_type', 'Link').should('have.value', 'Workspace Link');
|
||||
|
||||
cy.fill_field('chart_name', 'Test Chart', 'Data');
|
||||
|
||||
cy.get('[data-fieldname="filters_json"]').click().wait(200);
|
||||
cy.get('.modal-body .filter-action-buttons .add-filter').click();
|
||||
cy.get('.modal-body .fieldname-select-area').click();
|
||||
cy.get('.modal-actions .btn-modal-close').click();
|
||||
});
|
||||
});
|
||||
65
cypress/integration/dashboard_links.js
Normal file
65
cypress/integration/dashboard_links.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
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({ force: true });
|
||||
|
||||
//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('.document-link-badge[data-doctype="Contact"]').click();
|
||||
cy.wait(300);
|
||||
cy.findByRole('button', {name: 'Add Contact'}).should('be.visible');
|
||||
cy.findByRole('button', {name: 'Add 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({ force: true });
|
||||
|
||||
//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({ force: true });
|
||||
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({ force: true });
|
||||
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': ['Website Analytics']
|
||||
}
|
||||
];
|
||||
cur_frm.dashboard.render_report_links();
|
||||
cy.get('[data-report="Website Analytics"]').contains('Website Analytics').click();
|
||||
cy.findByText('Website Analytics');
|
||||
});
|
||||
});
|
||||
});
|
||||
19
cypress/integration/datetime_field_form_validation.js
Normal file
19
cypress/integration/datetime_field_form_validation.js
Normal file
|
|
@ -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');
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
|
@ -62,11 +62,11 @@ context('Depends On', () => {
|
|||
it('should set the field as mandatory depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('test_field', 'Some Value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible');
|
||||
cy.hide_dialog();
|
||||
cy.fill_field('test_field', 'Random value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible');
|
||||
});
|
||||
it('should set the field as read only depending on other fields value', () => {
|
||||
|
|
@ -84,7 +84,7 @@ context('Depends On', () => {
|
|||
cy.fill_field('dependant_field', 'Some Value');
|
||||
//cy.fill_field('test_field', 'Some Other Value');
|
||||
cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').findByRole('button', {name: 'Add Row'}).click();
|
||||
cy.get('@table').find('[data-idx="1"]').as('row1');
|
||||
cy.get('@row1').find('.btn-open-row').click();
|
||||
cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid');
|
||||
|
|
|
|||
79
cypress/integration/discussions.js
Normal file
79
cypress/integration/discussions.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
context('Discussions', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call('frappe.tests.ui_test_helpers.create_data_for_discussions');
|
||||
});
|
||||
});
|
||||
|
||||
const reply_through_modal = () => {
|
||||
cy.visit('/test-page-discussions');
|
||||
|
||||
// Open the modal
|
||||
cy.get('.reply').click();
|
||||
cy.wait(500);
|
||||
cy.get('.discussion-modal').should('be.visible');
|
||||
|
||||
// Enter title
|
||||
cy.get('.modal .topic-title').type('Discussion from tests')
|
||||
.should('have.value', 'Discussion from tests');
|
||||
|
||||
// Enter comment
|
||||
cy.get('.modal .comment-field')
|
||||
.type('This is a discussion from the cypress ui tests.')
|
||||
.should('have.value', 'This is a discussion from the cypress ui tests.');
|
||||
|
||||
// Submit
|
||||
cy.get('.modal .submit-discussion').click();
|
||||
cy.wait(2000);
|
||||
|
||||
// Check if discussion is added to page and content is visible
|
||||
cy.get('.sidebar-parent:first .discussion-topic-title').should('have.text', 'Discussion from tests');
|
||||
cy.get('.discussion-on-page:visible').should('have.class', 'show');
|
||||
cy.get('.discussion-on-page:visible .reply-card .reply-text')
|
||||
.should('have.text', 'This is a discussion from the cypress ui tests.\n');
|
||||
|
||||
};
|
||||
|
||||
const reply_through_comment_box = () => {
|
||||
cy.get('.discussion-on-page:visible .comment-field')
|
||||
.type('This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.')
|
||||
.should('have.value', 'This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.');
|
||||
|
||||
cy.get('.discussion-on-page:visible .submit-discussion').click();
|
||||
cy.wait(3000);
|
||||
cy.get('.discussion-on-page:visible').should('have.class', 'show');
|
||||
cy.get('.discussion-on-page:visible').children(".reply-card").eq(1).children(".reply-text")
|
||||
.should('have.text', 'This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.\n');
|
||||
};
|
||||
|
||||
const cancel_and_clear_comment_box = () => {
|
||||
cy.get('.discussion-on-page:visible .comment-field')
|
||||
.type('This is a discussion from the cypress ui tests.')
|
||||
.should('have.value', 'This is a discussion from the cypress ui tests.');
|
||||
|
||||
cy.get('.discussion-on-page:visible .cancel-comment').click();
|
||||
cy.get('.discussion-on-page:visible .comment-field').should('have.value', '');
|
||||
};
|
||||
|
||||
const single_thread_discussion = () => {
|
||||
cy.visit('/test-single-thread');
|
||||
cy.get('.discussions-sidebar').should('have.length', 0);
|
||||
cy.get('.reply').should('have.length', 0);
|
||||
|
||||
cy.get('.discussion-on-page .comment-field')
|
||||
.type('This comment is being made on a single thread discussion.')
|
||||
.should('have.value', 'This comment is being made on a single thread discussion.');
|
||||
|
||||
cy.get('.discussion-on-page .submit-discussion').click();
|
||||
cy.wait(3000);
|
||||
cy.get('.discussion-on-page').children(".reply-card").eq(-1).children(".reply-text")
|
||||
.should('have.text', 'This comment is being made on a single thread discussion.\n');
|
||||
};
|
||||
|
||||
it('reply through modal', reply_through_modal);
|
||||
it('reply through comment box', reply_through_comment_box);
|
||||
it('cancel and clear comment box', cancel_and_clear_comment_box);
|
||||
it('single thread discussion', single_thread_discussion);
|
||||
});
|
||||
|
|
@ -25,7 +25,7 @@ context('FileUploader', () => {
|
|||
|
||||
cy.get_open_dialog().find('.file-name').should('contain', 'example.json');
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-modal-primary').click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.statusCode').should('eq', 200);
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
|
@ -33,11 +33,11 @@ context('FileUploader', () => {
|
|||
it('should accept uploaded files', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('.btn-file-upload div:contains("Library")').click();
|
||||
cy.get('.file-filter').type('example.json');
|
||||
cy.get_open_dialog().find('.tree-label:contains("example.json")').first().click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Library'}).click();
|
||||
cy.findByPlaceholderText('Search by filename or extension').type('example.json');
|
||||
cy.get_open_dialog().findAllByText('example.json').first().click();
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.body.message')
|
||||
.should('have.property', 'file_name', 'example.json');
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
|
|
@ -46,12 +46,33 @@ context('FileUploader', () => {
|
|||
it('should accept web links', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('.btn-file-upload div:contains("Link")').click();
|
||||
cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Link'}).click();
|
||||
cy.get_open_dialog()
|
||||
.findByPlaceholderText('Attach a web link')
|
||||
.type('https://github.com', { delay: 100, force: true });
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.body.message')
|
||||
.should('have.property', 'file_url', 'https://github.com');
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
it('should allow cropping and optimization for valid images', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('.file-upload-area').attachFile('sample_image.jpg', {
|
||||
subjectType: 'drag-n-drop',
|
||||
});
|
||||
|
||||
cy.get_open_dialog().findAllByText('sample_image.jpg').should('exist');
|
||||
cy.get_open_dialog().find('.btn-crop').first().click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Crop'}).click();
|
||||
cy.get_open_dialog().findAllByRole('checkbox', {name: 'Optimize'}).should('exist');
|
||||
cy.get_open_dialog().findAllByLabelText('Optimize').first().click();
|
||||
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Upload'}).click();
|
||||
cy.wait('@upload_file').its('response.statusCode').should('eq', 200);
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
79
cypress/integration/folder_navigation.js
Normal file
79
cypress/integration/folder_navigation.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
context('Folder Navigation', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/file');
|
||||
});
|
||||
|
||||
it('Adding Folders', () => {
|
||||
//Adding filter to go into the home folder
|
||||
cy.get('.filter-selector > .btn').findByText('1 filter').click();
|
||||
cy.findByRole('button', {name: 'Clear Filters'}).click();
|
||||
cy.get('.filter-action-buttons > .text-muted').findByText('+ Add a Filter').click();
|
||||
cy.get('.fieldname-select-area > .awesomplete > .form-control').type('Fol{enter}');
|
||||
cy.get('.filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback').type('Home{enter}');
|
||||
cy.get('.filter-action-buttons > div > .btn-primary').findByText('Apply Filters').click();
|
||||
|
||||
//Adding folder (Test Folder)
|
||||
cy.get('.menu-btn-group > .btn').click();
|
||||
cy.get('.menu-btn-group [data-label="New Folder"]').click();
|
||||
cy.get('form > [data-fieldname="value"]').type('Test Folder');
|
||||
cy.findByRole('button', {name: 'Create'}).click();
|
||||
});
|
||||
|
||||
it('Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct', () => {
|
||||
//Navigating inside the Attachments folder
|
||||
cy.get('[title="Attachments"] > span').click();
|
||||
|
||||
//To check if the URL formed after visiting the attachments folder is correct
|
||||
cy.location('pathname').should('eq', '/app/file/view/home/Attachments');
|
||||
cy.visit('/app/file/view/home/Attachments');
|
||||
|
||||
//Adding folder inside the attachments folder
|
||||
cy.get('.menu-btn-group > .btn').click();
|
||||
cy.get('.menu-btn-group [data-label="New Folder"]').click();
|
||||
cy.get('form > [data-fieldname="value"]').type('Test Folder');
|
||||
cy.findByRole('button', {name: 'Create'}).click();
|
||||
|
||||
//Navigating inside the added folder in the Attachments folder
|
||||
cy.get('[title="Test Folder"] > span').click();
|
||||
|
||||
//To check if the URL is correct after visiting the Test Folder
|
||||
cy.location('pathname').should('eq', '/app/file/view/home/Attachments/Test%20Folder');
|
||||
cy.visit('/app/file/view/home/Attachments/Test%20Folder');
|
||||
|
||||
//Adding a file inside the Test Folder
|
||||
cy.findByRole('button', {name: 'Add File'}).eq(0).click({force: true});
|
||||
cy.get('.file-uploader').findByText('Link').click();
|
||||
cy.get('.input-group > .form-control').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
|
||||
cy.findByRole('button', {name: 'Upload'}).click();
|
||||
|
||||
//To check if the added file is present in the Test Folder
|
||||
cy.get('span.level-item > span').should('contain', 'Test Folder');
|
||||
cy.get('.list-row-container').eq(0).should('contain.text', '72402.jpg');
|
||||
cy.get('.list-row-checkbox').eq(0).click();
|
||||
|
||||
//Deleting the added file from the Test folder
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.wait(700);
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
cy.wait(700);
|
||||
|
||||
//Deleting the Test Folder
|
||||
cy.visit('/app/file/view/home/Attachments');
|
||||
cy.get('.list-row-checkbox').eq(0).click();
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
});
|
||||
|
||||
it('Deleting Test Folder from the home', () => {
|
||||
//Deleting the Test Folder added in the home directory
|
||||
cy.visit('/app/file/view/home');
|
||||
cy.get('.level-left > .list-subject > .file-select >.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();
|
||||
});
|
||||
});
|
||||
|
|
@ -8,8 +8,7 @@ context('Form', () => {
|
|||
});
|
||||
it('create a new form', () => {
|
||||
cy.visit('/app/todo/new');
|
||||
cy.fill_field('description', 'this is a test todo', 'Text Editor');
|
||||
cy.wait(300);
|
||||
cy.get_field('description', 'Text Editor').type('this is a test todo', {force: true}).wait(200);
|
||||
cy.get('.page-title').should('contain', 'Not Saved');
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
|
|
@ -17,29 +16,34 @@ context('Form', () => {
|
|||
}).as('form_save');
|
||||
cy.get('.primary-action').click();
|
||||
cy.wait('@form_save').its('response.statusCode').should('eq', 200);
|
||||
|
||||
cy.visit('/app/todo');
|
||||
cy.wait(300);
|
||||
cy.get('.title-text').should('be.visible').and('contain', 'To Do');
|
||||
cy.get('.page-head').findByTitle('To Do').should('exist');
|
||||
cy.get('.list-row').should('contain', 'this is a test todo');
|
||||
});
|
||||
|
||||
it('navigates between documents with child table list filters applied', () => {
|
||||
cy.visit('/app/contact');
|
||||
cy.add_filter();
|
||||
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true });
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.visit('/app/contact/Test Form Contact 3');
|
||||
|
||||
cy.clear_filters();
|
||||
cy.get('.standard-filter-section [data-fieldname="name"] input').type('Test Form Contact 3').blur();
|
||||
cy.click_listview_row_item(0);
|
||||
|
||||
cy.get('.prev-doc').should('be.visible').click();
|
||||
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
|
||||
cy.hide_dialog();
|
||||
cy.get('.next-doc').click();
|
||||
cy.wait(200);
|
||||
|
||||
cy.get('.next-doc').should('be.visible').click();
|
||||
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
|
||||
cy.hide_dialog();
|
||||
cy.contains('Test Form Contact 2').should('not.exist');
|
||||
cy.get('.title-text').should('contain', 'Test Form Contact 3');
|
||||
|
||||
cy.get('#page-Contact .page-head').findByTitle('Test Form Contact 3').should('exist');
|
||||
|
||||
// clear filters
|
||||
cy.visit('/app/contact');
|
||||
cy.clear_filters();
|
||||
});
|
||||
|
||||
it('validates behaviour of Data options validations in child table', () => {
|
||||
// test email validations for set_invalid controller
|
||||
let website_input = 'website.in';
|
||||
|
|
|
|||
31
cypress/integration/form_tab_break.js
Normal file
31
cypress/integration/form_tab_break.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import doctype_with_tab_break from '../fixtures/doctype_with_tab_break';
|
||||
const doctype_name = doctype_with_tab_break.name;
|
||||
context("Form Tab Break", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.insert_doc('DocType', doctype_with_tab_break, true);
|
||||
});
|
||||
it("Should switch tab and open correct tabs on validation error", () => {
|
||||
cy.new_form(doctype_name);
|
||||
// test tab switch
|
||||
cy.findByRole("tab", {name: "Tab 2"}).click();
|
||||
cy.findByText("Phone");
|
||||
cy.findByRole("tab", {name: "Details"}).click();
|
||||
cy.findByText("Name");
|
||||
|
||||
// form should switch to the tab with un-filled mandatory field
|
||||
cy.fill_field("username", "Test");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
cy.findByText("Missing Fields");
|
||||
cy.hide_dialog();
|
||||
cy.findByText("Phone");
|
||||
cy.fill_field("phone", "12345678");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
|
||||
// After save, first tab should have dashboard
|
||||
cy.get(".form-tabs > .nav-item").eq(0).click();
|
||||
cy.findByText("Connections");
|
||||
|
||||
});
|
||||
});
|
||||
88
cypress/integration/form_tour.js
Normal file
88
cypress/integration/form_tour.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
context('Form Tour', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/form-tour');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call("frappe.tests.ui_test_helpers.create_form_tour");
|
||||
});
|
||||
});
|
||||
|
||||
const open_test_form_tour = () => {
|
||||
cy.visit('/app/form-tour/Test Form Tour');
|
||||
cy.findByRole('button', {name: 'Show Tour'}).should('be.visible').as('show_tour');
|
||||
cy.get('@show_tour').click();
|
||||
cy.wait(500);
|
||||
cy.url().should('include', '/app/contact');
|
||||
};
|
||||
|
||||
it('jump to a form tour', open_test_form_tour);
|
||||
|
||||
it('navigates a form tour', () => {
|
||||
open_test_form_tour();
|
||||
|
||||
cy.get('.frappe-driver').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname="first_name"]').as('first_name');
|
||||
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
|
||||
cy.get('.frappe-driver').findByRole('button', {name: 'Next'}).as('next_btn');
|
||||
|
||||
// next btn shouldn't move to next step, if first name is not entered
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// after filling the field, next step should be highlighted
|
||||
cy.fill_field('first_name', 'Test Name', 'Data');
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert field is highlighted
|
||||
cy.get('.frappe-control[data-fieldname="last_name"]').as('last_name');
|
||||
cy.get('@last_name').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// after filling the field, next step should be highlighted
|
||||
cy.fill_field('last_name', 'Test Last Name', 'Data');
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert field is highlighted
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('phone_nos');
|
||||
cy.get('@phone_nos').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// move to next step
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert add row btn is highlighted
|
||||
cy.get('@phone_nos').find('.grid-add-row').as('add_row');
|
||||
cy.get('@add_row').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// add a row & move to next step
|
||||
cy.wait(500);
|
||||
cy.get('@add_row').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert table field is highlighted
|
||||
cy.get('.grid-row-open .frappe-control[data-fieldname="phone"]').as('phone');
|
||||
cy.get('@phone').should('have.class', 'driver-highlighted-element');
|
||||
// enter value in a table field
|
||||
let field = cy.fill_table_field('phone_nos', '1', 'phone', '1234567890');
|
||||
field.blur();
|
||||
|
||||
// move to collapse row step
|
||||
cy.wait(500);
|
||||
cy.get('.driver-popover-title').contains('Test Title 4').siblings().get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
// collapse row
|
||||
cy.get('.grid-row-open .grid-collapse-row').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert save btn is highlighted
|
||||
cy.get('.primary-action').should('have.class', 'driver-highlighted-element');
|
||||
cy.wait(500);
|
||||
cy.get('.frappe-driver').findByRole('button', {name: 'Save'}).should('be.visible');
|
||||
|
||||
});
|
||||
});
|
||||
23
cypress/integration/grid_configuration.js
Normal file
23
cypress/integration/grid_configuration.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
context('Grid Configuration', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/doctype/User');
|
||||
});
|
||||
it('Set user wise grid settings', () => {
|
||||
cy.wait(100);
|
||||
cy.get('.frappe-control[data-fieldname="fields"]').as('table');
|
||||
cy.get('@table').find('.icon-sm').click();
|
||||
cy.wait(100);
|
||||
cy.get('.frappe-control[data-fieldname="fields_html"]').as('modal');
|
||||
cy.get('@modal').find('.add-new-fields').click();
|
||||
cy.wait(100);
|
||||
cy.get('[type="checkbox"][data-unit="read_only"]').check();
|
||||
cy.findByRole('button', {name: 'Add'}).click();
|
||||
cy.wait(100);
|
||||
cy.get('[data-fieldname="options"]').invoke('attr', 'value', '1');
|
||||
cy.get('.form-control.column-width[data-fieldname="options"]').trigger('change');
|
||||
cy.findByRole('button', {name: 'Update'}).click();
|
||||
cy.wait(200);
|
||||
cy.get('[title="Read Only"').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
|
@ -30,12 +30,12 @@ context('Grid Pagination', () => {
|
|||
it('adds and deletes rows and changes page', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').findByRole('button', {name: 'Add Row'}).click();
|
||||
cy.get('@table').find('.grid-body .row-index').should('contain', 1001);
|
||||
cy.get('@table').find('.current-page-number').should('contain', '21');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '21');
|
||||
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({ force: true });
|
||||
cy.get('@table').find('button.grid-remove-rows').click();
|
||||
cy.get('@table').findByRole('button', {name: 'Delete'}).click();
|
||||
cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000);
|
||||
cy.get('@table').find('.current-page-number').should('contain', '20');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
|
|
|
|||
|
|
@ -6,12 +6,28 @@ context('List View', () => {
|
|||
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow");
|
||||
});
|
||||
});
|
||||
|
||||
it('Keep checkbox checked after Bulk Update', () => {
|
||||
cy.go_to_list('ToDo');
|
||||
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true });
|
||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
|
||||
cy.get('.dropdown-menu li:visible .dropdown-item .menu-item-label[data-label="Edit"]').click();
|
||||
|
||||
cy.get('.modal-body .form-control[data-fieldname="field"]').first().select('Priority').wait(200);
|
||||
|
||||
cy.get('.modal-footer .standard-actions .btn-primary').click();
|
||||
cy.wait(500);
|
||||
|
||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
|
||||
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible');
|
||||
});
|
||||
|
||||
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.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('.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]);
|
||||
}).then((elements) => {
|
||||
cy.intercept({
|
||||
|
|
@ -24,10 +40,11 @@ context('List View', () => {
|
|||
}).as('real-time-update');
|
||||
cy.wrap(elements).contains('Approve').click();
|
||||
cy.wait(['@bulk-approval', '@real-time-update']);
|
||||
cy.hide_dialog();
|
||||
cy.wait(300);
|
||||
cy.get_open_dialog().find('.btn-modal-close').click();
|
||||
cy.reload();
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row-container:visible').should('contain', 'Approved');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ context('List View Settings', () => {
|
|||
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'DocType Settings');
|
||||
|
||||
cy.get('input[data-fieldname="disable_count"]').check({ force: true });
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({ force: true });
|
||||
cy.get('button').filter(':visible').contains('Save').click();
|
||||
cy.findByLabelText('Disable Count').check({ force: true });
|
||||
cy.findByLabelText('Disable Sidebar Stats').check({ force: true });
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
|
||||
cy.reload({ force: true });
|
||||
|
||||
|
|
@ -29,8 +29,8 @@ context('List View Settings', () => {
|
|||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'DocType Settings');
|
||||
cy.get('input[data-fieldname="disable_count"]').uncheck({ force: true });
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({ force: true });
|
||||
cy.get('button').filter(':visible').contains('Save').click();
|
||||
cy.findByLabelText('Disable Count').uncheck({ force: true });
|
||||
cy.findByLabelText('Disable Sidebar Stats').uncheck({ force: true });
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ context('Login', () => {
|
|||
|
||||
it('validates password', () => {
|
||||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
it('validates email', () => {
|
||||
cy.get('#login_password').type('qwe');
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
|
|
@ -25,8 +25,8 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type('qwer');
|
||||
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.get('.btn-login:visible').contains('Invalid Login. Try again.');
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.findByRole('button', {name: 'Invalid Login. Try again.'}).should('exist');
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
cy.location('pathname').should('eq', '/app');
|
||||
cy.window().its('frappe.session.user').should('eq', 'Administrator');
|
||||
});
|
||||
|
|
@ -60,7 +60,7 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.findByRole('button', {name: 'Login'}).click();
|
||||
|
||||
// verify redirected location and url params after login
|
||||
cy.url().should('include', '/me?' + payload.toString().replace('+', '%20'));
|
||||
|
|
|
|||
58
cypress/integration/multi_select_dialog.js
Normal file
58
cypress/integration/multi_select_dialog.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
context('MultiSelectDialog', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
});
|
||||
|
||||
function open_multi_select_dialog() {
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
new frappe.ui.form.MultiSelectDialog({
|
||||
doctype: "Assignment Rule",
|
||||
target: {},
|
||||
setters: {
|
||||
document_type: null,
|
||||
priority: null
|
||||
},
|
||||
add_filters_group: 1,
|
||||
allow_child_item_selection: 1,
|
||||
child_fieldname: "assignment_days",
|
||||
child_columns: ["day"]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('multi select dialog api works', () => {
|
||||
open_multi_select_dialog();
|
||||
cy.get_open_dialog().should('contain', 'Select Assignment Rules');
|
||||
});
|
||||
|
||||
it('checks for filters', () => {
|
||||
['search_term', 'document_type', 'priority'].forEach(fieldname => {
|
||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="${fieldname}"]`).should('exist');
|
||||
});
|
||||
|
||||
// add_filters_group: 1 should add a filter group
|
||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="filter_area"]`).should('exist');
|
||||
|
||||
});
|
||||
|
||||
it('checks for child item selection', () => {
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('not.exist');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="allow_child_item_selection"]`)
|
||||
.should('exist')
|
||||
.click();
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="child_selection_area"]`)
|
||||
.should('exist');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('contain', 'Assignment Rule');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('contain', 'Day');
|
||||
});
|
||||
});
|
||||
25
cypress/integration/navigation.js
Normal file
25
cypress/integration/navigation.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
context('Navigation', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
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');
|
||||
});
|
||||
|
||||
it.only('Navigate to previous page after login', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.page-head').findByTitle('To Do').should('be.visible');
|
||||
cy.request('/api/method/logout');
|
||||
cy.reload().as('reload');
|
||||
cy.get('@reload').get('.page-card .btn-primary').contains('Login').click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
cy.location('pathname').should('eq', '/app/todo');
|
||||
});
|
||||
});
|
||||
|
|
@ -2,32 +2,62 @@ context('Query Report', () => {
|
|||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
cy.insert_doc('Report', {
|
||||
'report_name': 'Test ToDo Report',
|
||||
'ref_doctype': 'ToDo',
|
||||
'report_type': 'Query Report',
|
||||
'query': 'select * from tabToDo'
|
||||
}, true).as('doc');
|
||||
cy.create_records({
|
||||
doctype: 'ToDo',
|
||||
description: 'this is a test todo for query report'
|
||||
}).as('todos');
|
||||
});
|
||||
|
||||
it('add custom column in report', () => {
|
||||
cy.visit('/app/query-report/Permitted Documents For User');
|
||||
|
||||
cy.get('.page-form.flex', { timeout: 60000 }).should('have.length', 1).then(() => {
|
||||
cy.get('#page-query-report input[data-fieldname="user"]').as('input');
|
||||
cy.get('@input').focus().type('test@erpnext.com', { delay: 100 }).blur();
|
||||
cy.get('#page-query-report input[data-fieldname="user"]').as('input-user');
|
||||
cy.get('@input-user').focus().type('test@erpnext.com', { delay: 100 }).blur();
|
||||
cy.wait(300);
|
||||
cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-test');
|
||||
cy.get('@input-test').focus().type('Role', { delay: 100 }).blur();
|
||||
cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-role');
|
||||
cy.get('@input-role').focus().type('Role', { delay: 100 }).blur();
|
||||
|
||||
cy.get('.datatable').should('exist');
|
||||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').contains('Add Column').click({ force: true });
|
||||
cy.get('.modal-dialog').should('contain', 'Add Column');
|
||||
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
|
||||
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Add Column').click({ force: true });
|
||||
cy.get_open_dialog().get('.modal-title').should('contain', 'Add Column');
|
||||
cy.get('select[data-fieldname="doctype"]').select("Role", { force: true });
|
||||
cy.get('select[data-fieldname="field"]').select("Role Name", { force: true });
|
||||
cy.get('select[data-fieldname="insert_after"]').select("Name", { force: true });
|
||||
cy.get('button').contains('Submit').click({ force: true });
|
||||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').contains('Save').click({ force: true });
|
||||
cy.get('.modal-dialog').should('contain', 'Save Report');
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ force: true });
|
||||
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
|
||||
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Save').click({ timeout: 100, force: true });
|
||||
cy.get_open_dialog().get('.modal-title').should('contain', 'Save Report');
|
||||
|
||||
cy.get('input[data-fieldname="report_name"]').type("Test Report", { delay: 100, force: true });
|
||||
cy.get('button').contains('Submit').click({ timeout: 1000, force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ timeout: 1000, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
let save_report_and_open = (report, update_name) => {
|
||||
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
|
||||
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Save').click({ timeout: 100, force: true });
|
||||
cy.get_open_dialog().get('.modal-title').should('contain', 'Save Report');
|
||||
|
||||
cy.get('input[data-fieldname="report_name"]').type(update_name, { delay: 100, force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ timeout: 1000, force: true });
|
||||
|
||||
cy.visit('/app/query-report/'+report);
|
||||
cy.get('.datatable').should('exist');
|
||||
};
|
||||
|
||||
it('test multi level query report', () => {
|
||||
cy.visit('/app/query-report/Test ToDo Report');
|
||||
cy.get('.datatable').should('exist');
|
||||
|
||||
save_report_and_open('Test ToDo Report 1', ' 1');
|
||||
save_report_and_open('Test ToDo Report 11', '1');
|
||||
});
|
||||
});
|
||||
|
|
@ -13,56 +13,52 @@ context('Recorder', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Navigate to Recorder', () => {
|
||||
cy.visit('/app');
|
||||
cy.awesomebar('recorder');
|
||||
cy.get('h3').should('contain', 'Recorder');
|
||||
cy.url().should('include', '/recorder/detail');
|
||||
});
|
||||
|
||||
it('Recorder Empty State', () => {
|
||||
cy.get('.title-text').should('contain', 'Recorder');
|
||||
cy.get('.page-head').findByTitle('Recorder').should('exist');
|
||||
|
||||
cy.get('.indicator-pill').should('contain', 'Inactive').should('have.class', 'red');
|
||||
|
||||
cy.get('.primary-action').should('contain', 'Start');
|
||||
cy.get('.btn-secondary').should('contain', 'Clear');
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Start'}).should('exist');
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Clear'}).should('exist');
|
||||
|
||||
cy.get('.msg-box').should('contain', 'Inactive');
|
||||
cy.get('.msg-box .btn-primary').should('contain', 'Start Recording');
|
||||
cy.get('.msg-box').should('contain', 'Recorder is Inactive');
|
||||
cy.get('.msg-box').findByRole('button', {name: 'Start Recording'}).should('exist');
|
||||
});
|
||||
|
||||
it('Recorder Start', () => {
|
||||
cy.get('.primary-action').should('contain', 'Start').click();
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Start'}).click();
|
||||
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 found');
|
||||
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.wait('@list_refresh');
|
||||
|
||||
cy.get('.title-text').should('contain', 'DocType');
|
||||
cy.get('.page-head').findByTitle('DocType').should('exist');
|
||||
cy.get('.list-count').should('contain', '20 of ');
|
||||
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.title-text').should('contain', 'Recorder');
|
||||
cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
|
||||
cy.get('.page-head').findByTitle('Recorder').should('exist');
|
||||
cy.get('.frappe-list .result-list').should('contain', '/api/method/frappe.desk.reportview.get');
|
||||
});
|
||||
|
||||
it('Recorder View Request', () => {
|
||||
cy.get('.primary-action').should('contain', 'Start').click();
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Start'}).click();
|
||||
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.wait('@list_refresh');
|
||||
|
||||
cy.get('.title-text').should('contain', 'DocType');
|
||||
cy.get('.page-head').findByTitle('DocType').should('exist');
|
||||
cy.get('.list-count').should('contain', '20 of ');
|
||||
|
||||
cy.visit('/app/recorder');
|
||||
|
||||
cy.get('.list-row-container span').contains('/api/method/frappe').click();
|
||||
cy.get('.frappe-list .list-row-container span')
|
||||
.contains('/api/method/frappe')
|
||||
.should('be.visible')
|
||||
.click({force: true});
|
||||
|
||||
cy.url().should('include', '/recorder/request');
|
||||
cy.get('form').should('contain', '/api/method/frappe');
|
||||
|
|
|
|||
|
|
@ -1,44 +1,47 @@
|
|||
context('Relative Timeframe', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
|
||||
});
|
||||
});
|
||||
it('sets relative timespan filter for last week and filters list', () => {
|
||||
cy.visit('/app/List/ToDo/List');
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.add_filter();
|
||||
cy.get('.fieldname-select-area').should('exist');
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.wait('@list_refresh');
|
||||
cy.get('.list-row-container').its('length').should('eq', 1);
|
||||
cy.get('.list-row-container').should('contain', 'this is second todo');
|
||||
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
.as('save_user_settings');
|
||||
cy.clear_filters();
|
||||
cy.wait('@save_user_settings');
|
||||
});
|
||||
it('sets relative timespan filter for next week and filters list', () => {
|
||||
cy.visit('/app/List/ToDo/List');
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.add_filter();
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.wait('@list_refresh');
|
||||
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
.as('save_user_settings');
|
||||
cy.clear_filters();
|
||||
cy.wait('@save_user_settings');
|
||||
});
|
||||
});
|
||||
// TODO: Enable this again
|
||||
// currently this is flaky possibly because of different timezone in CI
|
||||
|
||||
// context('Relative Timeframe', () => {
|
||||
// before(() => {
|
||||
// cy.login();
|
||||
// cy.visit('/app/website');
|
||||
// cy.window().its('frappe').then(frappe => {
|
||||
// frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
|
||||
// });
|
||||
// });
|
||||
// it('sets relative timespan filter for last week and filters list', () => {
|
||||
// cy.visit('/app/List/ToDo/List');
|
||||
// cy.clear_filters();
|
||||
// cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
// cy.add_filter();
|
||||
// cy.get('.fieldname-select-area').should('exist');
|
||||
// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
// cy.get('select.condition.form-control').select("Timespan");
|
||||
// cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
|
||||
// cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
// cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
// cy.wait('@list_refresh');
|
||||
// cy.get('.list-row-container').its('length').should('eq', 1);
|
||||
// cy.get('.list-row-container').should('contain', 'this is second todo');
|
||||
// cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
// .as('save_user_settings');
|
||||
// cy.clear_filters();
|
||||
// cy.wait('@save_user_settings');
|
||||
// });
|
||||
// it('sets relative timespan filter for next week and filters list', () => {
|
||||
// cy.visit('/app/List/ToDo/List');
|
||||
// cy.clear_filters();
|
||||
// cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
// cy.add_filter();
|
||||
// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
// cy.get('select.condition.form-control').select("Timespan");
|
||||
// cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
|
||||
// cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
// cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
// cy.wait('@list_refresh');
|
||||
// cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
// .as('save_user_settings');
|
||||
// cy.clear_filters();
|
||||
// cy.wait('@save_user_settings');
|
||||
// });
|
||||
// });
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ context('Report View', () => {
|
|||
let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
|
||||
// select the cell
|
||||
cell.dblclick();
|
||||
cell.find('input[data-fieldname="enabled"]').check({ force: true });
|
||||
cy.get('.dt-row-0 > .dt-cell--col-5').click();
|
||||
cell.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true });
|
||||
cy.wait('@value-update');
|
||||
cy.get('@doc').then(doc => {
|
||||
cy.call('frappe.client.get_value', {
|
||||
|
|
|
|||
55
cypress/integration/sidebar.js
Normal file
55
cypress/integration/sidebar.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
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.visit('/app/doctype/ToDo');
|
||||
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/ToDo');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ context('Table MultiSelect', () => {
|
|||
cy.new_form('Assignment Rule');
|
||||
cy.fill_field('__newname', name);
|
||||
cy.fill_field('document_type', 'Blog Post');
|
||||
cy.get('.section-head').contains('Assignment Rules').scrollIntoView();
|
||||
cy.fill_field('assign_condition', 'status=="Open"', 'Code');
|
||||
cy.get('input[data-fieldname="users"]').focus().as('input');
|
||||
cy.get('input[data-fieldname="users"] + ul').should('be.visible');
|
||||
|
|
|
|||
89
cypress/integration/timeline.js
Normal file
89
cypress/integration/timeline.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
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/new-todo-1');
|
||||
cy.get('[data-fieldname="description"] .ql-editor.ql-blank').type('Test ToDo', {force: true}).wait(200);
|
||||
cy.get('.page-head .page-actions').findByRole('button', {name: 'Save'}).click();
|
||||
|
||||
cy.visit('/app/todo');
|
||||
cy.click_listview_row_item(0);
|
||||
|
||||
//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.get('.comment-box').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.click_timeline_action_btn("Dismiss");
|
||||
|
||||
//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('.timeline-message-box .more-actions > .action-btn').click(); //Menu button in timeline item
|
||||
cy.get('.timeline-message-box .more-actions .dropdown-item').contains('Delete').click({ force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Yes'}).click({ force: true });
|
||||
|
||||
cy.get('.timeline-content').should('not.contain', 'Testing Timeline 123');
|
||||
});
|
||||
|
||||
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.click_modal_primary_button('Save');
|
||||
cy.click_modal_primary_button('Submit');
|
||||
|
||||
cy.visit('/app/custom-submittable-doctype');
|
||||
cy.click_listview_row_item(0);
|
||||
|
||||
//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.get('[id="page-Custom Submittable DocType"] .page-actions').findByRole('button', {name: 'Cancel'}).click();
|
||||
cy.get_open_dialog().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.select_listview_row_checkbox(0);
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
|
||||
//Deleting the custom doctype
|
||||
cy.visit('/app/doctype');
|
||||
cy.select_listview_row_checkbox(0);
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
});
|
||||
});
|
||||
74
cypress/integration/timeline_email.js
Normal file
74
cypress/integration/timeline_email.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
context('Timeline Email', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/todo');
|
||||
});
|
||||
|
||||
it('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);
|
||||
});
|
||||
|
||||
it('Adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => {
|
||||
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.wait(500);
|
||||
cy.get('.modal-footer:visible > .standard-actions > .btn-primary').contains('Yes').click();
|
||||
|
||||
//To check if the removed attachment is shown in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Removed 72402.jpg');
|
||||
cy.wait(500);
|
||||
|
||||
//To check if the discard button functionality in email is working correctly
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal-footer > .standard-actions > .btn-secondary').contains('Discard').click();
|
||||
cy.wait(500);
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.wait(500);
|
||||
cy.get_field('recipients', 'MultiSelect').should('have.text', '');
|
||||
cy.get('.modal-header:visible > .modal-actions > .btn-modal-close > .icon').click();
|
||||
|
||||
//Deleting the added ToDo
|
||||
cy.get('.menu-btn-group:visible > .btn').click();
|
||||
cy.get('.menu-btn-group:visible > .dropdown-menu > li > .dropdown-item').contains('Delete').click();
|
||||
cy.get('.modal-footer:visible > .standard-actions > .btn-primary').click();
|
||||
});
|
||||
});
|
||||
90
cypress/integration/workspace.js
Normal file
90
cypress/integration/workspace.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
context('Workspace 2.0', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Navigate to page from sidebar', () => {
|
||||
cy.visit('/app/build');
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.sidebar-item-container[item-name="Settings"]').first().click();
|
||||
cy.location('pathname').should('eq', '/app/settings');
|
||||
});
|
||||
|
||||
it('Create Private Page', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
|
||||
cy.fill_field('title', 'Test Private Page', 'Data');
|
||||
cy.fill_field('icon', 'edit', 'Icon');
|
||||
cy.get_open_dialog().find('.modal-header').click();
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
|
||||
// check if sidebar item is added in pubic section
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
cy.wait(300);
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.wait(500);
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
});
|
||||
|
||||
it('Add New Block', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click();
|
||||
cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Heading').click();
|
||||
cy.get(":focus").type('Header');
|
||||
cy.get(".ce-block:last").find('.ce-header').should('exist');
|
||||
|
||||
cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click();
|
||||
cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Text').click();
|
||||
cy.get(":focus").type('Paragraph text');
|
||||
cy.get(".ce-block:last").find('.ce-paragraph').should('exist');
|
||||
});
|
||||
|
||||
it('Delete A Block', () => {
|
||||
cy.get(".ce-block:last").find('.delete-paragraph').click();
|
||||
cy.get(".ce-block:last").find('.ce-paragraph').should('not.exist');
|
||||
});
|
||||
|
||||
it('Shrink and Expand A Block', () => {
|
||||
cy.get(".ce-block:last").find('.tune-btn').click();
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-11');
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-10');
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-9');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-10');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-11');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-12');
|
||||
});
|
||||
|
||||
it('Change Header Text Size', () => {
|
||||
cy.get('.ce-settings--opened .cdx-settings-button[data-level="3"]').click();
|
||||
cy.get(".ce-block:last").find('.widget-head h3').should('exist');
|
||||
cy.get('.ce-settings--opened .cdx-settings-button[data-level="4"]').click();
|
||||
cy.get(".ce-block:last").find('.widget-head h4').should('exist');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
});
|
||||
|
||||
it('Delete Private Page', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').find('.sidebar-item-control .delete-page').click();
|
||||
cy.wait(300);
|
||||
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
module.exports = () => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
};
|
||||
module.exports = (on, config) => {
|
||||
require('@cypress/code-coverage/task')(on, config);
|
||||
return config;
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import 'cypress-file-upload';
|
||||
import '@testing-library/cypress/add-commands';
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
|
|
@ -186,22 +187,22 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
|
|||
if (fieldtype === 'Select') {
|
||||
cy.get('@input').select(value);
|
||||
} 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');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
|
||||
let selector = `.form-control[data-fieldname="${fieldname}"]`;
|
||||
let selector = `[data-fieldname="${fieldname}"] input:visible`;
|
||||
|
||||
if (fieldtype === 'Text Editor') {
|
||||
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
|
||||
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]:visible`;
|
||||
}
|
||||
if (fieldtype === 'Code') {
|
||||
selector = `[data-fieldname="${fieldname}"] .ace_text-input`;
|
||||
}
|
||||
|
||||
return cy.get(selector);
|
||||
return cy.get(selector).first();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
|
||||
|
|
@ -240,7 +241,7 @@ Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fie
|
|||
});
|
||||
|
||||
Cypress.Commands.add('awesomebar', text => {
|
||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
|
||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 700});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('new_form', doctype => {
|
||||
|
|
@ -251,7 +252,8 @@ Cypress.Commands.add('new_form', 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', () => {
|
||||
|
|
@ -315,7 +317,11 @@ Cypress.Commands.add('add_filter', () => {
|
|||
});
|
||||
|
||||
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.get('.filter-popover').should('exist');
|
||||
cy.get('.filter-popover').find('.clear-filters').click();
|
||||
|
|
@ -323,4 +329,33 @@ Cypress.Commands.add('clear_filters', () => {
|
|||
cy.window().its('cur_list').then(cur_list => {
|
||||
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 > .level-item > .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-message-box .actions .action-btn').contains(btn_name).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('select_listview_row_checkbox', (row_no) => {
|
||||
cy.get('.frappe-list .select-like > .list-row-checkbox').eq(row_no).click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
import '@cypress/code-coverage/support';
|
||||
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
|
|
|
|||
4
dev-requirements.txt
Normal file
4
dev-requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
coverage==5.5
|
||||
Faker~=8.1.0
|
||||
pyngrok~=5.0.5
|
||||
unittest-xml-reporting~=3.0.4
|
||||
38
esbuild/build-cleanup.js
Normal file
38
esbuild/build-cleanup.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/* eslint-disable no-console */
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const glob = require("fast-glob");
|
||||
|
||||
module.exports = {
|
||||
name: 'build_cleanup',
|
||||
setup(build) {
|
||||
build.onEnd(result => {
|
||||
if (result.errors.length) return;
|
||||
clean_dist_files(Object.keys(result.metafile.outputs));
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function clean_dist_files(new_files) {
|
||||
new_files.forEach(
|
||||
file => {
|
||||
if (file.endsWith(".map")) return;
|
||||
|
||||
const pattern = file.split(".").slice(0, -2).join(".") + "*";
|
||||
glob.sync(pattern).forEach(
|
||||
file_to_delete => {
|
||||
if (file_to_delete.startsWith(file)) return;
|
||||
|
||||
fs.unlink(path.resolve(file_to_delete), err => {
|
||||
if (!err) return;
|
||||
|
||||
console.error(
|
||||
`Error deleting ${file.split(path.sep).pop()}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -1,17 +1,20 @@
|
|||
/* eslint-disable no-console */
|
||||
let path = require("path");
|
||||
let fs = require("fs");
|
||||
let glob = require("fast-glob");
|
||||
let esbuild = require("esbuild");
|
||||
let vue = require("esbuild-vue");
|
||||
let yargs = require("yargs");
|
||||
let cliui = require("cliui")();
|
||||
let chalk = require("chalk");
|
||||
let html_plugin = require("./frappe-html");
|
||||
let postCssPlugin = require("esbuild-plugin-postcss2").default;
|
||||
let ignore_assets = require("./ignore-assets");
|
||||
let sass_options = require("./sass_options");
|
||||
let {
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const glob = require("fast-glob");
|
||||
const esbuild = require("esbuild");
|
||||
const vue = require("esbuild-vue");
|
||||
const yargs = require("yargs");
|
||||
const cliui = require("cliui")();
|
||||
const chalk = require("chalk");
|
||||
const html_plugin = require("./frappe-html");
|
||||
const rtlcss = require('rtlcss');
|
||||
const postCssPlugin = require("esbuild-plugin-postcss2").default;
|
||||
const ignore_assets = require("./ignore-assets");
|
||||
const sass_options = require("./sass_options");
|
||||
const build_cleanup_plugin = require("./build-cleanup");
|
||||
|
||||
const {
|
||||
app_list,
|
||||
assets_path,
|
||||
apps_path,
|
||||
|
|
@ -25,7 +28,7 @@ let {
|
|||
get_redis_subscriber
|
||||
} = require("./utils");
|
||||
|
||||
let argv = yargs
|
||||
const argv = yargs
|
||||
.usage("Usage: node esbuild [options]")
|
||||
.option("apps", {
|
||||
type: "string",
|
||||
|
|
@ -43,6 +46,11 @@ let argv = yargs
|
|||
type: "boolean",
|
||||
description: "Run in watch mode and rebuild on file changes"
|
||||
})
|
||||
.option("live-reload", {
|
||||
type: "boolean",
|
||||
description: `Automatically reload Desk when assets are rebuilt.
|
||||
Can only be used with the --watch flag.`
|
||||
})
|
||||
.option("production", {
|
||||
type: "boolean",
|
||||
description: "Run build in production mode"
|
||||
|
|
@ -92,28 +100,30 @@ if (WATCH_MODE) {
|
|||
|
||||
async function execute() {
|
||||
console.time(TOTAL_BUILD_TIME);
|
||||
if (!FILES_TO_BUILD.length) {
|
||||
await clean_dist_folders(APPS);
|
||||
}
|
||||
|
||||
let result;
|
||||
let results;
|
||||
try {
|
||||
result = await build_assets_for_apps(APPS, FILES_TO_BUILD);
|
||||
results = await build_assets_for_apps(APPS, FILES_TO_BUILD);
|
||||
} catch (e) {
|
||||
log_error("There were some problems during build");
|
||||
log();
|
||||
log(chalk.dim(e.stack));
|
||||
if (process.env.CI) {
|
||||
process.kill(process.pid);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!WATCH_MODE) {
|
||||
log_built_assets(result.metafile);
|
||||
log_built_assets(results);
|
||||
console.timeEnd(TOTAL_BUILD_TIME);
|
||||
log();
|
||||
} else {
|
||||
log("Watching for changes...");
|
||||
}
|
||||
return await write_assets_json(result.metafile);
|
||||
for (const result of results) {
|
||||
await write_assets_json(result.metafile);
|
||||
}
|
||||
}
|
||||
|
||||
function build_assets_for_apps(apps, files) {
|
||||
|
|
@ -125,6 +135,8 @@ function build_assets_for_apps(apps, files) {
|
|||
let output_path = assets_path;
|
||||
|
||||
let file_map = {};
|
||||
let style_file_map = {};
|
||||
let rtl_style_file_map = {};
|
||||
for (let file of files) {
|
||||
let relative_app_path = path.relative(apps_path, file);
|
||||
let app = relative_app_path.split(path.sep)[0];
|
||||
|
|
@ -140,19 +152,32 @@ function build_assets_for_apps(apps, files) {
|
|||
}
|
||||
output_name = path.join(app, "dist", output_name);
|
||||
|
||||
if (Object.keys(file_map).includes(output_name)) {
|
||||
if (Object.keys(file_map).includes(output_name) || Object.keys(style_file_map).includes(output_name)) {
|
||||
log_warn(
|
||||
`Duplicate output file ${output_name} generated from ${file}`
|
||||
);
|
||||
}
|
||||
|
||||
file_map[output_name] = file;
|
||||
if ([".css", ".scss", ".less", ".sass", ".styl"].includes(extension)) {
|
||||
style_file_map[output_name] = file;
|
||||
rtl_style_file_map[output_name.replace('/css/', '/css-rtl/')] = file;
|
||||
} else {
|
||||
file_map[output_name] = file;
|
||||
}
|
||||
}
|
||||
|
||||
return build_files({
|
||||
let build = build_files({
|
||||
files: file_map,
|
||||
outdir: output_path
|
||||
});
|
||||
let style_build = build_style_files({
|
||||
files: style_file_map,
|
||||
outdir: output_path
|
||||
});
|
||||
let rtl_style_build = build_style_files({
|
||||
files: rtl_style_file_map,
|
||||
outdir: output_path,
|
||||
rtl_style: true
|
||||
});
|
||||
return Promise.all([build, style_build, rtl_style_build]);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +228,35 @@ function get_files_to_build(files) {
|
|||
}
|
||||
|
||||
function build_files({ files, outdir }) {
|
||||
return esbuild.build({
|
||||
let build_plugins = [
|
||||
html_plugin,
|
||||
build_cleanup_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,
|
||||
build_cleanup_plugin,
|
||||
postCssPlugin({
|
||||
plugins: plugins,
|
||||
sassOptions: sass_options
|
||||
})
|
||||
];
|
||||
|
||||
plugins.push(require("autoprefixer"));
|
||||
return esbuild.build(get_build_options(files, outdir, build_plugins));
|
||||
}
|
||||
|
||||
function get_build_options(files, outdir, plugins) {
|
||||
return {
|
||||
entryPoints: files,
|
||||
entryNames: "[dir]/[name].[hash]",
|
||||
outdir,
|
||||
|
|
@ -217,17 +270,9 @@ function build_files({ files, outdir }) {
|
|||
PRODUCTION ? "production" : "development"
|
||||
)
|
||||
},
|
||||
plugins: [
|
||||
html_plugin,
|
||||
ignore_assets,
|
||||
vue(),
|
||||
postCssPlugin({
|
||||
plugins: [require("autoprefixer")],
|
||||
sassOptions: sass_options
|
||||
})
|
||||
],
|
||||
plugins: plugins,
|
||||
watch: get_watch_config()
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function get_watch_config() {
|
||||
|
|
@ -244,10 +289,24 @@ function get_watch_config() {
|
|||
assets_json,
|
||||
prev_assets_json
|
||||
} = await write_assets_json(result.metafile);
|
||||
|
||||
let changed_files;
|
||||
if (prev_assets_json) {
|
||||
log_rebuilt_assets(prev_assets_json, assets_json);
|
||||
changed_files = get_rebuilt_assets(
|
||||
prev_assets_json,
|
||||
assets_json
|
||||
);
|
||||
|
||||
let timestamp = new Date().toLocaleTimeString();
|
||||
let message = `${timestamp}: Compiled ${changed_files.length} files...`;
|
||||
log(chalk.yellow(message));
|
||||
for (let filepath of changed_files) {
|
||||
let filename = path.basename(filepath);
|
||||
log(" " + filename);
|
||||
}
|
||||
log();
|
||||
}
|
||||
notify_redis({ success: true });
|
||||
notify_redis({ success: true, changed_files });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -255,19 +314,11 @@ function get_watch_config() {
|
|||
return null;
|
||||
}
|
||||
|
||||
async function clean_dist_folders(apps) {
|
||||
for (let app of apps) {
|
||||
let public_path = get_public_path(app);
|
||||
await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
|
||||
recursive: true
|
||||
});
|
||||
await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
|
||||
recursive: true
|
||||
});
|
||||
function log_built_assets(results) {
|
||||
let outputs = {};
|
||||
for (const result of results) {
|
||||
outputs = Object.assign(outputs, result.metafile.outputs);
|
||||
}
|
||||
}
|
||||
|
||||
function log_built_assets(metafile) {
|
||||
let column_widths = [60, 20];
|
||||
cliui.div(
|
||||
{
|
||||
|
|
@ -282,9 +333,9 @@ function log_built_assets(metafile) {
|
|||
cliui.div("");
|
||||
|
||||
let output_by_dist_path = {};
|
||||
for (let outfile in metafile.outputs) {
|
||||
for (let outfile in outputs) {
|
||||
if (outfile.endsWith(".map")) continue;
|
||||
let data = metafile.outputs[outfile];
|
||||
let data = outputs[outfile];
|
||||
outfile = path.resolve(outfile);
|
||||
outfile = path.relative(assets_path, outfile);
|
||||
let filename = path.basename(outfile);
|
||||
|
|
@ -339,7 +390,11 @@ async function write_assets_json(metafile) {
|
|||
let info = metafile.outputs[output];
|
||||
let asset_path = "/" + path.relative(sites_path, output);
|
||||
if (info.entryPoint) {
|
||||
out[path.basename(info.entryPoint)] = asset_path;
|
||||
let key = path.basename(info.entryPoint);
|
||||
if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) {
|
||||
key = `rtl_${key}`;
|
||||
}
|
||||
out[key] = asset_path;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -403,7 +458,7 @@ function run_build_command_for_apps(apps) {
|
|||
process.chdir(cwd);
|
||||
}
|
||||
|
||||
async function notify_redis({ error, success }) {
|
||||
async function notify_redis({ error, success, changed_files }) {
|
||||
// notify redis which in turns tells socketio to publish this to browser
|
||||
let subscriber = get_redis_subscriber("redis_socketio");
|
||||
subscriber.on("error", _ => {
|
||||
|
|
@ -425,7 +480,9 @@ async function notify_redis({ error, success }) {
|
|||
}
|
||||
if (success) {
|
||||
payload = {
|
||||
success: true
|
||||
success: true,
|
||||
changed_files,
|
||||
live_reload: argv["live-reload"]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -455,7 +512,7 @@ function open_in_editor() {
|
|||
subscriber.subscribe("open_in_editor");
|
||||
}
|
||||
|
||||
function log_rebuilt_assets(prev_assets, new_assets) {
|
||||
function get_rebuilt_assets(prev_assets, new_assets) {
|
||||
let added_files = [];
|
||||
let old_files = Object.values(prev_assets);
|
||||
let new_files = Object.values(new_assets);
|
||||
|
|
@ -465,17 +522,5 @@ function log_rebuilt_assets(prev_assets, new_assets) {
|
|||
added_files.push(filepath);
|
||||
}
|
||||
}
|
||||
|
||||
log(
|
||||
chalk.yellow(
|
||||
`${new Date().toLocaleTimeString()}: Compiled ${
|
||||
added_files.length
|
||||
} files...`
|
||||
)
|
||||
);
|
||||
for (let filepath of added_files) {
|
||||
let filename = path.basename(filepath);
|
||||
log(" " + filename);
|
||||
}
|
||||
log();
|
||||
return added_files;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -28,8 +28,7 @@ from .exceptions import *
|
|||
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
|
||||
from .utils.lazy_loader import lazy_import
|
||||
|
||||
# Lazy imports
|
||||
faker = lazy_import('faker')
|
||||
from frappe.query_builder import get_query_builder, patch_query_execute
|
||||
|
||||
__version__ = '14.0.0-dev'
|
||||
|
||||
|
|
@ -42,7 +41,8 @@ class _dict(dict):
|
|||
"""dict like object that exposes keys as attributes"""
|
||||
def __getattr__(self, key):
|
||||
ret = self.get(key)
|
||||
if not ret and key.startswith("__"):
|
||||
# "__deepcopy__" exception added to fix frappe#14833 via DFP
|
||||
if not ret and key.startswith("__") and key != "__deepcopy__":
|
||||
raise AttributeError()
|
||||
return ret
|
||||
def __setattr__(self, key, value):
|
||||
|
|
@ -118,6 +118,7 @@ def set_user_lang(user, user_language=None):
|
|||
|
||||
# local-globals
|
||||
db = local("db")
|
||||
qb = local("qb")
|
||||
conf = local("conf")
|
||||
form = form_dict = local("form_dict")
|
||||
request = local("request")
|
||||
|
|
@ -137,7 +138,11 @@ lang = local("lang")
|
|||
if typing.TYPE_CHECKING:
|
||||
from frappe.database.mariadb.database import MariaDBDatabase
|
||||
from frappe.database.postgres.database import PostgresDatabase
|
||||
from frappe.query_builder.builder import MariaDB, Postgres
|
||||
|
||||
db: typing.Union[MariaDBDatabase, PostgresDatabase]
|
||||
qb: typing.Union[MariaDB, Postgres]
|
||||
|
||||
# end: static analysis hack
|
||||
|
||||
def init(site, sites_path=None, new_site=False):
|
||||
|
|
@ -202,8 +207,10 @@ def init(site, sites_path=None, new_site=False):
|
|||
local.form_dict = _dict()
|
||||
local.session = _dict()
|
||||
local.dev_server = _dev_server
|
||||
local.qb = get_query_builder(local.conf.db_type or "mariadb")
|
||||
|
||||
setup_module_map()
|
||||
patch_query_execute()
|
||||
|
||||
local.initialised = True
|
||||
|
||||
|
|
@ -226,12 +233,13 @@ def connect_replica():
|
|||
from frappe.database import get_db
|
||||
user = local.conf.db_name
|
||||
password = local.conf.db_password
|
||||
port = local.conf.replica_db_port
|
||||
|
||||
if local.conf.different_credentials_for_replica:
|
||||
user = local.conf.replica_db_name
|
||||
password = local.conf.replica_db_password
|
||||
|
||||
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password)
|
||||
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password, port=port)
|
||||
|
||||
# swap db connections
|
||||
local.primary_db = local.db
|
||||
|
|
@ -480,11 +488,11 @@ def get_request_header(key, default=None):
|
|||
:param default: Default value."""
|
||||
return request.headers.get(key, default)
|
||||
|
||||
def sendmail(recipients=[], sender="", subject="No Subject", message="No Message",
|
||||
def sendmail(recipients=None, sender="", subject="No Subject", message="No Message",
|
||||
as_markdown=False, delayed=True, reference_doctype=None, reference_name=None,
|
||||
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, add_unsubscribe_link=1,
|
||||
attachments=None, content=None, doctype=None, name=None, reply_to=None, queue_separately=False,
|
||||
cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None,
|
||||
cc=None, bcc=None, message_id=None, in_reply_to=None, send_after=None, expose_recipients=None,
|
||||
send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False,
|
||||
inline_images=None, template=None, args=None, header=None, print_letterhead=False, with_container=False):
|
||||
"""Send email using user's default **Email Account** or global default **Email Account**.
|
||||
|
|
@ -514,6 +522,14 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
|
|||
:param header: Append header in email
|
||||
:param with_container: Wraps email inside a styled container
|
||||
"""
|
||||
|
||||
if recipients is None:
|
||||
recipients = []
|
||||
if cc is None:
|
||||
cc = []
|
||||
if bcc is None:
|
||||
bcc = []
|
||||
|
||||
text_content = None
|
||||
if template:
|
||||
message, text_content = get_email_from_template(template, args)
|
||||
|
|
@ -609,8 +625,6 @@ def read_only():
|
|||
|
||||
try:
|
||||
retval = fn(*args, **get_newargs(fn, kwargs))
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
if local and hasattr(local, 'primary_db'):
|
||||
local.db.close()
|
||||
|
|
@ -620,6 +634,29 @@ def read_only():
|
|||
return wrapper_fn
|
||||
return innfn
|
||||
|
||||
def write_only():
|
||||
# if replica connection exists, we have to replace it momentarily with the primary connection
|
||||
def innfn(fn):
|
||||
def wrapper_fn(*args, **kwargs):
|
||||
primary_db = getattr(local, "primary_db", None)
|
||||
replica_db = getattr(local, "replica_db", None)
|
||||
in_read_only = getattr(local, "db", None) != primary_db
|
||||
|
||||
# switch to primary connection
|
||||
if in_read_only and primary_db:
|
||||
local.db = local.primary_db
|
||||
|
||||
try:
|
||||
retval = fn(*args, **get_newargs(fn, kwargs))
|
||||
finally:
|
||||
# switch back to replica connection
|
||||
if in_read_only and replica_db:
|
||||
local.db = replica_db
|
||||
|
||||
return retval
|
||||
return wrapper_fn
|
||||
return innfn
|
||||
|
||||
def only_for(roles, message=False):
|
||||
"""Raise `frappe.PermissionError` if the user does not have any of the given **Roles**.
|
||||
|
||||
|
|
@ -690,18 +727,20 @@ def only_has_select_perm(doctype, user=None, ignore_permissions=False):
|
|||
else:
|
||||
return False
|
||||
|
||||
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False):
|
||||
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False, parent_doctype=None):
|
||||
"""Raises `frappe.PermissionError` if not permitted.
|
||||
|
||||
:param doctype: DocType for which permission is to be check.
|
||||
:param ptype: Permission type (`read`, `write`, `create`, `submit`, `cancel`, `amend`). Default: `read`.
|
||||
:param doc: [optional] Checks User permissions for given doc.
|
||||
:param user: [optional] Check for given user. Default: current user."""
|
||||
:param user: [optional] Check for given user. Default: current user.
|
||||
:param parent_doctype: Required when checking permission for a child DocType (unless doc is specified)."""
|
||||
if not doctype and doc:
|
||||
doctype = doc.doctype
|
||||
|
||||
import frappe.permissions
|
||||
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user, raise_exception=throw)
|
||||
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user,
|
||||
raise_exception=throw, parent_doctype=parent_doctype)
|
||||
if throw and not out:
|
||||
if doc:
|
||||
frappe.throw(_("No permission for {0}").format(doc.doctype + " " + doc.name))
|
||||
|
|
@ -1449,7 +1488,10 @@ def get_value(*args, **kwargs):
|
|||
|
||||
def as_json(obj, indent=1):
|
||||
from frappe.utils.response import json_handler
|
||||
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': '))
|
||||
try:
|
||||
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': '))
|
||||
except TypeError:
|
||||
return json.dumps(obj, indent=indent, default=json_handler, separators=(',', ': '))
|
||||
|
||||
def are_emails_muted():
|
||||
from frappe.utils import cint
|
||||
|
|
@ -1491,7 +1533,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
|
|||
:param style: Print Format style.
|
||||
:param as_pdf: Return as PDF. Default False.
|
||||
:param password: Password to encrypt the pdf with. Default None"""
|
||||
from frappe.website.render import build_page
|
||||
from frappe.website.serve import get_response_content
|
||||
from frappe.utils.pdf import get_pdf
|
||||
|
||||
local.form_dict.doctype = doctype
|
||||
|
|
@ -1506,7 +1548,7 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
|
|||
options = {'password': password}
|
||||
|
||||
if not html:
|
||||
html = build_page("printview")
|
||||
html = get_response_content("printview")
|
||||
|
||||
if as_pdf:
|
||||
return get_pdf(html, output = output, options = options)
|
||||
|
|
@ -1683,7 +1725,7 @@ def get_desk_link(doctype, name):
|
|||
)
|
||||
|
||||
def bold(text):
|
||||
return '<b>{0}</b>'.format(text)
|
||||
return '<strong>{0}</strong>'.format(text)
|
||||
|
||||
def safe_eval(code, eval_globals=None, eval_locals=None):
|
||||
'''A safer `eval`'''
|
||||
|
|
@ -1756,7 +1798,7 @@ def get_version(doctype, name, limit=None, head=False, raise_err=True):
|
|||
'limit': limit
|
||||
}, as_list=1)
|
||||
|
||||
from frappe.chat.util import squashify, dictify, safe_json_loads
|
||||
from frappe.utils import squashify, dictify, safe_json_loads
|
||||
|
||||
versions = []
|
||||
|
||||
|
|
@ -1804,6 +1846,7 @@ def parse_json(val):
|
|||
return parse_json(val)
|
||||
|
||||
def mock(type, size=1, locale='en'):
|
||||
import faker
|
||||
results = []
|
||||
fake = faker.Faker(locale)
|
||||
if type not in dir(fake):
|
||||
|
|
@ -1813,7 +1856,7 @@ def mock(type, size=1, locale='en'):
|
|||
data = getattr(fake, type)()
|
||||
results.append(data)
|
||||
|
||||
from frappe.chat.util import squashify
|
||||
from frappe.utils import squashify
|
||||
return squashify(results)
|
||||
|
||||
def validate_and_sanitize_search_inputs(fn):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
# License: MIT. See LICENSE
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
|
|
@ -82,7 +82,7 @@ def handle():
|
|||
if frappe.local.request.method=="PUT":
|
||||
data = get_request_form_data()
|
||||
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
doc = frappe.get_doc(doctype, name, for_update=True)
|
||||
|
||||
if "flags" in data:
|
||||
del data["flags"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
|
@ -16,9 +16,9 @@ import frappe.handler
|
|||
import frappe.auth
|
||||
import frappe.api
|
||||
import frappe.utils.response
|
||||
import frappe.website.render
|
||||
from frappe.utils import get_site_name, sanitize_html
|
||||
from frappe.middlewares import StaticDataMiddleware
|
||||
from frappe.website.serve import get_response
|
||||
from frappe.utils.error import make_error_snapshot
|
||||
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
|
||||
from frappe import _
|
||||
|
|
@ -72,7 +72,7 @@ def application(request):
|
|||
response = frappe.utils.response.download_private_file(request.path)
|
||||
|
||||
elif request.method in ('GET', 'HEAD', 'POST'):
|
||||
response = frappe.website.render.render()
|
||||
response = get_response()
|
||||
|
||||
else:
|
||||
raise NotFound
|
||||
|
|
@ -266,8 +266,7 @@ def handle_exception(e):
|
|||
make_error_snapshot(e)
|
||||
|
||||
if return_as_message:
|
||||
response = frappe.website.render.render("message",
|
||||
http_status_code=http_status_code)
|
||||
response = get_response("message", http_status_code=http_status_code)
|
||||
|
||||
return response
|
||||
|
||||
|
|
|
|||
113
frappe/auth.py
113
frappe/auth.py
|
|
@ -1,31 +1,58 @@
|
|||
# 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.database
|
||||
import frappe.utils
|
||||
from frappe.utils import cint, flt, get_datetime, datetime, date_diff, today
|
||||
import frappe.utils.user
|
||||
from frappe import conf
|
||||
from frappe.sessions import Session, clear_sessions, delete_session
|
||||
from frappe.modules.patch_handler import check_session_stopped
|
||||
from frappe.translate import get_lang_code
|
||||
from frappe.utils.password import check_password, delete_login_failed_cache
|
||||
from frappe import _, conf
|
||||
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
|
||||
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,
|
||||
confirm_otp_token, get_cached_user_pass)
|
||||
from frappe.modules.patch_handler import check_session_stopped
|
||||
from frappe.sessions import Session, clear_sessions, delete_session
|
||||
from frappe.translate import get_language
|
||||
from frappe.twofactor import authenticate_for_2factor, confirm_otp_token, get_cached_user_pass, should_run_2fa
|
||||
from frappe.utils import cint, date_diff, datetime, get_datetime, today
|
||||
from frappe.utils.password import check_password
|
||||
from frappe.website.utils import get_home_page
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
class HTTPRequest:
|
||||
def __init__(self):
|
||||
# Get Environment variables
|
||||
self.domain = frappe.request.host
|
||||
if self.domain and self.domain.startswith('www.'):
|
||||
self.domain = self.domain[4:]
|
||||
# set frappe.local.request_ip
|
||||
self.set_request_ip()
|
||||
|
||||
# load cookies
|
||||
self.set_cookies()
|
||||
|
||||
# set frappe.local.db
|
||||
self.connect()
|
||||
|
||||
# login and start/resume user session
|
||||
self.set_session()
|
||||
|
||||
# set request language
|
||||
self.set_lang()
|
||||
|
||||
# match csrf token from current session
|
||||
self.validate_csrf_token()
|
||||
|
||||
# write out latest cookies
|
||||
frappe.local.cookie_manager.init_cookies()
|
||||
|
||||
# check session status
|
||||
check_session_stopped()
|
||||
|
||||
@property
|
||||
def domain(self):
|
||||
if not getattr(self, "_domain", None):
|
||||
self._domain = frappe.request.host
|
||||
if self._domain and self._domain.startswith('www.'):
|
||||
self._domain = self._domain[4:]
|
||||
|
||||
return self._domain
|
||||
|
||||
def set_request_ip(self):
|
||||
if frappe.get_request_header('X-Forwarded-For'):
|
||||
frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip()
|
||||
|
||||
|
|
@ -35,37 +62,21 @@ class HTTPRequest:
|
|||
else:
|
||||
frappe.local.request_ip = '127.0.0.1'
|
||||
|
||||
# language
|
||||
self.set_lang()
|
||||
|
||||
# load cookies
|
||||
def set_cookies(self):
|
||||
frappe.local.cookie_manager = CookieManager()
|
||||
|
||||
# set db
|
||||
self.connect()
|
||||
|
||||
# login
|
||||
def set_session(self):
|
||||
frappe.local.login_manager = LoginManager()
|
||||
|
||||
if frappe.form_dict._lang:
|
||||
lang = get_lang_code(frappe.form_dict._lang)
|
||||
if lang:
|
||||
frappe.local.lang = lang
|
||||
|
||||
self.validate_csrf_token()
|
||||
|
||||
# write out latest cookies
|
||||
frappe.local.cookie_manager.init_cookies()
|
||||
|
||||
# check status
|
||||
check_session_stopped()
|
||||
|
||||
def validate_csrf_token(self):
|
||||
if frappe.local.request and frappe.local.request.method in ("POST", "PUT", "DELETE"):
|
||||
if not frappe.local.session: return
|
||||
if not frappe.local.session.data.csrf_token \
|
||||
or frappe.local.session.data.device=="mobile" \
|
||||
or frappe.conf.get('ignore_csrf', None):
|
||||
if not frappe.local.session:
|
||||
return
|
||||
if (
|
||||
not frappe.local.session.data.csrf_token
|
||||
or frappe.local.session.data.device == "mobile"
|
||||
or frappe.conf.get('ignore_csrf', None)
|
||||
):
|
||||
# not via boot
|
||||
return
|
||||
|
||||
|
|
@ -79,17 +90,18 @@ class HTTPRequest:
|
|||
frappe.throw(_("Invalid Request"), frappe.CSRFTokenError)
|
||||
|
||||
def set_lang(self):
|
||||
from frappe.translate import guess_language
|
||||
frappe.local.lang = guess_language()
|
||||
frappe.local.lang = get_language()
|
||||
|
||||
def get_db_name(self):
|
||||
"""get database name from conf"""
|
||||
return conf.db_name
|
||||
|
||||
def connect(self, ac_name = None):
|
||||
def connect(self):
|
||||
"""connect to db, from ac_name or db_name"""
|
||||
frappe.local.db = frappe.database.get_db(user = self.get_db_name(), \
|
||||
password = getattr(conf, 'db_password', ''))
|
||||
frappe.local.db = frappe.database.get_db(
|
||||
user=self.get_db_name(),
|
||||
password=getattr(conf, 'db_password', '')
|
||||
)
|
||||
|
||||
class LoginManager:
|
||||
def __init__(self):
|
||||
|
|
@ -116,7 +128,6 @@ class LoginManager:
|
|||
self.make_session()
|
||||
self.set_user_info()
|
||||
|
||||
@frappe.whitelist()
|
||||
def login(self):
|
||||
# clear cache
|
||||
frappe.clear_cache(user = frappe.form_dict.get('usr'))
|
||||
|
|
@ -143,7 +154,7 @@ class LoginManager:
|
|||
self.setup_boot_cache()
|
||||
self.set_user_info()
|
||||
|
||||
def get_user_info(self, resume=False):
|
||||
def get_user_info(self):
|
||||
self.info = frappe.db.get_value("User", self.user,
|
||||
["user_type", "first_name", "last_name", "user_image"], as_dict=1)
|
||||
|
||||
|
|
@ -181,11 +192,13 @@ class LoginManager:
|
|||
frappe.local.response["redirect_to"] = redirect_to
|
||||
frappe.cache().hdel('redirect_after_login', self.user)
|
||||
|
||||
|
||||
frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
|
||||
frappe.local.cookie_manager.set_cookie("user_id", self.user)
|
||||
frappe.local.cookie_manager.set_cookie("user_image", self.info.user_image or "")
|
||||
|
||||
def clear_preferred_language(self):
|
||||
frappe.local.cookie_manager.delete_cookie("preferred_language")
|
||||
|
||||
def make_session(self, resume=False):
|
||||
# start session
|
||||
frappe.local.session_obj = Session(user=self.user, resume=resume,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Assign Condition",
|
||||
"options": "PythonExpression",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -82,7 +83,8 @@
|
|||
"description": "Simple Python Expression, Example: Status in (\"Closed\", \"Cancelled\")",
|
||||
"fieldname": "unassign_condition",
|
||||
"fieldtype": "Code",
|
||||
"label": "Unassign Condition"
|
||||
"label": "Unassign Condition",
|
||||
"options": "PythonExpression"
|
||||
},
|
||||
{
|
||||
"fieldname": "assign_to_users_section",
|
||||
|
|
@ -120,7 +122,8 @@
|
|||
"description": "Simple Python Expression, Example: Status in (\"Invalid\")",
|
||||
"fieldname": "close_condition",
|
||||
"fieldtype": "Code",
|
||||
"label": "Close Condition"
|
||||
"label": "Close Condition",
|
||||
"options": "PythonExpression"
|
||||
},
|
||||
{
|
||||
"fieldname": "sb",
|
||||
|
|
@ -151,7 +154,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-20 14:47:20.662954",
|
||||
"modified": "2021-07-16 22:51:35.505575",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Assignment Rule",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import unittest
|
||||
from frappe.utils import random_string
|
||||
|
|
@ -76,7 +76,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
# clear 5 assignments for first user
|
||||
# can't do a limit in "delete" since postgres does not support it
|
||||
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', owner = 'test@example.com'), limit=5):
|
||||
frappe.db.sql("delete from tabToDo where name = %s", d.name)
|
||||
frappe.db.delete("ToDo", {"name": d.name})
|
||||
|
||||
# add 5 more assignments
|
||||
for i in range(5):
|
||||
|
|
@ -177,7 +177,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
), 'owner'), 'test@example.com')
|
||||
|
||||
def check_assignment_rule_scheduling(self):
|
||||
frappe.db.sql("DELETE FROM `tabAssignment Rule`")
|
||||
frappe.db.delete("Assignment Rule")
|
||||
|
||||
days_1 = [dict(day = 'Sunday'), dict(day = 'Monday'), dict(day = 'Tuesday')]
|
||||
|
||||
|
|
@ -204,7 +204,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
), 'owner'), ['test3@example.com'])
|
||||
|
||||
def test_assignment_rule_condition(self):
|
||||
frappe.db.sql("DELETE FROM `tabAssignment Rule`")
|
||||
frappe.db.delete("Assignment Rule")
|
||||
|
||||
# Add expiry_date custom field
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
|
@ -253,7 +253,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
assignment_rule.delete()
|
||||
|
||||
def clear_assignments():
|
||||
frappe.db.sql("delete from tabToDo where reference_type = 'Note'")
|
||||
frappe.db.delete("ToDo", {"reference_type": "Note"})
|
||||
|
||||
def get_assignment_rule(days, assign=None):
|
||||
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
refresh: function(frm) {
|
||||
// auto repeat message
|
||||
if (frm.is_new()) {
|
||||
let customize_form_link = `<a href="/app/customize form">${__('Customize Form')}</a>`;
|
||||
let customize_form_link = `<a href="/app/customize-form">${__('Customize Form')}</a>`;
|
||||
frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link]));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
# License: MIT. See LICENSE
|
||||
#import frappe
|
||||
import unittest
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import frappe.cache_manager
|
||||
import unittest
|
||||
|
||||
class TestMilestoneTracker(unittest.TestCase):
|
||||
def test_milestone(self):
|
||||
frappe.db.sql('delete from `tabMilestone Tracker`')
|
||||
frappe.db.delete("Milestone Tracker")
|
||||
|
||||
frappe.cache().delete_key('milestone_tracker_map')
|
||||
|
||||
|
|
@ -44,5 +44,5 @@ class TestMilestoneTracker(unittest.TestCase):
|
|||
self.assertEqual(milestones[0].value, 'Closed')
|
||||
|
||||
# cleanup
|
||||
frappe.db.sql('delete from tabMilestone')
|
||||
frappe.db.delete("Milestone")
|
||||
milestone_tracker.delete()
|
||||
|
|
@ -1,22 +1,20 @@
|
|||
{
|
||||
"category": "Administration",
|
||||
"charts": [],
|
||||
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"ToDo\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Note\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"File\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Assignment Rule\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Auto Repeat\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Email\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Automation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Event Streaming\", \"col\": 4}}]",
|
||||
"creation": "2020-03-02 14:53:24.980279",
|
||||
"developer_mode_only": 0,
|
||||
"disable_user_customization": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"extends_another_page": 0,
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "tool",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Tools",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Tools",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
|
|
@ -25,6 +23,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "To Do",
|
||||
"link_count": 0,
|
||||
"link_to": "ToDo",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
|
|
@ -35,6 +34,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Calendar",
|
||||
"link_count": 0,
|
||||
"link_to": "Event",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
|
|
@ -45,6 +45,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Note",
|
||||
"link_count": 0,
|
||||
"link_to": "Note",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
|
|
@ -55,6 +56,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Files",
|
||||
"link_count": 0,
|
||||
"link_to": "File",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -65,6 +67,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Activity",
|
||||
"link_count": 0,
|
||||
"link_to": "activity",
|
||||
"link_type": "Page",
|
||||
"onboard": 0,
|
||||
|
|
@ -74,6 +77,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
|
|
@ -82,6 +86,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Newsletter",
|
||||
"link_count": 0,
|
||||
"link_to": "Newsletter",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
|
|
@ -92,6 +97,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Email Group",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -101,6 +107,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Automation",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
|
|
@ -109,6 +116,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Assignment Rule",
|
||||
"link_count": 0,
|
||||
"link_to": "Assignment Rule",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -119,6 +127,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Milestone",
|
||||
"link_count": 0,
|
||||
"link_to": "Milestone",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -129,6 +138,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Auto Repeat",
|
||||
"link_count": 0,
|
||||
"link_to": "Auto Repeat",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -138,6 +148,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Streaming",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
|
|
@ -146,6 +157,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Producer",
|
||||
"link_count": 0,
|
||||
"link_to": "Event Producer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -156,6 +168,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Consumer",
|
||||
"link_count": 0,
|
||||
"link_to": "Event Consumer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -166,6 +179,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Update Log",
|
||||
"link_count": 0,
|
||||
"link_to": "Event Update Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -176,6 +190,7 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Sync Log",
|
||||
"link_count": 0,
|
||||
"link_to": "Event Sync Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
|
|
@ -186,19 +201,23 @@
|
|||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Document Type Mapping",
|
||||
"link_count": 0,
|
||||
"link_to": "Document Type Mapping",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2020-12-01 13:38:39.950350",
|
||||
"modified": "2021-08-05 12:16:02.839181",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Tools",
|
||||
"owner": "Administrator",
|
||||
"pin_to_bottom": 0,
|
||||
"pin_to_top": 0,
|
||||
"parent_page": "",
|
||||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 26,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "ToDo",
|
||||
|
|
@ -225,5 +244,6 @@
|
|||
"link_to": "Auto Repeat",
|
||||
"type": "DocType"
|
||||
}
|
||||
]
|
||||
],
|
||||
"title": "Tools"
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
# License: MIT. See LICENSE
|
||||
"""
|
||||
bootstrap client session
|
||||
"""
|
||||
|
|
@ -107,8 +107,8 @@ def load_conf_settings(bootinfo):
|
|||
if key in conf: bootinfo[key] = conf.get(key)
|
||||
|
||||
def load_desktop_data(bootinfo):
|
||||
from frappe.desk.desktop import get_desk_sidebar_items
|
||||
bootinfo.allowed_workspaces = get_desk_sidebar_items()
|
||||
from frappe.desk.desktop import get_wspace_sidebar_items
|
||||
bootinfo.allowed_workspaces = get_wspace_sidebar_items().get('pages')
|
||||
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
|
|
|
|||
146
frappe/build.py
146
frappe/build.py
|
|
@ -1,10 +1,11 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from subprocess import getoutput
|
||||
from io import StringIO
|
||||
from tempfile import mkdtemp, mktemp
|
||||
from distutils.spawn import find_executable
|
||||
|
|
@ -15,8 +16,9 @@ from frappe.utils.minify import JavascriptMinify
|
|||
import click
|
||||
import psutil
|
||||
from urllib.parse import urlparse
|
||||
from simple_chalk import green
|
||||
from semantic_version import Version
|
||||
from requests import head
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
|
||||
timestamps = {}
|
||||
|
|
@ -24,6 +26,12 @@ app_paths = None
|
|||
sites_path = os.path.abspath(os.getcwd())
|
||||
|
||||
|
||||
class AssetsNotDownloadedError(Exception):
|
||||
pass
|
||||
|
||||
class AssetsDontExistError(HTTPError):
|
||||
pass
|
||||
|
||||
def download_file(url, prefix):
|
||||
from requests import get
|
||||
|
||||
|
|
@ -70,81 +78,94 @@ def build_missing_files():
|
|||
bundle(build_mode, apps="frappe")
|
||||
|
||||
|
||||
def get_assets_link(frappe_head):
|
||||
from subprocess import getoutput
|
||||
from requests import head
|
||||
|
||||
def get_assets_link(frappe_head) -> str:
|
||||
tag = getoutput(
|
||||
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
|
||||
r" refs/tags/,,' -e 's/\^{}//'"
|
||||
% frappe_head
|
||||
)
|
||||
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
|
||||
r" refs/tags/,,' -e 's/\^{}//'"
|
||||
% frappe_head
|
||||
)
|
||||
|
||||
if tag:
|
||||
# if tag exists, download assets from github release
|
||||
url = "https://github.com/frappe/frappe/releases/download/{0}/assets.tar.gz".format(tag)
|
||||
url = f"https://github.com/frappe/frappe/releases/download/{tag}/assets.tar.gz"
|
||||
else:
|
||||
url = "http://assets.frappeframework.com/{0}.tar.gz".format(frappe_head)
|
||||
url = f"http://assets.frappeframework.com/{frappe_head}.tar.gz"
|
||||
|
||||
if not head(url):
|
||||
raise ValueError("URL {0} doesn't exist".format(url))
|
||||
reference = f"Release {tag}" if tag else f"Commit {frappe_head}"
|
||||
raise AssetsDontExistError(f"Assets for {reference} don't exist")
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def fetch_assets(url, frappe_head):
|
||||
click.secho("Retrieving assets...", fg="yellow")
|
||||
|
||||
prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head)
|
||||
assets_archive = download_file(url, prefix)
|
||||
|
||||
if not assets_archive:
|
||||
raise AssetsNotDownloadedError(f"Assets could not be retrived from {url}")
|
||||
|
||||
click.echo(click.style("✔", fg="green") + f" Downloaded Frappe assets from {url}")
|
||||
|
||||
return assets_archive
|
||||
|
||||
|
||||
def setup_assets(assets_archive):
|
||||
import tarfile
|
||||
directories_created = set()
|
||||
|
||||
click.secho("\nExtracting assets...\n", fg="yellow")
|
||||
with tarfile.open(assets_archive) as tar:
|
||||
for file in tar:
|
||||
if not file.isdir():
|
||||
dest = "." + file.name.replace("./frappe-bench/sites", "")
|
||||
asset_directory = os.path.dirname(dest)
|
||||
show = dest.replace("./assets/", "")
|
||||
|
||||
if asset_directory not in directories_created:
|
||||
if not os.path.exists(asset_directory):
|
||||
os.makedirs(asset_directory, exist_ok=True)
|
||||
directories_created.add(asset_directory)
|
||||
|
||||
tar.makefile(file, dest)
|
||||
click.echo(click.style("✔", fg="green") + f" Restored {show}")
|
||||
|
||||
return directories_created
|
||||
|
||||
|
||||
def download_frappe_assets(verbose=True):
|
||||
"""Downloads and sets up Frappe assets if they exist based on the current
|
||||
commit HEAD.
|
||||
Returns True if correctly setup else returns False.
|
||||
"""
|
||||
from subprocess import getoutput
|
||||
|
||||
assets_setup = False
|
||||
frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD")
|
||||
|
||||
if frappe_head:
|
||||
if not frappe_head:
|
||||
return False
|
||||
|
||||
try:
|
||||
url = get_assets_link(frappe_head)
|
||||
assets_archive = fetch_assets(url, frappe_head)
|
||||
setup_assets(assets_archive)
|
||||
build_missing_files()
|
||||
return True
|
||||
|
||||
except AssetsDontExistError as e:
|
||||
click.secho(str(e), fg="yellow")
|
||||
|
||||
except Exception as e:
|
||||
# TODO: log traceback in bench.log
|
||||
click.secho(str(e), fg="red")
|
||||
|
||||
finally:
|
||||
try:
|
||||
url = get_assets_link(frappe_head)
|
||||
click.secho("Retrieving assets...", fg="yellow")
|
||||
prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head)
|
||||
assets_archive = download_file(url, prefix)
|
||||
print("\n{0} Downloaded Frappe assets from {1}".format(green('✔'), url))
|
||||
|
||||
if assets_archive:
|
||||
import tarfile
|
||||
directories_created = set()
|
||||
|
||||
click.secho("\nExtracting assets...\n", fg="yellow")
|
||||
with tarfile.open(assets_archive) as tar:
|
||||
for file in tar:
|
||||
if not file.isdir():
|
||||
dest = "." + file.name.replace("./frappe-bench/sites", "")
|
||||
asset_directory = os.path.dirname(dest)
|
||||
show = dest.replace("./assets/", "")
|
||||
|
||||
if asset_directory not in directories_created:
|
||||
if not os.path.exists(asset_directory):
|
||||
os.makedirs(asset_directory, exist_ok=True)
|
||||
directories_created.add(asset_directory)
|
||||
|
||||
tar.makefile(file, dest)
|
||||
print("{0} Restored {1}".format(green('✔'), show))
|
||||
|
||||
build_missing_files()
|
||||
return True
|
||||
else:
|
||||
raise
|
||||
shutil.rmtree(os.path.dirname(assets_archive))
|
||||
except Exception:
|
||||
# TODO: log traceback in bench.log
|
||||
click.secho("An Error occurred while downloading assets...", fg="red")
|
||||
assets_setup = False
|
||||
finally:
|
||||
try:
|
||||
shutil.rmtree(os.path.dirname(assets_archive))
|
||||
except Exception:
|
||||
pass
|
||||
pass
|
||||
|
||||
return assets_setup
|
||||
return False
|
||||
|
||||
|
||||
def symlink(target, link_name, overwrite=False):
|
||||
|
|
@ -224,7 +245,7 @@ def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, ver
|
|||
|
||||
check_node_executable()
|
||||
frappe_app_path = frappe.get_app_path("frappe", "..")
|
||||
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
|
||||
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env(), raise_err=True)
|
||||
|
||||
|
||||
def watch(apps=None):
|
||||
|
|
@ -235,6 +256,13 @@ def watch(apps=None):
|
|||
if apps:
|
||||
command += " --apps {apps}".format(apps=apps)
|
||||
|
||||
live_reload = frappe.utils.cint(
|
||||
os.environ.get("LIVE_RELOAD", frappe.conf.live_reload)
|
||||
)
|
||||
|
||||
if live_reload:
|
||||
command += " --live-reload"
|
||||
|
||||
check_node_executable()
|
||||
frappe_app_path = frappe.get_app_path("frappe", "..")
|
||||
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
|
||||
|
|
@ -350,7 +378,7 @@ def make_asset_dirs(hard_link=False):
|
|||
except Exception:
|
||||
print(fail_message, end="\r")
|
||||
|
||||
print(unstrip(f"{green('✔')} Application Assets Linked") + "\n")
|
||||
click.echo(unstrip(click.style("✔", fg="green") + " Application Assets Linked") + "\n")
|
||||
|
||||
|
||||
def link_assets_dir(source, target, hard_link=False):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe, json
|
||||
from frappe.model.document import Document
|
||||
|
|
@ -53,7 +53,7 @@ def clear_domain_cache(user=None):
|
|||
cache.delete_value(domain_cache_keys)
|
||||
|
||||
def clear_global_cache():
|
||||
from frappe.website.render import clear_cache as clear_website_cache
|
||||
from frappe.website.utils import clear_website_cache
|
||||
|
||||
clear_doctype_cache()
|
||||
clear_website_cache()
|
||||
|
|
@ -141,18 +141,13 @@ def build_table_count_cache():
|
|||
return
|
||||
|
||||
_cache = frappe.cache()
|
||||
data = frappe.db.multisql({
|
||||
"mariadb": """
|
||||
SELECT table_name AS name,
|
||||
table_rows AS count
|
||||
FROM information_schema.tables""",
|
||||
"postgres": """
|
||||
SELECT "relname" AS name,
|
||||
"n_tup_ins" AS count
|
||||
FROM "pg_stat_all_tables"
|
||||
"""
|
||||
}, as_dict=1)
|
||||
table_name = frappe.qb.Field("table_name").as_("name")
|
||||
table_rows = frappe.qb.Field("table_rows").as_("count")
|
||||
information_schema = frappe.qb.Schema("information_schema")
|
||||
|
||||
data = (
|
||||
frappe.qb.from_(information_schema.tables).select(table_name, table_rows)
|
||||
).run(as_dict=True)
|
||||
counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data}
|
||||
_cache.set_value("information_schema:counts", counts)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
session = frappe.session
|
||||
|
||||
def authenticate(user, raise_err = True):
|
||||
if session.user == 'Guest':
|
||||
if not frappe.db.exists('Chat Token', user):
|
||||
if raise_err:
|
||||
frappe.throw(_("Sorry, you're not authorized."))
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
if user != session.user:
|
||||
if raise_err:
|
||||
frappe.throw(_("Sorry, you're not authorized."))
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue