study_engine 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +42 -0
  3. data/app/assets/images/study_engine/spinner.gif +0 -0
  4. data/app/assets/images/study_engine/sprite/delete.png +0 -0
  5. data/app/assets/images/study_engine/sprite/download.png +0 -0
  6. data/app/assets/images/study_engine/sprite/edit.png +0 -0
  7. data/app/assets/images/study_engine/sprite/incomplete.png +0 -0
  8. data/app/assets/images/study_engine/sprite/metrc-sm.png +0 -0
  9. data/app/assets/images/study_engine/sprite/metrc.png +0 -0
  10. data/app/assets/images/study_engine/sprite/new.png +0 -0
  11. data/app/assets/javascripts/study_engine/admin_notes.js.coffee +12 -0
  12. data/app/assets/javascripts/study_engine/assessment_notifications.js.coffee +36 -0
  13. data/app/assets/javascripts/study_engine/assessment_timeout.js.coffee +24 -0
  14. data/app/assets/javascripts/study_engine/disable_buttons_after_click.js.coffee +6 -0
  15. data/app/assets/javascripts/study_engine/fire_click_event_immediately_on_mobile.js.coffee.erb +10 -0
  16. data/app/assets/javascripts/study_engine/response_validation.js.coffee +8 -0
  17. data/app/assets/javascripts/study_engine/restart_assessment_button.js.coffee +3 -0
  18. data/app/assets/javascripts/study_engine/study_engine.js.coffee +9 -0
  19. data/app/assets/javascripts/study_engine/study_id_picker.js.coffee +28 -0
  20. data/app/assets/stylesheets/study_engine.sass +1 -0
  21. data/app/assets/stylesheets/study_engine/_constant.sass +52 -0
  22. data/app/assets/stylesheets/study_engine/_mobile.sass +8 -0
  23. data/app/assets/stylesheets/study_engine/_tablet.sass +36 -0
  24. data/app/assets/stylesheets/study_engine/application.sass +101 -0
  25. data/app/assets/stylesheets/study_engine/base/_form.sass +131 -0
  26. data/app/assets/stylesheets/study_engine/base/_list.sass +38 -0
  27. data/app/assets/stylesheets/study_engine/base/_tables.sass +33 -0
  28. data/app/assets/stylesheets/study_engine/base/_text.sass +78 -0
  29. data/app/assets/stylesheets/study_engine/components/-fields/_login-fields.sass +14 -0
  30. data/app/assets/stylesheets/study_engine/components/-fields/_start-fields.sass +101 -0
  31. data/app/assets/stylesheets/study_engine/components/-nav/_global-nav.sass +19 -0
  32. data/app/assets/stylesheets/study_engine/components/-table/_results-table.sass +75 -0
  33. data/app/assets/stylesheets/study_engine/components/-table/_users-table.sass +11 -0
  34. data/app/assets/stylesheets/study_engine/components/_assessment-questions.sass +30 -0
  35. data/app/assets/stylesheets/study_engine/components/_form-buttons.sass +102 -0
  36. data/app/assets/stylesheets/study_engine/components/_helpt-text.sass +2 -0
  37. data/app/assets/stylesheets/study_engine/components/_method-icons.sass +15 -0
  38. data/app/assets/stylesheets/study_engine/components/_notes-modal.sass +65 -0
  39. data/app/assets/stylesheets/study_engine/components/_progress-bar.sass +25 -0
  40. data/app/assets/stylesheets/study_engine/components/_sortable-columns.sass +26 -0
  41. data/app/assets/stylesheets/study_engine/components/_table-actions.sass +8 -0
  42. data/app/assets/stylesheets/study_engine/components/_table-buttons.sass +17 -0
  43. data/app/assets/stylesheets/study_engine/components/_user-privileges.sass +3 -0
  44. data/app/assets/stylesheets/study_engine/mixins/_actions-table.sass +11 -0
  45. data/app/assets/stylesheets/study_engine/mixins/_buttons.sass +55 -0
  46. data/app/assets/stylesheets/study_engine/mixins/_checkbox-columns.sass +50 -0
  47. data/app/assets/stylesheets/study_engine/mixins/_custom-radios.sass +47 -0
  48. data/app/assets/stylesheets/study_engine/mixins/_icons.sass +38 -0
  49. data/app/assets/stylesheets/study_engine/mixins/_notification-text.sass +23 -0
  50. data/app/controllers/study_engine/admin/application_controller.rb +6 -0
  51. data/app/controllers/study_engine/admin/assessments_controller.rb +79 -0
  52. data/app/controllers/study_engine/admin/notes_controller.rb +17 -0
  53. data/app/controllers/study_engine/application_controller.rb +12 -0
  54. data/app/controllers/study_engine/assessments_controller.rb +94 -0
  55. data/app/helpers/study_engine/helpers.rb +15 -0
  56. data/app/helpers/study_engine/mailer_helpers.rb +11 -0
  57. data/app/mailers/study_engine/mailer.rb +25 -0
  58. data/app/models/study_engine/answer.rb +36 -0
  59. data/app/models/study_engine/assessment.rb +118 -0
  60. data/app/models/study_engine/begin_assessment_form.rb +110 -0
  61. data/app/models/study_engine/existing_assessments_form.rb +99 -0
  62. data/app/models/study_engine/form.rb +59 -0
  63. data/app/models/study_engine/report.rb +69 -0
  64. data/app/models/study_engine/result.rb +46 -0
  65. data/app/models/study_engine/screen_results.rb +54 -0
  66. data/app/models/study_engine/study_id.rb +47 -0
  67. data/app/models/study_engine/study_id/bank.rb +38 -0
  68. data/app/models/study_engine/study_id/sync.rb +52 -0
  69. data/app/models/study_engine/update_assessments_form.rb +53 -0
  70. data/app/views/study_engine/_progress_bar.html.slim +4 -0
  71. data/app/views/study_engine/admin/assessments/_assessment.html.slim +36 -0
  72. data/app/views/study_engine/admin/assessments/domain_results.csv.csvbuilder +17 -0
  73. data/app/views/study_engine/admin/assessments/edit.html.slim +68 -0
  74. data/app/views/study_engine/admin/assessments/index.html.slim +26 -0
  75. data/app/views/study_engine/admin/assessments/item_results.csv.csvbuilder +24 -0
  76. data/app/views/study_engine/admin/assessments/new.html.slim +46 -0
  77. data/app/views/study_engine/admin/assessments/screen_results.csv.csvbuilder +2 -0
  78. data/app/views/study_engine/assessments/complete.html.slim +25 -0
  79. data/app/views/study_engine/assessments/create.html.slim +46 -0
  80. data/app/views/study_engine/assessments/index.html.slim +43 -0
  81. data/app/views/study_engine/assessments/restart.html.slim +28 -0
  82. data/app/views/study_engine/assessments/show.html.slim +60 -0
  83. data/app/views/study_engine/mailer/assessment_recorded_notification.erb +8 -0
  84. data/app/views/study_engine/mailer/assessment_url_notification.erb +6 -0
  85. data/config/routes.rb +15 -0
  86. data/db/migrate/20160422185859_create_tables.rb +52 -0
  87. data/lib/study_engine.rb +5 -0
  88. data/lib/study_engine/config.rb +37 -0
  89. data/lib/study_engine/cucumber.rb +96 -0
  90. data/lib/study_engine/engine.rb +7 -0
  91. data/lib/study_engine/version.rb +3 -0
  92. data/lib/tasks/study_engine_tasks.rake +10 -0
  93. data/spec/ar_helper.rb +30 -0
  94. data/spec/models/study_engine/answer_spec.rb +28 -0
  95. data/spec/models/study_engine/assessment_spec.rb +250 -0
  96. data/spec/models/study_engine/begin_assessment_form_spec.rb +87 -0
  97. data/spec/models/study_engine/existing_assessments_form_spec.rb +171 -0
  98. data/spec/models/study_engine/report_spec.rb +48 -0
  99. data/spec/models/study_engine/result_spec.rb +102 -0
  100. data/spec/models/study_engine/screen_results_spec.rb +69 -0
  101. data/spec/models/study_engine/study_id/bank_spec.rb +72 -0
  102. data/spec/models/study_engine/study_id/sync_spec.rb +61 -0
  103. data/spec/models/study_engine/study_id_spec.rb +7 -0
  104. data/spec/models/study_engine/update_assessments_form_spec.rb +38 -0
  105. data/spec/rails_helper.rb +50 -0
  106. data/spec/schema.rb +65 -0
  107. data/spec/spec_helper.rb +72 -0
  108. data/spec/support/CATEngine3.exe +0 -0
  109. data/spec/support/forms/01. Physical Function Bank.xml +1213 -0
  110. data/spec/support/forms/02. Pain Interference Bank.xml +443 -0
  111. data/spec/support/forms/03. Anxiety Bank.xml +322 -0
  112. data/spec/support/forms/04. Depression Bank.xml +311 -0
  113. data/spec/support/forms/05. Ability to Participate Social bank Version 2.xml +353 -0
  114. data/spec/support/forms/06. Psychosocial Illness Impact Pos Bank.xml +900 -0
  115. data/spec/support/forms/07. Applied Cognition Gen Concerns Bank.xml +377 -0
  116. data/spec/support/forms/08. Satisfaction with Roles and Activities Bank Version 2.xml +443 -0
  117. data/spec/support/forms/09. Sleep Disturbance Bank.xml +300 -0
  118. data/spec/support/forms/10. Emotional Support Bank.xml +163 -0
  119. data/spec/support/forms/A - 07. Psychosocial Illness Impact Pos Bank.xml +471 -0
  120. data/spec/support/forms/A - 08. Pain interference Bank.xml +443 -0
  121. data/spec/support/forms/A - 09. Depression Bank.xml +311 -0
  122. data/spec/support/forms/B - 07. Physical Function Short Form.xml +123 -0
  123. data/spec/support/forms/B - 08. Physical Function Remainder A.xml +543 -0
  124. data/spec/support/forms/B - 09. Ability to Participate Social bank Version 2.xml +353 -0
  125. data/spec/support/forms/C - 07. Physical Function Short Form.xml +123 -0
  126. data/spec/support/forms/C - 08. Physical Function Remainder B.xml +553 -0
  127. data/spec/support/forms/C - 09. Anxiety Bank.xml +322 -0
  128. data/spec/support/parameters/01. Physical Function Bank.xml +682 -0
  129. data/spec/support/parameters/02. Pain Interference Bank.xml +256 -0
  130. data/spec/support/parameters/03. Anxiety Bank.xml +190 -0
  131. data/spec/support/parameters/04. Depression Bank.xml +184 -0
  132. data/spec/support/parameters/05. Ability to Participate Social bank Version 2.xml +226 -0
  133. data/spec/support/parameters/06. Psychosocial Illness Impact Pos Bank.xml +211 -0
  134. data/spec/support/parameters/07. Applied Cognition Gen Concerns Bank.xml +1 -0
  135. data/spec/support/parameters/08. Satisfaction with Roles and Activities Bank Version 2.xml +1 -0
  136. data/spec/support/parameters/09. Sleep Disturbance Bank.xml +1 -0
  137. data/spec/support/parameters/10. Emotional Support Bank.xml +1 -0
  138. metadata +394 -0
@@ -0,0 +1,17 @@
1
+ module StudyEngine
2
+ class Admin::NotesController < Admin::ApplicationController
3
+ before_action :require_admin
4
+
5
+ def update
6
+ assessment.update_column :notes, params[:notes]
7
+ render text: assessment.notes?
8
+ end
9
+
10
+ private
11
+
12
+ def assessment
13
+ Assessment.find(params[:assessment_id])
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,12 @@
1
+ class StudyEngine::ApplicationController < ::ApplicationController
2
+ helper Helpers
3
+
4
+ protect_from_forgery with: :exception
5
+
6
+ def self.expose *methods
7
+ methods.each do |method|
8
+ attr_reader method
9
+ helper_method method
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,94 @@
1
+ require "study_engine/config"
2
+ require "cat_engine/question"
3
+
4
+ module StudyEngine
5
+ class AssessmentsController < ::StudyEngine::ApplicationController
6
+ expose :assessment, :welcome_content, :events, :enabled_application_methods
7
+
8
+ def index
9
+ @welcome_content = StudyEngine.welcome_content
10
+ @assessment = BeginAssessmentForm.new
11
+ assessment.coordinator = current_user
12
+ end
13
+
14
+ def create
15
+ @assessment = BeginAssessmentForm.new(params[:assessment])
16
+ assessment.coordinator = current_user
17
+ if assessment.save
18
+ if assessment.by_url?
19
+ flash.now.notice = "An assessment has been created."
20
+ ::StudyEngine::Mailer.assessment_url_notification(current_user.email, assessment).deliver_now
21
+ render :create
22
+ elsif assessment.by_mail?
23
+ flash.now.notice = "An assessment has been created."
24
+ render :create
25
+ else
26
+ redirect_to assessment
27
+ end
28
+ elsif assessment.restartable?
29
+ flash.now[:alert] = "Partial assessment data already exists for this ID number and event. Would you like to resume or restart?"
30
+ render :restart
31
+ else
32
+ flash.now[:alert] = assessment.errors.full_messages.join(", ")
33
+ render :index
34
+ end
35
+ end
36
+
37
+ def show
38
+ if assessment.complete?
39
+ if assessment.completed_at.nil?
40
+ assessment.touch :completed_at
41
+ send_notification
42
+ end
43
+ render :complete
44
+ else
45
+ render :show
46
+ end
47
+ end
48
+
49
+ def edit
50
+ redirect_to assessment
51
+ end
52
+
53
+ def update
54
+ assessment.save_answer Answer.new(question: CatEngine::Question.new(params[:cat_engine_question]))
55
+ redirect_to assessment
56
+ end
57
+
58
+ def timeout
59
+ @assessment = BeginAssessmentForm.new_from_model(assessment)
60
+ flash.now[:alert] = "Assessment timed out due to inactivity. Would you like to resume or restart?"
61
+ render :restart
62
+ end
63
+
64
+ def notification
65
+ send_notification
66
+ head :ok
67
+ end
68
+
69
+ private
70
+
71
+ def send_notification
72
+ if email = assessment.coordinator_email
73
+ ::StudyEngine::Mailer.assessment_recorded_notification(email, assessment).deliver_now
74
+ end
75
+ end
76
+
77
+ def assessment
78
+ @assessment ||= Assessment.find(params[:id])
79
+ end
80
+
81
+ def current_user_required?
82
+ return true unless %w[show update notification].include?(params[:action]) && assessment.by_url?
83
+ end
84
+
85
+ def events
86
+ StudyEngine.events
87
+ end
88
+
89
+ def enabled_application_methods
90
+ StudyEngine.enabled_application_methods
91
+ end
92
+ end
93
+ end
94
+
@@ -0,0 +1,15 @@
1
+ module StudyEngine
2
+ module Helpers
3
+ def progress_bar percent
4
+ render partial: "study_engine/progress_bar", locals: { percent: percent }
5
+ end
6
+
7
+ def sortable_link_url report, field
8
+ "?field=#{field}&direction=#{(report.field == field && report.direction == "ASC") ? "DESC" : "ASC"}"
9
+ end
10
+
11
+ def sortable_link_class report, field
12
+ "current-#{report.direction.downcase}" if report.field == field
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ module StudyEngine
2
+ module MailerHelpers
3
+ def domains_and_group_summary assessment
4
+ finished = assessment.results.select(&:finished?).count
5
+ total = assessment.results.count
6
+ domains = [finished, total].join("/")
7
+ [domains, assessment.group_index_name].join(" ").strip
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,25 @@
1
+ require "study_engine/config"
2
+
3
+ module StudyEngine
4
+ class Mailer < ActionMailer::Base
5
+ helper MailerHelpers
6
+
7
+ default content_type: "text/html"
8
+
9
+ def assessment_url_notification email, assessment
10
+ @assessment = assessment
11
+ subject = "URL for #{assessment.study_id} #{assessment.event}"
12
+ mail to: email,
13
+ subject: subject,
14
+ content_type: "text/plain"
15
+ end
16
+
17
+ def assessment_recorded_notification email, assessment
18
+ @study_name = StudyEngine.study_name
19
+ @assessment = assessment
20
+ subject = "#{assessment.study_id} #{assessment.event} recorded"
21
+ mail to: email, subject: subject, content_type: "text/plain", cc: StudyEngine.notification_email
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,36 @@
1
+ require "cat_engine/response"
2
+ require "cat_engine/question"
3
+
4
+ module StudyEngine
5
+ class Answer < ActiveRecord::Base
6
+ belongs_to :result
7
+
8
+ def question
9
+ CatEngine::Question.new(id: question_id, promis_id: question_promis_id, context: question_context, title: question_title, responses: [])
10
+ end
11
+
12
+ def question= value
13
+ self.response = value.selected_response
14
+ value.each_pair do |key, value|
15
+ if key == :response_before
16
+ self.response_before = value
17
+ else
18
+ send :"question_#{key}=", value
19
+ end
20
+ end
21
+ end
22
+
23
+ def response
24
+ CatEngine::Response.new(id: response_id, promis_id: response_promis_id, before: response_before, title: response_title)
25
+ end
26
+
27
+ def response= value
28
+ value.each_pair do |key, value|
29
+ send :"response_#{key}=", value
30
+ end
31
+ end
32
+
33
+ attr_accessor :question_responses
34
+ end
35
+ end
36
+
@@ -0,0 +1,118 @@
1
+ require "study_engine/config"
2
+ require "study_engine/study_id"
3
+
4
+ module StudyEngine
5
+ class Assessment < ActiveRecord::Base
6
+ cattr_accessor(:timeout_interval) { 10.minutes }
7
+
8
+ default_scope -> { includes(:persisted_results) }
9
+
10
+ def timestamp_attributes_for_create
11
+ super + [:assessment_updated_at]
12
+ end
13
+
14
+ def self.by_event event
15
+ where(event: event)
16
+ end
17
+
18
+ def self.by_study_id study_id
19
+ return none unless study_id
20
+ where(study_id: study_id)
21
+ end
22
+
23
+ def self.by_site site
24
+ where("study_id RLIKE '...-#{site}-....'")
25
+ end
26
+
27
+ has_many :persisted_results, class_name: "Result", dependent: :destroy
28
+
29
+ before_create :assign_group_index
30
+
31
+ belongs_to :coordinator, class_name: "User"
32
+
33
+ def coordinator_email
34
+ coordinator.try(:email)
35
+ end
36
+
37
+ def restarted_assessment
38
+ Assessment.by_study_id(study_id).where(event: event).where.not(id: id).last
39
+ end
40
+
41
+ def stream_id
42
+ StudyID.new("STR-#{study_id.site}-#{1000 + id}") unless new_record?
43
+ end
44
+
45
+ def notes?
46
+ notes.present?
47
+ end
48
+
49
+ serialize :study_id, StudyID
50
+
51
+ def results
52
+ @results ||= begin
53
+ bank_ids = StudyEngine.bank_finder.call(self)
54
+ return persisted_results if persisted_results.length == bank_ids.length
55
+ bank_ids.map do |bank_id|
56
+ persisted_results.find_or_initialize_by(bank_id: bank_id)
57
+ end
58
+ end
59
+ end
60
+
61
+ def current_result
62
+ results.find { |result| !result.finished? }
63
+ end
64
+
65
+ def percent_complete
66
+ (results.select(&:finished?).count / results.count.to_f * 100).round
67
+ end
68
+
69
+ delegate :current_question, to: :current_result
70
+
71
+ def save_answer answer
72
+ current_result.save_answer answer
73
+ save!
74
+ touch :assessment_updated_at
75
+ end
76
+
77
+ def complete?
78
+ current_result.nil?
79
+ end
80
+
81
+ def incomplete?
82
+ !complete?
83
+ end
84
+
85
+ def results_summary
86
+ "#{results.select(&:finished?).count}/#{results.count}"
87
+ end
88
+
89
+ def full_history
90
+ persisted_results.flat_map(&:answers).map(&:question_id)
91
+ end
92
+
93
+ def by_url?
94
+ method_applied == "By URL"
95
+ end
96
+
97
+ def by_mail?
98
+ method_applied == "By mail"
99
+ end
100
+
101
+ def group_index_name
102
+ return "" if group_index.nil?
103
+ event_groups[group_index]
104
+ end
105
+
106
+ private
107
+
108
+ def assign_group_index
109
+ return unless event_groups.any?
110
+ self.group_index = restarted_assessment.try(:group_index) || self.class.by_event(event).count % event_groups.length
111
+ end
112
+
113
+ def event_groups
114
+ StudyEngine.groups[event] || []
115
+ end
116
+ end
117
+ end
118
+
@@ -0,0 +1,110 @@
1
+ require "study_engine/form"
2
+ require "study_engine/assessment"
3
+ require "study_engine/config"
4
+
5
+ module StudyEngine
6
+ class BeginAssessmentForm < Form
7
+ wraps "study_engine/assessment"
8
+
9
+ attributes :study_id, :event, :method_applied, :coordinator
10
+
11
+ attr_accessor :restart
12
+
13
+ delegate :study, :site, :id, to: :study_id, prefix: true
14
+ delegate :study=, :site=, :id=, to: :study_id, prefix: true
15
+
16
+ validate :presence_of_event
17
+ validate :format_of_study_id
18
+ validate :uniqueness_of_study_id_and_event, unless: :restart
19
+ validate :presence_of_previous_events
20
+
21
+ def study_id_study
22
+ study_options.one? ? study_options.first : study_id.study
23
+ end
24
+
25
+ def study_id_site
26
+ site_options.one? ? site_options.first : study_id.site
27
+ end
28
+
29
+ def study_id_id
30
+ id_options.one? ? id_options.first : study_id.id
31
+ end
32
+
33
+ def study_id
34
+ @study_id ||= StudyID.new
35
+ end
36
+
37
+ def method_applied
38
+ @method_applied || "In-person"
39
+ end
40
+
41
+ def existing_assessment
42
+ if study_id and new_record?
43
+ @existing_assessment ||= Assessment.by_study_id(study_id).where(event: event).last
44
+ end
45
+ end
46
+
47
+ def restartable?
48
+ existing_assessment.try(:incomplete?)
49
+ end
50
+
51
+ def study_options
52
+ StudyID::Bank.studies_grouped_by_site.fetch(coordinator.try(:site_id)) { StudyID::Bank.studies }
53
+ end
54
+
55
+ def site_options
56
+ coordinator.try(:site_id).present? ? [coordinator.site_id] : StudyID::Bank.sites
57
+ end
58
+
59
+ def id_options
60
+ ids_grouped_by_study_and_site[study_id_study.to_s + study_id_site.to_s]
61
+ end
62
+
63
+ def ids_grouped_by_study_and_site
64
+ StudyID::Bank.ids_grouped_by_study_and_site
65
+ end
66
+
67
+ delegate :by_url?, :by_mail?, :group_index_name, :created_at, :assessment_updated_at, to: :@model
68
+
69
+ private
70
+
71
+ def format_of_study_id
72
+ if study_id.present?
73
+ unless study_id.valid?
74
+ errors.add :base, "METRC study ID must be in the following format: XXX-XXX-####"
75
+ end
76
+ else
77
+ errors.add :base, "METRC study ID can not be blank. Enter ID in the following format: XXX-XXX-####"
78
+ end
79
+ end
80
+
81
+ def presence_of_event
82
+ if event.blank?
83
+ events = StudyEngine.events.dup
84
+ events.map!(&:titleize)
85
+ events.last.prepend("or ")
86
+ events = events.join(", ")
87
+ errors.add :base, "Study event can not be blank. Choose #{events} assessment."
88
+ end
89
+ end
90
+
91
+ def uniqueness_of_study_id_and_event
92
+ if existing_assessment
93
+ errors.add :base, "Assessment data has already been collected for this ID number and event. Please check these fields to confirm you are beginning an assessment for the correct ID and event."
94
+ end
95
+ end
96
+
97
+ def presence_of_previous_events
98
+ index = StudyEngine.events.index(event)
99
+ return if index.nil? || index.zero?
100
+ previous_events = StudyEngine.events[0...index]
101
+
102
+ previous_events.each do |previous_event|
103
+ if Assessment.by_study_id(study_id).by_event(previous_event).none?
104
+ errors.add :base, "#{previous_event.capitalize} assessment must already exist to administer #{event} assessment"
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+