Skip to content

Commit

Permalink
Merge pull request #92 from epochtalk/post-drafts
Browse files Browse the repository at this point in the history
Post drafts
  • Loading branch information
unenglishable authored Mar 19, 2024
2 parents bff9fa7 + 181bc90 commit c7603f5
Show file tree
Hide file tree
Showing 16 changed files with 298 additions and 11 deletions.
1 change: 1 addition & 0 deletions .iex.exs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ alias EpochtalkServer.Models.{
Notification,
Permission,
Post,
PostDraft,
Poll,
PollAnswer,
PollResponse,
Expand Down
1 change: 0 additions & 1 deletion lib/epochtalk_server/models/poll_answer.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
defmodule EpochtalkServer.Models.PollAnswer do
use Ecto.Schema
import Ecto.Changeset
# import Ecto.Query
alias EpochtalkServer.Repo
alias EpochtalkServer.Models.PollAnswer
alias EpochtalkServer.Models.Poll
Expand Down
85 changes: 85 additions & 0 deletions lib/epochtalk_server/models/post_draft.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
defmodule EpochtalkServer.Models.PostDraft do
use Ecto.Schema
import Ecto.Query
import Ecto.Changeset
alias EpochtalkServer.Repo
alias EpochtalkServer.Models.User
alias EpochtalkServer.Models.PostDraft

@moduledoc """
`PostDraft` model, for performing actions relating to a `User`'s `Post` draft
"""
@type t :: %__MODULE__{
user_id: non_neg_integer | nil,
draft: String.t() | nil,
updated_at: NaiveDateTime.t() | nil
}
@derive {Jason.Encoder,
only: [
:user_id,
:draft,
:updated_at
]}
@schema_prefix "posts"
@primary_key false
schema "user_drafts" do
belongs_to :user, User
field :draft, :string
field :updated_at, :naive_datetime
end

## === Changeset Functions ===

@doc """
Create a changeset for upserting a `PostDraft`
"""
@spec upsert_changeset(draft :: PostDraft.t(), attrs :: map() | nil) :: Ecto.Changeset.t()
def upsert_changeset(draft, attrs \\ %{}) do
updated_at = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)

draft =
draft
|> Map.put(:updated_at, updated_at)

draft
|> cast(attrs, [:user_id, :draft, :updated_at])
|> validate_required([:user_id, :updated_at])
|> validate_length(:draft, min: 1, max: 64_000)
|> unique_constraint(:user_id, name: :user_drafts_user_id_index)
|> foreign_key_constraint(:user_id, name: :user_drafts_user_id_fkey)
end

## === Database Functions ===

@doc """
Returns `PostDraft` Data given a `User` id
"""
@spec by_user_id(user_id :: integer) :: t() | nil
def by_user_id(user_id) do
query =
from p in PostDraft,
where: p.user_id == ^user_id

Repo.one(query)
end

@doc """
Used to upsert a `PostDraft` for a specific `User`
"""
@spec upsert(user_id :: non_neg_integer, attrs :: map()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def upsert(user_id, attrs) when is_integer(user_id) do
post_draft_cs = upsert_changeset(%PostDraft{user_id: user_id}, attrs)

Repo.insert(
post_draft_cs,
on_conflict: [
set: [
draft: Map.get(post_draft_cs.changes, :draft),
updated_at: Map.get(post_draft_cs.data, :updated_at)
]
],
conflict_target: [:user_id]
)
end
end
2 changes: 1 addition & 1 deletion lib/epochtalk_server_web/controllers/mention.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ defmodule EpochtalkServerWeb.Controllers.Mention do
limit <- Validate.cast(attrs, "limit", :integer, min: 1),
extended <- Validate.cast(attrs, "extended", :boolean),
:ok <- ACL.allow!(conn, "mentions.page"),
{:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
{:auth, %{} = user} <- {:auth, Guardian.Plug.current_resource(conn)},
{:ok, mentions, data} <-
Mention.page_by_user_id(user.id, page, per_page: limit, extended: extended),
do:
Expand Down
6 changes: 3 additions & 3 deletions lib/epochtalk_server_web/controllers/notification.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule EpochtalkServerWeb.Controllers.Notification do
Used to retrieve `Notification` counts for a specific `User`
"""
def counts(conn, attrs) do
with {:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
with {:auth, %{} = user} <- {:auth, Guardian.Plug.current_resource(conn)},
:ok <- ACL.allow!(conn, "notifications.counts"),
max <- Validate.cast(attrs, "max", :integer, min: 1) do
render(conn, :counts, data: Notification.counts_by_user_id(user.id, max: max || 99))
Expand All @@ -39,7 +39,7 @@ defmodule EpochtalkServerWeb.Controllers.Notification do
Used to dismiss `Notification` counts for a specific `User`
"""
def dismiss(conn, %{"id" => id}) do
with {:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
with {:auth, %{} = user} <- {:auth, Guardian.Plug.current_resource(conn)},
:ok <- ACL.allow!(conn, "notifications.dismiss"),
{_count, nil} <- Notification.dismiss(id) do
EpochtalkServerWeb.Endpoint.broadcast("user:#{user.id}", "refreshMentions", %{})
Expand All @@ -62,7 +62,7 @@ defmodule EpochtalkServerWeb.Controllers.Notification do
end

def dismiss(conn, %{"type" => type}) do
with {:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
with {:auth, %{} = user} <- {:auth, Guardian.Plug.current_resource(conn)},
:ok <- ACL.allow!(conn, "notifications.dismiss"),
{_count, nil} <- Notification.dismiss_type_by_user_id(user.id, type) do
EpochtalkServerWeb.Endpoint.broadcast("user:#{user.id}", "refreshMentions", %{})
Expand Down
4 changes: 2 additions & 2 deletions lib/epochtalk_server_web/controllers/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule EpochtalkServerWeb.Controllers.Post do
# * Ensure mentions and notifications are created
def create(conn, attrs) do
# Authorizations Checks
with {:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
with {:auth, %{} = user} <- {:auth, Guardian.Plug.current_resource(conn)},
:ok <- ACL.allow!(conn, "posts.create"),
# normally we use model changesets for validating POST requests parameters,
# this is an exception to save us from doing excessive processing between here
Expand Down Expand Up @@ -144,7 +144,7 @@ defmodule EpochtalkServerWeb.Controllers.Post do
"""
def update(conn, attrs) do
# Authorizations Checks
with {:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
with {:auth, %{} = user} <- {:auth, Guardian.Plug.current_resource(conn)},
:ok <- ACL.allow!(conn, "posts.update"),
# normally we use model changesets for validating POST requests parameters,
# this is an exception to save us from doing excessive processing between here
Expand Down
41 changes: 41 additions & 0 deletions lib/epochtalk_server_web/controllers/post_draft.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule EpochtalkServerWeb.Controllers.PostDraft do
use EpochtalkServerWeb, :controller

@moduledoc """
Controller for `PostDraft` related API requests
"""
alias EpochtalkServer.Auth.Guardian
alias EpochtalkServerWeb.ErrorHelpers
alias EpochtalkServer.Models.PostDraft

@doc """
Used to upsert a specific `PostDraft`
Returns `PostDraft` on success
"""
def upsert(conn, attrs) do
with {:auth, %{} = user} <- {:auth, Guardian.Plug.current_resource(conn)},
{:ok, draft_data} <- PostDraft.upsert(user.id, attrs) do
render(conn, :upsert, %{draft_data: draft_data, user_id: user.id})
else
{:auth, nil} ->
ErrorHelpers.render_json_error(conn, 403, "Not logged in, cannot upsert post draft")

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

@doc """
Get `PostDraft` by `User` id
"""
def by_user_id(conn, _) do
with {:auth, %{} = user} <- {:auth, Guardian.Plug.current_resource(conn)},
draft_data <- PostDraft.by_user_id(user.id) do
render(conn, :by_user_id, %{draft_data: draft_data, user_id: user.id})
else
{:auth, nil} ->
ErrorHelpers.render_json_error(conn, 403, "Not logged in, cannot get post draft")
end
end
end
2 changes: 1 addition & 1 deletion lib/epochtalk_server_web/controllers/preference.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule EpochtalkServerWeb.Controllers.Preference do
Used to retrieve preferences of a specific `User`
"""
def preferences(conn, _attrs) do
with {:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
with {:auth, %{} = user} <- {:auth, Guardian.Plug.current_resource(conn)},
do: render(conn, :preferences, preferences: Preference.by_user_id(user.id)),
else:
({:auth, nil} ->
Expand Down
4 changes: 2 additions & 2 deletions lib/epochtalk_server_web/controllers/role.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule EpochtalkServerWeb.Controllers.Role do
Returns id of `Role` on success
"""
def update(conn, attrs) do
with {:auth, _user} <- {:auth, Guardian.Plug.current_resource(conn)},
with {:auth, %{} = _user} <- {:auth, Guardian.Plug.current_resource(conn)},
:ok <- ACL.allow!(conn, "roles.update"),
{:ok, _role_permission_data} <- RolePermission.modify_by_role(attrs),
{:ok, _role_data} <- Role.update(attrs) do
Expand All @@ -34,7 +34,7 @@ defmodule EpochtalkServerWeb.Controllers.Role do
Get all `Role`s
"""
def all(conn, _) do
with {:auth, _user} <- {:auth, Guardian.Plug.current_resource(conn)},
with {:auth, %{} = _user} <- {:auth, Guardian.Plug.current_resource(conn)},
roles <- Role.all() do
render(conn, :all, roles: roles)
else
Expand Down
2 changes: 1 addition & 1 deletion lib/epochtalk_server_web/controllers/thread.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ defmodule EpochtalkServerWeb.Controllers.Thread do
# authorization checks
with :ok <- ACL.allow!(conn, "threads.create"),
board_id <- Validate.cast(attrs, "board_id", :integer, required: true),
{:auth, user} <- {:auth, Guardian.Plug.current_resource(conn)},
{:auth, %{} = user} <- {:auth, Guardian.Plug.current_resource(conn)},
user_priority <- ACL.get_user_priority(conn),
{:can_read, {:ok, true}} <-
{:can_read, Board.get_read_access_by_id(board_id, user_priority)},
Expand Down
38 changes: 38 additions & 0 deletions lib/epochtalk_server_web/json/post_draft_json.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
defmodule EpochtalkServerWeb.Controllers.PostDraftJSON do
@moduledoc """
Renders and formats `PostDraft` data, in JSON format for frontend
"""

@doc """
Renders `PostDraft` upsert result data in JSON
## Example
iex> draft_data = %{
iex> user_id: 99,
iex> draft: "Hello world",
iex> updated_at: ~N[2024-03-12 01:28:15]
iex> }
iex> EpochtalkServerWeb.Controllers.PostDraftJSON.upsert(%{draft_data: draft_data})
draft_data
"""
def upsert(%{draft_data: draft_data}), do: draft_data

@doc """
Renders `PostDraft` by_user_id result data in JSON
## Example
iex> EpochtalkServerWeb.Controllers.PostDraftJSON.by_user_id(%{draft_data: nil, user_id: 98})
%{user_id: 98, draft: nil, updated_at: nil}
iex> draft_data = %{
iex> user_id: 98,
iex> draft: "Hello world",
iex> updated_at: ~N[2024-03-12 01:28:15]
iex> }
iex> EpochtalkServerWeb.Controllers.PostDraftJSON.by_user_id(%{draft_data: draft_data, user_id: 98})
draft_data
"""
def by_user_id(%{draft_data: nil, user_id: user_id}),
do: %{user_id: user_id, draft: nil, updated_at: nil}

def by_user_id(%{draft_data: draft_data, user_id: _user_id}), do: draft_data
end
2 changes: 2 additions & 0 deletions lib/epochtalk_server_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ defmodule EpochtalkServerWeb.Router do
get "/register/username/:username", User, :username
get "/register/email/:email", User, :email
get "/posts", Post, :by_thread
get "/posts/draft", PostDraft, :by_user_id
put "/posts/draft", PostDraft, :upsert
post "/posts", Post, :create
post "/posts/:id", Post, :update
post "/register", User, :register
Expand Down
91 changes: 91 additions & 0 deletions test/epochtalk_server_web/controllers/post_draft_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
defmodule Test.EpochtalkServerWeb.Controllers.PostDraft do
use Test.Support.ConnCase, async: true
import Test.Support.Factory

setup %{users: %{admin_user: admin_user}} do
draft_data =
build(:post_draft, %{
user_id: admin_user.id,
draft: "draft test"
})

{:ok, draft_data: draft_data}
end

describe "by_user_id/1" do
test "when unauthenticated, returns Unauthorized error", %{conn: conn} do
response =
conn
|> get(Routes.post_draft_path(conn, :by_user_id))
|> json_response(403)

assert response["error"] == "Forbidden"
assert response["message"] == "Not logged in, cannot get post draft"
end

@tag :authenticated
test "when authenticated with no existing post draft, returns empty post draft",
%{conn: conn, users: %{user: user}} do
response =
conn
|> get(Routes.post_draft_path(conn, :by_user_id))
|> json_response(200)

assert response["user_id"] == user.id
assert response["draft"] == nil
assert response["updated_at"] == nil
end

@tag authenticated: :admin
test "when authenticated with an existing post draft, returns existing post draft",
%{conn: conn, users: %{admin_user: admin_user}, draft_data: draft_data} do
response =
conn
|> get(Routes.post_draft_path(conn, :by_user_id))
|> json_response(200)

assert response["user_id"] == admin_user.id
assert response["draft"] == draft_data.draft
assert response["updated_at"] != nil
end
end

describe "upsert/1" do
test "when unauthenticated, returns Unauthorized error", %{conn: conn} do
response =
conn
|> put(Routes.post_draft_path(conn, :upsert), %{"draft" => "Hello World"})
|> json_response(403)

assert response["error"] == "Forbidden"
assert response["message"] == "Not logged in, cannot upsert post draft"
end

@tag :authenticated
test "when authenticated with no existing post draft, creates and returns new post draft",
%{conn: conn, users: %{user: user}} do
response =
conn
|> put(Routes.post_draft_path(conn, :upsert), %{"draft" => "Hello World"})
|> json_response(200)

assert response["user_id"] == user.id
assert response["draft"] == "Hello World"
assert response["updated_at"] != nil
end

@tag authenticated: :admin
test "when authenticated with an existing post draft, overrides existing and returns new post draft",
%{conn: conn, users: %{admin_user: admin_user}, draft_data: draft_data} do
response =
conn
|> put(Routes.post_draft_path(conn, :upsert), %{"draft" => "Hello World"})
|> json_response(200)

assert response["user_id"] == admin_user.id
assert response["draft"] != draft_data.draft
assert response["draft"] == "Hello World"
assert response["updated_at"] != nil
end
end
end
7 changes: 7 additions & 0 deletions test/epochtalk_server_web/json/post_draft_json_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Test.EpochtalkServerWeb.Controllers.PostDraftJSON do
use Test.Support.ConnCase, async: true
alias EpochtalkServerWeb.Controllers.PostDraftJSON

# Specify that we want to use doctests:
doctest PostDraftJSON
end
Loading

0 comments on commit c7603f5

Please sign in to comment.