Skip to content

Commit

Permalink
Merge pull request #1299 from code-corps/1291-conversation-part-contr…
Browse files Browse the repository at this point in the history
…oller

Add conversation part controller
  • Loading branch information
joshsmith authored Dec 15, 2017
2 parents 79b92aa + 8a108ce commit 44e53c1
Show file tree
Hide file tree
Showing 15 changed files with 673 additions and 5 deletions.
28 changes: 28 additions & 0 deletions lib/code_corps/messages/conversation_parts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule CodeCorps.Messages.ConversationParts do
@moduledoc ~S"""
An individual part of a conversation in a `CodeCorps.Conversation` thread,
i.e. a reply to the `CodeCorps.Conversation` by any participant.
"""

import Ecto.Changeset, only: [assoc_constraint: 2, cast: 3, validate_required: 2]

alias CodeCorps.{
ConversationPart,
Repo
}

@spec create(map) :: ConversationPart.t | Ecto.Changeset.t
def create(attrs) do
%ConversationPart{} |> create_changeset(attrs) |> Repo.insert()
end

@doc false
@spec create_changeset(ConversationPart.t, map) :: Ecto.Changeset.t
def create_changeset(%ConversationPart{} = conversation_part, attrs) do
conversation_part
|> cast(attrs, [:author_id, :body, :conversation_id, :read_at])
|> validate_required([:author_id, :body, :conversation_id])
|> assoc_constraint(:author)
|> assoc_constraint(:conversation)
end
end
28 changes: 27 additions & 1 deletion lib/code_corps/messages/messages.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ defmodule CodeCorps.Messages do
Main context for work with the Messaging feature.
"""

alias CodeCorps.{Conversation, Helpers.Query, Message, Messages, Repo}
alias CodeCorps.{
Conversation,
ConversationPart,
Helpers.Query,
Message,
Messages,
Repo
}
alias Ecto.{Changeset, Queryable}

@doc ~S"""
Expand All @@ -29,6 +36,14 @@ defmodule CodeCorps.Messages do
|> Repo.all()
end

@doc ~S"""
Lists pre-scoped `CodeCorps.ConversationPart` records filtered by parameters
"""
@spec list_parts(Queryable.t, map) :: list(Conversation.t)
def list_parts(scope, %{} = _params) do
scope |> Repo.all()
end

@doc ~S"""
Gets a `CodeCorps.Conversation` record
"""
Expand All @@ -37,6 +52,14 @@ defmodule CodeCorps.Messages do
Conversation |> Repo.get(id)
end

@doc ~S"""
Gets a `CodeCorps.ConversationPart` record
"""
@spec get_part(integer) :: Conversation.t
def get_part(id) do
ConversationPart |> Repo.get(id)
end

@doc ~S"""
Creates a `CodeCorps.Message` from a set of parameters.
"""
Expand All @@ -46,4 +69,7 @@ defmodule CodeCorps.Messages do
|> Message.changeset(params)
|> Repo.insert()
end

@spec add_part(map) :: {:ok, ConversationPart.t} | {:error, Changeset.t}
def add_part(map), do: Messages.ConversationParts.create(map)
end
56 changes: 56 additions & 0 deletions lib/code_corps/policy/conversation_part.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule CodeCorps.Policy.ConversationPart do
@moduledoc ~S"""
Handles `CodeCorps.User` authorization of actions on `CodeCorps.Conversation`
records.
"""

import CodeCorps.Policy.Helpers,
only: [
administered_by?: 2, get_conversation: 1, get_message: 1, get_project: 1
]
import Ecto.Query

alias CodeCorps.{Conversation, ConversationPart, Policy, Repo, User}

@spec scope(Ecto.Queryable.t, User.t) :: Ecto.Queryable.t
def scope(queryable, %User{admin: true}), do: queryable
def scope(queryable, %User{id: id} = current_user) do
scoped_conversation_ids =
Conversation
|> Policy.Conversation.scope(current_user)
|> select([c], c.id)
|> Repo.all()

queryable
|> where(author_id: ^id)
|> or_where([cp], cp.conversation_id in ^scoped_conversation_ids)
end

def create?(%User{} = user, %{"conversation_id" => _} = params) do
authorize(user, params)
end
def create?(_, _), do: false

def show?(%User{} = user, %ConversationPart{conversation_id: _} = part) do
authorize(user, part)
end
def show?(_, _), do: false

@spec authorize(User.t, ConversationPart.t | map) :: boolean
defp authorize(%User{} = user, attrs) do
%Conversation{} = conversation = attrs |> get_conversation()
is_target? = conversation |> conversation_target?(user)

is_admin? =
conversation
|> get_message()
|> get_project()
|> administered_by?(user)

is_target? or is_admin?
end

defp conversation_target?(%Conversation{user_id: target_id}, %User{id: user_id}) do
target_id == user_id
end
end
9 changes: 9 additions & 0 deletions lib/code_corps/policy/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule CodeCorps.Policy.Helpers do

alias CodeCorps.{
Conversation,
ConversationPart,
Message,
Organization,
ProjectUser,
Expand Down Expand Up @@ -100,6 +101,14 @@ defmodule CodeCorps.Policy.Helpers do
defp owner?("owner"), do: true
defp owner?(_), do: false

@doc """
Retrieves conversation from associated record
"""
@spec get_conversation(Changeset.t() | ConversationPart.t() | map) :: Message.t()
def get_conversation(%ConversationPart{conversation_id: conversation_id}), do: Repo.get(Conversation, conversation_id)
def get_conversation(%{"conversation_id" => conversation_id}), do: Repo.get(Conversation, conversation_id)
def get_conversation(%Changeset{changes: %{conversation_id: conversation_id}}), do: Repo.get(Conversation, conversation_id)

@doc """
Retrieves message from associated record
"""
Expand Down
43 changes: 40 additions & 3 deletions lib/code_corps/policy/policy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,41 @@ defmodule CodeCorps.Policy do
Handles authorization for various API actions performed on objects in the database.
"""

alias CodeCorps.{Category, Comment, Conversation, DonationGoal, GithubAppInstallation, GithubEvent, GithubRepo, Message, Organization, OrganizationInvite, OrganizationGithubAppInstallation, Preview, Project, ProjectCategory, ProjectSkill, ProjectUser, Role, RoleSkill, Skill, StripeConnectAccount, StripeConnectPlan, StripeConnectSubscription, StripePlatformCard, StripePlatformCustomer, Task, TaskSkill, User, UserCategory, UserRole, UserSkill, UserTask}

alias CodeCorps.Policy
alias CodeCorps.{
Category,
Comment,
Conversation,
ConversationPart,
DonationGoal,
GithubAppInstallation,
GithubEvent,
GithubRepo,
Message,
Organization,
OrganizationInvite,
OrganizationGithubAppInstallation,
Policy,
Preview,
Project,
ProjectCategory,
ProjectSkill,
ProjectUser,
Role,
RoleSkill,
Skill,
StripeConnectAccount,
StripeConnectPlan,
StripeConnectSubscription,
StripePlatformCard,
StripePlatformCustomer,
Task,
TaskSkill,
User,
UserCategory,
UserRole,
UserSkill,
UserTask
}

@doc ~S"""
Determines if the specified user can perform the specified action on the
Expand All @@ -29,6 +61,7 @@ defmodule CodeCorps.Policy do
@spec scope(module, User.t) :: Ecto.Queryable.t
def scope(Message, %User{} = current_user), do: Message |> Policy.Message.scope(current_user)
def scope(Conversation, %User{} = current_user), do: Conversation |> Policy.Conversation.scope(current_user)
def scope(ConversationPart, %User{} = current_user), do: ConversationPart |> Policy.ConversationPart.scope(current_user)

@spec can?(User.t, atom, struct, map) :: boolean

Expand All @@ -43,6 +76,10 @@ defmodule CodeCorps.Policy do
# Conversation
defp can?(%User{} = current_user, :show, %Conversation{} = conversation, %{}), do: Policy.Conversation.show?(current_user, conversation)

# ConversationPart
defp can?(%User{} = current_user, :create, %ConversationPart{}, %{} = params), do: Policy.ConversationPart.create?(current_user, params)
defp can?(%User{} = current_user, :show, %ConversationPart{} = conversation_part, %{}), do: Policy.ConversationPart.show?(current_user, conversation_part)

# DonationGoal
defp can?(%User{} = current_user, :create, %DonationGoal{}, %{} = params), do: Policy.DonationGoal.create?(current_user, params)
defp can?(%User{} = current_user, :update, %DonationGoal{} = donation_goal, %{}), do: Policy.DonationGoal.update?(current_user, donation_goal)
Expand Down
48 changes: 48 additions & 0 deletions lib/code_corps_web/controllers/conversation_part_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
defmodule CodeCorpsWeb.ConversationPartController do
@moduledoc false
use CodeCorpsWeb, :controller

alias CodeCorps.{
ConversationPart,
Messages,
User
}

action_fallback CodeCorpsWeb.FallbackController
plug CodeCorpsWeb.Plug.DataToAttributes
plug CodeCorpsWeb.Plug.IdsToIntegers

@spec index(Conn.t, map) :: Conn.t
def index(%Conn{} = conn, %{} = params) do
with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource,
conversation_parts <- ConversationPart |> Policy.scope(current_user) |> Messages.list_parts(params) do
conn |> render("index.json-api", data: conversation_parts)
end
end

@spec create(Plug.Conn.t, map) :: Conn.t
def create(%Conn{} = conn, %{} = params) do
with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource,
{:ok, :authorized} <- current_user |> Policy.authorize(:create, %ConversationPart{}, params),
{:ok, %ConversationPart{} = message} <- Messages.add_part(params),
message <- preload(message)
do
conn |> put_status(:created) |> render("show.json-api", data: message)
end
end

@spec show(Conn.t, map) :: Conn.t
def show(%Conn{} = conn, %{"id" => id}) do
with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource,
%ConversationPart{} = conversation_part <- Messages.get_part(id),
{:ok, :authorized} <- current_user |> Policy.authorize(:show, conversation_part, %{}) do
conn |> render("show.json-api", data: conversation_part)
end
end

@preloads [:author, :conversation]

def preload(data) do
Repo.preload(data, @preloads)
end
end
1 change: 1 addition & 0 deletions lib/code_corps_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ defmodule CodeCorpsWeb.Router do
resources "/categories", CategoryController, only: [:create, :update]
resources "/comments", CommentController, only: [:create, :update]
resources "/conversations", ConversationController, only: [:index, :show]
resources "/conversation-parts", ConversationPartController, only: [:index, :show, :create]
resources "/donation-goals", DonationGoalController, only: [:create, :update, :delete]
post "/oauth/github", UserController, :github_oauth
resources "/github-app-installations", GithubAppInstallationController, only: [:create]
Expand Down
10 changes: 10 additions & 0 deletions lib/code_corps_web/views/conversation_part_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule CodeCorpsWeb.ConversationPartView do
@moduledoc false
use CodeCorpsWeb, :view
use JaSerializer.PhoenixView

attributes [:body, :inserted_at, :read_at, :updated_at]

has_one :author, type: "user", field: :author_id
has_one :conversation, type: "conversation", field: :conversation_id
end
1 change: 1 addition & 0 deletions priv/repo/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4159,3 +4159,4 @@ ALTER TABLE ONLY users
--

INSERT INTO "schema_migrations" (version) VALUES (20160723215749), (20160804000000), (20160804001111), (20160805132301), (20160805203929), (20160808143454), (20160809214736), (20160810124357), (20160815125009), (20160815143002), (20160816020347), (20160816034021), (20160817220118), (20160818000944), (20160818132546), (20160820113856), (20160820164905), (20160822002438), (20160822004056), (20160822011624), (20160822020401), (20160822044612), (20160830081224), (20160830224802), (20160911233738), (20160912002705), (20160912145957), (20160918003206), (20160928232404), (20161003185918), (20161019090945), (20161019110737), (20161020144622), (20161021131026), (20161031001615), (20161121005339), (20161121014050), (20161121043941), (20161121045709), (20161122015942), (20161123081114), (20161123150943), (20161124085742), (20161125200620), (20161126045705), (20161127054559), (20161205024856), (20161207112519), (20161209192504), (20161212005641), (20161214005935), (20161215052051), (20161216051447), (20161218005913), (20161219160401), (20161219163909), (20161220141753), (20161221085759), (20161226213600), (20161231063614), (20170102130055), (20170102181053), (20170104113708), (20170104212623), (20170104235423), (20170106013143), (20170115035159), (20170115230549), (20170121014100), (20170131234029), (20170201014901), (20170201025454), (20170201035458), (20170201183258), (20170220032224), (20170224233516), (20170226050552), (20170228085250), (20170308214128), (20170308220713), (20170308222552), (20170313130611), (20170318032449), (20170318082740), (20170324194827), (20170424215355), (20170501225441), (20170505224222), (20170526095401), (20170602000208), (20170622205732), (20170626231059), (20170628092119), (20170628213609), (20170629183404), (20170630140136), (20170706132431), (20170707213648), (20170711122252), (20170717092127), (20170725060612), (20170727052644), (20170731130121), (20170814131722), (20170913114958), (20170921014405), (20170925214512), (20170925230419), (20170926134646), (20170927100300), (20170928234412), (20171003134956), (20171003225853), (20171006063358), (20171006161407), (20171012215106), (20171012221231), (20171016125229), (20171016125516), (20171016223356), (20171016235656), (20171017235433), (20171019191035), (20171025184225), (20171026010933), (20171027061833), (20171028011642), (20171028173508), (20171030182857), (20171031232023), (20171031234356), (20171101023309), (20171104013543), (20171106045740), (20171106050209), (20171106103153), (20171106200036), (20171109231538), (20171110001134), (20171114010851), (20171114033357), (20171114225214), (20171114225713), (20171114232534), (20171115201624), (20171115225358), (20171119004204), (20171121075226), (20171121144138), (20171123065902), (20171127215847), (20171201073818), (20171205161052), (20171213062707);

67 changes: 67 additions & 0 deletions test/lib/code_corps/messages/conversation_parts_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule CodeCorps.Messages.ConversationPartsTest do
use CodeCorps.ModelCase

alias CodeCorps.{
ConversationPart,
Messages.ConversationParts,
Repo
}

@valid_attrs %{
body: "Test body."
}

describe "create_changeset/2" do
test "with valid attributes" do
attrs = @valid_attrs |> Map.merge(%{author_id: 1, conversation_id: 1})
changeset = ConversationParts.create_changeset(%ConversationPart{}, attrs)
assert changeset.valid?
end

test "requires author_id" do
conversation_id = insert(:conversation).id

changeset = ConversationParts.create_changeset(%ConversationPart{}, %{conversation_id: conversation_id})

refute changeset.valid?
assert_error_message(changeset, :author_id, "can't be blank")
end

test "requires conversation_id" do
author_id = insert(:user).id

changeset = ConversationParts.create_changeset(%ConversationPart{}, %{author_id: author_id})

refute changeset.valid?
assert_error_message(changeset, :conversation_id, "can't be blank")
end

test "requires id of actual author" do
author_id = -1
conversation_id = insert(:conversation).id
attrs = @valid_attrs |> Map.merge(%{author_id: author_id, conversation_id: conversation_id})

{result, changeset} =
ConversationParts.create_changeset(%ConversationPart{}, attrs)
|> Repo.insert()

assert result == :error
refute changeset.valid?
assert_error_message(changeset, :author, "does not exist")
end

test "requires id of actual conversation" do
author_id = insert(:user).id
conversation_id = -1
attrs = @valid_attrs |> Map.merge(%{author_id: author_id, conversation_id: conversation_id})

{result, changeset} =
ConversationParts.create_changeset(%ConversationPart{}, attrs)
|> Repo.insert()

assert result == :error
refute changeset.valid?
assert_error_message(changeset, :conversation, "does not exist")
end
end
end
Loading

0 comments on commit 44e53c1

Please sign in to comment.