Models for upload and post; generated liveview controller for posts

This commit is contained in:
Logan Hunt 2022-04-06 12:55:12 -06:00
parent 66d871e546
commit 4067339e8c
Signed by untrusted user who does not match committer: simponic
GPG Key ID: 52B3774857EB24B1
20 changed files with 829 additions and 0 deletions

View File

@ -108,4 +108,100 @@ defmodule Aggiedit.Rooms do
nil -> create_room(%{domain: domain})
end
end
alias Aggiedit.Rooms.Post
@doc """
Returns the list of posts.
## Examples
iex> list_posts()
[%Post{}, ...]
"""
def list_posts do
Repo.all(Post)
end
@doc """
Gets a single post.
Raises `Ecto.NoResultsError` if the Post does not exist.
## Examples
iex> get_post!(123)
%Post{}
iex> get_post!(456)
** (Ecto.NoResultsError)
"""
def get_post!(id), do: Repo.get!(Post, id)
@doc """
Creates a post.
## Examples
iex> create_post(%{field: value})
{:ok, %Post{}}
iex> create_post(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_post(attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a post.
## Examples
iex> update_post(post, %{field: new_value})
{:ok, %Post{}}
iex> update_post(post, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_post(%Post{} = post, attrs) do
post
|> Post.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a post.
## Examples
iex> delete_post(post)
{:ok, %Post{}}
iex> delete_post(post)
{:error, %Ecto.Changeset{}}
"""
def delete_post(%Post{} = post) do
Repo.delete(post)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking post changes.
## Examples
iex> change_post(post)
%Ecto.Changeset{data: %Post{}}
"""
def change_post(%Post{} = post, attrs \\ %{}) do
Post.changeset(post, attrs)
end
end

View File

@ -0,0 +1,21 @@
defmodule Aggiedit.Rooms.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :body, :string
field :title, :string
field :user_id, :id
field :upload_id, :id
field :room_id, :id
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body])
|> validate_required([:title, :body])
end
end

104
lib/aggiedit/uploads.ex Normal file
View File

@ -0,0 +1,104 @@
defmodule Aggiedit.Uploads do
@moduledoc """
The Uploads context.
"""
import Ecto.Query, warn: false
alias Aggiedit.Repo
alias Aggiedit.Uploads.Upload
@doc """
Returns the list of uploads.
## Examples
iex> list_uploads()
[%Upload{}, ...]
"""
def list_uploads do
Repo.all(Upload)
end
@doc """
Gets a single upload.
Raises `Ecto.NoResultsError` if the Upload does not exist.
## Examples
iex> get_upload!(123)
%Upload{}
iex> get_upload!(456)
** (Ecto.NoResultsError)
"""
def get_upload!(id), do: Repo.get!(Upload, id)
@doc """
Creates a upload.
## Examples
iex> create_upload(%{field: value})
{:ok, %Upload{}}
iex> create_upload(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_upload(attrs \\ %{}) do
%Upload{}
|> Upload.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a upload.
## Examples
iex> update_upload(upload, %{field: new_value})
{:ok, %Upload{}}
iex> update_upload(upload, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_upload(%Upload{} = upload, attrs) do
upload
|> Upload.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a upload.
## Examples
iex> delete_upload(upload)
{:ok, %Upload{}}
iex> delete_upload(upload)
{:error, %Ecto.Changeset{}}
"""
def delete_upload(%Upload{} = upload) do
Repo.delete(upload)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking upload changes.
## Examples
iex> change_upload(upload)
%Ecto.Changeset{data: %Upload{}}
"""
def change_upload(%Upload{} = upload, attrs \\ %{}) do
Upload.changeset(upload, attrs)
end
end

View File

@ -0,0 +1,21 @@
defmodule Aggiedit.Uploads.Upload do
use Ecto.Schema
import Ecto.Changeset
schema "uploads" do
field :file, :string
field :mime, :string
field :size, :integer
belongs_to :user, Aggiedit.Accounts.User
timestamps()
end
@doc false
def changeset(upload, attrs) do
upload
|> cast(attrs, [:file, :mime, :size])
|> validate_required([:file, :mime, :size])
end
end

View File

@ -91,6 +91,7 @@ defmodule AggieditWeb do
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
import Phoenix.LiveView.Helpers
import AggieditWeb.LiveHelpers
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View

View File

@ -0,0 +1,60 @@
defmodule AggieditWeb.LiveHelpers do
import Phoenix.LiveView
import Phoenix.LiveView.Helpers
alias Phoenix.LiveView.JS
@doc """
Renders a live component inside a modal.
The rendered modal receives a `:return_to` option to properly update
the URL when the modal is closed.
## Examples
<.modal return_to={Routes.post_index_path(@socket, :index)}>
<.live_component
module={AggieditWeb.PostLive.FormComponent}
id={@post.id || :new}
title={@page_title}
action={@live_action}
return_to={Routes.post_index_path(@socket, :index)}
post: @post
/>
</.modal>
"""
def modal(assigns) do
assigns = assign_new(assigns, :return_to, fn -> nil end)
~H"""
<div id="modal" class="phx-modal fade-in" phx-remove={hide_modal()}>
<div
id="modal-content"
class="phx-modal-content fade-in-scale"
phx-click-away={JS.dispatch("click", to: "#close")}
phx-window-keydown={JS.dispatch("click", to: "#close")}
phx-key="escape"
>
<%= if @return_to do %>
<%= live_patch "",
to: @return_to,
id: "close",
class: "phx-modal-close",
phx_click: hide_modal()
%>
<% else %>
<a id="close" href="#" class="phx-modal-close" phx-click={hide_modal()}></a>
<% end %>
<%= render_slot(@inner_block) %>
</div>
</div>
"""
end
defp hide_modal(js \\ %JS{}) do
js
|> JS.hide(to: "#modal", transition: "fade-out")
|> JS.hide(to: "#modal-content", transition: "fade-out-scale")
end
end

View File

@ -0,0 +1,55 @@
defmodule AggieditWeb.PostLive.FormComponent do
use AggieditWeb, :live_component
alias Aggiedit.Rooms
@impl true
def update(%{post: post} = assigns, socket) do
changeset = Rooms.change_post(post)
{:ok,
socket
|> assign(assigns)
|> assign(:changeset, changeset)}
end
@impl true
def handle_event("validate", %{"post" => post_params}, socket) do
changeset =
socket.assigns.post
|> Rooms.change_post(post_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
def handle_event("save", %{"post" => post_params}, socket) do
save_post(socket, socket.assigns.action, post_params)
end
defp save_post(socket, :edit, post_params) do
case Rooms.update_post(socket.assigns.post, post_params) do
{:ok, _post} ->
{:noreply,
socket
|> put_flash(:info, "Post updated successfully")
|> push_redirect(to: socket.assigns.return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
defp save_post(socket, :new, post_params) do
case Rooms.create_post(post_params) do
{:ok, _post} ->
{:noreply,
socket
|> put_flash(:info, "Post created successfully")
|> push_redirect(to: socket.assigns.return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
end

View File

@ -0,0 +1,24 @@
<div>
<h2><%= @title %></h2>
<.form
let={f}
for={@changeset}
id="post-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save">
<%= label f, :title %>
<%= textarea f, :title %>
<%= error_tag f, :title %>
<%= label f, :body %>
<%= textarea f, :body %>
<%= error_tag f, :body %>
<div>
<%= submit "Save", phx_disable_with: "Saving..." %>
</div>
</.form>
</div>

View File

@ -0,0 +1,46 @@
defmodule AggieditWeb.PostLive.Index do
use AggieditWeb, :live_view
alias Aggiedit.Rooms
alias Aggiedit.Rooms.Post
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :posts, list_posts())}
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 Post")
|> assign(:post, Rooms.get_post!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Post")
|> assign(:post, %Post{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Posts")
|> assign(:post, nil)
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
post = Rooms.get_post!(id)
{:ok, _} = Rooms.delete_post(post)
{:noreply, assign(socket, :posts, list_posts())}
end
defp list_posts do
Rooms.list_posts()
end
end

View File

@ -0,0 +1,41 @@
<h1>Listing Posts</h1>
<%= if @live_action in [:new, :edit] do %>
<.modal return_to={Routes.post_index_path(@socket, :index)}>
<.live_component
module={AggieditWeb.PostLive.FormComponent}
id={@post.id || :new}
title={@page_title}
action={@live_action}
post={@post}
return_to={Routes.post_index_path(@socket, :index)}
/>
</.modal>
<% end %>
<table>
<thead>
<tr>
<th>Title</th>
<th>Body</th>
<th></th>
</tr>
</thead>
<tbody id="posts">
<%= for post <- @posts do %>
<tr id={"post-#{post.id}"}>
<td><%= post.title %></td>
<td><%= post.body %></td>
<td>
<span><%= live_redirect "Show", to: Routes.post_show_path(@socket, :show, post) %></span>
<span><%= live_patch "Edit", to: Routes.post_index_path(@socket, :edit, post) %></span>
<span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: post.id, data: [confirm: "Are you sure?"] %></span>
</td>
</tr>
<% end %>
</tbody>
</table>
<span><%= live_patch "New Post", to: Routes.post_index_path(@socket, :new) %></span>

View File

@ -0,0 +1,21 @@
defmodule AggieditWeb.PostLive.Show do
use AggieditWeb, :live_view
alias Aggiedit.Rooms
@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(:post, Rooms.get_post!(id))}
end
defp page_title(:show), do: "Show Post"
defp page_title(:edit), do: "Edit Post"
end

View File

@ -0,0 +1,31 @@
<h1>Show Post</h1>
<%= if @live_action in [:edit] do %>
<.modal return_to={Routes.post_show_path(@socket, :show, @post)}>
<.live_component
module={AggieditWeb.PostLive.FormComponent}
id={@post.id}
title={@page_title}
action={@live_action}
post={@post}
return_to={Routes.post_show_path(@socket, :show, @post)}
/>
</.modal>
<% end %>
<ul>
<li>
<strong>Title:</strong>
<%= @post.title %>
</li>
<li>
<strong>Body:</strong>
<%= @post.body %>
</li>
</ul>
<span><%= live_patch "Edit", to: Routes.post_show_path(@socket, :edit, @post), class: "button" %></span> |
<span><%= live_redirect "Back", to: Routes.post_index_path(@socket, :index) %></span>

View File

@ -21,6 +21,13 @@ defmodule AggieditWeb.Router do
pipe_through :browser
get "/", PageController, :index
live "/posts", PostLive.Index, :index
live "/posts/new", PostLive.Index, :new
live "/posts/:id/edit", PostLive.Index, :edit
live "/posts/:id", PostLive.Show, :show
live "/posts/:id/show/edit", PostLive.Show, :edit
end
# Other scopes may use custom stacks.

View File

@ -0,0 +1,16 @@
defmodule Aggiedit.Repo.Migrations.CreateUploads do
use Ecto.Migration
def change do
create table(:uploads) do
add :file, :text
add :mime, :text
add :size, :integer
add :user_id, references(:users, on_delete: :nothing)
timestamps()
end
create index(:uploads, [:user_id])
end
end

View File

@ -0,0 +1,19 @@
defmodule Aggiedit.Repo.Migrations.CreatePosts do
use Ecto.Migration
def change do
create table(:posts) do
add :title, :text
add :body, :text
add :user_id, references(:users, on_delete: :nothing)
add :upload_id, references(:uploads, on_delete: :nothing)
add :room_id, references(:rooms, on_delete: :nothing)
timestamps()
end
create index(:posts, [:user_id])
create index(:posts, [:upload_id])
create index(:posts, [:room_id])
end
end

View File

@ -56,4 +56,60 @@ defmodule Aggiedit.RoomsTest do
assert %Ecto.Changeset{} = Rooms.change_room(room)
end
end
describe "posts" do
alias Aggiedit.Rooms.Post
import Aggiedit.RoomsFixtures
@invalid_attrs %{body: nil, title: nil}
test "list_posts/0 returns all posts" do
post = post_fixture()
assert Rooms.list_posts() == [post]
end
test "get_post!/1 returns the post with given id" do
post = post_fixture()
assert Rooms.get_post!(post.id) == post
end
test "create_post/1 with valid data creates a post" do
valid_attrs = %{body: "some body", title: "some title"}
assert {:ok, %Post{} = post} = Rooms.create_post(valid_attrs)
assert post.body == "some body"
assert post.title == "some title"
end
test "create_post/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Rooms.create_post(@invalid_attrs)
end
test "update_post/2 with valid data updates the post" do
post = post_fixture()
update_attrs = %{body: "some updated body", title: "some updated title"}
assert {:ok, %Post{} = post} = Rooms.update_post(post, update_attrs)
assert post.body == "some updated body"
assert post.title == "some updated title"
end
test "update_post/2 with invalid data returns error changeset" do
post = post_fixture()
assert {:error, %Ecto.Changeset{}} = Rooms.update_post(post, @invalid_attrs)
assert post == Rooms.get_post!(post.id)
end
test "delete_post/1 deletes the post" do
post = post_fixture()
assert {:ok, %Post{}} = Rooms.delete_post(post)
assert_raise Ecto.NoResultsError, fn -> Rooms.get_post!(post.id) end
end
test "change_post/1 returns a post changeset" do
post = post_fixture()
assert %Ecto.Changeset{} = Rooms.change_post(post)
end
end
end

View File

@ -0,0 +1,63 @@
defmodule Aggiedit.UploadsTest do
use Aggiedit.DataCase
alias Aggiedit.Uploads
describe "uploads" do
alias Aggiedit.Uploads.Upload
import Aggiedit.UploadsFixtures
@invalid_attrs %{file: nil, mime: nil, size: nil}
test "list_uploads/0 returns all uploads" do
upload = upload_fixture()
assert Uploads.list_uploads() == [upload]
end
test "get_upload!/1 returns the upload with given id" do
upload = upload_fixture()
assert Uploads.get_upload!(upload.id) == upload
end
test "create_upload/1 with valid data creates a upload" do
valid_attrs = %{file: "some file", mime: "some mime", size: 42}
assert {:ok, %Upload{} = upload} = Uploads.create_upload(valid_attrs)
assert upload.file == "some file"
assert upload.mime == "some mime"
assert upload.size == 42
end
test "create_upload/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Uploads.create_upload(@invalid_attrs)
end
test "update_upload/2 with valid data updates the upload" do
upload = upload_fixture()
update_attrs = %{file: "some updated file", mime: "some updated mime", size: 43}
assert {:ok, %Upload{} = upload} = Uploads.update_upload(upload, update_attrs)
assert upload.file == "some updated file"
assert upload.mime == "some updated mime"
assert upload.size == 43
end
test "update_upload/2 with invalid data returns error changeset" do
upload = upload_fixture()
assert {:error, %Ecto.Changeset{}} = Uploads.update_upload(upload, @invalid_attrs)
assert upload == Uploads.get_upload!(upload.id)
end
test "delete_upload/1 deletes the upload" do
upload = upload_fixture()
assert {:ok, %Upload{}} = Uploads.delete_upload(upload)
assert_raise Ecto.NoResultsError, fn -> Uploads.get_upload!(upload.id) end
end
test "change_upload/1 returns a upload changeset" do
upload = upload_fixture()
assert %Ecto.Changeset{} = Uploads.change_upload(upload)
end
end
end

View File

@ -0,0 +1,110 @@
defmodule AggieditWeb.PostLiveTest do
use AggieditWeb.ConnCase
import Phoenix.LiveViewTest
import Aggiedit.RoomsFixtures
@create_attrs %{body: "some body", title: "some title"}
@update_attrs %{body: "some updated body", title: "some updated title"}
@invalid_attrs %{body: nil, title: nil}
defp create_post(_) do
post = post_fixture()
%{post: post}
end
describe "Index" do
setup [:create_post]
test "lists all posts", %{conn: conn, post: post} do
{:ok, _index_live, html} = live(conn, Routes.post_index_path(conn, :index))
assert html =~ "Listing Posts"
assert html =~ post.body
end
test "saves new post", %{conn: conn} do
{:ok, index_live, _html} = live(conn, Routes.post_index_path(conn, :index))
assert index_live |> element("a", "New Post") |> render_click() =~
"New Post"
assert_patch(index_live, Routes.post_index_path(conn, :new))
assert index_live
|> form("#post-form", post: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
{:ok, _, html} =
index_live
|> form("#post-form", post: @create_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.post_index_path(conn, :index))
assert html =~ "Post created successfully"
assert html =~ "some body"
end
test "updates post in listing", %{conn: conn, post: post} do
{:ok, index_live, _html} = live(conn, Routes.post_index_path(conn, :index))
assert index_live |> element("#post-#{post.id} a", "Edit") |> render_click() =~
"Edit Post"
assert_patch(index_live, Routes.post_index_path(conn, :edit, post))
assert index_live
|> form("#post-form", post: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
{:ok, _, html} =
index_live
|> form("#post-form", post: @update_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.post_index_path(conn, :index))
assert html =~ "Post updated successfully"
assert html =~ "some updated body"
end
test "deletes post in listing", %{conn: conn, post: post} do
{:ok, index_live, _html} = live(conn, Routes.post_index_path(conn, :index))
assert index_live |> element("#post-#{post.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#post-#{post.id}")
end
end
describe "Show" do
setup [:create_post]
test "displays post", %{conn: conn, post: post} do
{:ok, _show_live, html} = live(conn, Routes.post_show_path(conn, :show, post))
assert html =~ "Show Post"
assert html =~ post.body
end
test "updates post within modal", %{conn: conn, post: post} do
{:ok, show_live, _html} = live(conn, Routes.post_show_path(conn, :show, post))
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Post"
assert_patch(show_live, Routes.post_show_path(conn, :edit, post))
assert show_live
|> form("#post-form", post: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
{:ok, _, html} =
show_live
|> form("#post-form", post: @update_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.post_show_path(conn, :show, post))
assert html =~ "Post updated successfully"
assert html =~ "some updated body"
end
end
end

View File

@ -17,4 +17,19 @@ defmodule Aggiedit.RoomsFixtures do
room
end
@doc """
Generate a post.
"""
def post_fixture(attrs \\ %{}) do
{:ok, post} =
attrs
|> Enum.into(%{
body: "some body",
title: "some title"
})
|> Aggiedit.Rooms.create_post()
post
end
end

View File

@ -0,0 +1,22 @@
defmodule Aggiedit.UploadsFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `Aggiedit.Uploads` context.
"""
@doc """
Generate a upload.
"""
def upload_fixture(attrs \\ %{}) do
{:ok, upload} =
attrs
|> Enum.into(%{
file: "some file",
mime: "some mime",
size: 42
})
|> Aggiedit.Uploads.create_upload()
upload
end
end