Skip to content

Commit 2116863

Browse files
Merge pull request #34 from epochtalk/threads-by-board
Threads by board
2 parents f45b5d9 + 0d3555e commit 2116863

File tree

19 files changed

+1191
-3
lines changed

19 files changed

+1191
-3
lines changed

.iex.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ alias EpochtalkServer.Models.{
77
Ban,
88
BannedAddress,
99
Board,
10+
BoardBan,
1011
BoardMapping,
1112
BoardModerator,
1213
Category,
1314
Configuration,
1415
Invitation,
1516
Mention,
1617
MetadataBoard,
18+
MetadataThread,
1719
Notification,
1820
Permission,
1921
Post,

lib/epochtalk_server/models/board.ex

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule EpochtalkServer.Models.Board do
22
use Ecto.Schema
33
import Ecto.Changeset
4+
import Ecto.Query
45
alias EpochtalkServer.Repo
56
alias EpochtalkServer.Models.Category
67
alias EpochtalkServer.Models.BoardMapping
@@ -121,4 +122,112 @@ defmodule EpochtalkServer.Models.Board do
121122
{:error, cs}
122123
end
123124
end
125+
126+
@doc """
127+
Determines if the provided `user_priority` has write access to the board with the specified `id`
128+
129+
TODO(akinsey): Should this check against banned user_priority?
130+
"""
131+
@spec get_write_access_by_id(id :: non_neg_integer, user_priority :: non_neg_integer) ::
132+
{:ok, can_write :: boolean} | {:error, :board_does_not_exist}
133+
def get_write_access_by_id(id, user_priority) do
134+
query =
135+
from b in Board,
136+
where: b.id == ^id,
137+
select: %{postable_by: b.postable_by}
138+
139+
board = Repo.one(query)
140+
141+
if board do
142+
# allow write if postable_by is nil
143+
can_write = is_nil(board.postable_by)
144+
# if postable_by is an integer, check against user priority
145+
can_write =
146+
if is_integer(board.postable_by),
147+
do: user_priority <= board.postable_by,
148+
else: can_write
149+
150+
{:ok, can_write}
151+
else
152+
{:error, :board_does_not_exist}
153+
end
154+
end
155+
156+
@doc """
157+
Determines if the provided `user_priority` has read access to the `Board` with the specified `id`.
158+
If the user doesn't have read access to the parent of the specified `Board`, the user does not have
159+
read access to the `Board` either.
160+
161+
TODO(akinsey): Should this check against banned user_priority?
162+
"""
163+
@spec get_read_access_by_id(id :: non_neg_integer, user_priority :: non_neg_integer) ::
164+
{:ok, can_read :: boolean} | {:error, :board_does_not_exist}
165+
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+
188+
board_and_parents =
189+
Board
190+
|> recursive_ctes(true)
191+
|> with_cte("find_parent", as: ^find_parent_query)
192+
|> join(:inner, [b], fp in "find_parent", on: b.id == fp.board_id)
193+
|> join(:left, [b, fp], c in Category, on: c.id == fp.category_id)
194+
|> select([b, fp, c], %{
195+
board_id: fp.board_id,
196+
parent_id: fp.parent_id,
197+
category_id: fp.category_id,
198+
cat_viewable_by: c.viewable_by,
199+
board_viewable_by: b.viewable_by
200+
})
201+
|> Repo.all()
202+
203+
# not readable if nothing in list, readable if viewable_by is nil, otherwise check viewable_by against user priority
204+
can_read =
205+
Enum.reduce(board_and_parents, length(board_and_parents) > 0, fn i, acc ->
206+
boards_viewable =
207+
not (is_integer(i.board_viewable_by) && user_priority > i.board_viewable_by)
208+
209+
cats_viewable = not (is_integer(i.cat_viewable_by) && user_priority > i.cat_viewable_by)
210+
acc && boards_viewable && cats_viewable
211+
end)
212+
213+
{:ok, can_read}
214+
end
215+
216+
@doc """
217+
Converts a board's `slug` to `id`
218+
"""
219+
@spec slug_to_id(slug :: String.t()) ::
220+
{:ok, id :: non_neg_integer} | {:error, :board_does_not_exist}
221+
def slug_to_id(slug) when is_binary(slug) do
222+
query =
223+
from b in Board,
224+
where: b.slug == ^slug,
225+
select: b.id
226+
227+
id = Repo.one(query)
228+
229+
if id,
230+
do: {:ok, id},
231+
else: {:error, :board_does_not_exist}
232+
end
124233
end
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
defmodule EpochtalkServer.Models.BoardBan do
2+
use Ecto.Schema
3+
import Ecto.Query
4+
import Ecto.Changeset
5+
alias EpochtalkServer.Repo
6+
alias EpochtalkServer.Models.User
7+
alias EpochtalkServer.Models.Thread
8+
alias EpochtalkServer.Models.Post
9+
alias EpochtalkServer.Models.BoardBan
10+
alias EpochtalkServer.Models.Board
11+
12+
@moduledoc """
13+
`BoardBan` model, for performing actions relating a user's profile
14+
"""
15+
@type t :: %__MODULE__{
16+
user_id: non_neg_integer | nil,
17+
user_id: non_neg_integer | nil
18+
}
19+
@primary_key false
20+
@schema_prefix "users"
21+
schema "board_bans" do
22+
belongs_to :user, User
23+
belongs_to :board, Board
24+
end
25+
26+
## === Changesets Functions ===
27+
28+
@doc """
29+
Creates a generic changeset for `BoardBan` model
30+
"""
31+
@spec changeset(board_ban :: t(), attrs :: map() | nil) :: Ecto.Changeset.t()
32+
def changeset(board_ban, attrs \\ %{}) do
33+
board_ban
34+
|> cast(attrs, [
35+
:user_id,
36+
:board_id
37+
])
38+
|> validate_required([:user_id, :board_id])
39+
end
40+
41+
## === Database Functions ===
42+
43+
@doc """
44+
Used to check if `User` is banned from board.
45+
Accepts a `board_id`, `post_id` or `thread_id` as an option.
46+
47+
Returns `true` if the user is banned from the specified `Board` or `false`
48+
otherwise.
49+
"""
50+
@spec is_banned_from_board(user :: User.t(), opts :: list) :: {:ok, banned :: boolean}
51+
def is_banned_from_board(%{id: user_id} = _user, opts) when is_integer(user_id) do
52+
board_id = Keyword.get(opts, :board_id)
53+
post_id = Keyword.get(opts, :post_id)
54+
thread_id = Keyword.get(opts, :thread_id)
55+
56+
query =
57+
cond do
58+
is_integer(board_id) ->
59+
from bb in BoardBan,
60+
where: bb.user_id == ^user_id and bb.board_id == ^board_id,
61+
select: bb.user_id
62+
63+
is_integer(post_id) ->
64+
from bb in BoardBan,
65+
where:
66+
bb.user_id == ^user_id and
67+
bb.board_id ==
68+
subquery(
69+
from(p in Post,
70+
left_join: t in Thread,
71+
on: t.id == p.thread_id,
72+
where: p.id == ^post_id,
73+
select: t.board_id
74+
)
75+
),
76+
select: bb.user_id
77+
78+
is_integer(thread_id) ->
79+
from bb in BoardBan,
80+
where:
81+
bb.user_id == ^user_id and
82+
bb.board_id ==
83+
subquery(from(t in Thread, where: t.id == ^thread_id, select: t.board_id)),
84+
select: bb.user_id
85+
86+
true ->
87+
nil
88+
end
89+
90+
result = if query, do: is_integer(Repo.one(query)), else: false
91+
{:ok, result}
92+
end
93+
94+
def is_banned_from_board(nil, _opts), do: {:ok, false}
95+
96+
@doc """
97+
Used to check if `User` with specified `user_id` is not banned from board.
98+
Accepts a `board_id`, `post_id` or `thread_id` as an option.
99+
100+
Returns `true` if the user is **not** banned from the specified `Board` or `false`
101+
otherwise.
102+
"""
103+
@spec is_not_banned_from_board(user :: User.t(), opts :: list) :: {:ok, not_banned :: boolean}
104+
def is_not_banned_from_board(user, opts \\ [])
105+
106+
def is_not_banned_from_board(%{id: user_id} = user, opts) when is_integer(user_id) do
107+
{:ok, banned} = is_banned_from_board(user, opts)
108+
{:ok, !banned}
109+
end
110+
111+
def is_not_banned_from_board(nil, _opts), do: {:ok, true}
112+
end

lib/epochtalk_server/models/board_mapping.ex

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ defmodule EpochtalkServer.Models.BoardMapping do
44
import Ecto.Query, only: [from: 2]
55
alias EpochtalkServer.Repo
66
alias EpochtalkServer.Models.Board
7+
alias EpochtalkServer.Models.MetadataBoard
78
alias EpochtalkServer.Models.BoardMapping
9+
alias EpochtalkServer.Models.Thread
810
alias EpochtalkServer.Models.Category
911

1012
@moduledoc """
@@ -16,12 +18,23 @@ defmodule EpochtalkServer.Models.BoardMapping do
1618
category: Category.t() | term(),
1719
view_order: non_neg_integer | nil
1820
}
21+
@derive {Jason.Encoder,
22+
only: [
23+
:board_id,
24+
:parent_id,
25+
:category_id,
26+
:view_order,
27+
:stats,
28+
:thread
29+
]}
1930
@primary_key false
2031
schema "board_mapping" do
2132
belongs_to :board, Board, primary_key: true
2233
belongs_to :parent, Board, primary_key: true
2334
belongs_to :category, Category, primary_key: true
2435
field :view_order, :integer
36+
field :stats, :map, virtual: true
37+
field :thread, :map, virtual: true
2538
end
2639

2740
## === Changesets Functions ===
@@ -65,6 +78,37 @@ defmodule EpochtalkServer.Models.BoardMapping do
6578
end)
6679
end
6780

81+
@doc """
82+
Returns BoardMapping with loaded boards and relevant metadata
83+
84+
TODO(akinsey): writing this assuming other models will update metadata board table properly.
85+
Old implementation was querying metadata boards then filling in holes in the data after the fact.
86+
"""
87+
@spec all(opts :: list() | nil) :: [t()]
88+
def all(opts \\ []) do
89+
stripped = Keyword.get(opts, :stripped, false)
90+
91+
query =
92+
if stripped do
93+
from bm in BoardMapping,
94+
left_join: b in Board,
95+
on: bm.board_id == b.id,
96+
select_merge: %{
97+
board: %{id: b.id, slug: b.slug, name: b.name, viewable_by: b.viewable_by}
98+
}
99+
else
100+
from bm in BoardMapping,
101+
left_join: mb in MetadataBoard,
102+
on: bm.board_id == mb.board_id,
103+
left_join: t in Thread,
104+
on: mb.last_thread_id == t.id,
105+
select_merge: %{stats: mb, thread: t},
106+
preload: [:board]
107+
end
108+
109+
Repo.all(query)
110+
end
111+
68112
## === Private Helper Functions ===
69113

70114
defp update(cat, "category"), do: Category.update_for_board_mapping(cat)

lib/epochtalk_server/models/board_moderator.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
defmodule EpochtalkServer.Models.BoardModerator do
22
use Ecto.Schema
33
import Ecto.Changeset
4+
import Ecto.Query
5+
alias EpochtalkServer.Repo
46
alias EpochtalkServer.Models.User
57
alias EpochtalkServer.Models.Board
8+
alias EpochtalkServer.Models.BoardModerator
69

710
@moduledoc """
811
`BoardModerator` model, for performing actions relating to `Board` moderators
@@ -11,6 +14,11 @@ defmodule EpochtalkServer.Models.BoardModerator do
1114
user_id: non_neg_integer,
1215
board_id: non_neg_integer
1316
}
17+
@derive {Jason.Encoder,
18+
only: [
19+
:user_id,
20+
:board_id
21+
]}
1422
@primary_key false
1523
schema "board_moderators" do
1624
belongs_to :user, User
@@ -31,4 +39,6 @@ defmodule EpochtalkServer.Models.BoardModerator do
3139
|> cast(attrs, [:user_id, :board_id])
3240
|> validate_required([:user_id, :board_id])
3341
end
42+
43+
def all(), do: Repo.all(from BoardModerator, preload: [user: ^from(User, select: [:username])])
3444
end

lib/epochtalk_server/models/category.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule EpochtalkServer.Models.Category do
22
use Ecto.Schema
33
import Ecto.Changeset
4+
import Ecto.Query
45
alias EpochtalkServer.Repo
56
alias EpochtalkServer.Models.Board
67
alias EpochtalkServer.Models.Category
@@ -103,4 +104,10 @@ defmodule EpochtalkServer.Models.Category do
103104
|> update_for_board_mapping_changeset(category_map)
104105
|> Repo.update()
105106
end
107+
108+
@doc """
109+
Returns a list of all categories
110+
"""
111+
@spec all() :: [t()]
112+
def all(), do: Repo.all(from Category, order_by: [asc: :view_order])
106113
end

lib/epochtalk_server/models/metadata_board.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ defmodule EpochtalkServer.Models.MetadataBoard do
2222
last_thread_title: String.t() | nil,
2323
last_post_position: non_neg_integer | nil
2424
}
25+
@derive {Jason.Encoder,
26+
only: [
27+
:id,
28+
:board_id,
29+
:post_count,
30+
:thread_count,
31+
:total_post,
32+
:total_thread_count,
33+
:last_post_username,
34+
:last_post_created_at,
35+
:last_thread_id,
36+
:last_thread_title,
37+
:last_post_position
38+
]}
2539
@schema_prefix "metadata"
2640
schema "boards" do
2741
belongs_to :board, Board

0 commit comments

Comments
 (0)