Skip to content

Commit

Permalink
Merge pull request #1262 from code-corps/improve-organization-creation
Browse files Browse the repository at this point in the history
Improve organization creation
  • Loading branch information
joshsmith committed Nov 28, 2017
2 parents 5d56096 + 11da4e1 commit 2f49598
Show file tree
Hide file tree
Showing 22 changed files with 302 additions and 95 deletions.
23 changes: 17 additions & 6 deletions blueprint/api.apib
Original file line number Diff line number Diff line change
Expand Up @@ -507,13 +507,11 @@ Note that a Github Repo relates 1:1 to a Github App Installation, since only a s

This endpoint is for Creating, Updating, returning Organization Invites on Code Corps.

`fulfilled` field may only change from false to true.

##Organization Invites [/organization-invites]

### Create an organization invite [POST]

Admin creates new organization invites which contains `email` and `title`. A `code` is automatically generated on creation and `fulfilled` is set to false.
Admin creates new organization invites which contains `email` and `title`. A `code` is automatically generated on creation.

On successful creation an email is sent to the given `email` address.

Expand Down Expand Up @@ -614,7 +612,7 @@ On successful creation an email is sent to the given `email` address.

This endpoint retrieves Organizations on Code Corps. Organizations usually have one or more Projects.

Until the Code Corps platform is open to new organizations, only admin users can create new organizations.
A valid `invite_code` parameter is required to create an Organization. Without this parameter, only admin users are allowed to create new organizations.

## Organizations [/organizations]

Expand All @@ -626,6 +624,7 @@ Until the Code Corps platform is open to new organizations, only admin users can

Accept: application/vnd.api+json
Authorization: Bearer <token>
+ Attributes (Organization Create Request)

+ Response 201 (application/vnd.api+json; charset=utf-8)

Expand Down Expand Up @@ -3035,10 +3034,23 @@ The platform stores Stripe customers and cards so they can be reused across diff
+ `icon-large-url`: `//res.cloudinary.com/dlfnmtoq1/image/upload/c_fill,h_500,w_500/pp0md2banaw7k6oa1ew9`
+ `icon-thumb-url`: `//res.cloudinary.com/dlfnmtoq1/image/upload/c_fill,h_100,w_100/pp0md2banaw7k6oa1ew9`
+ `inserted-at`: `2016-07-08T03:03:51.967Z` (string)
+ `invite-code`: `valid-invite-code` (string)
+ name: `Code Corps`
+ slug: `code_corps`
+ slug: `code-corps`
+ `updated-at`: `2016-07-08T03:03:51.967Z` (string)

## Organization Create Request (object)
+ type: `organization` (string, required)
+ attributes
+ `cloudinary-public-id`: `pp0md2banaw7k6oa1ew9` (string, required)
+ description: `Build a better future.` (string, required)
+ `invite-code`: `valid-invite-code` (string)
+ name: `Code Corps` (string, required)
+ slug: `code-corps` (string)
+ relationships
+ owner
+ data(User Resource Identifier)

## Organization Resource (object)
+ include Organization Resource Identifier
+ attributes(Organization Attributes)
Expand Down Expand Up @@ -3069,7 +3081,6 @@ The platform stores Stripe customers and cards so they can be reused across diff
+ email: `head@corecorps.com` (string)
+ `inserted-at`: `2016-07-08T03:03:51.967Z` (string)
+ `updated-at`: `2016-07-08T03:03:51.967Z` (string)
+ fulfilled: `false` (boolean)

## Organization Invite Resource (object)
+ include Organization Invite Resource Identifier
Expand Down
26 changes: 19 additions & 7 deletions lib/code_corps/model/organization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,22 @@ defmodule CodeCorps.Organization do
import CodeCorps.Validators.SlugValidator

alias CodeCorps.SluggedRoute
alias Ecto.Changeset

@type t :: %__MODULE__{}

schema "organizations" do
field :approved, :boolean
field :cloudinary_public_id
field :default_color
field :description, :string
field :invite_code, :string, virtual: true
field :name, :string
field :slug, :string
field :approved, :boolean

belongs_to :owner, CodeCorps.User

has_one :organization_invite, CodeCorps.OrganizationInvite
has_one :slugged_route, CodeCorps.SluggedRoute
has_one :stripe_connect_account, CodeCorps.StripeConnectAccount

Expand All @@ -37,8 +40,8 @@ defmodule CodeCorps.Organization do
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name, :description, :slug, :cloudinary_public_id, :default_color])
|> validate_required([:name, :description])
|> cast(params, [:cloudinary_public_id, :description, :default_color, :name, :slug])
|> validate_required([:description, :name])
end

@doc """
Expand All @@ -47,16 +50,25 @@ defmodule CodeCorps.Organization do
def create_changeset(struct, params) do
struct
|> changeset(params)
|> cast(params, [:owner_id])
|> generate_slug(:name, :slug)
|> validate_required([:description, :owner_id, :slug])
|> cast(params, [:invite_code, :owner_id])
|> maybe_generate_slug()
|> validate_required([:cloudinary_public_id, :description, :owner_id, :slug])
|> assoc_constraint(:owner)
|> validate_slug(:slug)
|> unique_constraint(:slug, name: :organizations_lower_slug_index)
|> put_slugged_route()
|> generate_icon_color(:default_color)
|> put_change(:approved, false)
end

defp maybe_generate_slug(%Changeset{changes: %{slug: _}} = changeset) do
changeset
end
defp maybe_generate_slug(%Changeset{} = changeset) do
changeset |> generate_slug(:name, :slug)
end

defp put_slugged_route(changeset) do
defp put_slugged_route(%Changeset{} = changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{slug: slug}} ->
slugged_route_changeset = SluggedRoute.create_changeset(%SluggedRoute{}, %{slug: slug})
Expand Down
21 changes: 10 additions & 11 deletions lib/code_corps/model/organization_invite.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ defmodule CodeCorps.OrganizationInvite do
schema "organization_invites" do
field :code, :string
field :email, :string
field :fulfilled, :boolean, default: false
field :organization_name, :string

belongs_to :organization, CodeCorps.Organization

timestamps()
end

Expand All @@ -21,10 +22,9 @@ defmodule CodeCorps.OrganizationInvite do
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:email, :organization_name, :fulfilled])
|> cast(params, [:email, :organization_name])
|> validate_required([:email, :organization_name])
|> validate_format(:email, ~r/@/)
|> validate_change(:fulfilled, &check_fulfilled_changes_to_true/2)
end

@doc """
Expand All @@ -37,6 +37,13 @@ defmodule CodeCorps.OrganizationInvite do
|> unique_constraint(:code)
end

def update_changeset(struct, params) do
struct
|> changeset(params)
|> cast(params, [:organization_id])
|> assoc_constraint(:organization)
end

defp generate_code(changeset) do
case changeset do
%Ecto.Changeset{valid?: true} ->
Expand All @@ -52,12 +59,4 @@ defmodule CodeCorps.OrganizationInvite do
|> Base.encode64
|> binary_part(0, length)
end

defp check_fulfilled_changes_to_true :fulfilled, fulfilled do
if fulfilled == false do
[fulfillled: "Fulfilled can only change from false to true"]
else
[]
end
end
end
41 changes: 41 additions & 0 deletions lib/code_corps/organizations/organizations.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule CodeCorps.Organizations do
@moduledoc ~S"""
"""

alias CodeCorps.{Organization, OrganizationInvite, Repo}
alias Ecto.{Changeset, Multi}

@doc ~S"""
Creates a `CodeCorps.Organization` from a set of parameters,
fulfilling the associated `CodeCorps.OrganizationInvite`, if it exists, by
associating it with the created record.
"""
@spec create(map) :: {:ok, Organization.t} | {:error, Changeset.t}
def create(%{} = params) do
Multi.new()
|> Multi.insert(:organization, Organization.create_changeset(%Organization{}, params))
|> Multi.run(:organization_invite, fn %{organization: organization} -> organization |> fulfill_associated_invite(params) end)
|> Repo.transaction()
|> handle_result()
end

@spec fulfill_associated_invite(Organization.t, map) :: {:ok, OrganizationInvite.t | nil} | {:error, Changeset.t}
defp fulfill_associated_invite(%Organization{id: organization_id}, %{"invite_code" => code}) do
OrganizationInvite
|> Repo.get_by(code: code)
|> OrganizationInvite.update_changeset(%{organization_id: organization_id})
|> Repo.update()
end
defp fulfill_associated_invite(%Organization{}, %{}), do: {:ok, nil}

@spec handle_result(tuple) :: tuple
defp handle_result({:ok, %{organization: %Organization{} = organization}}) do
{:ok, organization}
end
defp handle_result({:error, :organization, %Changeset{} = changeset, _steps}) do
{:error, changeset}
end
defp handle_result({:error, :organization_invite, %Changeset{} = changeset, _steps}) do
{:error, changeset}
end
end
10 changes: 0 additions & 10 deletions lib/code_corps/policy/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ defmodule CodeCorps.Policy.Helpers do

alias CodeCorps.{
Organization,
OrganizationInvite,
ProjectUser,
Project,
ProjectUser,
Expand Down Expand Up @@ -67,15 +66,6 @@ defmodule CodeCorps.Policy.Helpers do

def get_organization(_), do: nil

@doc """
Retrieves an organiation invite from a struct, containing a `code` field
Returns `CodeCorps.OrganizationInvite` or nil
"""
@spec get_organization_invite(struct) :: OrganizationInvite.t() | nil
def get_organization_invite(%{"code" => code}),
do: OrganizationInvite |> Repo.get_by(code: code, fulfilled: false)
def get_organization_invite(%{}), do: nil

@doc """
Retrieves a project record, from a model struct, or an `Ecto.Changeset`
containing a `project_id` field
Expand Down
32 changes: 25 additions & 7 deletions lib/code_corps/policy/organization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,38 @@ defmodule CodeCorps.Policy.Organization do
@moduledoc ~S"""
Authorization policies for performing actions on `Organization` records
"""
import CodeCorps.Policy.Helpers,
only: [owned_by?: 2, get_organization_invite: 1]
import CodeCorps.Policy.Helpers, only: [owned_by?: 2]

alias CodeCorps.{Organization, User}
import Ecto.Query

alias CodeCorps.{Organization, OrganizationInvite, Repo, User}

@doc ~S"""
Returns a boolean indicating if the specified user is allowed to create the
organization specified by a map of attributes.
"""
@spec create?(User.t, map) :: boolean
def create?(%User{admin: true}, %{}), do: true
def create?(%User{}, %{} = params) do
case get_organization_invite(params) do
def create?(%User{}, %{"invite_code" => invite_code}) do
case invite_code |> get_invite() do
nil -> false
_ -> true
_invite -> true
end
end
def create?(%{}, %{}), do: false
def create?(%User{}, %{}), do: false

@doc ~S"""
Returns a boolean indicating if the specified user is allowed to update the
specified organization.
"""
@spec update?(User.t, Organization.t) :: boolean
def update?(%User{admin: true}, %Organization{}), do: true
def update?(%User{} = user, %Organization{} = organization), do: organization |> owned_by?(user)

@spec get_invite(String.t) :: OrganizationInvite.t | nil
defp get_invite(code) do
OrganizationInvite
|> where([oi], is_nil(oi.organization_id))
|> Repo.get_by(code: code)
end
end
4 changes: 2 additions & 2 deletions lib/code_corps_web/controllers/organization_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule CodeCorpsWeb.OrganizationController do
@moduledoc false
use CodeCorpsWeb, :controller

alias CodeCorps.{Helpers.Query, Organization, User}
alias CodeCorps.{Helpers.Query, Organization, Organizations, User}

action_fallback CodeCorpsWeb.FallbackController
plug CodeCorpsWeb.Plug.DataToAttributes
Expand Down Expand Up @@ -30,7 +30,7 @@ defmodule CodeCorpsWeb.OrganizationController do
def create(%Conn{} = conn, %{} = params) do
with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource,
{:ok, :authorized} <- current_user |> Policy.authorize(:create, %Organization{}, params),
{:ok, %Organization{} = organization} <- %Organization{} |> Organization.create_changeset(params) |> Repo.insert,
{:ok, %Organization{} = organization} <- Organizations.create(params),
organization <- preload(organization)
do
conn |> put_status(:created) |> render("show.json-api", data: organization)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ defmodule CodeCorpsWeb.OrganizationInviteController do

alias CodeCorps.{Emails, Helpers.Query, Mailer, OrganizationInvite, User}

import Ecto.Query

action_fallback CodeCorpsWeb.FallbackController
plug CodeCorpsWeb.Plug.DataToAttributes
plug CodeCorpsWeb.Plug.IdsToIntegers
Expand Down
6 changes: 4 additions & 2 deletions lib/code_corps_web/views/changeset_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ defmodule CodeCorpsWeb.ChangesetView do
defp format_attribute_errors(errors, attribute) do
errors
|> Map.get(attribute)
|> Enum.map(fn(message) -> create_error(attribute, message) end)
|> Enum.map(&create_error(attribute, &1))
end

def create_error(attribute, message) do
%{
detail: format_detail(attribute, message),
title: message,
source: %{
pointer: "data/attributes/#{attribute}"
pointer: "data/attributes/#{Utils.format_key(attribute)}"
},
status: "422"
}
Expand All @@ -61,6 +61,8 @@ defmodule CodeCorpsWeb.ChangesetView do
"#{attribute |> Utils.humanize |> translate_attribute} #{message}"
end

defp translate_attribute("Cloudinary public"), do: dgettext("errors", "Cloudinary public")
defp translate_attribute("Github"), do: dgettext("errors", "Github")
defp translate_attribute("Slug"), do: dgettext("errors", "Slug")
defp translate_attribute(attribute), do: attribute
end
4 changes: 3 additions & 1 deletion lib/code_corps_web/views/organization_invite_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule CodeCorpsWeb.OrganizationInviteView do
use JaSerializer.PhoenixView

attributes [
:email, :fulfilled, :inserted_at, :organization_name, :updated_at
:email, :inserted_at, :organization_name, :updated_at
]

has_one :organization, type: "organization", field: :organization_id
end
12 changes: 10 additions & 2 deletions priv/gettext/en/LC_MESSAGES/errors.po
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,18 @@ msgstr ""
msgid "must be equal to %{number}"
msgstr ""

#: lib/code_corps/accounts/changesets.ex:50
#: lib/code_corps/accounts/changesets.ex:71
msgid "account is already connected to someone else"
msgstr ""

#: lib/code_corps_web/views/changeset_view.ex:63
#: lib/code_corps_web/views/changeset_view.ex:65
msgid "Github"
msgstr "GitHub"

#: lib/code_corps_web/views/changeset_view.ex:66
msgid "Slug"
msgstr "Vanity URL"

#: lib/code_corps_web/views/changeset_view.ex:64
msgid "Cloudinary public"
msgstr "Image"
12 changes: 10 additions & 2 deletions priv/gettext/errors.pot
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,18 @@ msgstr ""
msgid "must be equal to %{number}"
msgstr ""

#: lib/code_corps/accounts/changesets.ex:50
#: lib/code_corps/accounts/changesets.ex:71
msgid "account is already connected to someone else"
msgstr ""

#: lib/code_corps_web/views/changeset_view.ex:63
#: lib/code_corps_web/views/changeset_view.ex:65
msgid "Github"
msgstr ""

#: lib/code_corps_web/views/changeset_view.ex:66
msgid "Slug"
msgstr ""

#: lib/code_corps_web/views/changeset_view.ex:64
msgid "Cloudinary public"
msgstr ""
Loading

0 comments on commit 2f49598

Please sign in to comment.