Replace nonprofit creation with houdini:nonprofit:create Rails command

This commit is contained in:
Eric Schultz 2021-02-08 10:55:22 -06:00 committed by Eric Schultz
parent 801cb8750a
commit e26581dba5
21 changed files with 310 additions and 108 deletions

View file

@ -371,12 +371,10 @@ app/javascript/legacy/supporters/info-card.es6
app/javascript/stories/index.jsx app/javascript/stories/index.jsx
# tsx files # tsx files
app/javascript/components/RegistrationPage.tsx
app/javascript/legacy_react/app/create_new_offsite_payment_pane.tsx app/javascript/legacy_react/app/create_new_offsite_payment_pane.tsx
app/javascript/legacy_react/app/edit_payment_pane.tsx app/javascript/legacy_react/app/edit_payment_pane.tsx
app/javascript/legacy_react/javascripts/app/create_new_offsite_payment_pane.tsx app/javascript/legacy_react/javascripts/app/create_new_offsite_payment_pane.tsx
app/javascript/legacy_react/javascripts/app/edit_payment_pane.tsx app/javascript/legacy_react/javascripts/app/edit_payment_pane.tsx
app/javascript/legacy_react/javascripts/app/registration_page.tsx
app/javascript/legacy_react/src/components/common/BootstrapWrapper.tsx app/javascript/legacy_react/src/components/common/BootstrapWrapper.tsx
app/javascript/legacy_react/src/components/common/DefaultCloseButton.tsx app/javascript/legacy_react/src/components/common/DefaultCloseButton.tsx
app/javascript/legacy_react/src/components/common/fields.tsx app/javascript/legacy_react/src/components/common/fields.tsx
@ -423,7 +421,6 @@ app/javascript/legacy_react/src/components/registration_page/NonprofitInfoForm.s
app/javascript/legacy_react/src/components/registration_page/NonprofitInfoForm.tsx app/javascript/legacy_react/src/components/registration_page/NonprofitInfoForm.tsx
app/javascript/legacy_react/src/components/registration_page/NonprofitInfoPanel.spec.tsx app/javascript/legacy_react/src/components/registration_page/NonprofitInfoPanel.spec.tsx
app/javascript/legacy_react/src/components/registration_page/NonprofitInfoPanel.tsx app/javascript/legacy_react/src/components/registration_page/NonprofitInfoPanel.tsx
app/javascript/legacy_react/src/components/registration_page/RegistrationPage.tsx
app/javascript/legacy_react/src/components/registration_page/RegistrationWizard.tsx app/javascript/legacy_react/src/components/registration_page/RegistrationWizard.tsx
app/javascript/legacy_react/src/components/registration_page/UserInfoForm.tsx app/javascript/legacy_react/src/components/registration_page/UserInfoForm.tsx
app/javascript/legacy_react/src/components/registration_page/UserInfoPanel.spec.tsx app/javascript/legacy_react/src/components/registration_page/UserInfoPanel.spec.tsx

View file

@ -181,11 +181,43 @@ Coverage report generated for RSpec to .../houdini/coverage. 10552 / 12716 LOC (
The important thing to look for is that the number of The important thing to look for is that the number of
failures is zero. failures is zero.
##### Creating your first nonprofits and user
To create a nonprofit, use the command line to run the following command and fill in the questions with the required information:
```bash
bin/rails houdini:nonprofit:create
```
There are available arguments that add congirugrations on the nonprofit's creation:
```bash
-su, [--super-admin], [--no-super-admin] # Make the nonprofit admin a super user (they can access any nonprofit's dashboards)
[--confirm-admin], [--no-confirm-admin] # Require the nonprofit admin to be confirmed via email
# Default: true
```
Additionally, it is possible to provide arguments to fill in the fields for the nonprofit creation without coming across the questions:
```bash
[--nonprofit-name=NONPROFIT_NAME] # Provide the nonprofit's name
[--state-code=STATE_CODE] # Provide the nonprofit' state code
[--city=CITY] # Provide the nonprofit's city
[--nonprofit-website=NONPROFIT_WEBSITE] # Provide the nonprofit public website
[--nonprofit-email=NONPROFIT_EMAIL] # Provide the nonprofit public email
[--user-name=USER_NAME] # Provide the nonprofit's admin's name
[--user-email=USER_EMAIL] # Provide the nonprofit's admin's email address (It'll be used for logging in)
[--user-phone=USER_PHONE] # [OPTIONAL] Provide the nonprofit's 's phone
[--user-password=USER_PASSWORD] # Provide the nonprofit's admin's password
```
You can use this in the future for creating additional nonprofits.
#### Startup #### Startup
`bin/rails server` `bin/rails server`
You can connect to your server at http://localhost:5000 You can connect to your server at http://localhost:5000
##### Super admin ##### Super admin
There is a way to set your user as a super_admin. This role lets you access any of the nonprofits There is a way to set your user as a super_admin. This role lets you access any of the nonprofits

View file

@ -4,9 +4,7 @@
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE # Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
class FrontController < ApplicationController class FrontController < ApplicationController
def index def index
if Nonprofit.none? if current_role?(%i[nonprofit_admin nonprofit_associate])
redirect_to onboard_path
elsif current_role?(%i[nonprofit_admin nonprofit_associate])
redirect_to NonprofitPath.dashboard(administered_nonprofit) redirect_to NonprofitPath.dashboard(administered_nonprofit)
elsif current_user elsif current_user
redirect_to '/profiles/' + current_user.profile.id.to_s redirect_to '/profiles/' + current_user.profile.id.to_s

View file

@ -1,9 +0,0 @@
# frozen_string_literal: true
class OnboardController < ApplicationController
layout 'layouts/apified'
def index
@theme = 'minimal'
end
end

View file

@ -1,4 +0,0 @@
# frozen_string_literal: true
module OnboardHelper
end

View file

@ -1,11 +0,0 @@
import React from "react"
require('bootstrap-loader');
import Root from "../legacy_react/src/components/common/Root"
import RegPage from "../legacy_react/src/components/registration_page/RegistrationPage"
function RegistrationPage(props:{}){
return (<Root><RegPage/></Root>)
}
export default RegistrationPage

View file

@ -1,16 +0,0 @@
// License: LGPL-3.0-or-later
// require a root component here. This will be treated as the root of a webpack package
import Root from "../../src/components/common/Root"
import RegistrationPage from "../../src/components/registration_page/RegistrationPage"
import * as ReactDOM from 'react-dom'
import * as React from 'react'
function LoadReactPage(element:HTMLElement) {
ReactDOM.render(<Root><RegistrationPage/></Root>, element)
}
(window as any).LoadReactPage = LoadReactPage

View file

@ -1,25 +0,0 @@
// License: LGPL-3.0-or-later
import * as React from 'react';
import RegistrationWizard from "./RegistrationWizard";
import {observer} from 'mobx-react';
import { injectIntl, FormattedMessage, WrappedComponentProps} from 'react-intl';
export interface RegistrationPageProps
{
}
class RegistrationPage extends React.Component<RegistrationPageProps & WrappedComponentProps, {}> {
render() {
return <div className="tw-bs"><div className="container"><h1><FormattedMessage id="registration.get_started.header"/></h1><p><FormattedMessage id="registration.get_started.description"/></p><RegistrationWizard/></div></div>
}
}
export default injectIntl(observer(RegistrationPage))

View file

@ -33,8 +33,9 @@ class Base
end end
def add_nested_errors_for(record, attribute, other_validator) def add_nested_errors_for(record, attribute, other_validator)
byebug
record.errors.messages[attribute] = other_validator.errors.messages record.errors.messages[attribute] = other_validator.errors.messages
record.errors.details[attribute] = other_validator.errors.details #record.errors.details[attribute] = other_validator.errors.details
end end
end end
end end

View file

@ -1,9 +1,25 @@
class CustomAssociatedValidator < ActiveModel::EachValidator #:nodoc:
def validate_each(record, attribute, value)
byebug
if Array(value).reject { |r| valid_object?(r) }.any?
record.errors.add(attribute, :invalid, **options.merge(value: value))
end
end
private
def valid_object?(record)
record.valid?
end
end
class CreateModel < Base class CreateModel < Base
attr_accessor :nonprofit, :user attr_accessor :nonprofit, :user
validates_presence_of :user validates_presence_of :user
validates_presence_of :nonprofit validates_presence_of :nonprofit
validate_nested_attribute :user, model_class: User #validate_nested_attribute :user, model_class: User
validate_nested_attribute :nonprofit, model_class: Nonprofit # validate_nested_attribute :nonprofit, model_class: Nonprofit
validates_with CustomAssociatedValidator, attributes: [:nonprofit]
before_validation do before_validation do
nonprofit = Nonprofit.create(nonprofit) if !nonprofit.is_a? Nonprofit nonprofit = Nonprofit.create(nonprofit) if !nonprofit.is_a? Nonprofit
@ -45,4 +61,6 @@ class CreateModel < Base
def save! def save!
raise 'runtime' unless save raise 'runtime' unless save
end end
end end

View file

@ -75,6 +75,11 @@ class Nonprofit < ApplicationRecord
has_many :tickets, through: :events has_many :tickets, through: :events
has_many :roles, as: :host, dependent: :destroy has_many :roles, as: :host, dependent: :destroy
has_many :users, through: :roles has_many :users, through: :roles
has_many :admins, -> { where('name = ?', :nonprofit_admin) }, through: :roles, class_name: 'User', autosave: true, source: :user do
def build_admin(**kwargs)
build(kwargs.merge({name: :nonprofit_admin}))
end
end
has_many :tag_masters, dependent: :destroy has_many :tag_masters, dependent: :destroy
has_many :custom_field_masters, dependent: :destroy has_many :custom_field_masters, dependent: :destroy
@ -89,6 +94,7 @@ class Nonprofit < ApplicationRecord
has_one :billing_plan, through: :billing_subscription has_one :billing_plan, through: :billing_subscription
has_one :miscellaneous_np_info has_one :miscellaneous_np_info
validates_associated :admins, on: :create
validates :name, presence: true validates :name, presence: true
validates :city, presence: true validates :city, presence: true
validates :state_code, presence: true validates :state_code, presence: true

View file

@ -4,12 +4,12 @@
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE # Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
class Role < ApplicationRecord class Role < ApplicationRecord
Names = [ Names = [
:super_admin, # global access 'super_admin', # global access
:super_associate, # global access to everything except bank acct info 'super_associate', # global access to everything except bank acct info
:nonprofit_admin, # npo scoped access to everything 'nonprofit_admin', # npo scoped access to everything
:nonprofit_associate, # npo scoped access to everything except bank acct info 'nonprofit_associate', # npo scoped access to everything except bank acct info
:campaign_editor, # fundraising tools, without dashboard access 'campaign_editor', # fundraising tools, without dashboard access
:event_editor # // 'event_editor' # //
].freeze ].freeze
# :name, # :name,
@ -30,17 +30,13 @@ class Role < ApplicationRecord
validates :name, inclusion: { in: Names } validates :name, inclusion: { in: Names }
validates :host, presence: true, unless: %i[super_admin? super_associate?] validates :host, presence: true, unless: %i[super_admin? super_associate?]
def name
super.to_sym
end
def super_admin? def super_admin?
name == :super_admin name == 'super_admin'
end end
def super_associate? def super_associate?
name == :super_associate name == 'super_associate'
end end
def self.create_for_nonprofit(role_name, email, nonprofit) def self.create_for_nonprofit(role_name, email, nonprofit)
user = User.find_or_create_with_email(email) user = User.find_or_create_with_email(email)

View file

@ -1,7 +0,0 @@
<% content_for :title, t("registration.get_started.header") %>
<% content_for :javascripts do %>
<%= javascript_pack_tag 'loading_indicator', 'i18n', 'application' %>
<% end %>
<%= react_component('RegistrationPage', {}) %>

View file

@ -1,7 +1,9 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# frozen_string_literal: true # frozen_string_literal: true
$LOAD_PATH.unshift(File.expand_path('../commands', __dir__))
APP_PATH = File.expand_path('../config/application', __dir__) APP_PATH = File.expand_path('../config/application', __dir__)
require_relative "../config/boot" require_relative "../config/boot"
require_relative '../lib/generators/overrides' require_relative '../lib/generators/overrides'
require "rails/commands" require "rails/commands"

View file

@ -0,0 +1,69 @@
# frozen_string_literal: true
#
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
#
# NOTE: if this is moved to bess, it should be in the `houdini/lib/` subdirectory of bess.
#
module Houdini # rubocop:disable Style/ClassAndModuleChildren -- can't combine because Houdini hasn't been defined before
class Nonprofit # rubocop:disable Style/ClassAndModuleChildren -- can't combine becuase Nonprofit hasn't been defined before
# Used for creating nonprofits at the command line
class CreateCommand < Rails::Command::Base
desc 'Create a new nonprofit on your Houdini instance'
option :super_admin, aliases: '-su', default: false, type: :boolean,
desc: "Make the nonprofit admin a super user (they can access any nonprofit's dashboards)"
option :confirm_admin, default: true, type: :boolean, desc: 'Require the nonprofit admin to be confirmed via email'
option :nonprofit_name, default: nil, desc: "Provide the nonprofit's name"
option :state_code, default: nil, desc: "Provide the nonprofit's state code, e.g. WI for Wisconsin"
option :city, default: nil, desc: "Provide the nonprofit's city"
option :nonprofit_website, default: nil, desc: "[OPTIONAL] Provide the nonprofit's public website"
option :nonprofit_email, default: nil, desc: "[OPTIONAL] Provide the nonprofit's public email"
option :nonprofit_phone, default: nil, desc: "[OPTIONAL] Provide the nonprofit's public phone number"
option :user_name, default: nil, desc: "Provide the nonprofit's admin's name"
option :user_email, default: nil,
desc: "Provide the nonprofit's admin's email address (It'll be used for logging in)"
option :user_password, default: nil, desc: "Provide the nonprofit's admin's password"
def perform
result = {
nonprofit: ask_for_nonprofit_information(options),
user: ask_for_user_information(options)
}
say
require_application_and_environment!
creation_result = ::NonprofitCreation.new(result, options).call
creation_result[:messages].each do |msg|
say(msg)
end
end
private
def ask_for_nonprofit_information(options)
{
name: options[:nonprofit_name] || ask("What is the nonprofit's name?"),
state_code: options[:state_code] || ask("What is the nonprofit's state?"),
city: options[:city] || ask("What is the nonprofit's city?"),
website: options[:nonprofit_website] || ask("[OPTIONAL] What is the nonprofit's public website?"),
email: options[:nonprofit_email] || ask("[OPTIONAL] What is the nonprofit's public e-mail?"),
phone: options[:nonprofit_phone] || ask("[OPTIONAL] What is your nonprofit's public phone number?")
}
end
def ask_for_user_information(options)
{
name: options[:user_name] || ask("What is your nonprofit's admin's name?"),
email: options[:user_email] || ask(
"What is your nonprofit's admin's email address? (It'll be used for logging in)"
),
password: options[:user_password] || ask("What is the nonprofit's admin's password?", echo: false)
}
end
end
end
end

View file

@ -11,7 +11,6 @@ Rails.application.routes.draw do
get '/button_debug/embedded/:id' => 'button_debug#embedded' get '/button_debug/embedded/:id' => 'button_debug#embedded'
get '/button_debug/button/:id' => 'button_debug#button' get '/button_debug/button/:id' => 'button_debug#button'
end end
get 'onboard' => 'onboard#index'
namespace(:api) do namespace(:api) do
resources(:nonprofits) resources(:nonprofits)
@ -120,7 +119,6 @@ Rails.application.routes.draw do
end end
resources(:nonprofits, only: %i[show create update destroy]) do resources(:nonprofits, only: %i[show create update destroy]) do
post(:onboard, on: :collection)
get(:profile_todos, on: :member) get(:profile_todos, on: :member)
get(:recurring_donation_stats, on: :member) get(:recurring_donation_stats, on: :member)
get(:search, on: :collection) get(:search, on: :collection)

View file

@ -13,6 +13,7 @@ module Houdini
autoload :PaymentProvider autoload :PaymentProvider
autoload :EventPublisher autoload :EventPublisher
autoload :WebhookAdapter autoload :WebhookAdapter
autoload :NonprofitCreation
mattr_accessor :intl, :maintenance, :ccs mattr_accessor :intl, :maintenance, :ccs

63
lib/nonprofit_creation.rb Normal file
View file

@ -0,0 +1,63 @@
# frozen_string_literal: true
#
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
#
# NOTE: this should be moved to bess when Nonprofit and wiki is
class NonprofitCreation
def initialize(result, options = nil)
result = sanitize_optional_fields(result)
@nonprofit = ::Nonprofit.new(result[:nonprofit].merge({ register_np_only: true }))
@user = User.new(result[:user])
@options = options
end
def call
@user.skip_confirmation! if @options&.dig(:no_confirm_admin)
result = {}
ActiveRecord::Base.transaction do
result = if @user.save && @nonprofit.save && roles.each(&:save)
{ success: true, messages: ["Nonprofit #{@nonprofit.id} successfully created."] }
else
retrieve_error_messages
end
end
result
end
private
def retrieve_error_messages
result = { success: false, messages: [] }
result = retrieve_user_error_messages(result)
result = retrieve_nonprofit_error_messages(result)
retrieve_roles_error_messages(result)
end
def retrieve_user_error_messages(result)
@user.errors.full_messages.each { |i| result[:messages] << "Error creating user: #{i}" }
result
end
def retrieve_nonprofit_error_messages(result)
@nonprofit.errors.full_messages.each { |i| result[:messages] << "Error creating nonprofit: #{i}" }
result
end
def retrieve_roles_error_messages(result)
roles.each { |role| role.errors.full_messages.each { |i| result[:messages] << "Error creating role: #{i}" } }
result
end
def roles
roles = [Role.new(host: @nonprofit, name: 'nonprofit_admin', user: @user)]
roles << Role.new(host: @nonprofit, name: 'super_admin', user: @user) if @options&.dig(:super_admin)
roles
end
def sanitize_optional_fields(result)
result.transform_values! { |keys| keys.transform_values! { |value| value&.empty? ? nil : value } }
end
end

View file

@ -15,11 +15,6 @@ describe FrontController, type: :controller do
end end
end end
it 'index redirects to onboard with no non-profits' do
get(:index)
expect(response).to redirect_to onboard_url
end
describe 'have nonprofit info' do describe 'have nonprofit info' do
include_context :shared_user_context include_context :shared_user_context
it 'redirect to nonprofit admin' do it 'redirect to nonprofit admin' do

View file

@ -0,0 +1,98 @@
# frozen_string_literal: true
#
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
require 'rails_helper'
RSpec.describe NonprofitCreation do
subject { described_class.new(result, options).call }
let(:result) do
{
nonprofit: {
name: 'My Nonprofit',
state_code: 'DF',
city: 'Aguas Claras',
website: 'https://www.mynonprofit.org',
email: 'mynonprofit@email.com',
phone: '5561999999999'
},
user: {
name: 'User Name',
email: 'username@email.com',
password: 'P@ssw0rd!'
}
}
end
let(:options) { { confirm_admin: true } }
describe 'command side effects' do
it { is_expected.to include(success: true, messages: ["Nonprofit #{Nonprofit.last.id} successfully created."]) }
end
describe 'created nonprofit' do
subject do
super()
Nonprofit.find_by(name: 'My Nonprofit')
end
let(:expected_attributes) do
{
state_code: 'DF',
city: 'Aguas Claras',
website: 'https://www.mynonprofit.org',
email: 'mynonprofit@email.com',
phone: '5561999999999'
}
end
it { is_expected.to have_attributes(expected_attributes) }
end
describe 'created user' do
subject do
super()
User.find_by(email: 'username@email.com')
end
it { is_expected.to_not be_super_admin }
it { is_expected.to_not be_confirmed }
it { is_expected.to have_attributes(name: 'User Name') }
end
describe 'super_admin_option' do
subject do
super()
User.find_by(email: 'anotherusername@email.com')
end
before do
result[:user][:email] = 'anotherusername@email.com'
end
let(:options) { { super_admin: true } }
it { is_expected.to be_super_admin }
it { is_expected.to_not be_confirmed }
end
context 'when nonprofit can not be saved' do
before do
result[:user][:email] = nil
end
let(:expected_error_result) do
{
success: false,
messages: [
"Error creating user: Email can't be blank",
'Error creating user: Email is invalid'
]
}
end
it { is_expected.to eq(expected_error_result) }
end
end

View file

@ -11,11 +11,11 @@ describe 'Maintenance Mode' do
token = 'thoathioa' token = 'thoathioa'
include_context :shared_user_context include_context :shared_user_context
describe OnboardController, type: :controller do describe SettingsController, type: :controller do
describe '(Onboard is just a basic example controller)' describe '(Settings is just a basic example controller)'
it 'not in maintenance mode' do it 'not in maintenance mode' do
get :index get :index
assert_response 200 assert_response 302
end end
describe 'in maintenance' do describe 'in maintenance' do
@ -23,7 +23,7 @@ describe 'Maintenance Mode' do
Houdini.maintenance = Houdini::Maintenance.new(active: true, token: token, page: page) Houdini.maintenance = Houdini::Maintenance.new(active: true, token: token, page: page)
end end
it 'redirects for onboard' do it 'redirects for settings' do
get :index get :index
assert_redirected_to page assert_redirected_to page
end end