munificent-admin 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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