Merge branch 'develop' into fix-20689

This commit is contained in:
gavin 2023-04-27 14:38:32 +05:30 committed by GitHub
commit c4ecd33357
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 283 additions and 115 deletions

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

@ -4,7 +4,24 @@
frappe.ui.form.on("Document Naming Rule", {
refresh: function (frm) {
frm.trigger("document_type");
if (!frm.doc.__islocal) frm.trigger("add_update_counter_button");
frm.last_counter_value = frm.doc.counter;
frm.skip_before_save = false;
},
before_save: function (frm) {
if (frm.is_new() || frm.skip_before_save || frm.last_counter_value === frm.doc.counter)
return;
frappe.validated = false;
frappe.warn(
__("Are you sure?"),
__("Updating counter may lead to document name conflicts if not done properly"),
() => {
frm.skip_before_save = true;
frm.save();
},
__("Proceed"),
false
);
},
document_type: (frm) => {
// update the select field options with fieldnames
@ -26,45 +43,4 @@ frappe.ui.form.on("Document Naming Rule", {
});
}
},
add_update_counter_button: (frm) => {
frm.add_custom_button(__("Update Counter"), function () {
const fields = [
{
fieldtype: "Data",
fieldname: "new_counter",
label: __("New Counter"),
default: frm.doc.counter,
reqd: 1,
description: __(
"Warning: Updating counter may lead to document name conflicts if not done properly"
),
},
];
let primary_action_label = __("Save");
let primary_action = (fields) => {
frappe.call({
method: "frappe.core.doctype.document_naming_rule.document_naming_rule.update_current",
args: {
name: frm.doc.name,
new_counter: fields.new_counter,
},
callback: function () {
frm.set_value("counter", fields.new_counter);
dialog.hide();
},
});
};
const dialog = new frappe.ui.Dialog({
title: __("Update Counter Value for Prefix: {0}", [frm.doc.prefix]),
fields,
primary_action_label,
primary_action,
});
dialog.show();
});
},
});

View file

@ -12,8 +12,9 @@
"conditions",
"naming_section",
"prefix",
"prefix_digits",
"counter"
"counter",
"column_break_xfqa",
"prefix_digits"
],
"fields": [
{
@ -22,7 +23,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Document Type",
"options": "DocType"
"options": "DocType",
"reqd": 1
},
{
"default": "0",
@ -38,11 +40,12 @@
"reqd": 1
},
{
"default": "0",
"description": "Warning: Updating counter may lead to document name conflicts if not done properly",
"fieldname": "counter",
"fieldtype": "Int",
"label": "Counter",
"no_copy": 1,
"read_only": 1
"no_copy": 1
},
{
"default": "5",
@ -76,11 +79,15 @@
"fieldname": "priority",
"fieldtype": "Int",
"label": "Priority"
},
{
"fieldname": "column_break_xfqa",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-13 20:07:47.617615",
"modified": "2023-04-24 15:14:32.054272",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule",
@ -102,6 +109,7 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "document_type",
"track_changes": 1
}

View file

@ -47,9 +47,3 @@ class DocumentNamingRule(Document):
doc.name = naming_series + ("%0" + str(self.prefix_digits) + "d") % (counter + 1)
frappe.db.set_value(self.doctype, self.name, "counter", counter + 1)
@frappe.whitelist()
def update_current(name, new_counter):
frappe.only_for("System Manager")
frappe.db.set_value("Document Naming Rule", name, "counter", new_counter)

View file

@ -76,7 +76,9 @@
"max_auto_email_report_per_user",
"system_updates_section",
"disable_system_update_notification",
"disable_change_log_notification"
"disable_change_log_notification",
"telemetry_section",
"enable_telemetry"
],
"fields": [
{
@ -535,12 +537,24 @@
"fieldname": "disable_document_sharing",
"fieldtype": "Check",
"label": "Disable Document Sharing"
},
{
"collapsible": 1,
"fieldname": "telemetry_section",
"fieldtype": "Section Break",
"label": "Telemetry"
},
{
"default": "1",
"fieldname": "enable_telemetry",
"fieldtype": "Check",
"label": "Allow Sending Usage Data for Improving applications"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2023-03-14 11:30:56.465653",
"modified": "2023-04-23 11:14:59.302851",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

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

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

@ -178,6 +178,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
}
action_on_complete() {
frappe.telemetry.capture("initated_client_side", "setup");
if (!this.current_slide.set_values()) return;
this.update_values();
this.show_working_state();
@ -325,6 +326,7 @@ frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide {
make() {
super.make();
this.set_init_values();
this.setup_telemetry_events();
this.reset_action_button_state();
}
@ -340,6 +342,18 @@ frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide {
});
}
}
setup_telemetry_events() {
let me = this;
this.fields.filter(frappe.model.is_value_type).forEach((field) => {
me.get_input(field.fieldname).on("change", function () {
frappe.telemetry.capture(`${field.fieldname}_set`, "setup");
if (field.fieldname == "enable_telemetry" && !me.get_value("enable_telemetry")) {
frappe.telemetry.disable();
}
});
});
}
};
// Frappe slides settings
@ -384,6 +398,15 @@ frappe.setup.slides_settings = [
fieldtype: "Select",
reqd: 1,
},
{
fieldtype: "Section Break",
},
{
fieldname: "enable_telemetry",
label: __("Allow Sending Usage Data for Improving applications"),
fieldtype: "Check",
default: 1,
},
],
onload: function (slide) {

View file

@ -64,6 +64,9 @@ def setup_complete(args):
@frappe.task()
def process_setup_stages(stages, user_input, is_background_task=False):
from frappe.utils.telemetry import capture
capture("initated_server_side", "setup")
try:
frappe.flags.in_setup_wizard = True
current_task = None
@ -88,6 +91,7 @@ def process_setup_stages(stages, user_input, is_background_task=False):
)
else:
run_setup_success(user_input)
capture("completed_server_side", "setup")
if not is_background_task:
return {"status": "ok"}
frappe.publish_realtime("setup_task", {"status": "ok"}, user=frappe.session.user)
@ -171,6 +175,7 @@ def update_system_settings(args):
"number_format": number_format,
"enable_scheduler": 1 if not frappe.flags.in_test else 0,
"backup_limit": 3, # Default for downloadable backups
"enable_telemetry": cint(args.get("enable_telemetry")),
}
)
system_settings.save()

View file

@ -81,7 +81,15 @@ def search_widget(
try:
is_whitelisted(frappe.get_attr(query))
frappe.response["values"] = frappe.call(
query, doctype, txt, searchfield, start, page_length, filters, as_dict=as_dict
query,
doctype,
txt,
searchfield,
start,
page_length,
filters,
as_dict=as_dict,
reference_doctype=reference_doctype,
)
except frappe.exceptions.PermissionError as e:
if frappe.local.conf.developer_mode:

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

@ -29,6 +29,7 @@ app_include_js = [
"form.bundle.js",
"controls.bundle.js",
"report.bundle.js",
"telemetry.bundle.js",
]
app_include_css = [
"desk.bundle.css",
@ -419,3 +420,7 @@ after_job = [
"frappe.monitor.stop",
"frappe.utils.file_lock.release_document_locks",
]
extend_bootinfo = [
"frappe.utils.telemetry.add_bootinfo",
]

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

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

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

@ -0,0 +1 @@
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);

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

@ -0,0 +1 @@
import "./telemetry/index.js";

View file

@ -0,0 +1,44 @@
import "../lib/posthog.js";
class TelemetryManager {
constructor() {
this.enabled = false;
this.project_id = frappe.boot.posthog_project_id;
this.telemetry_host = frappe.boot.posthog_host;
if (cint(frappe.boot.enable_telemetry) && this.project_id && this.telemetry_host) {
this.enabled = true;
}
}
initialize() {
if (!this.enabled) return;
try {
posthog.init(this.project_id, {
api_host: this.telemetry_host,
autocapture: false,
capture_pageview: false,
capture_pageleave: false,
advanced_disable_decide: true,
});
posthog.identify(frappe.boot.sitename);
} catch (e) {
console.trace("Failed to initialize telemetry", e);
this.enabled = false;
}
}
capture(event, app) {
if (!this.enabled) return;
posthog.capture(`${app}_${event}`);
}
disable() {
this.enabled = false;
posthog.opt_out_capturing();
}
}
frappe.telemetry = new TelemetryManager();
frappe.telemetry.initialize();

View file

@ -130,12 +130,32 @@ class TestSearch(FrappeTestCase):
search_link("User", "user@random", searchfield="name")
self.assertListEqual(frappe.response["results"], [])
def test_reference_doctype(self):
"""search query methods should get reference_doctype if they want"""
search_link(
doctype="User",
txt="",
filters=None,
page_length=20,
reference_doctype="ToDo",
query="frappe.tests.test_search.query_with_reference_doctype",
)
self.assertListEqual(frappe.response["results"], [])
@frappe.validate_and_sanitize_search_inputs
def get_data(doctype, txt, searchfield, start, page_len, filters):
return [doctype, txt, searchfield, start, page_len, filters]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def query_with_reference_doctype(
doctype, txt, searchfield, start, page_len, filters, reference_doctype=None
):
return []
def setup_test_link_field_order(TestCase):
TestCase.tree_doctype_name = "Test Tree Order"
TestCase.child_doctype_list = []

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

47
frappe/utils/telemetry.py Normal file
View file

@ -0,0 +1,47 @@
""" Basic telemetry for improving apps.
WARNING: Everything in this file should be treated "internal" and is subjected to change or get
removed without any warning.
"""
from contextlib import suppress
from posthog import Posthog
import frappe
POSTHOG_PROJECT_FIELD = "posthog_project_id"
POSTHOG_HOST_FIELD = "posthog_host"
def add_bootinfo(bootinfo):
if not frappe.get_system_settings("enable_telemetry"):
return
bootinfo.posthog_host = frappe.conf.get(POSTHOG_HOST_FIELD)
bootinfo.posthog_project_id = frappe.conf.get(POSTHOG_PROJECT_FIELD)
bootinfo.enable_telemetry = True
def init_telemetry():
"""Init posthog for server side telemetry."""
if hasattr(frappe.local, "posthog"):
return
if not frappe.get_system_settings("enable_telemetry"):
return
posthog_host = frappe.conf.get(POSTHOG_HOST_FIELD)
posthog_project_id = frappe.conf.get(POSTHOG_PROJECT_FIELD)
if not posthog_host or not posthog_project_id:
return
with suppress(Exception):
frappe.local.posthog = Posthog(posthog_project_id, host=posthog_host)
def capture(event, app):
init_telemetry()
ph: Posthog = getattr(frappe.local, "posthog", None)
with suppress(Exception):
ph and ph.capture(frappe.local.site, f"{app}_{event}")

View file

@ -79,6 +79,7 @@ dependencies = [
"google-api-python-client~=2.2.0",
"google-auth-oauthlib~=0.4.4",
"google-auth~=1.29.0",
"posthog~=3.0.1",
]
[build-system]