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
|
||||
has_many :posts, Aggiedit.Rooms.Post
|
||||
has_many :votes, Aggiedit.Post.Vote
|
||||
has_many :comments, Aggiedit.Post.Comment
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
@ -6,7 +6,7 @@ defmodule Aggiedit.Roles do
|
||||
def guard?(user, action, object)
|
||||
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}, :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?(_, _, _), do: false
|
||||
|
||||
|
@ -6,9 +6,11 @@ defmodule Aggiedit.Rooms do
|
||||
import Ecto.Query, warn: false
|
||||
alias Aggiedit.Repo
|
||||
|
||||
alias Aggiedit.Accounts
|
||||
alias Aggiedit.Accounts.User
|
||||
alias Aggiedit.Rooms.Room
|
||||
|
||||
alias Aggiedit.Post.{Vote, Comment}
|
||||
|
||||
alias Phoenix.PubSub
|
||||
|
||||
def list_rooms do
|
||||
@ -91,6 +93,23 @@ defmodule Aggiedit.Rooms do
|
||||
Post.changeset(post, attrs)
|
||||
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({:ok, post}, event) do
|
||||
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
|
||||
field :body, :string
|
||||
field :title, :string
|
||||
field :score, :integer
|
||||
|
||||
belongs_to :room, Aggiedit.Rooms.Room
|
||||
belongs_to :user, Aggiedit.Accounts.User
|
||||
belongs_to :upload, Aggiedit.Uploads.Upload
|
||||
|
||||
has_many :votes, Aggiedit.Post.Vote
|
||||
has_many :comments, Aggiedit.Post.Comment
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(post, attrs) do
|
||||
post
|
||||
|> cast(attrs, [:title, :body])
|
||||
|> cast(attrs, [:title, :body, :score])
|
||||
|> validate_required([:title, :body])
|
||||
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
|
||||
%{:room => 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}
|
||||
end
|
||||
end
|
||||
@ -50,6 +57,16 @@ defmodule AggieditWeb.PostLive.Index do
|
||||
|> assign(:post, nil)
|
||||
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
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
post = Rooms.get_post!(id)
|
||||
@ -62,7 +79,7 @@ defmodule AggieditWeb.PostLive.Index do
|
||||
end
|
||||
|
||||
@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 ->
|
||||
[posts | post]
|
||||
end)}
|
||||
|
@ -18,13 +18,29 @@
|
||||
<div id="posts" phx-update="prepend">
|
||||
<%= for post <- @posts do %>
|
||||
<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 %>
|
||||
<%= live_redirect to: Routes.post_show_path(@socket, :show, @room, post) do %>
|
||||
<div class="card-image d-flex justify-content-center" style="width: 100px">
|
||||
<div class="d-flex flex-column m-2">
|
||||
<%=
|
||||
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}")} />
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<%= live_redirect to: Routes.post_show_path(@socket, :show, @room, post) do %>
|
||||
<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">
|
||||
<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>
|
||||
<body class="bg-secondary min-vh-100">
|
||||
<header>
|
||||
|
@ -5,6 +5,7 @@ defmodule Aggiedit.Repo.Migrations.CreatePosts do
|
||||
create table(:posts) do
|
||||
add :title, :text
|
||||
add :body, :text
|
||||
add :score, :integer, default: 0
|
||||
add :user_id, references(:users, on_delete: :nothing)
|
||||
add :upload_id, references(:uploads, 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