Skip to content
1 change: 1 addition & 0 deletions app/avo/resources/gem_name_reservation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Avo::Resources::GemNameReservation < Avo::BaseResource
def fields
field :id, as: :id
field :name, as: :text
field :organization, as: :belongs_to
field :audits, as: :has_many
end
end
1 change: 1 addition & 0 deletions app/avo/resources/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def fields
field :unconfirmed_memberships, as: :has_many
field :users, as: :has_many
field :rubygems, as: :has_many
field :gem_name_reservations, as: :has_many
field :organization_onboarding, as: :belongs_to
end
end
Expand Down
52 changes: 52 additions & 0 deletions app/controllers/organizations/gem_name_reservations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

class Organizations::GemNameReservationsController < Organizations::BaseController
PER_PAGE = 50

before_action :set_page, only: :index

rescue_from Pundit::NotAuthorizedError, with: :render_not_found

def index
authorize @organization, :list_gem_name_reservations?

reservations = @organization.gem_name_reservations.order(:name)
@gem_name_reservations_count = reservations.count
@gem_name_reservations = reservations.page(@page).per(PER_PAGE)
end

def new
@gem_name_reservation = @organization.gem_name_reservations.build
authorize @gem_name_reservation, :new?
end

def create
@gem_name_reservation = @organization.gem_name_reservations.build(gem_name_reservation_params)
authorize @gem_name_reservation, :create?

if @gem_name_reservation.save
redirect_to organization_gem_name_reservations_path(@organization), notice: t(".gem_name_reserved")
else
render :new, status: :unprocessable_content
end
end

def destroy
@gem_name_reservation = @organization.gem_name_reservations.find(params.expect(:id))
authorize @gem_name_reservation, :destroy?

@gem_name_reservation.destroy!

redirect_to organization_gem_name_reservations_path(@organization), notice: t(".gem_name_reservation_removed")
end

private

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

def gem_name_reservation_params
params.expect(gem_name_reservation: [:name])
end
end
1 change: 1 addition & 0 deletions app/models/feature_flag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

class FeatureFlag
ORGANIZATIONS = :organizations
UNLIMITED_GEM_NAME_RESERVATIONS = :unlimited_gem_name_reservations

class << self
def enabled?(flag_name, actor = nil)
Expand Down
26 changes: 26 additions & 0 deletions app/models/gem_name_reservation.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,43 @@
# frozen_string_literal: true

class GemNameReservation < ApplicationRecord
ORGANIZATION_LIMIT = 25

validates :name, uniqueness: { case_sensitive: false }, presence: true, length: { maximum: Gemcutter::MAX_FIELD_LENGTH }
validate :downcase_name_check
validate :rubygem_name_available, if: :needs_name_validation?
validate :organization_within_limit, if: :needs_limit_validation?

has_many :audits, as: :auditable, inverse_of: :auditable, dependent: :nullify

belongs_to :organization, optional: true

def self.reserved?(name)
where(name: name.downcase).any?
end

private

def needs_name_validation?
new_record? || name_changed?
end

def needs_limit_validation?
organization.present? && (new_record? || organization_id_changed?)
end

def organization_within_limit
return if organization.gem_name_reservations_unlimited?
return if organization.gem_name_reservations.count < ORGANIZATION_LIMIT

errors.add(:base, :organization_limit_reached, count: ORGANIZATION_LIMIT)
end

def rubygem_name_available
return unless Rubygem.where("lower(name) = ?", name&.downcase).any?
errors.add(:name, "rubygem exists with name")
end

def downcase_name_check
return unless name.to_s != name.to_s.downcase
errors.add(:name, "must be all lowercase")
Expand Down
28 changes: 26 additions & 2 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ class Organization < ApplicationRecord
validates :handle, presence: true,
uniqueness: { case_sensitive: false },
length: { within: 2..40 },
format: { with: Patterns::HANDLE_PATTERN }
format: { with: Patterns::ORGANIZATION_HANDLE_PATTERN }
validates :name, presence: true, length: { within: 2..255 }
validate :handle_not_reserved
validate :unique_with_user_handle, if: :handle_changed?

has_many :memberships, -> { where.not(confirmed_at: nil) }, dependent: :destroy, inverse_of: :organization
has_many :unconfirmed_memberships, -> { where(confirmed_at: nil) }, class_name: "Membership", dependent: :destroy, inverse_of: :organization
has_many :memberships_including_unconfirmed, class_name: "Membership", dependent: :destroy, inverse_of: :organization
has_many :users, through: :memberships
has_many :rubygems, dependent: :nullify
has_many :gem_name_reservations, dependent: :destroy
has_many :audits, as: :auditable, dependent: :nullify
has_one :organization_onboarding, foreign_key: :onboarded_organization_id, inverse_of: :organization, dependent: :destroy

Expand Down Expand Up @@ -52,13 +54,35 @@ def flipper_id
"org:#{handle}"
end

def gem_name_reservations_unlimited?
FeatureFlag.enabled?(FeatureFlag::UNLIMITED_GEM_NAME_RESERVATIONS, self)
end

def gem_name_reservation_limit
GemNameReservation::ORGANIZATION_LIMIT unless gem_name_reservations_unlimited?
end

def gem_name_reservations_remaining
return if gem_name_reservations_unlimited?

[GemNameReservation::ORGANIZATION_LIMIT - gem_name_reservations.count, 0].max
end

private

def handle_not_reserved
return if handle.blank?

return unless Handle.reserved?(handle)
return unless Organization::Handle.reserved?(handle)

errors.add(:handle, "is reserved and cannot be used")
end

# Prevent creating a organization if there's a User with the same handle
# TODO BRIAN: Is this a worth-while requirement?
def unique_with_user_handle
return if handle.blank?

errors.add(:handle, "has already been taken") if User.where("lower(handle) = lower(?)", handle).any?
end
end
Loading