Skip to content

Commit 978300d

Browse files
committed
Add messages
1 parent 18ac285 commit 978300d

File tree

17 files changed

+829
-4
lines changed

17 files changed

+829
-4
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule CodeCorps.Messages do
2+
@moduledoc ~S"""
3+
Main context for work with the Messaging feature.
4+
"""
5+
6+
alias CodeCorps.{Helpers.Query, Message, Messages, Repo}
7+
alias Ecto.{Changeset, Queryable}
8+
9+
@doc ~S"""
10+
Lists pre-scoped `CodeCorps.Message` records filtered by parameters.
11+
"""
12+
@spec list(Queryable.t, map) :: list(Message.t)
13+
def list(scope, %{} = params) do
14+
scope
15+
|> Query.id_filter(params)
16+
|> Messages.Query.author_filter(params)
17+
|> Messages.Query.project_filter(params)
18+
|> Repo.all()
19+
end
20+
21+
@doc ~S"""
22+
Creates a `CodeCorps.Message` from a set of parameters.
23+
"""
24+
@spec create(map) :: {:ok, Message.t} | {:error, Changeset.t}
25+
def create(%{} = params) do
26+
%Message{}
27+
|> Message.changeset(params)
28+
|> Repo.insert()
29+
end
30+
end

lib/code_corps/messages/query.ex

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
defmodule CodeCorps.Messages.Query do
2+
@moduledoc ~S"""
3+
Holds helpers to query `CodeCorps.Message` records using a map of params.
4+
"""
5+
6+
import Ecto.Query, only: [where: 3]
7+
8+
alias Ecto.Queryable
9+
10+
@doc ~S"""
11+
Narrows down a `CodeCorps.Message` query by `author_id`, if specified in a
12+
params map
13+
"""
14+
@spec author_filter(Queryable.t, map) :: Queryable.t
15+
def author_filter(queryable, %{"author_id" => author_id}) do
16+
queryable |> where([m], m.author_id == ^author_id)
17+
end
18+
def author_filter(queryable, %{}), do: queryable
19+
20+
@doc ~S"""
21+
Narrows down a `CodeCorps.Message` query by `project_id`, if specified in a
22+
params map
23+
"""
24+
@spec project_filter(Queryable.t, map) :: Queryable.t
25+
def project_filter(queryable, %{"project_id" => project_id}) do
26+
queryable |> where([m], m.project_id == ^project_id)
27+
end
28+
def project_filter(queryable, %{}), do: queryable
29+
end

lib/code_corps/model/message.ex

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
defmodule CodeCorps.Message do
2+
@moduledoc """
3+
A message sent from a project to a user or from a user to a project.
4+
5+
The author does not need to be a member of the project in order to send a
6+
message to the project.
7+
8+
No recipient will be defined for the message. The recipient is defined at the
9+
level of the `CodeCorps.Conversation`.
10+
11+
A message may be used as a broadcast to a number of users. A message MAY
12+
therefore have many conversations associated with it.
13+
"""
14+
15+
use CodeCorps.Model
16+
alias CodeCorps.Message
17+
18+
@type t :: %__MODULE__{}
19+
20+
schema "messages" do
21+
field :body, :string
22+
field :initiated_by, :string
23+
field :subject, :string
24+
25+
belongs_to :author, CodeCorps.User
26+
belongs_to :project, CodeCorps.Project
27+
28+
timestamps()
29+
end
30+
31+
@doc false
32+
@spec changeset(Message.t, map) :: Ecto.Changeset.t
33+
def changeset(%Message{} = message, attrs) do
34+
message
35+
|> cast(attrs, [:body, :initiated_by, :subject])
36+
|> validate_required([:body, :initiated_by])
37+
|> validate_inclusion(:initiated_by, initiated_by_sources())
38+
|> require_subject_if_admin()
39+
end
40+
41+
# validate subject only if initiated_by "admin"
42+
@spec require_subject_if_admin(Ecto.Changeset.t) :: Ecto.Changeset.t
43+
defp require_subject_if_admin(changeset) do
44+
initiated_by = changeset |> Ecto.Changeset.get_field(:initiated_by)
45+
changeset |> do_require_subject_if_admin(initiated_by)
46+
end
47+
48+
defp do_require_subject_if_admin(changeset, "admin") do
49+
changeset |> validate_required(:subject)
50+
end
51+
defp do_require_subject_if_admin(changeset, _), do: changeset
52+
53+
@spec initiated_by_sources :: list(String.t)
54+
defp initiated_by_sources do
55+
~w{ admin user }
56+
end
57+
end

lib/code_corps/policy/message.ex

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
defmodule CodeCorps.Policy.Message do
2+
@moduledoc """
3+
Handles `User` authorization of actions on `Message` records
4+
"""
5+
6+
import CodeCorps.Policy.Helpers, only: [administered_by?: 2, get_project: 1]
7+
import Ecto.Query
8+
9+
alias CodeCorps.{Message, Project, ProjectUser, User, Repo}
10+
11+
@spec scope(Ecto.Queryable.t, User.t) :: Ecto.Queryable.t
12+
def scope(queryable, %User{admin: true}), do: queryable
13+
def scope(queryable, %User{id: id}) do
14+
projects_administered_by_user_ids =
15+
Project
16+
|> join(:inner, [p], pu in ProjectUser, pu.project_id == p.id)
17+
|> where([_p, pu], pu.user_id == ^id)
18+
|> where([_p, pu], pu.role in ~w(admin owner))
19+
|> select([p], p.id)
20+
|> Repo.all
21+
22+
queryable
23+
|> where([m], m.author_id == ^id)
24+
|> or_where([m], m.project_id in ^projects_administered_by_user_ids)
25+
end
26+
27+
def show?(%User{id: user_id}, %{initiated_by: "user", author_id: author_id})
28+
when user_id == author_id do
29+
true
30+
end
31+
def show?(%User{} = user, %Message{} = message) do
32+
message |> get_project() |> administered_by?(user)
33+
end
34+
def show?(_, _), do: false
35+
36+
def create?(%User{id: id}, %{"initiated_by" => "user", "author_id" => author_id}) when id === author_id do
37+
true
38+
end
39+
def create?(%User{} = user, %{"initiated_by" => "admin", "project_id" => _} = params) do
40+
params |> get_project() |> administered_by?(user)
41+
end
42+
def create?(_, _), do: false
43+
end

lib/code_corps/policy/policy.ex

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule CodeCorps.Policy do
33
Handles authorization for various API actions performed on objects in the database.
44
"""
55

6-
alias CodeCorps.{Category, Comment, DonationGoal, GithubAppInstallation, GithubEvent, GithubRepo, Organization, OrganizationInvite, OrganizationGithubAppInstallation, Preview, Project, ProjectCategory, ProjectSkill, ProjectUser, Role, RoleSkill, Skill, StripeConnectAccount, StripeConnectPlan, StripeConnectSubscription, StripePlatformCard, StripePlatformCustomer, Task, TaskSkill, User, UserCategory, UserRole, UserSkill, UserTask}
6+
alias CodeCorps.{Category, Comment, 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}
77

88
alias CodeCorps.Policy
99

@@ -22,6 +22,13 @@ defmodule CodeCorps.Policy do
2222
end
2323
end
2424

25+
@doc ~S"""
26+
Scopes a queryable so it's only able to return those records the specified
27+
user is authorized to view.
28+
"""
29+
@spec scope(module, User.t) :: Ecto.Queryable.t
30+
def scope(Message, %User{} = current_user), do: Message |> Policy.Message.scope(current_user)
31+
2532
@spec can?(User.t, atom, struct, map) :: boolean
2633

2734
# Category
@@ -41,13 +48,17 @@ defmodule CodeCorps.Policy do
4148
defp can?(%User{} = current_user, :create, %GithubAppInstallation{}, %{} = params), do: Policy.GithubAppInstallation.create?(current_user, params)
4249

4350
# GithubEvent
44-
defp can?(%User{} = current_user, :index, %GithubEvent{}, %{}), do: Policy.GithubEvent.index?(current_user)
4551
defp can?(%User{} = current_user, :show, %GithubEvent{}, %{}), do: Policy.GithubEvent.show?(current_user)
52+
defp can?(%User{} = current_user, :index, %GithubEvent{}, %{}), do: Policy.GithubEvent.index?(current_user)
4653
defp can?(%User{} = current_user, :update, %GithubEvent{}, %{}), do: Policy.GithubEvent.update?(current_user)
4754

4855
# GithubRepo
4956
defp can?(%User{} = current_user, :update, %GithubRepo{} = github_repo, %{} = params), do: Policy.GithubRepo.update?(current_user, github_repo, params)
5057

58+
# Message
59+
defp can?(%User{} = current_user, :show, %Message{} = message, %{}), do: Policy.Message.show?(current_user, message)
60+
defp can?(%User{} = current_user, :create, %Message{}, %{} = params), do: Policy.Message.create?(current_user, params)
61+
5162
# Organization
5263
defp can?(%User{} = current_user, :create, %Organization{}, %{} = params), do: Policy.Organization.create?(current_user, params)
5364
defp can?(%User{} = current_user, :update, %Organization{} = organization, %{} = params), do: Policy.Organization.update?(current_user, organization, params)

lib/code_corps/policy/task.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule CodeCorps.Policy.Task do
22
@moduledoc ~S"""
3-
Authorization policy in charge of dermining if a `User` is authorized to
3+
Authorization policy in charge of determining if a `User` is authorized to
44
perform an action on a `Task`.
55
"""
66
import CodeCorps.Policy.Helpers,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
defmodule CodeCorpsWeb.MessageController do
2+
@moduledoc false
3+
use CodeCorpsWeb, :controller
4+
5+
alias CodeCorps.{
6+
Message,
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+
messages <- Message |> Policy.scope(current_user) |> Messages.list(params) do
19+
conn |> render("index.json-api", data: messages)
20+
end
21+
end
22+
23+
@spec show(Conn.t, map) :: Conn.t
24+
def show(%Conn{} = conn, %{"id" => id}) do
25+
with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource,
26+
%Message{} = message <- Message |> Repo.get(id),
27+
{:ok, :authorized} <- current_user |> Policy.authorize(:show, message, %{}) do
28+
conn |> render("show.json-api", data: message)
29+
end
30+
end
31+
32+
@spec create(Plug.Conn.t, map) :: Conn.t
33+
def create(%Conn{} = conn, %{} = params) do
34+
with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource,
35+
{:ok, :authorized} <- current_user |> Policy.authorize(:create, %Message{}, params),
36+
{:ok, %Message{} = message} <- Messages.create(params),
37+
message <- preload(message)
38+
do
39+
conn |> put_status(:created) |> render("show.json-api", data: message)
40+
end
41+
end
42+
43+
@preloads [:author, :project]
44+
45+
def preload(data) do
46+
Repo.preload(data, @preloads)
47+
end
48+
end

lib/code_corps_web/router.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ defmodule CodeCorpsWeb.Router do
7676
resources "/github-app-installations", GithubAppInstallationController, only: [:create]
7777
resources "/github-events", GithubEventController, only: [:index, :show, :update]
7878
resources "/github-repos", GithubRepoController, only: [:update]
79+
resources "/messages", MessageController, only: [:index, :show, :create]
7980
resources "/organization-github-app-installations", OrganizationGithubAppInstallationController, only: [:create, :delete]
8081
resources "/organizations", OrganizationController, only: [:create, :update]
8182
resources "/organization-invites", OrganizationInviteController, only: [:create, :update]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule CodeCorpsWeb.MessageView do
2+
@moduledoc false
3+
use CodeCorpsWeb, :view
4+
use JaSerializer.PhoenixView
5+
6+
attributes [:body, :initiated_by, :inserted_at, :subject, :updated_at]
7+
8+
has_one :author, type: "user", field: :author_id
9+
has_one :project, type: "project", field: :project_id
10+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
defmodule CodeCorps.Repo.Migrations.CreateMessages do
2+
use Ecto.Migration
3+
4+
def change do
5+
create table(:messages) do
6+
add :body, :text
7+
add :initiated_by, :string
8+
add :subject, :text
9+
add :author_id, references(:users, on_delete: :nothing)
10+
add :project_id, references(:projects, on_delete: :nothing)
11+
12+
timestamps()
13+
end
14+
15+
create index(:messages, [:author_id])
16+
create index(:messages, [:initiated_by])
17+
create index(:messages, [:project_id])
18+
end
19+
end

0 commit comments

Comments
 (0)