Merge branch 'develop' into refactor-file

This commit is contained in:
gavin 2022-06-08 12:43:35 +05:30 committed by GitHub
commit 44dba28159
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 709 additions and 278 deletions

View file

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

View file

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

View file

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

View file

@ -3,6 +3,9 @@ on:
pull_request:
types: [ opened, synchronize, reopened, edited ]
permissions:
contents: read
jobs:
docs-required:
name: 'Documentation Required'

View file

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

View file

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

View file

@ -11,6 +11,9 @@ concurrency:
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest

View file

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

View file

@ -10,6 +10,9 @@ concurrency:
group: ui-develop-${{ github.event.number }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest

View file

@ -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",
[

View file

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

View file

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

View file

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

View file

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

View file

@ -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(),
}
)

View 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) {
// }
});

View file

@ -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": []
}

View 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()

View file

@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
class TestDocumentShareKey(unittest.TestCase):
pass

View file

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

View file

@ -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()

View file

@ -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"
},

View file

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

View file

@ -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):

View file

@ -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()

View file

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

View file

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

View file

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

View file

@ -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,
)

View file

@ -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")

View file

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

View file

@ -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")

View file

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

View file

@ -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,
)

View file

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

View file

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

View file

@ -30,6 +30,7 @@ doctypes_to_skip = (
"Tag Link",
"Notification Log",
"Email Queue",
"Document Share Key",
)

View file

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

View file

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

View 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},
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

View file

@ -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) {

View file

@ -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) => {

View file

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

View file

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

View file

@ -248,7 +248,7 @@
--border-radius-full: 999px;
--primary-color: #2490EF;
--btn-height: 28px;
--btn-height: 30px;
// Checkbox
--checkbox-right-margin: var(--margin-xs);

View file

@ -492,4 +492,4 @@ body[data-route^="Module"] .main-menu {
.shared-user {
margin-bottom: 10px;
}
}

View file

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

View file

@ -125,6 +125,10 @@
align-items: center;
}
.page_content {
min-height: 50vh;
}
.breadcrumb-container {
margin-top: 1rem;
padding-top: 0.25rem;

View file

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

View file

@ -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):

View file

@ -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())

View file

@ -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")

View file

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

View file

@ -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):

View file

@ -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")

View file

@ -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&#39;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&#39;aperçu Popup,
Show Relapses,Afficher les Rechutes,
Show Report,Rapport d&#39;é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&#39;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&#39;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.

View file

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

View file

@ -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("/", "-")

View file

@ -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()}

View file

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

View file

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

View file

@ -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"):

View file

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

View file

@ -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("<", "&lt;").replace(">", "&gt;")}

View file

@ -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', () => {

View file

@ -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):