diff --git a/lib/sanbase/ecto_enum.ex b/lib/sanbase/ecto_enum.ex index 6371e5be7..aa1dcc7f7 100644 --- a/lib/sanbase/ecto_enum.ex +++ b/lib/sanbase/ecto_enum.ex @@ -35,7 +35,13 @@ defenum(SubscriptionStatusEnum, :status, [ defenum(LangEnum, :lang, ["en", "jp"]) -defenum(NotificationActionTypeEnum, :notification_action_type, [:create, :update, :delete, :alert]) +defenum(NotificationActionTypeEnum, :notification_action_type, [ + :create, + :update, + :delete, + :alert, + :manual +]) defenum(NotificationChannelEnum, :notification_channel, [:discord, :email, :telegram]) defenum(NotificationStatusEnum, :notification_status, [:pending, :completed, :failed]) diff --git a/lib/sanbase/notifications/template_renderer.ex b/lib/sanbase/notifications/template_renderer.ex index aa4c9d273..084f5f145 100644 --- a/lib/sanbase/notifications/template_renderer.ex +++ b/lib/sanbase/notifications/template_renderer.ex @@ -2,6 +2,14 @@ defmodule Sanbase.Notifications.TemplateRenderer do alias Sanbase.Notifications.{Notification, NotificationAction} alias Sanbase.TemplateEngine + def render_content(%Notification{ + notification_action: %NotificationAction{action_type: :manual}, + content: content + }) + when is_binary(content) do + String.trim(content) + end + def render_content(%Notification{ notification_action: %NotificationAction{action_type: action_type}, step: step, diff --git a/lib/sanbase_web/live/notification/manual_notification_live.ex b/lib/sanbase_web/live/notification/manual_notification_live.ex new file mode 100644 index 000000000..7d19ac877 --- /dev/null +++ b/lib/sanbase_web/live/notification/manual_notification_live.ex @@ -0,0 +1,152 @@ +defmodule SanbaseWeb.ManualNotificationLive do + use SanbaseWeb, :live_view + + alias Sanbase.Notifications + + @channel_discord :discord + @channel_email :email + @valid_channels [@channel_discord, @channel_email] + + @impl true + def mount(_params, _session, socket) do + {:ok, + assign(socket, + page_title: "Create Manual Notification", + form: + to_form( + %{ + "channels_discord" => "true", + "channels_email" => "false", + "content" => "", + "scheduled_at" => "" + }, + as: "notification" + ), + notification: nil + )} + end + + @impl true + def render(assigns) do + ~H""" +
+

Create Manual Notification

+ + <.form for={@form} phx-submit="save" class="space-y-6"> +
+ <.label>Channels +
+ <.input + type="checkbox" + field={@form[:channels_discord]} + value="true" + checked + label="Discord" + name="notification[channels_discord]" + /> + <.input + type="checkbox" + field={@form[:channels_email]} + value="true" + label="Email" + name="notification[channels_email]" + /> +
+
+ +
+ <.label>Content + <.input type="textarea" field={@form[:content]} rows="4" required class="w-full" /> +
+ +
+ <.label>Schedule For (optional) + <.input type="datetime-local" field={@form[:scheduled_at]} class="w-full" /> +
+ + <.button type="submit" phx-disable-with="Creating..."> + Create Notification + + + + <%= if @notification do %> +
+ Notification created successfully! +
+ <% end %> +
+ """ + end + + @impl true + def handle_event("save", %{"notification" => notification_params}, socket) do + # Convert the checkbox boolean values to actual channel names + discord_selected? = notification_params["channels_discord"] == "true" + email_selected? = notification_params["channels_email"] == "true" + + channels = + [] + |> then(fn list -> if discord_selected?, do: [@channel_discord | list], else: list end) + |> then(fn list -> if email_selected?, do: [@channel_email | list], else: list end) + |> then(fn list -> if list == [], do: [@channel_discord], else: list end) + + scheduled_at = + case notification_params["scheduled_at"] do + "" -> DateTime.utc_now() + datetime_str -> parse_datetime(datetime_str) + end + + {:ok, notification_action} = + Notifications.create_notification_action(%{ + action_type: :manual, + scheduled_at: scheduled_at, + status: :pending, + requires_verification: false, + verified: true + }) + + {:ok, notification} = + Notifications.create_notification(%{ + notification_action_id: notification_action.id, + step: :once, + status: :pending, + scheduled_at: scheduled_at, + channels: channels, + display_in_ui: true, + content: notification_params["content"] + }) + + {:noreply, + assign(socket, + notification: notification, + form: + to_form( + %{ + "channels_discord" => "true", + "channels_email" => "false", + "content" => "", + "scheduled_at" => "" + }, + as: "notification" + ) + )} + end + + defp parse_datetime(datetime_str) do + [date, time] = String.split(datetime_str, "T") + [year, month, day] = String.split(date, "-") + [hour, minute] = String.split(time, ":") + + {:ok, datetime} = + NaiveDateTime.new( + String.to_integer(year), + String.to_integer(month), + String.to_integer(day), + String.to_integer(hour), + String.to_integer(minute), + 0 + ) + + DateTime.from_naive!(datetime, "Etc/UTC") + end +end diff --git a/lib/sanbase_web/router.ex b/lib/sanbase_web/router.ex index 8b31859b2..d516c2075 100644 --- a/lib/sanbase_web/router.ex +++ b/lib/sanbase_web/router.ex @@ -83,6 +83,8 @@ defmodule SanbaseWeb.Router do live "/notifications/:id", NotificationLive.Show, :show live "/notifications/:id/show/edit", NotificationLive.Show, :edit + live "/notifications/manual/new", ManualNotificationLive, :new + resources("/reports", ReportController) resources("/sheets_templates", SheetsTemplateController) resources("/webinars", WebinarController) diff --git a/priv/repo/migrations/20241029151959_add_manual_notification_action_type.exs b/priv/repo/migrations/20241029151959_add_manual_notification_action_type.exs new file mode 100644 index 000000000..2411f364a --- /dev/null +++ b/priv/repo/migrations/20241029151959_add_manual_notification_action_type.exs @@ -0,0 +1,11 @@ +defmodule Sanbase.Repo.Migrations.AddManualNotificationActionType do + use Ecto.Migration + + def up do + execute("ALTER TYPE notification_action_type ADD VALUE IF NOT EXISTS 'manual'") + end + + def down do + :ok + end +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 4ffa65fee..9fc210110 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -63,7 +63,8 @@ CREATE TYPE public.notification_action_type AS ENUM ( 'create', 'update', 'delete', - 'alert' + 'alert', + 'manual' ); @@ -9537,3 +9538,4 @@ INSERT INTO public."schema_migrations" (version) VALUES (20241018073651); INSERT INTO public."schema_migrations" (version) VALUES (20241018075640); INSERT INTO public."schema_migrations" (version) VALUES (20241029080754); INSERT INTO public."schema_migrations" (version) VALUES (20241029082533); +INSERT INTO public."schema_migrations" (version) VALUES (20241029151959); diff --git a/test/sanbase/notifications/actions_test.exs b/test/sanbase/notifications/actions_test.exs index 802e32722..54726abe8 100644 --- a/test/sanbase/notifications/actions_test.exs +++ b/test/sanbase/notifications/actions_test.exs @@ -282,6 +282,41 @@ defmodule Sanbase.Notifications.ActionsTest do Sanbase.Notifications.Sender.send_notification(resolved_notification) end + test "sends manual notification with custom text on Discord" do + custom_text = "Important announcement: System maintenance scheduled for tomorrow" + + {:ok, notification_action} = + Notifications.create_notification_action(%{ + action_type: :manual, + scheduled_at: DateTime.utc_now(), + status: :pending, + requires_verification: false, + verified: true + }) + + {:ok, notification} = + Notifications.create_notification(%{ + notification_action_id: notification_action.id, + step: :once, + status: :pending, + scheduled_at: DateTime.utc_now(), + channels: [:discord], + display_in_ui: true, + content: custom_text + }) + + Sanbase.Notifications.MockDiscordClient + |> expect(:send_message, fn _webhook, message, _opts -> + assert String.trim(message) == custom_text + :ok + end) + + Sanbase.Notifications.Sender.send_notification(notification) + + notification = Notifications.get_notification!(notification.id) + assert notification.status == :completed + end + defp get_scheduled_at(:before), do: DateTime.utc_now() defp get_scheduled_at(:reminder), do: DateTime.add(DateTime.utc_now(), 3600 * 24 * 27, :second) defp get_scheduled_at(:after), do: DateTime.add(DateTime.utc_now(), 3600 * 24 * 30, :second)