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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +188 -0
- data/Rakefile +18 -0
- data/app/assets/config/effective_work_experience_manifest.js +3 -0
- data/app/assets/javascripts/effective_work_experience/base.js +0 -0
- data/app/assets/javascripts/effective_work_experience.js +1 -0
- data/app/assets/stylesheets/effective_work_experience/base.scss +7 -0
- data/app/assets/stylesheets/effective_work_experience.scss +1 -0
- data/app/controllers/admin/work_experience_categories_controller.rb +19 -0
- data/app/controllers/admin/work_experience_projects_controller.rb +19 -0
- data/app/controllers/admin/work_experience_records_controller.rb +19 -0
- data/app/controllers/admin/work_experience_reports_controller.rb +21 -0
- data/app/controllers/admin/work_experience_subcategories_controller.rb +19 -0
- data/app/controllers/admin/work_experience_summaries_controller.rb +19 -0
- data/app/controllers/effective/work_experience_projects_controller.rb +17 -0
- data/app/controllers/effective/work_experience_records_controller.rb +17 -0
- data/app/controllers/effective/work_experience_reports_controller.rb +19 -0
- data/app/controllers/effective/work_experience_summaries_controller.rb +17 -0
- data/app/datatables/admin/effective_work_experience_categories_datatable.rb +22 -0
- data/app/datatables/admin/effective_work_experience_projects_datatable.rb +24 -0
- data/app/datatables/admin/effective_work_experience_records_datatable.rb +37 -0
- data/app/datatables/admin/effective_work_experience_reports_datatable.rb +34 -0
- data/app/datatables/admin/effective_work_experience_subcategories_datatable.rb +24 -0
- data/app/datatables/admin/effective_work_experience_summaries_datatable.rb +57 -0
- data/app/datatables/effective_work_experience_projects_datatable.rb +38 -0
- data/app/datatables/effective_work_experience_records_datatable.rb +61 -0
- data/app/datatables/effective_work_experience_reports_review_datatable.rb +15 -0
- data/app/datatables/effective_work_experience_summaries_datatable.rb +49 -0
- data/app/datatables/effective_work_experience_summaries_review_datatable.rb +43 -0
- data/app/helpers/effective_work_experience_helper.rb +46 -0
- data/app/mailers/effective/work_experience_mailer.rb +39 -0
- data/app/models/concerns/effective_work_experience_summary.rb +264 -0
- data/app/models/concerns/effective_work_experience_user.rb +135 -0
- data/app/models/effective/work_experience_category.rb +29 -0
- data/app/models/effective/work_experience_entry.rb +43 -0
- data/app/models/effective/work_experience_outside_mentor.rb +35 -0
- data/app/models/effective/work_experience_project.rb +38 -0
- data/app/models/effective/work_experience_record.rb +99 -0
- data/app/models/effective/work_experience_report.rb +50 -0
- data/app/models/effective/work_experience_subcategory.rb +31 -0
- data/app/models/effective/work_experience_summary.rb +7 -0
- data/app/views/admin/work_experience/_user_fields.html.haml +17 -0
- data/app/views/admin/work_experience/_user_work_experience.html.haml +21 -0
- data/app/views/admin/work_experience_categories/_form.html.haml +7 -0
- data/app/views/admin/work_experience_categories/_form_work_experience_category.html.haml +5 -0
- data/app/views/admin/work_experience_projects/_form.html.haml +11 -0
- data/app/views/admin/work_experience_projects/_work_experience_project.html.haml +1 -0
- data/app/views/admin/work_experience_records/_form.html.haml +12 -0
- data/app/views/admin/work_experience_records/_work_experience_record.html.haml +1 -0
- data/app/views/admin/work_experience_reports/show.html.haml +2 -0
- data/app/views/admin/work_experience_subcategories/_form.html.haml +7 -0
- data/app/views/admin/work_experience_subcategories/_form_work_experience_subcategory.html.haml +10 -0
- data/app/views/admin/work_experience_summaries/_form.html.haml +7 -0
- data/app/views/admin/work_experience_summaries/_form_work_experience_summary.html.haml +32 -0
- data/app/views/admin/work_experience_summaries/_work_experience_summary.html.haml +1 -0
- data/app/views/effective/work_experience/_dashboard_intern.html.haml +73 -0
- data/app/views/effective/work_experience/_dashboard_mentor.html.haml +28 -0
- data/app/views/effective/work_experience_mailer/work_experience_summary_reviewed.liquid +13 -0
- data/app/views/effective/work_experience_mailer/work_experience_summary_submitted.liquid +15 -0
- data/app/views/effective/work_experience_outside_mentors/_fields.html.haml +5 -0
- data/app/views/effective/work_experience_projects/_fields.html.haml +7 -0
- data/app/views/effective/work_experience_projects/_form.html.haml +3 -0
- data/app/views/effective/work_experience_projects/_work_experience_project.html.haml +2 -0
- data/app/views/effective/work_experience_records/_fields.html.haml +52 -0
- data/app/views/effective/work_experience_records/_form.html.haml +5 -0
- data/app/views/effective/work_experience_records/_work_experience_record.html.haml +63 -0
- data/app/views/effective/work_experience_reports/_work_experience_report.html.haml +103 -0
- data/app/views/effective/work_experience_reports/show.html.haml +2 -0
- data/app/views/effective/work_experience_summaries/_content.html.haml +1 -0
- data/app/views/effective/work_experience_summaries/_layout.html.haml +3 -0
- data/app/views/effective/work_experience_summaries/_projects.html.haml +5 -0
- data/app/views/effective/work_experience_summaries/_projects_datatable.html.haml +2 -0
- data/app/views/effective/work_experience_summaries/_projects_fields.html.haml +1 -0
- data/app/views/effective/work_experience_summaries/_records.html.haml +65 -0
- data/app/views/effective/work_experience_summaries/_records_datatable.html.haml +2 -0
- data/app/views/effective/work_experience_summaries/_records_fields.html.haml +1 -0
- data/app/views/effective/work_experience_summaries/_review.html.haml +4 -0
- data/app/views/effective/work_experience_summaries/_review_fields.html.haml +4 -0
- data/app/views/effective/work_experience_summaries/_reviewed.html.haml +1 -0
- data/app/views/effective/work_experience_summaries/_start_fields.html.haml +1 -0
- data/app/views/effective/work_experience_summaries/_summary.html.haml +79 -0
- data/app/views/effective/work_experience_summaries/_user.html.haml +7 -0
- data/app/views/effective/work_experience_summaries/_work_experience_summary.html.haml +7 -0
- data/app/views/effective/work_experience_summaries/projects.html.haml +14 -0
- data/app/views/effective/work_experience_summaries/records.html.haml +14 -0
- data/app/views/effective/work_experience_summaries/review.html.haml +13 -0
- data/app/views/effective/work_experience_summaries/reviewed.html.haml +11 -0
- data/app/views/effective/work_experience_summaries/start.html.haml +16 -0
- data/app/views/effective/work_experience_summaries/submit.html.haml +10 -0
- data/app/views/effective/work_experience_summaries/submitted.html.haml +19 -0
- data/config/effective_work_experience.rb +34 -0
- data/config/locales/effective_work_experience.en.yml +94 -0
- data/config/routes.rb +28 -0
- data/db/migrate/101_create_effective_work_experience.rb +120 -0
- data/db/seeds.rb +40 -0
- data/lib/effective_work_experience/engine.rb +19 -0
- data/lib/effective_work_experience/version.rb +3 -0
- data/lib/effective_work_experience.rb +34 -0
- data/lib/generators/effective_work_experience/install_generator.rb +24 -0
- data/lib/generators/templates/effective_work_experience_mailer_preview.rb +4 -0
- data/lib/tasks/effective_work_experience_tasks.rake +8 -0
- 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
|