Merge remote-tracking branch 'upstream/develop' into fix-note-2

This commit is contained in:
barredterra 2023-04-24 15:55:02 +02:00
commit 150d677a28
54 changed files with 365 additions and 263 deletions

View file

@ -7,8 +7,8 @@ context("Folder Navigation", () => {
it("Adding Folders", () => {
//Adding filter to go into the home folder
cy.get(".filter-selector > .btn").findByText("1 filter").click();
cy.findByRole("button", { name: "Clear Filters" }).click();
cy.get(".filter-x-button").click();
cy.click_filter_button();
cy.get(".filter-action-buttons > .text-muted").findByText("+ Add a Filter").click();
cy.get(".fieldname-select-area > .awesomplete > .form-control:last").type("Fol{enter}");
cy.get(
@ -47,9 +47,13 @@ context("Folder Navigation", () => {
//Adding a file inside the Test Folder
cy.findByRole("button", { name: "Add File" }).eq(0).click({ force: true });
cy.get(".file-uploader").findByText("Link").click();
cy.get(".input-group > .form-control").type(
"https://wallpaperplay.com/walls/full/8/2/b/72402.jpg"
);
cy.get(".input-group > input.form-control:visible").as("upload_input");
cy.get("@upload_input").type("https://wallpaperplay.com/walls/full/8/2/b/72402.jpg", {
waitForAnimations: false,
parseSpecialCharSequences: false,
force: true,
delay: 100,
});
cy.click_modal_primary_button("Upload");
//To check if the added file is present in the Test Folder

View file

@ -12,7 +12,7 @@ context("List Paging", () => {
it("test load more with count selection buttons", () => {
cy.visit("/app/todo/view/report");
cy.clear_filters();
cy.get(".filter-x-button").click();
cy.get(".list-paging-area .list-count").should("contain.text", "20 of");
cy.get(".list-paging-area .btn-more").click();

View file

@ -5,6 +5,7 @@ context("List View", () => {
});
it("List view check rows on drag", () => {
cy.get(".filter-x-button").click();
cy.get(".list-row-checkbox").then(($checkbox) => {
cy.wrap($checkbox).first().trigger("mousedown");
cy.get(".level.list-row").each(($ele) => {

View file

@ -53,7 +53,7 @@ context("Sidebar", () => {
);
//To check if there is no filter added to the listview
cy.get(".filter-selector > .btn").should("contain", "Filter");
cy.get(".filter-button").should("contain", "Filter");
//To add a filter to display data into the listview
cy.get(".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item").click();

View file

@ -479,7 +479,7 @@ Cypress.Commands.add("click_listview_row_item_with_text", (text) => {
});
Cypress.Commands.add("click_filter_button", () => {
cy.get(".filter-selector > .btn").click();
cy.get(".filter-button").click();
});
Cypress.Commands.add("click_listview_primary_button", (btn_name) => {

View file

@ -149,18 +149,26 @@ def get_permitted_and_not_permitted_links(doctype):
return {"permitted_links": permitted_links, "not_permitted_links": not_permitted_links}
def delete_contact_and_address(doctype, docname):
def delete_contact_and_address(doctype: str, docname: str) -> None:
for parenttype in ("Contact", "Address"):
items = frappe.db.sql_list(
"""select parent from `tabDynamic Link`
where parenttype=%s and link_doctype=%s and link_name=%s""",
(parenttype, doctype, docname),
)
for name in items:
for name in frappe.get_all(
"Dynamic Link",
filters={
"parenttype": parenttype,
"link_doctype": doctype,
"link_name": docname,
},
pluck="parent",
):
doc = frappe.get_doc(parenttype, name)
if len(doc.links) == 1:
doc.delete()
else:
for link in doc.links:
if link.link_doctype == doctype and link.link_name == docname:
doc.remove(link)
doc.save()
break
@frappe.whitelist()

View file

@ -510,7 +510,7 @@
"label": "Disable Username/Password Login"
},
{
"default": "0",
"default": "1",
"description": "Allow users to log in without a password, using a login link sent to their email",
"fieldname": "login_with_email_link",
"fieldtype": "Check",

View file

@ -114,9 +114,9 @@ frappe.ui.form.on("User", {
return;
}
function hasChanged(doc_attr, boot_attr) {
return (doc_attr || boot_attr) && doc_attr !== boot_attr;
}
const hasChanged = (doc_attr, boot_attr) => {
return doc_attr && boot_attr && doc_attr !== boot_attr;
};
if (
doc.name === frappe.session.user &&

View file

@ -21,6 +21,9 @@ class PropertySetter(Document):
delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name)
frappe.clear_cache(doctype=self.doc_type)
def on_trash(self):
frappe.clear_cache(doctype=self.doc_type)
def validate_fieldtype_change(self):
if self.property == "fieldtype" and self.field_name in not_allowed_fieldtype_change:
frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name))

View file

@ -153,6 +153,8 @@ class Workspace:
return True
if item_type == "dashboard":
return True
if item_type == "url":
return True
return False

View file

@ -17,7 +17,10 @@ class Workspace(Document):
def validate(self):
if self.public and not is_workspace_manager() and not disable_saving_as_public():
frappe.throw(_("You need to be Workspace Manager to edit this document"))
validate_route_conflict(self.doctype, self.name)
if self.has_value_changed("title"):
validate_route_conflict(self.doctype, self.title)
else:
validate_route_conflict(self.doctype, self.name)
try:
if not isinstance(loads(self.content), list):

View file

@ -7,6 +7,7 @@
"field_order": [
"type",
"link_to",
"url",
"doc_view",
"column_break_4",
"label",
@ -24,16 +25,16 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "DocType\nReport\nPage\nDashboard",
"options": "DocType\nReport\nPage\nDashboard\nURL",
"reqd": 1
},
{
"depends_on": "eval:doc.type != \"URL\"",
"fieldname": "link_to",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Link To",
"options": "type",
"reqd": 1
"options": "type"
},
{
"depends_on": "eval:doc.type == \"DocType\"",
@ -94,12 +95,20 @@
"fieldname": "format",
"fieldtype": "Data",
"label": "Format"
},
{
"depends_on": "eval:doc.type == \"URL\"",
"fieldname": "url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "URL",
"options": "URL"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-01-12 13:13:17.571324",
"modified": "2023-04-19 13:32:31.005443",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Shortcut",

View file

@ -147,35 +147,36 @@ def add_multiple(args=None):
def close_all_assignments(doctype, name):
assignments = frappe.get_all(
"ToDo",
fields=["allocated_to"],
fields=["allocated_to", "name"],
filters=dict(reference_type=doctype, reference_name=name, status=("!=", "Cancelled")),
)
if not assignments:
return False
for assign_to in assignments:
set_status(doctype, name, assign_to.allocated_to, status="Closed")
set_status(doctype, name, todo=assign_to.name, assign_to=assign_to.allocated_to, status="Closed")
return True
@frappe.whitelist()
def remove(doctype, name, assign_to):
return set_status(doctype, name, assign_to, status="Cancelled")
return set_status(doctype, name, "", assign_to, status="Cancelled")
def set_status(doctype, name, assign_to, status="Cancelled"):
def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"):
"""remove from todo"""
try:
todo = frappe.db.get_value(
"ToDo",
{
"reference_type": doctype,
"reference_name": name,
"allocated_to": assign_to,
"status": ("!=", status),
},
)
if not todo:
todo = frappe.db.get_value(
"ToDo",
{
"reference_type": doctype,
"reference_name": name,
"allocated_to": assign_to,
"status": ("!=", status),
},
)
if todo:
todo = frappe.get_doc("ToDo", todo)
todo.status = status
@ -197,13 +198,17 @@ def clear(doctype, name):
Clears assignments, return False if not assigned.
"""
assignments = frappe.get_all(
"ToDo", fields=["allocated_to"], filters=dict(reference_type=doctype, reference_name=name)
"ToDo",
fields=["allocated_to", "name"],
filters=dict(reference_type=doctype, reference_name=name),
)
if not assignments:
return False
for assign_to in assignments:
set_status(doctype, name, assign_to.allocated_to, "Cancelled")
set_status(
doctype, name, todo=assign_to.name, assign_to=assign_to.allocated_to, status="Cancelled"
)
return True

View file

@ -203,7 +203,7 @@ class SendMailContext:
# Note: smtp session will have to be manually closed
self.retain_smtp_session = bool(smtp_server_instance)
self.sent_to = [rec.recipient for rec in self.queue_doc.recipients if rec.is_main_sent()]
self.sent_to = [rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent()]
def __enter__(self):
self.queue_doc.update_status(status="Sending", commit=True)
@ -213,7 +213,6 @@ class SendMailContext:
exceptions = [
smtplib.SMTPServerDisconnected,
smtplib.SMTPAuthenticationError,
smtplib.SMTPRecipientsRefused,
smtplib.SMTPConnectError,
smtplib.SMTPHeloError,
JobTimeoutException,

View file

@ -11,7 +11,7 @@ class EmailQueueRecipient(Document):
def is_mail_to_be_sent(self):
return self.status == "Not Sent"
def is_main_sent(self):
def is_mail_sent(self):
return self.status == "Sent"
def update_db(self, commit=False, **kwargs):

View file

@ -9,6 +9,7 @@ import json
import poplib
import re
import time
from contextlib import suppress
from email.header import decode_header
import _socket
@ -37,7 +38,7 @@ from frappe.utils.html_utils import clean_email_html
from frappe.utils.user import is_system_user
# fix due to a python bug in poplib that limits it to 2048
poplib._MAXLINE = 20480
poplib._MAXLINE = 1_00_000
THREAD_ID_PATTERN = re.compile(r"(?<=\[)[\w/-]+")
WORDS_PATTERN = re.compile(r"\w+")
@ -51,10 +52,6 @@ class EmailTimeoutError(frappe.ValidationError):
pass
class TotalSizeExceededError(frappe.ValidationError):
pass
class LoginLimitExceeded(frappe.ValidationError):
pass
@ -67,26 +64,11 @@ class EmailServer:
"""Wrapper for POP server to pull emails."""
def __init__(self, args=None):
self.setup(args)
def setup(self, args=None):
# overrride
self.settings = args or frappe._dict()
def check_mails(self):
# overrride
return True
def process_message(self, mail):
# overrride
pass
def connect(self):
"""Connect to **Email Account**."""
if cint(self.settings.use_imap):
return self.connect_imap()
else:
return self.connect_pop()
return self.connect_imap() if cint(self.settings.use_imap) else self.connect_pop()
def connect_imap(self):
"""Connect to IMAP"""
@ -150,7 +132,6 @@ class EmailServer:
return True
except _socket.error:
# log performs rollback and logs error in Error Log
frappe.log_error("POP: Unable to connect")
# Invalid mail server -- due to refusing connection
@ -177,66 +158,33 @@ class EmailServer:
return
def get_messages(self, folder="INBOX"):
"""Returns new email messages in a list."""
if not (self.check_mails() or self.connect()):
return []
"""Returns new email messages."""
frappe.db.commit()
self.latest_messages = []
self.seen_status = {}
self.uid_reindexed = False
uid_list = []
email_list = self.get_new_mails(folder)
try:
# track if errors arised
self.errors = False
self.latest_messages = []
self.seen_status = {}
self.uid_reindexed = False
uid_list = email_list = self.get_new_mails(folder)
if not email_list:
return
num = num_copy = len(email_list)
# WARNING: Hard coded max no. of messages to be popped
if num > 50:
num = 50
# size limits
self.total_size = 0
self.max_email_size = cint(frappe.local.conf.get("max_email_size"))
self.max_total_size = 5 * self.max_email_size
for i, message_meta in enumerate(email_list[:num]):
try:
self.retrieve_message(message_meta, i + 1)
except (TotalSizeExceededError, EmailTimeoutError, LoginLimitExceeded):
break
# WARNING: Mark as read - message number 101 onwards from the pop list
# This is to avoid having too many messages entering the system
num = num_copy
if not cint(self.settings.use_imap):
if num > 100 and not self.errors:
for m in range(101, num + 1):
self.pop.dele(m)
except Exception as e:
if not self.has_login_limit_exceeded(e):
raise
for i, uid in enumerate(email_list[:100]):
try:
self.retrieve_message(uid, i + 1)
except (EmailTimeoutError, LoginLimitExceeded):
# get whatever messages were retrieved
break
out = {"latest_messages": self.latest_messages}
if self.settings.use_imap:
out.update(
{"uid_list": uid_list, "seen_status": self.seen_status, "uid_reindexed": self.uid_reindexed}
{"uid_list": email_list, "seen_status": self.seen_status, "uid_reindexed": self.uid_reindexed}
)
return out
def get_new_mails(self, folder):
"""Return list of new mails"""
email_list = []
if cint(self.settings.use_imap):
email_list = []
self.check_imap_uidvalidity(folder)
readonly = False if self.settings.email_sync_rule == "UNSEEN" else True
@ -294,9 +242,6 @@ class EmailServer:
self.settings.email_sync_rule = f"UID {from_uid}:{uidnext}"
self.uid_reindexed = True
elif uid_validity == current_uid_validity:
return
def parse_imap_response(self, cmd, response):
pattern = rf"(?<={cmd} )[0-9]*"
match = re.search(pattern, response.decode("utf-8"), re.U | re.I)
@ -306,49 +251,28 @@ class EmailServer:
else:
return None
def retrieve_message(self, message_meta, msg_num=None):
incoming_mail = None
def retrieve_message(self, uid, msg_num):
try:
self.validate_message_limits(message_meta)
if cint(self.settings.use_imap):
status, message = self.imap.uid("fetch", message_meta, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)")
status, message = self.imap.uid("fetch", uid, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)")
raw = message[0]
self.get_email_seen_status(message_meta, raw[0])
self.get_email_seen_status(uid, raw[0])
self.latest_messages.append(raw[1])
else:
msg = self.pop.retr(msg_num)
self.latest_messages.append(b"\n".join(msg[1]))
except (TotalSizeExceededError, EmailTimeoutError):
except EmailTimeoutError:
# propagate this error to break the loop
self.errors = True
raise
except Exception as e:
if self.has_login_limit_exceeded(e):
self.errors = True
raise LoginLimitExceeded(e)
else:
# log performs rollback and logs error in Error Log
frappe.log_error("Unable to fetch email", self.make_error_msg(msg_num, incoming_mail))
self.errors = True
frappe.db.rollback()
frappe.log_error("Unable to fetch email", self.make_error_msg(uid, msg_num))
if not cint(self.settings.use_imap):
self.pop.dele(msg_num)
else:
# mark as seen if email sync rule is UNSEEN (syncing only unseen mails)
if self.settings.email_sync_rule == "UNSEEN":
self.imap.uid("STORE", message_meta, "+FLAGS", "(\\SEEN)")
else:
if not cint(self.settings.use_imap):
self.pop.dele(msg_num)
else:
# mark as seen if email sync rule is UNSEEN (syncing only unseen mails)
if self.settings.email_sync_rule == "UNSEEN":
self.imap.uid("STORE", message_meta, "+FLAGS", "(\\SEEN)")
self._post_retrieve_cleanup(uid, msg_num)
def get_email_seen_status(self, uid, flag_string):
"""parse the email FLAGS response"""
@ -368,6 +292,15 @@ class EmailServer:
def has_login_limit_exceeded(self, e):
return "-ERR Exceeded the login limit" in strip(cstr(e))
def _post_retrieve_cleanup(self, uid, msg_num):
with suppress(Exception):
if not cint(self.settings.use_imap):
self.pop.dele(msg_num)
else:
# mark as seen if email sync rule is UNSEEN (syncing only unseen mails)
if self.settings.email_sync_rule == "UNSEEN":
self.imap.uid("STORE", uid, "+FLAGS", "(\\SEEN)")
def is_temporary_system_problem(self, e):
messages = (
"-ERR [SYS/TEMP] Temporary system problem. Please try again later.",
@ -378,36 +311,28 @@ class EmailServer:
return True
return False
def validate_message_limits(self, message_meta):
# throttle based on email size
if not self.max_email_size:
return
def make_error_msg(self, uid, msg_num):
partial_mail = None
traceback = frappe.get_traceback(with_context=True)
with suppress(Exception):
# retrieve headers
if not cint(self.settings.use_imap):
headers = b"\n".join(self.pop.top(msg_num, 5)[1])
else:
headers = self.imap.uid("fetch", uid, "(BODY.PEEK[HEADER])")[1][0][1]
m, size = message_meta.split()
size = cint(size)
partial_mail = Email(headers)
if size < self.max_email_size:
self.total_size += size
if self.total_size > self.max_total_size:
raise TotalSizeExceededError
else:
raise EmailSizeExceededError
def make_error_msg(self, msg_num, incoming_mail):
error_msg = "Error in retrieving email."
if not incoming_mail:
try:
# retrieve headers
incoming_mail = Email(b"\n".join(self.pop.top(msg_num, 5)[1]))
except Exception:
pass
if incoming_mail:
error_msg += "\nDate: {date}\nFrom: {from_email}\nSubject: {subject}\n".format(
date=incoming_mail.date, from_email=incoming_mail.from_email, subject=incoming_mail.subject
if partial_mail:
return (
"\nDate: {date}\nFrom: {from_email}\nSubject: {subject}\n\n\nTraceback: \n{traceback}".format(
date=partial_mail.date,
from_email=partial_mail.from_email,
subject=partial_mail.subject,
traceback=traceback,
)
)
return error_msg
return traceback
def update_flag(self, folder, uid_list=None):
"""set all uids mails the flag as seen"""

View file

@ -83,6 +83,11 @@ on_logout = (
"frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults"
)
# PDF
pdf_header_html = "frappe.utils.pdf.pdf_header_html"
pdf_body_html = "frappe.utils.pdf.pdf_body_html"
pdf_footer_html = "frappe.utils.pdf.pdf_footer_html"
# permissions
permission_query_conditions = {

View file

@ -111,6 +111,7 @@ def authorize_access(g_calendar, reauthorize=None):
"""
google_settings = frappe.get_doc("Google Settings")
google_calendar = frappe.get_doc("Google Calendar", g_calendar)
google_calendar.check_permission("write")
redirect_uri = (
get_request_site_address(True)

View file

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "format:GC-{email_id}",
"creation": "2019-06-14 00:09:39.441961",
"doctype": "DocType",
@ -97,10 +98,12 @@
"label": "Push to Google Contacts"
}
],
"modified": "2020-09-18 17:26:09.703215",
"links": [],
"modified": "2023-03-30 11:25:48.832384",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Contacts",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
@ -116,17 +119,14 @@
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"if_owner": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View file

@ -36,10 +36,10 @@ def authorize_access(g_contact, reauthorize=False, code=None):
If no Authorization code get it from Google and then request for Refresh Token.
Google Contact Name is set to flags to set_value after Authorization Code is obtained.
"""
contact = frappe.get_doc("Google Contacts", g_contact)
contact.check_permission("write")
oauth_code = (
frappe.db.get_value("Google Contacts", g_contact, "authorization_code") if not code else code
)
oauth_code = code or contact.get_password("authorization_code")
oauth_obj = GoogleOAuth("contacts")
if not oauth_code or reauthorize:
@ -51,11 +51,9 @@ def authorize_access(g_contact, reauthorize=False, code=None):
)
r = oauth_obj.authorize(oauth_code)
frappe.db.set_value(
"Google Contacts",
g_contact,
{"authorization_code": oauth_code, "refresh_token": r.get("refresh_token")},
)
contact.authorization_code = oauth_code
contact.refresh_token = r.get("refresh_token")
contact.save()
def get_google_contacts_object(g_contact):

View file

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestGoogleContacts(FrappeTestCase):
pass

View file

@ -160,7 +160,7 @@ def sync_customizations_for_doctype(data: dict, folder: str, filename: str = "")
if not frappe.db.exists("DocType", doctype):
print(_("DocType {0} does not exist.").format(doctype))
print(_("Skipping fixture syncing for doctyoe {0} from file {1} ").format(doctype, filename))
print(_("Skipping fixture syncing for doctype {0} from file {1}").format(doctype, filename))
return
if data["custom_fields"]:

View file

@ -3,7 +3,9 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat
def execute():
if not frappe.get_value("Email Account", {"auth_method": "OAuth"}):
if frappe.get_all(
"Email Account", {"auth_method": "OAuth", "connected_user": ["is", "set"]}, limit=1
):
return
# Setting awaiting password to 1 for email accounts where Oauth is enabled.

View file

@ -237,8 +237,14 @@
<path d="M2.5 3.5h2m7 9h2m-10-6h6m-3 3h6" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"></path>
</symbol>
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-filter">
<path d="M2 4h12M4 8h8m-5.5 4h3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path>
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-filter">
<path stroke-width="1.2" d="M3.68016 3L15.4502 3C16.1 3 16.4787 3.73367 16.1023 4.26337L11.6585 10.5177C11.5383 10.6869 11.4737 10.8893 11.4737 11.0969L11.4737 16.4053C11.4737 16.6516 11.1934 16.7929 10.9954 16.6466L8.72152 14.9665C8.46635 14.7779 8.31579 14.4795 8.31579 14.1622L8.31579 11.1327C8.31579 10.9031 8.2368 10.6805 8.09208 10.5023L3.05913 4.3043C2.63456 3.78145 3.00664 3 3.68016 3Z" stroke="var(--icon-stroke)" stroke-linecap="round"/>
</symbol>
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-filter-x">
<path stroke-width="1.2" d="M8.5 3L3.66449 3.00002C2.99369 3.00002 2.62075 3.77596 3.0398 4.29977L8.15768 10.6971C8.29953 10.8744 8.37681 11.0947 8.37681 11.3218L8.37681 14.4565C8.37681 14.7713 8.525 15.0677 8.77681 15.2565L11.0852 16.9878C11.283 17.1362 11.5652 16.9951 11.5652 16.7478L11.5652 11.3742C11.5652 11.1155 11.6654 10.8669 11.8448 10.6806L12.5 10" stroke="var(--icon-stroke)" stroke-linecap="round"/>
<path stroke-width="1.2" d="M11 3L16 8" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path stroke-width="1.2" d="M16 3L11 8" 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-list">

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View file

@ -24,8 +24,8 @@
</div>
<div class="flex config-area">
<label v-if="is_optimizable" class="frappe-checkbox"><input type="checkbox" :checked="optimize" @change="emit('toggle_optimize')">Optimize</label>
<label class="frappe-checkbox"><input type="checkbox" :checked="file.private" @change="emit('toggle_private')">Private</label>
<label v-if="is_optimizable" class="frappe-checkbox"><input type="checkbox" :checked="optimize" @change="emit('toggle_optimize')">{{ __('Optimize') }}</label>
<label class="frappe-checkbox"><input type="checkbox" :checked="file.private" @change="emit('toggle_private')">{{ __('Private') }}</label>
</div>
<div>
<span v-if="file.error_message" class="file-error text-danger">

View file

@ -6,6 +6,7 @@ frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlI
else {
let value = this.get_input_value();
this.parse_validate_and_set_in_model(value, e);
this.refresh();
}
};
// convert to number format on focusout since focus converts it to flt.

View file

@ -35,6 +35,7 @@ frappe.form.formatters = {
},
Data: function (value, df) {
if (df && df.options == "URL") {
if (!value) return;
return `<a href="${value}" title="Open Link" target="_blank">${value}</a>`;
}
value = value == null ? "" : value;

View file

@ -831,22 +831,31 @@ class FilterArea {
make_filter_list() {
$(`<div class="filter-selector">
<button class="btn btn-default btn-sm filter-button">
<span class="filter-icon">
${frappe.utils.icon("filter")}
</span>
<span class="button-label hidden-xs">
<div class="btn-group">
<button class="btn btn-default btn-sm filter-button">
<span class="filter-icon">
${frappe.utils.icon("filter")}
</span>
<span class="button-label hidden-xs">
${__("Filter")}
<span>
</button>
<span>
</button>
<button class="btn btn-default btn-sm filter-x-button" title="${__("Clear all filters")}">
<span class="filter-icon">
${frappe.utils.icon("filter-x")}
</span>
</button>
</div>
</div>`).appendTo(this.$filter_list_wrapper);
this.filter_button = this.$filter_list_wrapper.find(".filter-button");
this.filter_x_button = this.$filter_list_wrapper.find(".filter-x-button");
this.filter_list = new frappe.ui.FilterGroup({
base_list: this.list_view,
parent: this.$filter_list_wrapper,
doctype: this.list_view.doctype,
filter_button: this.filter_button,
filter_x_button: this.filter_x_button,
default_filters: [],
on_change: () => this.refresh_list_view(),
});

View file

@ -256,19 +256,16 @@ frappe.views.ListSidebar = class ListSidebar {
this.insights_banner.remove();
}
const message = "Get more insights from your data with Frappe Insights.";
const message = "Get more insights with";
const link = "https://frappe.io/s/insights";
const cta = "Get Frappe Insights";
const cta = "Frappe Insights";
this.insights_banner = $(`
<div style="position: relative;">
<div class="">
${message}
<div class="pr-3">
${message} <a href="${link}" target="_blank" style="color: var(--primary-color)">${cta} &rarr; </a>
</div>
<div class="mt-2">
<a href="${link}" target="_blank" style="color: var(--primary-color)">${cta} -> </a>
</div>
<div style="position: absolute; top: 0px; right: 0px; cursor: pointer;" title="Dismiss"
<div style="position: absolute; top: -1px; right: -4px; cursor: pointer;" title="Dismiss"
onclick="localStorage.setItem('show_insights_banner', 'false') || this.parentElement.remove()">
<svg class="icon icon-sm" style="">
<use class="" href="#icon-close"></use>

View file

@ -348,11 +348,9 @@ $.extend(frappe.model, {
},
unscrub: function (txt) {
return __(txt || "")
.replace(/-|_/g, " ")
.replace(/\w*/g, function (keywords) {
return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();
});
return (txt || "").replace(/-|_/g, " ").replace(/\w*/g, function (keywords) {
return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();
});
},
can_create: function (doctype) {

View file

@ -53,7 +53,7 @@
<div class="static-area ellipsis">{{ __("Query") }}</div>
</div>
<div class="col grid-static-col col-xs-2">
<div class="static-area ellipsis text-right">{{ __("Duration (ms)") }}"</div>
<div class="static-area ellipsis text-right">{{ __("Duration (ms)") }}</div>
</div>
<div class="col grid-static-col col-xs-2">
<div class="static-area ellipsis text-right">{{ __("Exact Copies") }}</div>

View file

@ -59,7 +59,7 @@ frappe.RoleEditor = class {
const $body = $(this.perm_dialog.body);
if (!permissions.length) {
$body.append(`<div class="text-muted text-center padding">
${__("{0} role does not have permission on any doctype", [role])}
${__("{0} role does not have permission on any doctype", [__(role)])}
</div>`);
} else {
$body.append(`
@ -68,7 +68,7 @@ frappe.RoleEditor = class {
<tr>
<th> ${__("Document Type")} </th>
<th> ${__("Level")} </th>
${frappe.perm.rights.map((p) => `<th> ${frappe.unscrub(p)}</th>`).join("")}
${frappe.perm.rights.map((p) => `<th> ${__(frappe.unscrub(p))}</th>`).join("")}
</tr>
</thead>
<tbody></tbody>
@ -77,7 +77,7 @@ frappe.RoleEditor = class {
permissions.forEach((perm) => {
$body.find("tbody").append(`
<tr>
<td>${perm.parent}</td>
<td>${__(perm.parent)}</td>
<td>${perm.permlevel}</td>
${frappe.perm.rights
.map(
@ -91,7 +91,7 @@ frappe.RoleEditor = class {
`);
});
}
this.perm_dialog.set_title(role);
this.perm_dialog.set_title(__(role));
this.perm_dialog.show();
});
}
@ -102,8 +102,10 @@ frappe.RoleEditor = class {
this.perm_dialog.$wrapper
.find(".modal-dialog")
.css("width", "1200px")
.css("max-width", "80vw");
.css("width", "auto")
.css("max-width", "1200px");
this.perm_dialog.$wrapper.find(".modal-body").css("overflow", "overlay");
}
show() {
this.reset();

View file

@ -203,7 +203,7 @@ frappe.router = {
? meta.default_view
: null
);
} else if (route[1] && route[1] !== "view" && !route[2]) {
} else if (route[1] && route[1] !== "view") {
let docname = route[1];
if (route.length > 2) {
docname = route.slice(1).join("/");

View file

@ -14,9 +14,31 @@ frappe.ui.FilterGroup = class {
make_popover() {
this.init_filter_popover();
this.set_clear_all_filters_event();
this.set_popover_events();
}
set_clear_all_filters_event() {
if (!this.filter_x_button) return;
this.filter_x_button.on("click", () => {
this.toggle_empty_filters(true);
if (typeof this.base_list !== "undefined") {
// It's a list view. Clear all the filters, also the ones in the
// FilterArea outside this FilterGroup
this.base_list.filter_area.clear();
} else {
// Not a list view, just clear the filters in this FilterGroup
this.clear_filters();
}
this.update_filter_button();
});
}
hide_popover() {
this.filter_button.popover("hide");
}
init_filter_popover() {
this.filter_button.popover({
content: this.get_filter_area_template(),
@ -54,7 +76,7 @@ frappe.ui.FilterGroup = class {
!$(e.target).is(this.filter_button) &&
!in_datepicker
) {
this.wrapper && this.filter_button.popover("hide");
this.wrapper && this.hide_popover();
}
}
});
@ -85,7 +107,7 @@ frappe.ui.FilterGroup = class {
// REDESIGN-TODO: (Temporary) Review and find best solution for this
frappe.router.on("change", () => {
if (this.wrapper && this.wrapper.is(":visible")) {
this.filter_button.popover("hide");
this.hide_popover();
}
});
}
@ -130,11 +152,10 @@ frappe.ui.FilterGroup = class {
this.toggle_empty_filters(true);
this.clear_filters();
this.on_change();
this.hide_popover();
});
this.wrapper.find(".apply-filters").on("click", () => {
this.filter_button.popover("hide");
});
this.wrapper.find(".apply-filters").on("click", () => this.hide_popover());
}
add_filters(filters) {

View file

@ -137,11 +137,13 @@ frappe.ui.toolbar.Toolbar = class {
__("Generate Tracking URL")
);
if (frappe.perm.has_perm("RQ Job")) {
frappe.search.utils.make_function_searchable(function () {
frappe.set_route("List", "RQ Job");
}, __("Background Jobs"));
}
frappe.model.with_doctype("RQ Job").then(() => {
if (frappe.perm.has_perm("RQ Job", 0, "read")) {
frappe.search.utils.make_function_searchable(function () {
frappe.set_route("List", "RQ Job");
}, __("Background Jobs"));
}
});
}
}

View file

@ -339,7 +339,7 @@ frappe.views.CommunicationComposer = class {
await this.dialog.set_value(fieldname, this[fieldname] || "");
}
const subject = frappe.utils.html2text(this.subject) || "";
const subject = this.subject ? frappe.utils.html2text(this.subject) : "";
await this.dialog.set_value("subject", subject);
await this.set_values_from_last_edited_communication();

View file

@ -1424,9 +1424,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
const applied_filters = this.get_filter_values();
return Object.keys(applied_filters)
.map((fieldname) => {
const label = frappe.query_report.get_filter(fieldname).df.label;
const docfield = frappe.query_report.get_filter(fieldname).df;
const value = applied_filters[fieldname];
return `<h6>${__(label)}: ${value}</h6>`;
return `<h6>${__(docfield.label)}: ${frappe.format(value, docfield)}</h6>`;
})
.join("");
}

View file

@ -1349,9 +1349,8 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
.map((f) => {
const [doctype, fieldname, condition, value] = f;
if (condition !== "=") return "";
const label = frappe.meta.get_label(doctype, fieldname);
return `<h6>${__(label)}: ${value}</h6>`;
const docfield = frappe.meta.get_docfield(doctype, fieldname);
return `<h6>${__(docfield.label)}: ${frappe.format(value, docfield)}</h6>`;
})
.join("");
}

View file

@ -158,7 +158,7 @@ frappe.views.Workspace = class Workspace {
}
if (
sidebar_section.find("sidebar-item-container").length &&
sidebar_section.find(".sidebar-item-container").length &&
sidebar_section.find("> [item-is-hidden='0']").length == 0
) {
sidebar_section.addClass("hidden show-in-edit-mode");

View file

@ -20,6 +20,7 @@ export default class ShortcutWidget extends Widget {
restrict_to_domain: this.restrict_to_domain,
stats_filter: this.stats_filter,
type: this.type,
url: this.url,
};
}
@ -45,6 +46,16 @@ export default class ShortcutWidget extends Widget {
frappe.open_in_new_tab = true;
}
if (this.type == "URL") {
if (frappe.open_in_new_tab) {
window.open(this.url, "_blank");
frappe.open_in_new_tab = false;
} else {
window.location.href = this.url;
}
return;
}
frappe.set_route(route);
});
}

View file

@ -350,7 +350,7 @@ class ShortcutDialog extends WidgetDialog {
fieldname: "type",
label: "Type",
reqd: 1,
options: "DocType\nReport\nPage\nDashboard",
options: "DocType\nReport\nPage\nDashboard\nURL",
onchange: () => {
if (this.dialog.get_value("type") == "DocType") {
this.dialog.fields_dict.link_to.get_query = () => {
@ -379,7 +379,6 @@ class ShortcutDialog extends WidgetDialog {
fieldtype: "Dynamic Link",
fieldname: "link_to",
label: "Link To",
reqd: 1,
options: "type",
onchange: () => {
const doctype = this.dialog.get_value("link_to");
@ -404,6 +403,17 @@ class ShortcutDialog extends WidgetDialog {
this.hide_filters();
}
},
depends_on: (s) => s.type != "URL",
mandatory_depends_on: (s) => s.type != "URL",
},
{
fieldtype: "Data",
fieldname: "url",
label: "URL",
options: "URL",
default: "",
depends_on: (s) => s.type == "URL",
mandatory_depends_on: (s) => s.type == "URL",
},
{
fieldtype: "Select",
@ -500,6 +510,19 @@ class ShortcutDialog extends WidgetDialog {
data.label = data.label ? data.label : frappe.model.unscrub(data.link_to);
if (data.url) {
!validate_url(data.url) &&
frappe.throw({
message: __("<b>{0}</b> is not a valid URL", [data.url]),
title: __("Invalid URL"),
indicator: "red",
});
if (!data.label) {
data.label = "No Label (URL)";
}
}
return data;
}
}

View file

@ -130,7 +130,7 @@ onMounted(() => {
margin-top: auto;
margin-bottom: 1.2rem;
}
.preview-control >>> .form-control {
.preview-control :deep(.form-control) {
background: var(--control-bg-on-gray);
}
</style>

View file

@ -332,7 +332,7 @@ watch(print_format, () => (store.dirty.value = true), { deep: true });
margin-bottom: 0;
}
.control-font >>> .frappe-control[data-fieldname="font"] label {
.control-font :deep(.frappe-control[data-fieldname="font"] label) {
display: none;
}
</style>

View file

@ -1,7 +1,5 @@
.filter-icon.active {
use {
stroke: var(--text-on-blue);
}
--icon-stroke: var(--text-on-blue);
}
.filter-popover {

View file

@ -378,13 +378,14 @@ input.list-check-all {
padding: 0 var(--padding-xs);
}
.filter-button {
margin: 5px;
// padding: 4px 8px;
.filter-selector .btn-group {
margin: var(--margin-xs);
}
.filter-button.btn-primary-light {
color: var(--text-on-blue);
outline: 1px solid var(--bg-dark-blue);
z-index: 1;
}
.sort-selector {

View file

@ -126,7 +126,7 @@ def can_subscribe_doctype(doctype: str) -> bool:
def get_user_info():
return {
"user": frappe.session.user,
"user_type": frappe.session.user_type,
"user_type": frappe.session.data.user_type,
}

View file

@ -2290,8 +2290,8 @@ Setup Auto Email,Einstellungen Auto E-Mail,
Setup Complete,Einrichtung abgeschlossen,
Setup Notifications based on various criteria.,Setup Benachrichtigungen basierend auf verschiedenen Kriterien.,
Setup Reports to be emailed at regular intervals,Berichte regelmäßig per E-Mail senden,
"Setup of top navigation bar, footer and logo.","Einrichten der oberen Navigationsleiste, der Fußzeile und des Logos",
Share,Aktie,
"Setup of top navigation bar, footer and logo.","Einrichten der oberen Navigationsleiste, der Fußzeile und des Logos",
Share,Freigeben,
Share URL,URL teilen,
Share With,Freigeben für,
Share this document with,Dieses Dokument teilen mit,
@ -4840,3 +4840,4 @@ Non-numeric,Nicht-numerische,
Minimal,Minimal,
This value is fetched from {0}'s {1} field,Dieser Wert ergibt sich aus dem Feld {1} von {0},
This form is not editable due to a Workflow.,Dieses Formular kann in diesem Workflow-Status nicht bearbeitet werden.,
{0} role does not have permission on any doctype,Die Rolle {0} hat auf keinen DocType Zugriff,

1 A4 A4
2290 Setup Complete Einrichtung abgeschlossen
2291 Setup Notifications based on various criteria. Setup Benachrichtigungen basierend auf verschiedenen Kriterien.
2292 Setup Reports to be emailed at regular intervals Berichte regelmäßig per E-Mail senden
2293 Setup of top navigation bar, footer and logo. Einrichten der oberen Navigationsleiste, der Fußzeile und des Logos Einrichten der oberen Navigationsleiste, der Fußzeile und des Logos
2294 Share Aktie Freigeben
2295 Share URL URL teilen
2296 Share With Freigeben für
2297 Share this document with Dieses Dokument teilen mit
4840 Minimal Minimal
4841 This value is fetched from {0}'s {1} field Dieser Wert ergibt sich aus dem Feld {1} von {0}
4842 This form is not editable due to a Workflow. Dieses Formular kann in diesem Workflow-Status nicht bearbeitet werden.
4843 {0} role does not have permission on any doctype Die Rolle {0} hat auf keinen DocType Zugriff

View file

@ -23,6 +23,31 @@ PDF_CONTENT_ERRORS = [
]
def pdf_header_html(soup, head, content, styles, html_id, css):
return frappe.render_template(
"templates/print_formats/pdf_header_footer.html",
{
"head": head,
"content": content,
"styles": styles,
"html_id": html_id,
"css": css,
"lang": frappe.local.lang,
"layout_direction": "rtl" if is_rtl() else "ltr",
},
)
def pdf_body_html(template, args, **kwargs):
return template.render(args, filters={"len": len})
def pdf_footer_html(soup, head, content, styles, html_id, css):
return pdf_header_html(
soup=soup, head=head, content=content, styles=styles, html_id=html_id, css=css
)
def get_pdf(html, options=None, output: PdfWriter | None = None):
html = scrub_urls(html)
html, options = prepare_options(html, options)
@ -196,17 +221,15 @@ def prepare_header_footer(soup):
tag.extract()
toggle_visible_pdf(content)
html = frappe.render_template(
"templates/print_formats/pdf_header_footer.html",
{
"head": head,
"content": content,
"styles": styles,
"html_id": html_id,
"css": css,
"lang": frappe.local.lang,
"layout_direction": "rtl" if is_rtl() else "ltr",
},
id_map = {"header-html": "pdf_header_html", "footer-html": "pdf_footer_html"}
hook_func = frappe.get_hooks(id_map.get(html_id))
html = frappe.get_attr(hook_func[-1])(
soup=soup,
head=head,
content=content,
styles=styles,
html_id=html_id,
css=css,
)
# create temp file

View file

@ -17,7 +17,9 @@ from frappe.www.printview import validate_print_permission
@frappe.whitelist()
def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=None):
def download_multi_pdf(
doctype, name, format=None, no_letterhead=False, letterhead=None, options=None
):
"""
Concatenate multiple docs as PDF .
@ -76,6 +78,7 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=
as_pdf=True,
output=output,
no_letterhead=no_letterhead,
letterhead=letterhead,
pdf_options=options,
)
frappe.local.response.filename = "{doctype}.pdf".format(
@ -92,6 +95,7 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=
as_pdf=True,
output=output,
no_letterhead=no_letterhead,
letterhead=letterhead,
pdf_options=options,
)
except Exception:

View file

@ -68,7 +68,7 @@
<h1 class="ellipsis">{{ _(title) }}</h1>
{% endif %}
</div>
<span class="indicator-pill orange hide">Not Saved</span>
<span class="indicator-pill orange hide">{{ _("Not Saved") }}</span>
<div class="web-form-actions">
{{ header_buttons() }}
</div>

View file

@ -42,6 +42,12 @@ frappe.ui.form.on("Web Form", {
render_list_settings_message(frm);
},
anonymous: function (frm) {
if (frm.doc.anonymous) {
frm.set_value("login_required", 0);
}
},
validate: function (frm) {
if (!frm.doc.login_required) {
frm.set_value("allow_multiple", 0);

View file

@ -9,7 +9,7 @@
"title",
"route",
"published",
"column_break_1",
"column_break_vdhm",
"doc_type",
"module",
"is_standard",
@ -21,6 +21,7 @@
"allow_multiple",
"allow_edit",
"allow_delete",
"anonymous",
"column_break_2",
"apply_document_permissions",
"allow_print",
@ -96,10 +97,12 @@
"default": "0",
"fieldname": "published",
"fieldtype": "Check",
"hidden": 1,
"label": "Published"
},
{
"default": "0",
"depends_on": "eval:!doc.anonymous",
"fieldname": "login_required",
"fieldtype": "Check",
"label": "Login Required"
@ -301,6 +304,7 @@
{
"collapsible": 1,
"collapsible_depends_on": "show_list",
"depends_on": "eval:!doc.anonymous",
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"label": "List Settings"
@ -308,6 +312,7 @@
{
"collapsible": 1,
"collapsible_depends_on": "show_sidebar",
"depends_on": "eval:!doc.anonymous",
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"label": "Sidebar Settings"
@ -358,13 +363,24 @@
"fieldname": "meta_image",
"fieldtype": "Attach Image",
"label": "Meta Image"
},
{
"fieldname": "column_break_vdhm",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Receive anonymous response",
"fieldname": "anonymous",
"fieldtype": "Check",
"label": "Anonymous"
}
],
"has_web_view": 1,
"icon": "icon-edit",
"is_published_field": "published",
"links": [],
"modified": "2023-01-02 10:19:15.680960",
"modified": "2023-04-20 17:24:42.657731",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form",

View file

@ -387,6 +387,10 @@ def accept(web_form, data):
web_form = frappe.get_doc("Web Form", web_form)
doctype = web_form.doc_type
user = frappe.session.user
if web_form.anonymous and frappe.session.user != "Guest":
frappe.session.user = "Guest"
if data.name and not web_form.allow_edit:
frappe.throw(_("You are not allowed to update this Web Form Document"))
@ -468,6 +472,9 @@ def accept(web_form, data):
if f:
remove_file_by_url(f, doctype=doctype, name=doc.name)
if web_form.anonymous and frappe.session.user == "Guest" and user:
frappe.session.user = user
frappe.flags.web_form_doc = doc
return doc

View file

@ -208,8 +208,10 @@ def get_rendered_template(
"print_settings": print_settings,
}
)
html = template.render(args, filters={"len": len})
hook_func = frappe.get_hooks("pdf_body_html")
html = frappe.get_attr(hook_func[-1])(
jenv=jenv, template=template, print_format=print_format, args=args
)
if cint(trigger_print):
html += trigger_print_script