mumuki-laboratory 5.5.0 → 5.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/application/discussions.js +43 -0
  3. data/app/assets/stylesheets/application/_modules.scss +2 -0
  4. data/app/assets/stylesheets/application/modules/_checkboxes.scss +21 -8
  5. data/app/assets/stylesheets/application/modules/_discussion.scss +339 -0
  6. data/app/assets/stylesheets/application/modules/_pagination.scss +10 -0
  7. data/app/controllers/application_controller.rb +4 -9
  8. data/app/controllers/book_discussions_controller.rb +7 -0
  9. data/app/controllers/chapters_controller.rb +2 -0
  10. data/app/controllers/discussions_controller.rb +65 -0
  11. data/app/controllers/discussions_messages_controller.rb +25 -0
  12. data/app/controllers/exercises_controller.rb +1 -0
  13. data/app/controllers/guide_container_controller.rb +1 -0
  14. data/app/controllers/messages_controller.rb +1 -1
  15. data/app/controllers/users_controller.rb +1 -0
  16. data/app/helpers/application_helper.rb +7 -0
  17. data/app/helpers/assignment_result_helper.rb +10 -30
  18. data/app/helpers/breadcrumbs_helper.rb +7 -3
  19. data/app/helpers/contextualization_result_helper.rb +31 -0
  20. data/app/helpers/discussions_helper.rb +163 -0
  21. data/app/helpers/exercise_input_helper.rb +33 -5
  22. data/app/helpers/icons_helper.rb +20 -3
  23. data/app/helpers/messages_helper.rb +4 -0
  24. data/app/helpers/status_helper.rb +2 -2
  25. data/app/models/assignment.rb +31 -47
  26. data/app/models/book.rb +3 -0
  27. data/app/models/chapter.rb +2 -0
  28. data/app/models/concerns/contextualization.rb +85 -0
  29. data/app/models/concerns/submittable/solvable.rb +1 -0
  30. data/app/models/concerns/with_assignments.rb +1 -1
  31. data/app/models/concerns/with_discussion_creation.rb +9 -0
  32. data/app/models/concerns/with_discussion_creation/subscription.rb +33 -0
  33. data/app/models/concerns/with_discussion_creation/upvote.rb +28 -0
  34. data/app/models/concerns/with_discussion_status.rb +11 -0
  35. data/app/models/concerns/with_discussions.rb +23 -0
  36. data/app/models/concerns/with_randomizations.rb +34 -0
  37. data/app/models/concerns/with_scoped_queries.rb +47 -0
  38. data/app/models/concerns/with_scoped_queries/filter.rb +15 -0
  39. data/app/models/concerns/with_scoped_queries/page.rb +10 -0
  40. data/app/models/concerns/with_scoped_queries/sort.rb +47 -0
  41. data/app/models/discussion.rb +128 -0
  42. data/app/models/exercise.rb +10 -1
  43. data/app/models/guide.rb +1 -1
  44. data/app/models/message.rb +24 -3
  45. data/app/models/organization.rb +4 -0
  46. data/app/models/submission/confirmation.rb +1 -1
  47. data/app/models/submission/console_submission.rb +1 -1
  48. data/app/models/submission/submission.rb +13 -0
  49. data/app/models/subscription.rb +12 -0
  50. data/app/models/topic.rb +1 -1
  51. data/app/models/upvote.rb +4 -0
  52. data/app/models/user.rb +3 -2
  53. data/app/views/book_discussions/index.html.erb +23 -0
  54. data/app/views/chapters/show.html.erb +0 -1
  55. data/app/views/discussions/_message.html.erb +22 -0
  56. data/app/views/discussions/_message_container.html.erb +8 -0
  57. data/app/views/discussions/_new_message.html.erb +19 -0
  58. data/app/views/discussions/index.html.erb +30 -0
  59. data/app/views/discussions/show.html.erb +76 -0
  60. data/app/views/exercise_solutions/_contextualization_results.html.erb +21 -0
  61. data/app/views/exercise_solutions/_contextualization_results_container.html.erb +4 -0
  62. data/app/views/exercise_solutions/_expectations.html.erb +9 -3
  63. data/app/views/exercise_solutions/_kids_results.html.erb +1 -1
  64. data/app/views/exercise_solutions/_results.html.erb +21 -55
  65. data/app/views/exercise_solutions/_results_title.html.erb +2 -2
  66. data/app/views/exercises/_exercise_assignment.html.erb +20 -0
  67. data/app/views/exercises/_read_only.html.erb +104 -0
  68. data/app/views/exercises/show.html.erb +4 -20
  69. data/app/views/layouts/_discussions.html.erb +68 -0
  70. data/app/views/layouts/_kids.html.erb +9 -6
  71. data/app/views/layouts/_messages.html.erb +1 -1
  72. data/app/views/layouts/_result.html.erb +2 -2
  73. data/app/views/layouts/_test_results.html.erb +6 -6
  74. data/app/views/layouts/application.html.erb +10 -9
  75. data/app/views/layouts/exercise_inputs/forms/_form.html.erb +5 -6
  76. data/app/views/layouts/exercise_inputs/forms/_interactive_form.html.erb +5 -5
  77. data/app/views/layouts/exercise_inputs/forms/_kids_form.html.erb +9 -17
  78. data/app/views/layouts/exercise_inputs/forms/_playground_form.html.erb +5 -5
  79. data/app/views/layouts/exercise_inputs/forms/_problem_form.html.erb +11 -11
  80. data/app/views/layouts/exercise_inputs/forms/_reading_form.html.erb +1 -1
  81. data/app/views/layouts/exercise_inputs/layouts/_input_bottom.html.erb +1 -1
  82. data/app/views/layouts/exercise_inputs/layouts/_input_kids.html.erb +7 -7
  83. data/app/views/layouts/exercise_inputs/layouts/_input_right.html.erb +1 -1
  84. data/app/views/layouts/exercise_inputs/read_only_editors/_code.html.erb +3 -0
  85. data/app/views/layouts/exercise_inputs/read_only_editors/_custom.html.erb +6 -0
  86. data/app/views/layouts/exercise_inputs/read_only_editors/_multiple_choice.html.erb +8 -0
  87. data/app/views/layouts/exercise_inputs/read_only_editors/_single_choice.html.erb +8 -0
  88. data/app/views/layouts/modals/_kids_context.html.erb +2 -2
  89. data/app/views/layouts/modals/_new_discussion.html.erb +27 -0
  90. data/app/views/users/show.html.erb +23 -3
  91. data/config/routes.rb +21 -1
  92. data/db/migrate/20180504173548_create_discussions.rb +12 -0
  93. data/db/migrate/20180504185845_add_discussion_id_to_message.rb +5 -0
  94. data/db/migrate/20180605143727_add_submission_to_discussion.rb +16 -0
  95. data/db/migrate/20180619182555_create_subscriptions.rb +9 -0
  96. data/db/migrate/20180702153442_create_upvotes.rb +8 -0
  97. data/db/migrate/20180702175220_add_upvotes_count_to_discussions.rb +5 -0
  98. data/db/migrate/20180704150839_rename_assignment_status_to_submission_status.rb +5 -0
  99. data/lib/mumuki/laboratory.rb +2 -0
  100. data/lib/mumuki/laboratory/controllers.rb +2 -1
  101. data/lib/mumuki/laboratory/controllers/content.rb +12 -0
  102. data/lib/mumuki/laboratory/controllers/notifications.rb +31 -0
  103. data/lib/mumuki/laboratory/controllers/results_rendering.rb +1 -1
  104. data/lib/mumuki/laboratory/engine.rb +0 -2
  105. data/lib/mumuki/laboratory/evaluation/manual.rb +1 -1
  106. data/lib/mumuki/laboratory/locales/en.yml +37 -1
  107. data/lib/mumuki/laboratory/locales/es.yml +42 -1
  108. data/lib/mumuki/laboratory/locales/pt.yml +33 -1
  109. data/lib/mumuki/laboratory/status.rb +51 -44
  110. data/lib/mumuki/laboratory/status/discussion/closed.rb +15 -0
  111. data/lib/mumuki/laboratory/status/discussion/discussion.rb +56 -0
  112. data/lib/mumuki/laboratory/status/discussion/opened.rb +27 -0
  113. data/lib/mumuki/laboratory/status/discussion/pending_review.rb +15 -0
  114. data/lib/mumuki/laboratory/status/discussion/solved.rb +19 -0
  115. data/lib/mumuki/laboratory/status/submission/aborted.rb +11 -0
  116. data/lib/mumuki/laboratory/status/submission/errored.rb +15 -0
  117. data/lib/mumuki/laboratory/status/{failed.rb → submission/failed.rb} +2 -2
  118. data/lib/mumuki/laboratory/status/submission/manual_evaluation_pending.rb +15 -0
  119. data/lib/mumuki/laboratory/status/{passed.rb → submission/passed.rb} +2 -2
  120. data/lib/mumuki/laboratory/status/{passed_with_warnings.rb → submission/passed_with_warnings.rb} +2 -2
  121. data/lib/mumuki/laboratory/status/submission/pending.rb +11 -0
  122. data/lib/mumuki/laboratory/status/submission/running.rb +11 -0
  123. data/lib/mumuki/laboratory/status/submission/submission.rb +49 -0
  124. data/lib/mumuki/laboratory/status/{unknown.rb → submission/unknown.rb} +2 -2
  125. data/lib/mumuki/laboratory/version.rb +1 -1
  126. data/spec/controllers/chapters_controller_spec.rb +17 -0
  127. data/spec/controllers/discussions_controller_spec.rb +19 -0
  128. data/spec/dummy/config/environments/development.rb +0 -2
  129. data/spec/dummy/config/environments/test.rb +0 -2
  130. data/spec/dummy/db/schema.rb +42 -2
  131. data/spec/evaluation_helper.rb +1 -1
  132. data/spec/factories/discussion_factory.rb +8 -0
  133. data/spec/features/dynamic_exam_spec.rb +1 -1
  134. data/spec/helpers/exercise_input_helper_spec.rb +25 -0
  135. data/spec/helpers/test_results_rendering_spec.rb +7 -7
  136. data/spec/models/assignment_spec.rb +17 -2
  137. data/spec/models/discussion_spec.rb +153 -0
  138. metadata +108 -27
  139. data/app/models/concerns/with_status.rb +0 -43
  140. data/app/models/status_rendering_verbosity.rb +0 -40
  141. data/lib/mumuki/laboratory/controllers/messages.rb +0 -9
  142. data/lib/mumuki/laboratory/status/aborted.rb +0 -7
  143. data/lib/mumuki/laboratory/status/base.rb +0 -47
  144. data/lib/mumuki/laboratory/status/errored.rb +0 -15
  145. data/lib/mumuki/laboratory/status/manual_evaluation_pending.rb +0 -15
  146. data/lib/mumuki/laboratory/status/pending.rb +0 -11
  147. data/lib/mumuki/laboratory/status/running.rb +0 -11
  148. data/spec/models/randomizer_spec.rb +0 -18
  149. data/spec/models/verbosity_spec.rb +0 -24
@@ -1,10 +1,10 @@
1
1
  module ExerciseInputHelper
2
2
  def render_exercise_input_layout(exercise)
3
- render "layouts/exercise_inputs/layouts/#{exercise.layout}"
3
+ render "layouts/exercise_inputs/layouts/#{exercise.layout}", exercise: exercise
4
4
  end
5
5
 
6
6
  def render_exercise_input_form(exercise)
7
- render "layouts/exercise_inputs/forms/#{exercise.class.name.underscore}_form"
7
+ render "layouts/exercise_inputs/forms/#{input_form_for(exercise)}_form", exercise: exercise
8
8
  end
9
9
 
10
10
  def render_exercise_input_editor(form, exercise)
@@ -15,14 +15,38 @@ module ExerciseInputHelper
15
15
  exercise.custom? ? 'mu-custom-editor-default-value' : 'default_content'
16
16
  end
17
17
 
18
+ def render_exercise_read_only_editor(exercise, content)
19
+ render "layouts/exercise_inputs/read_only_editors/#{exercise.editor}", exercise: exercise, content: content
20
+ end
21
+
22
+ def input_form_for(exercise)
23
+ if exercise&.input_kids?
24
+ 'kids'
25
+ else
26
+ exercise.class.name.underscore
27
+ end
28
+ end
29
+
30
+ def should_render_exercise_tabs?(exercise, &block)
31
+ !exercise.hidden? && (exercise.queriable? || exercise.extra_visible? || block&.call)
32
+ end
33
+
18
34
  def should_render_problem_tabs?(exercise, user)
19
- !exercise.hidden? && (exercise.queriable? || exercise.extra_visible? || exercise.has_messages_for?(user))
35
+ should_render_exercise_tabs?(exercise) { exercise.has_messages_for? user }
36
+ end
37
+
38
+ def should_render_read_only_exercise_tabs?(discussion)
39
+ should_render_exercise_tabs?(discussion.exercise) { discussion.has_submission? }
20
40
  end
21
41
 
22
42
  def should_render_message_input?(exercise, organization = Organization.current)
23
43
  exercise.is_a?(Problem) && !exercise.hidden? && organization.raise_hand_enabled?
24
44
  end
25
45
 
46
+ def should_render_need_help_dropdown?(assignment, organization = Organization.current)
47
+ !assignment.passed? && organization.ask_for_help_enabled?
48
+ end
49
+
26
50
  def render_submit_button(exercise)
27
51
  options = submit_button_options(exercise)
28
52
  text = t(options.t) if options.t.present?
@@ -34,9 +58,13 @@ module ExerciseInputHelper
34
58
  </#{options.tag}>}.html_safe
35
59
  end
36
60
 
37
- def render_custom_editor(exercise)
61
+ def render_custom_editor(exercise, read_only=false)
38
62
  custom_editor_tag = "mu-#{exercise.language}-custom-editor"
39
- "<#{custom_editor_tag}> </#{custom_editor_tag}>".html_safe
63
+ "<#{custom_editor_tag} #{custom_editor_read_only if read_only}> </#{custom_editor_tag}>".html_safe
64
+ end
65
+
66
+ def custom_editor_read_only
67
+ "read-only=true"
40
68
  end
41
69
 
42
70
  def input_kids?
@@ -1,7 +1,7 @@
1
1
  module IconsHelper
2
2
  #FIXME refactor names
3
3
  def status_icon(status_like)
4
- fa_icon *icon_for_status(status_like.to_mumuki_status)
4
+ fa_icon *icon_for_status(status_like.to_submission_status)
5
5
  end
6
6
 
7
7
  def fixed_fa_icon(name, options={})
@@ -19,8 +19,16 @@ module IconsHelper
19
19
 
20
20
  private
21
21
 
22
+ def status_fa_icon(status)
23
+ fa_icon(*icon_for_status(status))
24
+ end
25
+
22
26
  def exercise_status_fa_icon(exercise)
23
- fa_icon(*icon_for_status(exercise.status_for(current_user)))
27
+ status_fa_icon(exercise.status_for(current_user))
28
+ end
29
+
30
+ def discussion_status_fa_icon(discussion)
31
+ status_fa_icon(discussion.status)
24
32
  end
25
33
 
26
34
  def icon_for_status(s)
@@ -28,8 +36,17 @@ module IconsHelper
28
36
  [iconized[:type], class: "text-#{iconized[:class]} status-icon"]
29
37
  end
30
38
 
39
+ def label_for_status(s)
40
+ iconized = s.iconize
41
+ %Q{
42
+ <span class="text-#{iconized[:class]} status-label">
43
+ #{fa_icon "#{iconized[:type]}"}
44
+ <span>#{t s}</span>
45
+ </span>
46
+ }.html_safe
47
+ end
48
+
31
49
  def icon_for_read(read)
32
50
  tag('i', class: "fa fa-envelope#{read ? '-o' : ''}")
33
51
  end
34
-
35
52
  end
@@ -18,4 +18,8 @@ module MessagesHelper
18
18
  def read_messages_caption(assignment)
19
19
  assignment&.pending_messages? ? :read_messages : :exit
20
20
  end
21
+
22
+ def sender_class(message)
23
+ message.blank? || message.from_user?(current_user) ? 'self' : 'other'
24
+ end
21
25
  end
@@ -1,10 +1,10 @@
1
1
  module StatusHelper
2
2
  def class_for_status(s)
3
- s.to_mumuki_status.iconize[:class].to_s
3
+ s.to_submission_status.iconize[:class].to_s
4
4
  end
5
5
 
6
6
  def icon_type_for_status(s)
7
- s.to_mumuki_status.iconize[:type].to_s
7
+ s.to_submission_status.iconize[:type].to_s
8
8
  end
9
9
 
10
10
  def class_for_exercise(exercise)
@@ -1,5 +1,5 @@
1
1
  class Assignment < ApplicationRecord
2
- include WithStatus
2
+ include Contextualization
3
3
  include WithMessages
4
4
 
5
5
  belongs_to :exercise
@@ -10,14 +10,9 @@ class Assignment < ApplicationRecord
10
10
 
11
11
  validates_presence_of :exercise, :submitter
12
12
 
13
- [:expectation_results, :test_results, :query_results].each do |field|
14
- serialize field
15
- define_method(field) { self[field]&.map { |it| it.symbolize_keys } }
16
- end
13
+ delegate :language, :name, to: :exercise
17
14
 
18
- delegate :language, :name, :visible_success_output?, to: :exercise
19
- delegate :output_content_type, to: :language
20
- delegate :should_retry?, to: :status
15
+ alias_attribute :status, :submission_status
21
16
 
22
17
  scope :by_exercise_ids, -> (exercise_ids) {
23
18
  where(exercise_id: exercise_ids) if exercise_ids
@@ -27,48 +22,10 @@ class Assignment < ApplicationRecord
27
22
  joins(:submitter).where('users.name' => usernames) if usernames
28
23
  }
29
24
 
30
- def queries_with_results
31
- queries.zip(query_results).map do |query, result|
32
- {query: query, status: result&.dig(:status).defaulting(:pending), result: result&.dig(:result)}
33
- end
34
- end
35
-
36
25
  def evaluate_manually!(teacher_evaluation)
37
26
  update! status: teacher_evaluation[:status], manual_evaluation_comment: teacher_evaluation[:manual_evaluation]
38
27
  end
39
28
 
40
- def single_visual_result?
41
- test_results.size == 1 && test_results.first[:title].blank? && visible_success_output?
42
- end
43
-
44
- def single_visual_result_html
45
- output_content_type.to_html test_results.first[:result]
46
- end
47
-
48
- def results_visible?
49
- (visible_success_output? || should_retry?) && !exercise.choices?
50
- end
51
-
52
- def result_preview
53
- result.truncate(100) if should_retry?
54
- end
55
-
56
- def result_html
57
- output_content_type.to_html(result)
58
- end
59
-
60
- def feedback_html
61
- output_content_type.to_html(feedback)
62
- end
63
-
64
- def expectation_results_visible?
65
- visible_expectation_results.present?
66
- end
67
-
68
- def visible_expectation_results
69
- StatusRenderingVerbosity.visible_expectation_results(status, expectation_results || [])
70
- end
71
-
72
29
  def persist_submission!(submission)
73
30
  transaction do
74
31
  messages.destroy_all
@@ -107,6 +64,32 @@ class Assignment < ApplicationRecord
107
64
  exercise.extra_for submitter
108
65
  end
109
66
 
67
+ def run_update!
68
+ running!
69
+ begin
70
+ update! yield
71
+ rescue => e
72
+ errored! e.message
73
+ raise e
74
+ end
75
+ end
76
+
77
+ def passed!
78
+ update! submission_status: :passed
79
+ end
80
+
81
+ def running!
82
+ update! submission_status: :running,
83
+ result: nil,
84
+ test_results: nil,
85
+ expectation_results: [],
86
+ manual_evaluation_comment: nil
87
+ end
88
+
89
+ def errored!(message)
90
+ update! result: message, submission_status: :errored
91
+ end
92
+
110
93
  %w(query try tests).each do |key|
111
94
  name = "run_#{key}!"
112
95
  define_method(name) { |params| exercise.send name, params.merge(extra: extra) }
@@ -114,7 +97,7 @@ class Assignment < ApplicationRecord
114
97
 
115
98
  def as_platform_json
116
99
  navigable_parent = exercise.navigable_parent
117
- as_json(except: [:exercise_id, :submission_id, :id, :submitter_id, :solution, :created_at, :updated_at],
100
+ as_json(except: [:exercise_id, :submission_id, :id, :submitter_id, :solution, :created_at, :updated_at, :submission_status],
118
101
  include: {
119
102
  guide: {
120
103
  only: [:slug, :name],
@@ -129,6 +112,7 @@ class Assignment < ApplicationRecord
129
112
  'sid' => submission_id,
130
113
  'created_at' => updated_at,
131
114
  'content' => solution,
115
+ 'status' => submission_status,
132
116
  'exercise' => {
133
117
  'eid' => exercise.bibliotheca_id
134
118
  },
data/app/models/book.rb CHANGED
@@ -5,6 +5,9 @@ class Book < Content
5
5
  has_many :chapters, -> { order(number: :asc) }, dependent: :delete_all
6
6
  has_many :complements, dependent: :delete_all
7
7
 
8
+ has_many :exercises, through: :chapters
9
+ has_many :discussions, through: :exercises
10
+
8
11
  delegate :first_lesson, to: :first_chapter
9
12
 
10
13
  def to_s
@@ -9,6 +9,8 @@ class Chapter < ApplicationRecord
9
9
  belongs_to :book, optional: true
10
10
  belongs_to :topic
11
11
 
12
+ has_many :exercises, through: :topic
13
+
12
14
  include SiblingsNavigation
13
15
  include TerminalNavigation
14
16
 
@@ -0,0 +1,85 @@
1
+ module Contextualization
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+
6
+ private
7
+
8
+ def submission_mapping
9
+ class_attrs = Submission.mapping_attributes.map { |it| submission_fields_overrides[it] || it }
10
+ class_attrs.zip Submission.mapping_attributes
11
+ end
12
+
13
+ def submission_fields_overrides
14
+ { status: :submission_status }
15
+ end
16
+ end
17
+
18
+ included do
19
+ serialize :submission_status, Mumuki::Laboratory::Status::Submission
20
+ validates_presence_of :submission_status
21
+
22
+ [:expectation_results, :test_results, :query_results].each do |field|
23
+ serialize field
24
+ define_method(field) { self[field]&.map { |it| it.symbolize_keys } }
25
+ end
26
+
27
+ composed_of :submission, mapping: submission_mapping, constructor: :from_attributes
28
+
29
+ delegate :visible_success_output?, to: :exercise
30
+ delegate :output_content_type, to: :language
31
+ delegate :should_retry?, :to_submission_status, :passed?, :aborted?, to: :submission_status
32
+ delegate :inspection_keywords, to: :exercise
33
+ end
34
+
35
+ def queries_with_results
36
+ queries.zip(query_results).map do |query, result|
37
+ {query: query, status: result&.dig(:status).defaulting(:pending), result: result&.dig(:result)}
38
+ end
39
+ end
40
+
41
+ def single_visual_result?
42
+ test_results.size == 1 && test_results.first[:title].blank? && visible_success_output?
43
+ end
44
+
45
+ def single_visual_result_html
46
+ output_content_type.to_html test_results.first[:result]
47
+ end
48
+
49
+ def results_visible?
50
+ (visible_success_output? || should_retry?) && !exercise.choices?
51
+ end
52
+
53
+ def result_preview
54
+ result.truncate(100) if should_retry?
55
+ end
56
+
57
+ def result_html
58
+ output_content_type.to_html(result)
59
+ end
60
+
61
+ def feedback_html
62
+ output_content_type.to_html(feedback)
63
+ end
64
+
65
+ def failed_expectation_results
66
+ (expectation_results || []).select { |it| it[:result].failed? }
67
+ end
68
+
69
+ def expectation_results_visible?
70
+ failed_expectation_results.present?
71
+ end
72
+
73
+ def visible_expectation_results
74
+ exercise.input_kids? ? failed_expectation_results.first(1) : failed_expectation_results
75
+ end
76
+
77
+ def humanized_expectation_results
78
+ visible_expectation_results.map do |it|
79
+ {
80
+ result: it[:result],
81
+ explanation: Mumukit::Inspection::Expectation.parse(it).translate(inspection_keywords)
82
+ }
83
+ end
84
+ end
85
+ end
@@ -1,6 +1,7 @@
1
1
  module Solvable
2
2
  def submit_solution!(user, attributes={})
3
3
  assignment, _ = find_assignment_and_submit! user, attributes[:content].to_mumuki_solution(language)
4
+ try_solve_discussions(user) if assignment.passed?
4
5
  assignment
5
6
  end
6
7
 
@@ -60,7 +60,7 @@ module WithAssignments
60
60
  end
61
61
 
62
62
  def status_for(user)
63
- assignment_for(user).defaulting(Mumuki::Laboratory::Status::Unknown, &:status) if user
63
+ assignment_for(user).defaulting(Mumuki::Laboratory::Status::Submission::Unknown, &:status) if user
64
64
  end
65
65
 
66
66
  def last_submission_date_for(user)
@@ -0,0 +1,9 @@
1
+ module WithDiscussionCreation
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :discussions, foreign_key: 'initiator_id'
6
+ include WithDiscussionCreation::Subscription
7
+ include WithDiscussionCreation::Upvote
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ module WithDiscussionCreation::Subscription
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :discussions, foreign_key: 'initiator_id'
6
+ has_many :subscriptions
7
+ has_many :watched_discussions, through: :subscriptions, source: :discussion
8
+ end
9
+
10
+ def subscribed_to?(discussion)
11
+ discussion.subscription_for(self).present?
12
+ end
13
+
14
+ def subscribe_to!(discussion)
15
+ watched_discussions << discussion
16
+ end
17
+
18
+ def unsubscribe_to!(discussion)
19
+ watched_discussions.delete(discussion)
20
+ end
21
+
22
+ def toggle_subscription!(discussion)
23
+ if subscribed_to?(discussion)
24
+ unsubscribe_to!(discussion)
25
+ else
26
+ subscribe_to!(discussion)
27
+ end
28
+ end
29
+
30
+ def unread_discussions
31
+ subscriptions.where(read: false).map(&:discussion)
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ module WithDiscussionCreation::Upvote
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :upvotes
6
+ has_many :upvoted_discussions, through: :upvotes, source: :discussion
7
+ end
8
+
9
+ def upvoted?(discussion)
10
+ discussion.upvote_for(self).present?
11
+ end
12
+
13
+ def upvote!(discussion)
14
+ upvoted_discussions << discussion
15
+ end
16
+
17
+ def undo_upvote!(discussion)
18
+ upvoted_discussions.delete(discussion)
19
+ end
20
+
21
+ def toggle_upvote!(discussion)
22
+ if upvoted?(discussion)
23
+ undo_upvote!(discussion)
24
+ else
25
+ upvote!(discussion)
26
+ end
27
+ end
28
+ end