Merge remote-tracking branch 'upstream/develop' into fix_web_template_type

This commit is contained in:
Faris Ansari 2020-10-22 18:05:01 +05:30
commit 84c8bf68a8
46 changed files with 537 additions and 182 deletions

View file

@ -31,12 +31,12 @@ matrix:
- name: "Python 3.7 MariaDB"
python: 3.7
env: DB=mariadb TYPE=server
script: bench --verbose --site test_site run-tests --coverage
script: bench --site test_site run-tests --coverage
- name: "Python 3.7 PostgreSQL"
python: 3.7
env: DB=postgres TYPE=server
script: bench --verbose --site test_site run-tests --coverage
script: bench --site test_site run-tests --coverage
- name: "Cypress"
python: 3.7

View file

@ -8,10 +8,10 @@ website/ @scmmishra
web_form/ @scmmishra
templates/ @scmmishra
www/ @scmmishra
integrations/ @Mangesh-Khairnar
integrations/ @nextchamp-saqib
patches/ @sahil28297
dashboard/ @prssanna
email/ @Thunderbottom
email/ @saurabh6790
event_streaming/ @ruchamahabal
data_import* @netchampfaris
core/ @surajshetty3416

View file

@ -23,6 +23,11 @@ def get_data():
"description": _("Company, Fiscal Year and Currency defaults"),
"hide_count": True
},
{
"type": "doctype",
"name": "Log Settings",
"description": _("Log cleanup and notification configuration")
},
{
"type": "doctype",
"name": "Error Log",

View file

@ -40,7 +40,11 @@ def add_authentication_log(subject, user, operation="Login", status="Success"):
"operation": operation,
}).insert(ignore_permissions=True, ignore_links=True)
def clear_authentication_logs():
"""clear 100 day old authentication logs"""
def clear_activity_logs(days=None):
"""clear 90 day old authentication logs or configured in log settings"""
if not days:
days = 90
frappe.db.sql("""delete from `tabActivity Log` where \
creation< (NOW() - INTERVAL '100' DAY)""")
creation< (NOW() - INTERVAL '{0}' DAY)""".format(days))

View file

@ -17,9 +17,6 @@ def set_old_logs_as_seen():
frappe.db.sql("""UPDATE `tabError Log` SET `seen`=1
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)""")
# clear old logs
frappe.db.sql("""DELETE FROM `tabError Log` WHERE `creation` < (NOW() - INTERVAL '30' DAY)""")
@frappe.whitelist()
def clear_error_logs():
'''Flush all Error Logs'''

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Log Setting User', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,34 @@
{
"actions": [],
"autoname": "field:user",
"creation": "2020-10-08 13:09:36.034430",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user"
],
"fields": [
{
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"reqd": 1,
"unique": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-08 17:22:04.690348",
"modified_by": "Administrator",
"module": "Core",
"name": "Log Setting User",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class LogSettingUser(Document):
pass

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestLogSettingUser(unittest.TestCase):
pass

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Log Settings', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,83 @@
{
"actions": [],
"creation": "2020-10-08 12:12:21.694424",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"error_log_notification_section",
"users_to_notify",
"log_cleanup_section",
"clear_error_log_after",
"clear_activity_log_after",
"column_break_4",
"clear_email_queue_after"
],
"fields": [
{
"fieldname": "log_cleanup_section",
"fieldtype": "Section Break",
"label": "Log Cleanup"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "error_log_notification_section",
"fieldtype": "Section Break",
"label": "Error Log Notification"
},
{
"fieldname": "users_to_notify",
"fieldtype": "Table MultiSelect",
"label": "Users To Notify",
"options": "Log Setting User"
},
{
"default": "90",
"description": "In Days",
"fieldname": "clear_error_log_after",
"fieldtype": "Int",
"label": "Clear Error log After"
},
{
"default": "90",
"description": "In Days",
"fieldname": "clear_activity_log_after",
"fieldtype": "Int",
"label": "Clear Activity Log After"
},
{
"default": "90",
"description": "In Days",
"fieldname": "clear_email_queue_after",
"fieldtype": "Int",
"label": "Clear Email Queue After"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 12:18:48.649038",
"modified_by": "Administrator",
"module": "Core",
"name": "Log Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class LogSettings(Document):
def clear_logs(self):
self.clear_error_logs()
self.clear_activity_logs()
self.clear_email_queue()
def clear_error_logs(self):
frappe.db.sql(""" DELETE FROM `tabError Log`
WHERE `creation` < (NOW() - INTERVAL '{0}' DAY)
""".format(self.clear_error_log_after))
def clear_activity_logs(self):
from frappe.core.doctype.activity_log.activity_log import clear_activity_logs
clear_activity_logs(days=self.clear_activity_log_after)
def clear_email_queue(self):
from frappe.email.queue import clear_outbox
clear_outbox(days=self.clear_email_queue_after)
def run_log_clean_up():
doc = frappe.get_doc("Log Settings")
doc.clear_logs()
@frappe.whitelist()
def has_unseen_error_log(user):
def _get_response(show_alert=True):
return {
'show_alert': True,
'message': _("You have unseen {0}").format('<a href="/desk#List/Error%20Log/List"> Error Logs </a>')
}
if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"):
log_settings = frappe.get_cached_doc('Log Settings')
if log_settings.users_to_notify:
if user in [u.user for u in log_settings.users_to_notify]:
return _get_response()
else:
return _get_response(show_alert=False)
else:
return _get_response()

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestLogSettings(unittest.TestCase):
pass

View file

@ -49,6 +49,10 @@ class Report(Document):
self.export_doc()
def on_trash(self):
if (self.is_standard == 'Yes'
and not cint(getattr(frappe.local.conf, 'developer_mode', 0))
and not frappe.flags.in_patch):
frappe.throw(_("You are not allowed to delete Standard Report"))
delete_custom_role('report', self.name)
def get_columns(self):

View file

@ -4,6 +4,8 @@
from __future__ import unicode_literals
import frappe, json, os
import unittest
from frappe.desk.query_report import run, save_report
from frappe.custom.doctype.customize_form.customize_form import reset_customization
test_records = frappe.get_test_records('Report')
test_dependencies = ['User']
@ -27,7 +29,57 @@ class TestReport(unittest.TestCase):
columns, data = report.get_data(filters={'user': 'Administrator', 'doctype': 'DocType'})
self.assertEqual(columns[0].get('label'), 'Name')
self.assertEqual(columns[1].get('label'), 'Module')
self.assertTrue('User' in [d[0] for d in data])
self.assertTrue('User' in [d.get('name') for d in data])
def test_custom_report(self):
reset_customization('User')
custom_report_name = save_report(
'Permitted Documents For User',
'Permitted Documents For User Custom',
json.dumps([{
'fieldname': 'email',
'fieldtype': 'Data',
'label': 'Email',
'insert_after_index': 0,
'link_field': 'name',
'doctype': 'User',
'options': 'Email',
'width': 100,
'id':'email',
'name': 'Email'
}]))
custom_report = frappe.get_doc('Report', custom_report_name)
columns, result = custom_report.run_query_report(
filters={
'user': 'Administrator',
'doctype': 'User'
}, user=frappe.session.user)
self.assertListEqual(['email'], [column.get('fieldname') for column in columns])
admin_dict = frappe.core.utils.find(result, lambda d: d['name'] == 'Administrator')
self.assertDictEqual({'name': 'Administrator', 'user_type': 'System User', 'email': 'admin@example.com'}, admin_dict)
def test_report_with_custom_column(self):
reset_customization('User')
response = run('Permitted Documents For User',
filters={'user': 'Administrator', 'doctype': 'User'},
custom_columns=[{
'fieldname': 'email',
'fieldtype': 'Data',
'label': 'Email',
'insert_after_index': 0,
'link_field': 'name',
'doctype': 'User',
'options': 'Email',
'width': 100,
'id':'email',
'name': 'Email'
}])
result = response.get('result')
columns = response.get('columns')
self.assertListEqual(['name', 'email', 'user_type'], [column.get('fieldname') for column in columns])
admin_dict = frappe.core.utils.find(result, lambda d: d['name'] == 'Administrator')
self.assertDictEqual({'name': 'Administrator', 'user_type': 'System User', 'email': 'admin@example.com'}, admin_dict)
def test_report_permissions(self):
frappe.set_user('test@example.com')

View file

@ -22,7 +22,7 @@ class TestScheduledJobType(unittest.TestCase):
self.assertEqual(all_job.frequency, 'All')
daily_job = frappe.get_doc('Scheduled Job Type',
dict(method='frappe.email.queue.clear_outbox'))
dict(method='frappe.email.queue.set_expiry_for_email_queue'))
self.assertEqual(daily_job.frequency, 'Daily')
# check if cron jobs are synced
@ -38,7 +38,7 @@ class TestScheduledJobType(unittest.TestCase):
self.assertEqual(updated_scheduled_job.frequency, "Hourly")
def test_daily_job(self):
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.clear_outbox'))
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.set_expiry_for_email_queue'))
job.db_set('last_execution', '2019-01-01 00:00:00')
self.assertTrue(job.is_event_due(get_datetime('2019-01-02 00:00:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:00:06')))

View file

@ -32,6 +32,7 @@ class CustomField(Document):
self.fieldname = self.fieldname.lower()
def before_insert(self):
self.set_fieldname()
meta = frappe.get_meta(self.dt, cached=False)
fieldnames = [df.fieldname for df in meta.get("fields")]

View file

@ -425,8 +425,13 @@ class CustomizeForm(Document):
if not self.doc_type:
return
frappe.db.sql("""DELETE FROM `tabProperty Setter` WHERE doc_type=%s
and `field_name`!='naming_series'
and `property`!='options'""", self.doc_type)
frappe.clear_cache(doctype=self.doc_type)
reset_customization(self.doc_type)
self.fetch_to_customize()
def reset_customization(doctype):
frappe.db.sql("""
DELETE FROM `tabProperty Setter` WHERE doc_type=%s
and `field_name`!='naming_series'
and `property`!='options'
""", doctype)
frappe.clear_cache(doctype=doctype)

View file

@ -22,6 +22,9 @@
"use_ssl": 0,
"auto_email_id": "hello@example.com",
"google_analytics_id": "google_analytics_id",
"google_analytics_anonymize_ip": 1,
"google_login": {
"client_id": "google_client_id",
"client_secret": "google_client_secret"

View file

@ -12,6 +12,7 @@ from frappe.modules import scrub, get_module_path
from frappe.utils import (
flt,
cint,
cstr,
get_html_format,
get_url_to_form,
gzip_decompress,
@ -74,23 +75,27 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
res = report.execute_script_report(filters)
columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6)
columns = [get_column_as_dict(col) for col in columns]
report_column_names = [col["fieldname"] for col in columns]
# convert to list of dicts
result = normalize_result(result, columns)
if report.custom_columns:
# Original query columns, needed to reorder data as per custom columns
query_columns = columns
# Reordered columns
# saved columns (with custom columns / with different column order)
columns = json.loads(report.custom_columns)
result = reorder_data_for_custom_columns(columns, query_columns, result)
result = add_data_to_custom_columns(columns, result)
# unsaved custom_columns
if custom_columns:
result = add_data_to_custom_columns(custom_columns, result)
for custom_column in custom_columns:
columns.insert(custom_column["insert_after_index"] + 1, custom_column)
# all columns which are not in original report
report_custom_columns = [column for column in columns if column["fieldname"] not in report_column_names]
if report_custom_columns:
result = add_custom_column_data(report_custom_columns, result)
if result:
result = get_filtered_data(report.ref_doctype, columns, result, user)
@ -109,6 +114,20 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
or 0,
}
def normalize_result(result, columns):
# Converts to list of dicts from list of lists/tuples
data = []
column_names = [column["fieldname"] for column in columns]
if result and isinstance(result[0], (list, tuple)):
for row in result:
row_obj = {}
for idx, column_name in enumerate(column_names):
row_obj[column_name] = row[idx]
data.append(row_obj)
else:
data = result
return data
@frappe.whitelist()
def background_enqueue_run(report_name, filters=None, user=None):
@ -177,14 +196,7 @@ def get_script(report_name):
@frappe.whitelist()
@frappe.read_only()
def run(
report_name,
filters=None,
user=None,
ignore_prepared_report=False,
custom_columns=None,
):
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None):
report = get_report_doc(report_name)
if not user:
user = frappe.session.user
@ -221,69 +233,20 @@ def run(
return result
def add_data_to_custom_columns(columns, result):
custom_fields_data = get_data_for_custom_report(columns)
def add_custom_column_data(custom_columns, result):
custom_column_data = get_data_for_custom_report(custom_columns)
data = []
for row in result:
row_obj = {}
if isinstance(row, tuple):
row = list(row)
for column in custom_columns:
key = (column.get('doctype'), column.get('fieldname'))
if key in custom_column_data:
for row in result:
row_reference = row.get(column.get('link_field'))
# possible if the row is empty
if not row_reference:
continue
row[column.get('fieldname')] = custom_column_data.get(key).get(row_reference)
if isinstance(row, list):
for idx, column in enumerate(columns):
if column.get("link_field"):
row_obj[column["fieldname"]] = None
row.insert(idx, None)
else:
row_obj[column["fieldname"]] = row[idx]
data.append(row_obj)
else:
data.append(row)
for row in data:
for column in columns:
if column.get("link_field"):
fieldname = column["fieldname"]
key = (column["doctype"], fieldname)
link_field = column["link_field"]
row[fieldname] = custom_fields_data.get(key, {}).get(
row.get(link_field)
)
return data
def reorder_data_for_custom_columns(custom_columns, columns, result):
if not result:
return []
columns = [get_column_as_dict(col) for col in columns]
if isinstance(result[0], list) or isinstance(result[0], tuple):
# If the result is a list of lists
custom_column_names = [col["label"] for col in custom_columns]
original_column_names = [col["label"] for col in columns]
return get_columns_from_list(custom_column_names, original_column_names, result)
else:
# columns do not need to be reordered if result is a list of dicts
return result
def get_columns_from_list(columns, target_columns, result):
reordered_result = []
for res in result:
r = []
for col_name in columns:
try:
idx = target_columns.index(col_name)
r.append(res[idx])
except ValueError:
pass
reordered_result.append(r)
return reordered_result
return result
def get_prepared_report_result(report, filters, dn="", user=None):
@ -343,31 +306,27 @@ def get_prepared_report_result(report, filters, dn="", user=None):
@frappe.whitelist()
def export_query():
"""export from query reports"""
data = frappe._dict(frappe.local.form_dict)
del data["cmd"]
if "csrf_token" in data:
del data["csrf_token"]
data.pop("cmd", None)
data.pop("csrf_token", None)
if isinstance(data.get("filters"), string_types):
filters = json.loads(data["filters"])
if isinstance(data.get("report_name"), string_types):
if data.get("report_name"):
report_name = data["report_name"]
frappe.permissions.can_export(
frappe.get_cached_value("Report", report_name, "ref_doctype"),
raise_exception=True,
)
if isinstance(data.get("file_format_type"), string_types):
file_format_type = data["file_format_type"]
custom_columns = frappe.parse_json(data["custom_columns"])
file_format_type = data.get("file_format_type")
custom_columns = frappe.parse_json(data.get("custom_columns", "[]"))
include_indentation = data.get("include_indentation")
visible_idx = data.get("visible_idx")
include_indentation = data["include_indentation"]
if isinstance(data.get("visible_idx"), string_types):
visible_idx = json.loads(data.get("visible_idx"))
else:
visible_idx = None
if isinstance(visible_idx, string_types):
visible_idx = json.loads(visible_idx)
if file_format_type == "Excel":
data = run(report_name, filters, custom_columns=custom_columns)
@ -386,8 +345,8 @@ def export_query():
data["result"] = handle_duration_fieldtype_values(
data.get("result"), data.get("columns")
)
xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report")
xlsx_data, column_widths = build_xlsx_data(columns, data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths)
frappe.response["filename"] = report_name + ".xlsx"
frappe.response["filecontent"] = xlsx_file.getvalue()
@ -421,34 +380,38 @@ def handle_duration_fieldtype_values(result, columns):
def build_xlsx_data(columns, data, visible_idx, include_indentation):
result = [[]]
column_widths = []
# add column headings
for idx in range(len(data.columns)):
if not columns[idx].get("hidden"):
result[0].append(columns[idx]["label"])
for column in data.columns:
if column.get("hidden"):
continue
result[0].append(column["label"])
column_width = cint(column.get('width', 0))
# to convert into scale accepted by openpyxl
column_width /= 10
column_widths.append(column_width)
# build table from result
for i, row in enumerate(data.result):
for row_idx, row in enumerate(data.result):
# only pick up rows that are visible in the report
if i in visible_idx:
if row_idx in visible_idx:
row_data = []
if isinstance(row, dict) and row:
for idx in range(len(data.columns)):
# check if column is not hidden
if not columns[idx].get("hidden"):
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
cell_value = row.get(fieldname, row.get(label, ""))
if cint(include_indentation) and "indent" in row and idx == 0:
cell_value = (" " * cint(row["indent"])) + cell_value
row_data.append(cell_value)
else:
if isinstance(row, dict):
for col_idx, column in enumerate(data.columns):
if column.get("hidden"):
continue
label = column.get("label")
fieldname = column.get("fieldname")
cell_value = row.get(fieldname, row.get(label, ""))
if cint(include_indentation) and "indent" in row and col_idx == 0:
cell_value = (" " * cint(row["indent"])) + cstr(cell_value)
row_data.append(cell_value)
elif row:
row_data = row
result.append(row_data)
return result
return result, column_widths
def add_total_row(result, columns, meta=None):
@ -755,6 +718,8 @@ def get_column_as_dict(col):
col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
else:
col_dict["fieldtype"] = col[1]
if len(col) == 3:
col_dict["width"] = col[2]
col_dict["label"] = col[0]
col_dict["fieldname"] = frappe.scrub(col[0])

View file

@ -17,6 +17,8 @@ class EmailDomain(Document):
def validate(self):
"""Validate email id and check POP3/IMAP and SMTP connections is enabled."""
logger = frappe.logger()
if self.email_id:
validate_email_address(self.email_id, True)
@ -26,19 +28,25 @@ class EmailDomain(Document):
if not frappe.local.flags.in_install and not frappe.local.flags.in_patch:
try:
if self.use_imap:
logger.info('Checking incoming IMAP email server {host}:{port} ssl={ssl}...'.format(
host=self.email_server, port=get_port(self), ssl=self.use_ssl))
if self.use_ssl:
test = imaplib.IMAP4_SSL(self.email_server, port=get_port(self))
else:
test = imaplib.IMAP4(self.email_server, port=get_port(self))
else:
logger.info('Checking incoming POP3 email server {host}:{port} ssl={ssl}...'.format(
host=self.email_server, port=get_port(self), ssl=self.use_ssl))
if self.use_ssl:
test = poplib.POP3_SSL(self.email_server, port=get_port(self))
else:
test = poplib.POP3(self.email_server, port=get_port(self))
except Exception:
frappe.throw(_("Incoming email account not correct"))
except Exception as e:
logger.warn('Incoming email account "{host}" not correct'.format(host=self.email_server), exc_info=e)
frappe.throw(title=_("Incoming email account not correct"),
msg='Error connecting IMAP/POP3 "{host}": {e}'.format(host=self.email_server, e=e))
finally:
try:
@ -54,15 +62,21 @@ class EmailDomain(Document):
if not self.get('smtp_port'):
self.smtp_port = 465
logger.info('Checking outgoing SMTPS email server {host}:{port}...'.format(
host=self.smtp_server, port=self.smtp_port))
sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'),
cint(self.smtp_port) or None)
else:
if self.use_tls and not self.smtp_port:
self.smtp_port = 587
logger.info('Checking outgoing SMTP email server {host}:{port} STARTTLS={tls}...'.format(
host=self.smtp_server, port=self.get('smtp_port'), tls=self.use_tls))
sess = smtplib.SMTP(cstr(self.smtp_server or ""), cint(self.smtp_port) or None)
sess.quit()
except Exception:
frappe.throw(_("Outgoing email account not correct"))
except Exception as e:
logger.warn('Outgoing email account "{host}" not correct'.format(host=self.smtp_server), exc_info=e)
frappe.throw(title=_("Outgoing email account not correct"),
msg='Error connecting SMTP "{host}": {e}'.format(host=self.smtp_server, e=e))
def on_update(self):
"""update all email accounts using this domain"""

View file

@ -584,14 +584,15 @@ def prepare_message(email, recipient, recipients_list):
return safe_encode(message.as_string())
def clear_outbox():
"""Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days.
Called daily via scheduler.
def clear_outbox(days=None):
"""Remove low priority older than 31 days in Outbox or configured in Log Settings.
Note: Used separate query to avoid deadlock
"""
if not days:
days=31
email_queues = frappe.db.sql_list("""SELECT `name` FROM `tabEmail Queue`
WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '31' DAY)""")
WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '{0}' DAY)""".format(days))
if email_queues:
frappe.db.sql("""DELETE FROM `tabEmail Queue` WHERE `name` IN ({0})""".format(
@ -602,6 +603,11 @@ def clear_outbox():
','.join(['%s']*len(email_queues)
)), tuple(email_queues))
def set_expiry_for_email_queue():
''' Mark emails as expire that has not sent for 7 days.
Called daily via scheduler.
'''
frappe.db.sql("""
UPDATE `tabEmail Queue`
SET `status`='Expired'

View file

@ -2,7 +2,6 @@
# MIT License. See license.txt
from __future__ import unicode_literals
from six import reraise as raise_
import frappe
import smtplib
import email.utils
@ -242,16 +241,20 @@ class SMTPServer:
return self._sess
except smtplib.SMTPAuthenticationError as e:
frappe.throw(
_("Incorrect email or password. Please check your login credentials."),
exc=frappe.ValidationError,
title=_("Invalid Credentials")
)
except _socket.error as e:
# Invalid mail server -- due to refusing connection
frappe.msgprint(_('Invalid Outgoing Mail Server or Port'))
traceback = sys.exc_info()[2]
raise_(frappe.ValidationError, e, traceback)
except smtplib.SMTPAuthenticationError as e:
frappe.msgprint(_("Invalid login or password"))
traceback = sys.exc_info()[2]
raise_(frappe.ValidationError, e, traceback)
frappe.throw(
_("Invalid Outgoing Mail Server or Port"),
exc=frappe.ValidationError,
title=_("Incorrect Configuration")
)
except smtplib.SMTPException:
frappe.msgprint(_('Unable to send emails at this time'))

View file

@ -206,7 +206,7 @@ scheduler_events = {
"frappe.utils.password.delete_password_reset_cache"
],
"daily": [
"frappe.email.queue.clear_outbox",
"frappe.email.queue.set_expiry_for_email_queue",
"frappe.desk.notifications.clear_notifications",
"frappe.core.doctype.error_log.error_log.set_old_logs_as_seen",
"frappe.desk.doctype.event.event.send_event_digest",
@ -215,7 +215,6 @@ scheduler_events = {
"frappe.realtime.remove_old_task_logs",
"frappe.utils.scheduler.restrict_scheduler_events_if_dormant",
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
"frappe.core.doctype.activity_log.activity_log.clear_authentication_logs",
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record",
"frappe.desk.form.document_follow.send_daily_updates",
"frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points",
@ -223,7 +222,8 @@ scheduler_events = {
"frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
"frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed",
"frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails",
"frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports"
"frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports",
"frappe.core.doctype.log_settings.log_settings.run_log_clean_up"
],
"daily_long": [
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",

View file

@ -6,6 +6,8 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
from six import string_types
from frappe.integrations.utils import json_handler
class IntegrationRequest(Document):
def autoname(self):
@ -20,3 +22,17 @@ class IntegrationRequest(Document):
self.status = status
self.save(ignore_permissions=True)
frappe.db.commit()
def handle_success(self, response):
"""update the output field with the response along with the relevant status"""
if isinstance(response, string_types):
response = json.loads(response)
self.db_set("status", "Completed")
self.db_set("output", json.dumps(response, default=json_handler))
def handle_failure(self, response):
"""update the error field with the response along with the relevant status"""
if isinstance(response, string_types):
response = json.loads(response)
self.db_set("status", "Failed")
self.db_set("error", json.dumps(response, default=json_handler))

View file

@ -49,16 +49,20 @@ def make_post_request(url, auth=None, headers=None, data=None):
frappe.log_error()
raise exc
def create_request_log(data, integration_type, service_name, name=None):
def create_request_log(data, integration_type, service_name, name=None, error=None):
if isinstance(data, string_types):
data = json.loads(data)
if isinstance(error, string_types):
error = json.loads(error)
integration_request = frappe.get_doc({
"doctype": "Integration Request",
"integration_type": integration_type,
"integration_request_service": service_name,
"reference_doctype": data.get("reference_doctype"),
"reference_docname": data.get("reference_docname"),
"error": json.dumps(error, default=json_handler),
"data": json.dumps(data, default=json_handler)
})

View file

@ -139,6 +139,26 @@ frappe.Application = Class.extend({
}
});
}, 300000); // check every 5 minutes
if(frappe.user.has_role("System Manager")){
setInterval(function() {
frappe.call({
method: 'frappe.core.doctype.log_settings.log_settings.has_unseen_error_log',
args: {
user: frappe.session.user
},
callback: function(r) {
console.log(r);
if(r.message.show_alert){
frappe.show_alert({
indicator: 'red',
message: r.message.message
});
}
}
});
}, 600000); // check every 10 minutes
}
}
this.fetch_tags();

View file

@ -1517,11 +1517,12 @@ frappe.ui.form.Form = class FrappeForm {
const escaped_name = encodeURIComponent(value);
return repl('<a class="indicator %(color)s" href="#Form/%(doctype)s/%(name)s">%(label)s</a>', {
return repl('<a class="indicator %(color)s" href="#Form/%(doctype)s/%(escaped_name)s" data-doctype="%(doctype)s" data-name="%(name)s">%(label)s</a>', {
color: get_color(doc || {}),
doctype: df.options,
name: escaped_name,
label: label
escaped_name: escaped_name,
label: label,
name: value
});
} else {
return '';

View file

@ -6,6 +6,9 @@
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{ google_analytics_id }}', 'auto');
{% if google_analytics_anonymize_ip %}
ga('set', 'anonymizeIp', true);
{% endif %}
$(document).on("mousedown", function(event) {
if(!frappe && !frappe.get_route) return;

View file

@ -15,7 +15,7 @@
<ul class="footer-group-links list-unstyled">
{%- for child in group.child_items -%}
<li class="footer-child-item" data-label="{{ child.label }}">
<a href="{{ child.url | abs_url }}" {% if child.target %} target="_blank" {% endif %}>
<a href="{{ child.url | abs_url }}" {% if child.target %} target="_blank" {% endif %} rel="noreferrer">
{%- if child.icon -%}
<img src="{{ child.icon }}" alt="{{ child.label }}">
{%- else -%}

View file

@ -1,5 +1,5 @@
{% macro footer_link(item) %}
<a href="{{ item.url | abs_url }}" {{ item.target }} class="footer-link">
<a href="{{ item.url | abs_url }}" {{ item.target }} class="footer-link" rel="noreferrer">
{%- if item.icon -%}
<img src="{{ item.icon }}" alt="{{ item.label }}">
{%- else -%}

View file

@ -130,8 +130,10 @@ class TestEmail(unittest.TestCase):
def test_expired(self):
self.test_email_queue()
frappe.db.sql("UPDATE `tabEmail Queue` SET `modified`=(NOW() - INTERVAL '8' day)")
from frappe.email.queue import clear_outbox
clear_outbox()
from frappe.email.queue import set_expiry_for_email_queue
set_expiry_for_email_queue()
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Expired'""", as_dict=1)
self.assertEqual(len(email_queue), 1)
queue_recipients = [r.recipient for r in frappe.db.sql("""select recipient from `tabEmail Queue Recipient`

View file

@ -23,7 +23,11 @@ class TestQueryReport(unittest.TestCase):
# Create mock data
data = frappe._dict()
data.columns = ["column_a", "column_b", "column_c"]
data.columns = [
{"label": "Column A", "fieldname": "column_a"},
{"label": "Column B", "fieldname": "column_b", "width": 150},
{"label": "Column C", "fieldname": "column_c", "width": 100}
]
data.result = [
[1.0, 3.0, 5.5],
{"column_a": 22.1, "column_b": 21.8, "column_c": 30.2},
@ -35,10 +39,12 @@ class TestQueryReport(unittest.TestCase):
visible_idx = [0, 2, 3]
# Build the result
xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation=0)
xlsx_data, column_widths = build_xlsx_data(columns, data, visible_idx, include_indentation=0)
self.assertEqual(type(xlsx_data), list)
self.assertEqual(len(xlsx_data), 4) # columns + data
# column widths are divided by 10 to match the scale that is supported by openpyxl
self.assertListEqual(column_widths, [0, 15, 10])
for row in xlsx_data:
self.assertEqual(type(row), list)

View file

@ -31,7 +31,7 @@ class TestScheduler(TestCase):
enqueue_events(site = frappe.local.site)
frappe.flags.execute_job = False
self.assertTrue('frappe.email.queue.clear_outbox', frappe.flags.enqueued_jobs)
self.assertTrue('frappe.email.queue.set_expiry_for_email_queue', frappe.flags.enqueued_jobs)
self.assertTrue('frappe.utils.change_log.check_for_update', frappe.flags.enqueued_jobs)
self.assertTrue('frappe.email.doctype.auto_email_report.auto_email_report.send_monthly', frappe.flags.enqueued_jobs)

View file

@ -9,19 +9,24 @@ import xlrd
import re
from openpyxl.styles import Font
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
from six import BytesIO, string_types
ILLEGAL_CHARACTERS_RE = re.compile(r'[\000-\010]|[\013-\014]|[\016-\037]')
# return xlsx file object
def make_xlsx(data, sheet_name, wb=None):
def make_xlsx(data, sheet_name, wb=None, column_widths=None):
column_widths = column_widths or []
if wb is None:
wb = openpyxl.Workbook(write_only=True)
ws = wb.create_sheet(sheet_name, 0)
for i, column_width in enumerate(column_widths):
if column_width:
ws.column_dimensions[get_column_letter(i + 1)].width = column_width
row1 = ws.row_dimensions[1]
row1.font = Font(name='Calibri',bold=True)
row1.font = Font(name='Calibri', bold=True)
for row in data:
clean_row = []

View file

@ -2,11 +2,21 @@
// For license information, please see license.txt
frappe.ui.form.on('Web Template', {
refresh(frm) {
refresh: function(frm) {
if (!frappe.boot.developer_mode && frm.doc.standard) {
frm.disable_form();
}
frm.toggle_display('standard', frappe.boot.developer_mode);
},
standard: function(frm) {
if (!frm.doc.standard && !frm.is_new()) {
// If standard changes from true to false, hide template until
// the next save. Changes will get overwritten from the backend
// on save and should not be possible in the UI.
frm.toggle_display('template', false);
frm.dashboard.clear_headline();
frm.dashboard.set_headline(__('Please save to edit the template.'));
}
}
});

View file

@ -49,6 +49,7 @@
"fieldname": "module",
"fieldtype": "Link",
"label": "Module",
"mandatory_depends_on": "standard",
"options": "Module Def"
}
],
@ -82,4 +83,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View file

@ -26,15 +26,13 @@ class WebTemplate(Document):
if not field.fieldname:
field.fieldname = frappe.scrub(field.label)
if self.standard and not self.module:
frappe.throw(_("Please select which module this Web Template belongs to."))
def on_update(self):
if frappe.conf.developer_mode:
# custom to standard
if self.standard:
export_to_files(record_list=[["Web Template", self.name]], create_init=True)
self.create_template_file()
self.template = ""
# standard to custom
was_standard = (self.get_doc_before_save() or {}).get("standard")

View file

@ -52,6 +52,7 @@
"indexing_authorization_code",
"column_break_17",
"google_analytics_id",
"google_analytics_anonymize_ip",
"misc_section",
"subdomain",
"disable_signup",
@ -206,7 +207,6 @@
"label": "Integrations"
},
{
"description": "Add Google Analytics ID: eg. UA-89XXX57-1. Please search help on Google Analytics for more information.",
"fieldname": "google_analytics_id",
"fieldtype": "Data",
"label": "Google Analytics ID"
@ -401,6 +401,12 @@
"fieldname": "edit_footer_template_values",
"fieldtype": "Button",
"label": "Edit Values"
},
{
"default": "1",
"fieldname": "google_analytics_anonymize_ip",
"fieldtype": "Check",
"label": "Google Analytics Anonymize IP"
}
],
"icon": "fa fa-cog",
@ -409,7 +415,7 @@
"issingle": 1,
"links": [],
"max_attachments": 10,
"modified": "2020-08-21 14:02:55.168829",
"modified": "2020-09-28 18:47:18.506700",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Settings",
@ -433,4 +439,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

View file

@ -43,6 +43,7 @@ def get_context(context):
"boot": boot if context.get("for_mobile") else boot_json,
"csrf_token": csrf_token,
"google_analytics_id": frappe.conf.get("google_analytics_id"),
"google_analytics_anonymize_ip": frappe.conf.get("google_analytics_anonymize_ip"),
"mixpanel_id": frappe.conf.get("mixpanel_id")
})

View file

@ -9,6 +9,9 @@ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{ google_analytics_id }}', 'auto');
{% if google_analytics_anonymize_ip %}
ga('set', 'anonymizeIp', true);
{% endif %}
ga('send', 'pageview');
// End Google Analytics
{%- endif %}

View file

@ -18,5 +18,11 @@ def get_context(context):
context.javascript += "\n" + js
if not frappe.conf.developer_mode:
context["google_analytics_id"] = (frappe.db.get_single_value("Website Settings", "google_analytics_id")
or frappe.conf.get("google_analytics_id"))
context['google_analytics_id'] = get_setting('google_analytics_id')
context['google_analytics_anonymize_ip'] = get_setting('google_analytics_anonymize_ip')
def get_setting(field_name):
"""Return value of field_name frok Website Settings or Site Config."""
website_settings = frappe.db.get_single_value('Website Settings', field_name)
conf = frappe.conf.get(field_name)
return website_settings or conf

View file

@ -19,7 +19,7 @@
"homepage": "https://frappeframework.com",
"dependencies": {
"ace-builds": "^1.4.8",
"air-datepicker": "http://github.com/frappe/air-datepicker",
"air-datepicker": "github:frappe/air-datepicker",
"autoprefixer": "^9.8.6",
"awesomplete": "^1.1.5",
"bootstrap": "^4.4.1",
@ -29,7 +29,7 @@
"express": "^4.17.1",
"fast-deep-equal": "^2.0.1",
"frappe-charts": "^1.5.1",
"frappe-datatable": "^1.15.1",
"frappe-datatable": "^1.15.3",
"frappe-gantt": "^0.5.0",
"fuse.js": "^3.4.6",
"highlight.js": "^9.18.1",

View file

@ -348,9 +348,9 @@ agent-base@~4.2.1:
dependencies:
es6-promisify "^5.0.0"
"air-datepicker@http://github.com/frappe/air-datepicker":
"air-datepicker@github:frappe/air-datepicker":
version "2.2.3"
resolved "http://github.com/frappe/air-datepicker#ed37b94d95c68d8544357e330be0c89d044a3eea"
resolved "https://codeload.github.com/frappe/air-datepicker/tar.gz/ed37b94d95c68d8544357e330be0c89d044a3eea"
dependencies:
jquery ">=2.0.0 <4.0.0"
@ -2251,10 +2251,10 @@ frappe-charts@^1.5.1:
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.1.tgz#77b9e61400b1657d4ca2eb2202053e3a4d18d54b"
integrity sha512-Cvj6IyDkiH6LKw558A8syJUmkQSdNVnfC+WAzDaAtOfs+u2nST6HExA6JUZMaHU4+VJhC2PWwyRjRNw3B5FaUQ==
frappe-datatable@^1.15.1:
version "1.15.1"
resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.15.1.tgz#3895f6c42158c85a40a82ebd268b6427765facc2"
integrity sha512-bMWJnHwCjwLWSWlZswW66wPUF3oIz7sHeRf2I5rXUd/2m6HqfAAaali0qgDDiO/6VeZBDNttCbovLitjl0ouTA==
frappe-datatable@^1.15.3:
version "1.15.3"
resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.15.3.tgz#1737e9aebfd363ffadffced71a3534c40e350223"
integrity sha512-tUE3pNbxCMX0HPKvwurLBPRAOAdS0gNo1+MpoyFSqXI7b7sp6/TCBRht6qu1Luw+VyIzBtXkJdnnqU+Uoy8iow==
dependencies:
hyperlist "^1.0.0-beta"
lodash "^4.17.5"