munificent-admin 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/LICENCE +2 -0
  3. data/README.md +83 -0
  4. data/Rakefile +12 -0
  5. data/app/abilities/munificent/admin/ability.rb +51 -0
  6. data/app/abilities/munificent/admin/application_ability.rb +37 -0
  7. data/app/assets/config/munificent_admin_manifest.js +4 -0
  8. data/app/assets/javascripts/munificent_admin/application.js +6 -0
  9. data/app/assets/stylesheets/munificent/admin/application.scss +2 -0
  10. data/app/assets/stylesheets/munificent/admin/layout.scss +64 -0
  11. data/app/assets/stylesheets/munificent/admin/records.scss +7 -0
  12. data/app/controllers/munificent/admin/application_controller.rb +61 -0
  13. data/app/controllers/munificent/admin/bundle_tiers_controller.rb +12 -0
  14. data/app/controllers/munificent/admin/bundles_controller.rb +86 -0
  15. data/app/controllers/munificent/admin/charities_controller.rb +60 -0
  16. data/app/controllers/munificent/admin/dashboard_controller.rb +9 -0
  17. data/app/controllers/munificent/admin/donations_controller.rb +9 -0
  18. data/app/controllers/munificent/admin/donator_bundles_controller.rb +9 -0
  19. data/app/controllers/munificent/admin/donators_controller.rb +9 -0
  20. data/app/controllers/munificent/admin/fundraisers_controller.rb +68 -0
  21. data/app/controllers/munificent/admin/games_controller.rb +65 -0
  22. data/app/controllers/munificent/admin/otp_controller.rb +57 -0
  23. data/app/controllers/munificent/admin/user_sessions_controller.rb +44 -0
  24. data/app/controllers/munificent/admin/users_controller.rb +77 -0
  25. data/app/form_builders/munificent/admin/form_builder.rb +112 -0
  26. data/app/helpers/munificent/admin/application_helper.rb +6 -0
  27. data/app/helpers/munificent/admin/form_helper.rb +9 -0
  28. data/app/helpers/munificent/admin/layout_helper.rb +11 -0
  29. data/app/helpers/munificent/admin/state_machine_helper.rb +49 -0
  30. data/app/mailers/munificent/admin/application_mailer.rb +8 -0
  31. data/app/mailers/munificent/admin/panic_mailer.rb +19 -0
  32. data/app/models/concerns/authenticable.rb +9 -0
  33. data/app/models/munificent/admin/application_record.rb +7 -0
  34. data/app/models/munificent/admin/user.rb +54 -0
  35. data/app/models/munificent/admin/user_session.rb +19 -0
  36. data/app/presenters/munificent/admin/application_presenter.rb +29 -0
  37. data/app/presenters/munificent/admin/user_presenter.rb +24 -0
  38. data/app/presenters/munificent/bundle_presenter.rb +27 -0
  39. data/app/presenters/munificent/bundle_tier_presenter.rb +20 -0
  40. data/app/presenters/munificent/charity_presenter.rb +14 -0
  41. data/app/presenters/munificent/donation_presenter.rb +39 -0
  42. data/app/presenters/munificent/donator_bundle_presenter.rb +12 -0
  43. data/app/presenters/munificent/donator_presenter.rb +12 -0
  44. data/app/presenters/munificent/fundraiser_presenter.rb +41 -0
  45. data/app/presenters/munificent/game_presenter.rb +20 -0
  46. data/app/views/layouts/munificent/admin/application.html.erb +87 -0
  47. data/app/views/munificent/admin/bundles/_form.html.erb +15 -0
  48. data/app/views/munificent/admin/bundles/_tier_form.html.erb +13 -0
  49. data/app/views/munificent/admin/bundles/edit.html.erb +12 -0
  50. data/app/views/munificent/admin/bundles/index.html.erb +15 -0
  51. data/app/views/munificent/admin/bundles/new.html.erb +9 -0
  52. data/app/views/munificent/admin/bundles/show.html.erb +44 -0
  53. data/app/views/munificent/admin/charities/_form.html.erb +11 -0
  54. data/app/views/munificent/admin/charities/edit.html.erb +3 -0
  55. data/app/views/munificent/admin/charities/index.html.erb +11 -0
  56. data/app/views/munificent/admin/charities/new.html.erb +3 -0
  57. data/app/views/munificent/admin/charities/show.html.erb +23 -0
  58. data/app/views/munificent/admin/common/_record.html.erb +18 -0
  59. data/app/views/munificent/admin/common/_records.html.erb +38 -0
  60. data/app/views/munificent/admin/dashboard/index.html.erb +3 -0
  61. data/app/views/munificent/admin/donations/index.html.erb +15 -0
  62. data/app/views/munificent/admin/donator_bundles/index.html.erb +10 -0
  63. data/app/views/munificent/admin/donators/index.html.erb +11 -0
  64. data/app/views/munificent/admin/fundraisers/_form.html.erb +16 -0
  65. data/app/views/munificent/admin/fundraisers/edit.html.erb +3 -0
  66. data/app/views/munificent/admin/fundraisers/index.html.erb +17 -0
  67. data/app/views/munificent/admin/fundraisers/new.html.erb +3 -0
  68. data/app/views/munificent/admin/fundraisers/show.html.erb +41 -0
  69. data/app/views/munificent/admin/games/_form.html.erb +16 -0
  70. data/app/views/munificent/admin/games/csv_upload.html.erb +10 -0
  71. data/app/views/munificent/admin/games/edit.html.erb +3 -0
  72. data/app/views/munificent/admin/games/index.html.erb +15 -0
  73. data/app/views/munificent/admin/games/new.html.erb +3 -0
  74. data/app/views/munificent/admin/games/show.html.erb +25 -0
  75. data/app/views/munificent/admin/otp/_input_form.html.erb +6 -0
  76. data/app/views/munificent/admin/otp/input.html.erb +3 -0
  77. data/app/views/munificent/admin/otp/setup.html.erb +7 -0
  78. data/app/views/munificent/admin/panic_mailer/missing_key.html.erb +6 -0
  79. data/app/views/munificent/admin/panic_mailer/missing_key.text.erb +4 -0
  80. data/app/views/munificent/admin/user_sessions/new.html.erb +9 -0
  81. data/app/views/munificent/admin/users/_form.html.erb +29 -0
  82. data/app/views/munificent/admin/users/edit.html.erb +3 -0
  83. data/app/views/munificent/admin/users/index.html.erb +15 -0
  84. data/app/views/munificent/admin/users/new.html.erb +3 -0
  85. data/app/views/munificent/admin/users/show.html.erb +20 -0
  86. data/config/cucumber.yml +9 -0
  87. data/config/importmap.rb +4 -0
  88. data/config/initializers/inflections.rb +24 -0
  89. data/config/locales/en.yml +4 -0
  90. data/config/routes.rb +40 -0
  91. data/db/migrate/20220524130247_create_munificent_admin_user.rb +50 -0
  92. data/lib/munificent/admin/engine.rb +32 -0
  93. data/lib/munificent/admin/seeds.rb +19 -0
  94. data/lib/munificent/admin/version.rb +5 -0
  95. data/lib/munificent/admin.rb +7 -0
  96. data/lib/tasks/munificent/admin_tasks.rake +4 -0
  97. data/lib/tasks/munificent/cucumber.rake +75 -0
  98. data/lib/tasks/munificent/default.rake +32 -0
  99. metadata +354 -0
@@ -0,0 +1,57 @@
1
+ require "rotp"
2
+ require "rqrcode"
3
+
4
+ module Munificent
5
+ module Admin
6
+ class OTPController < ApplicationController
7
+ skip_authorization_check
8
+ skip_before_action :enforce_2sv
9
+
10
+ before_action :require_setup, only: %i[input]
11
+ before_action :prevent_double_setup, only: %i[setup]
12
+ before_action :require_otp_issuer
13
+
14
+ def input; end
15
+
16
+ def setup
17
+ session[:otp_secret] = ROTP::Base32.random
18
+ totp = ROTP::TOTP.new(session[:otp_secret], issuer: ENV.fetch("OTP_ISSUER", nil))
19
+ @otp_url = totp.provisioning_uri(current_user.email_address)
20
+ @qr_code = RQRCode::QRCode.new(@otp_url).as_svg(standalone: false, module_size: 5)
21
+ end
22
+
23
+ def verify
24
+ otp_secret = current_user.otp_secret || session[:otp_secret]
25
+ raise "Missing OTP secret" if otp_secret.blank?
26
+
27
+ totp = ROTP::TOTP.new(otp_secret, issuer: ENV.fetch("OTP_ISSUER", nil), after: current_user.last_otp_at)
28
+
29
+ if totp.verify(params[:otp_code], drift_behind: 3)
30
+ current_user.otp_secret ||= session[:otp_secret]
31
+ current_user.update(last_otp_at: (session[:last_otp_at] = Time.zone.now))
32
+ session[:otp_secret] = nil
33
+
34
+ redirect_to root_path
35
+ elsif current_user.has_2sv?
36
+ redirect_to otp_input_path
37
+ else
38
+ redirect_to otp_setup_path
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def require_setup
45
+ redirect_to otp_setup_path unless current_user&.has_2sv?
46
+ end
47
+
48
+ def prevent_double_setup
49
+ redirect_to otp_input_path if current_user&.has_2sv?
50
+ end
51
+
52
+ def require_otp_issuer
53
+ raise "Missing OTP issuer" if ENV["OTP_ISSUER"].blank?
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,44 @@
1
+ module Munificent
2
+ module Admin
3
+ class UserSessionsController < ApplicationController
4
+ skip_authorization_check
5
+
6
+ skip_before_action :require_login, only: %i[new create]
7
+ skip_before_action :enforce_2sv
8
+
9
+ def new
10
+ @user_session = UserSession.new
11
+ end
12
+
13
+ def create
14
+ @user_session = UserSession.new(user_session_params.to_h)
15
+
16
+ if @user_session.save
17
+ redirect_to root_path
18
+ else
19
+ flash[:alert] = @user_session.errors.full_messages.join(", ")
20
+ redirect_to new_user_session_path
21
+ end
22
+ end
23
+
24
+ def destroy
25
+ current_user_session.destroy
26
+
27
+ redirect_to new_user_session_path
28
+ end
29
+
30
+ private
31
+
32
+ def user_session_params
33
+ params.require(:user_session).permit(
34
+ *%i[
35
+ email_address
36
+ password
37
+ password_confirmation
38
+ remember_me
39
+ ],
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ module Munificent
2
+ module Admin
3
+ class UsersController < ApplicationController
4
+ load_and_authorize_resource class: "Munificent::Admin::User"
5
+
6
+ def index
7
+ authorize!(:read, :user_accounts)
8
+ end
9
+
10
+ def show; end
11
+ def new; end
12
+ def edit; end
13
+
14
+ def create
15
+ if @user.save
16
+ flash[:notice] = "User created"
17
+ redirect_to edit_user_path(@user)
18
+ else
19
+ flash[:alert] = @user.errors.full_messages.join(", ")
20
+ redirect_to new_user_path
21
+ end
22
+ end
23
+
24
+ def update
25
+ model = :user
26
+ if params[model][:password].blank?
27
+ %w[password password_confirmation].each { |p| params[model].delete(p) }
28
+ end
29
+
30
+ @user.assign_attributes(user_params)
31
+
32
+ if @user.save
33
+ flash[:notice] = "User saved"
34
+ else
35
+ flash[:alert] = @user.errors.full_messages.join(", ")
36
+ end
37
+
38
+ redirect_to edit_user_path(@user)
39
+ end
40
+
41
+ def destroy
42
+ @user.destroy
43
+ redirect_to users_path
44
+ end
45
+
46
+ private
47
+
48
+ def user_params
49
+ params.require(:user).permit(
50
+ *%i[
51
+ active
52
+ approved
53
+ confirmed
54
+ data_entry
55
+ email_address
56
+ full_access
57
+ manages_users
58
+ name
59
+ password
60
+ password_confirmation
61
+ support
62
+ ],
63
+ )
64
+ end
65
+
66
+ def resource
67
+ @user
68
+ end
69
+ helper_method :resource
70
+
71
+ def presenter
72
+ @presenter ||= Munificent::Admin::ApplicationPresenter.present(@user)
73
+ end
74
+ helper_method :presenter
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,112 @@
1
+ module Munificent
2
+ module Admin
3
+ class FormBuilder < ActionView::Helpers::FormBuilder
4
+ include ActionView::Helpers::TagHelper
5
+ include ActionView::Context
6
+
7
+ TEXT_FIELDS = %i[
8
+ email_field
9
+ number_field
10
+ password_field
11
+ text_area
12
+ text_field
13
+ ].freeze
14
+
15
+ UNFLOATABLE_TYPES = %i[
16
+ select
17
+ ].freeze
18
+
19
+ def field(type, name, label: name.to_s.humanize, **opts)
20
+ field_tag = case type
21
+ when *TEXT_FIELDS
22
+ public_send(type, name, class: "form-control", placeholder: (label unless table_mode?))
23
+ when :select
24
+ select(name, opts[:source], select_field_opts(opts), select_html_opts(opts))
25
+ else
26
+ send(type, name, class: "form-control")
27
+ end
28
+
29
+ wrap(
30
+ label(name, label, class: "form-label"),
31
+ field_tag,
32
+ field_name: name,
33
+ float: UNFLOATABLE_TYPES.exclude?(type),
34
+ )
35
+ end
36
+
37
+ def check_box(name, label: name.to_s.humanize, **opts)
38
+ check_block = tag.div(class: "form-check") {
39
+ super(name, class: "form-check-input", **check_box_opts(opts)) +
40
+ label(name, label, class: "form-check-label")
41
+ }
42
+
43
+ wrap(check_block, field_name: name)
44
+ end
45
+
46
+ def table(css_class: nil, &block)
47
+ @table_mode = true
48
+ contents = @template.capture(&block)
49
+ @table_mode = false
50
+
51
+ css_class = (%w[table table-bordered] + Array(css_class)).compact.join(" ")
52
+ tag.table(contents, class: css_class)
53
+ end
54
+
55
+ def money(name, default_currency: nil)
56
+ field(:select, "#{name}_currency",
57
+ label: "Currency",
58
+ source: Munificent::Currency.present_all,
59
+ selected: (object.public_send("#{name}_currency").presence || default_currency),
60
+ ) + field(:text_field, "human_#{name}", label: name.to_s.humanize)
61
+ end
62
+
63
+ private
64
+
65
+ def wrap(*args, field_name: nil, float: true)
66
+ row_class = ["field-#{field_name}"]
67
+
68
+ args.reverse! if float
69
+
70
+ if table_mode?
71
+ tag.tr(class: row_class.compact.join(" ")) do
72
+ args.compact.map { |arg| tag.td(arg) }.reverse.reduce(:+)
73
+ end
74
+ else
75
+ row_class << "mb-3"
76
+ row_class += %w[form-floating mb-3] if float
77
+
78
+ tag.div(class: row_class.compact.join(" ")) do
79
+ args.compact.reduce(:+)
80
+ end
81
+ end
82
+ end
83
+
84
+ def table_mode?
85
+ !!@table_mode
86
+ end
87
+
88
+ def select_field_opts(opts)
89
+ { include_blank: opts[:optional] }.tap do |field_opts|
90
+ field_opts[:selected] = opts[:selected] if opts[:selected]
91
+ end
92
+ end
93
+
94
+ def select_html_opts(opts)
95
+ { class: "form-select" }.tap do |html_opts|
96
+ if opts[:multiple]
97
+ html_opts.merge!(
98
+ multiple: true,
99
+ aria: { label: "multiple select" },
100
+ )
101
+ end
102
+ end
103
+ end
104
+
105
+ def check_box_opts(opts)
106
+ {}.tap do |field_opts|
107
+ field_opts[:checked] = opts[:checked] unless opts[:checked].nil?
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,6 @@
1
+ module Munificent
2
+ module Admin
3
+ module ApplicationHelper
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module Munificent
2
+ module Admin
3
+ module FormHelper
4
+ def form_for(*args, **kwargs, &)
5
+ super(*args, builder: Munificent::Admin::FormBuilder, **kwargs, &)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module Munificent
2
+ module Admin
3
+ module LayoutHelper
4
+ def nav_link(*args, **kwargs)
5
+ link_to_unless_current(*args, **kwargs) do |text|
6
+ content_tag(:span, text)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,49 @@
1
+ module Munificent
2
+ module Admin
3
+ module StateMachineHelper
4
+ module_function
5
+
6
+ def aasm_buttons(resource, tag_name: "li")
7
+ resource.aasm.permitted_transitions.each do |transition|
8
+ event = transition.fetch(:event).to_s
9
+
10
+ content_for(:aasm_buttons) do
11
+ tag.public_send(tag_name) do
12
+ button_to(
13
+ event.humanize,
14
+ send("#{event}_#{resource.class.name.split('::').last.underscore}_path", resource),
15
+ method: :post,
16
+ data: { confirm: "Are you sure?" },
17
+ )
18
+ end
19
+ end
20
+ end
21
+
22
+ content_for(:aasm_buttons).presence
23
+ end
24
+
25
+ def define_routes(router, resource_class)
26
+ router.send(:member) do
27
+ resource_class.aasm.events.each do |event|
28
+ router.send(:post, event.to_s)
29
+ end
30
+ end
31
+ end
32
+
33
+ def define_actions(controller, resource_class)
34
+ singular_name = resource_class.name.split("::").last.underscore
35
+
36
+ resource_class.aasm.events.each do |event|
37
+ controller.define_method(event.name) do
38
+ resource.public_send("#{event.name}!")
39
+ redirect_to(send("#{singular_name}_path", resource),
40
+ notice: "#{event.name.to_s.humanize} was successful",
41
+ )
42
+ end
43
+ end
44
+
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,8 @@
1
+ module Munificent
2
+ module Admin
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: -> { ENV.fetch("FROM_EMAIL_ADDRESS", nil) }
5
+ layout "mailer"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ module Munificent
2
+ module Admin
3
+ class PanicMailer < ApplicationMailer
4
+ # Subject can be set in your I18n file at config/locales/en.yml
5
+ # with the following lookup:
6
+ #
7
+ # en.panic_mailer.missing_key.subject
8
+ #
9
+ def missing_key(donator, game)
10
+ return if User.none?
11
+
12
+ @donator = donator
13
+ @game = game
14
+
15
+ mail to: User.pluck(:email_address)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ module Authenticable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ acts_as_authentic do |config|
6
+ config.crypto_provider = ::Authlogic::CryptoProviders::SCrypt
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module Munificent
2
+ module Admin
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,54 @@
1
+ module Munificent
2
+ module Admin
3
+ class User < Munificent::ApplicationRecord
4
+ include Authenticable
5
+
6
+ attr_accessor :password_confirmation
7
+ attr_writer :require_password
8
+
9
+ def require_password?
10
+ !!@require_password
11
+ end
12
+
13
+ validates :password,
14
+ presence: true,
15
+ confirmation: true,
16
+ length: { minimum: 10 },
17
+ if: :require_password?
18
+ validates :email_address, presence: true
19
+
20
+ def has_2sv?
21
+ otp_secret.present?
22
+ end
23
+
24
+ def permissions
25
+ if full_access?
26
+ ["full access"]
27
+ else
28
+ [].tap do |perms|
29
+ perms << "data entry" if data_entry?
30
+ perms << "manages users" if manages_users?
31
+ perms << "support" if support?
32
+ end
33
+ end
34
+ end
35
+
36
+ def states
37
+ [].tap do |states|
38
+ states << "active" if active?
39
+ states << "approved" if approved?
40
+ states << "confirmed" if confirmed?
41
+ end
42
+ end
43
+
44
+ # We may use these later, but for now default them to `true`.
45
+ def active? = true
46
+ def approved? = true
47
+ def confirmed? = true
48
+
49
+ def to_s
50
+ name.presence || email_address
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,19 @@
1
+ module Munificent
2
+ module Admin
3
+ class UserSession < Authlogic::Session::Base
4
+ login_field :email_address
5
+ record_selection_method :find_by_email_address
6
+
7
+ # This is a fairly restrictive configuration
8
+ # because this is an admin application.
9
+ consecutive_failed_logins_limit 5
10
+ encrypt_cookie true
11
+ generalize_credentials_error_messages true
12
+ httponly true
13
+ logout_on_timeout true
14
+ remember_me_for 20.hours
15
+ same_site "Strict"
16
+ single_access_allowed_request_types []
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module Munificent
2
+ module Admin
3
+ class ApplicationPresenter < ActionView::Base
4
+ def self.delegate(array, to: :record)
5
+ super(*array, to:)
6
+ end
7
+
8
+ def self.present(record)
9
+ presenter_class_for(record).new(record)
10
+ end
11
+
12
+ def self.presenter_class_for(record)
13
+ "#{record.class.name}Presenter".constantize
14
+ rescue NameError
15
+ raise ArgumentError, "No presenter available for record type `#{record.class.name}`"
16
+ end
17
+
18
+ attr_reader :record
19
+
20
+ def initialize(record) # rubocop:disable Lint/MissingSuper
21
+ @record = record
22
+ end
23
+
24
+ def name
25
+ record
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ module Munificent
2
+ module Admin
3
+ class UserPresenter < ApplicationPresenter
4
+ delegate(
5
+ %i[
6
+ id
7
+ email_address
8
+ current_login_at
9
+ login_count
10
+ permissions
11
+ states
12
+ ],
13
+ )
14
+
15
+ def permissions
16
+ record.permissions.to_sentence.capitalize
17
+ end
18
+
19
+ def states
20
+ record.states.to_sentence.capitalize
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module Munificent
2
+ class BundlePresenter < Admin::ApplicationPresenter
3
+ delegate(
4
+ %i[
5
+ id
6
+ fundraiser
7
+ ],
8
+ )
9
+
10
+ delegate(
11
+ %i[
12
+ ends_at
13
+ starts_at
14
+ ], to: :"record.highest_tier",
15
+ )
16
+
17
+ def price
18
+ record.highest_tier&.human_price(symbol: true)
19
+ end
20
+
21
+ def state
22
+ tag.span(class: "badge text-bg-#{record.live? ? 'success' : 'secondary'}") do
23
+ record.state.humanize
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ module Munificent
2
+ class BundleTierPresenter < Admin::ApplicationPresenter
3
+ delegate(
4
+ %i[
5
+ id
6
+ ends_at
7
+ fundraiser
8
+ starts_at
9
+ ],
10
+ )
11
+
12
+ def price
13
+ record.human_price(symbol: true)
14
+ end
15
+
16
+ def games
17
+ record.bundle_tier_games.map(&:game).map(&:name).join(", ")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ module Munificent
2
+ class CharityPresenter < Admin::ApplicationPresenter
3
+ delegate(
4
+ %i[
5
+ id
6
+ description
7
+ ],
8
+ )
9
+
10
+ def fundraisers
11
+ record.fundraisers.map(&:name).join(", ")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ module Munificent
2
+ class DonationPresenter < Admin::ApplicationPresenter
3
+ delegate(
4
+ %i[
5
+ id
6
+ amount
7
+ created_at
8
+ curated_streamer
9
+ donated_by
10
+ donator_name
11
+ fundraiser
12
+ message
13
+ ],
14
+ )
15
+
16
+ def state
17
+ tag.span(class: "badge text-bg-#{badge_type}") do
18
+ record.state.humanize
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def badge_type
25
+ case record.state
26
+ when "pending"
27
+ "secondary"
28
+ when "paid"
29
+ "info"
30
+ when "cancelled"
31
+ "danger"
32
+ when "fulfilled"
33
+ "success"
34
+ else
35
+ "light"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ module Munificent
2
+ class DonatorBundlePresenter < Admin::ApplicationPresenter
3
+ delegate(
4
+ %i[
5
+ id
6
+ bundle
7
+ created_at
8
+ donator
9
+ ],
10
+ )
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Munificent
2
+ class DonatorPresenter < Admin::ApplicationPresenter
3
+ delegate(
4
+ %i[
5
+ id
6
+ chosen_name
7
+ created_at
8
+ email_address
9
+ ],
10
+ )
11
+ end
12
+ end