Skip to content

Commit 44e53c1

Browse files
authored
Merge pull request #1299 from code-corps/1291-conversation-part-controller
Add conversation part controller
2 parents 79b92aa + 8a108ce commit 44e53c1

15 files changed

+673
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule CodeCorps.Messages.ConversationParts do
2+
@moduledoc ~S"""
3+
An individual part of a conversation in a `CodeCorps.Conversation` thread,
4+
i.e. a reply to the `CodeCorps.Conversation` by any participant.
5+
"""
6+
7+
import Ecto.Changeset, only: [assoc_constraint: 2, cast: 3, validate_required: 2]
8+
9+
alias CodeCorps.{
10+
ConversationPart,
11+
Repo
12+
}
13+
14+
@spec create(map) :: ConversationPart.t | Ecto.Changeset.t
15+
def create(attrs) do
16+
%ConversationPart{} |> create_changeset(attrs) |> Repo.insert()
17+
end
18+
19+
@doc false
20+
@spec create_changeset(ConversationPart.t, map) :: Ecto.Changeset.t
21+
def create_changeset(%ConversationPart{} = conversation_part, attrs) do
22+
conversation_part
23+
|> cast(attrs, [:author_id, :body, :conversation_id, :read_at])
24+
|> validate_required([:author_id, :body, :conversation_id])
25+
|> assoc_constraint(:author)
26+
|> assoc_constraint(:conversation)
27+
end
28+
end

lib/code_corps/messages/messages.ex

+27-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ defmodule CodeCorps.Messages do
33
Main context for work with the Messaging feature.
44
"""
55

6-
alias CodeCorps.{Conversation, Helpers.Query, Message, Messages, Repo}
6+
alias CodeCorps.{
7+
Conversation,
8+
ConversationPart,
9+
Helpers.Query,
10+
Message,
11+
Messages,
12+
Repo
13+
}
714
alias Ecto.{Changeset, Queryable}
815

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

39+
@doc ~S"""
40+
Lists pre-scoped `CodeCorps.ConversationPart` records filtered by parameters
41+
"""
42+
@spec list_parts(Queryable.t, map) :: list(Conversation.t)
43+
def list_parts(scope, %{} = _params) do
44+
scope |> Repo.all()
45+
end
46+
3247
@doc ~S"""
3348
Gets a `CodeCorps.Conversation` record
3449
"""
@@ -37,6 +52,14 @@ defmodule CodeCorps.Messages do
3752
Conversation |> Repo.get(id)
3853
end
3954

55+
@doc ~S"""
56+
Gets a `CodeCorps.ConversationPart` record
57+
"""
58+
@spec get_part(integer) :: Conversation.t
59+
def get_part(id) do
60+
ConversationPart |> Repo.get(id)
61+
end
62+
4063
@doc ~S"""
4164
Creates a `CodeCorps.Message` from a set of parameters.
4265
"""
@@ -46,4 +69,7 @@ defmodule CodeCorps.Messages do
4669
|> Message.changeset(params)
4770
|> Repo.insert()
4871
end
72+
73+
@spec add_part(map) :: {:ok, ConversationPart.t} | {:error, Changeset.t}
74+
def add_part(map), do: Messages.ConversationParts.create(map)
4975
end
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
defmodule CodeCorps.Policy.ConversationPart do
2+
@moduledoc ~S"""
3+
Handles `CodeCorps.User` authorization of actions on `CodeCorps.Conversation`
4+
records.
5+
"""
6+
7+
import CodeCorps.Policy.Helpers,
8+
only: [
9+
administered_by?: 2, get_conversation: 1, get_message: 1, get_project: 1
10+
]
11+
import Ecto.Query
12+
13+
alias CodeCorps.{Conversation, ConversationPart, Policy, Repo, User}
14+
15+
@spec scope(Ecto.Queryable.t, User.t) :: Ecto.Queryable.t
16+
def scope(queryable, %User{admin: true}), do: queryable
17+
def scope(queryable, %User{id: id} = current_user) do
18+
scoped_conversation_ids =
19+
Conversation
20+
|> Policy.Conversation.scope(current_user)
21+
|> select([c], c.id)
22+
|> Repo.all()
23+
24+
queryable
25+
|> where(author_id: ^id)
26+
|> or_where([cp], cp.conversation_id in ^scoped_conversation_ids)
27+
end
28+
29+
def create?(%User{} = user, %{"conversation_id" => _} = params) do
30+
authorize(user, params)
31+
end
32+
def create?(_, _), do: false
33+
34+
def show?(%User{} = user, %ConversationPart{conversation_id: _} = part) do
35+
authorize(user, part)
36+
end
37+
def show?(_, _), do: false
38+
39+
@spec authorize(User.t, ConversationPart.t | map) :: boolean
40+
defp authorize(%User{} = user, attrs) do
41+
%Conversation{} = conversation = attrs |> get_conversation()
42+
is_target? = conversation |> conversation_target?(user)
43+
44+
is_admin? =
45+
conversation
46+
|> get_message()
47+
|> get_project()
48+
|> administered_by?(user)
49+
50+
is_target? or is_admin?
51+
end
52+
53+
defp conversation_target?(%Conversation{user_id: target_id}, %User{id: user_id}) do
54+
target_id == user_id
55+
end
56+
end

lib/code_corps/policy/helpers.ex

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule CodeCorps.Policy.Helpers do
66

77
alias CodeCorps.{
88
Conversation,
9+
ConversationPart,
910
Message,
1011
Organization,
1112
ProjectUser,
@@ -100,6 +101,14 @@ defmodule CodeCorps.Policy.Helpers do
100101
defp owner?("owner"), do: true
101102
defp owner?(_), do: false
102103

104+
@doc """
105+
Retrieves conversation from associated record
106+
"""
107+
@spec get_conversation(Changeset.t() | ConversationPart.t() | map) :: Message.t()
108+
def get_conversation(%ConversationPart{conversation_id: conversation_id}), do: Repo.get(Conversation, conversation_id)
109+
def get_conversation(%{"conversation_id" => conversation_id}), do: Repo.get(Conversation, conversation_id)
110+
def get_conversation(%Changeset{changes: %{conversation_id: conversation_id}}), do: Repo.get(Conversation, conversation_id)
111+
103112
@doc """
104113
Retrieves message from associated record
105114
"""

lib/code_corps/policy/policy.ex

+40-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,41 @@ defmodule CodeCorps.Policy do
33
Handles authorization for various API actions performed on objects in the database.
44
"""
55

6-
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}
7-
8-
alias CodeCorps.Policy
6+
alias CodeCorps.{
7+
Category,
8+
Comment,
9+
Conversation,
10+
ConversationPart,
11+
DonationGoal,
12+
GithubAppInstallation,
13+
GithubEvent,
14+
GithubRepo,
15+
Message,
16+
Organization,
17+
OrganizationInvite,
18+
OrganizationGithubAppInstallation,
19+
Policy,
20+
Preview,
21+
Project,
22+
ProjectCategory,
23+
ProjectSkill,
24+
ProjectUser,
25+
Role,
26+
RoleSkill,
27+
Skill,
28+
StripeConnectAccount,
29+
StripeConnectPlan,
30+
StripeConnectSubscription,
31+
StripePlatformCard,
32+
StripePlatformCustomer,
33+
Task,
34+
TaskSkill,
35+
User,
36+
UserCategory,
37+
UserRole,
38+
UserSkill,
39+
UserTask
40+
}
941

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

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

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

79+
# ConversationPart
80+
defp can?(%User{} = current_user, :create, %ConversationPart{}, %{} = params), do: Policy.ConversationPart.create?(current_user, params)
81+
defp can?(%User{} = current_user, :show, %ConversationPart{} = conversation_part, %{}), do: Policy.ConversationPart.show?(current_user, conversation_part)
82+
4683
# DonationGoal
4784
defp can?(%User{} = current_user, :create, %DonationGoal{}, %{} = params), do: Policy.DonationGoal.create?(current_user, params)
4885
defp can?(%User{} = current_user, :update, %DonationGoal{} = donation_goal, %{}), do: Policy.DonationGoal.update?(current_user, donation_goal)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
defmodule CodeCorpsWeb.ConversationPartController do
2+
@moduledoc false
3+
use CodeCorpsWeb, :controller
4+
5+
alias CodeCorps.{
6+
ConversationPart,
7+
Messages,
8+
User
9+
}
10+
11+
action_fallback CodeCorpsWeb.FallbackController
12+
plug CodeCorpsWeb.Plug.DataToAttributes
13+
plug CodeCorpsWeb.Plug.IdsToIntegers
14+
15+
@spec index(Conn.t, map) :: Conn.t
16+
def index(%Conn{} = conn, %{} = params) do
17+
with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource,
18+
conversation_parts <- ConversationPart |> Policy.scope(current_user) |> Messages.list_parts(params) do
19+
conn |> render("index.json-api", data: conversation_parts)
20+
end
21+
end
22+
23+
@spec create(Plug.Conn.t, map) :: Conn.t
24+
def create(%Conn{} = conn, %{} = params) do
25+
with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource,
26+
{:ok, :authorized} <- current_user |> Policy.authorize(:create, %ConversationPart{}, params),
27+
{:ok, %ConversationPart{} = message} <- Messages.add_part(params),
28+
message <- preload(message)
29+
do
30+
conn |> put_status(:created) |> render("show.json-api", data: message)
31+
end
32+
end
33+
34+
@spec show(Conn.t, map) :: Conn.t
35+
def show(%Conn{} = conn, %{"id" => id}) do
36+
with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource,
37+
%ConversationPart{} = conversation_part <- Messages.get_part(id),
38+
{:ok, :authorized} <- current_user |> Policy.authorize(:show, conversation_part, %{}) do
39+
conn |> render("show.json-api", data: conversation_part)
40+
end
41+
end
42+
43+
@preloads [:author, :conversation]
44+
45+
def preload(data) do
46+
Repo.preload(data, @preloads)
47+
end
48+
end

lib/code_corps_web/router.ex

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ defmodule CodeCorpsWeb.Router do
7272
resources "/categories", CategoryController, only: [:create, :update]
7373
resources "/comments", CommentController, only: [:create, :update]
7474
resources "/conversations", ConversationController, only: [:index, :show]
75+
resources "/conversation-parts", ConversationPartController, only: [:index, :show, :create]
7576
resources "/donation-goals", DonationGoalController, only: [:create, :update, :delete]
7677
post "/oauth/github", UserController, :github_oauth
7778
resources "/github-app-installations", GithubAppInstallationController, only: [:create]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule CodeCorpsWeb.ConversationPartView do
2+
@moduledoc false
3+
use CodeCorpsWeb, :view
4+
use JaSerializer.PhoenixView
5+
6+
attributes [:body, :inserted_at, :read_at, :updated_at]
7+
8+
has_one :author, type: "user", field: :author_id
9+
has_one :conversation, type: "conversation", field: :conversation_id
10+
end

priv/repo/structure.sql

+1
Original file line numberDiff line numberDiff line change
@@ -4159,3 +4159,4 @@ ALTER TABLE ONLY users
41594159
--
41604160

41614161
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);
4162+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
defmodule CodeCorps.Messages.ConversationPartsTest do
2+
use CodeCorps.ModelCase
3+
4+
alias CodeCorps.{
5+
ConversationPart,
6+
Messages.ConversationParts,
7+
Repo
8+
}
9+
10+
@valid_attrs %{
11+
body: "Test body."
12+
}
13+
14+
describe "create_changeset/2" do
15+
test "with valid attributes" do
16+
attrs = @valid_attrs |> Map.merge(%{author_id: 1, conversation_id: 1})
17+
changeset = ConversationParts.create_changeset(%ConversationPart{}, attrs)
18+
assert changeset.valid?
19+
end
20+
21+
test "requires author_id" do
22+
conversation_id = insert(:conversation).id
23+
24+
changeset = ConversationParts.create_changeset(%ConversationPart{}, %{conversation_id: conversation_id})
25+
26+
refute changeset.valid?
27+
assert_error_message(changeset, :author_id, "can't be blank")
28+
end
29+
30+
test "requires conversation_id" do
31+
author_id = insert(:user).id
32+
33+
changeset = ConversationParts.create_changeset(%ConversationPart{}, %{author_id: author_id})
34+
35+
refute changeset.valid?
36+
assert_error_message(changeset, :conversation_id, "can't be blank")
37+
end
38+
39+
test "requires id of actual author" do
40+
author_id = -1
41+
conversation_id = insert(:conversation).id
42+
attrs = @valid_attrs |> Map.merge(%{author_id: author_id, conversation_id: conversation_id})
43+
44+
{result, changeset} =
45+
ConversationParts.create_changeset(%ConversationPart{}, attrs)
46+
|> Repo.insert()
47+
48+
assert result == :error
49+
refute changeset.valid?
50+
assert_error_message(changeset, :author, "does not exist")
51+
end
52+
53+
test "requires id of actual conversation" do
54+
author_id = insert(:user).id
55+
conversation_id = -1
56+
attrs = @valid_attrs |> Map.merge(%{author_id: author_id, conversation_id: conversation_id})
57+
58+
{result, changeset} =
59+
ConversationParts.create_changeset(%ConversationPart{}, attrs)
60+
|> Repo.insert()
61+
62+
assert result == :error
63+
refute changeset.valid?
64+
assert_error_message(changeset, :conversation, "does not exist")
65+
end
66+
end
67+
end

0 commit comments

Comments
 (0)