Add comments and vote models; pub/sub voting on posts
This commit is contained in:
parent
db7c2321cd
commit
3cf9f4a364
@ -14,6 +14,8 @@ defmodule Aggiedit.Accounts.User do
|
|||||||
|
|
||||||
belongs_to :room, Room, on_replace: :update
|
belongs_to :room, Room, on_replace: :update
|
||||||
has_many :posts, Aggiedit.Rooms.Post
|
has_many :posts, Aggiedit.Rooms.Post
|
||||||
|
has_many :votes, Aggiedit.Post.Vote
|
||||||
|
has_many :comments, Aggiedit.Post.Comment
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
@ -6,7 +6,7 @@ defmodule Aggiedit.Roles do
|
|||||||
def guard?(user, action, object)
|
def guard?(user, action, object)
|
||||||
def guard?(%User{role: :admin}, _, _), do: true
|
def guard?(%User{role: :admin}, _, _), do: true
|
||||||
def guard?(%User{room_id: rid}, :index, %Room{id: rid}), do: true
|
def guard?(%User{room_id: rid}, :index, %Room{id: rid}), do: true
|
||||||
def guard?(%User{room_id: rid}, :show, %Post{room_id: rid}), do: true
|
def guard?(%User{room_id: rid}, action, %Post{room_id: rid}) when action in [:show, :vote], do: true
|
||||||
def guard?(%User{id: id, room_id: rid}, action, %Post{user_id: id, room_id: rid}) when action in [:delete, :edit], do: true
|
def guard?(%User{id: id, room_id: rid}, action, %Post{user_id: id, room_id: rid}) when action in [:delete, :edit], do: true
|
||||||
def guard?(_, _, _), do: false
|
def guard?(_, _, _), do: false
|
||||||
|
|
||||||
|
@ -6,9 +6,11 @@ defmodule Aggiedit.Rooms do
|
|||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
alias Aggiedit.Repo
|
alias Aggiedit.Repo
|
||||||
|
|
||||||
alias Aggiedit.Accounts
|
alias Aggiedit.Accounts.User
|
||||||
alias Aggiedit.Rooms.Room
|
alias Aggiedit.Rooms.Room
|
||||||
|
|
||||||
|
alias Aggiedit.Post.{Vote, Comment}
|
||||||
|
|
||||||
alias Phoenix.PubSub
|
alias Phoenix.PubSub
|
||||||
|
|
||||||
def list_rooms do
|
def list_rooms do
|
||||||
@ -91,6 +93,23 @@ defmodule Aggiedit.Rooms do
|
|||||||
Post.changeset(post, attrs)
|
Post.changeset(post, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def vote_count(post) do
|
||||||
|
votes = post
|
||||||
|
|> Repo.preload(:votes)
|
||||||
|
|> Map.get(:votes)
|
||||||
|
|> Enum.map(fn vote -> if vote.is_up, do: 1, else: -1 end)
|
||||||
|
|> Enum.sum()
|
||||||
|
end
|
||||||
|
|
||||||
|
def vote_post(%Post{} = post, %User{} = user, direction) do
|
||||||
|
is_up = direction == "upvote"
|
||||||
|
vote = %Vote{is_up: is_up, user: user, post: post}
|
||||||
|
|> Repo.insert(on_conflict: [set: [is_up: is_up]], conflict_target: [:user_id, :post_id])
|
||||||
|
post = change_post(post, %{score: vote_count(post)})
|
||||||
|
|> Repo.update()
|
||||||
|
broadcast_post_over_room(post, :post_voted)
|
||||||
|
end
|
||||||
|
|
||||||
defp broadcast_post_over_room({:error, _reason}=error, _event), do: error
|
defp broadcast_post_over_room({:error, _reason}=error, _event), do: error
|
||||||
defp broadcast_post_over_room({:ok, post}, event) do
|
defp broadcast_post_over_room({:ok, post}, event) do
|
||||||
PubSub.broadcast(Aggiedit.PubSub, "room:#{post.room_id}:posts", {event, post})
|
PubSub.broadcast(Aggiedit.PubSub, "room:#{post.room_id}:posts", {event, post})
|
||||||
|
20
lib/aggiedit/rooms/comment.ex
Normal file
20
lib/aggiedit/rooms/comment.ex
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
defmodule Aggiedit.Post.Comment do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
schema "post_comments" do
|
||||||
|
field :comment, :string
|
||||||
|
|
||||||
|
belongs_to :user, Aggiedit.Accounts.User
|
||||||
|
belongs_to :post, Aggiedit.Rooms.Post
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(comment, attrs) do
|
||||||
|
comment
|
||||||
|
|> cast(attrs, [:comment])
|
||||||
|
|> validate_required([:comment])
|
||||||
|
end
|
||||||
|
end
|
@ -5,18 +5,22 @@ defmodule Aggiedit.Rooms.Post do
|
|||||||
schema "posts" do
|
schema "posts" do
|
||||||
field :body, :string
|
field :body, :string
|
||||||
field :title, :string
|
field :title, :string
|
||||||
|
field :score, :integer
|
||||||
|
|
||||||
belongs_to :room, Aggiedit.Rooms.Room
|
belongs_to :room, Aggiedit.Rooms.Room
|
||||||
belongs_to :user, Aggiedit.Accounts.User
|
belongs_to :user, Aggiedit.Accounts.User
|
||||||
belongs_to :upload, Aggiedit.Uploads.Upload
|
belongs_to :upload, Aggiedit.Uploads.Upload
|
||||||
|
|
||||||
|
has_many :votes, Aggiedit.Post.Vote
|
||||||
|
has_many :comments, Aggiedit.Post.Comment
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(post, attrs) do
|
def changeset(post, attrs) do
|
||||||
post
|
post
|
||||||
|> cast(attrs, [:title, :body])
|
|> cast(attrs, [:title, :body, :score])
|
||||||
|> validate_required([:title, :body])
|
|> validate_required([:title, :body])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
20
lib/aggiedit/rooms/vote.ex
Normal file
20
lib/aggiedit/rooms/vote.ex
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
defmodule Aggiedit.Post.Vote do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
schema "post_votes" do
|
||||||
|
field :is_up, :boolean
|
||||||
|
|
||||||
|
belongs_to :user, Aggiedit.Accounts.User
|
||||||
|
belongs_to :post, Aggiedit.Rooms.Post
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(vote, attrs) do
|
||||||
|
vote
|
||||||
|
|> cast(attrs, [:direction])
|
||||||
|
|> validate_required([:direction])
|
||||||
|
end
|
||||||
|
end
|
@ -12,7 +12,14 @@ defmodule AggieditWeb.PostLive.Index do
|
|||||||
case socket.assigns do
|
case socket.assigns do
|
||||||
%{:room => room} ->
|
%{:room => room} ->
|
||||||
if connected?(socket), do: Rooms.subscribe(socket.assigns.room)
|
if connected?(socket), do: Rooms.subscribe(socket.assigns.room)
|
||||||
{:ok, assign(socket, %{:posts => room |> Repo.preload(posts: [:user, :upload]) |> Map.get(:posts)}), temporary_assigns: [posts: []]}
|
posts = room
|
||||||
|
|> Repo.preload(posts: [:user, :upload])
|
||||||
|
|> Map.get(:posts)
|
||||||
|
votes = socket.assigns.current_user
|
||||||
|
|> Repo.preload(:votes)
|
||||||
|
|> Map.get(:votes)
|
||||||
|
|> Enum.reduce(%{}, fn v, a -> Map.put(a, v.post_id, v) end)
|
||||||
|
{:ok, assign(socket, %{:posts => posts, :votes => votes}), temporary_assigns: [posts: []]}
|
||||||
_ -> {:ok, socket}
|
_ -> {:ok, socket}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -50,6 +57,16 @@ defmodule AggieditWeb.PostLive.Index do
|
|||||||
|> assign(:post, nil)
|
|> assign(:post, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event(vote, %{"id" => id}, socket) when vote in ["upvote", "downvote"] do
|
||||||
|
post = Rooms.get_post!(id)
|
||||||
|
if Roles.guard?(socket.assigns.current_user, :vote, post) do
|
||||||
|
Rooms.vote_post(post, socket.assigns.current_user, vote)
|
||||||
|
{:noreply, socket}
|
||||||
|
else
|
||||||
|
{:noreply, socket |> put_flash(:error, "You don't have permission to do that.") |> redirect(to: Routes.post_show_path(socket, :show, socket.assigns.room, post))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
post = Rooms.get_post!(id)
|
post = Rooms.get_post!(id)
|
||||||
@ -62,7 +79,7 @@ defmodule AggieditWeb.PostLive.Index do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({action, post}, socket) when action in [:post_created, :post_updated, :post_deleted] do
|
def handle_info({action, post}, socket) when action in [:post_created, :post_updated, :post_deleted, :post_voted] do
|
||||||
{:noreply, update(socket, :posts, fn posts ->
|
{:noreply, update(socket, :posts, fn posts ->
|
||||||
[posts | post]
|
[posts | post]
|
||||||
end)}
|
end)}
|
||||||
|
@ -18,13 +18,29 @@
|
|||||||
<div id="posts" phx-update="prepend">
|
<div id="posts" phx-update="prepend">
|
||||||
<%= for post <- @posts do %>
|
<%= for post <- @posts do %>
|
||||||
<div id={"post-#{post.id}"} class="card d-flex flex-row align-items-center p-2 m-2 shadow">
|
<div id={"post-#{post.id}"} class="card d-flex flex-row align-items-center p-2 m-2 shadow">
|
||||||
<%= if !is_nil(post.upload) do %>
|
<div class="d-flex flex-column m-2">
|
||||||
<%= live_redirect to: Routes.post_show_path(@socket, :show, @room, post) do %>
|
<%=
|
||||||
<div class="card-image d-flex justify-content-center" style="width: 100px">
|
has_vote = Map.has_key?(@votes, post.id)
|
||||||
|
is_upvote = has_vote && @votes[post.id].is_up
|
||||||
|
""
|
||||||
|
%>
|
||||||
|
<div class="d-flex">
|
||||||
|
<span><%= link "", to: "#", phx_click: "upvote", phx_value_id: post.id, class: "bi bi-arrow-up-circle#{if has_vote && is_upvote, do: "-fill", else: ""}" %></span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<%= post.score %>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<span><%= link "", to: "#", phx_click: "downvote", phx_value_id: post.id, class: "bi bi-arrow-down-circle#{if has_vote && !is_upvote, do: "-fill", else: ""}" %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-2 card-image d-flex justify-content-center" style="width: 100px">
|
||||||
|
<%= if !is_nil(post.upload) do %>
|
||||||
|
<%= live_redirect to: Routes.post_show_path(@socket, :show, @room, post) do %>
|
||||||
<img class="fluid-img thumbnail" src={Routes.static_path(@socket, "/uploads/#{post.upload.file}")} />
|
<img class="fluid-img thumbnail" src={Routes.static_path(@socket, "/uploads/#{post.upload.file}")} />
|
||||||
</div>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<%= live_redirect to: Routes.post_show_path(@socket, :show, @room, post) do %>
|
<%= live_redirect to: Routes.post_show_path(@socket, :show, @room, post) do %>
|
||||||
<h4 class="card-title"><%= post.title %></h4>
|
<h4 class="card-title"><%= post.title %></h4>
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-secondary min-vh-100">
|
<body class="bg-secondary min-vh-100">
|
||||||
<header>
|
<header>
|
||||||
|
@ -5,6 +5,7 @@ defmodule Aggiedit.Repo.Migrations.CreatePosts do
|
|||||||
create table(:posts) do
|
create table(:posts) do
|
||||||
add :title, :text
|
add :title, :text
|
||||||
add :body, :text
|
add :body, :text
|
||||||
|
add :score, :integer, default: 0
|
||||||
add :user_id, references(:users, on_delete: :nothing)
|
add :user_id, references(:users, on_delete: :nothing)
|
||||||
add :upload_id, references(:uploads, on_delete: :nothing)
|
add :upload_id, references(:uploads, on_delete: :nothing)
|
||||||
add :room_id, references(:rooms, on_delete: :nothing)
|
add :room_id, references(:rooms, on_delete: :nothing)
|
||||||
|
15
priv/repo/migrations/20220415015055_create_post_votes.exs
Normal file
15
priv/repo/migrations/20220415015055_create_post_votes.exs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
defmodule Aggiedit.Repo.Migrations.CreatePostVotes do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:post_votes) do
|
||||||
|
add :is_up, :boolean
|
||||||
|
add :user_id, references(:users, on_delete: :nothing)
|
||||||
|
add :post_id, references(:posts, on_delete: :delete_all)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create unique_index(:post_votes, [:user_id, :post_id])
|
||||||
|
end
|
||||||
|
end
|
16
priv/repo/migrations/20220415021530_create_post_comments.exs
Normal file
16
priv/repo/migrations/20220415021530_create_post_comments.exs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
defmodule Aggiedit.Repo.Migrations.CreatePostComments do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:post_comments) do
|
||||||
|
add :comment, :text
|
||||||
|
add :user_id, references(:users, on_delete: :delete_all)
|
||||||
|
add :post_id, references(:posts, on_delete: :delete_all)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(:post_comments, [:user_id])
|
||||||
|
create index(:post_comments, [:post_id])
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user