We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Custom Page Redirection in Notifications
<!--
Welcome to the Frappe Framework issue tracker! Before creating an issue, please heed the following:

1. This tracker should only be used to report bugs and request features / enhancements to Frappe
    - For questions and general support, refer to https://stackoverflow.com/questions/tagged/frappe
    - For documentation issues, use https://frappeframework.com/docs/user/en or the developer cheetsheet https://frappeframework.com/docs/user/en/bench/resources/bench-commands-cheatsheet
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
   the original discussion.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the     greater the drive to make it happen.
-->

Is your feature request related to a problem? Please describe.
Allow redirection to custom URL on notification click.

Describe the solution you'd like
Currently, in the Frappe framework, notifications only support a default redirection mechanism to a particular doctype.

We should have ability to specify:

portal URL (custom app like desk has some notification)
Framework page 
Kanban link or something like that

Suggested Changes:

1. We will add “Custom Link” in type
2. We will have another field called “Link Uri” where we will store the custom link.
3. We will check if it is “Custom Link”. If it is Custom Link then we will Open the specified link instead of Opening Reference Document
4. We will specify the

Example use case:

We are having a custom  WhatsApp-like which is built inside the framework, we want the user to be directed to a custom page in Frappe when they click on a notification (which is linked to whatsapp message), rather than the standard WhatsApp message doctype.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `frappe/desk/doctype/notification_log/notification_log.py`
Content:
```
1 # Copyright (c) 2019, Frappe Technologies and contributors
2 # License: MIT. See LICENSE
3 
4 import frappe
5 from frappe import _
6 from frappe.desk.doctype.notification_settings.notification_settings import (
7 	is_email_notifications_enabled_for_type,
8 	is_notifications_enabled,
9 )
10 from frappe.model.document import Document
11 
12 
13 class NotificationLog(Document):
14 	# begin: auto-generated types
15 	# This code is auto-generated. Do not modify anything in this block.
16 
17 	from typing import TYPE_CHECKING
18 
19 	if TYPE_CHECKING:
20 		from frappe.types import DF
21 
22 		attached_file: DF.Code | None
23 		document_name: DF.Data | None
24 		document_type: DF.Link | None
25 		email_content: DF.TextEditor | None
26 		for_user: DF.Link | None
27 		from_user: DF.Link | None
28 		read: DF.Check
29 		subject: DF.Text | None
30 		type: DF.Literal["Mention", "Energy Point", "Assignment", "Share", "Alert"]
31 	# end: auto-generated types
32 	def after_insert(self):
33 		frappe.publish_realtime("notification", after_commit=True, user=self.for_user)
34 		set_notifications_as_unseen(self.for_user)
35 		if is_email_notifications_enabled_for_type(self.for_user, self.type):
36 			try:
37 				send_notification_email(self)
38 			except frappe.OutgoingEmailError:
39 				self.log_error(_("Failed to send notification email"))
40 
41 	@staticmethod
42 	def clear_old_logs(days=180):
43 		from frappe.query_builder import Interval
44 		from frappe.query_builder.functions import Now
45 
46 		table = frappe.qb.DocType("Notification Log")
47 		frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
48 
49 
50 def get_permission_query_conditions(for_user):
51 	if not for_user:
52 		for_user = frappe.session.user
53 
54 	if for_user == "Administrator":
55 		return
56 
57 	return f"""(`tabNotification Log`.for_user = {frappe.db.escape(for_user)})"""
58 
59 
60 def get_title(doctype, docname, title_field=None):
61 	if not title_field:
62 		title_field = frappe.get_meta(doctype).get_title_field()
63 	return docname if title_field == "name" else frappe.db.get_value(doctype, docname, title_field)
64 
65 
66 def get_title_html(title):
67 	return f'<b class="subject-title">{title}</b>'
68 
69 
70 def enqueue_create_notification(users: list[str] | str, doc: dict):
71 	"""Send notification to users.
72 
73 	users: list of user emails or string of users with comma separated emails
74 	doc: contents of `Notification` doc
75 	"""
76 
77 	# During installation of new site, enqueue_create_notification tries to connect to Redis.
78 	# This breaks new site creation if Redis server is not running.
79 	# We do not need any notifications in fresh installation
80 	if frappe.flags.in_install:
81 		return
82 
83 	doc = frappe._dict(doc)
84 
85 	if isinstance(users, str):
86 		users = [user.strip() for user in users.split(",") if user.strip()]
87 	users = list(set(users))
88 
89 	frappe.enqueue(
90 		"frappe.desk.doctype.notification_log.notification_log.make_notification_logs",
91 		doc=doc,
92 		users=users,
93 		now=frappe.flags.in_test,
94 	)
95 
96 
97 def make_notification_logs(doc, users):
98 	for user in _get_user_ids(users):
99 		notification = frappe.new_doc("Notification Log")
100 		notification.update(doc)
101 		notification.for_user = user
102 		if (
103 			notification.for_user != notification.from_user
104 			or doc.type == "Energy Point"
105 			or doc.type == "Alert"
106 		):
107 			notification.insert(ignore_permissions=True)
108 
109 
110 def _get_user_ids(user_emails):
111 	user_names = frappe.db.get_values(
112 		"User", {"enabled": 1, "email": ("in", user_emails)}, "name", pluck=True
113 	)
114 	return [user for user in user_names if is_notifications_enabled(user)]
115 
116 
117 def send_notification_email(doc):
118 
119 	if doc.type == "Energy Point" and doc.email_content is None:
120 		return
121 
122 	from frappe.utils import get_url_to_form, strip_html
123 
124 	email = frappe.db.get_value("User", doc.for_user, "email")
125 	if not email:
126 		return
127 
128 	doc_link = get_url_to_form(doc.document_type, doc.document_name)
129 	header = get_email_header(doc)
130 	email_subject = strip_html(doc.subject)
131 
132 	frappe.sendmail(
133 		recipients=email,
134 		subject=email_subject,
135 		template="new_notification",
136 		args = {
137 			"body_content": doc.subject,
138 			"description": doc.email_content,
139 			"document_type": doc.document_type,
140 			"document_name": doc.document_name,
141 			"doc_link": doc_link,
142 		},
143 		header=[header, "orange"],
144 		now=frappe.flags.in_test,
145 	)
146 
147 
148 def get_email_header(doc):
149 	docname = doc.document_name
150 	header_map = {
151 		"Default": _("New Notification"),
152 		"Mention": _("New Mention on {0}").format(docname),
153 		"Assignment": _("Assignment Update on {0}").format(docname),
154 		"Share": _("New Document Shared {0}").format(docname),
155 		"Energy Point": _("Energy Point Update on {0}").format(docname),
156 	}
157 
158 	return header_map[doc.type or "Default"]
159 
160 
161 @frappe.whitelist()
162 def get_notification_logs(limit=20):
163 	notification_logs = frappe.db.get_list(
164 		"Notification Log", fields=["*"], limit=limit, order_by="modified desc"
165 	)
166 
167 	users = [log.from_user for log in notification_logs]
168 	users = [*set(users)]  # remove duplicates
169 	user_info = frappe._dict()
170 
171 	for user in users:
172 		frappe.utils.add_user_info(user, user_info)
173 
174 	return {"notification_logs": notification_logs, "user_info": user_info}
175 
176 
177 @frappe.whitelist()
178 def mark_all_as_read():
179 	unread_docs_list = frappe.get_all(
180 		"Notification Log", filters={"read": 0, "for_user": frappe.session.user}
181 	)
182 	unread_docnames = [doc.name for doc in unread_docs_list]
183 	if unread_docnames:
184 		filters = {"name": ["in", unread_docnames]}
185 		frappe.db.set_value("Notification Log", filters, "read", 1, update_modified=False)
186 
187 
188 @frappe.whitelist()
189 def mark_as_read(docname: str):
190 	if frappe.flags.read_only:
191 		return
192 
193 	if docname:
194 		frappe.db.set_value("Notification Log", str(docname), "read", 1, update_modified=False)
195 
196 
197 @frappe.whitelist()
198 def trigger_indicator_hide():
199 	frappe.publish_realtime("indicator_hide", user=frappe.session.user)
200 
201 
202 def set_notifications_as_unseen(user):
203 	try:
204 		frappe.db.set_value("Notification Settings", user, "seen", 0, update_modified=False)
205 	except frappe.DoesNotExistError:
206 		return
207
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -25,6 +25,7 @@
 		email_content: DF.TextEditor | None
 		for_user: DF.Link | None
 		from_user: DF.Link | None
+		link: DF.Data | None
 		read: DF.Check
 		subject: DF.Text | None
 		type: DF.Literal["Mention", "Energy Point", "Assignment", "Share", "Alert"]
@@ -125,21 +126,24 @@
 	if not email:
 		return
 
-	doc_link = get_url_to_form(doc.document_type, doc.document_name)
 	header = get_email_header(doc)
 	email_subject = strip_html(doc.subject)
+	args = {
+		"body_content": doc.subject,
+		"description": doc.email_content,
+	}
+	if doc.link:
+		args["doc_link"] = doc.link
+	else:
+		args["document_type"] = doc.document_type
+		args["document_name"] = doc.document_name
+		args["doc_link"] = get_url_to_form(doc.document_type, doc.document_name)
 
 	frappe.sendmail(
 		recipients=email,
 		subject=email_subject,
 		template="new_notification",
-		args = {
-			"body_content": doc.subject,
-			"description": doc.email_content,
-			"document_type": doc.document_type,
-			"document_name": doc.document_name,
-			"doc_link": doc_link,
-		},
+		args=args,
 		header=[header, "orange"],
 		now=frappe.flags.in_test,
 	)

{"golden_diff": "diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py\n--- a/frappe/desk/doctype/notification_log/notification_log.py\n+++ b/frappe/desk/doctype/notification_log/notification_log.py\n@@ -25,6 +25,7 @@\n \t\temail_content: DF.TextEditor | None\n \t\tfor_user: DF.Link | None\n \t\tfrom_user: DF.Link | None\n+\t\tlink: DF.Data | None\n \t\tread: DF.Check\n \t\tsubject: DF.Text | None\n \t\ttype: DF.Literal[\"Mention\", \"Energy Point\", \"Assignment\", \"Share\", \"Alert\"]\n@@ -125,21 +126,24 @@\n \tif not email:\n \t\treturn\n \n-\tdoc_link = get_url_to_form(doc.document_type, doc.document_name)\n \theader = get_email_header(doc)\n \temail_subject = strip_html(doc.subject)\n+\targs = {\n+\t\t\"body_content\": doc.subject,\n+\t\t\"description\": doc.email_content,\n+\t}\n+\tif doc.link:\n+\t\targs[\"doc_link\"] = doc.link\n+\telse:\n+\t\targs[\"document_type\"] = doc.document_type\n+\t\targs[\"document_name\"] = doc.document_name\n+\t\targs[\"doc_link\"] = get_url_to_form(doc.document_type, doc.document_name)\n \n \tfrappe.sendmail(\n \t\trecipients=email,\n \t\tsubject=email_subject,\n \t\ttemplate=\"new_notification\",\n-\t\targs = {\n-\t\t\t\"body_content\": doc.subject,\n-\t\t\t\"description\": doc.email_content,\n-\t\t\t\"document_type\": doc.document_type,\n-\t\t\t\"document_name\": doc.document_name,\n-\t\t\t\"doc_link\": doc_link,\n-\t\t},\n+\t\targs=args,\n \t\theader=[header, \"orange\"],\n \t\tnow=frappe.flags.in_test,\n \t)\n", "issue": "Custom Page Redirection in Notifications\n<!--\r\nWelcome to the Frappe Framework issue tracker! Before creating an issue, please heed the following:\r\n\r\n1. This tracker should only be used to report bugs and request features / enhancements to Frappe\r\n    - For questions and general support, refer to https://stackoverflow.com/questions/tagged/frappe\r\n    - For documentation issues, use https://frappeframework.com/docs/user/en or the developer cheetsheet https://frappeframework.com/docs/user/en/bench/resources/bench-commands-cheatsheet\r\n2. Use the search function before creating a new issue. Duplicates will be closed and directed to\r\n   the original discussion.\r\n3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the     greater the drive to make it happen.\r\n-->\r\n\r\nIs your feature request related to a problem? Please describe.\r\nAllow redirection to custom URL on notification click.\r\n\r\nDescribe the solution you'd like\r\nCurrently, in the Frappe framework, notifications only support a default redirection mechanism to a particular doctype.\r\n\r\nWe should have ability to specify:\r\n\r\nportal URL (custom app like desk has some notification)\r\nFramework page \r\nKanban link or something like that\r\n\r\nSuggested Changes:\r\n\r\n1. We will add \u201cCustom Link\u201d in type\r\n2. We will have another field called \u201cLink Uri\u201d where we will store the custom link.\r\n3. We will check if it is \u201cCustom Link\u201d. If it is Custom Link then we will Open the specified link instead of Opening Reference Document\r\n4. We will specify the\r\n\r\nExample use case:\r\n\r\nWe are having a custom  WhatsApp-like which is built inside the framework, we want the user to be directed to a custom page in Frappe when they click on a notification (which is linked to whatsapp message), rather than the standard WhatsApp message doctype.\r\n\n", "before_files": [{"content": "# Copyright (c) 2019, Frappe Technologies and contributors\n# License: MIT. See LICENSE\n\nimport frappe\nfrom frappe import _\nfrom frappe.desk.doctype.notification_settings.notification_settings import (\n\tis_email_notifications_enabled_for_type,\n\tis_notifications_enabled,\n)\nfrom frappe.model.document import Document\n\n\nclass NotificationLog(Document):\n\t# begin: auto-generated types\n\t# This code is auto-generated. Do not modify anything in this block.\n\n\tfrom typing import TYPE_CHECKING\n\n\tif TYPE_CHECKING:\n\t\tfrom frappe.types import DF\n\n\t\tattached_file: DF.Code | None\n\t\tdocument_name: DF.Data | None\n\t\tdocument_type: DF.Link | None\n\t\temail_content: DF.TextEditor | None\n\t\tfor_user: DF.Link | None\n\t\tfrom_user: DF.Link | None\n\t\tread: DF.Check\n\t\tsubject: DF.Text | None\n\t\ttype: DF.Literal[\"Mention\", \"Energy Point\", \"Assignment\", \"Share\", \"Alert\"]\n\t# end: auto-generated types\n\tdef after_insert(self):\n\t\tfrappe.publish_realtime(\"notification\", after_commit=True, user=self.for_user)\n\t\tset_notifications_as_unseen(self.for_user)\n\t\tif is_email_notifications_enabled_for_type(self.for_user, self.type):\n\t\t\ttry:\n\t\t\t\tsend_notification_email(self)\n\t\t\texcept frappe.OutgoingEmailError:\n\t\t\t\tself.log_error(_(\"Failed to send notification email\"))\n\n\t@staticmethod\n\tdef clear_old_logs(days=180):\n\t\tfrom frappe.query_builder import Interval\n\t\tfrom frappe.query_builder.functions import Now\n\n\t\ttable = frappe.qb.DocType(\"Notification Log\")\n\t\tfrappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))\n\n\ndef get_permission_query_conditions(for_user):\n\tif not for_user:\n\t\tfor_user = frappe.session.user\n\n\tif for_user == \"Administrator\":\n\t\treturn\n\n\treturn f\"\"\"(`tabNotification Log`.for_user = {frappe.db.escape(for_user)})\"\"\"\n\n\ndef get_title(doctype, docname, title_field=None):\n\tif not title_field:\n\t\ttitle_field = frappe.get_meta(doctype).get_title_field()\n\treturn docname if title_field == \"name\" else frappe.db.get_value(doctype, docname, title_field)\n\n\ndef get_title_html(title):\n\treturn f'<b class=\"subject-title\">{title}</b>'\n\n\ndef enqueue_create_notification(users: list[str] | str, doc: dict):\n\t\"\"\"Send notification to users.\n\n\tusers: list of user emails or string of users with comma separated emails\n\tdoc: contents of `Notification` doc\n\t\"\"\"\n\n\t# During installation of new site, enqueue_create_notification tries to connect to Redis.\n\t# This breaks new site creation if Redis server is not running.\n\t# We do not need any notifications in fresh installation\n\tif frappe.flags.in_install:\n\t\treturn\n\n\tdoc = frappe._dict(doc)\n\n\tif isinstance(users, str):\n\t\tusers = [user.strip() for user in users.split(\",\") if user.strip()]\n\tusers = list(set(users))\n\n\tfrappe.enqueue(\n\t\t\"frappe.desk.doctype.notification_log.notification_log.make_notification_logs\",\n\t\tdoc=doc,\n\t\tusers=users,\n\t\tnow=frappe.flags.in_test,\n\t)\n\n\ndef make_notification_logs(doc, users):\n\tfor user in _get_user_ids(users):\n\t\tnotification = frappe.new_doc(\"Notification Log\")\n\t\tnotification.update(doc)\n\t\tnotification.for_user = user\n\t\tif (\n\t\t\tnotification.for_user != notification.from_user\n\t\t\tor doc.type == \"Energy Point\"\n\t\t\tor doc.type == \"Alert\"\n\t\t):\n\t\t\tnotification.insert(ignore_permissions=True)\n\n\ndef _get_user_ids(user_emails):\n\tuser_names = frappe.db.get_values(\n\t\t\"User\", {\"enabled\": 1, \"email\": (\"in\", user_emails)}, \"name\", pluck=True\n\t)\n\treturn [user for user in user_names if is_notifications_enabled(user)]\n\n\ndef send_notification_email(doc):\n\n\tif doc.type == \"Energy Point\" and doc.email_content is None:\n\t\treturn\n\n\tfrom frappe.utils import get_url_to_form, strip_html\n\n\temail = frappe.db.get_value(\"User\", doc.for_user, \"email\")\n\tif not email:\n\t\treturn\n\n\tdoc_link = get_url_to_form(doc.document_type, doc.document_name)\n\theader = get_email_header(doc)\n\temail_subject = strip_html(doc.subject)\n\n\tfrappe.sendmail(\n\t\trecipients=email,\n\t\tsubject=email_subject,\n\t\ttemplate=\"new_notification\",\n\t\targs = {\n\t\t\t\"body_content\": doc.subject,\n\t\t\t\"description\": doc.email_content,\n\t\t\t\"document_type\": doc.document_type,\n\t\t\t\"document_name\": doc.document_name,\n\t\t\t\"doc_link\": doc_link,\n\t\t},\n\t\theader=[header, \"orange\"],\n\t\tnow=frappe.flags.in_test,\n\t)\n\n\ndef get_email_header(doc):\n\tdocname = doc.document_name\n\theader_map = {\n\t\t\"Default\": _(\"New Notification\"),\n\t\t\"Mention\": _(\"New Mention on {0}\").format(docname),\n\t\t\"Assignment\": _(\"Assignment Update on {0}\").format(docname),\n\t\t\"Share\": _(\"New Document Shared {0}\").format(docname),\n\t\t\"Energy Point\": _(\"Energy Point Update on {0}\").format(docname),\n\t}\n\n\treturn header_map[doc.type or \"Default\"]\n\n\n@frappe.whitelist()\ndef get_notification_logs(limit=20):\n\tnotification_logs = frappe.db.get_list(\n\t\t\"Notification Log\", fields=[\"*\"], limit=limit, order_by=\"modified desc\"\n\t)\n\n\tusers = [log.from_user for log in notification_logs]\n\tusers = [*set(users)]  # remove duplicates\n\tuser_info = frappe._dict()\n\n\tfor user in users:\n\t\tfrappe.utils.add_user_info(user, user_info)\n\n\treturn {\"notification_logs\": notification_logs, \"user_info\": user_info}\n\n\n@frappe.whitelist()\ndef mark_all_as_read():\n\tunread_docs_list = frappe.get_all(\n\t\t\"Notification Log\", filters={\"read\": 0, \"for_user\": frappe.session.user}\n\t)\n\tunread_docnames = [doc.name for doc in unread_docs_list]\n\tif unread_docnames:\n\t\tfilters = {\"name\": [\"in\", unread_docnames]}\n\t\tfrappe.db.set_value(\"Notification Log\", filters, \"read\", 1, update_modified=False)\n\n\n@frappe.whitelist()\ndef mark_as_read(docname: str):\n\tif frappe.flags.read_only:\n\t\treturn\n\n\tif docname:\n\t\tfrappe.db.set_value(\"Notification Log\", str(docname), \"read\", 1, update_modified=False)\n\n\n@frappe.whitelist()\ndef trigger_indicator_hide():\n\tfrappe.publish_realtime(\"indicator_hide\", user=frappe.session.user)\n\n\ndef set_notifications_as_unseen(user):\n\ttry:\n\t\tfrappe.db.set_value(\"Notification Settings\", user, \"seen\", 0, update_modified=False)\n\texcept frappe.DoesNotExistError:\n\t\treturn\n", "path": "frappe/desk/doctype/notification_log/notification_log.py"}], "after_files": [{"content": "# Copyright (c) 2019, Frappe Technologies and contributors\n# License: MIT. See LICENSE\n\nimport frappe\nfrom frappe import _\nfrom frappe.desk.doctype.notification_settings.notification_settings import (\n\tis_email_notifications_enabled_for_type,\n\tis_notifications_enabled,\n)\nfrom frappe.model.document import Document\n\n\nclass NotificationLog(Document):\n\t# begin: auto-generated types\n\t# This code is auto-generated. Do not modify anything in this block.\n\n\tfrom typing import TYPE_CHECKING\n\n\tif TYPE_CHECKING:\n\t\tfrom frappe.types import DF\n\n\t\tattached_file: DF.Code | None\n\t\tdocument_name: DF.Data | None\n\t\tdocument_type: DF.Link | None\n\t\temail_content: DF.TextEditor | None\n\t\tfor_user: DF.Link | None\n\t\tfrom_user: DF.Link | None\n\t\tlink: DF.Data | None\n\t\tread: DF.Check\n\t\tsubject: DF.Text | None\n\t\ttype: DF.Literal[\"Mention\", \"Energy Point\", \"Assignment\", \"Share\", \"Alert\"]\n\t# end: auto-generated types\n\tdef after_insert(self):\n\t\tfrappe.publish_realtime(\"notification\", after_commit=True, user=self.for_user)\n\t\tset_notifications_as_unseen(self.for_user)\n\t\tif is_email_notifications_enabled_for_type(self.for_user, self.type):\n\t\t\ttry:\n\t\t\t\tsend_notification_email(self)\n\t\t\texcept frappe.OutgoingEmailError:\n\t\t\t\tself.log_error(_(\"Failed to send notification email\"))\n\n\t@staticmethod\n\tdef clear_old_logs(days=180):\n\t\tfrom frappe.query_builder import Interval\n\t\tfrom frappe.query_builder.functions import Now\n\n\t\ttable = frappe.qb.DocType(\"Notification Log\")\n\t\tfrappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))\n\n\ndef get_permission_query_conditions(for_user):\n\tif not for_user:\n\t\tfor_user = frappe.session.user\n\n\tif for_user == \"Administrator\":\n\t\treturn\n\n\treturn f\"\"\"(`tabNotification Log`.for_user = {frappe.db.escape(for_user)})\"\"\"\n\n\ndef get_title(doctype, docname, title_field=None):\n\tif not title_field:\n\t\ttitle_field = frappe.get_meta(doctype).get_title_field()\n\treturn docname if title_field == \"name\" else frappe.db.get_value(doctype, docname, title_field)\n\n\ndef get_title_html(title):\n\treturn f'<b class=\"subject-title\">{title}</b>'\n\n\ndef enqueue_create_notification(users: list[str] | str, doc: dict):\n\t\"\"\"Send notification to users.\n\n\tusers: list of user emails or string of users with comma separated emails\n\tdoc: contents of `Notification` doc\n\t\"\"\"\n\n\t# During installation of new site, enqueue_create_notification tries to connect to Redis.\n\t# This breaks new site creation if Redis server is not running.\n\t# We do not need any notifications in fresh installation\n\tif frappe.flags.in_install:\n\t\treturn\n\n\tdoc = frappe._dict(doc)\n\n\tif isinstance(users, str):\n\t\tusers = [user.strip() for user in users.split(\",\") if user.strip()]\n\tusers = list(set(users))\n\n\tfrappe.enqueue(\n\t\t\"frappe.desk.doctype.notification_log.notification_log.make_notification_logs\",\n\t\tdoc=doc,\n\t\tusers=users,\n\t\tnow=frappe.flags.in_test,\n\t)\n\n\ndef make_notification_logs(doc, users):\n\tfor user in _get_user_ids(users):\n\t\tnotification = frappe.new_doc(\"Notification Log\")\n\t\tnotification.update(doc)\n\t\tnotification.for_user = user\n\t\tif (\n\t\t\tnotification.for_user != notification.from_user\n\t\t\tor doc.type == \"Energy Point\"\n\t\t\tor doc.type == \"Alert\"\n\t\t):\n\t\t\tnotification.insert(ignore_permissions=True)\n\n\ndef _get_user_ids(user_emails):\n\tuser_names = frappe.db.get_values(\n\t\t\"User\", {\"enabled\": 1, \"email\": (\"in\", user_emails)}, \"name\", pluck=True\n\t)\n\treturn [user for user in user_names if is_notifications_enabled(user)]\n\n\ndef send_notification_email(doc):\n\n\tif doc.type == \"Energy Point\" and doc.email_content is None:\n\t\treturn\n\n\tfrom frappe.utils import get_url_to_form, strip_html\n\n\temail = frappe.db.get_value(\"User\", doc.for_user, \"email\")\n\tif not email:\n\t\treturn\n\n\theader = get_email_header(doc)\n\temail_subject = strip_html(doc.subject)\n\targs = {\n\t\t\"body_content\": doc.subject,\n\t\t\"description\": doc.email_content,\n\t}\n\tif doc.link:\n\t\targs[\"doc_link\"] = doc.link\n\telse:\n\t\targs[\"document_type\"] = doc.document_type\n\t\targs[\"document_name\"] = doc.document_name\n\t\targs[\"doc_link\"] = get_url_to_form(doc.document_type, doc.document_name)\n\n\tfrappe.sendmail(\n\t\trecipients=email,\n\t\tsubject=email_subject,\n\t\ttemplate=\"new_notification\",\n\t\targs=args,\n\t\theader=[header, \"orange\"],\n\t\tnow=frappe.flags.in_test,\n\t)\n\n\ndef get_email_header(doc):\n\tdocname = doc.document_name\n\theader_map = {\n\t\t\"Default\": _(\"New Notification\"),\n\t\t\"Mention\": _(\"New Mention on {0}\").format(docname),\n\t\t\"Assignment\": _(\"Assignment Update on {0}\").format(docname),\n\t\t\"Share\": _(\"New Document Shared {0}\").format(docname),\n\t\t\"Energy Point\": _(\"Energy Point Update on {0}\").format(docname),\n\t}\n\n\treturn header_map[doc.type or \"Default\"]\n\n\n@frappe.whitelist()\ndef get_notification_logs(limit=20):\n\tnotification_logs = frappe.db.get_list(\n\t\t\"Notification Log\", fields=[\"*\"], limit=limit, order_by=\"modified desc\"\n\t)\n\n\tusers = [log.from_user for log in notification_logs]\n\tusers = [*set(users)]  # remove duplicates\n\tuser_info = frappe._dict()\n\n\tfor user in users:\n\t\tfrappe.utils.add_user_info(user, user_info)\n\n\treturn {\"notification_logs\": notification_logs, \"user_info\": user_info}\n\n\n@frappe.whitelist()\ndef mark_all_as_read():\n\tunread_docs_list = frappe.get_all(\n\t\t\"Notification Log\", filters={\"read\": 0, \"for_user\": frappe.session.user}\n\t)\n\tunread_docnames = [doc.name for doc in unread_docs_list]\n\tif unread_docnames:\n\t\tfilters = {\"name\": [\"in\", unread_docnames]}\n\t\tfrappe.db.set_value(\"Notification Log\", filters, \"read\", 1, update_modified=False)\n\n\n@frappe.whitelist()\ndef mark_as_read(docname: str):\n\tif frappe.flags.read_only:\n\t\treturn\n\n\tif docname:\n\t\tfrappe.db.set_value(\"Notification Log\", str(docname), \"read\", 1, update_modified=False)\n\n\n@frappe.whitelist()\ndef trigger_indicator_hide():\n\tfrappe.publish_realtime(\"indicator_hide\", user=frappe.session.user)\n\n\ndef set_notifications_as_unseen(user):\n\ttry:\n\t\tfrappe.db.set_value(\"Notification Settings\", user, \"seen\", 0, update_modified=False)\n\texcept frappe.DoesNotExistError:\n\t\treturn\n", "path": "frappe/desk/doctype/notification_log/notification_log.py"}]}