Skip to content

Commit 2fd4c5c

Browse files
begedinjoshsmith
authored andcommitted
Add ProjectUser model, table, model tests
1 parent 5998fec commit 2fd4c5c

5 files changed

Lines changed: 321 additions & 13 deletions

File tree

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

test/models/project_user_test.exs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
defmodule CodeCorps.ProjectUserTest do
2+
use CodeCorps.ModelCase
3+
4+
alias CodeCorps.{ProjectUser, Repo}
5+
6+
describe "update_changeset role validation" do
7+
test "includes pending" do
8+
attrs = %{role: "pending"}
9+
changeset = ProjectUser.update_changeset(%ProjectUser{}, attrs)
10+
assert changeset.valid?
11+
end
12+
13+
test "includes contributor" do
14+
attrs = %{role: "contributor"}
15+
changeset = ProjectUser.update_changeset(%ProjectUser{}, attrs)
16+
assert changeset.valid?
17+
end
18+
19+
test "includes admin" do
20+
attrs = %{role: "admin"}
21+
changeset = ProjectUser.update_changeset(%ProjectUser{}, attrs)
22+
assert changeset.valid?
23+
end
24+
25+
test "includes owner" do
26+
attrs = %{role: "owner"}
27+
changeset = ProjectUser.update_changeset(%ProjectUser{}, attrs)
28+
assert changeset.valid?
29+
end
30+
31+
test "does not include invalid values" do
32+
attrs = %{role: "invalid"}
33+
changeset = ProjectUser.update_changeset(%ProjectUser{}, attrs)
34+
refute changeset.valid?
35+
end
36+
end
37+
38+
describe "create_owner_changeset/2" do
39+
@attributes ~w(project_id user_id role)
40+
41+
test "casts #{@attributes}, with role cast to 'owner'" do
42+
attrs = %{foo: "bar", project_id: 1, user_id: 2}
43+
changeset = ProjectUser.create_owner_changeset(%ProjectUser{}, attrs)
44+
assert changeset.changes == %{project_id: 1, user_id: 2, role: "owner"}
45+
end
46+
47+
test "ensures user record exists" do
48+
project = insert(:project)
49+
attrs = %{project_id: project.id, user_id: -1}
50+
changeset = ProjectUser.create_owner_changeset(%ProjectUser{}, attrs)
51+
52+
{:error, invalid_changeset} = changeset |> Repo.insert
53+
refute invalid_changeset.valid?
54+
55+
assert assoc_constraint_triggered?(invalid_changeset, :user)
56+
end
57+
58+
test "ensures project record exists" do
59+
user = insert(:user)
60+
attrs = %{project_id: -1, user_id: user.id}
61+
changeset = ProjectUser.create_owner_changeset(%ProjectUser{}, attrs)
62+
63+
{:error, invalid_changeset} = changeset |> Repo.insert
64+
refute invalid_changeset.valid?
65+
66+
assert assoc_constraint_triggered?(invalid_changeset, :project)
67+
end
68+
end
69+
70+
describe "create_pending_changeset/2" do
71+
@attributes ~w(project_id user_id role)
72+
73+
test "casts #{@attributes}, with role cast to 'pending'" do
74+
attrs = %{foo: "bar", project_id: 1, user_id: 2}
75+
changeset = ProjectUser.create_pending_changeset(%ProjectUser{}, attrs)
76+
assert changeset.changes == %{project_id: 1, user_id: 2, role: "pending"}
77+
end
78+
79+
test "ensures user record exists" do
80+
project = insert(:project)
81+
attrs = %{project_id: project.id, user_id: -1}
82+
changeset = ProjectUser.create_pending_changeset(%ProjectUser{}, attrs)
83+
84+
{:error, invalid_changeset} = changeset |> Repo.insert
85+
refute invalid_changeset.valid?
86+
87+
assert assoc_constraint_triggered?(invalid_changeset, :user)
88+
end
89+
90+
test "ensures project record exists" do
91+
user = insert(:user)
92+
attrs = %{project_id: -1, user_id: user.id}
93+
changeset = ProjectUser.create_pending_changeset(%ProjectUser{}, attrs)
94+
95+
{:error, invalid_changeset} = changeset |> Repo.insert
96+
refute invalid_changeset.valid?
97+
98+
assert assoc_constraint_triggered?(invalid_changeset, :project)
99+
end
100+
end
101+
end

test/support/model_case.ex

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,6 @@ defmodule CodeCorps.ModelCase do
2323
import Ecto.Query
2424
import CodeCorps.Factories
2525
import CodeCorps.ModelCase
26-
27-
defp assert_error_message(changeset, field, expected_message) do
28-
{actual_message, _} = changeset.errors[field]
29-
assert actual_message == expected_message
30-
end
31-
32-
defp assert_validation_triggered(changeset, field, type) do
33-
{_message, status} = changeset.errors[field]
34-
assert status[:validation] == type
35-
end
3626
end
3727
end
3828

@@ -73,4 +63,72 @@ defmodule CodeCorps.ModelCase do
7363
|> Ecto.Changeset.traverse_errors(&CodeCorps.ErrorHelpers.translate_error/1)
7464
|> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end)
7565
end
66+
67+
@doc """
68+
Asserts if a specific error message has been added to a specific field on the
69+
changeset. It is more flexible to use `error_message/2` directly instead of
70+
this one.
71+
72+
```
73+
assert_error_message(changeset, :foo, "bar")
74+
```
75+
76+
Compared to
77+
78+
```
79+
assert error_message(changeset, :foo) == "bar"
80+
refute error_message?(changeset, :foo) == "baz"
81+
```
82+
"""
83+
def assert_error_message(changeset, field, expected_message) do
84+
assert error_message(changeset, field) == expected_message
85+
end
86+
87+
@doc """
88+
Asserts if a specific validation type has been triggered on a specific field
89+
on the changeset. It is more flexible to use `validation_triggered/2` directly
90+
instead of this one.
91+
92+
```
93+
assert_validation_triggered(changeset, :foo, "bar")
94+
```
95+
96+
Compared to
97+
98+
```
99+
assert validation_triggered(changeset, :foo) == :required
100+
refute validation_triggered?(changeset, :bar) == :required
101+
```
102+
"""
103+
def assert_validation_triggered(changeset, field, type) do
104+
assert validation_triggered(changeset, field) == type
105+
end
106+
107+
@doc """
108+
Returns an error message on a specific field on the specified changeset
109+
"""
110+
@spec error_message(Ecto.Changeset.t, Atom.t) :: String.t
111+
def error_message(changeset, field) do
112+
{message, _} = changeset.errors[field]
113+
message
114+
end
115+
116+
@doc """
117+
Returns an atom indicating the type of validation that was triggered on a
118+
field in a changeset.
119+
"""
120+
@spec validation_triggered(Ecto.Changeset.t, Atom.t) :: Atom.t
121+
def validation_triggered(changeset, field) do
122+
{_message, status} = changeset.errors[field]
123+
status[:validation]
124+
end
125+
126+
@doc """
127+
Returns true or false depending on if an assoc_constraint validation has been
128+
triggered in the provided changeset on the specified field.
129+
"""
130+
@spec assoc_constraint_triggered?(Ecto.Changeset.t, Atom.t) :: boolean
131+
def assoc_constraint_triggered?(changeset, field) do
132+
error_message(changeset, field) == "does not exist"
133+
end
76134
end

web/models/project_user.ex

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
defmodule CodeCorps.ProjectUser do
2+
@moduledoc """
3+
Represents a membership of a user in a project.
4+
"""
5+
6+
use CodeCorps.Web, :model
7+
8+
@type t :: %__MODULE__{}
9+
10+
schema "project_users" do
11+
field :role, :string
12+
13+
belongs_to :project, CodeCorps.Project
14+
belongs_to :user, CodeCorps.User
15+
16+
timestamps()
17+
end
18+
19+
20+
@doc """
21+
Builds a changeset to create a pending membership
22+
"""
23+
def create_pending_changeset(struct, params \\ %{}) do
24+
struct
25+
|> create_changeset(params)
26+
|> put_change(:role, "pending")
27+
end
28+
29+
@doc """
30+
Builds a changeset to create an owner membership
31+
"""
32+
def create_owner_changeset(struct, params \\ %{}) do
33+
struct
34+
|> create_changeset(params)
35+
|> put_change(:role, "owner")
36+
end
37+
38+
@doc """
39+
Builds a changeset for inserting a new record into the database
40+
"""
41+
defp create_changeset(struct, params \\ %{}) do
42+
struct
43+
|> cast(params, [:user_id, :project_id])
44+
|> validate_required([:user_id, :project_id])
45+
|> assoc_constraint(:user)
46+
|> assoc_constraint(:project)
47+
|> unique_constraint(:project, name: :project_users_user_id_project_id_index)
48+
end
49+
50+
@doc """
51+
Builds a changeset for updating a record. Only the role can be updated.
52+
"""
53+
def update_changeset(struct, params \\ %{}) do
54+
struct
55+
|> cast(params, [:role])
56+
|> validate_required([:role])
57+
|> validate_inclusion(:role, roles())
58+
end
59+
60+
defp roles do
61+
~w{ pending contributor admin owner }
62+
end
63+
end

0 commit comments

Comments
 (0)