effective_work_experience 0.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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +188 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/config/effective_work_experience_manifest.js +3 -0
  6. data/app/assets/javascripts/effective_work_experience/base.js +0 -0
  7. data/app/assets/javascripts/effective_work_experience.js +1 -0
  8. data/app/assets/stylesheets/effective_work_experience/base.scss +7 -0
  9. data/app/assets/stylesheets/effective_work_experience.scss +1 -0
  10. data/app/controllers/admin/work_experience_categories_controller.rb +19 -0
  11. data/app/controllers/admin/work_experience_projects_controller.rb +19 -0
  12. data/app/controllers/admin/work_experience_records_controller.rb +19 -0
  13. data/app/controllers/admin/work_experience_reports_controller.rb +21 -0
  14. data/app/controllers/admin/work_experience_subcategories_controller.rb +19 -0
  15. data/app/controllers/admin/work_experience_summaries_controller.rb +19 -0
  16. data/app/controllers/effective/work_experience_projects_controller.rb +17 -0
  17. data/app/controllers/effective/work_experience_records_controller.rb +17 -0
  18. data/app/controllers/effective/work_experience_reports_controller.rb +19 -0
  19. data/app/controllers/effective/work_experience_summaries_controller.rb +17 -0
  20. data/app/datatables/admin/effective_work_experience_categories_datatable.rb +22 -0
  21. data/app/datatables/admin/effective_work_experience_projects_datatable.rb +24 -0
  22. data/app/datatables/admin/effective_work_experience_records_datatable.rb +37 -0
  23. data/app/datatables/admin/effective_work_experience_reports_datatable.rb +34 -0
  24. data/app/datatables/admin/effective_work_experience_subcategories_datatable.rb +24 -0
  25. data/app/datatables/admin/effective_work_experience_summaries_datatable.rb +57 -0
  26. data/app/datatables/effective_work_experience_projects_datatable.rb +38 -0
  27. data/app/datatables/effective_work_experience_records_datatable.rb +61 -0
  28. data/app/datatables/effective_work_experience_reports_review_datatable.rb +15 -0
  29. data/app/datatables/effective_work_experience_summaries_datatable.rb +49 -0
  30. data/app/datatables/effective_work_experience_summaries_review_datatable.rb +43 -0
  31. data/app/helpers/effective_work_experience_helper.rb +46 -0
  32. data/app/mailers/effective/work_experience_mailer.rb +39 -0
  33. data/app/models/concerns/effective_work_experience_summary.rb +264 -0
  34. data/app/models/concerns/effective_work_experience_user.rb +135 -0
  35. data/app/models/effective/work_experience_category.rb +29 -0
  36. data/app/models/effective/work_experience_entry.rb +43 -0
  37. data/app/models/effective/work_experience_outside_mentor.rb +35 -0
  38. data/app/models/effective/work_experience_project.rb +38 -0
  39. data/app/models/effective/work_experience_record.rb +99 -0
  40. data/app/models/effective/work_experience_report.rb +50 -0
  41. data/app/models/effective/work_experience_subcategory.rb +31 -0
  42. data/app/models/effective/work_experience_summary.rb +7 -0
  43. data/app/views/admin/work_experience/_user_fields.html.haml +17 -0
  44. data/app/views/admin/work_experience/_user_work_experience.html.haml +21 -0
  45. data/app/views/admin/work_experience_categories/_form.html.haml +7 -0
  46. data/app/views/admin/work_experience_categories/_form_work_experience_category.html.haml +5 -0
  47. data/app/views/admin/work_experience_projects/_form.html.haml +11 -0
  48. data/app/views/admin/work_experience_projects/_work_experience_project.html.haml +1 -0
  49. data/app/views/admin/work_experience_records/_form.html.haml +12 -0
  50. data/app/views/admin/work_experience_records/_work_experience_record.html.haml +1 -0
  51. data/app/views/admin/work_experience_reports/show.html.haml +2 -0
  52. data/app/views/admin/work_experience_subcategories/_form.html.haml +7 -0
  53. data/app/views/admin/work_experience_subcategories/_form_work_experience_subcategory.html.haml +10 -0
  54. data/app/views/admin/work_experience_summaries/_form.html.haml +7 -0
  55. data/app/views/admin/work_experience_summaries/_form_work_experience_summary.html.haml +32 -0
  56. data/app/views/admin/work_experience_summaries/_work_experience_summary.html.haml +1 -0
  57. data/app/views/effective/work_experience/_dashboard_intern.html.haml +73 -0
  58. data/app/views/effective/work_experience/_dashboard_mentor.html.haml +28 -0
  59. data/app/views/effective/work_experience_mailer/work_experience_summary_reviewed.liquid +13 -0
  60. data/app/views/effective/work_experience_mailer/work_experience_summary_submitted.liquid +15 -0
  61. data/app/views/effective/work_experience_outside_mentors/_fields.html.haml +5 -0
  62. data/app/views/effective/work_experience_projects/_fields.html.haml +7 -0
  63. data/app/views/effective/work_experience_projects/_form.html.haml +3 -0
  64. data/app/views/effective/work_experience_projects/_work_experience_project.html.haml +2 -0
  65. data/app/views/effective/work_experience_records/_fields.html.haml +52 -0
  66. data/app/views/effective/work_experience_records/_form.html.haml +5 -0
  67. data/app/views/effective/work_experience_records/_work_experience_record.html.haml +63 -0
  68. data/app/views/effective/work_experience_reports/_work_experience_report.html.haml +103 -0
  69. data/app/views/effective/work_experience_reports/show.html.haml +2 -0
  70. data/app/views/effective/work_experience_summaries/_content.html.haml +1 -0
  71. data/app/views/effective/work_experience_summaries/_layout.html.haml +3 -0
  72. data/app/views/effective/work_experience_summaries/_projects.html.haml +5 -0
  73. data/app/views/effective/work_experience_summaries/_projects_datatable.html.haml +2 -0
  74. data/app/views/effective/work_experience_summaries/_projects_fields.html.haml +1 -0
  75. data/app/views/effective/work_experience_summaries/_records.html.haml +65 -0
  76. data/app/views/effective/work_experience_summaries/_records_datatable.html.haml +2 -0
  77. data/app/views/effective/work_experience_summaries/_records_fields.html.haml +1 -0
  78. data/app/views/effective/work_experience_summaries/_review.html.haml +4 -0
  79. data/app/views/effective/work_experience_summaries/_review_fields.html.haml +4 -0
  80. data/app/views/effective/work_experience_summaries/_reviewed.html.haml +1 -0
  81. data/app/views/effective/work_experience_summaries/_start_fields.html.haml +1 -0
  82. data/app/views/effective/work_experience_summaries/_summary.html.haml +79 -0
  83. data/app/views/effective/work_experience_summaries/_user.html.haml +7 -0
  84. data/app/views/effective/work_experience_summaries/_work_experience_summary.html.haml +7 -0
  85. data/app/views/effective/work_experience_summaries/projects.html.haml +14 -0
  86. data/app/views/effective/work_experience_summaries/records.html.haml +14 -0
  87. data/app/views/effective/work_experience_summaries/review.html.haml +13 -0
  88. data/app/views/effective/work_experience_summaries/reviewed.html.haml +11 -0
  89. data/app/views/effective/work_experience_summaries/start.html.haml +16 -0
  90. data/app/views/effective/work_experience_summaries/submit.html.haml +10 -0
  91. data/app/views/effective/work_experience_summaries/submitted.html.haml +19 -0
  92. data/config/effective_work_experience.rb +34 -0
  93. data/config/locales/effective_work_experience.en.yml +94 -0
  94. data/config/routes.rb +28 -0
  95. data/db/migrate/101_create_effective_work_experience.rb +120 -0
  96. data/db/seeds.rb +40 -0
  97. data/lib/effective_work_experience/engine.rb +19 -0
  98. data/lib/effective_work_experience/version.rb +3 -0
  99. data/lib/effective_work_experience.rb +34 -0
  100. data/lib/generators/effective_work_experience/install_generator.rb +24 -0
  101. data/lib/generators/templates/effective_work_experience_mailer_preview.rb +4 -0
  102. data/lib/tasks/effective_work_experience_tasks.rake +8 -0
  103. metadata +325 -0
@@ -0,0 +1,38 @@
1
+ # My work experience projects
2
+ class EffectiveWorkExperienceProjectsDatatable < Effective::Datatable
3
+ datatable do
4
+ order :start_on
5
+
6
+ col :updated_at, visible: false
7
+ col :created_at, visible: false
8
+ col :id, visible: false
9
+
10
+ col :start_on
11
+ col :end_on
12
+
13
+ col :name
14
+ col :description
15
+
16
+ unless attributes[:skip_actions]
17
+ actions_col
18
+ end
19
+ end
20
+
21
+ collection do
22
+ scope = Effective::WorkExperienceProject.deep.all
23
+ scope = scope.where(user_id: attributes[:user_id], user_type: attributes[:user_type]) if attributes[:user_id].present?
24
+
25
+ if work_experience_summary.present?
26
+ scope = scope.during(work_experience_summary.months)
27
+ end
28
+
29
+ scope
30
+ end
31
+
32
+ def work_experience_summary
33
+ @work_experience_summary ||= if attributes[:work_experience_summary_id].present?
34
+ EffectiveWorkExperience.WorkExperienceSummary.find_by_id(attributes[:work_experience_summary_id])
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,61 @@
1
+ # My work experience records
2
+ class EffectiveWorkExperienceRecordsDatatable < Effective::Datatable
3
+ datatable do
4
+ order :month, :desc
5
+
6
+ col :updated_at, visible: false
7
+ col :created_at, visible: false
8
+ col :id, visible: false
9
+
10
+ col(:month) do |work_experience_record|
11
+ work_experience_record.month&.strftime('%F') || 'Backdated'
12
+ end
13
+
14
+ col(:total_hours, label: 'Hours') do |work_experience_record|
15
+ work_experience_hours_to_s(work_experience_record.total_hours)
16
+ end.aggregate do |work_experience_records|
17
+ total = work_experience_records.sum { |work_experience_record| work_experience_record.total_hours }
18
+ work_experience_hours_to_s(total)
19
+ end
20
+
21
+ col(:status) do |work_experience_record|
22
+ work_experience_record.status unless work_experience_record.backdated?
23
+ end
24
+
25
+ col(:submitted_at, label: 'Submitted') do |work_experience_record|
26
+ unless work_experience_record.backdated?
27
+ work_experience_record.submitted_at&.strftime('%F') || 'Not yet submitted'
28
+ end
29
+ end
30
+
31
+ col(:reviewed_at, label: 'Reviewed', as: :date) do |work_experience_record|
32
+ unless work_experience_record.backdated?
33
+ work_experience_record.reviewed_at&.strftime('%F') || 'Not yet reviewed'
34
+ end
35
+ end
36
+
37
+ aggregate :total
38
+
39
+ unless attributes[:skip_actions]
40
+ actions_col
41
+ end
42
+ end
43
+
44
+ collection do
45
+ scope = Effective::WorkExperienceRecord.deep.all
46
+ scope = scope.where(user_id: attributes[:user_id], user_type: attributes[:user_type]) if attributes[:user_id].present?
47
+
48
+ if work_experience_summary.present?
49
+ scope = scope.during(work_experience_summary.months)
50
+ end
51
+
52
+ scope
53
+ end
54
+
55
+ def work_experience_summary
56
+ @work_experience_summary ||= if attributes[:work_experience_summary_id].present?
57
+ EffectiveWorkExperience.WorkExperienceSummary.find_by_id(attributes[:work_experience_summary_id])
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,15 @@
1
+ # The work experience reports of the interns I am a mentor for
2
+ class EffectiveWorkExperienceReportsReviewDatatable < Effective::Datatable
3
+ datatable do
4
+ col :user, label: 'Intern'
5
+
6
+ actions_col do |user|
7
+ dropdown_link_to("Show #{et(Effective::WorkExperienceReport)}", effective_work_experience.work_experience_report_path(user))
8
+ end
9
+ end
10
+
11
+ collection do
12
+ current_user.work_experience_mentees.map { |user| [user, user] }
13
+ end
14
+
15
+ end
@@ -0,0 +1,49 @@
1
+ # My work experience summaries
2
+ class EffectiveWorkExperienceSummariesDatatable < Effective::Datatable
3
+ datatable do
4
+ order :created_at
5
+
6
+ col :token, visible: false
7
+ col :created_at, visible: false
8
+
9
+ col :start_on
10
+ col :end_on, visible: false
11
+ col :period
12
+
13
+ col :mentor
14
+
15
+ col(:total_hours, label: 'Hours') do |work_experience_summary|
16
+ work_experience_hours_to_s(work_experience_summary.total_hours)
17
+ end
18
+
19
+ col(:total_hours_to_date, as: :decimal) do |work_experience_summary|
20
+ work_experience_hours_to_s(work_experience_summary.total_hours_to_date)
21
+ end
22
+
23
+ col :status
24
+
25
+ col(:submitted_at, label: 'Submitted') do |work_experience_summary|
26
+ work_experience_summary.submitted_at&.strftime('%F') || 'Incomplete'
27
+ end
28
+
29
+ col(:reviewed_at, label: 'Reviewed', as: :date) do |work_experience_summary|
30
+ work_experience_summary.reviewed_at&.strftime('%F') || 'Not yet reviewed'
31
+ end
32
+
33
+ actions_col(show: false) do |work_experience_summary|
34
+ if work_experience_summary.draft?
35
+ dropdown_link_to('Continue', effective_work_experience.work_experience_summary_build_path(work_experience_summary, work_experience_summary.next_step), 'data-turbolinks' => false)
36
+ dropdown_link_to('Delete', effective_work_experience.work_experience_summary_path(work_experience_summary), 'data-confirm': "Really delete #{work_experience_summary}?", 'data-method': :delete)
37
+ else
38
+ dropdown_link_to('Show', effective_work_experience.work_experience_summary_path(work_experience_summary))
39
+ end
40
+ end
41
+ end
42
+
43
+ collection do
44
+ scope = EffectiveWorkExperience.WorkExperienceSummary.deep.all
45
+ scope = scope.where(user_id: attributes[:user_id], user_type: attributes[:user_type]) if attributes[:user_id].present?
46
+ scope
47
+ end
48
+
49
+ end
@@ -0,0 +1,43 @@
1
+ # The mentor's work experience summaries to review
2
+ class EffectiveWorkExperienceSummariesReviewDatatable < Effective::Datatable
3
+ datatable do
4
+ order :created_at
5
+
6
+ col :token, visible: false
7
+ col :created_at, visible: false
8
+
9
+ col(:submitted_at, label: 'Submitted') do |work_experience_summary|
10
+ work_experience_summary.submitted_at&.strftime('%F') || 'Incomplete'
11
+ end
12
+
13
+ col :user, label: 'Intern'
14
+ col :period
15
+ col :start_on, visible: false
16
+ col :end_on, visible: false
17
+
18
+ col(:total_hours) do |work_experience_summary|
19
+ work_experience_hours_to_s(work_experience_summary.total_hours)
20
+ end
21
+
22
+ col :status
23
+
24
+ col :recommendation, search: :string, visible: false
25
+ col :comments, visible: false
26
+
27
+ actions_col(show: false) do |work_experience_summary|
28
+ if work_experience_summary.submitted?
29
+ dropdown_link_to('Start Review', effective_work_experience.work_experience_summary_build_path(work_experience_summary, work_experience_summary.next_step), 'data-turbolinks' => false)
30
+ else
31
+ dropdown_link_to('Show', effective_work_experience.work_experience_summary_path(work_experience_summary))
32
+ dropdown_link_to('Review again', effective_work_experience.work_experience_summary_build_path(work_experience_summary, :review), 'data-turbolinks' => false)
33
+ end
34
+ end
35
+ end
36
+
37
+ collection do
38
+ scope = EffectiveWorkExperience.WorkExperienceSummary.deep.where.not(status: :draft)
39
+ scope = scope.where(mentor_id: attributes[:mentor_id], mentor_type: attributes[:mentor_type]) if attributes[:mentor_id].present?
40
+ scope
41
+ end
42
+
43
+ end
@@ -0,0 +1,46 @@
1
+ module EffectiveWorkExperienceHelper
2
+
3
+ # Display hours with one decimal place
4
+ def work_experience_hours_to_s(hours)
5
+ return if hours.blank?
6
+ "%0.1f" % hours
7
+ end
8
+
9
+ # Display a collection of every summary period going back 10 years
10
+ def work_experience_summary_start_on_collection(work_experience_summary = nil)
11
+ disabled = if work_experience_summary.present?
12
+ work_experience_summary.user.work_experience_summaries.where.not(id: work_experience_summary.id).pluck(:start_on)
13
+ end
14
+
15
+ disabled ||= []
16
+
17
+ date = Time.zone.now.beginning_of_quarter
18
+ end_date = (date - 10.years).beginning_of_year
19
+
20
+ collection = []
21
+
22
+ while date >= end_date
23
+ start_on = date.beginning_of_quarter
24
+ end_on = date.end_of_quarter
25
+
26
+ collection << [
27
+ "#{start_on.strftime('%B %Y')} to #{end_on.strftime('%B %Y')}",
28
+ date.strftime('%Y-%m-%d'),
29
+ disabled: disabled.include?(date)
30
+ ]
31
+
32
+ date -= 3.months
33
+ end
34
+
35
+ collection
36
+ end
37
+
38
+ def work_experience_summary_status_collection
39
+ EffectiveWorkExperience.WorkExperienceSummary::STATUSES
40
+ end
41
+
42
+ def work_experience_recommendation_collection
43
+ Array(EffectiveWorkExperience.recommendations)
44
+ end
45
+
46
+ end
@@ -0,0 +1,39 @@
1
+ module Effective
2
+ class WorkExperienceMailer < EffectiveWorkExperience.parent_mailer_class
3
+
4
+ include EffectiveMailer
5
+ include EffectiveEmailTemplatesMailer if EffectiveWorkExperience.use_effective_email_templates
6
+
7
+ def work_experience_summary_submitted(resource, opts = {})
8
+ @assigns = work_experience_summary_assigns(resource)
9
+ mail(to: resource.mentor.email, **headers_for(resource, opts))
10
+ end
11
+
12
+ def work_experience_summary_reviewed(resource, opts = {})
13
+ @assigns = work_experience_summary_assigns(resource)
14
+ mail(to: resource.user.email, **headers_for(resource, opts))
15
+ end
16
+
17
+ protected
18
+
19
+ def assigns_for(resource)
20
+ return work_experience_summary_assigns(resource) if resource.class.try(:effective_work_experience_summary?)
21
+ raise('unexpected resource')
22
+ end
23
+
24
+ def work_experience_summary_assigns(resource)
25
+ raise('expected a work experience summary') unless resource.class.try(:effective_work_experience_summary?)
26
+
27
+ user = resource.user
28
+ mentor = resource.mentor
29
+
30
+ {
31
+ user: { name: user.try(:email_to_s) || user.to_s, email: user.email },
32
+ mentor: { name: mentor.try(:email_to_s) || mentor.to_s, email: mentor.try(:email) },
33
+ period: resource.period,
34
+ url: link_to(effective_work_experience.work_experience_summary_url(resource))
35
+ }
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ # EffectiveWorkExperienceSummary
4
+ #
5
+ # Mark your model with effective_work_experience_summary to get all the includes
6
+ #
7
+ # The summary of an intern's work experience for one period, usually 3 months.
8
+ # Submitted by the intern, and reviewed by their mentor.
9
+
10
+ module EffectiveWorkExperienceSummary
11
+ extend ActiveSupport::Concern
12
+
13
+ module Base
14
+ def effective_work_experience_summary
15
+ include ::EffectiveWorkExperienceSummary
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ def effective_work_experience_summary?; true; end
21
+ end
22
+
23
+ included do
24
+ # The intern
25
+ belongs_to :user, polymorphic: true
26
+
27
+ # The mentor reviews the submitted work experience summary
28
+ belongs_to :mentor, polymorphic: true, optional: true
29
+
30
+ # The supervisor is responsible for the intern's day to day work
31
+ belongs_to :supervisor, polymorphic: true, optional: true
32
+
33
+ acts_as_tokened
34
+
35
+ log_changes(to: :user) if respond_to?(:log_changes)
36
+
37
+ acts_as_statused(
38
+ :draft, # In-progress submission
39
+ :submitted, # Submitted by intern
40
+ :reviewed # Reviewed by mentor
41
+ )
42
+
43
+ acts_as_wizard(
44
+ # Intern steps
45
+ start: 'Start',
46
+ records: 'Work Experience Hours',
47
+ projects: 'Work Experience Projects',
48
+ submit: 'Submit',
49
+
50
+ # Intern and mentor step
51
+ submitted: 'Submitted',
52
+
53
+ # Mentor steps
54
+ review: 'Mentor Review',
55
+ reviewed: 'Reviewed'
56
+ )
57
+
58
+ # The mentor review step
59
+ attr_accessor :approve_work_experience_summary
60
+
61
+ # Set to true when importing historic data. Skips sending emails.
62
+ attr_accessor :importing
63
+
64
+ effective_resource do
65
+ start_on :date
66
+ end_on :date
67
+
68
+ total_hours :decimal # The total number of hours worked this period
69
+
70
+ recommendation :string
71
+ comments :text # Private rolling comments displayed to the mentor
72
+
73
+ # Acts as Statused
74
+ status :string
75
+ status_steps :text
76
+
77
+ submitted_at :datetime
78
+ reviewed_at :datetime
79
+
80
+ # Acts as Wizard
81
+ wizard_steps :text, permitted: false
82
+
83
+ # Acts as Tokened
84
+ token :string
85
+
86
+ timestamps
87
+ end
88
+
89
+ scope :sorted, -> { order(:start_on) }
90
+ scope :deep, -> { includes(:user, :mentor, :supervisor) }
91
+
92
+ scope :in_progress, -> { where(status: :draft) }
93
+ scope :in_progress_for, ->(user) { where(user: user, status: :draft) }
94
+ scope :done, -> { where(status: [:submitted, :reviewed]) }
95
+
96
+ before_validation do
97
+ # Only assign from the user when they have the association. Otherwise leave whatever was assigned.
98
+ assign_attributes(mentor: user.work_experience_mentor) if user.respond_to?(:work_experience_mentor)
99
+ assign_attributes(supervisor: user.supervisor) if user.respond_to?(:supervisor)
100
+ end
101
+
102
+ before_validation do
103
+ # The admin forms assign the mentor and supervisor id without the polymorphic type
104
+ assign_attributes(mentor_type: (mentor_type.presence || user&.class&.name)) if mentor_id.present?
105
+ assign_attributes(supervisor_type: (supervisor_type.presence || user&.class&.name)) if supervisor_id.present?
106
+
107
+ assign_attributes(mentor_type: nil) if mentor_id.blank?
108
+ assign_attributes(supervisor_type: nil) if supervisor_id.blank?
109
+ end
110
+
111
+ before_validation do
112
+ assign_attributes(start_on: start_on&.beginning_of_quarter, end_on: start_on&.end_of_quarter)
113
+ assign_attributes(total_hours: calculate_total_hours)
114
+ end
115
+
116
+ before_validation do
117
+ assign_attributes(recommendation: recommendations.first) if approve_work_experience_summary
118
+ end
119
+
120
+ validates :start_on, presence: true, uniqueness: { scope: [:user_id, :user_type] }
121
+ validates :end_on, presence: true
122
+ validates :total_hours, numericality: { greater_than_or_equal_to: 0.0, allow_blank: true }
123
+
124
+ validate(if: -> { user.present? }) do
125
+ errors.add(:user, 'must have a mentor') unless mentor_present?
126
+ end
127
+
128
+ validate(if: -> { start_on.present? && end_on.present? }) do
129
+ errors.add(:end_on, 'must be after start date') unless end_on > start_on
130
+ errors.add(:end_on, "must be #{summary_months} months after start date") unless (end_on.month - start_on.month) == (summary_months - 1)
131
+ end
132
+
133
+ with_options(if: -> { current_step == :review }) do
134
+ validates :approve_work_experience_summary, acceptance: true
135
+ validates :recommendation, presence: true
136
+ end
137
+ end
138
+
139
+ def to_s
140
+ start_on&.strftime('%B %Y') || model_name.human
141
+ end
142
+
143
+ # The intern completes the first half of the wizard, the mentor completes the second half
144
+ def intern_steps
145
+ [:start, :records, :projects, :submit, :submitted, :reviewed]
146
+ end
147
+
148
+ def mentor_steps
149
+ [:submitted, :review, :reviewed]
150
+ end
151
+
152
+ def recommendations
153
+ Array(EffectiveWorkExperience.recommendations)
154
+ end
155
+
156
+ def summary_months
157
+ (EffectiveWorkExperience.summary_months || 3)
158
+ end
159
+
160
+ # The first day of each month in this summary period
161
+ def months
162
+ @months ||= start_on.all_quarter.to_a.select { |month| month.day == 1 }
163
+ end
164
+
165
+ def period
166
+ return if start_on.blank?
167
+ "#{months.first.strftime('%B %Y')} to #{months.last.strftime('%B %Y')}"
168
+ end
169
+
170
+ # An intern with an outside mentor has no mentor user. Their summaries are automatically reviewed.
171
+ def mentor_present?
172
+ mentor.present? || user.try(:work_experience_outside_mentor?).present?
173
+ end
174
+
175
+ def can_visit_step?(current_step)
176
+ return false if current_user == user && intern_steps.exclude?(current_step)
177
+ return false if current_user == mentor && mentor_steps.exclude?(current_step)
178
+
179
+ # Once submitted, the intern cannot go back and submit again
180
+ if was_submitted?
181
+ return false if [:start, :records, :projects, :submit].include?(current_step)
182
+ end
183
+
184
+ # Once reviewed, the mentor can go back and review again
185
+ if was_reviewed?
186
+ return [:review, :reviewed].include?(current_step)
187
+ end
188
+
189
+ can_revisit_completed_steps(current_step)
190
+ end
191
+
192
+ def total_hours_to_date
193
+ user.work_experience_total_hours_to_date(month: end_on)
194
+ end
195
+
196
+ def status_label
197
+ (status_was || status).to_s.gsub('_', ' ').titleize
198
+ end
199
+
200
+ def summary
201
+ case status_was
202
+ when 'draft'
203
+ "Work experience summary has not yet been submitted."
204
+ when 'submitted'
205
+ "Work experience summary has been submitted."
206
+ when 'reviewed'
207
+ "Work experience summary has been reviewed."
208
+ else
209
+ raise("unexpected status #{status}")
210
+ end.html_safe
211
+ end
212
+
213
+ def work_experience_subcategories
214
+ Effective::WorkExperienceSubcategory.all.sorted
215
+ end
216
+
217
+ def work_experience_records
218
+ @work_experience_records ||= user.work_experience_records.where(month: months)
219
+ end
220
+
221
+ def work_experience_projects
222
+ @work_experience_projects ||= user.work_experience_projects.during(months)
223
+ end
224
+
225
+ def submit!
226
+ raise('already submitted') if was_submitted?
227
+
228
+ wizard_steps[:submit] ||= Time.zone.now
229
+ wizard_steps[:submitted] = Time.zone.now
230
+
231
+ if mentor.present? && !importing
232
+ after_commit { EffectiveWorkExperience.mailer_class.work_experience_summary_submitted(self).deliver }
233
+ end
234
+
235
+ work_experience_records.reject(&:was_submitted?).each { |work_experience_record| work_experience_record.submitted! }
236
+ submitted!
237
+
238
+ # Auto review summaries for interns without a mentor user
239
+ review! if mentor.blank?
240
+
241
+ true
242
+ end
243
+
244
+ def review!
245
+ wizard_steps[:review] ||= Time.zone.now
246
+ wizard_steps[:reviewed] = Time.zone.now
247
+
248
+ # If it was previously reviewed, or there is no mentor, we don't want to send an email
249
+ unless was_reviewed? || mentor.blank? || importing
250
+ after_commit { EffectiveWorkExperience.mailer_class.work_experience_summary_reviewed(self).deliver }
251
+ end
252
+
253
+ work_experience_records.reject(&:was_reviewed?).each { |work_experience_record| work_experience_record.reviewed! }
254
+ reviewed!
255
+ end
256
+
257
+ private
258
+
259
+ def calculate_total_hours
260
+ return if user.blank? || start_on.blank? || end_on.blank?
261
+ user.work_experience_records.where(month: months).sum { |work_experience_record| work_experience_record.total_hours.to_f }.round(2)
262
+ end
263
+
264
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ # EffectiveWorkExperienceUser
4
+ #
5
+ # Mark your user model with effective_work_experience_user to get the work experience associations
6
+ # and the hours calculations used by the records, summaries and reports.
7
+ #
8
+ # Requires a work_experience_mentor_id column on your users table.
9
+ #
10
+ # Your user model may also define:
11
+ # - supervisor the user responsible for this intern's day to day work
12
+
13
+ module EffectiveWorkExperienceUser
14
+ extend ActiveSupport::Concern
15
+
16
+ module Base
17
+ def effective_work_experience_user
18
+ include ::EffectiveWorkExperienceUser
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ def effective_work_experience_user?; true; end
24
+ end
25
+
26
+ included do
27
+ # The user who reviews my work experience summaries
28
+ belongs_to :work_experience_mentor, class_name: name, optional: true
29
+
30
+ # The interns I am a mentor for
31
+ has_many :work_experience_mentees, -> { order(:id) }, class_name: name, foreign_key: :work_experience_mentor_id, inverse_of: :work_experience_mentor, dependent: :nullify
32
+
33
+ # There is only ever one. The has_many is for the admin form fields.
34
+ # reject_if so the blank one built by the form is ignored unless it's filled in.
35
+ has_one :work_experience_outside_mentor, as: :user, class_name: 'Effective::WorkExperienceOutsideMentor'
36
+ has_many :work_experience_outside_mentors, as: :user, dependent: :destroy, class_name: 'Effective::WorkExperienceOutsideMentor'
37
+ accepts_nested_attributes_for :work_experience_outside_mentors, allow_destroy: true, reject_if: :all_blank
38
+
39
+ # Virtual. The admin form checkbox that toggles the outside mentor fields.
40
+ attr_accessor :work_experience_outside_mentor_present
41
+
42
+ # An intern has a mentor user or an outside mentor. Never both.
43
+ before_validation(if: -> { work_experience_outside_mentor_present.present? }) do
44
+ if EffectiveResources.truthy?(work_experience_outside_mentor_present)
45
+ assign_attributes(work_experience_mentor: nil)
46
+ else
47
+ work_experience_outside_mentors.each { |outside_mentor| outside_mentor.mark_for_destruction }
48
+ end
49
+ end
50
+
51
+ has_many :work_experience_records, -> { order(:month) }, as: :user, dependent: :destroy, class_name: 'Effective::WorkExperienceRecord'
52
+ has_many :work_experience_entries, -> { order(:id) }, as: :user, dependent: :destroy, class_name: 'Effective::WorkExperienceEntry'
53
+ has_many :work_experience_projects, -> { order(:start_on) }, as: :user, dependent: :destroy, class_name: 'Effective::WorkExperienceProject'
54
+
55
+ has_many :work_experience_summaries, -> { order(:start_on) }, as: :user, dependent: :destroy, class_name: EffectiveWorkExperience.class_name(name, :work_experience_summaries)
56
+
57
+ # I'm the mentor for these work experience summaries
58
+ has_many :mentee_work_experience_summaries, -> { order(:start_on) }, as: :mentor, class_name: EffectiveWorkExperience.class_name(name, :work_experience_summaries)
59
+
60
+ scope :work_experience_mentors, -> { where(id: unscoped.select(:work_experience_mentor_id)) }
61
+
62
+ scope :deep_work_experience, -> {
63
+ includes(:work_experience_mentor)
64
+ .includes(work_experience_records: [work_experience_entries: { work_experience_subcategory: :work_experience_category }])
65
+ }
66
+ end
67
+
68
+ def work_experience_intern?
69
+ return true if try(:intern?)
70
+ work_experience_records.present? || work_experience_summaries.present?
71
+ end
72
+
73
+ def work_experience_mentor?
74
+ work_experience_mentees.present? || mentee_work_experience_summaries.present?
75
+ end
76
+
77
+ # This intern's mentor does not have an account. Their summaries are reviewed automatically.
78
+ def work_experience_outside_mentor?
79
+ work_experience_outside_mentor.present?
80
+ end
81
+
82
+ def work_experience_subcategories
83
+ @work_experience_subcategories ||= Effective::WorkExperienceSubcategory.all.sorted
84
+ end
85
+
86
+ # One subcategory and month
87
+ def work_experience_hours(month:, work_experience_subcategory: nil)
88
+ work_experience_record = work_experience_records.find { |record| record.month == month }
89
+ return 0.0 if work_experience_record.blank?
90
+
91
+ hours = if work_experience_subcategory.present?
92
+ work_experience_record.work_experience_entry(work_experience_subcategory: work_experience_subcategory).hours
93
+ else
94
+ work_experience_record.work_experience_entries.sum { |work_experience_entry| work_experience_entry.hours }
95
+ end
96
+
97
+ hours.round(2)
98
+ end
99
+
100
+ # One subcategory for every month in this year
101
+ def work_experience_hours_by_year(year:, work_experience_subcategory: nil)
102
+ work_experience_records_in_year = work_experience_records.select { |record| record.month.present? && record.month.year == year }
103
+ return 0.0 if work_experience_records_in_year.blank?
104
+
105
+ hours = if work_experience_subcategory.present?
106
+ work_experience_records_in_year.sum { |record| record.work_experience_entry(work_experience_subcategory: work_experience_subcategory).hours }
107
+ else
108
+ work_experience_records_in_year.sum { |record| record.work_experience_entries.sum { |work_experience_entry| work_experience_entry.hours } }
109
+ end
110
+
111
+ hours.round(2)
112
+ end
113
+
114
+ # One subcategory for all months up to and including this month
115
+ def work_experience_total_hours_to_date(month:, work_experience_subcategory: nil)
116
+ work_experience_records_to_date = if month.blank?
117
+ work_experience_records.select { |record| record.backdated? }
118
+ else
119
+ work_experience_records.select { |record| record.backdated? || record.month <= month }
120
+ end
121
+
122
+ return 0.0 if work_experience_records_to_date.blank?
123
+
124
+ hours = work_experience_records_to_date.sum do |work_experience_record|
125
+ if work_experience_subcategory.present?
126
+ work_experience_record.work_experience_entry(work_experience_subcategory: work_experience_subcategory).hours
127
+ else
128
+ work_experience_record.work_experience_entries.sum { |work_experience_entry| work_experience_entry.hours }
129
+ end
130
+ end
131
+
132
+ hours.round(2)
133
+ end
134
+
135
+ end