Skip to content

Commit 0339fd5

Browse files
authored
Merge pull request #49 from epochtalk/breadcrumbs
Breadcrumbs
2 parents a677cc3 + 44349c0 commit 0339fd5

File tree

9 files changed

+663
-23
lines changed

9 files changed

+663
-23
lines changed

lib/epochtalk_server/models/board.ex

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -163,31 +163,9 @@ defmodule EpochtalkServer.Models.Board do
163163
@spec get_read_access_by_id(id :: non_neg_integer, user_priority :: non_neg_integer) ::
164164
{:ok, can_read :: boolean} | {:error, :board_does_not_exist}
165165
def get_read_access_by_id(id, user_priority) do
166-
find_parent_initial_query =
167-
BoardMapping
168-
|> where([bm], bm.board_id == ^id)
169-
|> select([bm], %{
170-
board_id: bm.board_id,
171-
parent_id: bm.parent_id,
172-
category_id: bm.category_id
173-
})
174-
175-
find_parent_recursion_query =
176-
BoardMapping
177-
|> join(:inner, [bm], fp in "find_parent", on: bm.board_id == fp.parent_id)
178-
|> select([bm], %{
179-
board_id: bm.board_id,
180-
parent_id: bm.parent_id,
181-
category_id: bm.category_id
182-
})
183-
184-
find_parent_query =
185-
find_parent_initial_query
186-
|> union(^find_parent_recursion_query)
187-
188166
Board
189167
|> recursive_ctes(true)
190-
|> with_cte("find_parent", as: ^find_parent_query)
168+
|> with_cte("find_parent", as: ^get_parent_query(id))
191169
|> join(:inner, [b], fp in "find_parent", on: b.id == fp.board_id)
192170
|> join(:left, [b, fp], c in Category, on: c.id == fp.category_id)
193171
|> select([b, fp, c], %{
@@ -236,4 +214,65 @@ defmodule EpochtalkServer.Models.Board do
236214
do: {:ok, id},
237215
else: {:error, :board_does_not_exist}
238216
end
217+
218+
@doc """
219+
Used to obtain breadcrumb data for a specific `Board` given it's `slug`
220+
"""
221+
@spec breadcrumb(slug :: String.t()) ::
222+
{:ok, board :: t()} | {:error, :board_does_not_exist}
223+
def breadcrumb(slug) when is_binary(slug) do
224+
case slug_to_id(slug) do
225+
{:ok, id} ->
226+
Board
227+
|> recursive_ctes(true)
228+
|> with_cte("find_parent", as: ^get_parent_query(id))
229+
|> join(:inner, [b], fp in "find_parent", on: b.id == fp.board_id)
230+
|> join(:left, [b, fp], b2 in Board, on: b2.id == fp.parent_id)
231+
|> join(:left, [b, fp, b2], c in Category, on: c.id == fp.category_id)
232+
|> select([b, fp, b2, c], %{
233+
id: b.id,
234+
name: b.name,
235+
parent_slug: b2.slug,
236+
board_id: fp.board_id,
237+
parent_id: fp.parent_id,
238+
category_id: fp.category_id
239+
})
240+
|> Repo.all()
241+
|> case do
242+
[] ->
243+
{:error, :board_does_not_exist}
244+
245+
board_and_parents ->
246+
{:ok, board_and_parents}
247+
end
248+
249+
{:error, :board_does_not_exist} ->
250+
{:error, :board_does_not_exist}
251+
end
252+
end
253+
254+
## === Private Helper Functions ===
255+
256+
defp get_parent_query(id) when is_integer(id) do
257+
find_parent_initial_query =
258+
BoardMapping
259+
|> where([bm], bm.board_id == ^id)
260+
|> select([bm], %{
261+
board_id: bm.board_id,
262+
parent_id: bm.parent_id,
263+
category_id: bm.category_id
264+
})
265+
266+
find_parent_recursion_query =
267+
BoardMapping
268+
|> join(:inner, [bm], fp in "find_parent", on: bm.board_id == fp.parent_id)
269+
|> select([bm], %{
270+
board_id: bm.board_id,
271+
parent_id: bm.parent_id,
272+
category_id: bm.category_id
273+
})
274+
275+
find_parent_initial_query
276+
|> union(^find_parent_recursion_query)
277+
end
239278
end

lib/epochtalk_server/models/category.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,17 @@ defmodule EpochtalkServer.Models.Category do
110110
"""
111111
@spec all() :: [t()]
112112
def all(), do: Repo.all(from Category, order_by: [asc: :view_order])
113+
114+
@doc """
115+
Used to find a specific `Category` by it's `id`
116+
"""
117+
@spec find_by_id(id :: non_neg_integer) ::
118+
{:ok, category :: t()} | {:error, :category_does_not_exist}
119+
def find_by_id(id) when is_integer(id) do
120+
category = Repo.one(from c in Category, where: c.id == ^id)
121+
122+
if category,
123+
do: {:ok, category},
124+
else: {:error, :category_does_not_exist}
125+
end
113126
end

lib/epochtalk_server/models/post.ex

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,138 @@ defmodule EpochtalkServer.Models.Post do
171171
from(p in Post, where: p.id == ^id)
172172
|> Repo.update_all(set: [position: thread_post_count + 1])
173173
end
174+
175+
@doc """
176+
Paginates `Post` records for a given a `Thread`
177+
"""
178+
@spec page_by_thread_id(
179+
thread_id :: non_neg_integer,
180+
page :: non_neg_integer | nil,
181+
opts :: list() | nil
182+
) :: [map()] | []
183+
def page_by_thread_id(thread_id, page \\ 1, opts \\ []) do
184+
per_page = Keyword.get(opts, :per_page, 25)
185+
user_id = Keyword.get(opts, :user_id)
186+
page = if start = Keyword.get(opts, :start), do: ceil(start / per_page), else: page
187+
start = page * per_page - per_page
188+
start = if start < 0, do: 0, else: start
189+
190+
inner_query =
191+
Post
192+
|> where([p], p.thread_id == ^thread_id and p.position > ^start)
193+
|> order_by([p], p.position)
194+
|> limit(^per_page)
195+
|> select([p], %{id: p.id, position: p.position})
196+
197+
from(plist in subquery(inner_query))
198+
|> join(
199+
:left_lateral,
200+
[plist],
201+
p1 in fragment(
202+
"""
203+
SELECT
204+
p.thread_id, t.board_id, t.slug, b.right_to_left, p.user_id, p.content ->> \'title\' as title,
205+
p.content ->> \'body\' as body, p.metadata, p.deleted, p.locked, p.created_at,
206+
p.updated_at, p.imported_at, CASE WHEN EXISTS (
207+
SELECT rp.id
208+
FROM administration.reports_posts rp
209+
WHERE rp.offender_post_id = p.id AND rp.reporter_user_id = ?
210+
)
211+
THEN \'TRUE\'::boolean ELSE \'FALSE\'::boolean END AS reported,
212+
CASE WHEN EXISTS (
213+
SELECT ru.id
214+
FROM administration.reports_users ru
215+
WHERE ru.offender_user_id = p.user_id AND ru.reporter_user_id = ? AND ru.status = \'Pending\'
216+
)
217+
THEN \'TRUE\'::boolean ELSE \'FALSE\'::boolean END AS reported_author,
218+
CASE WHEN p.user_id = (
219+
SELECT user_id
220+
FROM posts
221+
WHERE thread_id = t.id ORDER BY created_at limit 1
222+
)
223+
THEN \'TRUE\'::boolean ELSE \'FALSE\'::boolean END AS original_poster,
224+
u.username, u.deleted as user_deleted, up.signature, up.post_count, up.avatar,
225+
up.fields->\'name\' as name
226+
FROM posts p
227+
LEFT JOIN users u ON p.user_id = u.id
228+
LEFT JOIN users.profiles up ON u.id = up.user_id
229+
LEFT JOIN threads t ON p.thread_id = t.id
230+
LEFT JOIN boards b ON t.board_id = b.id
231+
WHERE p.id = ?
232+
""",
233+
^user_id,
234+
^user_id,
235+
plist.id
236+
),
237+
on: true
238+
)
239+
|> join(
240+
:left_lateral,
241+
[plist, p1],
242+
p2 in fragment(
243+
"""
244+
SELECT r.priority, r.highlight_color, r.name as role_name
245+
FROM roles_users ru
246+
LEFT JOIN roles r ON ru.role_id = r.id
247+
WHERE ? = ru.user_id
248+
ORDER BY r.priority limit 1
249+
""",
250+
p1.user_id
251+
),
252+
on: true
253+
)
254+
|> join(
255+
:left_lateral,
256+
[],
257+
p3 in fragment("""
258+
SELECT priority FROM roles WHERE lookup =\'user\'
259+
"""),
260+
on: true
261+
)
262+
|> select([plist, p1, p2, p3], %{
263+
id: plist.id,
264+
position: plist.position,
265+
thread_id: p1.thread_id,
266+
board_id: p1.board_id,
267+
slug: p1.slug,
268+
user_id: p1.user_id,
269+
title: p1.title,
270+
body: p1.body,
271+
deleted: p1.deleted,
272+
locked: p1.locked,
273+
right_to_left: p1.right_to_left,
274+
metadata: p1.metadata,
275+
created_at: p1.created_at,
276+
updated_at: p1.updated_at,
277+
imported_at: p1.updated_at,
278+
username: p1.username,
279+
reported: p1.reported,
280+
reported_author: p1.reported_author,
281+
original_poster: p1.original_poster,
282+
user_deleted: p1.user_deleted,
283+
signature: p1.signature,
284+
avatar: p1.avatar,
285+
post_count: p1.post_count,
286+
name: p1.name,
287+
priority: p2.priority,
288+
highlight_color: p2.highlight_color,
289+
role_name: p2.role_name,
290+
default_priority: p3.priority
291+
})
292+
|> order_by([plist], plist.position)
293+
|> Repo.all()
294+
end
295+
296+
@doc """
297+
Used to find a specific `Post` by it's `id`
298+
"""
299+
@spec find_by_id(id :: non_neg_integer) :: t()
300+
def find_by_id(id) when is_integer(id) do
301+
query =
302+
from p in Post,
303+
where: p.id == ^id,
304+
preload: [:thread, user: :profile]
305+
306+
Repo.one(query)
307+
end
174308
end

lib/epochtalk_server/models/thread.ex

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,29 @@ defmodule EpochtalkServer.Models.Thread do
259259
}
260260
end
261261

262+
@doc """
263+
Used to obtain breadcrumb data for a specific `Thread` given it's `slug`
264+
"""
265+
@spec breadcrumb(slug :: String.t()) ::
266+
{:ok, thread :: t()} | {:error, :thread_does_not_exist}
267+
def breadcrumb(slug) when is_binary(slug) do
268+
post_query =
269+
from p in Post,
270+
order_by: p.created_at,
271+
limit: 1
272+
273+
thread_query =
274+
from t in Thread,
275+
where: t.slug == ^slug,
276+
preload: [:board, posts: ^post_query]
277+
278+
thread = Repo.one(thread_query)
279+
280+
if thread,
281+
do: {:ok, thread},
282+
else: {:error, :thread_does_not_exist}
283+
end
284+
262285
## === Private Helper Functions ===
263286

264287
defp sticky_by_board_id(board_id, page, opts) when page == 1,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule EpochtalkServerWeb.Controllers.Breadcrumb do
2+
use EpochtalkServerWeb, :controller
3+
4+
@moduledoc """
5+
Controller For `Breadcrumbs` related API requests
6+
"""
7+
alias EpochtalkServerWeb.Helpers.Breadcrumbs
8+
alias EpochtalkServerWeb.ErrorHelpers
9+
alias EpochtalkServerWeb.Helpers.Validate
10+
11+
@doc """
12+
Used to return breadcrumbs to the frontend`
13+
"""
14+
def breadcrumbs(conn, attrs) do
15+
with id <- parse_id(attrs, attrs["id"]),
16+
type <- Validate.cast(attrs, "type", :string, required: true),
17+
{:ok, breadcrumbs} <- Breadcrumbs.build_crumbs(id, type, []) do
18+
render(conn, :breadcrumbs, breadcrumbs: breadcrumbs)
19+
else
20+
{:error, data} ->
21+
ErrorHelpers.render_json_error(conn, 400, data)
22+
23+
_ ->
24+
ErrorHelpers.render_json_error(conn, 500, "There was an issue obtaining the breadcrumbs")
25+
end
26+
end
27+
28+
defp parse_id(attrs, id) when is_integer(id),
29+
do: Validate.cast(attrs, "id", :integer, min: 1, required: true)
30+
31+
defp parse_id(attrs, id) when is_binary(id),
32+
do: Validate.cast(attrs, "id", :string, required: true)
33+
end

0 commit comments

Comments
 (0)