Skip to content

Commit

Permalink
17472 New option to acknowledge user messages
Browse files Browse the repository at this point in the history
CMK-21555

Change-Id: Ic69c7945cabc0ad131e5318677dc5a74d9d75f3e
  • Loading branch information
makanakoeln committed Feb 7, 2025
1 parent 543996f commit 726c790
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 33 deletions.
25 changes: 25 additions & 0 deletions .werks/17472.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[//]: # (werk v2)
# New option to acknowledge user messages

key | value
---------- | ---
date | 2025-02-05T14:01:52+00:00
version | 2.4.0b1
class | feature
edition | cre
component | multisite
level | 1
compatible | yes

User messages send via "Setup" - "User" - "Send user message" with option "Show
hint in the 'User' menu" will show a link for new messages in the "User" menu.

As security messages will now be kept for a defined time, this hint would be
shown for the lifetime of that messages.

We now added a new Topic within the "User" menu, named "User messages".
Via the entry "Received messages" in this topic, you get to your personal user
messages page.

This page now allows to acknowledge one or all messages, removing the hint from
the "User" menu.
65 changes: 52 additions & 13 deletions cmk/gui/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Any
from typing import Any, Literal, NotRequired, TypedDict

from cmk.ccc import store

Expand Down Expand Up @@ -51,7 +51,25 @@
TextAreaUnicode,
)

Message = DictionaryModel
MessageMethod = Literal["gui_hint", "gui_popup", "mail", "dashlet"]


class MessageMandatory(TypedDict):
"""From dictionary valuespec"""

text: str
dest: tuple[str, list[UserId]]
methods: list[MessageMethod]


class Message(MessageMandatory):
"""Later added by _process_message"""

valid_till: NotRequired[int]
id: NotRequired[str]
time: NotRequired[int]
security: NotRequired[bool]
acknowledged: NotRequired[bool]


def register(page_registry: PageRegistry) -> None:
Expand Down Expand Up @@ -97,6 +115,21 @@ def delete_gui_message(msg_id: str) -> None:
save_gui_messages(messages)


def acknowledge_gui_message(msg_id: str) -> None:
messages = get_gui_messages()
for index, msg in enumerate(messages):
if msg["id"] == msg_id:
messages[index]["acknowledged"] = True
save_gui_messages(messages)


def acknowledge_all_messages() -> None:
messages = get_gui_messages()
for index, _msg in enumerate(messages):
messages[index]["acknowledged"] = True
save_gui_messages(messages)


def save_gui_messages(messages: MutableSequence[Message], user_id: UserId | None = None) -> None:
if user_id is None:
user_id = user.ident
Expand All @@ -105,7 +138,7 @@ def save_gui_messages(messages: MutableSequence[Message], user_id: UserId | None
store.save_object_to_file(path, messages)


def _messaging_methods() -> dict[str, dict[str, Any]]:
def _messaging_methods() -> dict[MessageMethod, dict[str, Any]]:
return {
"gui_popup": {
"title": _("Show popup message"),
Expand Down Expand Up @@ -159,7 +192,12 @@ def page_message() -> None:
try:
msg = vs_message.from_html_vars("_message")
vs_message.validate_value(msg, "_message")
_process_message_message(msg)
message = Message(
text=msg["text"],
dest=msg["dest"],
methods=msg["methods"],
)
_process_message(message)
except MKUserError as e:
html.user_error(e)

Expand Down Expand Up @@ -279,7 +317,7 @@ def _vs_message() -> Dictionary:
)


def _validate_msg(msg: Message, _varprefix: str) -> None:
def _validate_msg(msg: DictionaryModel, _varprefix: str) -> None:
if not msg.get("methods"):
raise MKUserError("methods", _("Please select at least one messaging method."))

Expand All @@ -296,9 +334,10 @@ def _validate_msg(msg: Message, _varprefix: str) -> None:
raise MKUserError("dest", _('A user with the id "%s" does not exist.') % user_id)


def _process_message_message(msg: Message) -> None: # pylint: disable=R0912
def _process_message(msg: Message) -> None: # pylint: disable=R0912
msg["id"] = utils.gen_id()
msg["time"] = time.time()
msg["time"] = int(time.time())
msg["acknowledged"] = False

if isinstance(msg["dest"], str):
dest_what = msg["dest"]
Expand All @@ -321,7 +360,7 @@ def _process_message_message(msg: Message) -> None: # pylint: disable=R0912
num_success[method] = 0

# Now loop all messaging methods to send the messages
errors: dict[str, list[tuple]] = {}
errors: dict[MessageMethod, list[tuple]] = {}
for user_id in recipients:
for method in msg["methods"]:
try:
Expand Down Expand Up @@ -383,7 +422,7 @@ def message_mail(user_id: UserId, msg: Message) -> bool:
if not user_spec:
raise MKInternalError(_("This user does not exist."))

if not user_spec.get("email"):
if not (user_email := user_spec.get("email")):
raise MKInternalError(_("This user has no mail address configured."))

recipient_name = user_spec.get("alias")
Expand All @@ -402,10 +441,10 @@ def message_mail(user_id: UserId, msg: Message) -> bool:
msg["text"],
)

if msg["valid_till"]:
if valid_till := msg.get("valid_till"):
body += _("This message has been created at %s and is valid till %s.") % (
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(msg["time"])),
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(msg["valid_till"])),
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(valid_till)),
)

mail = MIMEMultipart(_charset="utf-8")
Expand All @@ -414,13 +453,13 @@ def message_mail(user_id: UserId, msg: Message) -> bool:
try:
send_mail_sendmail(
set_mail_headers(
MailString(user_spec["email"]),
MailString(user_email),
MailString("Checkmk: Message"),
MailString(default_from_address()),
MailString(reply_to),
mail,
),
target=MailString(user_spec["email"]),
target=MailString(user_email),
from_address=MailString(default_from_address()),
)
except Exception as exc:
Expand Down
4 changes: 2 additions & 2 deletions cmk/gui/sidebar/main_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def page(self) -> PageResult:
hint_msg: int = 0

for msg in message.get_gui_messages():
if "gui_hint" in msg["methods"]:
if "gui_hint" in msg["methods"] and not msg.get("acknowledged"):
hint_msg += 1
if "gui_popup" in msg["methods"]:
popup_msg.append({"id": msg["id"], "text": msg["text"]})
Expand All @@ -139,7 +139,7 @@ def page(self) -> PageResult:
"popup_messages": popup_msg,
"hint_messages": {
"title": _("User message"),
"text": ungettext("message", "messages", hint_msg),
"text": _("new"),
"count": hint_msg,
},
}
Expand Down
108 changes: 101 additions & 7 deletions cmk/gui/user_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from cmk.gui.htmllib.header import make_header
from cmk.gui.htmllib.html import html
from cmk.gui.http import request
from cmk.gui.i18n import _
from cmk.gui.i18n import _, ungettext
from cmk.gui.logged_in import user
from cmk.gui.main_menu import mega_menu_registry
from cmk.gui.page_menu import (
Expand All @@ -24,11 +24,17 @@
from cmk.gui.pages import Page, PageRegistry
from cmk.gui.table import table_element
from cmk.gui.utils.csrf_token import check_csrf_token
from cmk.gui.utils.flashed_messages import flash, get_flashed_messages
from cmk.gui.utils.transaction_manager import transactions
from cmk.gui.utils.urls import make_confirm_delete_link, makeactionuri


def register(page_registry: PageRegistry) -> None:
page_registry.register_page("user_message")(PageUserMessage)
page_registry.register_page_handler("ajax_delete_user_message", ajax_delete_user_message)
page_registry.register_page_handler(
"ajax_acknowledge_user_message", ajax_acknowledge_user_message
)


class PageUserMessage(Page):
Expand All @@ -38,6 +44,16 @@ def title(self) -> str:
def page_menu(self, breadcrumb: Breadcrumb) -> PageMenu:
return PageMenu(
dropdowns=[
PageMenuDropdown(
name="messages",
title=_("Messages"),
topics=[
PageMenuTopic(
title=_("Received messages"),
entries=list(_page_menu_entries_ack_all_messages()),
),
],
),
PageMenuDropdown(
name="related",
title=_("Related"),
Expand All @@ -55,7 +71,55 @@ def page_menu(self, breadcrumb: Breadcrumb) -> PageMenu:
def page(self) -> None:
breadcrumb = make_simple_page_breadcrumb(mega_menu_registry.menu_user(), _("Messages"))
make_header(html, self.title(), breadcrumb, self.page_menu(breadcrumb))

for flashed_msg in get_flashed_messages():
html.show_message(flashed_msg.msg)

_handle_ack_all()

html.open_div(class_="wato")
render_user_message_table("gui_hint")
html.close_div()

html.footer()


def _handle_ack_all() -> None:
if not transactions.check_transaction():
return

if request.var("_ack_all"):
num = len([msg for msg in message.get_gui_messages() if not msg.get("acknowledged")])
message.acknowledge_all_messages()
flash(
_("%d %s.")
% (
num,
ungettext(
"received message has been acknowledged",
"received messages have been acknowledged",
num,
),
)
)
html.reload_whole_page()


def _page_menu_entries_ack_all_messages() -> Iterator[PageMenuEntry]:
yield PageMenuEntry(
title=_("Acknowledge all"),
icon_name="werk_ack",
is_shortcut=True,
is_suggested=True,
item=make_simple_link(
make_confirm_delete_link(
url=makeactionuri(request, transactions, [("_ack_all", "1")]),
title=_("Acknowledge all received messages"),
confirm_button=_("Acknowledge all"),
)
),
is_enabled=bool([msg for msg in message.get_gui_messages() if not msg.get("acknowledged")]),
)


def _page_menu_entries_related() -> Iterator[PageMenuEntry]:
Expand All @@ -82,7 +146,10 @@ def _page_menu_entries_related() -> Iterator[PageMenuEntry]:
def render_user_message_table(what: str) -> None:
html.open_div()
with table_element(
"user_messages", sortable=False, searchable=False, omit_if_empty=True
"user_messages",
sortable=False,
searchable=False,
empty_text=_("Currently you have no recieved messages"),
) as table:
for entry in sorted(message.get_gui_messages(), key=lambda e: e["time"], reverse=True):
if what not in entry["methods"]:
Expand All @@ -91,12 +158,35 @@ def render_user_message_table(what: str) -> None:
table.row()

msg_id = entry["id"]
datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(entry["time"]))
expiretime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(entry["valid_till"]))
datetime = (
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(entry["time"]))
if "time" in entry
else "-"
)
expiretime = (
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(entry["valid_till"]))
if "valid_till" in entry
else "-"
)
msg = entry["text"].replace("\n", " ")

table.cell(_("Actions"), css=["buttons"], sortable=False)
if not entry.get("security"):
if entry.get("acknowledged"):
html.icon("checkmark", _("Acknowledged"))
else:
html.icon_button(
"",
_("Acknowledge message"),
"werk_ack",
onclick="cmk.utils.acknowledge_user_message('%s');cmk.utils.reload_whole_page();"
% msg_id,
)

if entry.get("security"):
html.icon(
"delete", _("Cannot be deleted manually, must expire"), cssclass="colorless"
)
else:
onclick = (
"cmk.utils.delete_user_message('%s', this);cmk.utils.reload_whole_page();"
% msg_id
Expand All @@ -109,8 +199,6 @@ def render_user_message_table(what: str) -> None:
"delete",
onclick=onclick,
)
else:
html.icon("warning", _("Cannot be deleted manually, must expire"))

table.cell(_("Message"), msg)
table.cell(_("Date sent"), datetime)
Expand All @@ -123,3 +211,9 @@ def ajax_delete_user_message() -> None:
check_csrf_token()
msg_id = request.get_str_input_mandatory("id")
message.delete_gui_message(msg_id)


def ajax_acknowledge_user_message() -> None:
check_csrf_token()
msg_id = request.get_str_input_mandatory("id")
message.acknowledge_gui_message(msg_id)
21 changes: 11 additions & 10 deletions cmk/gui/utils/user_security_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from cmk.utils.user import UserId

from cmk.gui import config, userdb, utils
from cmk.gui.message import message_gui
from cmk.gui.message import Message, message_gui


class SecurityNotificationEvent(Enum):
Expand Down Expand Up @@ -74,13 +74,14 @@ def _send_gui(user_id: UserId, event: SecurityNotificationEvent, event_time: dat
duration = int(config.active_config.user_security_notification_duration["max_duration"])
message_gui(
user_id,
{
"text": str(event.value),
"dest": ("list", [user_id]),
"methods": ["gui_hint"],
"valid_till": timestamp + duration, # 1 week
"id": utils.gen_id(),
"time": timestamp,
"security": True,
},
Message(
text=str(event.value),
dest=("list", [user_id]),
methods=["gui_hint"],
valid_till=timestamp + duration, # 1 week
id=utils.gen_id(),
time=timestamp,
security=True,
acknowledged=False,
),
)
Loading

0 comments on commit 726c790

Please sign in to comment.