Skip to content

Commit

Permalink
Merge pull request #103 from epochtalk/move-thread
Browse files Browse the repository at this point in the history
Move thread
  • Loading branch information
unenglishable authored Aug 22, 2024
2 parents c1663ac + c1cfbc7 commit 2e293de
Show file tree
Hide file tree
Showing 23 changed files with 581 additions and 83 deletions.
4 changes: 2 additions & 2 deletions lib/epochtalk_server/models/board.ex
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ defmodule EpochtalkServer.Models.Board do
Creates a new `Board` in the database
"""
@spec create(board_attrs :: map()) :: {:ok, board :: t()} | {:error, Ecto.Changeset.t()}
def create(board) do
board_cs = create_changeset(%Board{}, board)
def create(board_attrs) do
board_cs = create_changeset(%Board{}, board_attrs)

case Repo.insert(board_cs) do
{:ok, db_board} ->
Expand Down
13 changes: 12 additions & 1 deletion lib/epochtalk_server/models/board_mapping.ex
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,18 @@ defmodule EpochtalkServer.Models.BoardMapping do
left_join: s in subquery(sticky_count_subquery),
on: bm.board_id == s.board_id,
select_merge: %{
stats: mb,
stats: %{
board_id: mb.board_id,
post_count: mb.post_count,
thread_count: mb.thread_count,
total_post: mb.total_post,
total_thread_count: mb.total_thread_count,
last_post_username: mb.last_post_username,
last_post_created_at: mb.last_post_created_at,
last_thread_id: mb.last_thread_id,
last_thread_title: mb.last_thread_title,
last_post_position: mb.last_post_position
},
thread: %{
last_thread_slug: t.slug,
last_thread_post_count: t.post_count,
Expand Down
4 changes: 2 additions & 2 deletions lib/epochtalk_server/models/category.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ defmodule EpochtalkServer.Models.Category do
|> Map.put(:updated_at, now)

category
|> cast(attrs, [:name, :viewable_by, :created_at, :updated_at])
|> cast(attrs, [:name, :view_order, :viewable_by, :postable_by, :created_at, :updated_at])
end

@doc """
Expand All @@ -79,7 +79,7 @@ defmodule EpochtalkServer.Models.Category do
Ecto.Changeset.t()
def update_for_board_mapping_changeset(category, attrs) do
category
|> cast(attrs, [:id, :name, :view_order, :viewable_by])
|> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by])
|> unique_constraint(:id, name: :categories_pkey)
end

Expand Down
67 changes: 67 additions & 0 deletions lib/epochtalk_server/models/metadata_board.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
defmodule EpochtalkServer.Models.MetadataBoard do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias EpochtalkServer.Repo
alias EpochtalkServer.Models.Board
alias EpochtalkServer.Models.Post
alias EpochtalkServer.Models.Thread
alias EpochtalkServer.Models.User
alias EpochtalkServer.Models.MetadataBoard

@moduledoc """
Expand Down Expand Up @@ -85,4 +89,67 @@ defmodule EpochtalkServer.Models.MetadataBoard do
@spec insert(metadata_board :: t()) ::
{:ok, metadata_board :: t()} | {:error, Ecto.Changeset.t()}
def insert(%MetadataBoard{} = metadata_board), do: Repo.insert(metadata_board)

@doc """
Queries then updates `MetadataBoard` info for the specified Board`
"""
@spec update_last_post_info(metadata_board :: t(), board_id :: non_neg_integer) :: t()
def update_last_post_info(metadata_board, board_id) do
# query most recent post in thread and it's authoring user's data
last_post_subquery =
from t in Thread,
left_join: p in Post,
on: t.id == p.thread_id,
left_join: u in User,
on: u.id == p.user_id,
where: t.board_id == ^board_id,
order_by: [desc: p.created_at],
select: %{
thread_id: p.thread_id,
created_at: p.created_at,
username: u.username,
position: p.position
}

# query most recent thread in board, join last post subquery
last_post_query =
from t in Thread,
left_join: p in Post,
on: p.thread_id == t.id,
left_join: lp in subquery(last_post_subquery),
on: p.thread_id == lp.thread_id,
where: t.board_id == ^board_id,
order_by: [desc: t.created_at],
limit: 1,
select: %{
board_id: t.board_id,
thread_id: t.id,
title: p.content["title"],
username: lp.username,
created_at: lp.created_at,
position: lp.position
}

# update board metadata using queried data
updated_metadata_board =
if lp = Repo.one(last_post_query) do
change(metadata_board,
last_post_username: lp.username,
last_post_created_at: lp.created_at,
last_thread_id: lp.thread_id,
last_thread_title: lp.title,
last_post_position: lp.position
)
else
change(metadata_board,
last_post_username: nil,
last_post_created_at: nil,
last_thread_id: nil,
last_thread_title: nil,
last_post_position: nil
)
end

Repo.update!(updated_metadata_board)
end
end
84 changes: 84 additions & 0 deletions lib/epochtalk_server/models/thread.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule EpochtalkServer.Models.Thread do
alias EpochtalkServer.Models.User
alias EpochtalkServer.Models.Thread
alias EpochtalkServer.Models.MetadataThread
alias EpochtalkServer.Models.MetadataBoard
alias EpochtalkServer.Models.Board
alias EpochtalkServer.Models.Poll
alias EpochtalkServer.Models.Post
Expand Down Expand Up @@ -243,6 +244,89 @@ defmodule EpochtalkServer.Models.Thread do
end
end

@doc """
Moves `Thread` to the specified `Board` given a `thread_id` and `board_id`
"""
@spec move(thread_id :: non_neg_integer, board_id :: non_neg_integer) ::
{:ok, thread :: t()} | {:error, Ecto.Changeset.t()}
def move(thread_id, board_id) do
case Repo.transaction(fn ->
# query and lock thread for update,
thread_lock_query =
from t in Thread,
where: t.id == ^thread_id,
select: t,
lock: "FOR UPDATE"

thread = Repo.one(thread_lock_query)

# prevent moving board to same board or non existent board
if thread.board_id != board_id && Repo.get_by(Board, id: board_id) != nil do
# query old board and lock for update
old_board_lock_query =
from b in Board,
join: mb in MetadataBoard,
on: mb.board_id == b.id,
where: b.id == ^thread.board_id,
select: {b, mb},
lock: "FOR UPDATE"

# locked old_board, reference this when updating
{old_board, old_board_meta} = Repo.one(old_board_lock_query)

# query new board and lock for update
new_board_lock_query =
from b in Board,
join: mb in MetadataBoard,
on: mb.board_id == b.id,
where: b.id == ^board_id,
select: {b, mb},
lock: "FOR UPDATE"

# locked new_board, reference this when updating
{new_board, new_board_meta} = Repo.one(new_board_lock_query)

# update old_board, thread and post count
old_board
|> change(
thread_count: old_board.thread_count - 1,
post_count: old_board.post_count - thread.post_count
)
|> Repo.update!()

# update new_board, thread and post count
new_board
|> change(
thread_count: new_board.thread_count + 1,
post_count: new_board.post_count + thread.post_count
)
|> Repo.update!()

# update thread's original board_id with new_board's id
thread
|> change(board_id: new_board.id)
|> Repo.update!()

# update last post metadata info of both the old board and new board
MetadataBoard.update_last_post_info(old_board_meta, old_board.id)
MetadataBoard.update_last_post_info(new_board_meta, new_board.id)

# return old board data for reference
%{old_board_name: old_board.name, old_board_id: old_board.id}
else
Repo.rollback(:invalid_board_id)
end
end) do
# transaction success return purged thread data
{:ok, thread_data} ->
{:ok, thread_data}

# some other error
{:error, cs} ->
{:error, cs}
end
end

@doc """
Sets boolean indicating if the specified `Thread` is sticky given a `Thread` id
"""
Expand Down
75 changes: 75 additions & 0 deletions lib/epochtalk_server_web/controllers/thread.ex
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,67 @@ defmodule EpochtalkServerWeb.Controllers.Thread do
end
end

@doc """
Used to move a `Thread`
"""
def move(conn, attrs) do
with user <- Guardian.Plug.current_resource(conn),
thread_id <- Validate.cast(attrs, "thread_id", :integer, required: true),
new_board_id <- Validate.cast(attrs, "new_board_id", :integer, required: true),
:ok <- ACL.allow!(conn, "threads.move"),
user_priority <- ACL.get_user_priority(conn),
{:can_read, {:ok, true}} <-
{:can_read, Board.get_read_access_by_thread_id(thread_id, user_priority)},
{:can_write, {:ok, true}} <-
{:can_write, Board.get_write_access_by_thread_id(thread_id, user_priority)},
{:is_active, true} <-
{:is_active, User.is_active?(user.id)},
{:board_banned, {:ok, false}} <-
{:board_banned, BoardBan.banned_from_board?(user, thread_id: thread_id)},
{:bypass_thread_owner, true} <-
{:bypass_thread_owner, can_authed_user_bypass_owner_on_thread_move(user, thread_id)},
{:ok, old_board_data} <- Thread.move(thread_id, new_board_id) do
render(conn, :move, old_board_data: old_board_data)
else
{:can_read, {:ok, false}} ->
ErrorHelpers.render_json_error(
conn,
403,
"Unauthorized, you do not have permission to read"
)

{:can_write, {:ok, false}} ->
ErrorHelpers.render_json_error(
conn,
403,
"Unauthorized, you do not have permission to write"
)

{:bypass_thread_owner, false} ->
ErrorHelpers.render_json_error(
conn,
403,
"Unauthorized, you do not have permission to move another user's thread"
)

{:board_banned, {:ok, true}} ->
ErrorHelpers.render_json_error(conn, 403, "Unauthorized, you are banned from this board")

{:is_active, false} ->
ErrorHelpers.render_json_error(
conn,
400,
"Account must be active to move thread"
)

{:error, data} ->
ErrorHelpers.render_json_error(conn, 400, data)

_ ->
ErrorHelpers.render_json_error(conn, 400, "Error, cannot move thread")
end
end

@doc """
Used to convert `Thread` slug to id
"""
Expand Down Expand Up @@ -685,4 +746,18 @@ defmodule EpochtalkServerWeb.Controllers.Thread do
true
)
end

defp can_authed_user_bypass_owner_on_thread_move(user, thread_id) do
post = Thread.get_first_post_data_by_id(thread_id)

ACL.bypass_post_owner(
user,
post,
"threads.move",
"owner",
false,
true,
true
)
end
end
17 changes: 11 additions & 6 deletions lib/epochtalk_server_web/json/board_json.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ defmodule EpochtalkServerWeb.Controllers.BoardJSON do
# flatten needed boards data
board =
board
|> Map.merge(to_map_remove_nil(board.board))
|> Map.merge(remove_nil(board.board))
|> Map.merge(
to_map_remove_nil(board.stats)
remove_nil(board.stats)
|> Map.delete(:id)
)
|> Map.merge(board.thread)
Expand Down Expand Up @@ -157,8 +157,8 @@ defmodule EpochtalkServerWeb.Controllers.BoardJSON do
# flatten needed boards data
board =
board
|> Map.merge(to_map_remove_nil(board.board))
|> Map.merge(to_map_remove_nil(board.stats))
|> Map.merge(remove_nil(board.board))
|> Map.merge(remove_nil(board.stats))
|> Map.merge(board.thread)

# delete unneeded properties
Expand Down Expand Up @@ -204,11 +204,16 @@ defmodule EpochtalkServerWeb.Controllers.BoardJSON do
parent
end

defp to_map_remove_nil(nil), do: %{}
defp remove_nil(nil), do: %{}

defp to_map_remove_nil(struct) do
defp remove_nil(struct) when is_struct(struct) do
struct
|> Map.from_struct()
|> remove_nil()
end

defp remove_nil(map) when is_map(map) do
map
|> Enum.reject(fn {_, v} -> is_nil(v) end)
|> Map.new()
end
Expand Down
13 changes: 13 additions & 0 deletions lib/epochtalk_server_web/json/thread_json.ex
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ defmodule EpochtalkServerWeb.Controllers.ThreadJSON do
def purge(%{thread: thread}),
do: thread

@doc """
Renders move `Thread`.
iex> old_board_data = %{
iex> old_board_id: 2,
iex> old_board_name: "General Discussion"
iex> }
iex> EpochtalkServerWeb.Controllers.ThreadJSON.move(%{old_board_data: old_board_data})
old_board_data
"""
def move(%{old_board_data: old_board_data}),
do: old_board_data

@doc """
Renders `Thread` id for slug to id route.
"""
Expand Down
1 change: 1 addition & 0 deletions lib/epochtalk_server_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ defmodule EpochtalkServerWeb.Router do
post "/threads", Thread, :create
post "/threads/:thread_id/lock", Thread, :lock
post "/threads/:thread_id/sticky", Thread, :sticky
post "/threads/:thread_id/move", Thread, :move
delete "/threads/:thread_id", Thread, :purge
post "/threads/:thread_id/polls/vote", Poll, :vote
delete "/threads/:thread_id/polls/vote", Poll, :delete_vote
Expand Down
Loading

0 comments on commit 2e293de

Please sign in to comment.