Merge branch 'develop' into refactor-file
This commit is contained in:
commit
44dba28159
76 changed files with 709 additions and 278 deletions
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
|
|
@ -17,7 +17,7 @@ if [ "$TYPE" == "server" ]; then
|
|||
fi
|
||||
|
||||
if [ "$DB" == "mariadb" ];then
|
||||
sudo apt update && sudo apt install mariadb-client-10.3
|
||||
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'";
|
||||
|
||||
|
|
|
|||
6
.github/helper/install_dependencies.sh
vendored
6
.github/helper/install_dependencies.sh
vendored
|
|
@ -16,8 +16,4 @@ sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
|
|||
sudo chmod o+x /usr/local/bin/wkhtmltopdf
|
||||
|
||||
# install cups
|
||||
sudo apt-get install libcups2-dev
|
||||
|
||||
# install redis
|
||||
sudo apt-get install redis-server
|
||||
|
||||
sudo apt update && sudo apt install libcups2-dev redis-server
|
||||
|
|
|
|||
5
.github/workflows/docker-release.yml
vendored
5
.github/workflows/docker-release.yml
vendored
|
|
@ -2,8 +2,13 @@ name: 'Trigger Docker build on release'
|
|||
on:
|
||||
release:
|
||||
types: [released]
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
curl:
|
||||
permissions:
|
||||
contents: none
|
||||
name: 'Trigger Docker build on release'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
|
|
|
|||
3
.github/workflows/docs-checker.yml
vendored
3
.github/workflows/docs-checker.yml
vendored
|
|
@ -3,6 +3,9 @@ on:
|
|||
pull_request:
|
||||
types: [ opened, synchronize, reopened, edited ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
docs-required:
|
||||
name: 'Documentation Required'
|
||||
|
|
|
|||
3
.github/workflows/patch-mariadb-tests.yml
vendored
3
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -7,6 +7,9 @@ concurrency:
|
|||
group: patch-mariadb-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
|
|
@ -2,7 +2,10 @@ name: Generate Semantic Release
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- [version-13, version-14-beta]
|
||||
- version-14-beta
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
|
|
|
|||
3
.github/workflows/server-mariadb-tests.yml
vendored
3
.github/workflows/server-mariadb-tests.yml
vendored
|
|
@ -11,6 +11,9 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
3
.github/workflows/server-postgres-tests.yml
vendored
3
.github/workflows/server-postgres-tests.yml
vendored
|
|
@ -10,6 +10,9 @@ concurrency:
|
|||
group: server-postgres-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
3
.github/workflows/ui-tests.yml
vendored
3
.github/workflows/ui-tests.yml
vendored
|
|
@ -10,6 +10,9 @@ concurrency:
|
|||
group: ui-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
{
|
||||
"branches": ["version-13"],
|
||||
"branches": ["develop", {"name": "version-14-beta", "channel": "beta", "prerelease": true}],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular",
|
||||
"releaseRules": [
|
||||
{"breaking": true, "release": false}
|
||||
]
|
||||
"preset": "angular"
|
||||
},
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1032,7 +1032,7 @@ def get_cached_doc(*args, **kwargs):
|
|||
return doc
|
||||
|
||||
if key := can_cache_doc(args):
|
||||
# local cache
|
||||
# local cache - has "ready" `Document` objects
|
||||
if doc := local.document_cache.get(key):
|
||||
return _respond(doc)
|
||||
|
||||
|
|
@ -1040,9 +1040,16 @@ def get_cached_doc(*args, **kwargs):
|
|||
if doc := cache().hget("document_cache", key):
|
||||
return _respond(doc, True)
|
||||
|
||||
# database
|
||||
# Not found in local/redis, fetch from DB and store in cache
|
||||
doc = get_doc(*args, **kwargs)
|
||||
|
||||
# Store in redis cache
|
||||
key = get_document_cache_key(doc.doctype, doc.name)
|
||||
|
||||
local.document_cache[key] = doc
|
||||
# Avoid setting in local.cache since there's separate cache
|
||||
cache().hset("document_cache", key, doc.as_dict(), cache_locally=False)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
|
|
@ -1113,10 +1120,12 @@ def get_doc(*args, **kwargs):
|
|||
|
||||
doc = frappe.model.document.get_doc(*args, **kwargs)
|
||||
|
||||
# set in cache
|
||||
# Replace cache
|
||||
if key := can_cache_doc(args):
|
||||
local.document_cache[key] = doc
|
||||
cache().hset("document_cache", key, doc.as_dict())
|
||||
if key in local.document_cache:
|
||||
local.document_cache[key] = doc
|
||||
if cache().hexists("document_cache", key):
|
||||
cache().hset("document_cache", key, doc.as_dict())
|
||||
|
||||
return doc
|
||||
|
||||
|
|
|
|||
|
|
@ -298,8 +298,6 @@ def apply(doc=None, method=None, doctype=None, name=None):
|
|||
if reopened:
|
||||
break
|
||||
|
||||
# print(f"Rule:{assignment_rule}\nDoc: {doc}\nReOpened: {reopened}")
|
||||
|
||||
assignment_rule.close_assignments(doc)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -144,10 +144,6 @@ def restore(
|
|||
)
|
||||
from frappe.utils.backups import Backup
|
||||
|
||||
if not os.path.exists(sql_file_path):
|
||||
print("Invalid path", sql_file_path)
|
||||
sys.exit(1)
|
||||
|
||||
_backup = Backup(sql_file_path)
|
||||
|
||||
site = get_site(context)
|
||||
|
|
|
|||
|
|
@ -22,14 +22,6 @@ if TYPE_CHECKING:
|
|||
from frappe.core.doctype.communication.communication import Communication
|
||||
|
||||
|
||||
OUTGOING_EMAIL_ACCOUNT_MISSING = _(
|
||||
"""
|
||||
Unable to send mail because of a missing email account.
|
||||
Please setup default Email Account from Setup > Email > Email Account
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make(
|
||||
doctype=None,
|
||||
|
|
@ -170,7 +162,12 @@ def _make(
|
|||
|
||||
if cint(send_email):
|
||||
if not comm.get_outgoing_email_account():
|
||||
frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError)
|
||||
frappe.throw(
|
||||
_(
|
||||
"Unable to send mail because of a missing email account. Please setup default Email Account from Setup > Email > Email Account"
|
||||
),
|
||||
exc=frappe.OutgoingEmailError,
|
||||
)
|
||||
|
||||
comm.send_email(
|
||||
print_html=print_html,
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ class CommunicationEmailMixin:
|
|||
"doctype": self.reference_doctype,
|
||||
"name": self.reference_name,
|
||||
"print_format": print_format,
|
||||
"key": get_parent_doc(self).get_signature(),
|
||||
"key": get_parent_doc(self).get_document_share_key(),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
0
frappe/core/doctype/document_share_key/__init__.py
Normal file
0
frappe/core/doctype/document_share_key/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2021, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Document Share Key', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2022-01-14 13:40:49.487646",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_doctype",
|
||||
"reference_docname",
|
||||
"key",
|
||||
"expires_on"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_docname",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Reference Document Name",
|
||||
"options": "reference_doctype",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "key",
|
||||
"fieldtype": "Data",
|
||||
"label": "Key",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "expires_on",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Expires On",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-14 13:57:28.050678",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Document Share Key",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
20
frappe/core/doctype/document_share_key/document_share_key.py
Normal file
20
frappe/core/doctype/document_share_key/document_share_key.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from random import randrange
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DocumentShareKey(Document):
|
||||
def before_insert(self):
|
||||
self.key = frappe.generate_hash(length=randrange(25, 35))
|
||||
if not self.expires_on and not self.flags.no_expiry:
|
||||
self.expires_on = frappe.utils.add_days(
|
||||
None, days=frappe.get_system_settings("document_share_key_expiry") or 90
|
||||
)
|
||||
|
||||
|
||||
def is_expired(expires_on):
|
||||
return expires_on and expires_on < frappe.utils.getdate()
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestDocumentShareKey(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -55,11 +55,11 @@ class PackageImport(Document):
|
|||
for module in os.listdir(package_path):
|
||||
module_path = os.path.join(package_path, module)
|
||||
if os.path.isdir(module_path):
|
||||
get_doc_files(files, module_path)
|
||||
files = get_doc_files(files, module_path)
|
||||
|
||||
# import files
|
||||
for file in files:
|
||||
import_file_by_path(file, force=self.force, ignore_version=True, for_sync=True)
|
||||
import_file_by_path(file, force=self.force, ignore_version=True)
|
||||
log.append("Imported {}".format(file))
|
||||
|
||||
self.log = "\n".join(log)
|
||||
|
|
|
|||
|
|
@ -185,9 +185,12 @@ def insert_single_event(frequency: str, event: str, cron_format: str = None):
|
|||
if not frappe.db.exists(
|
||||
"Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr}
|
||||
):
|
||||
savepoint = "scheduled_job_type_creation"
|
||||
try:
|
||||
frappe.db.savepoint(savepoint)
|
||||
doc.insert()
|
||||
except frappe.DuplicateEntryError:
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
doc.delete()
|
||||
doc.insert()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2014-04-17 16:53:52.640856",
|
||||
"creation": "2022-01-06 03:18:16.326761",
|
||||
"doctype": "DocType",
|
||||
"document_type": "System",
|
||||
"engine": "InnoDB",
|
||||
|
|
@ -34,12 +34,14 @@
|
|||
"security",
|
||||
"session_expiry",
|
||||
"session_expiry_mobile",
|
||||
"document_share_key_expiry",
|
||||
"column_break_13",
|
||||
"deny_multiple_sessions",
|
||||
"allow_login_using_mobile_number",
|
||||
"allow_login_using_user_name",
|
||||
"allow_error_traceback",
|
||||
"strip_exif_metadata_from_uploaded_images",
|
||||
"allow_older_web_view_links",
|
||||
"password_settings",
|
||||
"logout_on_password_reset",
|
||||
"force_user_to_reset_password",
|
||||
|
|
@ -482,6 +484,19 @@
|
|||
"options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday"
|
||||
},
|
||||
{
|
||||
"default": "30",
|
||||
"description": "Number of days after which the document Web View link shared on email will be expired",
|
||||
"fieldname": "document_share_key_expiry",
|
||||
"fieldtype": "Int",
|
||||
"label": "Document Share Key Expiry (in Days)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_older_web_view_links",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Older Web View Links (Insecure)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_64",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ frappe.ui.form.on('User', {
|
|||
onload: function(frm) {
|
||||
frm.can_edit_roles = has_access_to_edit_user();
|
||||
|
||||
if (frm.is_new() && frm.roles_editor) {
|
||||
frm.roles_editor.reset();
|
||||
}
|
||||
|
||||
if (frm.can_edit_roles && !frm.is_new() && in_list(['System User', 'Website User'], frm.doc.user_type)) {
|
||||
if (!frm.roles_editor) {
|
||||
const role_area = $('<div class="role-editor">')
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ def create_custom_field(doctype, df, ignore_validate=False, is_system_generated=
|
|||
custom_field.update(df)
|
||||
custom_field.flags.ignore_validate = ignore_validate
|
||||
custom_field.insert()
|
||||
return custom_field
|
||||
|
||||
|
||||
def create_custom_fields(custom_fields, ignore_validate=False, update=True):
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ from frappe.query_builder.functions import Count
|
|||
from frappe.query_builder.utils import DocType
|
||||
from frappe.utils import cast, get_datetime, get_table_name, getdate, now, sbool
|
||||
|
||||
from .query import Query
|
||||
|
||||
|
||||
class Database(object):
|
||||
"""
|
||||
|
|
@ -65,7 +63,15 @@ class Database(object):
|
|||
|
||||
self.password = password or frappe.conf.db_password
|
||||
self.value_cache = {}
|
||||
self.query = Query()
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
if not hasattr(self, "_query"):
|
||||
from .query import Query
|
||||
|
||||
self._query = Query()
|
||||
del Query
|
||||
return self._query
|
||||
|
||||
def setup_type_map(self):
|
||||
pass
|
||||
|
|
@ -195,6 +201,9 @@ class Database(object):
|
|||
|
||||
elif frappe.conf.db_type == "postgres":
|
||||
# TODO: added temporarily
|
||||
import traceback
|
||||
|
||||
traceback.print_stack()
|
||||
print(e)
|
||||
raise
|
||||
|
||||
|
|
@ -914,6 +923,9 @@ class Database(object):
|
|||
frappe.call(method[0], *(method[1] or []), **(method[2] or {}))
|
||||
|
||||
self.sql("commit")
|
||||
if frappe.conf.db_type == "postgres":
|
||||
# Postgres requires explicitly starting new transaction
|
||||
self.begin()
|
||||
|
||||
frappe.local.rollback_observers = []
|
||||
self.flush_realtime_log()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import frappe
|
|||
def setup_database(force, source_sql=None, verbose=False):
|
||||
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
|
||||
root_conn.commit()
|
||||
root_conn.sql("end")
|
||||
root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(frappe.conf.db_name))
|
||||
root_conn.sql("DROP USER IF EXISTS {0}".format(frappe.conf.db_name))
|
||||
root_conn.sql("CREATE DATABASE `{0}`".format(frappe.conf.db_name))
|
||||
|
|
|
|||
|
|
@ -268,14 +268,7 @@ class Query:
|
|||
return conditions
|
||||
if isinstance(filters, list):
|
||||
for f in filters:
|
||||
if not isinstance(f, (list, tuple)):
|
||||
_operator = self.OPERATOR_MAP[filters[1].casefold()]
|
||||
if not isinstance(filters[0], str):
|
||||
conditions = make_function(filters[0], filters[2])
|
||||
break
|
||||
conditions = conditions.where(_operator(Field(filters[0]), filters[2]))
|
||||
break
|
||||
else:
|
||||
if isinstance(f, (list, tuple)):
|
||||
_operator = self.OPERATOR_MAP[f[-2].casefold()]
|
||||
if len(f) == 4:
|
||||
table_object = self.get_table(f[0])
|
||||
|
|
@ -283,6 +276,15 @@ class Query:
|
|||
else:
|
||||
_field = Field(f[0])
|
||||
conditions = conditions.where(_operator(_field, f[-1]))
|
||||
elif isinstance(f, dict):
|
||||
conditions = self.dict_query(table, f, **kwargs)
|
||||
else:
|
||||
_operator = self.OPERATOR_MAP[filters[1].casefold()]
|
||||
if not isinstance(filters[0], str):
|
||||
conditions = make_function(filters[0], filters[2])
|
||||
break
|
||||
conditions = conditions.where(_operator(Field(filters[0]), filters[2]))
|
||||
break
|
||||
|
||||
return self.add_conditions(conditions, **kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ def toggle_notifications(user: str, enable: bool = False):
|
|||
try:
|
||||
settings = frappe.get_doc("Notification Settings", user)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.clear_last_message()
|
||||
return
|
||||
|
||||
if settings.enabled != enable:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from typing import Dict, List
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.query_builder.terms import SubQuery
|
||||
from frappe.query_builder.utils import DocType
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -24,37 +30,36 @@ def set_list_settings(doctype, values):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_group_by_count(doctype, current_filters, field):
|
||||
def get_group_by_count(doctype: str, current_filters: str, field: str) -> List[Dict]:
|
||||
current_filters = frappe.parse_json(current_filters)
|
||||
subquery_condition = ""
|
||||
|
||||
subquery = frappe.get_all(doctype, filters=current_filters, run=False)
|
||||
if field == "assigned_to":
|
||||
subquery_condition = " and `tabToDo`.reference_name in ({subquery})".format(subquery=subquery)
|
||||
return frappe.db.sql(
|
||||
"""select `tabToDo`.allocated_to as name, count(*) as count
|
||||
from
|
||||
`tabToDo`, `tabUser`
|
||||
where
|
||||
`tabToDo`.status!='Cancelled' and
|
||||
`tabToDo`.allocated_to = `tabUser`.name and
|
||||
`tabUser`.user_type = 'System User'
|
||||
{subquery_condition}
|
||||
group by
|
||||
`tabToDo`.allocated_to
|
||||
order by
|
||||
count desc
|
||||
limit 50""".format(
|
||||
subquery_condition=subquery_condition
|
||||
),
|
||||
as_dict=True,
|
||||
)
|
||||
else:
|
||||
return frappe.db.get_list(
|
||||
doctype,
|
||||
filters=current_filters,
|
||||
group_by="`tab{0}`.{1}".format(doctype, field),
|
||||
fields=["count(*) as count", "`{}` as name".format(field)],
|
||||
order_by="count desc",
|
||||
limit=50,
|
||||
ToDo = DocType("ToDo")
|
||||
User = DocType("User")
|
||||
count = Count("*").as_("count")
|
||||
filtered_records = frappe.db.query.build_conditions(doctype, current_filters).select("name")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(ToDo)
|
||||
.from_(User)
|
||||
.select(ToDo.allocated_to.as_("name"), count)
|
||||
.where(
|
||||
(ToDo.status != "Cancelled")
|
||||
& (ToDo.allocated_to == User.name)
|
||||
& (User.user_type == "System User")
|
||||
& (ToDo.reference_name.isin(SubQuery(filtered_records)))
|
||||
)
|
||||
.groupby(ToDo.allocated_to)
|
||||
.orderby(count, order=Order.desc)
|
||||
.limit(50)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
return frappe.get_list(
|
||||
doctype,
|
||||
filters=current_filters,
|
||||
group_by=f"`tab{doctype}`.{field}",
|
||||
fields=["count(*) as count", f"`{field}` as name"],
|
||||
order_by="count desc",
|
||||
limit=50,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -431,22 +431,13 @@ def make_records(records, debug=False):
|
|||
if doc.meta.get_field(parent_link_field) and not doc.get(parent_link_field):
|
||||
doc.flags.ignore_mandatory = True
|
||||
|
||||
savepoint = "setup_fixtures_creation"
|
||||
try:
|
||||
doc.insert(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
except frappe.DuplicateEntryError as e:
|
||||
# print("Failed to insert duplicate {0} {1}".format(doctype, doc.name))
|
||||
|
||||
# pass DuplicateEntryError and continue
|
||||
if e.args and e.args[0] == doc.doctype and e.args[1] == doc.name:
|
||||
# make sure DuplicateEntryError is for the exact same doc and not a related doc
|
||||
frappe.clear_messages()
|
||||
else:
|
||||
raise
|
||||
|
||||
frappe.db.savepoint(savepoint)
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.clear_last_message()
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
exception = record.get("__exception")
|
||||
if exception:
|
||||
config = _dict(exception)
|
||||
|
|
@ -461,3 +452,4 @@ def make_records(records, debug=False):
|
|||
def show_document_insert_error():
|
||||
print("Document Insert Error")
|
||||
print(frappe.get_traceback())
|
||||
frappe.log_error("Exception during Setup")
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ def get_names_for_mentions(search_term):
|
|||
|
||||
|
||||
def get_users_for_mentions():
|
||||
return frappe.get_list(
|
||||
return frappe.get_all(
|
||||
"User",
|
||||
fields=["name as id", "full_name as value"],
|
||||
filters={
|
||||
|
|
@ -361,7 +361,7 @@ def get_users_for_mentions():
|
|||
|
||||
|
||||
def get_user_groups():
|
||||
return frappe.get_list(
|
||||
return frappe.get_all(
|
||||
"User Group", fields=["name as id", "name as value"], update={"is_group": True}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,10 +23,6 @@ from frappe.utils.error import raise_error_on_no_output
|
|||
from frappe.utils.jinja import render_template
|
||||
from frappe.utils.user import get_system_managers
|
||||
|
||||
OUTGOING_EMAIL_ACCOUNT_MISSING = _(
|
||||
"Please setup default Email Account from Setup > Email > Email Account"
|
||||
)
|
||||
|
||||
|
||||
class SentEmailInInbox(Exception):
|
||||
pass
|
||||
|
|
@ -319,7 +315,7 @@ class EmailAccount(Document):
|
|||
@classmethod
|
||||
@raise_error_on_no_output(
|
||||
keep_quiet=lambda: not cint(frappe.get_system_settings("setup_complete")),
|
||||
error_message=OUTGOING_EMAIL_ACCOUNT_MISSING,
|
||||
error_message=_("Please setup default Email Account from Setup > Email > Email Account"),
|
||||
error_type=frappe.OutgoingEmailError,
|
||||
) # noqa
|
||||
@cache_email_account("outgoing_email_account")
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class TestNewsletterMixin:
|
|||
"doctype": doctype,
|
||||
**email_filters,
|
||||
}
|
||||
).insert()
|
||||
).insert(ignore_if_duplicate=True)
|
||||
except Exception:
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
frappe.db.update(doctype, email_filters, "unsubscribed", 0)
|
||||
|
|
|
|||
|
|
@ -1,24 +1,12 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import smtplib
|
||||
|
||||
import _socket
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, cstr
|
||||
|
||||
CONNECTION_FAILED = _("Could not connect to outgoing email server")
|
||||
AUTH_ERROR_TITLE = _("Invalid Credentials")
|
||||
AUTH_ERROR = _("Incorrect email or password. Please check your login credentials.")
|
||||
SOCKET_ERROR_TITLE = _("Incorrect Configuration")
|
||||
SOCKET_ERROR = _("Invalid Outgoing Mail Server or Port")
|
||||
SEND_MAIL_FAILED = _("Unable to send emails at this time")
|
||||
EMAIL_ACCOUNT_MISSING = _(
|
||||
"Email Account not setup. Please create a new Email Account from Setup > Email > Email Account"
|
||||
)
|
||||
|
||||
|
||||
class InvalidEmailCredentials(frappe.ValidationError):
|
||||
pass
|
||||
|
|
@ -65,7 +53,12 @@ class SMTPServer:
|
|||
self._session = None
|
||||
|
||||
if not self.server:
|
||||
frappe.msgprint(EMAIL_ACCOUNT_MISSING, raise_exception=frappe.OutgoingEmailError)
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Email Account not setup. Please create a new Email Account from Setup > Email > Email Account"
|
||||
),
|
||||
raise_exception=frappe.OutgoingEmailError,
|
||||
)
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
|
|
@ -93,7 +86,9 @@ class SMTPServer:
|
|||
try:
|
||||
_session = SMTP(self.server, self.port)
|
||||
if not _session:
|
||||
frappe.msgprint(CONNECTION_FAILED, raise_exception=frappe.OutgoingEmailError)
|
||||
frappe.msgprint(
|
||||
_("Could not connect to outgoing email server"), raise_exception=frappe.OutgoingEmailError
|
||||
)
|
||||
|
||||
self.secure_session(_session)
|
||||
if self.login and self.password:
|
||||
|
|
@ -106,16 +101,12 @@ class SMTPServer:
|
|||
self._session = _session
|
||||
return self._session
|
||||
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
self.throw_invalid_credentials_exception()
|
||||
|
||||
except _socket.error as e:
|
||||
except OSError:
|
||||
# Invalid mail server -- due to refusing connection
|
||||
frappe.throw(SOCKET_ERROR, title=SOCKET_ERROR_TITLE)
|
||||
|
||||
except smtplib.SMTPException:
|
||||
frappe.msgprint(SEND_MAIL_FAILED)
|
||||
raise
|
||||
frappe.throw(_("Invalid Outgoing Mail Server or Port"), title=_("Incorrect Configuration"))
|
||||
|
||||
def is_session_active(self):
|
||||
if self._session:
|
||||
|
|
@ -130,4 +121,8 @@ class SMTPServer:
|
|||
|
||||
@classmethod
|
||||
def throw_invalid_credentials_exception(cls):
|
||||
frappe.throw(AUTH_ERROR, title=AUTH_ERROR_TITLE, exc=InvalidEmailCredentials)
|
||||
frappe.throw(
|
||||
_("Incorrect email or password. Please check your login credentials."),
|
||||
title=_("Invalid Credentials"),
|
||||
exc=InvalidEmailCredentials,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -263,3 +263,15 @@ class ExecutableNotFound(FileNotFoundError):
|
|||
|
||||
class InvalidRemoteException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LinkExpired(ValidationError):
|
||||
http_status_code = 410
|
||||
title = "Link Expired"
|
||||
message = "The link has expired"
|
||||
|
||||
|
||||
class InvalidKeyError(ValidationError):
|
||||
http_status_code = 401
|
||||
title = "Invalid Key"
|
||||
message = "The document key is invalid"
|
||||
|
|
|
|||
|
|
@ -303,14 +303,8 @@ class DatabaseQuery(object):
|
|||
linked_field = frappe.get_meta(self.doctype).get_field(linked_fieldname)
|
||||
linked_doctype = linked_field.options
|
||||
if linked_field.fieldtype == "Link":
|
||||
self.link_tables.append(
|
||||
frappe._dict(
|
||||
doctype=linked_doctype, fieldname=linked_fieldname, table_name=f"`tab{linked_doctype}`"
|
||||
)
|
||||
)
|
||||
|
||||
field = field.replace(linked_fieldname, f"`tab{linked_doctype}`")
|
||||
field = field.replace(fieldname, f"`{fieldname}`")
|
||||
self.append_link_table(linked_doctype, linked_fieldname)
|
||||
field = f"`tab{linked_doctype}`.`{fieldname}`"
|
||||
if alias:
|
||||
field = f"{field} as {alias}"
|
||||
self.fields[self.fields.index(original_field)] = field
|
||||
|
|
@ -432,6 +426,19 @@ class DatabaseQuery(object):
|
|||
def append_table(self, table_name):
|
||||
self.tables.append(table_name)
|
||||
doctype = table_name[4:-1]
|
||||
self.check_read_permission(doctype)
|
||||
|
||||
def append_link_table(self, doctype, fieldname):
|
||||
for d in self.link_tables:
|
||||
if d.doctype == doctype and d.fieldname == fieldname:
|
||||
return
|
||||
|
||||
self.check_read_permission(doctype)
|
||||
self.link_tables.append(
|
||||
frappe._dict(doctype=doctype, fieldname=fieldname, table_name=f"`tab{doctype}`")
|
||||
)
|
||||
|
||||
def check_read_permission(self, doctype):
|
||||
ptype = "select" if frappe.only_has_select_perm(doctype) else "read"
|
||||
|
||||
if not self.flags.ignore_permissions and not frappe.has_permission(
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ doctypes_to_skip = (
|
|||
"Tag Link",
|
||||
"Notification Log",
|
||||
"Email Queue",
|
||||
"Document Share Key",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1383,6 +1383,30 @@ class Document(BaseDocument):
|
|||
"""Returns signature (hash) for private URL."""
|
||||
return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest()
|
||||
|
||||
def get_document_share_key(self, expires_on=None, no_expiry=False):
|
||||
if no_expiry:
|
||||
expires_on = None
|
||||
|
||||
existing_key = frappe.db.exists(
|
||||
"Document Share Key",
|
||||
{
|
||||
"reference_doctype": self.doctype,
|
||||
"reference_docname": self.name,
|
||||
"expires_on": expires_on,
|
||||
},
|
||||
)
|
||||
if existing_key:
|
||||
doc = frappe.get_doc("Document Share Key", existing_key)
|
||||
else:
|
||||
doc = frappe.new_doc("Document Share Key")
|
||||
doc.reference_doctype = self.doctype
|
||||
doc.reference_docname = self.name
|
||||
doc.expires_on = expires_on
|
||||
doc.flags.no_expiry = no_expiry
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
return doc.key
|
||||
|
||||
def get_liked_by(self):
|
||||
liked_by = getattr(self, "_liked_by", None)
|
||||
if liked_by:
|
||||
|
|
|
|||
|
|
@ -200,3 +200,4 @@ frappe.patches.v14_0.remove_db_aggregation
|
|||
frappe.patches.v14_0.update_color_names_in_kanban_board_column
|
||||
frappe.patches.v14_0.update_is_system_generated_flag
|
||||
frappe.patches.v14_0.update_auto_account_deletion_duration
|
||||
frappe.patches.v14_0.set_document_expiry_default
|
||||
|
|
|
|||
9
frappe/patches/v14_0/set_document_expiry_default.py
Normal file
9
frappe/patches/v14_0/set_document_expiry_default.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.set_value(
|
||||
"System Settings",
|
||||
"System Settings",
|
||||
{"document_share_key_expiry": 30, "allow_older_web_view_links": 1},
|
||||
)
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-up-line">
|
||||
<path d="M13 10.5L8 5.5L3 10.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" id="icon-small-up">
|
||||
<path d="M9.5 7.75L6 4.25L2.5 7.75" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-down">
|
||||
<path d="M3 5.5l5 5 5-5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</symbol>
|
||||
|
|
@ -14,15 +14,15 @@
|
|||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" id="icon-small-down">
|
||||
<path d="M2.625 4.375L6 7.75l3.375-3.375" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" id="icon-right">
|
||||
<path d="M4.25 9.5L7.75 6L4.25 2.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" id="icon-left">
|
||||
<path d="M7.5 9.5L4 6l3.5-3.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-up-arrow">
|
||||
<path d="M6.03335 3.23495L6.03169 9.23495" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.36665 5.43497L6.03332 2.7683L8.69998 5.43497" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
|
@ -52,22 +52,22 @@
|
|||
<path d="M12 18L18 12L12 6" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 18L12 12L6 6" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-change">
|
||||
<path d="M13.2818 11.5388H2.59961" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.06069 14L2.59961 11.539L5.06069 9.07788" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.91406 4.46118H12.9679" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.5059 2L12.9669 4.46108L10.5059 6.92217" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-sort">
|
||||
<path d="M9.5 10.5l2 2 2-2m-2 2v-9m-5 2l-2-2-2 2m2-2v9" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" id="icon-select">
|
||||
<path d="M4.5 3.636L6.136 2l1.637 1.636M4.5 8.364L6.136 10l1.637-1.636" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-arrow-up-right">
|
||||
<path d="M2.5 9.5L9.5 2.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.50002 8V2.5H4.00002" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
<path d="M9.5 2.5L2.5 9.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.49999 4L2.49998 9.5L7.99998 9.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" id="icon-expand">
|
||||
<path d="M10 2L6.844 5.158M7.053 2h2.948v2.948M5.158 6.842L2 10m0-2.947V10h2.947" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</symbol>
|
||||
|
|
@ -95,19 +95,19 @@
|
|||
<path d="M4.99998 6.00023L0.767046 6.00023" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.85092 3.91578L0.766818 5.99988L2.85092 8.08398" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-shrink">
|
||||
<path d="M6.76703 6.00006H11.233" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.85114 8.08422L6.76703 6.00012L8.85114 3.91602" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M0.767031 6.00006H5.23297" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.14886 8.08422L5.23297 6.00012L3.14886 3.91602" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" fill="#112B42" id="icon-up">
|
||||
<path d="M3 5h6L6 2 3 5z"></path>
|
||||
<path opacity=".5" d="M6 10l3-3H3l3 3z"></path>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" id="icon-both">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 2l3 3H3l3-3zm3 5l-3 3-3-3h6z" fill="#112B42"></path>
|
||||
</symbol>
|
||||
|
|
@ -303,7 +303,7 @@
|
|||
<path d="M12.4663 15.0275H16.5872L15.4292 13.8695C15.2737 13.714 15.1504 13.5293 15.0662 13.3261C14.9821 13.1229 14.9388 12.9051 14.9389 12.6852V9.09341C14.939 8.07055 14.622 7.07281 14.0316 6.23755C13.4412 5.40228 12.6064 4.77057 11.6421 4.4294V4.14835C11.6421 3.71118 11.4685 3.29192 11.1594 2.98279C10.8502 2.67367 10.431 2.5 9.9938 2.5C9.55663 2.5 9.13736 2.67367 8.82824 2.98279C8.51911 3.29192 8.34545 3.71118 8.34545 4.14835V4.4294C6.42512 5.10852 5.04874 6.94066 5.04874 9.09341V12.686C5.04874 13.1294 4.87237 13.5555 4.55836 13.8695L3.40039 15.0275H7.52127M12.4663 15.0275H7.52127M12.4663 15.0275C12.4663 15.6832 12.2058 16.3121 11.7421 16.7758C11.2785 17.2395 10.6496 17.5 9.9938 17.5C9.33804 17.5 8.70914 17.2395 8.24546 16.7758C7.78177 16.3121 7.52127 15.6832 7.52127 15.0275" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 6.75C15.5188 6.75 16.75 5.51878 16.75 4C16.75 2.48122 15.5188 1.25 14 1.25C12.4812 1.25 11.25 2.48122 11.25 4C11.25 5.51878 12.4812 6.75 14 6.75Z" fill="#FF5858" stroke="white" stroke-width="1.5"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-message">
|
||||
<path d="M11.501 7.5V6a3.5 3.5 0 0 0-7 0v1.5C4.5 9.15 3 9.55 3 10.502c0 .85 1.95 1.5 5 1.5 3.051 0 5.001-.65 5.001-1.5 0-.95-1.5-1.35-1.5-3z" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"></path>
|
||||
<mask id="a" fill="#fff">
|
||||
|
|
@ -947,5 +947,8 @@
|
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 0a1 1 0 011 1v10a3 3 0 003 3h12a2 2 0 012 2v26a4 4 0 01-4 4H4a4 4 0 01-4-4V3a3 3 0 013-3h16zM8 37a1 1 0 100 2h21a1 1 0 100-2H8zm-1-7a1 1 0 011-1h21a1 1 0 110 2H8a1 1 0 01-1-1zm1-9a1 1 0 100 2h6a1 1 0 100-2H8z"></path>
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-clipboard">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 4.0029C5.28752 4.00587 5.11559 4.01186 4.96784 4.02393C4.69617 4.04612 4.59545 4.0838 4.54601 4.10899C4.35785 4.20487 4.20487 4.35785 4.10899 4.54601C4.0838 4.59545 4.04612 4.69617 4.02393 4.96784C4.00078 5.25117 4 5.62345 4 6.2V10.8C4 11.3766 4.00078 11.7488 4.02393 12.0322C4.04612 12.3038 4.0838 12.4045 4.10899 12.454C4.20487 12.6422 4.35785 12.7951 4.54601 12.891C4.59545 12.9162 4.69617 12.9539 4.96784 12.9761C5.25117 12.9992 5.62345 13 6.2 13H9.8C10.3766 13 10.7488 12.9992 11.0322 12.9761C11.3038 12.9539 11.4045 12.9162 11.454 12.891C11.6422 12.7951 11.7951 12.6422 11.891 12.454C11.9162 12.4045 11.9539 12.3038 11.9761 12.0322C11.9992 11.7488 12 11.3766 12 10.8V6.2C12 5.62345 11.9992 5.25117 11.9761 4.96784C11.9539 4.69617 11.9162 4.59545 11.891 4.54601C11.7951 4.35785 11.6422 4.20487 11.454 4.10899C11.4045 4.0838 11.3038 4.04612 11.0322 4.02393C10.8844 4.01186 10.7125 4.00587 10.5 4.0029C10.4984 4.82999 9.82746 5.5 9 5.5H7C6.17254 5.5 5.50157 4.82999 5.5 4.0029ZM10.2924 3.00087C11.0944 3.00548 11.548 3.03457 11.908 3.21799C12.2843 3.40973 12.5903 3.71569 12.782 4.09202C13 4.51984 13 5.0799 13 6.2V10.8C13 11.9201 13 12.4802 12.782 12.908C12.5903 13.2843 12.2843 13.5903 11.908 13.782C11.4802 14 10.9201 14 9.8 14H6.2C5.0799 14 4.51984 14 4.09202 13.782C3.71569 13.5903 3.40973 13.2843 3.21799 12.908C3 12.4802 3 11.9201 3 10.8V6.2C3 5.07989 3 4.51984 3.21799 4.09202C3.40973 3.71569 3.71569 3.40973 4.09202 3.21799C4.45199 3.03457 4.90558 3.00548 5.70764 3.00087C6.09322 2.11745 6.9745 1.5 8 1.5C9.0255 1.5 9.90678 2.11745 10.2924 3.00087ZM6.5 4C6.5 3.17157 7.17157 2.5 8 2.5C8.82843 2.5 9.5 3.17157 9.5 4C9.5 4.27614 9.27614 4.5 9 4.5H7C6.72386 4.5 6.5 4.27614 6.5 4Z" stroke="none" fill="var(--icon-stroke)"/>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 107 KiB |
|
|
@ -58,7 +58,7 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
|
|||
this.has_input = true;
|
||||
this.bind_change_event();
|
||||
this.setup_autoname_check();
|
||||
|
||||
this.setup_copy_button();
|
||||
if (this.df.options == 'URL') {
|
||||
this.setup_url_field();
|
||||
}
|
||||
|
|
@ -112,6 +112,18 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
|
|||
});
|
||||
}
|
||||
|
||||
setup_copy_button() {
|
||||
if (this.df.with_copy_button) {
|
||||
this.$wrapper.find('.control-input').append(
|
||||
`<button class="btn action-btn">
|
||||
${frappe.utils.icon('clipboard', 'sm')}
|
||||
</button>`
|
||||
).find(".action-btn").click(() => {
|
||||
frappe.utils.copy_to_clipboard(this.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setup_barcode_field() {
|
||||
this.$wrapper.find('.control-input').append(
|
||||
`<span class="link-btn">
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
|
|||
dateFormat: date_format,
|
||||
startDate: this.get_start_date(),
|
||||
keyboardNav: false,
|
||||
minDate: this.df.min_date,
|
||||
maxDate: this.df.max_date,
|
||||
firstDay: frappe.datetime.get_first_day_of_the_week_index(),
|
||||
onSelect: () => {
|
||||
this.$input.trigger('change');
|
||||
|
|
|
|||
|
|
@ -913,7 +913,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
} else {
|
||||
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, me.handle_save_fail(btn, on_error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
savetrash() {
|
||||
this.validate_form_action("Delete");
|
||||
|
|
|
|||
|
|
@ -270,7 +270,6 @@ frappe.views.BaseList = class BaseList {
|
|||
doctype: this.doctype,
|
||||
stats: this.stats,
|
||||
parent: this.$page.find(".layout-side-section"),
|
||||
// set_filter: this.set_filter.bind(this),
|
||||
page: this.page,
|
||||
list_view: this,
|
||||
});
|
||||
|
|
@ -428,7 +427,7 @@ frappe.views.BaseList = class BaseList {
|
|||
const filter = this.get_filters_for_args().filter(f => f[1] == fieldname)[0];
|
||||
if (!filter) return;
|
||||
return {
|
||||
'like': filter[3].replace(/^%?|%$/g, ''),
|
||||
'like': filter[3]?.replace(/^%?|%$/g, ''),
|
||||
'not set': null
|
||||
}[filter[2]] || filter[3];
|
||||
}
|
||||
|
|
@ -620,9 +619,7 @@ class FilterArea {
|
|||
filters = [filter];
|
||||
}
|
||||
|
||||
filters = filters.filter((f) => {
|
||||
return !this.exists(f);
|
||||
});
|
||||
filters = filters.filter(f => !this.exists(f));
|
||||
|
||||
const { non_standard_filters, promise } = this.set_standard_filter(
|
||||
filters
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ frappe.provide('frappe.views');
|
|||
// stats = list of fields
|
||||
// doctype
|
||||
// parent
|
||||
// set_filter = function called on click
|
||||
|
||||
frappe.views.ListSidebar = class ListSidebar {
|
||||
constructor(opts) {
|
||||
|
|
|
|||
|
|
@ -94,10 +94,14 @@ frappe.RoleEditor = class {
|
|||
.css("max-width", "80vw");
|
||||
}
|
||||
show() {
|
||||
let user_roles = this.frm.doc.roles.map(a => a.role);
|
||||
this.reset();
|
||||
this.set_enable_disable();
|
||||
}
|
||||
|
||||
reset() {
|
||||
let user_roles = (this.frm.doc.roles || []).map(a => a.role);
|
||||
this.multicheck.selected_options = user_roles;
|
||||
this.multicheck.refresh_input();
|
||||
this.set_enable_disable();
|
||||
}
|
||||
set_roles_in_table() {
|
||||
let roles = this.frm.doc.roles || [];
|
||||
|
|
|
|||
|
|
@ -249,14 +249,21 @@ frappe.ui.Filter = class {
|
|||
const filter_value = this.filter_list.get_filter_value(fieldname);
|
||||
args[field_name] = filter_value;
|
||||
}
|
||||
frappe
|
||||
.xcall(this.filters_config[condition].get_field, args)
|
||||
.then(field => {
|
||||
df.fieldtype = field.fieldtype;
|
||||
df.options = field.options;
|
||||
df.fieldname = fieldname;
|
||||
this.make_field(df, cur.fieldtype);
|
||||
let setup_field = (field) => {
|
||||
df.fieldtype = field.fieldtype;
|
||||
df.options = field.options;
|
||||
df.fieldname = fieldname;
|
||||
this.make_field(df, cur.fieldtype);
|
||||
}
|
||||
if (this.filters_config[condition].data) {
|
||||
let field = this.filters_config[condition].data;
|
||||
setup_field(field);
|
||||
} else {
|
||||
frappe.xcall(this.filters_config[condition].get_field, args).then(field => {
|
||||
this.filters_config[condition].data = field;
|
||||
setup_field(field);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.make_field(df, cur.fieldtype);
|
||||
}
|
||||
|
|
@ -436,15 +443,17 @@ frappe.ui.filter_utils = {
|
|||
val = val == 'Yes' ? 1 : 0;
|
||||
}
|
||||
|
||||
if (condition.indexOf('like', 'not like') !== -1) {
|
||||
if (['like', 'not like'].includes(condition)) {
|
||||
// automatically append wildcards
|
||||
if (val && !(val.startsWith('%') || val.endsWith('%'))) {
|
||||
val = '%' + val + '%';
|
||||
}
|
||||
} else if (in_list(['in', 'not in'], condition)) {
|
||||
} else if (['in', 'not in'].includes(condition)) {
|
||||
if (val) {
|
||||
val = val.split(',').map((v) => strip(v));
|
||||
}
|
||||
} else if (frappe.boot.additional_filters_config[condition]) {
|
||||
val = field.value || val;
|
||||
}
|
||||
if (val === '%') {
|
||||
val = '';
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => {
|
|||
}
|
||||
let html = shortcuts
|
||||
.filter(s => s.condition ? s.condition() : true)
|
||||
.filter(s => !!s.description)
|
||||
.map(shortcut => {
|
||||
let shortcut_label = shortcut.shortcut
|
||||
.split('+')
|
||||
|
|
@ -94,6 +95,8 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => {
|
|||
<td width="60%">${shortcut.description || ''}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
if (!html) return '';
|
||||
|
||||
html = `<h5 style="margin: 0;">${heading}</h5>
|
||||
<table style="margin-top: 10px;" class="table table-bordered">
|
||||
${html}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
|
|||
frappe.set_route(item.route);
|
||||
}
|
||||
$input.val("");
|
||||
$input.trigger('blur');
|
||||
});
|
||||
|
||||
$input.on("awesomplete-selectcomplete", function(e) {
|
||||
|
|
|
|||
|
|
@ -214,10 +214,6 @@ export default class Block {
|
|||
$button.find('.dropdown-list').toggleClass('hidden');
|
||||
});
|
||||
|
||||
$(document).click(() => {
|
||||
$button.find('.dropdown-list').addClass('hidden');
|
||||
});
|
||||
|
||||
$widget_control.prepend($button);
|
||||
|
||||
this.dropdown_list.forEach((item) => {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ frappe.views.Workspace = class Workspace {
|
|||
|
||||
this.prepare_container();
|
||||
this.setup_pages();
|
||||
this.register_awesomebar_shortcut();
|
||||
}
|
||||
|
||||
prepare_container() {
|
||||
|
|
@ -692,11 +693,6 @@ frappe.views.Workspace = class Workspace {
|
|||
$button.filter('.dropdown-list').toggleClass('hidden');
|
||||
});
|
||||
|
||||
$(document).click(event => {
|
||||
event.stopPropagation();
|
||||
$('.dropdown-list:not(.hidden)').addClass('hidden');
|
||||
});
|
||||
|
||||
sidebar_control.append($button);
|
||||
|
||||
this.dropdown_list.forEach((i) => {
|
||||
|
|
@ -1228,4 +1224,18 @@ frappe.views.Workspace = class Workspace {
|
|||
$('.desk-sidebar').removeClass('hidden');
|
||||
$('.list-sidebar').find('.workspace-sidebar-skeleton').remove();
|
||||
}
|
||||
|
||||
register_awesomebar_shortcut() {
|
||||
'abcdefghijklmnopqrstuvwxyz'.split('').forEach(letter => {
|
||||
const default_shortcut = {
|
||||
action: (e) => {
|
||||
$("#navbar-search").focus();
|
||||
return false; // don't prevent default = type the letter in awesomebar
|
||||
},
|
||||
page: this.page,
|
||||
};
|
||||
frappe.ui.keys.add_shortcut({shortcut: letter, ...default_shortcut});
|
||||
frappe.ui.keys.add_shortcut({shortcut: `shift+${letter}`, ...default_shortcut});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -127,6 +127,24 @@ select.form-control {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.action-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
padding: 3px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
button.action-btn {
|
||||
padding: 3px 5px;
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
@extend .action-btn;
|
||||
background-color: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-control:not([data-fieldtype='MultiSelectPills']):not([data-fieldtype='Table MultiSelect']) {
|
||||
|
|
@ -289,15 +307,6 @@ textarea.form-control {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
padding: 3px;
|
||||
display: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.phone-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@
|
|||
--border-radius-full: 999px;
|
||||
|
||||
--primary-color: #2490EF;
|
||||
--btn-height: 28px;
|
||||
--btn-height: 30px;
|
||||
|
||||
// Checkbox
|
||||
--checkbox-right-margin: var(--margin-xs);
|
||||
|
|
|
|||
|
|
@ -492,4 +492,4 @@ body[data-route^="Module"] .main-menu {
|
|||
|
||||
.shared-user {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
@import "frappe/public/css/bootstrap.css";
|
||||
@import "./common/quill";
|
||||
|
||||
@import "./desk/variables";
|
||||
@import "~bootstrap/scss/utilities/spacing";
|
||||
@import "./desk/css_variables";
|
||||
@import "./element/checkbox";
|
||||
|
||||
|
|
@ -12,4 +15,23 @@
|
|||
svg[data-barcode-value] > g {
|
||||
fill: black !important;
|
||||
}
|
||||
.print-hide {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.action-banner {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-right: 20px;
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
|
||||
.invalid-state {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
height: 100vh;
|
||||
img {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -125,6 +125,10 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.page_content {
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.25rem;
|
||||
|
|
|
|||
|
|
@ -2,33 +2,32 @@
|
|||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.page-card {
|
||||
max-width: 360px;
|
||||
padding: 15px;
|
||||
margin: 70px auto;
|
||||
border-radius: 4px;
|
||||
background-color: var(--fg-color);
|
||||
/* box-shadow: var(--shadow-base); */
|
||||
max-width: 360px;
|
||||
padding: 15px;
|
||||
margin: 70px auto;
|
||||
border-radius: 4px;
|
||||
background-color: var(--fg-color);
|
||||
box-shadow: var(--shadow-base);
|
||||
}
|
||||
|
||||
.for-reset-password {
|
||||
margin: 80px 0;
|
||||
margin: 80px 0;
|
||||
}
|
||||
|
||||
.for-reset-password .page-card {
|
||||
border: 0;
|
||||
max-width: 450px;
|
||||
margin: auto;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
max-width: 450px;
|
||||
margin: auto;
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: 40px 60px;
|
||||
}
|
||||
|
||||
@media (min-width: 567px) {
|
||||
@media (max-width: 425px) {
|
||||
.for-reset-password .page-card {
|
||||
box-shadow: var(--shadow-base);
|
||||
padding: 40px 60px;
|
||||
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import frappe
|
||||
|
||||
|
||||
def update_system_settings(args):
|
||||
def update_system_settings(args, commit=False):
|
||||
doc = frappe.get_doc("System Settings")
|
||||
doc.update(args)
|
||||
doc.flags.ignore_mandatory = 1
|
||||
doc.save()
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_system_setting(key):
|
||||
|
|
|
|||
|
|
@ -182,10 +182,12 @@ class TestDB(unittest.TestCase):
|
|||
self.assertIn("tabToDo", frappe.flags.touched_tables)
|
||||
|
||||
frappe.flags.touched_tables = set()
|
||||
create_custom_field("ToDo", {"label": "ToDo Custom Field"})
|
||||
|
||||
cf = create_custom_field("ToDo", {"label": "ToDo Custom Field"})
|
||||
self.assertIn("tabToDo", frappe.flags.touched_tables)
|
||||
self.assertIn("tabCustom Field", frappe.flags.touched_tables)
|
||||
if cf:
|
||||
cf.delete()
|
||||
frappe.db.commit()
|
||||
frappe.flags.in_migrate = False
|
||||
frappe.flags.touched_tables.clear()
|
||||
|
||||
|
|
@ -867,3 +869,18 @@ class TestDDLCommandsPost(unittest.TestCase):
|
|||
self.assertIn(
|
||||
"is null", frappe.db.get_values(user, filters={user.name: ("is", "not set")}, run=False).lower()
|
||||
)
|
||||
|
||||
|
||||
@run_only_if(db_type_is.POSTGRES)
|
||||
class TestTransactionManagement(unittest.TestCase):
|
||||
def test_create_proper_transactions(self):
|
||||
def _get_transaction_id():
|
||||
return frappe.db.sql("select txid_current()", pluck=True)
|
||||
|
||||
self.assertEqual(_get_transaction_id(), _get_transaction_id())
|
||||
|
||||
frappe.db.rollback()
|
||||
self.assertEqual(_get_transaction_id(), _get_transaction_id())
|
||||
|
||||
frappe.db.commit()
|
||||
self.assertEqual(_get_transaction_id(), _get_transaction_id())
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@ from datetime import timedelta
|
|||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.app import make_form_dict
|
||||
from frappe.desk.doctype.note.note import Note
|
||||
from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last
|
||||
from frappe.utils import cint, now_datetime
|
||||
from frappe.utils import cint, now_datetime, set_request
|
||||
from frappe.website.serve import get_response
|
||||
|
||||
from . import update_system_settings
|
||||
|
||||
|
||||
class CustomTestNote(Note):
|
||||
|
|
@ -357,3 +361,49 @@ class TestDocument(unittest.TestCase):
|
|||
# setting None should init a table field to empty list
|
||||
doc.set("user_emails", None)
|
||||
self.assertEqual(doc.user_emails, [])
|
||||
|
||||
|
||||
class TestDocumentWebView(unittest.TestCase):
|
||||
def get(self, path, user="Guest"):
|
||||
frappe.set_user(user)
|
||||
set_request(method="GET", path=path)
|
||||
make_form_dict(frappe.local.request)
|
||||
response = get_response()
|
||||
frappe.set_user("Administrator")
|
||||
return response
|
||||
|
||||
def test_web_view_link_authentication(self):
|
||||
todo = frappe.get_doc({"doctype": "ToDo", "description": "Test"}).insert()
|
||||
document_key = todo.get_document_share_key()
|
||||
|
||||
# with old-style signature key
|
||||
update_system_settings({"allow_older_web_view_links": True}, True)
|
||||
old_document_key = todo.get_signature()
|
||||
url = f"/ToDo/{todo.name}?key={old_document_key}"
|
||||
self.assertEqual(self.get(url).status, "200 OK")
|
||||
|
||||
update_system_settings({"allow_older_web_view_links": False}, True)
|
||||
self.assertEqual(self.get(url).status, "401 UNAUTHORIZED")
|
||||
|
||||
# with valid key
|
||||
url = f"/ToDo/{todo.name}?key={document_key}"
|
||||
self.assertEqual(self.get(url).status, "200 OK")
|
||||
|
||||
# with invalid key
|
||||
invalid_key_url = f"/ToDo/{todo.name}?key=INVALID_KEY"
|
||||
self.assertEqual(self.get(invalid_key_url).status, "401 UNAUTHORIZED")
|
||||
|
||||
# expire the key
|
||||
document_key_doc = frappe.get_doc("Document Share Key", {"key": document_key})
|
||||
document_key_doc.expires_on = "2020-01-01"
|
||||
document_key_doc.save(ignore_permissions=True)
|
||||
|
||||
# with expired key
|
||||
self.assertEqual(self.get(url).status, "410 GONE")
|
||||
|
||||
# without key
|
||||
url_without_key = f"/ToDo/{todo.name}"
|
||||
self.assertEqual(self.get(url_without_key).status, "403 FORBIDDEN")
|
||||
|
||||
# Logged-in user can access the page without key
|
||||
self.assertEqual(self.get(url_without_key, "Administrator").status, "200 OK")
|
||||
|
|
|
|||
|
|
@ -100,8 +100,6 @@ class TestRenameDoc(unittest.TestCase):
|
|||
frappe.delete_doc("DocType", dt)
|
||||
frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{dt}`")
|
||||
|
||||
frappe.delete_doc_if_exists("Renamed Doc", "ToDo")
|
||||
|
||||
# reset original value of developer_mode conf
|
||||
frappe.conf.developer_mode = self._original_developer_flag
|
||||
|
||||
|
|
|
|||
|
|
@ -30,13 +30,17 @@ from frappe.utils import (
|
|||
validate_url,
|
||||
)
|
||||
from frappe.utils.data import (
|
||||
add_to_date,
|
||||
cast,
|
||||
get_first_day_of_week,
|
||||
get_time,
|
||||
get_timedelta,
|
||||
getdate,
|
||||
now_datetime,
|
||||
nowtime,
|
||||
validate_python_code,
|
||||
)
|
||||
from frappe.utils.dateutils import get_dates_from_timegrain
|
||||
from frappe.utils.diff import _get_value_from_version, get_version_diff, version_query
|
||||
from frappe.utils.image import optimize_image, strip_exif_data
|
||||
from frappe.utils.response import json_handler
|
||||
|
|
@ -445,6 +449,31 @@ class TestDateUtils(unittest.TestCase):
|
|||
self.assertIsInstance(get_timedelta(str(timedelta_input)), timedelta)
|
||||
self.assertIsInstance(get_timedelta(str(time_input)), timedelta)
|
||||
|
||||
def test_date_from_timegrain(self):
|
||||
start_date = getdate("2021-01-01")
|
||||
|
||||
daily = get_dates_from_timegrain(start_date, add_to_date(start_date, days=6), "Daily")
|
||||
self.assertEqual(len(daily), 7)
|
||||
for idx, d in enumerate(daily):
|
||||
self.assertEqual(d, add_to_date(start_date, days=idx))
|
||||
|
||||
start = get_first_day_of_week(start_date)
|
||||
end = add_to_date(add_to_date(start, weeks=52), days=-1)
|
||||
weekly = get_dates_from_timegrain(start, end, "Weekly")
|
||||
self.assertEqual(len(weekly), 52)
|
||||
for idx, d in enumerate(weekly, start=1):
|
||||
self.assertEqual(d, add_to_date(start, days=7 * idx - 1))
|
||||
|
||||
quarterly = get_dates_from_timegrain(start_date, add_to_date(start_date, months=5), "Quarterly")
|
||||
self.assertEqual(len(quarterly), 2)
|
||||
for idx, d in enumerate(quarterly, start=1):
|
||||
self.assertEqual(d, add_to_date(start_date, months=idx * 3, days=-1))
|
||||
|
||||
yearly = get_dates_from_timegrain(start_date, add_to_date(start_date, years=2), "Yearly")
|
||||
self.assertEqual(len(yearly), 3)
|
||||
for idx, d in enumerate(yearly, start=1):
|
||||
self.assertEqual(d, add_to_date(start_date, years=idx, days=-1))
|
||||
|
||||
|
||||
class TestResponse(unittest.TestCase):
|
||||
def test_json_handler(self):
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ class TestWebsite(unittest.TestCase):
|
|||
def test_error_page(self):
|
||||
set_request(method="GET", path="/_test/problematic_page")
|
||||
response = get_response()
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertEqual(response.status_code, 417)
|
||||
|
||||
def test_login(self):
|
||||
set_request(method="GET", path="/login")
|
||||
|
|
|
|||
|
|
@ -419,6 +419,7 @@ Also adding the dependent currency field {0},Ajout également du champ de devise
|
|||
Always use Account's Email Address as Sender,Toujours utiliser l'adresse Email du compte comme Expéditeur,
|
||||
Always use Account's Name as Sender's Name,Toujours utiliser le nom du compte comme nom de l'expéditeur,
|
||||
Amend,Nouv. version
|
||||
amend,Nouv. version
|
||||
Amending,Nouv. version en cours,
|
||||
Amount Based On Field,Montant Basé sur le Champ,
|
||||
Amount Field,Champ du Montant,
|
||||
|
|
@ -2296,7 +2297,7 @@ Show Line Breaks after Sections,Afficher les Sauts de Ligne après Sections,
|
|||
Show Permissions,Afficher les Autorisations,
|
||||
Show Preview Popup,Afficher l'aperçu Popup,
|
||||
Show Relapses,Afficher les Rechutes,
|
||||
Show Report,Rapport d'émission,
|
||||
Show Report,Afficher le rapport,
|
||||
Show Section Headings,Voir la Section Titres,
|
||||
Show Sidebar,Afficher la Barre Latérale,
|
||||
Show Title,Afficher le Titre,
|
||||
|
|
@ -4641,7 +4642,7 @@ Not permitted to view {0},Non autorisé à afficher {0},
|
|||
Camera,Caméra,
|
||||
Invalid filter: {0},Filtre non valide: {0},
|
||||
Let's Get Started,Commençons,
|
||||
Reports & Masters,Rapports et masters,
|
||||
Reports & Masters,Ecrans principaux et Rapports,
|
||||
New {0} {1} added to Dashboard {2},Nouveau {0} {1} ajouté au tableau de bord {2},
|
||||
New {0} {1} created,Nouveau {0} {1} créé,
|
||||
New {0} Created,Nouveau {0} créé,
|
||||
|
|
@ -4701,8 +4702,8 @@ Value cannot be negative for,La valeur ne peut pas être négative pour,
|
|||
Value cannot be negative for {0}: {1},La valeur ne peut pas être négative pour {0}: {1},
|
||||
Negative Value,Valeur négative,
|
||||
Authentication failed while receiving emails from Email Account: {0}.,L'authentification a échoué lors de la réception des e-mails du compte de messagerie: {0}.,
|
||||
Message from server: {0},Message du serveur: {0},
|
||||
{0} edited this {1},{0} a édité {1},
|
||||
Message from server: {0},Message du serveur: {0}
|
||||
{0} edited this {1},{0} a édité {1}
|
||||
{0} created this {1},{0} a créé {1}
|
||||
Report an Issue,Signaler une anomalie
|
||||
User Forum,Forum utilisateur
|
||||
|
|
@ -4720,3 +4721,8 @@ Don't have an account?,Vous n'avez pas de compte?
|
|||
Left:alignment,Gauche
|
||||
Right:alignment,Droite
|
||||
Set Properties,Gérer les proriétés
|
||||
Create Workspace,Créer un espace de travail
|
||||
Always use this email address as sender address,Toujours utiliser cet email comme expediteur
|
||||
Always use this name as sender name,Toujours utiliser ce nom comme expediteur
|
||||
Login to {0},Se connecter à {0}
|
||||
Add / Remove Fields,Ajouter / Supprimer des colonnes
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 421.
|
|
|
@ -106,11 +106,10 @@ def get_dates_from_timegrain(from_date, to_date, timegrain="Daily"):
|
|||
months = 1
|
||||
elif "Quarterly" == timegrain:
|
||||
months = 3
|
||||
elif "Yearly" == timegrain:
|
||||
months = 1
|
||||
|
||||
if "Weekly" == timegrain:
|
||||
dates = [get_last_day_of_week(from_date)]
|
||||
else:
|
||||
dates = [get_period_ending(from_date, timegrain)]
|
||||
dates = [get_period_ending(from_date, timegrain)]
|
||||
|
||||
while getdate(dates[-1]) < getdate(to_date):
|
||||
if "Weekly" == timegrain:
|
||||
|
|
@ -163,16 +162,12 @@ def get_period_beginning(date, timegrain, as_str=True):
|
|||
|
||||
|
||||
def get_period_ending(date, timegrain):
|
||||
date = getdate(date)
|
||||
if timegrain == "Daily":
|
||||
return date
|
||||
else:
|
||||
return getdate(
|
||||
{
|
||||
"Daily": date,
|
||||
"Weekly": get_last_day_of_week(date),
|
||||
"Monthly": get_last_day(date),
|
||||
"Quarterly": get_quarter_ending(date),
|
||||
"Yearly": get_year_ending(date),
|
||||
}[timegrain]
|
||||
)
|
||||
return getdate(
|
||||
{
|
||||
"Daily": date,
|
||||
"Weekly": get_last_day_of_week(date),
|
||||
"Monthly": get_last_day(date),
|
||||
"Quarterly": get_quarter_ending(date),
|
||||
"Yearly": get_year_ending(date),
|
||||
}[timegrain]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ no_cache = 1
|
|||
base_template_path = "www/printview.html"
|
||||
standard_format = "templates/print_formats/standard.html"
|
||||
|
||||
from frappe.www.printview import validate_print_permission
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=None):
|
||||
|
|
@ -115,8 +117,11 @@ def read_multi_pdf(output):
|
|||
return filedata
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def download_pdf(doctype, name, format=None, doc=None, no_letterhead=0):
|
||||
doc = doc or frappe.get_doc(doctype, name)
|
||||
validate_print_permission(doc)
|
||||
|
||||
html = frappe.get_print(doctype, name, format, doc=doc, no_letterhead=no_letterhead)
|
||||
frappe.local.response.filename = "{name}.pdf".format(
|
||||
name=name.replace(" ", "-").replace("/", "-")
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class RedisWrapper(redis.Redis):
|
|||
|
||||
return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8")
|
||||
|
||||
def set_value(self, key, val, user=None, expires_in_sec=None, shared=False):
|
||||
def set_value(self, key, val, user=None, expires_in_sec=None, shared=False, cache_locally=True):
|
||||
"""Sets cache value.
|
||||
|
||||
:param key: Cache key
|
||||
|
|
@ -40,7 +40,7 @@ class RedisWrapper(redis.Redis):
|
|||
"""
|
||||
key = self.make_key(key, user, shared)
|
||||
|
||||
if not expires_in_sec:
|
||||
if not expires_in_sec and cache_locally:
|
||||
frappe.local.cache[key] = val
|
||||
|
||||
try:
|
||||
|
|
@ -151,16 +151,17 @@ class RedisWrapper(redis.Redis):
|
|||
def ltrim(self, key, start, stop):
|
||||
return super(RedisWrapper, self).ltrim(self.make_key(key), start, stop)
|
||||
|
||||
def hset(self, name, key, value, shared=False):
|
||||
def hset(self, name: str, key: str, value, shared: bool = False, cache_locally: bool = True):
|
||||
if key is None:
|
||||
return
|
||||
|
||||
_name = self.make_key(name, shared=shared)
|
||||
|
||||
# set in local
|
||||
if _name not in frappe.local.cache:
|
||||
frappe.local.cache[_name] = {}
|
||||
frappe.local.cache[_name][key] = value
|
||||
if cache_locally:
|
||||
if _name not in frappe.local.cache:
|
||||
frappe.local.cache[_name] = {}
|
||||
frappe.local.cache[_name][key] = value
|
||||
|
||||
# set in redis
|
||||
try:
|
||||
|
|
@ -168,6 +169,15 @@ class RedisWrapper(redis.Redis):
|
|||
except redis.exceptions.ConnectionError:
|
||||
pass
|
||||
|
||||
def hexists(self, name: str, key: str, shared: bool = False) -> bool:
|
||||
if key is None:
|
||||
return False
|
||||
_name = self.make_key(name, shared=shared)
|
||||
try:
|
||||
return super(RedisWrapper, self).hexists(_name, key)
|
||||
except redis.exceptions.ConnectionError:
|
||||
return False
|
||||
|
||||
def hgetall(self, name):
|
||||
value = super(RedisWrapper, self).hgetall(self.make_key(name))
|
||||
return {key: pickle.loads(value) for key, value in value.items()}
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "To use Google Indexing, enable <a href=\"#Form/Google Settings\">Google Settings</a>.",
|
||||
"description": "To use Google Indexing, enable <a href=\"/app/google-settings\">Google Settings</a>.",
|
||||
"fieldname": "enable_google_indexing",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Google Indexing"
|
||||
|
|
@ -451,4 +451,4 @@
|
|||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@ class ErrorPage(TemplatePage):
|
|||
def __init__(self, path=None, http_status_code=None, exception=None):
|
||||
path = "error"
|
||||
super().__init__(path=path, http_status_code=http_status_code)
|
||||
self.http_status_code = getattr(exception, "http_status_code", None) or http_status_code or 500
|
||||
self.exception = exception
|
||||
|
||||
def can_render(self):
|
||||
return True
|
||||
|
||||
def init_context(self):
|
||||
super().init_context()
|
||||
self.context.http_status_code = getattr(self.exception, "http_status_code", None) or 500
|
||||
self.context.error_title = getattr(self.exception, "title", None)
|
||||
self.context.error_message = getattr(self.exception, "message", None)
|
||||
|
|
|
|||
|
|
@ -212,19 +212,13 @@ class TemplatePage(BaseTemplatePage):
|
|||
|
||||
def run_pymodule_method(self, method_name):
|
||||
if hasattr(self.pymodule, method_name):
|
||||
try:
|
||||
import inspect
|
||||
import inspect
|
||||
|
||||
method = getattr(self.pymodule, method_name)
|
||||
if inspect.getfullargspec(method).args:
|
||||
return method(self.context)
|
||||
else:
|
||||
return method()
|
||||
except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect):
|
||||
raise
|
||||
except Exception:
|
||||
if not frappe.flags.in_migrate:
|
||||
frappe.errprint(frappe.utils.get_traceback())
|
||||
method = getattr(self.pymodule, method_name)
|
||||
if inspect.getfullargspec(method).args:
|
||||
return method(self.context)
|
||||
else:
|
||||
return method()
|
||||
|
||||
def render_template(self):
|
||||
if self.template_path.endswith("min.js"):
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@
|
|||
<script></script>
|
||||
<div class="page-card">
|
||||
<div class="page-card-head">
|
||||
<span class="indicator red">{{_("Uncaught Server Exception")}}</span>
|
||||
<span class="indicator red">{{ error_title }}</span>
|
||||
</div>
|
||||
<p>{{_("There was an error building this page")}}</p>
|
||||
<p>{{ error_message }}</p>
|
||||
<div>
|
||||
<a href="/" class="btn btn-primary btn-sm">{{ _("Home") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted text-center small" style="margin-top: -20px;">
|
||||
{{ _("Error Code: {0}").format('500') }}
|
||||
{{ _("Error Code: {0}").format(http_status_code) }}
|
||||
</p>
|
||||
<div class="text-center mt-3">
|
||||
<button class="btn btn-xs btn-link text-muted small view-error" >{{ _("Show Traceback") if not dev_server else _("Hide Traceback") }}</a>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
no_cache = 1
|
||||
|
||||
|
|
@ -8,7 +9,8 @@ no_cache = 1
|
|||
def get_context(context):
|
||||
if frappe.flags.in_migrate:
|
||||
return
|
||||
context.http_status_code = 500
|
||||
|
||||
print(frappe.get_traceback())
|
||||
context.error_title = context.error_title or _("Uncaught Server Exception")
|
||||
context.error_message = context.error_message or _("There was an error building this page")
|
||||
|
||||
return {"error": frappe.get_traceback().replace("<", "<").replace(">", ">")}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,26 @@
|
|||
<title>{{ title }}</title>
|
||||
<meta name="generator" content="frappe">
|
||||
{{ include_style('print.bundle.css') }}
|
||||
<style>
|
||||
{{ css }}
|
||||
</style>
|
||||
{% if print_style %}
|
||||
<style>
|
||||
{{ print_style }}
|
||||
</style>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="action-banner print-hide">
|
||||
<a class="p-2" onclick="window.print();">
|
||||
{{ _("Print") }}
|
||||
</a>
|
||||
<a class="p-2"
|
||||
href="/api/method/frappe.utils.print_format.download_pdf?doctype={{doctype}}&name={{name}}&key={{key}}">
|
||||
{{ _('Get PDF') }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="print-format-gutter">
|
||||
<div class="print-format">
|
||||
{{ body }}
|
||||
</div>
|
||||
<div class="print-format">
|
||||
{{ body }}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import re
|
|||
import frappe
|
||||
from frappe import _, get_module_path
|
||||
from frappe.core.doctype.access_log.access_log import make_access_log
|
||||
from frappe.core.doctype.document_share_key.document_share_key import is_expired
|
||||
from frappe.utils import cint, sanitize_html, strip_html
|
||||
from frappe.utils.jinja_globals import is_rtl
|
||||
|
||||
|
|
@ -46,21 +47,28 @@ def get_context(context):
|
|||
doctype=frappe.form_dict.doctype, document=frappe.form_dict.name, file_type="PDF", method="Print"
|
||||
)
|
||||
|
||||
print_style = None
|
||||
body = get_rendered_template(
|
||||
doc,
|
||||
print_format=print_format,
|
||||
meta=meta,
|
||||
trigger_print=frappe.form_dict.trigger_print,
|
||||
no_letterhead=frappe.form_dict.no_letterhead,
|
||||
letterhead=letterhead,
|
||||
settings=settings,
|
||||
)
|
||||
print_style = get_print_style(frappe.form_dict.style, print_format)
|
||||
|
||||
return {
|
||||
"body": get_rendered_template(
|
||||
doc,
|
||||
print_format=print_format,
|
||||
meta=meta,
|
||||
trigger_print=frappe.form_dict.trigger_print,
|
||||
no_letterhead=frappe.form_dict.no_letterhead,
|
||||
letterhead=letterhead,
|
||||
settings=settings,
|
||||
),
|
||||
"css": get_print_style(frappe.form_dict.style, print_format),
|
||||
"body": body,
|
||||
"print_style": print_style,
|
||||
"comment": frappe.session.user,
|
||||
"title": doc.get(meta.title_field) if meta.title_field else doc.name,
|
||||
"title": frappe.utils.strip_html(doc.get_title()),
|
||||
"lang": frappe.local.lang,
|
||||
"layout_direction": "rtl" if is_rtl() else "ltr",
|
||||
"doctype": frappe.form_dict.doctype,
|
||||
"name": frappe.form_dict.name,
|
||||
"key": frappe.form_dict.get("key"),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -323,13 +331,34 @@ def get_rendered_raw_commands(doc, name=None, print_format=None, meta=None, lang
|
|||
|
||||
|
||||
def validate_print_permission(doc):
|
||||
if frappe.form_dict.get("key"):
|
||||
if frappe.form_dict.key == doc.get_signature():
|
||||
for ptype in ("read", "print"):
|
||||
if frappe.has_permission(doc.doctype, ptype, doc) or frappe.has_website_permission(doc):
|
||||
return
|
||||
|
||||
for ptype in ("read", "print"):
|
||||
if not frappe.has_permission(doc.doctype, ptype, doc) and not frappe.has_website_permission(doc):
|
||||
raise frappe.PermissionError(_("No {0} permission").format(ptype))
|
||||
key = frappe.form_dict.get("key")
|
||||
if key:
|
||||
validate_key(key, doc)
|
||||
else:
|
||||
raise frappe.PermissionError(_("You do not have permission to view this document"))
|
||||
|
||||
|
||||
def validate_key(key, doc):
|
||||
document_key_expiry = frappe.get_cached_value(
|
||||
"Document Share Key",
|
||||
{"reference_doctype": doc.doctype, "reference_docname": doc.name, "key": key},
|
||||
["expires_on"],
|
||||
)
|
||||
if document_key_expiry is not None:
|
||||
if is_expired(document_key_expiry[0]):
|
||||
raise frappe.exceptions.LinkExpired
|
||||
else:
|
||||
return
|
||||
|
||||
# TODO: Deprecate this! kept it for backward compatibility
|
||||
if frappe.get_system_settings("allow_older_web_view_links") and key == doc.get_signature():
|
||||
return
|
||||
|
||||
raise frappe.exceptions.InvalidKeyError
|
||||
|
||||
|
||||
def get_letter_head(doc, no_letterhead, letterhead=None):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue