Skip to content

Commit 6abc321

Browse files
committed
Organizations Members CRUD actions
1 parent 91fbb8a commit 6abc321

File tree

6 files changed

+271
-1
lines changed

6 files changed

+271
-1
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
class Organizations::MembersController < ApplicationController
2+
before_action :redirect_to_signin, only: :index, unless: :signed_in?
3+
before_action :redirect_to_new_mfa, only: :index, if: :mfa_required_not_yet_enabled?
4+
before_action :redirect_to_settings_strong_mfa_required, only: :index, if: :mfa_required_weak_level_enabled?
5+
6+
before_action :find_organization, only: %i[create update destroy]
7+
before_action :find_membership, only: %i[update destroy]
8+
9+
layout "subject"
10+
11+
def index
12+
@organization = Organization.find_by_handle!(params[:organization_id])
13+
authorize @organization, :list_memberships?
14+
15+
@memberships = @organization.memberships.includes(:user)
16+
@memberships_count = @organization.memberships.count
17+
end
18+
19+
def create
20+
username, role = create_membership_params.require([:username, :role])
21+
# we can open this up in the future to handle email too via find_by_name,
22+
# but it will need to use an invite process to handle non-existing users.
23+
member = User.find_by(handle: username)
24+
if member
25+
membership = authorize @organization.memberships.build(user: member, role:)
26+
if membership.save
27+
flash[:notice] = t(".success", username: member.name)
28+
else
29+
flash[:error] = t(".failure", error: membership.errors.full_messages.to_sentence)
30+
end
31+
else
32+
flash[:error] = t(".failure", error: t(".user_not_found"))
33+
end
34+
redirect_to organization_members_path(@organization)
35+
end
36+
37+
def update
38+
@membership.attributes = update_membership_params
39+
authorize @membership
40+
if @membership.save
41+
flash[:notice] = t(".success")
42+
else
43+
flash[:error] = t(".failure", error: membership.errors.full_messages.to_sentence)
44+
end
45+
redirect_to organization_members_path(@organization)
46+
end
47+
48+
def destroy
49+
authorize @membership
50+
flash[:notice] = t(".success") if @membership.destroy
51+
redirect_to organization_members_path(@organization)
52+
end
53+
54+
private
55+
56+
def find_organization
57+
@organization = Organization.find_by_handle!(params[:organization_id])
58+
authorize @organization, :manage_memberships?
59+
end
60+
61+
def find_membership
62+
handle = params.permit(:id).require(:id)
63+
@member = User.find_by_slug!(handle)
64+
@membership = @organization.memberships.find_by!(user: @member)
65+
end
66+
67+
def create_membership_params
68+
params.permit(membership: %i[username role]).require(:membership)
69+
end
70+
71+
def update_membership_params
72+
params.permit(membership: %i[role]).require(:membership)
73+
end
74+
end

app/views/organizations/_subject.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<%= nav.link t("layouts.application.header.dashboard"), organization_path(@organization), name: :dashboard, icon: "space-dashboard" %>
1919
<%= nav.link t("organizations.show.history"), organization_path(@organization), name: :subscriptions, icon: "notifications" %>
2020
<%= nav.link t("organizations.show.gems"), organization_gems_path(@organization), name: :gems, icon: "gems" %>
21-
<%= nav.link t("organizations.show.members"), organization_path(@organization), name: :organizations, icon: "organizations" %>
21+
<%= nav.link t("organizations.show.members"), organization_members_path(@organization), name: :members, icon: "organizations" %>
2222
<% if policy(@organization).edit? %>
2323
<%= nav.link t("layouts.application.header.settings"), edit_organization_path(@organization), name: :settings, icon: "settings" %>
2424
<% end %>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<%
2+
add_breadcrumb t("breadcrumbs.org_name", name: @organization.handle), organization_path(@organization)
3+
add_breadcrumb t("breadcrumbs.members")
4+
%>
5+
6+
<% content_for :subject do %>
7+
<%= render "organizations/subject", organization: @organization, current: :members %>
8+
<% end %>
9+
10+
<h1 class="text-h2 mb-10"><%= t("organizations.show.members") %></h1>
11+
12+
<%= render CardComponent.new do |c| %>
13+
<%= c.head do %>
14+
<%= c.title t("organizations.show.members"), icon: :organizations %>
15+
<% end %>
16+
<% if @memberships.empty? %>
17+
<%= prose do %>
18+
<i><%= t('organizations.show.no_members') %></i>
19+
<% end %>
20+
<% else %>
21+
<%= c.divided_list do %>
22+
<% @memberships.each do |membership| %>
23+
<%= c.list_item_to(profile_path(membership.user.handle)) do %>
24+
<div class="flex justify-between">
25+
<p class="text-neutral-800 dark:text-white"><%= membership.user.name %></p>
26+
<p class="text-neutral-500 capitalize"><%= membership.role %></p>
27+
</div>
28+
<% end %>
29+
<% end %>
30+
<% end %>
31+
<% end %>
32+
<% end %>

config/locales/en.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,16 @@ en:
428428
members: Members
429429
no_history: No events yet
430430
no_gems: No gems yet
431+
members:
432+
create:
433+
failure: "Failed to add member: %{error}"
434+
success: "Member added!"
435+
user_not_found: "User not found"
436+
destroy:
437+
success: "User was removed from the organization"
438+
update:
439+
failure: "Failed to update member: %{error}"
440+
success: "User was updated"
431441
pages:
432442
about:
433443
contributors_amount: "%{count} Rubyists"

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@
290290
end
291291
resources :organizations, only: %i[index show edit update], constraints: { id: Patterns::ROUTE_PATTERN } do
292292
resources :gems, only: :index, controller: 'organizations/gems'
293+
resources :members, only: %i[index create update destroy], controller: 'organizations/members'
293294
end
294295
end
295296

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
require "test_helper"
2+
3+
class Organizations::MembersTest < ActionDispatch::IntegrationTest
4+
setup do
5+
@user = create(:user, remember_token_expires_at: Gemcutter::REMEMBER_FOR.from_now)
6+
post session_path(session: { who: @user.handle, password: PasswordHelpers::SECURE_TEST_PASSWORD })
7+
end
8+
9+
test "index should render Not Found org" do
10+
get "/organizations/notfound/members"
11+
12+
assert_response :not_found
13+
end
14+
15+
test "index should render Forbidden" do
16+
create(:organization, handle: "chaos")
17+
18+
get "/organizations/chaos/members"
19+
20+
assert_response :forbidden
21+
end
22+
23+
test "should get index" do
24+
create(:organization, owners: [@user], handle: "chaos")
25+
26+
get "/organizations/chaos/members"
27+
28+
assert_response :success
29+
assert page.has_content?("Members")
30+
end
31+
32+
test "create should return Not Found org" do
33+
post "/organizations/notfound/members", params: { membership: { role: "owner" } }
34+
35+
assert_response :not_found
36+
end
37+
38+
test "create should return Forbidden when trying to create your own membership" do
39+
create(:organization, handle: "chaos")
40+
41+
post "/organizations/chaos/members", params: { membership: { username: @user.id, role: "maintainer" } }
42+
43+
assert_response :forbidden
44+
end
45+
46+
test "create membership with bad role should not work" do
47+
organization = create(:organization, owners: [@user], handle: "chaos")
48+
bdfl = create(:user, handle: "bdfl")
49+
50+
post "/organizations/chaos/members", params: { membership: { username: bdfl.handle, role: "bdfl" } }
51+
52+
assert_redirected_to organization_members_path(organization)
53+
follow_redirect!
54+
55+
assert page.has_content?("Failed to add member: Role is not included in the list")
56+
assert_nil organization.unconfirmed_memberships.find_by(user_id: bdfl.id)
57+
end
58+
59+
test "create membership by email should not work (yet)" do
60+
organization = create(:organization, owners: [@user], handle: "chaos")
61+
maintainer = create(:user, handle: "maintainer")
62+
63+
post "/organizations/chaos/members", params: { membership: { username: maintainer.email, role: "maintainer" } }
64+
65+
assert_redirected_to organization_members_path(organization)
66+
follow_redirect!
67+
68+
assert page.has_content?("Failed to add member: User not found")
69+
assert_nil organization.unconfirmed_memberships.find_by(user_id: maintainer.id)
70+
end
71+
72+
test "should create a membership by handle" do
73+
organization = create(:organization, owners: [@user], handle: "chaos")
74+
maintainer = create(:user, handle: "maintainer")
75+
76+
post "/organizations/chaos/members", params: { membership: { username: maintainer.handle, role: "maintainer" } }
77+
78+
assert_redirected_to organization_members_path(organization)
79+
membership = organization.unconfirmed_memberships.find_by(user_id: maintainer.id)
80+
81+
assert membership
82+
assert_predicate membership, :maintainer?
83+
refute_predicate membership, :confirmed?
84+
end
85+
86+
test "update should return Not Found org" do
87+
patch "/organizations/notfound/members/notfound", params: { membership: { role: "owner" } }
88+
89+
assert_response :not_found
90+
end
91+
92+
test "update should return Not Found membership" do
93+
create(:organization, owners: [@user], handle: "chaos")
94+
95+
patch "/organizations/chaos/members/notfound", params: { membership: { role: "owner" } }
96+
97+
assert_response :not_found
98+
end
99+
100+
test "update should return Forbidden" do
101+
organization = create(:organization, handle: "chaos")
102+
membership = create(:membership, :maintainer, user: @user, organization: organization)
103+
104+
patch "/organizations/chaos/members/#{@user.handle}", params: { membership: { role: "owner" } }
105+
106+
assert_response :forbidden
107+
end
108+
109+
test "should update" do
110+
organization = create(:organization, owners: [@user], handle: "chaos")
111+
maintainer = create(:user, handle: "maintainer")
112+
membership = create(:membership, :maintainer, user: maintainer, organization: organization)
113+
114+
patch "/organizations/chaos/members/#{maintainer.handle}", params: { membership: { role: "owner" } }
115+
116+
assert_redirected_to organization_members_path(organization)
117+
assert_predicate membership.reload, :owner?
118+
end
119+
120+
test "destroy should return Not Found org" do
121+
delete "/organizations/notfound/members/notfound"
122+
123+
assert_response :not_found
124+
end
125+
126+
test "destroy should return Not Found membership" do
127+
create(:organization, owners: [@user], handle: "chaos")
128+
129+
delete "/organizations/chaos/members/notfound"
130+
131+
assert_response :not_found
132+
end
133+
134+
test "destroy should return Forbidden" do
135+
organization = create(:organization, handle: "chaos")
136+
membership = create(:membership, :maintainer, user: @user, organization: organization)
137+
138+
delete "/organizations/chaos/members/#{@user.handle}"
139+
140+
assert_response :forbidden
141+
end
142+
143+
test "should destroy a membership" do
144+
organization = create(:organization, handle: "chaos", owners: [@user])
145+
maintainer = create(:user, handle: "maintainer")
146+
membership = create(:membership, :maintainer, user: maintainer, organization: organization)
147+
148+
delete "/organizations/chaos/members/#{maintainer.handle}"
149+
150+
assert_redirected_to organization_members_path(organization)
151+
assert_nil Membership.find_by(id: membership.id)
152+
end
153+
end

0 commit comments

Comments
 (0)