Merge remote-tracking branch 'upstream/develop' into fix_web_template_type
This commit is contained in:
commit
84c8bf68a8
46 changed files with 537 additions and 182 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -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'''
|
||||
|
|
|
|||
0
frappe/core/doctype/log_setting_user/__init__.py
Normal file
0
frappe/core/doctype/log_setting_user/__init__.py
Normal file
8
frappe/core/doctype/log_setting_user/log_setting_user.js
Normal file
8
frappe/core/doctype/log_setting_user/log_setting_user.js
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
34
frappe/core/doctype/log_setting_user/log_setting_user.json
Normal file
34
frappe/core/doctype/log_setting_user/log_setting_user.json
Normal 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
|
||||
}
|
||||
10
frappe/core/doctype/log_setting_user/log_setting_user.py
Normal file
10
frappe/core/doctype/log_setting_user/log_setting_user.py
Normal 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
|
||||
|
|
@ -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
|
||||
0
frappe/core/doctype/log_settings/__init__.py
Normal file
0
frappe/core/doctype/log_settings/__init__.py
Normal file
8
frappe/core/doctype/log_settings/log_settings.js
Normal file
8
frappe/core/doctype/log_settings/log_settings.js
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
83
frappe/core/doctype/log_settings/log_settings.json
Normal file
83
frappe/core/doctype/log_settings/log_settings.json
Normal 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
|
||||
}
|
||||
51
frappe/core/doctype/log_settings/log_settings.py
Normal file
51
frappe/core/doctype/log_settings/log_settings.py
Normal 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()
|
||||
10
frappe/core/doctype/log_settings/test_log_settings.py
Normal file
10
frappe/core/doctype/log_settings/test_log_settings.py
Normal 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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')))
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 -%}
|
||||
|
|
|
|||
|
|
@ -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 -%}
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
12
yarn.lock
12
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue