Skip to content

Commit 11da4e1

Browse files
committed
Improve organization creation
- Add Organizations modules and fix changesets, add migration - Changed fulfillment behavior to be code based - Updated API docs - Added explicit flagging as unapproved when creating organization - Add test coverage for slug generation behavior
1 parent 5d56096 commit 11da4e1

22 files changed

+302
-95
lines changed

blueprint/api.apib

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -507,13 +507,11 @@ Note that a Github Repo relates 1:1 to a Github App Installation, since only a s
507507

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

510-
`fulfilled` field may only change from false to true.
511-
512510
##Organization Invites [/organization-invites]
513511

514512
### Create an organization invite [POST]
515513

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

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

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

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

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

619617
## Organizations [/organizations]
620618

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

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

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

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

3042+
## Organization Create Request (object)
3043+
+ type: `organization` (string, required)
3044+
+ attributes
3045+
+ `cloudinary-public-id`: `pp0md2banaw7k6oa1ew9` (string, required)
3046+
+ description: `Build a better future.` (string, required)
3047+
+ `invite-code`: `valid-invite-code` (string)
3048+
+ name: `Code Corps` (string, required)
3049+
+ slug: `code-corps` (string)
3050+
+ relationships
3051+
+ owner
3052+
+ data(User Resource Identifier)
3053+
30423054
## Organization Resource (object)
30433055
+ include Organization Resource Identifier
30443056
+ attributes(Organization Attributes)
@@ -3069,7 +3081,6 @@ The platform stores Stripe customers and cards so they can be reused across diff
30693081
+ email: `head@corecorps.com` (string)
30703082
+ `inserted-at`: `2016-07-08T03:03:51.967Z` (string)
30713083
+ `updated-at`: `2016-07-08T03:03:51.967Z` (string)
3072-
+ fulfilled: `false` (boolean)
30733084

30743085
## Organization Invite Resource (object)
30753086
+ include Organization Invite Resource Identifier

lib/code_corps/model/organization.ex

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,22 @@ defmodule CodeCorps.Organization do
1010
import CodeCorps.Validators.SlugValidator
1111

1212
alias CodeCorps.SluggedRoute
13+
alias Ecto.Changeset
1314

1415
@type t :: %__MODULE__{}
1516

1617
schema "organizations" do
18+
field :approved, :boolean
1719
field :cloudinary_public_id
1820
field :default_color
1921
field :description, :string
22+
field :invite_code, :string, virtual: true
2023
field :name, :string
2124
field :slug, :string
22-
field :approved, :boolean
2325

2426
belongs_to :owner, CodeCorps.User
2527

28+
has_one :organization_invite, CodeCorps.OrganizationInvite
2629
has_one :slugged_route, CodeCorps.SluggedRoute
2730
has_one :stripe_connect_account, CodeCorps.StripeConnectAccount
2831

@@ -37,8 +40,8 @@ defmodule CodeCorps.Organization do
3740
"""
3841
def changeset(struct, params \\ %{}) do
3942
struct
40-
|> cast(params, [:name, :description, :slug, :cloudinary_public_id, :default_color])
41-
|> validate_required([:name, :description])
43+
|> cast(params, [:cloudinary_public_id, :description, :default_color, :name, :slug])
44+
|> validate_required([:description, :name])
4245
end
4346

4447
@doc """
@@ -47,16 +50,25 @@ defmodule CodeCorps.Organization do
4750
def create_changeset(struct, params) do
4851
struct
4952
|> changeset(params)
50-
|> cast(params, [:owner_id])
51-
|> generate_slug(:name, :slug)
52-
|> validate_required([:description, :owner_id, :slug])
53+
|> cast(params, [:invite_code, :owner_id])
54+
|> maybe_generate_slug()
55+
|> validate_required([:cloudinary_public_id, :description, :owner_id, :slug])
5356
|> assoc_constraint(:owner)
5457
|> validate_slug(:slug)
58+
|> unique_constraint(:slug, name: :organizations_lower_slug_index)
5559
|> put_slugged_route()
5660
|> generate_icon_color(:default_color)
61+
|> put_change(:approved, false)
62+
end
63+
64+
defp maybe_generate_slug(%Changeset{changes: %{slug: _}} = changeset) do
65+
changeset
66+
end
67+
defp maybe_generate_slug(%Changeset{} = changeset) do
68+
changeset |> generate_slug(:name, :slug)
5769
end
5870

59-
defp put_slugged_route(changeset) do
71+
defp put_slugged_route(%Changeset{} = changeset) do
6072
case changeset do
6173
%Ecto.Changeset{valid?: true, changes: %{slug: slug}} ->
6274
slugged_route_changeset = SluggedRoute.create_changeset(%SluggedRoute{}, %{slug: slug})

lib/code_corps/model/organization_invite.ex

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ defmodule CodeCorps.OrganizationInvite do
1010
schema "organization_invites" do
1111
field :code, :string
1212
field :email, :string
13-
field :fulfilled, :boolean, default: false
1413
field :organization_name, :string
1514

15+
belongs_to :organization, CodeCorps.Organization
16+
1617
timestamps()
1718
end
1819

@@ -21,10 +22,9 @@ defmodule CodeCorps.OrganizationInvite do
2122
"""
2223
def changeset(struct, params \\ %{}) do
2324
struct
24-
|> cast(params, [:email, :organization_name, :fulfilled])
25+
|> cast(params, [:email, :organization_name])
2526
|> validate_required([:email, :organization_name])
2627
|> validate_format(:email, ~r/@/)
27-
|> validate_change(:fulfilled, &check_fulfilled_changes_to_true/2)
2828
end
2929

3030
@doc """
@@ -37,6 +37,13 @@ defmodule CodeCorps.OrganizationInvite do
3737
|> unique_constraint(:code)
3838
end
3939

40+
def update_changeset(struct, params) do
41+
struct
42+
|> changeset(params)
43+
|> cast(params, [:organization_id])
44+
|> assoc_constraint(:organization)
45+
end
46+
4047
defp generate_code(changeset) do
4148
case changeset do
4249
%Ecto.Changeset{valid?: true} ->
@@ -52,12 +59,4 @@ defmodule CodeCorps.OrganizationInvite do
5259
|> Base.encode64
5360
|> binary_part(0, length)
5461
end
55-
56-
defp check_fulfilled_changes_to_true :fulfilled, fulfilled do
57-
if fulfilled == false do
58-
[fulfillled: "Fulfilled can only change from false to true"]
59-
else
60-
[]
61-
end
62-
end
6362
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
defmodule CodeCorps.Organizations do
2+
@moduledoc ~S"""
3+
"""
4+
5+
alias CodeCorps.{Organization, OrganizationInvite, Repo}
6+
alias Ecto.{Changeset, Multi}
7+
8+
@doc ~S"""
9+
Creates a `CodeCorps.Organization` from a set of parameters,
10+
fulfilling the associated `CodeCorps.OrganizationInvite`, if it exists, by
11+
associating it with the created record.
12+
"""
13+
@spec create(map) :: {:ok, Organization.t} | {:error, Changeset.t}
14+
def create(%{} = params) do
15+
Multi.new()
16+
|> Multi.insert(:organization, Organization.create_changeset(%Organization{}, params))
17+
|> Multi.run(:organization_invite, fn %{organization: organization} -> organization |> fulfill_associated_invite(params) end)
18+
|> Repo.transaction()
19+
|> handle_result()
20+
end
21+
22+
@spec fulfill_associated_invite(Organization.t, map) :: {:ok, OrganizationInvite.t | nil} | {:error, Changeset.t}
23+
defp fulfill_associated_invite(%Organization{id: organization_id}, %{"invite_code" => code}) do
24+
OrganizationInvite
25+
|> Repo.get_by(code: code)
26+
|> OrganizationInvite.update_changeset(%{organization_id: organization_id})
27+
|> Repo.update()
28+
end
29+
defp fulfill_associated_invite(%Organization{}, %{}), do: {:ok, nil}
30+
31+
@spec handle_result(tuple) :: tuple
32+
defp handle_result({:ok, %{organization: %Organization{} = organization}}) do
33+
{:ok, organization}
34+
end
35+
defp handle_result({:error, :organization, %Changeset{} = changeset, _steps}) do
36+
{:error, changeset}
37+
end
38+
defp handle_result({:error, :organization_invite, %Changeset{} = changeset, _steps}) do
39+
{:error, changeset}
40+
end
41+
end

lib/code_corps/policy/helpers.ex

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ defmodule CodeCorps.Policy.Helpers do
66

77
alias CodeCorps.{
88
Organization,
9-
OrganizationInvite,
109
ProjectUser,
1110
Project,
1211
ProjectUser,
@@ -67,15 +66,6 @@ defmodule CodeCorps.Policy.Helpers do
6766

6867
def get_organization(_), do: nil
6968

70-
@doc """
71-
Retrieves an organiation invite from a struct, containing a `code` field
72-
Returns `CodeCorps.OrganizationInvite` or nil
73-
"""
74-
@spec get_organization_invite(struct) :: OrganizationInvite.t() | nil
75-
def get_organization_invite(%{"code" => code}),
76-
do: OrganizationInvite |> Repo.get_by(code: code, fulfilled: false)
77-
def get_organization_invite(%{}), do: nil
78-
7969
@doc """
8070
Retrieves a project record, from a model struct, or an `Ecto.Changeset`
8171
containing a `project_id` field

lib/code_corps/policy/organization.ex

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,38 @@ defmodule CodeCorps.Policy.Organization do
22
@moduledoc ~S"""
33
Authorization policies for performing actions on `Organization` records
44
"""
5-
import CodeCorps.Policy.Helpers,
6-
only: [owned_by?: 2, get_organization_invite: 1]
5+
import CodeCorps.Policy.Helpers, only: [owned_by?: 2]
76

8-
alias CodeCorps.{Organization, User}
7+
import Ecto.Query
98

9+
alias CodeCorps.{Organization, OrganizationInvite, Repo, User}
10+
11+
@doc ~S"""
12+
Returns a boolean indicating if the specified user is allowed to create the
13+
organization specified by a map of attributes.
14+
"""
15+
@spec create?(User.t, map) :: boolean
1016
def create?(%User{admin: true}, %{}), do: true
11-
def create?(%User{}, %{} = params) do
12-
case get_organization_invite(params) do
17+
def create?(%User{}, %{"invite_code" => invite_code}) do
18+
case invite_code |> get_invite() do
1319
nil -> false
14-
_ -> true
20+
_invite -> true
1521
end
1622
end
17-
def create?(%{}, %{}), do: false
23+
def create?(%User{}, %{}), do: false
1824

25+
@doc ~S"""
26+
Returns a boolean indicating if the specified user is allowed to update the
27+
specified organization.
28+
"""
29+
@spec update?(User.t, Organization.t) :: boolean
1930
def update?(%User{admin: true}, %Organization{}), do: true
2031
def update?(%User{} = user, %Organization{} = organization), do: organization |> owned_by?(user)
32+
33+
@spec get_invite(String.t) :: OrganizationInvite.t | nil
34+
defp get_invite(code) do
35+
OrganizationInvite
36+
|> where([oi], is_nil(oi.organization_id))
37+
|> Repo.get_by(code: code)
38+
end
2139
end

lib/code_corps_web/controllers/organization_controller.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule CodeCorpsWeb.OrganizationController do
22
@moduledoc false
33
use CodeCorpsWeb, :controller
44

5-
alias CodeCorps.{Helpers.Query, Organization, User}
5+
alias CodeCorps.{Helpers.Query, Organization, Organizations, User}
66

77
action_fallback CodeCorpsWeb.FallbackController
88
plug CodeCorpsWeb.Plug.DataToAttributes
@@ -30,7 +30,7 @@ defmodule CodeCorpsWeb.OrganizationController do
3030
def create(%Conn{} = conn, %{} = params) do
3131
with %User{} = current_user <- conn |> CodeCorps.Guardian.Plug.current_resource,
3232
{:ok, :authorized} <- current_user |> Policy.authorize(:create, %Organization{}, params),
33-
{:ok, %Organization{} = organization} <- %Organization{} |> Organization.create_changeset(params) |> Repo.insert,
33+
{:ok, %Organization{} = organization} <- Organizations.create(params),
3434
organization <- preload(organization)
3535
do
3636
conn |> put_status(:created) |> render("show.json-api", data: organization)

lib/code_corps_web/controllers/organization_invite_controller.ex

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ defmodule CodeCorpsWeb.OrganizationInviteController do
44

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

7-
import Ecto.Query
8-
97
action_fallback CodeCorpsWeb.FallbackController
108
plug CodeCorpsWeb.Plug.DataToAttributes
119
plug CodeCorpsWeb.Plug.IdsToIntegers

lib/code_corps_web/views/changeset_view.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ defmodule CodeCorpsWeb.ChangesetView do
3232
defp format_attribute_errors(errors, attribute) do
3333
errors
3434
|> Map.get(attribute)
35-
|> Enum.map(fn(message) -> create_error(attribute, message) end)
35+
|> Enum.map(&create_error(attribute, &1))
3636
end
3737

3838
def create_error(attribute, message) do
3939
%{
4040
detail: format_detail(attribute, message),
4141
title: message,
4242
source: %{
43-
pointer: "data/attributes/#{attribute}"
43+
pointer: "data/attributes/#{Utils.format_key(attribute)}"
4444
},
4545
status: "422"
4646
}
@@ -61,6 +61,8 @@ defmodule CodeCorpsWeb.ChangesetView do
6161
"#{attribute |> Utils.humanize |> translate_attribute} #{message}"
6262
end
6363

64+
defp translate_attribute("Cloudinary public"), do: dgettext("errors", "Cloudinary public")
6465
defp translate_attribute("Github"), do: dgettext("errors", "Github")
66+
defp translate_attribute("Slug"), do: dgettext("errors", "Slug")
6567
defp translate_attribute(attribute), do: attribute
6668
end

lib/code_corps_web/views/organization_invite_view.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ defmodule CodeCorpsWeb.OrganizationInviteView do
44
use JaSerializer.PhoenixView
55

66
attributes [
7-
:email, :fulfilled, :inserted_at, :organization_name, :updated_at
7+
:email, :inserted_at, :organization_name, :updated_at
88
]
9+
10+
has_one :organization, type: "organization", field: :organization_id
911
end

0 commit comments

Comments
 (0)