From 3cf9f4a364ac91cca30799c8379a682139425e71 Mon Sep 17 00:00:00 2001 From: Logan Hunt Date: Fri, 15 Apr 2022 13:00:42 -0600 Subject: [PATCH] Add comments and vote models; pub/sub voting on posts --- lib/aggiedit/accounts/user.ex | 2 ++ lib/aggiedit/roles.ex | 2 +- lib/aggiedit/rooms.ex | 21 ++++++++++++++- lib/aggiedit/rooms/comment.ex | 20 ++++++++++++++ lib/aggiedit/rooms/post.ex | 6 ++++- lib/aggiedit/rooms/vote.ex | 20 ++++++++++++++ lib/aggiedit_web/live/post_live/index.ex | 21 +++++++++++++-- .../live/post_live/index.html.heex | 26 +++++++++++++++---- .../templates/layout/root.html.heex | 1 + .../20220406185124_create_posts.exs | 1 + .../20220415015055_create_post_votes.exs | 15 +++++++++++ .../20220415021530_create_post_comments.exs | 16 ++++++++++++ 12 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 lib/aggiedit/rooms/comment.ex create mode 100644 lib/aggiedit/rooms/vote.ex create mode 100644 priv/repo/migrations/20220415015055_create_post_votes.exs create mode 100644 priv/repo/migrations/20220415021530_create_post_comments.exs diff --git a/lib/aggiedit/accounts/user.ex b/lib/aggiedit/accounts/user.ex index 65c2463..45c6525 100644 --- a/lib/aggiedit/accounts/user.ex +++ b/lib/aggiedit/accounts/user.ex @@ -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 diff --git a/lib/aggiedit/roles.ex b/lib/aggiedit/roles.ex index 3ec6bfd..41da54a 100644 --- a/lib/aggiedit/roles.ex +++ b/lib/aggiedit/roles.ex @@ -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 diff --git a/lib/aggiedit/rooms.ex b/lib/aggiedit/rooms.ex index 898dfe1..41df588 100644 --- a/lib/aggiedit/rooms.ex +++ b/lib/aggiedit/rooms.ex @@ -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}) diff --git a/lib/aggiedit/rooms/comment.ex b/lib/aggiedit/rooms/comment.ex new file mode 100644 index 0000000..6747ba3 --- /dev/null +++ b/lib/aggiedit/rooms/comment.ex @@ -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 diff --git a/lib/aggiedit/rooms/post.ex b/lib/aggiedit/rooms/post.ex index ee9450d..e1aa59a 100644 --- a/lib/aggiedit/rooms/post.ex +++ b/lib/aggiedit/rooms/post.ex @@ -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 diff --git a/lib/aggiedit/rooms/vote.ex b/lib/aggiedit/rooms/vote.ex new file mode 100644 index 0000000..f402e87 --- /dev/null +++ b/lib/aggiedit/rooms/vote.ex @@ -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 diff --git a/lib/aggiedit_web/live/post_live/index.ex b/lib/aggiedit_web/live/post_live/index.ex index 59ec234..d231ea0 100644 --- a/lib/aggiedit_web/live/post_live/index.ex +++ b/lib/aggiedit_web/live/post_live/index.ex @@ -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)} diff --git a/lib/aggiedit_web/live/post_live/index.html.heex b/lib/aggiedit_web/live/post_live/index.html.heex index efb42cb..89767f8 100644 --- a/lib/aggiedit_web/live/post_live/index.html.heex +++ b/lib/aggiedit_web/live/post_live/index.html.heex @@ -18,13 +18,29 @@
<%= for post <- @posts do %>
- <%= if !is_nil(post.upload) do %> - <%= live_redirect to: Routes.post_show_path(@socket, :show, @room, post) do %> -
+
+ <%= + has_vote = Map.has_key?(@votes, post.id) + is_upvote = has_vote && @votes[post.id].is_up + "" + %> +
+ <%= link "", to: "#", phx_click: "upvote", phx_value_id: post.id, class: "bi bi-arrow-up-circle#{if has_vote && is_upvote, do: "-fill", else: ""}" %> +
+
+ <%= post.score %> +
+
+ <%= link "", to: "#", phx_click: "downvote", phx_value_id: post.id, class: "bi bi-arrow-down-circle#{if has_vote && !is_upvote, do: "-fill", else: ""}" %> +
+
+
+ <%= if !is_nil(post.upload) do %> + <%= live_redirect to: Routes.post_show_path(@socket, :show, @room, post) do %> -
+ <% end %> <% end %> - <% end %> +
<%= live_redirect to: Routes.post_show_path(@socket, :show, @room, post) do %>

<%= post.title %>

diff --git a/lib/aggiedit_web/templates/layout/root.html.heex b/lib/aggiedit_web/templates/layout/root.html.heex index a164571..14c7605 100644 --- a/lib/aggiedit_web/templates/layout/root.html.heex +++ b/lib/aggiedit_web/templates/layout/root.html.heex @@ -11,6 +11,7 @@ +
diff --git a/priv/repo/migrations/20220406185124_create_posts.exs b/priv/repo/migrations/20220406185124_create_posts.exs index 223265c..9febae8 100644 --- a/priv/repo/migrations/20220406185124_create_posts.exs +++ b/priv/repo/migrations/20220406185124_create_posts.exs @@ -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) diff --git a/priv/repo/migrations/20220415015055_create_post_votes.exs b/priv/repo/migrations/20220415015055_create_post_votes.exs new file mode 100644 index 0000000..444a89c --- /dev/null +++ b/priv/repo/migrations/20220415015055_create_post_votes.exs @@ -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 diff --git a/priv/repo/migrations/20220415021530_create_post_comments.exs b/priv/repo/migrations/20220415021530_create_post_comments.exs new file mode 100644 index 0000000..14797a2 --- /dev/null +++ b/priv/repo/migrations/20220415021530_create_post_comments.exs @@ -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