Skip to content
This repository has been archived by the owner on Mar 12, 2020. It is now read-only.

Allow a GitHub Organization to be used by multiple Classrooms #1248

Merged
merged 27 commits into from Oct 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cb8f599
Drop uniqueness validation on `Organization` for `github_id`
tarebyte Jan 31, 2018
ec1e688
Update Sluggable to find alternative slugs
tarebyte Jan 31, 2018
02c8fd6
Updat organization model to use new slug logic
tarebyte Jan 31, 2018
fd8ab6a
Update controller and views to allow selection of used orgs
tarebyte Jan 31, 2018
e888391
Merge branch 'master' into let-an-org-be-multiple-classrooms
tarebyte Jan 31, 2018
70808ed
Merge branch 'master' into let-an-org-be-multiple-classrooms
srinjoym Jul 16, 2018
5751b79
added uniqueness check for classroom name
srinjoym Jul 23, 2018
2e3039e
working on not removing webhook on org unless classroom
srinjoym Jul 24, 2018
6a05c92
move uniqueness validation to title scoped to organization instead of…
srinjoym Aug 22, 2018
4da0ea1
test multiple classroom cases and webhook from github case
srinjoym Aug 22, 2018
6519934
all the linter fixes
srinjoym Aug 22, 2018
5291038
Merge branch 'master' into let-an-org-be-multiple-classrooms
srinjoym Aug 22, 2018
71b96a5
remove webook id validation
srinjoym Aug 22, 2018
6045412
please the hound and refactoring fixes
srinjoym Aug 22, 2018
69a6839
please the hound again
srinjoym Aug 22, 2018
a2d1e51
bug fix for adding user to all classrooms they're not part of and rem…
srinjoym Aug 22, 2018
d4bc33a
please please the hound:
srinjoym Aug 22, 2018
24f69ff
Merge branch 'master' into let-an-org-be-multiple-classrooms
BenEmdon Aug 22, 2018
e3ca367
Merge branch 'master' into let-an-org-be-multiple-classrooms
BenEmdon Aug 22, 2018
5425052
cleanup code
srinjoym Oct 5, 2018
6498d67
Merge branch 'master' into let-an-org-be-multiple-classrooms
srinjoym Oct 5, 2018
b565a06
fix typo in classroom->user_org
srinjoym Oct 5, 2018
848ddc1
update messages when deleting classroom
srinjoym Oct 5, 2018
08f9c8f
Merge branch 'master' into let-an-org-be-multiple-classrooms
srinjoym Oct 8, 2018
6ad530b
add feature flag support
srinjoym Oct 27, 2018
0710f7f
please the hound
srinjoym Oct 27, 2018
77ffe32
Merge branch 'master' into let-an-org-be-multiple-classrooms
srinjoym Oct 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -9,6 +9,11 @@ def ensure_student_identifier_flipper_is_enabled
not_found unless student_identifier_enabled?
end

def multiple_classrooms_per_org_enabled?
logged_in? && current_user.feature_enabled?(:multiple_classrooms_per_org)
end
helper_method :multiple_classrooms_per_org_enabled?

def public_assistant_landing_page_enabled?
GitHubClassroom.flipper[:public_assistant_landing_page].enabled?
end
Expand Down
28 changes: 22 additions & 6 deletions app/controllers/organizations_controller.rb
Expand Up @@ -23,6 +23,8 @@ def new

# rubocop:disable MethodLength
def create
return unless validate_multiple_classrooms_on_org

result = Organization::Creator.perform(
github_id: new_organization_params[:github_id],
users: new_organization_params[:users]
Expand Down Expand Up @@ -120,10 +122,12 @@ def new_organization_params
params.require(:organization).permit(:github_id).merge(users: [current_user])
end

# rubocop:disable AbcSize
# rubocop:disable Metrics/AbcSize
def set_users_github_organizations
@users_github_organizations = current_user.github_user.organization_memberships.map do |membership|
{
# TODO: Remove `classroom` field after we turn off the feature flag
# for multiple classrooms in one org
classroom: Organization.unscoped.find_by(github_id: membership.organization.id),
github_id: membership.organization.id,
login: membership.organization.login,
Expand All @@ -132,15 +136,17 @@ def set_users_github_organizations
}
end
end
# rubocop:enable AbcSize
# rubocop:enable Metrics/AbcSize

# Check if the current user has any organizations with admin privilege,
# if so add the user to the corresponding classroom automatically.
def add_current_user_to_organizations
@users_github_organizations.each do |organization|
classroom = organization[:classroom]
if classroom.present? && !classroom.users.include?(current_user)
create_user_organization_access(classroom)
@users_github_organizations.each do |github_org|
user_classrooms = Organization.where(github_id: github_org[:github_id])

# Iterate over each classroom associate with this github organization
user_classrooms.map do |classroom|
create_user_organization_access(classroom) unless classroom.users.include?(current_user)
end
end
end
Expand Down Expand Up @@ -173,5 +179,15 @@ def transfer_assignments
assignment.update_attributes(creator_id: new_owner.id)
end
end

def validate_multiple_classrooms_on_org
classroom_exists_on_org = Organization.unscoped.find_by(github_id: new_organization_params[:github_id])
if classroom_exists_on_org && !multiple_classrooms_per_org_enabled?
flash[:error] = "Validation failed: GitHub ID has already been taken"
redirect_to new_organization_path
return false
end
true
end
end
# rubocop:enable Metrics/ClassLength
12 changes: 8 additions & 4 deletions app/models/concerns/sluggable.rb
Expand Up @@ -4,16 +4,20 @@ module Sluggable
extend ActiveSupport::Concern

included do
before_validation do
slugify
end
before_validation :slugify
end

def slugify
self.slug = title.parameterize
self.slug = name_for_slug.parameterize
end

def to_param
slug
end

private

def name_for_slug
title
end
end
8 changes: 8 additions & 0 deletions app/models/github_organization.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true

# rubocop:disable Metrics/ClassLength
class GitHubOrganization < GitHubResource
def accept_membership(user_github_login)
return if organization_member?(user_github_login)
Expand Down Expand Up @@ -116,6 +117,12 @@ def create_organization_webhook(config: {}, options: {})
end
end

def organization_webhooks
GitHub::Errors.with_error_handling do
@client.org_hooks(@id)
end
end

def remove_organization_webhook(webhook_id)
return if webhook_id.blank?
GitHub::Errors.with_error_handling do
Expand Down Expand Up @@ -152,3 +159,4 @@ def webhook_secret
Rails.application.secrets.webhook_secret
end
end
# rubocop:enable Metrics/ClassLength
16 changes: 11 additions & 5 deletions app/models/organization.rb
Expand Up @@ -17,15 +17,14 @@ class Organization < ApplicationRecord

has_and_belongs_to_many :users

validates :github_id, presence: true, uniqueness: true
validates :github_id, presence: true

validates :title, presence: true
validates :title, length: { maximum: 60 }
validates :title, uniqueness: { scope: :github_id }

validates :slug, uniqueness: true

validates :webhook_id, uniqueness: true, allow_nil: true

before_destroy :silently_remove_organization_webhook

def all_assignments(with_invitations: false)
Expand All @@ -48,15 +47,22 @@ def github_organization
@github_organization ||= GitHubOrganization.new(github_client, github_id)
end

def slugify
self.slug = "#{github_id} #{title}".parameterize
def name_for_slug
"#{github_id} #{title}"
end

def one_owner_remains?
users.count == 1
end

# Check if we are the last Classroom on this GitHub Organization
def last_classroom_on_org?
Organization.where(github_id: github_id).length <= 1
srinjoym marked this conversation as resolved.
Show resolved Hide resolved
end

def silently_remove_organization_webhook
return true unless last_classroom_on_org?
srinjoym marked this conversation as resolved.
Show resolved Hide resolved

begin
github_organization.remove_organization_webhook(webhook_id)
rescue GitHub::Error => err
Expand Down
80 changes: 76 additions & 4 deletions app/models/organization/creator.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true

# rubocop:disable Metrics/ClassLength
class Organization
class Creator
include Rails.application.routes.url_helpers
Expand Down Expand Up @@ -96,19 +97,29 @@ def perform
# is a user who has the correct token scope.
#
# Returns an Integer id, or raises a Result::Error
# rubocop:disable MethodLength
# rubocop:disable AbcSize
def create_organization_webhook!
return unless (user = user_with_admin_org_hook_scope)

begin
return existing_webhook_id unless existing_webhook_id.nil?

github_organization = GitHubOrganization.new(user.github_client, github_id)
webhook = github_organization.create_organization_webhook(config: { url: webhook_url })

return webhook.id if webhook.try(:id).present?
raise GitHub::Error
rescue GitHub::Error
raise GitHub::Error if webhook.try(:id).nil?

webhook.id
rescue GitHub::Error => e
github_webhook_id = process_webhook_error(e.message, github_organization)
return github_webhook_id unless github_webhook_id.nil?

raise Result::Error, "Could not create WebHook, please try again."
end
end
# rubocop:enable AbcSize
# rubocop:enable MethodLength

# Internal: Make sure every user being added to the
# Organization is an admin on GitHub.
Expand Down Expand Up @@ -167,7 +178,10 @@ def title

github_client = users.sample.github_client
github_org = GitHubOrganization.new(github_client, github_id)
github_org.name.present? ? github_org.name : github_org.login

org_identifier = github_org.name.presence || github_org.login

find_unique_title(org_identifier)
end

# Internal: Find User that has the `admin:org_hook` scope
Expand Down Expand Up @@ -222,5 +236,63 @@ def webhook_url

raise Result::Error, error_message
end

# Internal: Check error from GitHub when we try to create the webhook
# If the hook already exists get the webhook id from GitHub and return
#
# Returns a String for the webhook_id or nil
def process_webhook_error(message, github_organization)
return nil unless message == "Hook: Hook already exists on this organization"

get_organization_webhook_id(github_organization)
end

# Internal: Get id of webhook already on GitHub
#
# Returns a String for the webhook_id or nil
def get_organization_webhook_id(github_organization)
webhooks = github_organization.organization_webhooks

# There should only be one webhook that Classroom creates in production
webhooks.first.id
rescue GitHub::Error
nil
end

# Internal: Find webhook_id of any existing classrooms with
# same github id
#
# Returns a String for the webhook_id or nil
def existing_webhook_id
classroom_with_same_org = Organization.find_by(github_id: github_id)
classroom_with_same_org ? classroom_with_same_org.webhook_id : nil
end

# Internal: Generates unique name for classroom if default
# name has already been taken
#
# Returns a String with an unique title
def find_unique_title(identifier)
# First Classroom on Org will have postfix 1
base_title = "#{identifier}-classroom-1"

count = 1
until unique_title?(base_title)
# Increments count at end of title
base_title = base_title.gsub(/\-\d+$/, "") + "-#{count}"
count += 1
end

base_title
end

# Internal: Checks if a classroom with the same title and github_id
# exists already
#
# Returns a Boolean on whether duplicate classroom title is
def unique_title?(base_title)
Organization.where(title: base_title, github_id: github_id).blank?
end
end
end
# rubocop:enable Metrics/ClassLength
Expand Up @@ -8,11 +8,15 @@

<div class="remodal-description">
<p>
<%= t('views.organizations.delete_consequences_html', organization: sanitize(current_organization.github_organization.login)) %>
<% if multiple_classrooms_per_org_enabled? %>
<%= t('views.organizations.delete_classroom_consequences_html', organization: sanitize(current_organization.github_organization.login)) %>
<% else %>
<%= t('views.organizations.delete_org_consequences_html', organization: sanitize(current_organization.github_organization.login)) %>
<% end %>
</p>
</div>

<%= form_for current_organization, html: { "data-name" => current_organization.github_organization.login, method: 'delete' } do |f| %>
<%= form_for current_organization, html: { "data-name" => current_organization.title, method: 'delete' } do |f| %>
<dl class="form js-normalize-submit">
<dt><%= t('views.organizations.delete_confirm') %></dt>
<dd><input type="text" class="js-input-block form-control input-contrast input-block" autofocus></dd>
Expand Down
2 changes: 1 addition & 1 deletion app/views/organizations/new.html.erb
Expand Up @@ -16,7 +16,7 @@

<div class="org-select-grid">
<% @users_github_organizations.each do |org| %>
<% if org[:role] == 'admin' && !org[:classroom].present? %>
<% if org[:role] == 'admin' && (!org[:classroom].present? || multiple_classrooms_per_org_enabled?) %>
<%= render partial: 'organization_select', locals: { org: org } %>
<% else %>
<%= render partial: 'disabled_organization_select', locals: { org: org } %>
Expand Down
5 changes: 3 additions & 2 deletions config/locales/views/organizations/en.yml
Expand Up @@ -30,8 +30,9 @@ en:
group_assignments: 'Group Assignments'
are_you_sure: 'Are you ABSOLUTELY sure?'
delete_warning: "Unexpected things will happen if you don't read this!"
delete_consequences_html: "This action <strong>CANNOT</strong> be undone. Please note that reseting and removing this classroom will delete all repositories and teams under the <strong>%{organization}</strong> account that were created by GitHub Classroom."
delete_confirm: 'Please type the name of the organization to confirm'
delete_org_consequences_html: "This action <strong>CANNOT</strong> be undone. Please note that reseting and removing this classroom will delete all repositories and teams under the <strong>%{organization}</strong> account that were created by GitHub Classroom."
delete_classroom_consequences_html: "This action <strong>CANNOT</strong> be undone. Please note that reseting and removing this classroom will delete all repositories and teams created by this classroom in the <strong>%{organization}</strong> account."
delete_confirm: 'Please type the name of the classroom to confirm'
delete_submit: 'Reset and remove this classroom'
classroom_settings: 'Classroom settings'
classroom_profile: 'Classroom profile'
Expand Down
37 changes: 30 additions & 7 deletions spec/controllers/organizations_controller_spec.rb
Expand Up @@ -105,6 +105,36 @@
organization.destroy!
end

context "multiple_classrooms_per_org flag not enabled" do
before do
GitHubClassroom.flipper[:multiple_classrooms_per_org].disable
end

it "will not add an organization that already exists" do
existing_organization_options = { github_id: organization.github_id }
expect do
post :create, params: { organization: existing_organization_options }
end.to_not change(Organization, :count)
end
end

context "multiple_classrooms_per_org flag is enabled" do
before do
GitHubClassroom.flipper[:multiple_classrooms_per_org].enable
end

after do
GitHubClassroom.flipper[:multiple_classrooms_per_org].disable
end

it "will add a classroom on same organization" do
existing_organization_options = { github_id: organization.github_id }
expect do
post :create, params: { organization: existing_organization_options }
end.to change(Organization, :count)
end
end

it "will fail to add an organization the user is not an admin of" do
new_organization = build(:organization, github_id: 90)
new_organization_options = { github_id: new_organization.github_id }
Expand All @@ -114,13 +144,6 @@
end.to_not change(Organization, :count)
end

it "will not add an organization that already exists" do
existing_organization_options = { github_id: organization.github_id }
expect do
post :create, params: { organization: existing_organization_options }
end.to_not change(Organization, :count)
end

it "will add an organization that the user is admin of on GitHub" do
organization_params = { github_id: organization.github_id, users: organization.users }
organization.destroy!
Expand Down