feat: create base notification templates

This commit is contained in:
Smit Vora 2026-02-01 16:54:56 +05:30
parent e703fe9598
commit bf93eae041
7 changed files with 298 additions and 0 deletions

View file

@ -867,3 +867,75 @@ def _parse_receiver_by_document_field(s):
else:
data_field, child_field = fragments[0], None
return data_field, child_field
def create_notifications(notifications: list[dict], update: bool = False):
"""
Unlike standard notifications, these are NOT marked as is_standard=1,
so they won't be overwritten during migrations. Users can freely customize them.
Args:
notifications: List of notification dicts.
update: If True, update existing notification. If False (default), skip if exists.
"""
for notif_dict in notifications:
name = notif_dict.get("name")
existing = frappe.db.exists("Notification", name)
if existing and not update:
continue
if existing and update:
doc = frappe.get_doc("Notification", name)
doc.update(notif_dict)
doc.flags.ignore_validate = True
doc.save(ignore_permissions=True)
continue
notif_dict["doctype"] = "Notification"
notif_dict["is_standard"] = 0
notif_dict["owner"] = "Administrator"
doc = frappe.get_doc(notif_dict)
doc.flags.ignore_validate = True
doc.insert(ignore_permissions=True)
def get_notification_templates(templates_dir: str) -> list[dict]:
"""
Load notification templates from the templates directory.
Templates are stored in subdirectories:
<templates_dir>/<name>/<name>.json
<templates_dir>/<name>/<name>.html|.md|.txt (optional message content based on message_type)
"""
templates = []
if not os.path.exists(templates_dir):
return templates
for folder_name in os.listdir(templates_dir):
folder_path = os.path.join(templates_dir, folder_name)
if not os.path.isdir(folder_path):
continue
json_file = os.path.join(folder_path, f"{folder_name}.json")
template = frappe.get_file_json(json_file) if os.path.exists(json_file) else None
if not template:
continue
message_type = template.get("message_type", "HTML")
ext = FORMATS.get(message_type, ".html")
message_file = os.path.join(folder_path, f"{folder_name}{ext}")
if message := frappe.read_file(message_file):
template["message"] = message
templates.append(template)
return templates
def install_notification_templates():
templates_dir = frappe.get_module_path("Email", "doctype", "notification", "templates")
templates = get_notification_templates(templates_dir)
create_notifications(templates, update=False)

View file

@ -0,0 +1,66 @@
{% set error_lines = (doc.error or "").split('\n') %}
{% set first_lines = 10 %}
{% set last_lines = 15 %}
{% set max_lines = first_lines + last_lines %}
{% set total_lines = error_lines | length %}
{% set needs_truncation = total_lines > max_lines %}
<table class="email-header" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td>
<h1 class="email-header-title">
<span class="indicator red"></span>
Error Log
</h1>
</td>
</tr>
</table>
<table class="table table-bordered" width="100%">
<tr>
<td class="text-bold" style="background: #f8f8f8; width: 120px;">Site</td>
<td><a href="{{ frappe.utils.get_url() }}">{{ frappe.utils.get_url() }}</a></td>
</tr>
<tr>
<td class="text-bold" style="background: #f8f8f8;">Error ID</td>
<td>{{ frappe.utils.get_link_to_form("Error Log", doc.name, doc.name) }}</td>
</tr>
<tr>
<td class="text-bold" style="background: #f8f8f8;">Title</td>
<td>{{ doc.method or "N/A" }}</td>
</tr>
<tr>
<td class="text-bold" style="background: #f8f8f8;">Logged At</td>
<td>{{ doc.creation }}</td>
</tr>
{% if doc.reference_doctype and doc.reference_name %}
<tr>
<td class="text-bold" style="background: #f8f8f8;">Reference</td>
<td>{{ frappe.utils.get_link_to_form(doc.reference_doctype, doc.reference_name) }}</td>
</tr>
{% endif %}
</table>
<div style="margin-top: 20px;">
<div class="text-medium text-bold">
Error Details
{% if needs_truncation %}
<span class="text-muted" style="font-weight: normal;"> ({{ max_lines }} of {{ total_lines }} lines)</span>
{% endif %}
</div>
<div class="gray-container" style="margin-top: 8px;">
<pre class="text-small" style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">{% if needs_truncation %}{{ error_lines[:first_lines] | join('\n') }}
<span class="text-muted" style="font-style: italic;">... {{ total_lines - max_lines }} lines omitted ...</span>
{{ error_lines[-last_lines:] | join('\n') }}{% else %}{{ error_lines | join('\n') }}{% endif %}</pre>
</div>
</div>
<div class="more-info">
<a href="{{ frappe.utils.get_url_to_form('Error Log', doc.name) }}" class="btn btn-primary">View Error Log</a>
</div>
<p class="text-muted text-small" style="margin-top: 20px;">
This is an automated notification from {{ frappe.utils.get_host_name() }}.
</p>

View file

@ -0,0 +1,16 @@
{
"name": "Error Log",
"document_type": "Error Log",
"event": "New",
"channel": "Email",
"enabled": 0,
"subject": "[Error] {{ doc.method }}",
"message_type": "HTML",
"recipients": [
{
"receiver_by_role": "System Manager"
}
],
"send_system_notification": 0,
"send_to_all_assignees": 0
}

View file

@ -0,0 +1,122 @@
{% set error_lines = (doc.error or "").split('\n') %}
{% set output_lines = (doc.output or "").split('\n') %}
{% set first_lines = 10 %}
{% set last_lines = 15 %}
{% set max_lines = first_lines + last_lines %}
{% set total_error_lines = error_lines | length %}
{% set error_needs_truncation = total_error_lines > max_lines %}
{% set total_output_lines = output_lines | length %}
{% set output_needs_truncation = total_output_lines > max_lines %}
<table class="email-header" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td>
<h1 class="email-header-title">
<span class="indicator red"></span>
Integration Request
</h1>
</td>
</tr>
</table>
<table class="table table-bordered" style="width: 100%;">
<tr>
<td class="text-bold" style="background: #f8f8f8; width: 120px">Site</td>
<td>{{ frappe.utils.get_url() }}</td>
</tr>
<tr>
<td class="text-bold" style="background: #f8f8f8;">Request ID</td>
<td>{{ frappe.utils.get_link_to_form("Integration Request", doc.name, doc.request_id or
doc.name) }}</td>
</tr>
<tr>
<td class="text-bold" style="background: #f8f8f8;">Service</td>
<td>{{ doc.integration_request_service or "N/A" }}</td>
</tr>
<tr>
<td class="text-bold" style="background: #f8f8f8;">Status</td>
<td>
<span class="indicator-pill red">
{{ doc.status }}
</span>
</td>
</tr>
{% if doc.request_description %}
<tr>
<td class="text-bold" style="background: #f8f8f8;">Description</td>
<td>{{ doc.request_description }}</td>
</tr>
{% endif %}
<tr>
<td class="text-bold" style="background: #f8f8f8;">Logged At</td>
<td>{{ frappe.utils.format_datetime(doc.creation) }}</td>
</tr>
{% if doc.url %}
<tr>
<td class="text-bold" style="background: #f8f8f8;">Endpoint URL</td>
<td class="text-small" style="word-break: break-all;">{{ doc.url }}</td>
</tr>
{% endif %}
{% if doc.reference_doctype and doc.reference_docname %}
<tr>
<td class="text-bold" style="background: #f8f8f8;">Reference</td>
<td>
<a
href="{{ frappe.utils.get_url_to_form(doc.reference_doctype, doc.reference_docname) }}">
{{ doc.reference_doctype }}: {{ doc.reference_docname }}
</a>
</td>
</tr>
{% endif %}
</table>
{% if doc.error %}
<div style="margin-top: 20px;">
<div class="text-medium text-bold">
Error Details
{% if error_needs_truncation %}
<span class="text-muted" style="font-weight: normal;"> ({{ max_lines }} of {{
total_error_lines }} lines)</span>
{% endif %}
</div>
<div class="gray-container" style="margin-top: 8px;">
<pre class="text-small"
style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">{% if error_needs_truncation %}{{ error_lines[:first_lines] | join('\n') }}
<span class="text-muted" style="font-style: italic;">... {{ total_error_lines - max_lines }} lines omitted ...</span>
{{ error_lines[-last_lines:] | join('\n') }}{% else %}{{ error_lines | join('\n') }}{% endif %}</pre>
</div>
</div>
{% endif %}
{% if doc.output %}
<div style="margin-top: 20px;">
<div class="text-medium text-bold">
Response Output
{% if output_needs_truncation %}
<span class="text-muted" style="font-weight: normal;"> ({{ max_lines }} of {{
total_output_lines }} lines)</span>
{% endif %}
</div>
<div class="gray-container" style="margin-top: 8px;">
<pre class="text-small"
style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">{% if output_needs_truncation %}{{ output_lines[:first_lines] | join('\n') }}
<span class="text-muted" style="font-style: italic;">... {{ total_output_lines - max_lines }} lines omitted ...</span>
{{ output_lines[-last_lines:] | join('\n') }}{% else %}{{ output_lines | join('\n') }}{% endif %}</pre>
</div>
</div>
{% endif %}
<div class="more-info">
<a class="btn btn-primary"
href="{{ frappe.utils.get_url_to_form('Integration Request', doc.name) }}">
View Integration Request
</a>
</div>
<p class="text-muted text-small" style="margin-top: 20px;">
This is an automated notification from {{ frappe.utils.get_host_name() }}.
</p>

View file

@ -0,0 +1,17 @@
{
"name": "Integration Request",
"document_type": "Integration Request",
"event": "Save",
"channel": "Email",
"condition": "doc.status==\"Failed\"",
"enabled": 0,
"subject": "[Error] {{ doc.integration_request_service }}",
"message_type": "HTML",
"recipients": [
{
"receiver_by_role": "System Manager"
}
],
"send_system_notification": 0,
"send_to_all_assignees": 0
}

View file

@ -256,3 +256,4 @@ frappe.patches.v16_0.add_standard_field_in_workspace_sidebar
execute:frappe.db.set_single_value("Desktop Settings", "icon_style", "Solid")
execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Productivity")
frappe.patches.v16_0.unset_standard_field_for_auto_generated_icons
execute:frappe.email.doctype.notification.notification.install_notification_templates

View file

@ -3,6 +3,7 @@
import getpass
import frappe
from frappe.email.doctype.notification.notification import install_notification_templates
from frappe.geo.doctype.country.country import import_country_and_currency
from frappe.utils import cint
from frappe.utils.password import update_password
@ -53,6 +54,9 @@ def after_install():
add_standard_navbar_items()
# default templates
install_notification_templates()
frappe.db.commit()