Skip to content

Commit

Permalink
Merge pull request #1278 from code-corps/add-messages
Browse files Browse the repository at this point in the history
Add messages
  • Loading branch information
joshsmith authored Dec 13, 2017
2 parents 18ac285 + 978300d commit 8fe691f
Show file tree
Hide file tree
Showing 17 changed files with 829 additions and 4 deletions.
30 changes: 30 additions & 0 deletions lib/code_corps/messages/messages.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule CodeCorps.Messages do
@moduledoc ~S"""
Main context for work with the Messaging feature.
"""

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

@doc ~S"""
Lists pre-scoped `CodeCorps.Message` records filtered by parameters.
"""
@spec list(Queryable.t, map) :: list(Message.t)
def list(scope, %{} = params) do
scope
|> Query.id_filter(params)
|> Messages.Query.author_filter(params)
|> Messages.Query.project_filter(params)
|> Repo.all()
end

@doc ~S"""
Creates a `CodeCorps.Message` from a set of parameters.
"""
@spec create(map) :: {:ok, Message.t} | {:error, Changeset.t}
def create(%{} = params) do
%Message{}
|> Message.changeset(params)
|> Repo.insert()
end
end
29 changes: 29 additions & 0 deletions lib/code_corps/messages/query.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule CodeCorps.Messages.Query do
@moduledoc ~S"""
Holds helpers to query `CodeCorps.Message` records using a map of params.
"""

import Ecto.Query, only: [where: 3]

alias Ecto.Queryable

@doc ~S"""
Narrows down a `CodeCorps.Message` query by `author_id`, if specified in a
params map
"""
@spec author_filter(Queryable.t, map) :: Queryable.t
def author_filter(queryable, %{"author_id" => author_id}) do
queryable |> where([m], m.author_id == ^author_id)
end
def author_filter(queryable, %{}), do: queryable

@doc ~S"""
Narrows down a `CodeCorps.Message` query by `project_id`, if specified in a
params map
"""
@spec project_filter(Queryable.t, map) :: Queryable.t
def project_filter(queryable, %{"project_id" => project_id}) do
queryable |> where([m], m.project_id == ^project_id)
end
def project_filter(queryable, %{}), do: queryable
end
57 changes: 57 additions & 0 deletions lib/code_corps/model/message.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
defmodule CodeCorps.Message do
@moduledoc """
A message sent from a project to a user or from a user to a project.
The author does not need to be a member of the project in order to send a
message to the project.
No recipient will be defined for the message. The recipient is defined at the
level of the `CodeCorps.Conversation`.
A message may be used as a broadcast to a number of users. A message MAY
therefore have many conversations associated with it.
"""

use CodeCorps.Model
alias CodeCorps.Message

@type t :: %__MODULE__{}

schema "messages" do
field :body, :string
field :initiated_by, :string
field :subject, :string

belongs_to :author, CodeCorps.User
belongs_to :project, CodeCorps.Project

timestamps()
end

@doc false
@spec changeset(Message.t, map) :: Ecto.Changeset.t
def changeset(%Message{} = message, attrs) do
message
|> cast(attrs, [:body, :initiated_by, :subject])
|> validate_required([:body, :initiated_by])
|> validate_inclusion(:initiated_by, initiated_by_sources())
|> require_subject_if_admin()
end

# validate subject only if initiated_by "admin"
@spec require_subject_if_admin(Ecto.Changeset.t) :: Ecto.Changeset.t
defp require_subject_if_admin(changeset) do
initiated_by = changeset |> Ecto.Changeset.get_field(:initiated_by)
changeset |> do_require_subject_if_admin(initiated_by)
end

defp do_require_subject_if_admin(changeset, "admin") do
changeset |> validate_required(:subject)
end
defp do_require_subject_if_admin(changeset, _), do: changeset

@spec initiated_by_sources :: list(String.t)
defp initiated_by_sources do
~w{ admin user }
end
end
43 changes: 43 additions & 0 deletions lib/code_corps/policy/message.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
defmodule CodeCorps.Policy.Message do
@moduledoc """
Handles `User` authorization of actions on `Message` records
"""

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

alias CodeCorps.{Message, Project, ProjectUser, User, Repo}

@spec scope(Ecto.Queryable.t, User.t) :: Ecto.Queryable.t
def scope(queryable, %User{admin: true}), do: queryable
def scope(queryable, %User{id: id}) do
projects_administered_by_user_ids =
Project
|> join(:inner, [p], pu in ProjectUser, pu.project_id == p.id)
|> where([_p, pu], pu.user_id == ^id)
|> where([_p, pu], pu.role in ~w(admin owner))
|> select([p], p.id)
|> Repo.all

queryable
|> where([m], m.author_id == ^id)
|> or_where([m], m.project_id in ^projects_administered_by_user_ids)
end

def show?(%User{id: user_id}, %{initiated_by: "user", author_id: author_id})
when user_id == author_id do
true
end
def show?(%User{} = user, %Message{} = message) do
message |> get_project() |> administered_by?(user)
end
def show?(_, _), do: false

def create?(%User{id: id}, %{"initiated_by" => "user", "author_id" => author_id}) when id === author_id do
true
end
def create?(%User{} = user, %{"initiated_by" => "admin", "project_id" => _} = params) do
params |> get_project() |> administered_by?(user)
end
def create?(_, _), do: false
end
15 changes: 13 additions & 2 deletions lib/code_corps/policy/policy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule CodeCorps.Policy do
Handles authorization for various API actions performed on objects in the database.
"""

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}
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}

alias CodeCorps.Policy

Expand All @@ -22,6 +22,13 @@ defmodule CodeCorps.Policy do
end
end

@doc ~S"""
Scopes a queryable so it's only able to return those records the specified
user is authorized to view.
"""
@spec scope(module, User.t) :: Ecto.Queryable.t
def scope(Message, %User{} = current_user), do: Message |> Policy.Message.scope(current_user)

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

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

# GithubEvent
defp can?(%User{} = current_user, :index, %GithubEvent{}, %{}), do: Policy.GithubEvent.index?(current_user)
defp can?(%User{} = current_user, :show, %GithubEvent{}, %{}), do: Policy.GithubEvent.show?(current_user)
defp can?(%User{} = current_user, :index, %GithubEvent{}, %{}), do: Policy.GithubEvent.index?(current_user)
defp can?(%User{} = current_user, :update, %GithubEvent{}, %{}), do: Policy.GithubEvent.update?(current_user)

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

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

# Organization
defp can?(%User{} = current_user, :create, %Organization{}, %{} = params), do: Policy.Organization.create?(current_user, params)
defp can?(%User{} = current_user, :update, %Organization{} = organization, %{} = params), do: Policy.Organization.update?(current_user, organization, params)
Expand Down
2 changes: 1 addition & 1 deletion lib/code_corps/policy/task.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule CodeCorps.Policy.Task do
@moduledoc ~S"""
Authorization policy in charge of dermining if a `User` is authorized to
Authorization policy in charge of determining if a `User` is authorized to
perform an action on a `Task`.
"""
import CodeCorps.Policy.Helpers,
Expand Down
48 changes: 48 additions & 0 deletions lib/code_corps_web/controllers/message_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
defmodule CodeCorpsWeb.MessageController do
@moduledoc false
use CodeCorpsWeb, :controller

alias CodeCorps.{
Message,
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,
messages <- Message |> Policy.scope(current_user) |> Messages.list(params) do
conn |> render("index.json-api", data: messages)
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,
%Message{} = message <- Message |> Repo.get(id),
{:ok, :authorized} <- current_user |> Policy.authorize(:show, message, %{}) do
conn |> render("show.json-api", data: message)
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, %Message{}, params),
{:ok, %Message{} = message} <- Messages.create(params),
message <- preload(message)
do
conn |> put_status(:created) |> render("show.json-api", data: message)
end
end

@preloads [:author, :project]

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 @@ -76,6 +76,7 @@ defmodule CodeCorpsWeb.Router do
resources "/github-app-installations", GithubAppInstallationController, only: [:create]
resources "/github-events", GithubEventController, only: [:index, :show, :update]
resources "/github-repos", GithubRepoController, only: [:update]
resources "/messages", MessageController, only: [:index, :show, :create]
resources "/organization-github-app-installations", OrganizationGithubAppInstallationController, only: [:create, :delete]
resources "/organizations", OrganizationController, only: [:create, :update]
resources "/organization-invites", OrganizationInviteController, only: [:create, :update]
Expand Down
10 changes: 10 additions & 0 deletions lib/code_corps_web/views/message_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule CodeCorpsWeb.MessageView do
@moduledoc false
use CodeCorpsWeb, :view
use JaSerializer.PhoenixView

attributes [:body, :initiated_by, :inserted_at, :subject, :updated_at]

has_one :author, type: "user", field: :author_id
has_one :project, type: "project", field: :project_id
end
19 changes: 19 additions & 0 deletions priv/repo/migrations/20171205161052_create_messages.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule CodeCorps.Repo.Migrations.CreateMessages do
use Ecto.Migration

def change do
create table(:messages) do
add :body, :text
add :initiated_by, :string
add :subject, :text
add :author_id, references(:users, on_delete: :nothing)
add :project_id, references(:projects, on_delete: :nothing)

timestamps()
end

create index(:messages, [:author_id])
create index(:messages, [:initiated_by])
create index(:messages, [:project_id])
end
end
Loading

0 comments on commit 8fe691f

Please sign in to comment.