Skip to content

Commit 2e293de

Browse files
Merge pull request #103 from epochtalk/move-thread
Move thread
2 parents c1663ac + c1cfbc7 commit 2e293de

File tree

23 files changed

+581
-83
lines changed

23 files changed

+581
-83
lines changed

lib/epochtalk_server/models/board.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ defmodule EpochtalkServer.Models.Board do
124124
Creates a new `Board` in the database
125125
"""
126126
@spec create(board_attrs :: map()) :: {:ok, board :: t()} | {:error, Ecto.Changeset.t()}
127-
def create(board) do
128-
board_cs = create_changeset(%Board{}, board)
127+
def create(board_attrs) do
128+
board_cs = create_changeset(%Board{}, board_attrs)
129129

130130
case Repo.insert(board_cs) do
131131
{:ok, db_board} ->

lib/epochtalk_server/models/board_mapping.ex

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,18 @@ defmodule EpochtalkServer.Models.BoardMapping do
112112
left_join: s in subquery(sticky_count_subquery),
113113
on: bm.board_id == s.board_id,
114114
select_merge: %{
115-
stats: mb,
115+
stats: %{
116+
board_id: mb.board_id,
117+
post_count: mb.post_count,
118+
thread_count: mb.thread_count,
119+
total_post: mb.total_post,
120+
total_thread_count: mb.total_thread_count,
121+
last_post_username: mb.last_post_username,
122+
last_post_created_at: mb.last_post_created_at,
123+
last_thread_id: mb.last_thread_id,
124+
last_thread_title: mb.last_thread_title,
125+
last_post_position: mb.last_post_position
126+
},
116127
thread: %{
117128
last_thread_slug: t.slug,
118129
last_thread_post_count: t.post_count,

lib/epochtalk_server/models/category.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ defmodule EpochtalkServer.Models.Category do
6969
|> Map.put(:updated_at, now)
7070

7171
category
72-
|> cast(attrs, [:name, :viewable_by, :created_at, :updated_at])
72+
|> cast(attrs, [:name, :view_order, :viewable_by, :postable_by, :created_at, :updated_at])
7373
end
7474

7575
@doc """
@@ -79,7 +79,7 @@ defmodule EpochtalkServer.Models.Category do
7979
Ecto.Changeset.t()
8080
def update_for_board_mapping_changeset(category, attrs) do
8181
category
82-
|> cast(attrs, [:id, :name, :view_order, :viewable_by])
82+
|> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by])
8383
|> unique_constraint(:id, name: :categories_pkey)
8484
end
8585

lib/epochtalk_server/models/metadata_board.ex

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
defmodule EpochtalkServer.Models.MetadataBoard do
22
use Ecto.Schema
33
import Ecto.Changeset
4+
import Ecto.Query
45
alias EpochtalkServer.Repo
56
alias EpochtalkServer.Models.Board
7+
alias EpochtalkServer.Models.Post
8+
alias EpochtalkServer.Models.Thread
9+
alias EpochtalkServer.Models.User
610
alias EpochtalkServer.Models.MetadataBoard
711

812
@moduledoc """
@@ -85,4 +89,67 @@ defmodule EpochtalkServer.Models.MetadataBoard do
8589
@spec insert(metadata_board :: t()) ::
8690
{:ok, metadata_board :: t()} | {:error, Ecto.Changeset.t()}
8791
def insert(%MetadataBoard{} = metadata_board), do: Repo.insert(metadata_board)
92+
93+
@doc """
94+
Queries then updates `MetadataBoard` info for the specified Board`
95+
"""
96+
@spec update_last_post_info(metadata_board :: t(), board_id :: non_neg_integer) :: t()
97+
def update_last_post_info(metadata_board, board_id) do
98+
# query most recent post in thread and it's authoring user's data
99+
last_post_subquery =
100+
from t in Thread,
101+
left_join: p in Post,
102+
on: t.id == p.thread_id,
103+
left_join: u in User,
104+
on: u.id == p.user_id,
105+
where: t.board_id == ^board_id,
106+
order_by: [desc: p.created_at],
107+
select: %{
108+
thread_id: p.thread_id,
109+
created_at: p.created_at,
110+
username: u.username,
111+
position: p.position
112+
}
113+
114+
# query most recent thread in board, join last post subquery
115+
last_post_query =
116+
from t in Thread,
117+
left_join: p in Post,
118+
on: p.thread_id == t.id,
119+
left_join: lp in subquery(last_post_subquery),
120+
on: p.thread_id == lp.thread_id,
121+
where: t.board_id == ^board_id,
122+
order_by: [desc: t.created_at],
123+
limit: 1,
124+
select: %{
125+
board_id: t.board_id,
126+
thread_id: t.id,
127+
title: p.content["title"],
128+
username: lp.username,
129+
created_at: lp.created_at,
130+
position: lp.position
131+
}
132+
133+
# update board metadata using queried data
134+
updated_metadata_board =
135+
if lp = Repo.one(last_post_query) do
136+
change(metadata_board,
137+
last_post_username: lp.username,
138+
last_post_created_at: lp.created_at,
139+
last_thread_id: lp.thread_id,
140+
last_thread_title: lp.title,
141+
last_post_position: lp.position
142+
)
143+
else
144+
change(metadata_board,
145+
last_post_username: nil,
146+
last_post_created_at: nil,
147+
last_thread_id: nil,
148+
last_thread_title: nil,
149+
last_post_position: nil
150+
)
151+
end
152+
153+
Repo.update!(updated_metadata_board)
154+
end
88155
end

lib/epochtalk_server/models/thread.ex

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ defmodule EpochtalkServer.Models.Thread do
77
alias EpochtalkServer.Models.User
88
alias EpochtalkServer.Models.Thread
99
alias EpochtalkServer.Models.MetadataThread
10+
alias EpochtalkServer.Models.MetadataBoard
1011
alias EpochtalkServer.Models.Board
1112
alias EpochtalkServer.Models.Poll
1213
alias EpochtalkServer.Models.Post
@@ -243,6 +244,89 @@ defmodule EpochtalkServer.Models.Thread do
243244
end
244245
end
245246

247+
@doc """
248+
Moves `Thread` to the specified `Board` given a `thread_id` and `board_id`
249+
"""
250+
@spec move(thread_id :: non_neg_integer, board_id :: non_neg_integer) ::
251+
{:ok, thread :: t()} | {:error, Ecto.Changeset.t()}
252+
def move(thread_id, board_id) do
253+
case Repo.transaction(fn ->
254+
# query and lock thread for update,
255+
thread_lock_query =
256+
from t in Thread,
257+
where: t.id == ^thread_id,
258+
select: t,
259+
lock: "FOR UPDATE"
260+
261+
thread = Repo.one(thread_lock_query)
262+
263+
# prevent moving board to same board or non existent board
264+
if thread.board_id != board_id && Repo.get_by(Board, id: board_id) != nil do
265+
# query old board and lock for update
266+
old_board_lock_query =
267+
from b in Board,
268+
join: mb in MetadataBoard,
269+
on: mb.board_id == b.id,
270+
where: b.id == ^thread.board_id,
271+
select: {b, mb},
272+
lock: "FOR UPDATE"
273+
274+
# locked old_board, reference this when updating
275+
{old_board, old_board_meta} = Repo.one(old_board_lock_query)
276+
277+
# query new board and lock for update
278+
new_board_lock_query =
279+
from b in Board,
280+
join: mb in MetadataBoard,
281+
on: mb.board_id == b.id,
282+
where: b.id == ^board_id,
283+
select: {b, mb},
284+
lock: "FOR UPDATE"
285+
286+
# locked new_board, reference this when updating
287+
{new_board, new_board_meta} = Repo.one(new_board_lock_query)
288+
289+
# update old_board, thread and post count
290+
old_board
291+
|> change(
292+
thread_count: old_board.thread_count - 1,
293+
post_count: old_board.post_count - thread.post_count
294+
)
295+
|> Repo.update!()
296+
297+
# update new_board, thread and post count
298+
new_board
299+
|> change(
300+
thread_count: new_board.thread_count + 1,
301+
post_count: new_board.post_count + thread.post_count
302+
)
303+
|> Repo.update!()
304+
305+
# update thread's original board_id with new_board's id
306+
thread
307+
|> change(board_id: new_board.id)
308+
|> Repo.update!()
309+
310+
# update last post metadata info of both the old board and new board
311+
MetadataBoard.update_last_post_info(old_board_meta, old_board.id)
312+
MetadataBoard.update_last_post_info(new_board_meta, new_board.id)
313+
314+
# return old board data for reference
315+
%{old_board_name: old_board.name, old_board_id: old_board.id}
316+
else
317+
Repo.rollback(:invalid_board_id)
318+
end
319+
end) do
320+
# transaction success return purged thread data
321+
{:ok, thread_data} ->
322+
{:ok, thread_data}
323+
324+
# some other error
325+
{:error, cs} ->
326+
{:error, cs}
327+
end
328+
end
329+
246330
@doc """
247331
Sets boolean indicating if the specified `Thread` is sticky given a `Thread` id
248332
"""

lib/epochtalk_server_web/controllers/thread.ex

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,67 @@ defmodule EpochtalkServerWeb.Controllers.Thread do
512512
end
513513
end
514514

515+
@doc """
516+
Used to move a `Thread`
517+
"""
518+
def move(conn, attrs) do
519+
with user <- Guardian.Plug.current_resource(conn),
520+
thread_id <- Validate.cast(attrs, "thread_id", :integer, required: true),
521+
new_board_id <- Validate.cast(attrs, "new_board_id", :integer, required: true),
522+
:ok <- ACL.allow!(conn, "threads.move"),
523+
user_priority <- ACL.get_user_priority(conn),
524+
{:can_read, {:ok, true}} <-
525+
{:can_read, Board.get_read_access_by_thread_id(thread_id, user_priority)},
526+
{:can_write, {:ok, true}} <-
527+
{:can_write, Board.get_write_access_by_thread_id(thread_id, user_priority)},
528+
{:is_active, true} <-
529+
{:is_active, User.is_active?(user.id)},
530+
{:board_banned, {:ok, false}} <-
531+
{:board_banned, BoardBan.banned_from_board?(user, thread_id: thread_id)},
532+
{:bypass_thread_owner, true} <-
533+
{:bypass_thread_owner, can_authed_user_bypass_owner_on_thread_move(user, thread_id)},
534+
{:ok, old_board_data} <- Thread.move(thread_id, new_board_id) do
535+
render(conn, :move, old_board_data: old_board_data)
536+
else
537+
{:can_read, {:ok, false}} ->
538+
ErrorHelpers.render_json_error(
539+
conn,
540+
403,
541+
"Unauthorized, you do not have permission to read"
542+
)
543+
544+
{:can_write, {:ok, false}} ->
545+
ErrorHelpers.render_json_error(
546+
conn,
547+
403,
548+
"Unauthorized, you do not have permission to write"
549+
)
550+
551+
{:bypass_thread_owner, false} ->
552+
ErrorHelpers.render_json_error(
553+
conn,
554+
403,
555+
"Unauthorized, you do not have permission to move another user's thread"
556+
)
557+
558+
{:board_banned, {:ok, true}} ->
559+
ErrorHelpers.render_json_error(conn, 403, "Unauthorized, you are banned from this board")
560+
561+
{:is_active, false} ->
562+
ErrorHelpers.render_json_error(
563+
conn,
564+
400,
565+
"Account must be active to move thread"
566+
)
567+
568+
{:error, data} ->
569+
ErrorHelpers.render_json_error(conn, 400, data)
570+
571+
_ ->
572+
ErrorHelpers.render_json_error(conn, 400, "Error, cannot move thread")
573+
end
574+
end
575+
515576
@doc """
516577
Used to convert `Thread` slug to id
517578
"""
@@ -685,4 +746,18 @@ defmodule EpochtalkServerWeb.Controllers.Thread do
685746
true
686747
)
687748
end
749+
750+
defp can_authed_user_bypass_owner_on_thread_move(user, thread_id) do
751+
post = Thread.get_first_post_data_by_id(thread_id)
752+
753+
ACL.bypass_post_owner(
754+
user,
755+
post,
756+
"threads.move",
757+
"owner",
758+
false,
759+
true,
760+
true
761+
)
762+
end
688763
end

lib/epochtalk_server_web/json/board_json.ex

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ defmodule EpochtalkServerWeb.Controllers.BoardJSON do
9090
# flatten needed boards data
9191
board =
9292
board
93-
|> Map.merge(to_map_remove_nil(board.board))
93+
|> Map.merge(remove_nil(board.board))
9494
|> Map.merge(
95-
to_map_remove_nil(board.stats)
95+
remove_nil(board.stats)
9696
|> Map.delete(:id)
9797
)
9898
|> Map.merge(board.thread)
@@ -157,8 +157,8 @@ defmodule EpochtalkServerWeb.Controllers.BoardJSON do
157157
# flatten needed boards data
158158
board =
159159
board
160-
|> Map.merge(to_map_remove_nil(board.board))
161-
|> Map.merge(to_map_remove_nil(board.stats))
160+
|> Map.merge(remove_nil(board.board))
161+
|> Map.merge(remove_nil(board.stats))
162162
|> Map.merge(board.thread)
163163

164164
# delete unneeded properties
@@ -204,11 +204,16 @@ defmodule EpochtalkServerWeb.Controllers.BoardJSON do
204204
parent
205205
end
206206

207-
defp to_map_remove_nil(nil), do: %{}
207+
defp remove_nil(nil), do: %{}
208208

209-
defp to_map_remove_nil(struct) do
209+
defp remove_nil(struct) when is_struct(struct) do
210210
struct
211211
|> Map.from_struct()
212+
|> remove_nil()
213+
end
214+
215+
defp remove_nil(map) when is_map(map) do
216+
map
212217
|> Enum.reject(fn {_, v} -> is_nil(v) end)
213218
|> Map.new()
214219
end

lib/epochtalk_server_web/json/thread_json.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,19 @@ defmodule EpochtalkServerWeb.Controllers.ThreadJSON do
140140
def purge(%{thread: thread}),
141141
do: thread
142142

143+
@doc """
144+
Renders move `Thread`.
145+
146+
iex> old_board_data = %{
147+
iex> old_board_id: 2,
148+
iex> old_board_name: "General Discussion"
149+
iex> }
150+
iex> EpochtalkServerWeb.Controllers.ThreadJSON.move(%{old_board_data: old_board_data})
151+
old_board_data
152+
"""
153+
def move(%{old_board_data: old_board_data}),
154+
do: old_board_data
155+
143156
@doc """
144157
Renders `Thread` id for slug to id route.
145158
"""

lib/epochtalk_server_web/router.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ defmodule EpochtalkServerWeb.Router do
4848
post "/threads", Thread, :create
4949
post "/threads/:thread_id/lock", Thread, :lock
5050
post "/threads/:thread_id/sticky", Thread, :sticky
51+
post "/threads/:thread_id/move", Thread, :move
5152
delete "/threads/:thread_id", Thread, :purge
5253
post "/threads/:thread_id/polls/vote", Poll, :vote
5354
delete "/threads/:thread_id/polls/vote", Poll, :delete_vote

0 commit comments

Comments
 (0)