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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow admins to combine and delete CCLAs #521

Merged
merged 6 commits into from Jul 3, 2014
Copy path View file
@@ -50,7 +50,7 @@ public/system
*.rdb
# Ignore the generated sitemap
public/sitemap.xml.gz
public/sitemap*
# Ignore local configuration
.env.development
@@ -24,6 +24,7 @@
//= require flash
//= require select2.min
//= require collaborators
//= require organizations
// Hack to resolve bug with Foundation. Resolved in master
// here: https://github.com/zurb/foundation/issues/4684 so
@@ -0,0 +1,28 @@
$(function () {
var settings = {
placeholder: 'Search for an organization',
minimumInputLength: 3,
width: '100%',
ajax: {
url: function () {
return $(this).data('url');
},
dataType: 'json',
quietMillis: 200,
data: function (term, page) {
return { q: term };
},
results: function (data, page) {
return {results: data};
}
},
formatSelection: function(obj, container) {
return obj.company;
},
formatResult: function(obj, container) {
return obj.company;
}
}
$('.transfer-ccla-organization').select2(settings);
});
@@ -32,6 +32,10 @@
}
}
}
.select2-container {
margin-bottom: rem-calc(20);
}
}
.cla_signers_list {
@@ -24,6 +24,41 @@ def manage_contributors?
organization_or_supermarket_admin?
end
# Supermarket admins can manage organizations
#
# @return [Boolean]
#
def manage_organization?
user.is?(:admin)
end
#
# Supermarket admins can see the page to manage organizations
#
# @return [Boolean]
#
def show?
manage_organization?
end
#
# Supermarket admins can delete organizations
#
# @return [Boolean]
#
def destroy?
manage_organization?
end
#
# Supermarket admins can combine organizations
#
# @return [Boolean]
#
def combine?
manage_organization?
end
private
def organization_or_supermarket_admin?
@@ -0,0 +1,65 @@
class OrganizationsController < ApplicationController
before_filter :authenticate_user!, except: [:index]
before_filter :find_organization, except: [:index]
skip_before_filter :verify_authenticity_token, only: [:index]
#
# GET /organizations
#
# Lists out all organizations.
#
def index
organizations = if params[:q]
CclaSignature.search(params[:q])
else
Organization.includes(:ccla_signatures)
end
respond_to do |format|
format.json do
render json: organizations.to_json(only: [:id], methods: [:company])
end
end
end
#
# GET /organizations/:id
#
# Shows the management page for an organization, allowing deletion and
# merging with other organizations.
#
def show
authorize! @organization
end
#
# DELETE /organizations/:id
#
# Deletes an organization
#
def destroy
authorize! @organization
@organization.destroy
redirect_to root_path
end
#
# PUT /organizations/:id/combine
#
# Combines two organizations together into one.
#
def combine
authorize! @organization
org_to_combine = Organization.find(params[:organization][:combine_with_id])
@organization.combine!(org_to_combine)
redirect_to @organization
end
private
def find_organization
@organization = Organization.find(params[:id])
end
end
@@ -0,0 +1,17 @@
module CclaSignaturesHelper
#
# Generates the HTML for a tab in the organization partial, with conditional
# logic to make it active if it's the current page.
#
# @param txt [String] the text for the link
# @param path [String] the path for the link
#
# @return [String] HTML representing a tab
#
def organization_tab(txt, path)
cls = current_page?(path) ? 'active' : nil
content_tag :dd, class: cls do
link_to txt, path
end
end
end
Copy path View file
@@ -1,4 +1,6 @@
class CclaSignature < ActiveRecord::Base
include PgSearch
# Associations
# --------------------
belongs_to :user
@@ -32,6 +34,17 @@ class CclaSignature < ActiveRecord::Base
# --------------------
before_create -> (record) { record.signed_at = Time.now }
# Search
# --------------------
pg_search_scope(
:search,
against: :company,
using: {
tsearch: { dictionary: 'english' },
trigram: { threshold: 0.05 }
}
)
def name
"#{first_name} #{last_name}"
end
Copy path View file
@@ -1,10 +1,12 @@
class Organization < ActiveRecord::Base
attr_accessor :combine_with_id
# Associations
# --------------------
has_many :contributors
has_many :contributors, dependent: :destroy
has_many :users, through: :contributors
has_many :invitations
has_many :ccla_signatures
has_many :invitations, dependent: :destroy
has_many :ccla_signatures, dependent: :destroy
#
# Returns all admin contributors.
@@ -23,6 +25,7 @@ def admins
def name
latest_ccla_signature.company
end
alias_method :company, :name
#
# Retrieve the latest CCLA signature if they have signed a CCLA.
@@ -32,4 +35,26 @@ def name
def latest_ccla_signature
ccla_signatures.order(:signed_at).last
end
#
# Combine two organizations together by copying CCLA signatures, invitations
# and contributors from one to the other. This also destroys the organzation
# that's passed in.
#
# @param organization [Organization] The organization that we want to combine
# with this one
#
def combine!(organization)
transaction do
[:ccla_signatures, :invitations].each do |assoc|
organization.send(assoc).update_all(organization_id: id)
end
organization.contributors.
where('user_id NOT IN (?)', contributors.pluck(:user_id)).
update_all(organization_id: id, admin: false)
organization.reload.destroy
end
end
end
@@ -1,11 +1,15 @@
<dl class="tabs">
<% if policy(ccla_signature.organization).view_cclas? %>
<dd class="<%= "active" if current_page?(ccla_signature_path(ccla_signature)) %>"><%= link_to 'View Signature', ccla_signature %></dd>
<%= organization_tab('View Signature', ccla_signature_path(ccla_signature)) %>
<% end %>
<dd class="<%= "active" if current_page?(contributors_ccla_signature_path(ccla_signature)) %>"><%= link_to "View Contributors", contributors_ccla_signature_path(ccla_signature) %></dd>
<%= organization_tab('View Contributors', contributors_ccla_signature_path(ccla_signature)) %>
<% if policy(ccla_signature.organization).manage_contributors? %>
<dd class="<%= "active" if current_page?(organization_invitations_path(ccla_signature.organization)) %>"><%= link_to 'Manage Contributors', organization_invitations_path(ccla_signature.organization) %></dd>
<%= organization_tab('Manage Contributors', organization_invitations_path(ccla_signature.organization)) %>
<% end %>
<% if policy(ccla_signature.organization).manage_organization? %>
<%= organization_tab('Manage Organization', organization_path(ccla_signature.organization)) %>
<% end %>
</dl>
@@ -84,6 +84,5 @@
</tbody>
</table>
</div>
</div>
</div>
@@ -0,0 +1,24 @@
<%= provide(:title, "CCLA Invitations for #{@organization.name}") %>
<div class="page">
<div class="cla">
<h1><%= @organization.name %> Organization Management</h1>
<%= render "ccla_signatures/organization_tabs", ccla_signature: @organization.latest_ccla_signature %>
<p>As an admin you may combine an organization with another in the event that a duplicate organization was created in error. Note that all contributors will be transferred to the selected organization and this organization will be deleted. Alternatively you can simply delete this organization.</p>
<%= form_for @organization, url: { action: 'combine' }, method: :put do |f| %>
<div class="row collapse">
<div class="small-10 columns">
<%= f.hidden_field :combine_with_id, class: 'transfer-ccla-organization', 'data-url' => organizations_path %>
</div>
<div class="small-2 columns">
<%= f.submit 'Combine', class: 'button postfix radius' %>
</div>
</div>
<% end %>
<%= link_to 'Delete this Organization', organization_path(@organization), method: :delete, data: { confirm: 'This will permanently delete this organization and all of its signatures. Are you sure?' }, class: 'button alert radius small' %>
</div>
</div>
Copy path View file
@@ -86,7 +86,11 @@
end
end
resources :organizations, only: [] do
resources :organizations, only: [:index, :show, :destroy] do
member do
put :combine

This comment has been minimized.

@bcobb

bcobb Jul 1, 2014

Contributor

If an admin issues two identical requests to combine, the second will fail. As such, I think this should be a post instead of a put.

Ack! I fell into the idempotency trap! Disregard, please.

This comment has been minimized.

@brettchalupa
end
resources :contributors, only: [:update, :destroy], controller: :contributors
resources :invitations, only: [:index, :create, :update],
@@ -0,0 +1,5 @@
class MakeContributorsUnique < ActiveRecord::Migration
def change
add_index :contributors, [:user_id, :organization_id], unique: true
end
end
Copy path View file
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20140630175717) do
ActiveRecord::Schema.define(version: 20140702171009) do
create_table "accounts", force: true do |t|
t.integer "user_id"
@@ -82,6 +82,7 @@
end
add_index "contributors", ["organization_id"], name: "index_contributors_on_organization_id", using: :btree
add_index "contributors", ["user_id", "organization_id"], name: "index_contributors_on_user_id_and_organization_id", unique: true, using: :btree
add_index "contributors", ["user_id"], name: "index_contributors_on_user_id", using: :btree
create_table "cookbook_collaborators", force: true do |t|
@@ -11,6 +11,10 @@
it { should permit_authorization(:view_cclas) }
it { should permit_authorization(:resign_ccla) }
it { should permit_authorization(:manage_contributors) }
it { should permit_authorization(:manage_organization) }
it { should permit_authorization(:show) }
it { should permit_authorization(:destroy) }
it { should permit_authorization(:combine) }
end
context 'as an organization admin' do
@@ -40,4 +44,15 @@
it { should_not permit_authorization(:resign_ccla) }
it { should_not permit_authorization(:manage_contributors) }
end
context 'as a non-admin' do
let(:user) { build(:user) }
subject { described_class.new(user, record) }
it { should_not permit_authorization(:manage_organization) }
it { should_not permit_authorization(:show) }
it { should_not permit_authorization(:destroy) }
it { should_not permit_authorization(:combine) }
end
end
@@ -68,4 +68,13 @@
end
end
end
describe '.search' do
let!(:ihop) { create(:ccla_signature, company: 'International House of Pancakes') }
let!(:bhop) { create(:ccla_signature, company: "Bob's House of Pancakes") }
it 'returns ccla signatures with a similar company' do
expect(CclaSignature.search('pancakes')).to include(ihop, bhop)
end
end
end
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.