Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Organization members #5263

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions app/controllers/organizations/members_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
class Organizations::MembersController < ApplicationController
before_action :redirect_to_signin, only: :index, unless: :signed_in?
before_action :redirect_to_new_mfa, only: :index, if: :mfa_required_not_yet_enabled?
before_action :redirect_to_settings_strong_mfa_required, only: :index, if: :mfa_required_weak_level_enabled?

before_action :find_organization, only: %i[create update destroy]
before_action :find_membership, only: %i[update destroy]

layout "subject"

def index
@organization = Organization.find_by_handle!(params[:organization_id])
authorize @organization, :list_memberships?

@memberships = @organization.memberships.includes(:user)
@memberships_count = @organization.memberships.count
end

def create
username, role = create_membership_params.require([:username, :role])
# we can open this up in the future to handle email too via find_by_name,
# but it will need to use an invite process to handle non-existing users.
member = User.find_by(handle: username)
if member
membership = authorize @organization.memberships.build(user: member, role:)
if membership.save
flash[:notice] = t(".success", username: member.name)
else
flash[:error] = t(".failure", error: membership.errors.full_messages.to_sentence)
end
else
flash[:error] = t(".failure", error: t(".user_not_found"))
end
redirect_to organization_members_path(@organization)
end

def update
@membership.attributes = update_membership_params
authorize @membership
if @membership.save
flash[:notice] = t(".success")
else
flash[:error] = t(".failure", error: membership.errors.full_messages.to_sentence)

Check warning on line 43 in app/controllers/organizations/members_controller.rb

View check run for this annotation

Codecov / codecov/patch

app/controllers/organizations/members_controller.rb#L43

Added line #L43 was not covered by tests
end
redirect_to organization_members_path(@organization)
end

def destroy
authorize @membership
flash[:notice] = t(".success") if @membership.destroy
redirect_to organization_members_path(@organization)
end

private

def find_organization
@organization = Organization.find_by_handle!(params[:organization_id])
authorize @organization, :manage_memberships?
end

def find_membership
handle = params.permit(:id).require(:id)
@member = User.find_by_slug!(handle)
@membership = @organization.memberships.find_by!(user: @member)
end

def create_membership_params
params.permit(membership: %i[username role]).require(:membership)
end

def update_membership_params
params.permit(membership: %i[role]).require(:membership)
end
end
2 changes: 1 addition & 1 deletion app/views/components/card_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class CardComponent < ApplicationComponent
def view_template(&)
color = "bg-white dark:bg-black border border-neutral-200 dark:border-neutral-800 text-neutral-900 dark:text-white "
box = "w-full px-4 py-6 md:p-10 mb-10 rounded-md shadow overflow-hidden"
article(**classes(color, box), &)
section(**classes(color, box), &)
end

def head(title = nil, icon: nil, count: nil, url: nil, **options, &block)
Expand Down
2 changes: 1 addition & 1 deletion app/views/organizations/_subject.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<%= nav.link t("layouts.application.header.dashboard"), organization_path(@organization), name: :dashboard, icon: "space-dashboard" %>
<%= nav.link t("organizations.show.history"), organization_path(@organization), name: :subscriptions, icon: "notifications" %>
<%= nav.link t("organizations.show.gems"), organization_gems_path(@organization), name: :gems, icon: "gems" %>
<%= nav.link t("organizations.show.members"), organization_path(@organization), name: :organizations, icon: "organizations" %>
<%= nav.link t("organizations.show.members"), organization_members_path(@organization), name: :members, icon: "organizations" %>
<% if policy(@organization).edit? %>
<%= nav.link t("layouts.application.header.settings"), edit_organization_path(@organization), name: :settings, icon: "settings" %>
<% end %>
Expand Down
32 changes: 32 additions & 0 deletions app/views/organizations/members/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<%
add_breadcrumb t("breadcrumbs.org_name", name: @organization.handle), organization_path(@organization)
add_breadcrumb t("breadcrumbs.members")
%>
<% content_for :subject do %>
<%= render "organizations/subject", organization: @organization, current: :members %>
<% end %>

<h1 class="text-h2 mb-10"><%= t("organizations.show.members") %></h1>

<%= render CardComponent.new do |c| %>
<%= c.head do %>
<%= c.title t("organizations.show.members"), icon: :organizations %>
<% end %>
<% if @memberships.empty? %>
<%= prose do %>
<i><%= t('organizations.show.no_members') %></i>
<% end %>
<% else %>
<%= c.divided_list do %>
<% @memberships.each do |membership| %>
<%= c.list_item_to(profile_path(membership.user.handle)) do %>
<div class="flex justify-between">
<p class="text-neutral-800 dark:text-white"><%= membership.user.name %></p>
<p class="text-neutral-500 capitalize"><%= membership.role %></p>
</div>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
10 changes: 10 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,16 @@ en:
members: Members
no_history: No events yet
no_gems: No gems yet
members:
create:
failure: "Failed to add member: %{error}"
success: "Member added!"
user_not_found: "User not found"
destroy:
success: "User was removed from the organization"
update:
failure: "Failed to update member: %{error}"
success: "User was updated"
pages:
about:
contributors_amount: "%{count} Rubyists"
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@
end
resources :organizations, only: %i[index show edit update], constraints: { id: Patterns::ROUTE_PATTERN } do
resources :gems, only: :index, controller: 'organizations/gems'
resources :members, only: %i[index create update destroy], controller: 'organizations/members'
end
end

Expand Down
153 changes: 153 additions & 0 deletions test/integration/organizations/members_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
require "test_helper"

class Organizations::MembersTest < ActionDispatch::IntegrationTest
setup do
@user = create(:user, remember_token_expires_at: Gemcutter::REMEMBER_FOR.from_now)
post session_path(session: { who: @user.handle, password: PasswordHelpers::SECURE_TEST_PASSWORD })
end

test "index should render Not Found org" do
get "/organizations/notfound/members"

assert_response :not_found
end

test "index should render Forbidden" do
create(:organization, handle: "chaos")

get "/organizations/chaos/members"

assert_response :forbidden
end

test "should get index" do
create(:organization, owners: [@user], handle: "chaos")

get "/organizations/chaos/members"

assert_response :success
assert page.has_content?("Members")
end

test "create should return Not Found org" do
post "/organizations/notfound/members", params: { membership: { role: "owner" } }

assert_response :not_found
end

test "create should return Forbidden when trying to create your own membership" do
create(:organization, handle: "chaos")

post "/organizations/chaos/members", params: { membership: { username: @user.id, role: "maintainer" } }

assert_response :forbidden
end

test "create membership with bad role should not work" do
organization = create(:organization, owners: [@user], handle: "chaos")
bdfl = create(:user, handle: "bdfl")

post "/organizations/chaos/members", params: { membership: { username: bdfl.handle, role: "bdfl" } }

assert_redirected_to organization_members_path(organization)
follow_redirect!

assert page.has_content?("Failed to add member: Role is not included in the list")
assert_nil organization.unconfirmed_memberships.find_by(user_id: bdfl.id)
end

test "create membership by email should not work (yet)" do
organization = create(:organization, owners: [@user], handle: "chaos")
maintainer = create(:user, handle: "maintainer")

post "/organizations/chaos/members", params: { membership: { username: maintainer.email, role: "maintainer" } }

assert_redirected_to organization_members_path(organization)
follow_redirect!

assert page.has_content?("Failed to add member: User not found")
assert_nil organization.unconfirmed_memberships.find_by(user_id: maintainer.id)
end

test "should create a membership by handle" do
organization = create(:organization, owners: [@user], handle: "chaos")
maintainer = create(:user, handle: "maintainer")

post "/organizations/chaos/members", params: { membership: { username: maintainer.handle, role: "maintainer" } }

assert_redirected_to organization_members_path(organization)
membership = organization.unconfirmed_memberships.find_by(user_id: maintainer.id)

assert membership
assert_predicate membership, :maintainer?
refute_predicate membership, :confirmed?
end

test "update should return Not Found org" do
patch "/organizations/notfound/members/notfound", params: { membership: { role: "owner" } }

assert_response :not_found
end

test "update should return Not Found membership" do
create(:organization, owners: [@user], handle: "chaos")

patch "/organizations/chaos/members/notfound", params: { membership: { role: "owner" } }

assert_response :not_found
end

test "update should return Forbidden" do
organization = create(:organization, handle: "chaos")
membership = create(:membership, :maintainer, user: @user, organization: organization)

patch "/organizations/chaos/members/#{@user.handle}", params: { membership: { role: "owner" } }

assert_response :forbidden
end

test "should update" do
organization = create(:organization, owners: [@user], handle: "chaos")
maintainer = create(:user, handle: "maintainer")
membership = create(:membership, :maintainer, user: maintainer, organization: organization)

patch "/organizations/chaos/members/#{maintainer.handle}", params: { membership: { role: "owner" } }

assert_redirected_to organization_members_path(organization)
assert_predicate membership.reload, :owner?
end

test "destroy should return Not Found org" do
delete "/organizations/notfound/members/notfound"

assert_response :not_found
end

test "destroy should return Not Found membership" do
create(:organization, owners: [@user], handle: "chaos")

delete "/organizations/chaos/members/notfound"

assert_response :not_found
end

test "destroy should return Forbidden" do
organization = create(:organization, handle: "chaos")
membership = create(:membership, :maintainer, user: @user, organization: organization)

delete "/organizations/chaos/members/#{@user.handle}"

assert_response :forbidden
end

test "should destroy a membership" do
organization = create(:organization, handle: "chaos", owners: [@user])
maintainer = create(:user, handle: "maintainer")
membership = create(:membership, :maintainer, user: maintainer, organization: organization)

delete "/organizations/chaos/members/#{maintainer.handle}"

assert_redirected_to organization_members_path(organization)
assert_nil Membership.find_by(id: membership.id)
end
end
4 changes: 2 additions & 2 deletions test/views/card_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def render(...)
end
end

assert_selector "article"
assert_selector "section"
assert_selector "h3", text: "Gems"
assert_selector "svg.fill-orange"
assert_selector "span", text: "3"
Expand All @@ -36,7 +36,7 @@ def render(...)
end
end

assert_selector "article"
assert_selector "section"
assert_selector "h3", text: "History"
assert_text "content"
refute_text "View all"
Expand Down
Loading