Skip to content

Commit ee4ae75

Browse files
authored
Merge pull request #722 from code-corps/711-add-project-users
Add ProjectUsers infrastructure
2 parents 5998fec + 1c55a26 commit ee4ae75

25 files changed

+815
-18
lines changed

lib/code_corps/analytics/segment_event_name_builder.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ defmodule CodeCorps.Analytics.SegmentEventNameBuilder do
2020
defp get_event_name(:update, %CodeCorps.OrganizationMembership{}) do
2121
"Approved Organization Membership"
2222
end
23+
defp get_event_name(:create, %CodeCorps.ProjectUser{}) do
24+
"Requested Project Membership"
25+
end
26+
defp get_event_name(:update, %CodeCorps.ProjectUser{}) do
27+
"Approved Project Membership"
28+
end
2329
defp get_event_name(:payment_succeeded, %CodeCorps.StripeInvoice{}) do
2430
"Processed Subscription Payment"
2531
end

lib/code_corps/analytics/segment_tracking_support.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ defmodule CodeCorps.Analytics.SegmentTrackingSupport do
1313
def includes?(:update, %CodeCorps.DonationGoal{}), do: true
1414
def includes?(:create, %CodeCorps.OrganizationMembership{}), do: true
1515
def includes?(:update, %CodeCorps.OrganizationMembership{}), do: true
16+
def includes?(:create, %CodeCorps.ProjectUser{}), do: true
17+
def includes?(:update, %CodeCorps.ProjectUser{}), do: true
1618
def includes?(:create, %CodeCorps.StripeConnectAccount{}), do: true
1719
def includes?(:create, %CodeCorps.StripeConnectCharge{}), do: true
1820
def includes?(:create, %CodeCorps.StripeConnectPlan{}), do: true

lib/code_corps/analytics/segment_traits_builder.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ defmodule CodeCorps.Analytics.SegmentTraitsBuilder do
3838
}
3939
end
4040

41+
defp traits(record = %CodeCorps.ProjectUser{}) do
42+
record = record |> CodeCorps.Repo.preload(:project)
43+
%{
44+
project: record.project.title,
45+
project_id: record.project_id
46+
}
47+
end
48+
4149
defp traits(charge = %CodeCorps.StripeConnectCharge{}) do
4250
# NOTE: this only works for some currencies
4351
revenue = charge.amount / 100

lib/code_corps/helpers/policy.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ defmodule CodeCorps.Helpers.Policy do
88

99
alias CodeCorps.{
1010
Organization, OrganizationMembership,
11-
Project, Repo, StripeConnectAccount,
11+
Project, ProjectUser, Repo, StripeConnectAccount,
1212
TaskSkill, Task, User, UserTask
1313
}
1414
alias Ecto.Changeset
@@ -46,9 +46,10 @@ defmodule CodeCorps.Helpers.Policy do
4646
4747
Returns `:string`
4848
"""
49-
@spec get_role(nil | OrganizationMembership.t | Changeset.t) :: String.t | nil
49+
@spec get_role(nil | OrganizationMembership.t | ProjectUser.t | Changeset.t) :: String.t | nil
5050
def get_role(nil), do: nil
5151
def get_role(%OrganizationMembership{role: role}), do: role
52+
def get_role(%ProjectUser{role: role}), do: role
5253
def get_role(%Changeset{} = changeset), do: changeset |> Changeset.get_field(:role)
5354

5455
@doc """
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule CodeCorps.Repo.Migrations.CreateProjectUsers do
2+
use Ecto.Migration
3+
4+
def change do
5+
create table(:project_users) do
6+
add :role, :string, null: false
7+
add :project_id, references(:projects, on_delete: :nothing), null: false
8+
add :user_id, references(:users, on_delete: :nothing), null: false
9+
10+
timestamps()
11+
end
12+
13+
create index :project_users, [:user_id, :project_id], unique: true
14+
end
15+
end

priv/repo/structure.sql

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
-- PostgreSQL database dump
33
--
44

5-
-- Dumped from database version 9.5.4
6-
-- Dumped by pg_dump version 9.5.4
5+
-- Dumped from database version 9.5.1
6+
-- Dumped by pg_dump version 9.5.1
77

88
SET statement_timeout = 0;
99
SET lock_timeout = 0;
@@ -318,6 +318,39 @@ CREATE SEQUENCE project_skills_id_seq
318318
ALTER SEQUENCE project_skills_id_seq OWNED BY project_skills.id;
319319

320320

321+
--
322+
-- Name: project_users; Type: TABLE; Schema: public; Owner: -
323+
--
324+
325+
CREATE TABLE project_users (
326+
id integer NOT NULL,
327+
role character varying(255) NOT NULL,
328+
project_id integer NOT NULL,
329+
user_id integer NOT NULL,
330+
inserted_at timestamp without time zone NOT NULL,
331+
updated_at timestamp without time zone NOT NULL
332+
);
333+
334+
335+
--
336+
-- Name: project_users_id_seq; Type: SEQUENCE; Schema: public; Owner: -
337+
--
338+
339+
CREATE SEQUENCE project_users_id_seq
340+
START WITH 1
341+
INCREMENT BY 1
342+
NO MINVALUE
343+
NO MAXVALUE
344+
CACHE 1;
345+
346+
347+
--
348+
-- Name: project_users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
349+
--
350+
351+
ALTER SEQUENCE project_users_id_seq OWNED BY project_users.id;
352+
353+
321354
--
322355
-- Name: projects; Type: TABLE; Schema: public; Owner: -
323356
--
@@ -1364,6 +1397,13 @@ ALTER TABLE ONLY project_categories ALTER COLUMN id SET DEFAULT nextval('project
13641397
ALTER TABLE ONLY project_skills ALTER COLUMN id SET DEFAULT nextval('project_skills_id_seq'::regclass);
13651398

13661399

1400+
--
1401+
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
1402+
--
1403+
1404+
ALTER TABLE ONLY project_users ALTER COLUMN id SET DEFAULT nextval('project_users_id_seq'::regclass);
1405+
1406+
13671407
--
13681408
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
13691409
--
@@ -1603,6 +1643,14 @@ ALTER TABLE ONLY project_skills
16031643
ADD CONSTRAINT project_skills_pkey PRIMARY KEY (id);
16041644

16051645

1646+
--
1647+
-- Name: project_users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
1648+
--
1649+
1650+
ALTER TABLE ONLY project_users
1651+
ADD CONSTRAINT project_users_pkey PRIMARY KEY (id);
1652+
1653+
16061654
--
16071655
-- Name: projects_pkey; Type: CONSTRAINT; Schema: public; Owner: -
16081656
--
@@ -1875,6 +1923,13 @@ CREATE UNIQUE INDEX project_skills_project_id_skill_id_index ON project_skills U
18751923
CREATE INDEX project_skills_skill_id_index ON project_skills USING btree (skill_id);
18761924

18771925

1926+
--
1927+
-- Name: project_users_user_id_project_id_index; Type: INDEX; Schema: public; Owner: -
1928+
--
1929+
1930+
CREATE UNIQUE INDEX project_users_user_id_project_id_index ON project_users USING btree (user_id, project_id);
1931+
1932+
18781933
--
18791934
-- Name: projects_approved_index; Type: INDEX; Schema: public; Owner: -
18801935
--
@@ -2229,6 +2284,22 @@ ALTER TABLE ONLY project_skills
22292284
ADD CONSTRAINT project_skills_skill_id_fkey FOREIGN KEY (skill_id) REFERENCES skills(id);
22302285

22312286

2287+
--
2288+
-- Name: project_users_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
2289+
--
2290+
2291+
ALTER TABLE ONLY project_users
2292+
ADD CONSTRAINT project_users_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id);
2293+
2294+
2295+
--
2296+
-- Name: project_users_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
2297+
--
2298+
2299+
ALTER TABLE ONLY project_users
2300+
ADD CONSTRAINT project_users_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
2301+
2302+
22322303
--
22332304
-- Name: projects_organization_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
22342305
--
@@ -2537,5 +2608,5 @@ ALTER TABLE ONLY user_tasks
25372608
-- PostgreSQL database dump complete
25382609
--
25392610

2540-
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);
2611+
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);
25412612

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
defmodule CodeCorps.ProjectUserControllerTest do
2+
use CodeCorps.ApiCase, resource_name: :project_user
3+
4+
@attrs %{role: "contributor"}
5+
6+
describe "index" do
7+
test "lists all resources", %{conn: conn} do
8+
[record_1, record_2] = insert_pair(:project_user)
9+
10+
conn
11+
|> request_index
12+
|> json_response(200)
13+
|> assert_ids_from_response([record_1.id, record_2.id])
14+
end
15+
16+
test "filters resources by record id", %{conn: conn} do
17+
[record_1, record_2 | _] = insert_list(3, :project_user)
18+
19+
path = "project-users/?filter[id]=#{record_1.id},#{record_2.id}"
20+
21+
conn
22+
|> get(path)
23+
|> json_response(200)
24+
|> assert_ids_from_response([record_1.id, record_2.id])
25+
end
26+
end
27+
28+
describe "show" do
29+
test "shows chosen resource", %{conn: conn} do
30+
record = insert(:project_user)
31+
conn
32+
|> request_show(record)
33+
|> json_response(200)
34+
|> Map.get("data")
35+
|> assert_result_id(record.id)
36+
end
37+
38+
test "renders 404 when id is nonexistent", %{conn: conn} do
39+
assert conn |> request_show(:not_found) |> json_response(404)
40+
end
41+
end
42+
43+
describe "create" do
44+
@tag :authenticated
45+
test "creates and renders resource when data is valid", %{conn: conn, current_user: user} do
46+
project = insert(:project)
47+
attrs = @attrs |> Map.merge(%{project: project, user: user})
48+
49+
assert conn |> request_create(attrs) |> json_response(201)
50+
51+
user_id = user.id
52+
tracking_properties = %{
53+
project: project.title,
54+
project_id: project.id
55+
}
56+
57+
assert_received {:track, ^user_id, "Requested Project Membership", ^tracking_properties}
58+
end
59+
60+
@tag :authenticated
61+
test "does not create resource and renders 422 when data is invalid", %{conn: conn, current_user: user} do
62+
# only way to trigger a validation error is to provide a non-existant organization
63+
# anything else will fail on authorization level
64+
project = build(:project)
65+
attrs = @attrs |> Map.merge(%{project: project, user: user})
66+
assert conn |> request_create(attrs) |> json_response(422)
67+
end
68+
69+
test "does not create resource and renders 401 when not authenticated", %{conn: conn} do
70+
assert conn |> request_create |> json_response(401)
71+
end
72+
73+
@tag :authenticated
74+
test "does not create resource and renders 403 when not authorized", %{conn: conn} do
75+
assert conn |> request_create |> json_response(403)
76+
end
77+
end
78+
79+
describe "update" do
80+
@tag :authenticated
81+
test "updates and renders resource when data is valid", %{conn: conn, current_user: current_user} do
82+
project = insert(:project)
83+
record = insert(:project_user, project: project, role: "pending")
84+
insert(:project_user, project: project, user: current_user, role: "owner")
85+
86+
assert conn |> request_update(record, @attrs) |> json_response(200)
87+
88+
user_id = current_user.id
89+
tracking_properties = %{
90+
project: project.title,
91+
project_id: project.id
92+
}
93+
94+
assert_received {:track, ^user_id, "Approved Project Membership", ^tracking_properties}
95+
end
96+
97+
test "doesn't update and renders 401 when unauthenticated", %{conn: conn} do
98+
assert conn |> request_update |> json_response(401)
99+
end
100+
101+
@tag :authenticated
102+
test "doesn't update and renders 403 when not authorized", %{conn: conn} do
103+
assert conn |> request_update |> json_response(403)
104+
end
105+
106+
@tag :authenticated
107+
test "renders 404 when id is nonexistent on update", %{conn: conn} do
108+
assert conn |> request_update(:not_found) |> json_response(404)
109+
end
110+
end
111+
112+
describe "delete" do
113+
@tag :authenticated
114+
test "deletes resource", %{conn: conn, current_user: current_user} do
115+
project = insert(:project)
116+
record = insert(:project_user, project: project)
117+
insert(:project_user, project: project, user: current_user, role: "owner")
118+
119+
assert conn |> request_delete(record) |> response(204)
120+
end
121+
122+
test "renders 401 when unauthenticated", %{conn: conn} do
123+
assert conn |> request_delete |> json_response(401)
124+
end
125+
126+
@tag :authenticated
127+
test "renders 403 when not authorized", %{conn: conn} do
128+
assert conn |> request_delete |> json_response(403)
129+
end
130+
131+
@tag :authenticated
132+
test "renders 404 when id is nonexistent on delete", %{conn: conn} do
133+
assert conn |> request_delete(:not_found) |> json_response(404)
134+
end
135+
end
136+
end

test/lib/code_corps/analytics/segment_event_name_builder_test.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ defmodule CodeCorps.Analytics.SegmentEventNameBuilderTest do
1818
assert SegmentEventNameBuilder.build(:update, build(:organization_membership)) == "Approved Organization Membership"
1919
end
2020

21+
test "with project_user" do
22+
assert SegmentEventNameBuilder.build(:create, build(:project_user)) == "Requested Project Membership"
23+
assert SegmentEventNameBuilder.build(:update, build(:project_user)) == "Approved Project Membership"
24+
end
25+
2126
test "with task" do
2227
assert SegmentEventNameBuilder.build(:create, build(:task)) == "Created Task"
2328
assert SegmentEventNameBuilder.build(:update, build(:task)) == "Edited Task"

0 commit comments

Comments
 (0)