add context Core with resource Message

This commit is contained in:
Helmut Merz 2025-06-14 19:02:34 +02:00
parent e9e11843b8
commit 6b828b2574
12 changed files with 579 additions and 0 deletions

104
lib/scopes/core.ex Normal file
View file

@ -0,0 +1,104 @@
defmodule Scopes.Core do
@moduledoc """
The Core context.
"""
import Ecto.Query, warn: false
alias Scopes.Repo
alias Scopes.Core.Message
@doc """
Returns the list of messages.
## Examples
iex> list_messages()
[%Message{}, ...]
"""
def list_messages do
Repo.all(Message)
end
@doc """
Gets a single message.
Raises `Ecto.NoResultsError` if the Message does not exist.
## Examples
iex> get_message!(123)
%Message{}
iex> get_message!(456)
** (Ecto.NoResultsError)
"""
def get_message!(id), do: Repo.get!(Message, id)
@doc """
Creates a message.
## Examples
iex> create_message(%{field: value})
{:ok, %Message{}}
iex> create_message(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_message(attrs \\ %{}) do
%Message{}
|> Message.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a message.
## Examples
iex> update_message(message, %{field: new_value})
{:ok, %Message{}}
iex> update_message(message, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_message(%Message{} = message, attrs) do
message
|> Message.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a message.
## Examples
iex> delete_message(message)
{:ok, %Message{}}
iex> delete_message(message)
{:error, %Ecto.Changeset{}}
"""
def delete_message(%Message{} = message) do
Repo.delete(message)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking message changes.
## Examples
iex> change_message(message)
%Ecto.Changeset{data: %Message{}}
"""
def change_message(%Message{} = message, attrs \\ %{}) do
Message.changeset(message, attrs)
end
end

View file

@ -0,0 +1,21 @@
defmodule Scopes.Core.Message do
use Ecto.Schema
import Ecto.Changeset
schema "messages" do
field :data, :map
field :domain, :string
field :item, :string
field :action, :string
field :class, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(message, attrs) do
message
|> cast(attrs, [:domain, :action, :class, :item, :data])
|> validate_required([:domain, :action, :class, :item])
end
end

View file

@ -0,0 +1,85 @@
defmodule ScopesWeb.MessageLive.FormComponent do
use ScopesWeb, :live_component
alias Scopes.Core
@impl true
def render(assigns) do
~H"""
<div>
<.header>
{@title}
<:subtitle>Use this form to manage message records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="message-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:domain]} type="text" label="Domain" />
<.input field={@form[:action]} type="text" label="Action" />
<.input field={@form[:class]} type="text" label="Class" />
<.input field={@form[:item]} type="text" label="Item" />
<:actions>
<.button phx-disable-with="Saving...">Save Message</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{message: message} = assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_new(:form, fn ->
to_form(Core.change_message(message))
end)}
end
@impl true
def handle_event("validate", %{"message" => message_params}, socket) do
changeset = Core.change_message(socket.assigns.message, message_params)
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
def handle_event("save", %{"message" => message_params}, socket) do
save_message(socket, socket.assigns.action, message_params)
end
defp save_message(socket, :edit, message_params) do
case Core.update_message(socket.assigns.message, message_params) do
{:ok, message} ->
notify_parent({:saved, message})
{:noreply,
socket
|> put_flash(:info, "Message updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp save_message(socket, :new, message_params) do
case Core.create_message(message_params) do
{:ok, message} ->
notify_parent({:saved, message})
{:noreply,
socket
|> put_flash(:info, "Message created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

View file

@ -0,0 +1,47 @@
defmodule ScopesWeb.MessageLive.Index do
use ScopesWeb, :live_view
alias Scopes.Core
alias Scopes.Core.Message
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :messages, Core.list_messages())}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Message")
|> assign(:message, Core.get_message!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Message")
|> assign(:message, %Message{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Messages")
|> assign(:message, nil)
end
@impl true
def handle_info({ScopesWeb.MessageLive.FormComponent, {:saved, message}}, socket) do
{:noreply, stream_insert(socket, :messages, message)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
message = Core.get_message!(id)
{:ok, _} = Core.delete_message(message)
{:noreply, stream_delete(socket, :messages, message)}
end
end

View file

@ -0,0 +1,45 @@
<.header>
Listing Messages
<:actions>
<.link patch={~p"/messages/new"}>
<.button>New Message</.button>
</.link>
</:actions>
</.header>
<.table
id="messages"
rows={@streams.messages}
row_click={fn {_id, message} -> JS.navigate(~p"/messages/#{message}") end}
>
<:col :let={{_id, message}} label="Domain">{message.domain}</:col>
<:col :let={{_id, message}} label="Action">{message.action}</:col>
<:col :let={{_id, message}} label="Class">{message.class}</:col>
<:col :let={{_id, message}} label="Item">{message.item}</:col>
<:col :let={{_id, message}} label="Data">{message.data}</:col>
<:action :let={{_id, message}}>
<div class="sr-only">
<.link navigate={~p"/messages/#{message}"}>Show</.link>
</div>
<.link patch={~p"/messages/#{message}/edit"}>Edit</.link>
</:action>
<:action :let={{id, message}}>
<.link
phx-click={JS.push("delete", value: %{id: message.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
<.modal :if={@live_action in [:new, :edit]} id="message-modal" show on_cancel={JS.patch(~p"/messages")}>
<.live_component
module={ScopesWeb.MessageLive.FormComponent}
id={@message.id || :new}
title={@page_title}
action={@live_action}
message={@message}
patch={~p"/messages"}
/>
</.modal>

View file

@ -0,0 +1,21 @@
defmodule ScopesWeb.MessageLive.Show do
use ScopesWeb, :live_view
alias Scopes.Core
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:message, Core.get_message!(id))}
end
defp page_title(:show), do: "Show Message"
defp page_title(:edit), do: "Edit Message"
end

View file

@ -0,0 +1,30 @@
<.header>
Message {@message.id}
<:subtitle>This is a message record from your database.</:subtitle>
<:actions>
<.link patch={~p"/messages/#{@message}/show/edit"} phx-click={JS.push_focus()}>
<.button>Edit message</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Domain">{@message.domain}</:item>
<:item title="Action">{@message.action}</:item>
<:item title="Class">{@message.class}</:item>
<:item title="Item">{@message.item}</:item>
<:item title="Data">{@message.data}</:item>
</.list>
<.back navigate={~p"/messages"}>Back to messages</.back>
<.modal :if={@live_action == :edit} id="message-modal" show on_cancel={JS.patch(~p"/messages/#{@message}")}>
<.live_component
module={ScopesWeb.MessageLive.FormComponent}
id={@message.id}
title={@page_title}
action={@live_action}
message={@message}
patch={~p"/messages/#{@message}"}
/>
</.modal>

View file

@ -72,6 +72,13 @@ defmodule ScopesWeb.Router do
live "/users/settings", UserSettingsLive, :edit live "/users/settings", UserSettingsLive, :edit
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
live "/bingo", Bingo live "/bingo", Bingo
live "/messages", MessageLive.Index, :index
live "/messages/new", MessageLive.Index, :new
live "/messages/:id/edit", MessageLive.Index, :edit
live "/messages/:id", MessageLive.Show, :show
live "/messages/:id/show/edit", MessageLive.Show, :edit
end end
end end

View file

@ -0,0 +1,15 @@
defmodule Scopes.Repo.Migrations.CreateMessages do
use Ecto.Migration
def change do
create table(:messages) do
add :domain, :string
add :action, :string
add :class, :string
add :item, :string
add :data, :map
timestamps(type: :utc_datetime)
end
end
end

67
test/scopes/core_test.exs Normal file
View file

@ -0,0 +1,67 @@
defmodule Scopes.CoreTest do
use Scopes.DataCase
alias Scopes.Core
describe "messages" do
alias Scopes.Core.Message
import Scopes.CoreFixtures
@invalid_attrs %{data: nil, domain: nil, item: nil, action: nil, class: nil}
test "list_messages/0 returns all messages" do
message = message_fixture()
assert Core.list_messages() == [message]
end
test "get_message!/1 returns the message with given id" do
message = message_fixture()
assert Core.get_message!(message.id) == message
end
test "create_message/1 with valid data creates a message" do
valid_attrs = %{data: %{}, domain: "some domain", item: "some item", action: "some action", class: "some class"}
assert {:ok, %Message{} = message} = Core.create_message(valid_attrs)
assert message.data == %{}
assert message.domain == "some domain"
assert message.item == "some item"
assert message.action == "some action"
assert message.class == "some class"
end
test "create_message/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Core.create_message(@invalid_attrs)
end
test "update_message/2 with valid data updates the message" do
message = message_fixture()
update_attrs = %{data: %{}, domain: "some updated domain", item: "some updated item", action: "some updated action", class: "some updated class"}
assert {:ok, %Message{} = message} = Core.update_message(message, update_attrs)
assert message.data == %{}
assert message.domain == "some updated domain"
assert message.item == "some updated item"
assert message.action == "some updated action"
assert message.class == "some updated class"
end
test "update_message/2 with invalid data returns error changeset" do
message = message_fixture()
assert {:error, %Ecto.Changeset{}} = Core.update_message(message, @invalid_attrs)
assert message == Core.get_message!(message.id)
end
test "delete_message/1 deletes the message" do
message = message_fixture()
assert {:ok, %Message{}} = Core.delete_message(message)
assert_raise Ecto.NoResultsError, fn -> Core.get_message!(message.id) end
end
test "change_message/1 returns a message changeset" do
message = message_fixture()
assert %Ecto.Changeset{} = Core.change_message(message)
end
end
end

View file

@ -0,0 +1,113 @@
defmodule ScopesWeb.MessageLiveTest do
use ScopesWeb.ConnCase
import Phoenix.LiveViewTest
import Scopes.CoreFixtures
@create_attrs %{data: %{}, domain: "some domain", item: "some item", action: "some action", class: "some class"}
@update_attrs %{data: %{}, domain: "some updated domain", item: "some updated item", action: "some updated action", class: "some updated class"}
@invalid_attrs %{data: nil, domain: nil, item: nil, action: nil, class: nil}
defp create_message(_) do
message = message_fixture()
%{message: message}
end
describe "Index" do
setup [:create_message]
test "lists all messages", %{conn: conn, message: message} do
{:ok, _index_live, html} = live(conn, ~p"/messages")
assert html =~ "Listing Messages"
assert html =~ message.domain
end
test "saves new message", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/messages")
assert index_live |> element("a", "New Message") |> render_click() =~
"New Message"
assert_patch(index_live, ~p"/messages/new")
assert index_live
|> form("#message-form", message: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#message-form", message: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/messages")
html = render(index_live)
assert html =~ "Message created successfully"
assert html =~ "some domain"
end
test "updates message in listing", %{conn: conn, message: message} do
{:ok, index_live, _html} = live(conn, ~p"/messages")
assert index_live |> element("#messages-#{message.id} a", "Edit") |> render_click() =~
"Edit Message"
assert_patch(index_live, ~p"/messages/#{message}/edit")
assert index_live
|> form("#message-form", message: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#message-form", message: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/messages")
html = render(index_live)
assert html =~ "Message updated successfully"
assert html =~ "some updated domain"
end
test "deletes message in listing", %{conn: conn, message: message} do
{:ok, index_live, _html} = live(conn, ~p"/messages")
assert index_live |> element("#messages-#{message.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#messages-#{message.id}")
end
end
describe "Show" do
setup [:create_message]
test "displays message", %{conn: conn, message: message} do
{:ok, _show_live, html} = live(conn, ~p"/messages/#{message}")
assert html =~ "Show Message"
assert html =~ message.domain
end
test "updates message within modal", %{conn: conn, message: message} do
{:ok, show_live, _html} = live(conn, ~p"/messages/#{message}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Message"
assert_patch(show_live, ~p"/messages/#{message}/show/edit")
assert show_live
|> form("#message-form", message: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#message-form", message: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/messages/#{message}")
html = render(show_live)
assert html =~ "Message updated successfully"
assert html =~ "some updated domain"
end
end
end

View file

@ -0,0 +1,24 @@
defmodule Scopes.CoreFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `Scopes.Core` context.
"""
@doc """
Generate a message.
"""
def message_fixture(attrs \\ %{}) do
{:ok, message} =
attrs
|> Enum.into(%{
action: "some action",
class: "some class",
data: %{},
domain: "some domain",
item: "some item"
})
|> Scopes.Core.create_message()
message
end
end