effective_memberships 0.4.7 → 0.4.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/effective_memberships/applicant_experiences.js +0 -5
  3. data/app/controllers/admin/applicant_endorsements_controller.rb +9 -0
  4. data/app/controllers/admin/memberships_controller.rb +1 -1
  5. data/app/controllers/effective/applicant_endorsements_controller.rb +48 -0
  6. data/app/controllers/effective/applicant_references_controller.rb +1 -1
  7. data/app/datatables/admin/effective_applicant_endorsements_datatable.rb +35 -0
  8. data/app/datatables/effective_applicant_endorsements_datatable.rb +37 -0
  9. data/app/datatables/effective_applicant_references_datatable.rb +1 -1
  10. data/app/mailers/effective/memberships_mailer.rb +25 -0
  11. data/app/models/concerns/effective_memberships_applicant.rb +34 -6
  12. data/app/models/concerns/effective_memberships_fee_payment.rb +0 -1
  13. data/app/models/concerns/effective_memberships_organization.rb +5 -0
  14. data/app/models/concerns/effective_memberships_owner.rb +18 -2
  15. data/app/models/concerns/effective_memberships_registrar.rb +31 -30
  16. data/app/models/concerns/effective_memberships_user.rb +5 -0
  17. data/app/models/effective/applicant_endorsement.rb +100 -0
  18. data/app/models/effective/membership.rb +40 -3
  19. data/app/models/effective/membership_history.rb +2 -0
  20. data/app/views/admin/applicant_endorsements/_applicant_endorsement.html.haml +1 -0
  21. data/app/views/admin/applicants/_form.html.haml +4 -0
  22. data/app/views/admin/categories/_form_applicant_steps.html.haml +6 -0
  23. data/app/views/admin/fee_payments/_fee_payment.html.haml +4 -0
  24. data/app/views/admin/fees/_form.html.haml +2 -3
  25. data/app/views/admin/memberships/_form_membership.html.haml +1 -1
  26. data/app/views/admin/users/_col.html.haml +12 -0
  27. data/app/views/effective/applicant_endorsements/_applicant_endorsement.html.haml +46 -0
  28. data/app/views/effective/applicant_endorsements/_datatable_actions.html.haml +4 -0
  29. data/app/views/effective/applicant_endorsements/_form.html.haml +9 -0
  30. data/app/views/effective/applicant_endorsements/_form_declaration.html.haml +22 -0
  31. data/app/views/effective/applicant_endorsements/complete.html.haml +3 -0
  32. data/app/views/effective/applicant_endorsements/edit.html.haml +8 -0
  33. data/app/views/effective/applicants/_endorsements.html.haml +15 -0
  34. data/app/views/effective/applicants/endorsements.html.haml +25 -0
  35. data/app/views/effective/applicants/experience.html.haml +1 -1
  36. data/app/views/effective/applicants/select.html.haml +6 -1
  37. data/app/views/effective/fee_payments/_orders.html.haml +1 -1
  38. data/app/views/effective/memberships_mailer/applicant_endorsement_notification.liquid +15 -0
  39. data/config/effective_memberships.rb +3 -0
  40. data/config/routes.rb +9 -0
  41. data/db/migrate/01_create_effective_memberships.rb.erb +30 -0
  42. data/lib/effective_memberships/version.rb +1 -1
  43. data/lib/effective_memberships.rb +1 -1
  44. metadata +32 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a59918a13a42cc9da99200ecfa61ed9b45ae8881ca7e9c0d4e2fd0117605003
4
- data.tar.gz: b1955f238384bbb8cfc4836ad06357b1b815c47c23a9e8ac11303e858feb8379
3
+ metadata.gz: b7d09ef68f61058a8ee29567caa3a008d887482edab0fa3e8d251ea69f9ea3cb
4
+ data.tar.gz: 10a88e200464a6d2d7dbb8fbdcaa3aed77337b067e12fc3577b31a8032cbd2ce
5
5
  SHA512:
6
- metadata.gz: e2ad995fdad088a4db63c1f98467330a1315deb55fe6c912e12662a1a66d017365fd2c379038d7422f9cf5a5b9b7338aa24c05e65f519360be075098ffc4628d
7
- data.tar.gz: de64346cc2ce1119672f925d2f60589b4e9e5cd0c5ab506438cee5e7ec2e31e36a79924ed26896c5e0bec639f6033401317e254adcdd03dc8ccf279d75f0a0c9
6
+ metadata.gz: 3adbdb1ab5f18df509ebc6631f39a142f2857213ce9ee0a23b83eec2fa44dda3d8b77c42afe7e4e4b494bb938dac7aee672cc26dacbd398fd9f34cc3c55ed222
7
+ data.tar.gz: ab80c7ff8b01736429ecabc774b81c45702d21ae521e2e7db276191802fa212171f8e81e448a06e452fb97745c5aec3723fe37537669f3a3ed87b9cd30d03746
@@ -31,8 +31,3 @@ $(document).on('change dp.change keyup', "[data-applicant-experiences-month]", f
31
31
  const $form = $(event.currentTarget).closest('form');
32
32
  sumApplicantExperiences($form);
33
33
  });
34
-
35
-
36
- // $(document).on 'cocoon:after-remove', (event) ->
37
- // $form = $(event.target).closest('form')
38
- // sumMonths($form) if $form.find('[data-experience-month]').length > 0
@@ -0,0 +1,9 @@
1
+ module Admin
2
+ class ApplicantEndorsementsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_memberships) }
5
+
6
+ include Effective::CrudController
7
+
8
+ end
9
+ end
@@ -5,7 +5,7 @@ module Admin
5
5
 
6
6
  include Effective::CrudController
7
7
 
8
- submit :save, 'Update Membership',
8
+ submit :revise, 'Update Membership',
9
9
  success: -> { "#{resource.owner} has been successfully updated. Please double check the membership history is correct" },
10
10
  redirect: -> { admin_owners_path(resource) }
11
11
 
@@ -0,0 +1,48 @@
1
+ module Effective
2
+ class ApplicantEndorsementsController < ApplicationController
3
+ include Effective::CrudController
4
+ include Effective::Select2AjaxController
5
+
6
+ page_title 'Confidential Endorsement Form'
7
+
8
+ # The show and update actions are public routes but can only be reached by the token.
9
+ # The endorser must declare and submit the form
10
+ # To move an applicant endorsement from submitted to completed
11
+
12
+ submit :notify, 'Resend email notification',
13
+ success: -> { "Sent email notification to #{resource.email}" }
14
+
15
+ submit :complete, 'Complete Endorsement'
16
+
17
+ def show
18
+ @applicant_endorsement = ApplicantEndorsement.submitted.find(params[:id])
19
+ EffectiveResources.authorize!(self, :show, @applicant_endorsement)
20
+
21
+ render 'edit'
22
+ end
23
+
24
+ # Must be signed in
25
+ def select2_ajax_endorser
26
+ authenticate_user! if defined?(Devise)
27
+
28
+ applicant = EffectiveMemberships.Applicant.find(params[:applicant_id])
29
+ collection = Effective::ApplicantEndorsement.endorser_collection(applicant)
30
+
31
+ respond_with_select2_ajax(collection)
32
+ end
33
+
34
+ protected
35
+
36
+ def permitted_params
37
+ permitted = params.require(:effective_applicant_endorsement).permit!.except(:token, :last_notified_at, :status, :status_steps, :applicant_id, :endorser_id)
38
+
39
+ if current_user && current_user.memberships_owners.include?(resource.applicant&.owner)
40
+ permitted.except(:notes, :accept_declaration)
41
+ else
42
+ permitted
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+ end
@@ -23,7 +23,7 @@ module Effective
23
23
  protected
24
24
 
25
25
  def permitted_params
26
- permitted = params.require(:effective_applicant_reference).permit!.except(:token, :last_notified_at, :status, :status_steps)
26
+ permitted = params.require(:effective_applicant_reference).permit!.except(:token, :last_notified_at, :status, :status_steps, :applicant_id)
27
27
 
28
28
  if current_user && current_user.memberships_owners.include?(resource.applicant&.owner)
29
29
  permitted.except(:reservations, :reservations_reason, :work_history, :accept_declaration)
@@ -0,0 +1,35 @@
1
+ module Admin
2
+ class EffectiveApplicantEndorsementsDatatable < Effective::Datatable
3
+
4
+ datatable do
5
+ order :name
6
+
7
+ col :applicant
8
+
9
+ col :endorser
10
+ col :email
11
+ col :name
12
+ col :phone
13
+ col :unknown_member, visible: false
14
+
15
+ col :status do |endorsement|
16
+ if endorsement.submitted?
17
+ 'Waiting on response'
18
+ elsif endorsement.completed?
19
+ 'Completed'
20
+ end
21
+ end
22
+
23
+ col :last_notified_at do |endorsement|
24
+ endorsement.last_notified_at&.strftime('%F') unless endorsement.completed?
25
+ end
26
+
27
+ actions_col
28
+ end
29
+
30
+ collection do
31
+ Effective::ApplicantEndorsement.deep.all
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ # For the applicant complete step to add additional endorsements
2
+ class EffectiveApplicantEndorsementsDatatable < Effective::Datatable
3
+
4
+ datatable do
5
+ order :name
6
+
7
+ col :name do |endorsement|
8
+ endorser&.to_s || name
9
+ end
10
+
11
+ col :email
12
+ col :phone
13
+
14
+ col :status do |reference|
15
+ if reference.submitted?
16
+ 'Waiting on response'
17
+ elsif reference.completed?
18
+ 'Completed'
19
+ end
20
+ end
21
+
22
+ col :last_notified_at do |endorsement|
23
+ endorsement.last_notified_at&.strftime('%F') unless endorsement.completed?
24
+ end
25
+
26
+ actions_col partial: 'effective/applicant_endorsements/datatable_actions', partial_as: :applicant_endorsement
27
+ end
28
+
29
+ collection do
30
+ Effective::ApplicantEndorsement.deep.where(applicant: applicant)
31
+ end
32
+
33
+ def applicant
34
+ @applicant ||= EffectiveMemberships.Applicant.where(id: attributes[:applicant_id]).first!
35
+ end
36
+
37
+ end
@@ -1,4 +1,4 @@
1
- # For the applicant complete step to add additional applicants
1
+ # For the applicant complete step to add additional references
2
2
  class EffectiveApplicantReferencesDatatable < Effective::Datatable
3
3
 
4
4
  datatable do
@@ -44,6 +44,16 @@ module Effective
44
44
  mail(to: resource.user.email, subject: subject, **headers)
45
45
  end
46
46
 
47
+ def applicant_endorsement_notification(resource, opts = {})
48
+ @assigns = assigns_for(resource)
49
+ @applicant_endorsement = resource
50
+
51
+ subject = subject_for(__method__, 'Endorsement Requested', resource, opts)
52
+ headers = headers_for(resource, opts)
53
+
54
+ mail(to: resource.email, subject: subject, **headers)
55
+ end
56
+
47
57
  def applicant_reference_notification(resource, opts = {})
48
58
  @assigns = assigns_for(resource)
49
59
  @applicant_reference = resource
@@ -61,6 +71,10 @@ module Effective
61
71
  return applicant_assigns(resource).merge(owner_assigns(resource.owner))
62
72
  end
63
73
 
74
+ if resource.kind_of?(Effective::ApplicantEndorsement)
75
+ return endorsement_assigns(resource).merge(owner_assigns(resource.applicant.owner))
76
+ end
77
+
64
78
  if resource.kind_of?(Effective::ApplicantReference)
65
79
  return reference_assigns(resource).merge(owner_assigns(resource.applicant.owner))
66
80
  end
@@ -88,6 +102,17 @@ module Effective
88
102
  { applicant: values }
89
103
  end
90
104
 
105
+ def endorsement_assigns(applicant_endorsement)
106
+ raise('expected a endorsement') unless applicant_endorsement.kind_of?(Effective::ApplicantEndorsement)
107
+
108
+ values = {
109
+ name: (applicant_endorsement.endorser&.to_s || applicant_endorsement.name),
110
+ url: effective_memberships.applicant_endorsement_url(applicant_endorsement)
111
+ }
112
+
113
+ { endorsement: values }
114
+ end
115
+
91
116
  def reference_assigns(applicant_reference)
92
117
  raise('expected a reference') unless applicant_reference.kind_of?(Effective::ApplicantReference)
93
118
 
@@ -93,6 +93,9 @@ module EffectiveMembershipsApplicant
93
93
  has_many :applicant_educations, -> { order(:id) }, class_name: 'Effective::ApplicantEducation', as: :applicant, inverse_of: :applicant, dependent: :destroy
94
94
  accepts_nested_attributes_for :applicant_educations, reject_if: :all_blank, allow_destroy: true
95
95
 
96
+ has_many :applicant_endorsements, -> { order(:id) }, class_name: 'Effective::ApplicantEndorsement', as: :applicant, inverse_of: :applicant, dependent: :destroy
97
+ accepts_nested_attributes_for :applicant_endorsements, reject_if: :all_blank, allow_destroy: true
98
+
96
99
  has_many :applicant_experiences, -> { order(:id) }, class_name: 'Effective::ApplicantExperience', as: :applicant, inverse_of: :applicant, dependent: :destroy
97
100
  accepts_nested_attributes_for :applicant_experiences, reject_if: :all_blank, allow_destroy: true
98
101
 
@@ -238,6 +241,16 @@ module EffectiveMembershipsApplicant
238
241
  end
239
242
  end
240
243
 
244
+ # Applicant Endorsements Step
245
+ with_options(if: -> { current_step == :endorsements }) do
246
+ validate do
247
+ required = min_applicant_endorsements()
248
+ existing = applicant_endorsements().reject(&:marked_for_destruction?).length
249
+
250
+ self.errors.add(:applicant_endorsements, "please include #{required} or more endorsements") if existing < required
251
+ end
252
+ end
253
+
241
254
  # Applicant References Step
242
255
  with_options(if: -> { current_step == :references }) do
243
256
  validate do
@@ -361,6 +374,7 @@ module EffectiveMembershipsApplicant
361
374
  stamps.each { |stamp| stamp.submit! }
362
375
 
363
376
  after_commit do
377
+ applicant_endorsements.each { |endorsement| endorsement.notify! if endorsement.submitted? }
364
378
  applicant_references.each { |reference| reference.notify! if reference.submitted? }
365
379
  end
366
380
 
@@ -521,6 +535,11 @@ module EffectiveMembershipsApplicant
521
535
  category&.min_applicant_references.to_i
522
536
  end
523
537
 
538
+ # Endorsements Step
539
+ def min_applicant_endorsements
540
+ category&.min_applicant_endorsements.to_i
541
+ end
542
+
524
543
  # Files Step
525
544
  def min_applicant_files
526
545
  category&.min_applicant_files.to_i
@@ -536,6 +555,10 @@ module EffectiveMembershipsApplicant
536
555
  )
537
556
  end
538
557
 
558
+ def applicant_endorsements_required?
559
+ min_applicant_endorsements > 0
560
+ end
561
+
539
562
  def applicant_references_required?
540
563
  min_applicant_references > 0
541
564
  end
@@ -543,13 +566,18 @@ module EffectiveMembershipsApplicant
543
566
  # When an application is submitted, these must be done to go to completed.
544
567
  # An Admin can override this and just set them to completed.
545
568
  def completed_requirements
546
- if category&.applicant_wizard_steps&.include?(:references) || applicant_references_required?
547
- {
548
- 'Applicant References' => (!applicant_references_required? || applicant_references.count(&:completed?) >= min_applicant_references)
549
- }
550
- else
551
- {}
569
+ requirements = {}
570
+ return requirements unless category.present?
571
+
572
+ if category.applicant_wizard_steps.include?(:references) || applicant_references_required?
573
+ requirements['Applicant References'] = (!applicant_references_required? || applicant_references.count(&:completed?) >= min_applicant_references)
552
574
  end
575
+
576
+ if category.applicant_wizard_steps.include?(:endorsements) || applicant_endorsements_required?
577
+ requirements['Applicant Endorsements'] = (!applicant_endorsements_required? || applicant_endorsements.count(&:completed?) >= min_applicant_endorsements)
578
+ end
579
+
580
+ requirements
553
581
  end
554
582
 
555
583
  def complete!
@@ -129,7 +129,6 @@ module EffectiveMembershipsFeePayment
129
129
 
130
130
  fee_payment_steps.delete(:organization) unless organization?
131
131
 
132
-
133
132
  wizard_steps.select do |step|
134
133
  required_steps.include?(step) || category.blank? || fee_payment_steps.include?(step)
135
134
  end
@@ -62,6 +62,11 @@ module EffectiveMembershipsOrganization
62
62
  scope :deep, -> { includes(:representatives) }
63
63
  scope :sorted, -> { order(:title) }
64
64
 
65
+ scope :membership_applying, -> {
66
+ applicants = EffectiveMemberships.Applicant.all
67
+ without_role(:member).where(id: applicants.select(:organization_id))
68
+ }
69
+
65
70
  validates :title, presence: true, uniqueness: true
66
71
  validates :email, presence: true
67
72
  end
@@ -37,6 +37,22 @@ module EffectiveMembershipsOwner
37
37
  accepts_nested_attributes_for :membership_histories
38
38
 
39
39
  scope :members, -> { joins(:membership) }
40
+
41
+ scope :membership_removed, -> {
42
+ removed = Effective::MembershipHistory.removed.where(owner_type: name)
43
+ without_role(:member).where(id: removed.select(:owner_id))
44
+ }
45
+
46
+ scope :membership_bad_standing, -> {
47
+ bad_standing = Effective::Membership.where(bad_standing: true).where(owner_type: name)
48
+ where(id: bad_standing.select(:owner_id))
49
+ }
50
+
51
+ scope :membership_renewed_this_period, -> {
52
+ with_paid_fees_through = Effective::Membership.with_paid_fees_through.where(owner_type: name)
53
+ where(id: with_paid_fees_through.select(:owner_id))
54
+ }
55
+
40
56
  end
41
57
 
42
58
  def assign_member_role
@@ -181,8 +197,8 @@ module EffectiveMembershipsOwner
181
197
  # Build the renewal fee
182
198
  fee ||= fees.build()
183
199
 
184
- late_on ||= EffectiveMemberships.Registrar.late_fee_date(period: period)
185
- bad_standing_on ||= EffectiveMemberships.Registrar.bad_standing_date(period: period)
200
+ late_on ||= EffectiveMemberships.Registrar.late_fee_date(period: period) if category.create_late_fees?
201
+ bad_standing_on ||= EffectiveMemberships.Registrar.bad_standing_date(period: period) if category.create_bad_standing?
186
202
 
187
203
  fee.assign_attributes(
188
204
  fee_type: 'Renewal',
@@ -284,52 +284,53 @@ module EffectiveMembershipsRegistrar
284
284
  retval
285
285
  end
286
286
 
287
-
288
287
  # This is intended to be run once per day in a rake task
289
288
  # rake effective_memberships:create_fees
290
289
  # Create Renewal and Late fees
291
- def create_fees!(period: nil, late_on: nil, bad_standing_on: nil)
292
- # The current period, based on Time.zone.now
290
+ def create_fees!(period: nil)
293
291
  period ||= current_period
294
- late_on ||= late_fee_date(period: period)
295
- bad_standing_on ||= bad_standing_date(period: period)
292
+ memberships = Effective::Membership.deep.with_unpaid_fees_through(period)
296
293
 
297
294
  # Create Renewal Fees
298
- Effective::Membership.deep.with_unpaid_fees_through(period).find_each do |membership|
299
- membership.categories.select(&:create_renewal_fees?).map do |category|
300
- existing = membership.owner.membership_period_fee(category: category, period: period, except: 'Renewal')
301
- next if existing.present? # This might be an existing Prorated fee
295
+ memberships.find_each { |membership| create_renewal_fees!(membership, period: period) }
296
+ GC.start
302
297
 
303
- fee = membership.owner.build_renewal_fee(category: category, period: period, late_on: late_on, bad_standing_on: bad_standing_on)
304
- raise("expected build_renewal_fee to return a fee for period #{period}") unless fee.kind_of?(Effective::Fee)
305
- next if fee.purchased?
298
+ # Create Late Fees
299
+ memberships.find_each { |membership| create_late_fees!(membership, period: period) }
300
+ GC.start
306
301
 
307
- puts("Created renewal fee for #{membership.owner}") if fee.new_record? && !Rails.env.test?
302
+ # Update Membership Status - Assign In Bad Standing
303
+ memberships.find_each { |membership| update_membership_status!(membership, period: period) }
304
+ GC.start
308
305
 
309
- fee.save!
310
- end
311
- end
306
+ true
307
+ end
312
308
 
313
- GC.start
309
+ def create_renewal_fees!(membership, period:)
310
+ membership.categories.select(&:create_renewal_fees?).map do |category|
311
+ existing = membership.owner.membership_period_fee(category: category, period: period, except: 'Renewal')
312
+ next if existing.present? # This might be an existing Prorated fee
314
313
 
315
- # Create Late Fees
316
- Effective::Membership.deep.with_unpaid_fees_through(period).find_each do |membership|
317
- membership.categories.select(&:create_late_fees?).map do |category|
318
- fee = membership.owner.build_late_fee(category: category, period: period)
319
- next if fee.blank? || fee.purchased?
314
+ fee = membership.owner.build_renewal_fee(category: category, period: period)
315
+ raise("expected build_renewal_fee to return a fee for period #{period}") unless fee.kind_of?(Effective::Fee)
316
+ next if fee.purchased?
320
317
 
321
- fee.save!
322
- end
323
- end
318
+ puts("Created renewal fee for #{membership.owner}") if fee.new_record? && !Rails.env.test?
324
319
 
325
- GC.start
320
+ fee.save!
321
+ end
322
+ end
326
323
 
327
- # Update Membership Status - Assign In Bad Standing
328
- Effective::Membership.deep.with_unpaid_fees_through(period).find_each do |membership|
329
- membership.owner.update_membership_status!
324
+ def create_late_fees!(membership, period:)
325
+ membership.categories.select(&:create_late_fees?).map do |category|
326
+ fee = membership.owner.build_late_fee(category: category, period: period)
327
+ next if fee.blank? || fee.purchased?
328
+ fee.save!
330
329
  end
330
+ end
331
331
 
332
- true
332
+ def update_membership_status!(membership, period: nil)
333
+ membership.owner.update_membership_status!
333
334
  end
334
335
 
335
336
  # Called in the after_purchase of fee payment
@@ -27,6 +27,11 @@ module EffectiveMembershipsUser
27
27
  class_name: 'Effective::Representative', inverse_of: :user, dependent: :delete_all
28
28
 
29
29
  accepts_nested_attributes_for :representatives, allow_destroy: true
30
+
31
+ scope :membership_applying, -> {
32
+ applicants = EffectiveMemberships.Applicant.all
33
+ without_role(:member).where(id: applicants.select(:user_id))
34
+ }
30
35
  end
31
36
 
32
37
  # Instance Methods
@@ -0,0 +1,100 @@
1
+ # An ApplicantEndorsement is similar to a reference, except it must be an existing member
2
+
3
+ module Effective
4
+ class ApplicantEndorsement < ActiveRecord::Base
5
+ acts_as_tokened
6
+
7
+ acts_as_statused(
8
+ :submitted, # Was submitted by the applicant
9
+ :completed # Was completed by the endorser.
10
+ )
11
+
12
+ log_changes(to: :applicant) if respond_to?(:log_changes)
13
+
14
+ belongs_to :applicant, polymorphic: true
15
+ belongs_to :endorser, polymorphic: true, optional: true
16
+
17
+ effective_resource do
18
+ # These fields are submitted by the applicant
19
+ # They should try to select an endorser from the dropdown
20
+ # But we also allow a fallback
21
+ unknown_member :boolean
22
+ endorser_email :string
23
+ name :string
24
+ phone :string
25
+
26
+ # As per acts_as_statused. For tracking the state machine.
27
+ status :string
28
+ status_steps :text
29
+
30
+ # Endorser Declaration
31
+ notes :text
32
+ accept_declaration :boolean
33
+
34
+ # Tracking the submission
35
+ token :string
36
+ last_notified_at :datetime
37
+
38
+ timestamps
39
+ end
40
+
41
+ scope :deep, -> { all }
42
+
43
+ # All step validations
44
+ validates :applicant, presence: true
45
+ validates :endorser, presence: true, unless: -> { unknown_member? }
46
+
47
+ with_options(if: -> { unknown_member? }) do
48
+ validates :endorser_email, presence: true, email: true
49
+ validates :name, presence: true
50
+ validates :phone, presence: true
51
+ end
52
+
53
+ # When being submit by the reference
54
+ with_options(if: -> { completed? }) do
55
+ validates :accept_declaration, acceptance: true
56
+ end
57
+
58
+ after_commit(on: :create, if: -> { applicant.was_submitted? }) { notify! }
59
+
60
+ def self.endorser_collection(applicant)
61
+ raise('expected an effective memberships applicant') unless applicant.class.try(:effective_memberships_applicant?)
62
+
63
+ collection_method = EffectiveMemberships.applicant_endorsements_endorser_collection()
64
+
65
+ if collection_method.blank?
66
+ return (applicant.user.class.members)
67
+ end
68
+
69
+ collection = instance_exec(applicant, &collection_method)
70
+
71
+ unless collection.kind_of?(ActiveRecord::Relation)
72
+ raise("expected EffectiveMemberships.applicant_endorsements_endorsers_collection to return an ActiveRecord::Relation.")
73
+ end
74
+
75
+ collection
76
+ end
77
+
78
+ def to_s
79
+ 'endorsement'
80
+ end
81
+
82
+ def email
83
+ endorser&.email || endorser_email
84
+ end
85
+
86
+ def notify!
87
+ raise('expected endorsement email') unless email.present?
88
+
89
+ EffectiveMemberships.send_email(:applicant_endorsement_notification, self)
90
+ update!(last_notified_at: Time.zone.now)
91
+ end
92
+
93
+ def complete!
94
+ completed!
95
+ applicant.save!
96
+ end
97
+
98
+ end
99
+
100
+ end
@@ -38,8 +38,9 @@ module Effective
38
38
  }
39
39
 
40
40
  scope :with_unpaid_fees_through, -> (period = nil) {
41
- where(arel_table[:fees_paid_period].lt(period || EffectiveMemberships.Registrar.current_period))
42
- .or(where(fees_paid_period: nil))
41
+ joined = where(arel_table[:joined_on].lt(period || EffectiveMemberships.Registrar.current_period))
42
+ unpaid = where(arel_table[:fees_paid_period].lt(period || EffectiveMemberships.Registrar.current_period)).or(where(fees_paid_period: nil))
43
+ joined.merge(unpaid)
43
44
  }
44
45
 
45
46
  before_validation do
@@ -119,7 +120,24 @@ module Effective
119
120
  end
120
121
 
121
122
  def fees_paid?
122
- fees_paid_period == EffectiveMemberships.Registrar.current_period
123
+ paid_fees_through?(EffectiveMemberships.Registrar.current_period)
124
+ end
125
+
126
+ def paid_fees_through?(period = nil)
127
+ period ||= EffectiveMemberships.Registrar.current_period
128
+
129
+ return false if fees_paid_period.blank?
130
+ fees_paid_period >= period
131
+ end
132
+
133
+ def unpaid_fees_through?(period = nil)
134
+ period ||= EffectiveMemberships.Registrar.current_period
135
+
136
+ return false if joined_on.blank?
137
+ return false unless joined_on < period
138
+
139
+ return true if fees_paid_period.blank?
140
+ fees_paid_period < period
123
141
  end
124
142
 
125
143
  def change_fees_paid_period
@@ -127,6 +145,10 @@ module Effective
127
145
  end
128
146
 
129
147
  def change_fees_paid_period=(date)
148
+ if date.blank?
149
+ return assign_attributes(fees_paid_period: nil, fees_paid_through_period: nil)
150
+ end
151
+
130
152
  date = (date.respond_to?(:strftime) ? date : Date.parse(date))
131
153
 
132
154
  period = EffectiveMemberships.Registrar.period(date: date)
@@ -135,5 +157,20 @@ module Effective
135
157
  assign_attributes(fees_paid_period: period, fees_paid_through_period: period_end_on)
136
158
  end
137
159
 
160
+ # Admin updating membership info
161
+ def revise!
162
+ save!
163
+
164
+ period = EffectiveMemberships.Registrar.current_period
165
+ return true if paid_fees_through?(period)
166
+
167
+ # Otherwise build fees right now
168
+ EffectiveMemberships.Registrar.create_renewal_fees!(self, period: period)
169
+ EffectiveMemberships.Registrar.create_late_fees!(self, period: period)
170
+ EffectiveMemberships.Registrar.update_membership_status!(self, period: period)
171
+
172
+ true
173
+ end
174
+
138
175
  end
139
176
  end
@@ -25,6 +25,8 @@ module Effective
25
25
  scope :deep, -> { includes(:owner) }
26
26
  scope :sorted, -> { order(:start_on) }
27
27
 
28
+ scope :removed, -> { where(removed: true) }
29
+
28
30
  validates :owner, presence: true
29
31
 
30
32
  # validates :categories, presence: true, unless: -> { removed? }
@@ -0,0 +1 @@
1
+ = render '/effective/applicant_endorsements/applicant_endorsement', applicant_endorsement: applicant_endorsement, skip_actions: true
@@ -25,6 +25,10 @@
25
25
  = tab 'References' do
26
26
  .mb-4= render_inline_datatable(Admin::EffectiveApplicantReferencesDatatable.new(applicant: applicant))
27
27
 
28
+ - if applicant.applicant_endorsements.present?
29
+ = tab 'Endorsements' do
30
+ .mb-4= render_inline_datatable(Admin::EffectiveApplicantEndorsementsDatatable.new(applicant: applicant))
31
+
28
32
  - if applicant.fees.present? || applicant.orders.present?
29
33
  = tab 'Fees' do
30
34
  .mb-4
@@ -32,6 +32,12 @@
32
32
  %td= f.number_field :min_applicant_experiences_months, label: false
33
33
  %td minimimum months of work experience
34
34
 
35
+ - if applicant.wizard_step_keys.include?(:endorsements)
36
+ %tr
37
+ %td= applicant.wizard_step_title(:endorsements)
38
+ %td= f.number_field :min_applicant_endorsements, label: false
39
+ %td minimimum number of endorsements
40
+
35
41
  - if applicant.wizard_step_keys.include?(:references)
36
42
  %tr
37
43
  %td= applicant.wizard_step_title(:references)
@@ -1 +1,5 @@
1
+ .card
2
+ .card-body= render 'effective/fee_payments/summary', fee_payment: fee_payment
3
+
1
4
  = render 'effective/fee_payments/fee_payment', fee_payment: fee_payment
5
+ = render 'effective/fee_payments/orders', fee_payment: fee_payment
@@ -3,9 +3,8 @@
3
3
  = f.hidden_field :owner_id
4
4
  = f.hidden_field :owner_type
5
5
  - else
6
- - raise('todo')
7
- - collection = EffectiveMembershipsOwner.descendants.map { |d| [d.name.to_s, d.members.sorted] }.to_h
8
- = f.select :owner_id, collection, polymorphic: true
6
+ - collection = { 'Users' => current_user.class.sorted, 'Organizations' => EffectiveMemberships.Organization.sorted }
7
+ = f.select :owner, collection, polymorphic: true
9
8
 
10
9
  - if f.object.new_record?
11
10
  = f.select :fee_type, EffectiveMemberships.custom_fee_types
@@ -21,7 +21,7 @@
21
21
  - periods = registrar.periods(from: f.object.joined_on)
22
22
  - collection = periods.reverse.map { |period| [registrar.period_end_on(date: period), period] }
23
23
 
24
- = f.select :change_fees_paid_period, collection, label: 'Fees Paid Through', hint: 'Which period this user has fees paid through. Can change how renewal fees are created.'
24
+ = f.select :change_fees_paid_period, collection, label: 'Fees Paid Through', hint: 'Which period this user has fees paid through. Determines which renewal fees should be created. Setting this to a past or blank period may create a renewal fee in the current period.'
25
25
 
26
26
  %p.text-muted To update the current membership categories, use the 'Assign' or 'Reclassify' actions below
27
27
 
@@ -0,0 +1,12 @@
1
+ - user = (resource.respond_to?(:email) ? resource : resource.user)
2
+
3
+ = user.to_s
4
+
5
+ - if user.email.present?
6
+ %br
7
+ = mail_to user.email
8
+
9
+ - if user.membership.present?
10
+ %br
11
+ = '#' + user.membership.number
12
+ #{user.membership.categories.to_sentence}
@@ -0,0 +1,46 @@
1
+ - endorsement = applicant_endorsement
2
+
3
+ %table.table.table-sm
4
+ %tbody
5
+ %tr
6
+ %th Endorser
7
+ %td
8
+ - if endorsement.endorser.present?
9
+ - endorser = endorsement.endorser
10
+ = link_to("#{endorser.to_s} <#{endorser.email}>", "mailto:#{endorser.email}")
11
+ - else
12
+ = link_to("#{endorsement.name} <#{endorsement.email}>", "mailto:#{endorsement.email}")
13
+ %td
14
+
15
+ - if endorsement.phone.present?
16
+ %tr
17
+ %th Phone
18
+ %td= endorsement.phone
19
+
20
+ - if endorsement.applicant.was_submitted?
21
+ %tr
22
+ %th Status
23
+ %td
24
+ - if endorsement.submitted?
25
+ Waiting on response
26
+ - elsif endorsement.completed?
27
+ Response completed
28
+
29
+ - unless endorsement.applicant.was_approved?
30
+ %tr
31
+ %th Last Notified at
32
+ %td= endorsement.last_notified_at&.strftime('%F') || 'Never'
33
+
34
+ - if can?(:notify, endorsement) && !local_assigns[:skip_actions]
35
+ %tr
36
+ %th Notification
37
+ %td= link_to('Resend email notification', effective_memberships.notify_applicant_endorsement_path(endorsement), 'data-method': :post, 'data-confirm': "Really resend email notification to #{endorsement.email}?")
38
+
39
+ - if endorsement.completed? && can?(:review, endorsement)
40
+ %tr
41
+ %th Notes
42
+ %td= endorsement.notes
43
+
44
+ %tr
45
+ %th Endorsed On
46
+ %td= endorsement.completed_at.strftime('%F')
@@ -0,0 +1,4 @@
1
+ = dropdown(variation: :dropleft) do
2
+ - unless applicant_endorsement.completed?
3
+ = dropdown_link_to 'Notify', effective_memberships.notify_applicant_endorsement_path(applicant_endorsement),
4
+ data: { method: :post, confirm: "Really notify #{applicant_endorsement.email}?" }
@@ -0,0 +1,9 @@
1
+ = effective_form_with(model: applicant_endorsement, engine: true) do |f|
2
+ = f.hidden_field :applicant_id
3
+ = f.hidden_field :applicant_type
4
+
5
+ - if f.object.new_record?
6
+ %p An email will be sent asking them to complete a endorsement declaration form.
7
+ = f.submit('Create and Notify endorser')
8
+ - else
9
+ = f.submit
@@ -0,0 +1,22 @@
1
+ = effective_form_with(model: applicant_endorsement, engine: true) do |f|
2
+ %h2 Applicant endorsement
3
+
4
+ %p
5
+ I endorse and sponser
6
+ = applicant_endorsement.applicant.owner
7
+ as a candidate for
8
+ = applicant_endorsement.applicant.category
9
+ membership and certify that I have personal knowledge of the professional ability
10
+ and character, methods of practice, and experience of the applicant.
11
+
12
+ %p
13
+ The candidate has satisfied the requirements for membership in this category,
14
+ and to the best of my personal knowledge,
15
+ the applicant observes and upholds the Code of Ethics and Professional Practice.
16
+
17
+ .mt-4
18
+
19
+ = f.text_area :notes, label: "Additional comments"
20
+ = f.check_box :accept_declaration, label: 'Yes, I declare that the above information is true and accurate'
21
+
22
+ = f.submit 'Complete Endorsement'
@@ -0,0 +1,3 @@
1
+ .card
2
+ .card-body
3
+ %p Thank you! The confidential endorsement has been completed.
@@ -0,0 +1,8 @@
1
+ .card
2
+ .card-body
3
+ %p
4
+ Please provide a endorsement for the applicant
5
+ = succeed('.') do
6
+ %strong= @applicant_endorsement.applicant.owner
7
+
8
+ = render('form_declaration', applicant_endorsement: @applicant_endorsement)
@@ -0,0 +1,15 @@
1
+ .card
2
+ .card-body
3
+ .row
4
+ .col-sm
5
+ %h5.card-title= applicant.wizard_step_title(:endorsements)
6
+ .col-sm-auto.text-right
7
+ = link_to('Edit', wizard_path(:endorsements)) if edit_effective_wizard?
8
+
9
+ - applicant.applicant_endorsements.each_with_index do |applicant_endorsement, index|
10
+ - if index > 0
11
+ .mb-4
12
+
13
+ %h6 Endorsement ##{index + 1}
14
+
15
+ = render('effective/applicant_endorsements/applicant_endorsement', applicant_endorsement: applicant_endorsement)
@@ -0,0 +1,25 @@
1
+ = render 'layout' do
2
+ = render 'effective/applicants/content', resource: resource
3
+
4
+ .card
5
+ .card-body
6
+ - if resource.min_applicant_endorsements > 0
7
+ %p You must include #{resource.min_applicant_endorsements} or more endorsements.
8
+
9
+ = effective_form_with(model: resource, url: wizard_path(step), method: :put) do |f|
10
+ = f.hidden_field :id
11
+
12
+ = f.has_many(:applicant_endorsements, cards: true) do |aef|
13
+ %h4 Endorsement
14
+
15
+ = aef.select :endorser, Effective::ApplicantEndorsement.endorser_collection(resource), required: false,
16
+ ajax_url: effective_memberships.select2_ajax_endorser_applicant_endorsement_path(applicant_id: resource)
17
+
18
+ = aef.check_box :unknown_member, label: 'I cant find my endorser in the above list'
19
+
20
+ = aef.show_if(:unknown_member, true) do
21
+ = aef.email_field :endorser_email, required: true
22
+ = aef.text_field :name, required: true
23
+ = aef.phone_field :phone, required: true
24
+
25
+ = f.save 'Save and Continue'
@@ -12,7 +12,7 @@
12
12
  = f.error :applicant_experiences_months
13
13
 
14
14
  = f.has_many(:applicant_experiences, cards: true) do |aef|
15
- %h4 Work Experience
15
+ %h4.mb-4 Work Experience
16
16
 
17
17
  .row
18
18
  .col= aef.text_field :employer
@@ -27,9 +27,14 @@
27
27
  = f.show_if(:category_id, mc.id) do
28
28
  .mb-4
29
29
  %h3= mc.to_s
30
- %small.text-muted #{mc.category} #{mc.category_type} Membership
30
+
31
+ - if organization_categories.present?
32
+ %small.text-muted #{mc.category} #{mc.category_type} Membership
33
+
31
34
  = mc.rich_text_body
32
35
 
36
+ = render_if_exists("effective/applicants/select/#{mc.to_s.parameterize.underscore}", f: f, category: mc)
37
+
33
38
  - if organization_categories.present?
34
39
  = f.show_if_any(:category_id, organization_categories.map(&:id)) do
35
40
  = render('effective/applicants/select_organization', f: f)
@@ -1,4 +1,4 @@
1
1
  - if fee_payment.submit_order&.purchased?
2
2
  .card.mb-4
3
3
  .card-body
4
- = render(fee_payment.submit_order)
4
+ = render('effective/orders/order', order: fee_payment.submit_order)
@@ -0,0 +1,15 @@
1
+ ---
2
+ subject: 'Your endorsement has been requested'
3
+ from: 'admin@example.com'
4
+ ---
5
+ Hello {{ endorsement.name }},
6
+
7
+ An applicant, {{ user.name }}, has requested your endorsement.
8
+
9
+ Would you please complete the endorsement declaration form at this url:
10
+
11
+ {{ endorsement.url }}
12
+
13
+ Thank you.
14
+
15
+ Please contact us for assistance.
@@ -24,6 +24,9 @@ EffectiveMemberships.setup do |config|
24
24
  # The defaults include: Applicant, Prorated, Renewal, Late, Admin
25
25
  # config.additional_fee_types = []
26
26
 
27
+ # Applicant Endorsements
28
+ # config.applicant_endorsements_endorser_collection = Proc.new { |applicant| applicant.user.class.members }
29
+
27
30
  # Applicant Reviews
28
31
  # When true, display the reviewed state and require Category.min_applicant_reviews
29
32
  # When false, hide the reviewed state entirely
data/config/routes.rb CHANGED
@@ -9,6 +9,11 @@ EffectiveMemberships::Engine.routes.draw do
9
9
  resources :build, controller: :applicants, only: [:show, :update]
10
10
  end
11
11
 
12
+ resources :applicant_endorsements, only: [:new, :create, :show, :update] do
13
+ get :select2_ajax_endorser, on: :member
14
+ post :notify, on: :member
15
+ end
16
+
12
17
  resources :applicant_references, only: [:new, :create, :show, :update] do
13
18
  post :notify, on: :member
14
19
  end
@@ -32,6 +37,10 @@ EffectiveMemberships::Engine.routes.draw do
32
37
  namespace :admin do
33
38
  resources :applicants, except: [:new, :create, :show]
34
39
 
40
+ resources :applicant_endorsements do
41
+ post :notify, on: :member
42
+ end
43
+
35
44
  resources :applicant_references do
36
45
  post :notify, on: :member
37
46
  end
@@ -24,6 +24,7 @@ class CreateEffectiveMemberships < ActiveRecord::Migration[6.0]
24
24
  t.integer :min_applicant_educations
25
25
  t.integer :min_applicant_experiences_months
26
26
  t.integer :min_applicant_references
27
+ t.integer :min_applicant_endorsements
27
28
  t.integer :min_applicant_courses
28
29
  t.integer :min_applicant_files
29
30
 
@@ -295,6 +296,35 @@ class CreateEffectiveMemberships < ActiveRecord::Migration[6.0]
295
296
  add_index :applicant_references, :applicant_id
296
297
  add_index :applicant_references, :token
297
298
 
299
+ # Applicant Endorsements
300
+ create_table :applicant_endorsements do |t|
301
+ t.integer :applicant_id
302
+ t.string :applicant_type
303
+
304
+ t.integer :endorser_id
305
+ t.string :endorser_type
306
+
307
+ t.boolean :unknown_member, default: false
308
+ t.string :endorser_email
309
+ t.string :name
310
+ t.string :phone
311
+
312
+ t.string :status
313
+ t.text :status_steps
314
+
315
+ t.text :notes
316
+ t.boolean :accept_declaration
317
+
318
+ t.string :token
319
+ t.datetime :last_notified_at
320
+
321
+ t.datetime :created_at
322
+ t.datetime :updated_at
323
+ end
324
+
325
+ add_index :applicant_endorsements, :applicant_id
326
+ add_index :applicant_endorsements, :token
327
+
298
328
  # Applicant Courses
299
329
  create_table :applicant_course_areas do |t|
300
330
  t.string :title
@@ -1,3 +1,3 @@
1
1
  module EffectiveMemberships
2
- VERSION = '0.4.7'
2
+ VERSION = '0.4.10'
3
3
  end
@@ -8,7 +8,7 @@ module EffectiveMemberships
8
8
  [
9
9
  :categories_table_name, :applicants_table_name, :applicant_reviews_table_name, :fee_payments_table_name, :organizations_table_name, :representatives_table_name,
10
10
  :category_class_name, :organization_class_name, :applicant_class_name, :applicant_review_class_name, :fee_payment_class_name, :registrar_class_name, :membership_card_class_name,
11
- :additional_fee_types, :applicant_reviews,
11
+ :additional_fee_types, :applicant_reviews, :applicant_endorsements_endorser_collection,
12
12
  :layout,
13
13
  :mailer, :parent_mailer, :deliver_method, :mailer_layout, :mailer_sender, :mailer_admin, :mailer_subject, :use_effective_email_templates
14
14
  ]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_memberships
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.7
4
+ version: 0.4.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-12 00:00:00.000000000 Z
11
+ date: 2022-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: timecop
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
181
195
  - !ruby/object:Gem::Dependency
182
196
  name: effective_test_bot
183
197
  requirement: !ruby/object:Gem::Requirement
@@ -242,6 +256,7 @@ files:
242
256
  - app/assets/stylesheets/effective_memberships/base.scss
243
257
  - app/controllers/admin/applicant_course_areas_controller.rb
244
258
  - app/controllers/admin/applicant_course_names_controller.rb
259
+ - app/controllers/admin/applicant_endorsements_controller.rb
245
260
  - app/controllers/admin/applicant_references_controller.rb
246
261
  - app/controllers/admin/applicants_controller.rb
247
262
  - app/controllers/admin/categories_controller.rb
@@ -252,6 +267,7 @@ files:
252
267
  - app/controllers/admin/organizations_controller.rb
253
268
  - app/controllers/admin/registrar_actions_controller.rb
254
269
  - app/controllers/admin/representatives_controller.rb
270
+ - app/controllers/effective/applicant_endorsements_controller.rb
255
271
  - app/controllers/effective/applicant_references_controller.rb
256
272
  - app/controllers/effective/applicants_controller.rb
257
273
  - app/controllers/effective/fee_payments_controller.rb
@@ -261,6 +277,7 @@ files:
261
277
  - app/controllers/effective/representatives_controller.rb
262
278
  - app/datatables/admin/effective_applicant_course_areas_datatable.rb
263
279
  - app/datatables/admin/effective_applicant_course_names_datatable.rb
280
+ - app/datatables/admin/effective_applicant_endorsements_datatable.rb
264
281
  - app/datatables/admin/effective_applicant_references_datatable.rb
265
282
  - app/datatables/admin/effective_applicants_datatable.rb
266
283
  - app/datatables/admin/effective_categories_datatable.rb
@@ -272,6 +289,7 @@ files:
272
289
  - app/datatables/admin/effective_representatives_datatable.rb
273
290
  - app/datatables/effective_applicant_courses_datatable.rb
274
291
  - app/datatables/effective_applicant_educations_datatable.rb
292
+ - app/datatables/effective_applicant_endorsements_datatable.rb
275
293
  - app/datatables/effective_applicant_experiences_datatable.rb
276
294
  - app/datatables/effective_applicant_references_datatable.rb
277
295
  - app/datatables/effective_applicants_datatable.rb
@@ -295,6 +313,7 @@ files:
295
313
  - app/models/effective/applicant_course_area.rb
296
314
  - app/models/effective/applicant_course_name.rb
297
315
  - app/models/effective/applicant_education.rb
316
+ - app/models/effective/applicant_endorsement.rb
298
317
  - app/models/effective/applicant_experience.rb
299
318
  - app/models/effective/applicant_reference.rb
300
319
  - app/models/effective/applicant_review.rb
@@ -313,6 +332,7 @@ files:
313
332
  - app/views/admin/applicant_course_areas/_form_applicant_course_area.html.haml
314
333
  - app/views/admin/applicant_course_areas/index.html.haml
315
334
  - app/views/admin/applicant_course_name/_form.html.haml
335
+ - app/views/admin/applicant_endorsements/_applicant_endorsement.html.haml
316
336
  - app/views/admin/applicant_references/_applicant_reference.html.haml
317
337
  - app/views/admin/applicants/_form.html.haml
318
338
  - app/views/admin/applicants/_form_approve.html.haml
@@ -352,6 +372,13 @@ files:
352
372
  - app/views/admin/registrar_actions/_form_remove.html.haml
353
373
  - app/views/admin/representatives/_form.html.haml
354
374
  - app/views/admin/representatives/_user_fields.html.haml
375
+ - app/views/admin/users/_col.html.haml
376
+ - app/views/effective/applicant_endorsements/_applicant_endorsement.html.haml
377
+ - app/views/effective/applicant_endorsements/_datatable_actions.html.haml
378
+ - app/views/effective/applicant_endorsements/_form.html.haml
379
+ - app/views/effective/applicant_endorsements/_form_declaration.html.haml
380
+ - app/views/effective/applicant_endorsements/complete.html.haml
381
+ - app/views/effective/applicant_endorsements/edit.html.haml
355
382
  - app/views/effective/applicant_references/_applicant_reference.html.haml
356
383
  - app/views/effective/applicant_references/_datatable_actions.html.haml
357
384
  - app/views/effective/applicant_references/_form.html.haml
@@ -365,6 +392,7 @@ files:
365
392
  - app/views/effective/applicants/_declarations.html.haml
366
393
  - app/views/effective/applicants/_demographics.html.haml
367
394
  - app/views/effective/applicants/_education.html.haml
395
+ - app/views/effective/applicants/_endorsements.html.haml
368
396
  - app/views/effective/applicants/_experience.html.haml
369
397
  - app/views/effective/applicants/_files.html.haml
370
398
  - app/views/effective/applicants/_layout.html.haml
@@ -382,6 +410,7 @@ files:
382
410
  - app/views/effective/applicants/declarations.html.haml
383
411
  - app/views/effective/applicants/demographics.html.haml
384
412
  - app/views/effective/applicants/education.html.haml
413
+ - app/views/effective/applicants/endorsements.html.haml
385
414
  - app/views/effective/applicants/experience.html.haml
386
415
  - app/views/effective/applicants/files.html.haml
387
416
  - app/views/effective/applicants/organization.html.haml
@@ -417,6 +446,7 @@ files:
417
446
  - app/views/effective/memberships_mailer/applicant_approved.liquid
418
447
  - app/views/effective/memberships_mailer/applicant_completed.liquid
419
448
  - app/views/effective/memberships_mailer/applicant_declined.liquid
449
+ - app/views/effective/memberships_mailer/applicant_endorsement_notification.liquid
420
450
  - app/views/effective/memberships_mailer/applicant_missing_info.liquid
421
451
  - app/views/effective/memberships_mailer/applicant_reference_notification.liquid
422
452
  - app/views/effective/organizations/_dashboard.html.haml