mumuki-laboratory 7.6.2 → 7.7.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +193 -2
  3. data/Rakefile +3 -0
  4. data/app/assets/javascripts/mumuki_laboratory/application.js +0 -1
  5. data/app/assets/javascripts/mumuki_laboratory/application/assets-loader.js +1 -1
  6. data/app/assets/javascripts/mumuki_laboratory/application/bridge.js +36 -10
  7. data/app/assets/javascripts/mumuki_laboratory/application/button.js +90 -1
  8. data/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +1 -0
  9. data/app/assets/javascripts/mumuki_laboratory/application/custom-editor.js +46 -4
  10. data/app/assets/javascripts/mumuki_laboratory/application/discussions.js +14 -13
  11. data/app/assets/javascripts/mumuki_laboratory/application/kids.js +73 -36
  12. data/app/assets/javascripts/mumuki_laboratory/application/progress.js +3 -0
  13. data/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +51 -0
  14. data/app/assets/javascripts/mumuki_laboratory/application/submission.js +184 -35
  15. data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +1 -0
  16. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_discussion.scss +43 -5
  17. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kids.scss +3 -3
  18. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kindergarten.scss +55 -0
  19. data/app/controllers/assets_controller.rb +2 -0
  20. data/app/controllers/concerns/with_authorization.rb +4 -0
  21. data/app/controllers/concerns/with_user_discussion_validation.rb +14 -0
  22. data/app/controllers/discussions_controller.rb +6 -14
  23. data/app/controllers/discussions_messages_controller.rb +10 -1
  24. data/app/controllers/exercise_solutions_controller.rb +4 -2
  25. data/app/helpers/application_helper.rb +9 -5
  26. data/app/helpers/discussions_helper.rb +37 -23
  27. data/app/helpers/exercise_input_helper.rb +1 -1
  28. data/app/helpers/icons_helper.rb +3 -3
  29. data/app/views/book_discussions/index.html.erb +3 -3
  30. data/app/views/discussions/_message.html.erb +20 -8
  31. data/app/views/discussions/index.html.erb +0 -1
  32. data/app/views/discussions/new.html.erb +33 -0
  33. data/app/views/discussions/show.html.erb +18 -46
  34. data/app/views/exercise_solutions/_contextualization_results_container.html.erb +1 -1
  35. data/app/views/exercise_solutions/_results_title.html.erb +2 -2
  36. data/app/views/exercises/_read_only.html.erb +33 -6
  37. data/app/views/layouts/_copyright.html.erb +1 -1
  38. data/app/views/layouts/_discussions.html.erb +21 -3
  39. data/app/views/layouts/_test_results.html.erb +1 -1
  40. data/app/views/layouts/exercise_inputs/editors/_custom.html.erb +1 -1
  41. data/app/views/layouts/exercise_inputs/forms/_kids_form.html.erb +1 -1
  42. data/app/views/layouts/exercise_inputs/forms/_problem_form.html.erb +1 -1
  43. data/app/views/layouts/exercise_inputs/layouts/_input_bottom.html.erb +1 -1
  44. data/app/views/layouts/exercise_inputs/layouts/_input_kindergarten.html.erb +40 -0
  45. data/app/views/layouts/exercise_inputs/layouts/{_input_kids.html.erb → _input_primary.html.erb} +1 -1
  46. data/app/views/layouts/exercise_inputs/layouts/_input_right.html.erb +1 -1
  47. data/app/views/layouts/modals/_kids_context.html.erb +1 -8
  48. data/app/views/user_mailer/1st_reminder.html.erb +1 -1
  49. data/app/views/user_mailer/1st_reminder.text.erb +1 -1
  50. data/app/views/user_mailer/2nd_reminder.html.erb +1 -1
  51. data/app/views/user_mailer/2nd_reminder.text.erb +1 -1
  52. data/app/views/user_mailer/3rd_reminder.html.erb +1 -1
  53. data/app/views/user_mailer/3rd_reminder.text.erb +1 -1
  54. data/app/views/user_mailer/no_submissions_reminder.html.erb +1 -1
  55. data/app/views/user_mailer/no_submissions_reminder.text.erb +1 -1
  56. data/config/routes.rb +2 -1
  57. data/lib/mumuki/laboratory/controllers/results_rendering.rb +1 -2
  58. data/lib/mumuki/laboratory/locales/en.yml +8 -2
  59. data/lib/mumuki/laboratory/locales/es-CL.yml +292 -0
  60. data/lib/mumuki/laboratory/locales/es.yml +7 -1
  61. data/lib/mumuki/laboratory/locales/pt.yml +8 -4
  62. data/lib/mumuki/laboratory/version.rb +1 -1
  63. data/spec/controllers/confirmations_controller_spec.rb +1 -1
  64. data/spec/controllers/discussions_messages_controller_spec.rb +73 -0
  65. data/spec/controllers/exercise_solutions_controller_spec.rb +41 -6
  66. data/spec/dummy/db/schema.rb +12 -1
  67. data/spec/features/discussion_flow_spec.rb +190 -0
  68. data/spec/features/exercise_flow_spec.rb +1 -1
  69. data/spec/features/menu_bar_spec.rb +88 -7
  70. data/spec/helpers/breadcrumbs_helper_spec.rb +1 -1
  71. data/spec/javascripts/bridge-spec.js +5 -0
  72. data/spec/javascripts/csrf-token-spec.js +7 -0
  73. data/spec/javascripts/elipsis-spec.js +25 -0
  74. data/spec/javascripts/results-renderers-spec.js +17 -0
  75. data/spec/javascripts/spec-helper.js +30 -0
  76. data/spec/javascripts/speech-bubble-renderer-spec.js +11 -0
  77. data/spec/javascripts/timeout-spec.js +5 -0
  78. data/spec/javascripts/timer-spec.js +5 -0
  79. data/spec/teaspoon_env.rb +187 -0
  80. metadata +34 -9
  81. data/app/views/layouts/modals/_new_discussion.html.erb +0 -27
  82. data/vendor/assets/javascripts/hotjar.js +0 -8
@@ -19,6 +19,7 @@
19
19
  @import "modules/breadcrumb";
20
20
  @import "modules/gs-board";
21
21
  @import "modules/kids";
22
+ @import "modules/kindergarten";
22
23
  @import "modules/kids_results";
23
24
  @import "modules/discussion";
24
25
  @import "modules/popover";
@@ -201,10 +201,6 @@ $moderator-star-color: #dd9900;
201
201
  }
202
202
  }
203
203
 
204
- .discussion-create {
205
- cursor: pointer;
206
- }
207
-
208
204
  .discussion-context {
209
205
  margin-top: 40px;
210
206
  }
@@ -331,9 +327,9 @@ summary.discussion-summary {
331
327
  float: right;
332
328
  a {
333
329
  margin-left: 5px;
330
+ cursor: pointer;
334
331
  }
335
332
  .discussion-message-approved {
336
- cursor: pointer;
337
333
  text-decoration: none;
338
334
  &.selected {
339
335
  i {
@@ -341,6 +337,29 @@ summary.discussion-summary {
341
337
  }
342
338
  }
343
339
  }
340
+ .discussion-message-not-actually-a-question {
341
+ text-decoration: none;
342
+ i {
343
+ position: relative;
344
+ &:after {
345
+ position: absolute;
346
+ left: 6px;
347
+ content: ' ';
348
+ height: 19px;
349
+ width: 2px;
350
+ transform: rotate(-45deg);
351
+ background-color: #aaaaaa;
352
+ }
353
+ }
354
+ &.selected {
355
+ i {
356
+ color: $brand-primary;
357
+ &:after {
358
+ background-color: $brand-primary;
359
+ }
360
+ }
361
+ }
362
+ }
344
363
  i {
345
364
  color: #aaaaaa
346
365
  }
@@ -383,4 +402,23 @@ $statuses: (
383
402
  }
384
403
  }
385
404
 
405
+ .no-margin {
406
+ margin: 0 !important;
407
+ }
408
+
409
+ .discussion-requires-attention {
410
+ margin-right: 20px;
411
+ label {
412
+ font-weight: normal;
413
+ }
414
+ }
386
415
 
416
+ .discussion-moderator-access {
417
+ margin-right: 20px;
418
+ display: flex;
419
+ flex-direction: column;
420
+ align-items: center;
421
+ .moderator-initials {
422
+ font-size: 14px;
423
+ }
424
+ }
@@ -182,17 +182,17 @@ $kids-speech-tabs-width: 40px;
182
182
  cursor: pointer;
183
183
  }
184
184
  }
185
- div.mu-kids-character-speech-bubble-normal {
185
+ .mu-kids-character-speech-bubble-normal {
186
186
  width: 100%;
187
187
  height: 100%;
188
188
  overflow: hidden;
189
189
  }
190
- div.mu-kids-character-speech-bubble-failed {
190
+ .mu-kids-character-speech-bubble-failed {
191
191
  width: 100%;
192
192
  height: 100%;
193
193
  overflow: hidden;
194
194
  }
195
- ul.mu-kids-character-speech-bubble-tabs {
195
+ .mu-kids-character-speech-bubble-tabs {
196
196
  display: flex;
197
197
  flex-direction: column;
198
198
  justify-content: center;
@@ -0,0 +1,55 @@
1
+ .mu-kids-exercise.mu-kindergarten {
2
+
3
+ // layout
4
+
5
+ display: flex;
6
+ flex-flow: column;
7
+
8
+ .mu-kids-exercise-workspace {
9
+ display: flex;
10
+ flex-flow: row;
11
+ height: 100%;
12
+ width: 100%;
13
+ }
14
+
15
+ .mu-kids-single-state,
16
+ .mu-kids-blocks {
17
+ width: 50%;
18
+ height: 100%
19
+ }
20
+
21
+ .mu-kids-blocks {
22
+ margin-top: 0;
23
+ }
24
+
25
+ // no board numbers,
26
+ // but a thicker border instead
27
+
28
+ table.gbs_board.style-scope.gs-board {
29
+ padding: 3px;
30
+ }
31
+
32
+ .gbs_lv.gbs_lvl.style-scope.gs-board,
33
+ .gbs_lv.gbs_lvr.style-scope.gs-board,
34
+ tr.style-scope.gs-board:first-child,
35
+ tr.style-scope.gs-board:last-child {
36
+ display: none
37
+ }
38
+
39
+ // no final state,
40
+ // initial state fills its area
41
+
42
+ .mu-kids-state.mu-state-initial {
43
+ height: 100%;
44
+ }
45
+
46
+ .mu-kids-state.mu-state-final {
47
+ display: none;
48
+ }
49
+
50
+ .mu-kids-compass-rose {
51
+ display: none;
52
+ }
53
+ }
54
+
55
+
@@ -2,6 +2,8 @@ class AssetsController < ApplicationController
2
2
 
3
3
  protect_from_forgery except: [:theme_stylesheet, :extension_javascript]
4
4
  skip_before_action :authorize_if_private!
5
+ skip_before_action :validate_user_profile!
6
+ skip_before_action :validate_active_organization!
5
7
 
6
8
  def theme_stylesheet
7
9
  render inline: Organization.current.theme_stylesheet.to_s, content_type: 'text/css'
@@ -13,6 +13,10 @@ module WithAuthorization
13
13
  authorize! :owner
14
14
  end
15
15
 
16
+ def authorize_moderator!
17
+ authorize! :moderator
18
+ end
19
+
16
20
  def authorization_slug
17
21
  protection_slug || '_/_'
18
22
  end
@@ -0,0 +1,14 @@
1
+ module WithUserDiscussionValidation
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ # discussions are not enabled for all organizations nor all users
6
+ before_action :validate_user_can_discuss!
7
+ end
8
+
9
+ private
10
+
11
+ def validate_user_can_discuss!
12
+ raise Mumuki::Domain::NotFoundError unless current_user&.can_discuss_here?
13
+ end
14
+ end
@@ -1,11 +1,6 @@
1
1
  class DiscussionsController < ApplicationController
2
2
  include Mumuki::Laboratory::Controllers::Content
3
-
4
- # discussions are not enabled for all organitions nor all users
5
- # there is no need to validate forum existance when creating; next validation is stronger
6
- before_action :validate_forum_enabled!, except: :create
7
- before_action :validate_can_create_discusssion!, only: :create
8
-
3
+ include WithUserDiscussionValidation
9
4
  # users are not allowed to access discussions during exams
10
5
  before_action :validate_not_in_exam!
11
6
 
@@ -21,7 +16,12 @@ class DiscussionsController < ApplicationController
21
16
  @filtered_discussions = @discussions.scoped_query_by(discussion_filter_params)
22
17
  end
23
18
 
19
+ def new
20
+ @discussion = @debatable.new_discussion_for current_user
21
+ end
22
+
24
23
  def show
24
+ @discussion.update_last_moderator_access! current_user
25
25
  end
26
26
 
27
27
  def update
@@ -71,14 +71,6 @@ class DiscussionsController < ApplicationController
71
71
  @filter_params ||= params.permit(Discussion.permitted_query_params)
72
72
  end
73
73
 
74
- def validate_forum_enabled!
75
- raise Mumuki::Domain::NotFoundError unless Organization.current.forum_enabled?
76
- end
77
-
78
- def validate_can_create_discusssion!
79
- raise Mumuki::Domain::NotFoundError unless Organization.current.can_create_discussions?(current_user)
80
- end
81
-
82
74
  def validate_not_in_exam!
83
75
  raise Mumuki::Domain::BlockedForumError if current_user&.currently_in_exam?
84
76
  end
@@ -1,6 +1,10 @@
1
1
  class DiscussionsMessagesController < AjaxController
2
+ include WithAuthorization
3
+ include WithUserDiscussionValidation
4
+
2
5
  before_action :set_discussion!, only: [:create, :destroy]
3
- before_action :authorize_user!, only: [:destroy, :approve]
6
+ before_action :authorize_user!, only: [:destroy]
7
+ before_action :authorize_moderator!, only: [:question, :approve]
4
8
 
5
9
  def create
6
10
  @discussion.submit_message! message_params, current_user
@@ -17,6 +21,11 @@ class DiscussionsMessagesController < AjaxController
17
21
  head :ok
18
22
  end
19
23
 
24
+ def question
25
+ current_message.toggle_not_actually_a_question!
26
+ head :ok
27
+ end
28
+
20
29
  private
21
30
 
22
31
  def set_discussion!
@@ -22,7 +22,9 @@ class ExerciseSolutionsController < AjaxController
22
22
  end
23
23
 
24
24
  def solution_params
25
- params_h = params.require(:solution).permit!.to_h
26
- {content: params_h[:content]}
25
+ {
26
+ content: params.require(:solution).permit!.to_h[:content],
27
+ client_result: params[:client_result].try { |it| it.permit(:status, test_results: [:title, :status, :result, :summary]).to_h }
28
+ }
27
29
  end
28
30
  end
@@ -7,10 +7,14 @@ module ApplicationHelper
7
7
  end
8
8
 
9
9
  def profile_picture
10
- image_tag(current_user.profile_picture, height: 40, class: 'img-circle', onError: "this.onerror = null; this.src = '#{image_url('user_shape.png')}'")
10
+ profile_picture_for current_user
11
11
  end
12
12
 
13
- def paginate(object, options={})
13
+ def profile_picture_for(user, height = 40)
14
+ image_tag(user.profile_picture, height: height, class: 'img-circle', onError: "this.onerror = null; this.src = '#{image_url('user_shape.png')}'")
15
+ end
16
+
17
+ def paginate(object, options = {})
14
18
  "<div class=\"text-center\">#{super(object, {theme: 'twitter-bootstrap-3'}.merge(options))}</div>".html_safe
15
19
  end
16
20
 
@@ -32,10 +36,10 @@ module ApplicationHelper
32
36
  t :chapter_finished_html, chapter: link_to_path_element(chapter) if chapter
33
37
  end
34
38
 
35
- def span_toggle(hidden_text, active_text, active)
39
+ def span_toggle(hidden_text, active_text, active, **options)
36
40
  %Q{
37
- <span class="#{'hidden' if active}">#{hidden_text}</span>
38
- <span class="#{'hidden' unless active}">#{active_text}</span>
41
+ <span class="#{'hidden' if active} #{options[:class]}">#{hidden_text}</span>
42
+ <span class="#{'hidden' unless active} #{options[:class]}">#{active_text}</span>
39
43
  }.html_safe
40
44
  end
41
45
  end
@@ -23,31 +23,32 @@ module DiscussionsHelper
23
23
  fixed_fa_icon 'comment', text: text
24
24
  end
25
25
 
26
- def discussions_link(item, path, html_options=nil, organization=Organization.current)
27
- link_to item, path, html_options if organization.forum_enabled?
26
+ def discussions_link(item, path, html_options = nil)
27
+ return unless current_user&.can_discuss_here?
28
+ link_to item, path, html_options
28
29
  end
29
30
 
30
- def item_discussion_path(discussion, params={})
31
+ def item_discussion_path(discussion, params = {})
31
32
  polymorphic_path([discussion.item, discussion], params)
32
33
  end
33
34
 
34
- def item_discussions_path(item, params={})
35
+ def item_discussions_path(item, params = {})
35
36
  polymorphic_path([item, :discussions], params)
36
37
  end
37
38
 
38
39
  def solve_discussion_params_for(user)
39
40
  if user&.moderator_here?
40
- { status: :pending_review, sort: :created_at_asc }
41
+ {status: :pending_review, sort: :responses_count_asc, requires_moderator_response: true}
41
42
  else
42
- { status: :opened, sort: :created_at_asc }
43
+ {status: :opened, sort: :responses_count_desc}
43
44
  end
44
45
  end
45
46
 
46
47
  def default_discussions_params
47
- { status: :solved, sort: :upvotes_count_desc }
48
+ {status: :solved, sort: :upvotes_count_desc}
48
49
  end
49
50
 
50
- def user_avatar(user, image_class='')
51
+ def user_avatar(user, image_class = '')
51
52
  image_tag user.profile_picture, height: 40, class: "img-circle #{image_class}"
52
53
  end
53
54
 
@@ -67,7 +68,7 @@ module DiscussionsHelper
67
68
  %Q{
68
69
  <span class="discussion-icon fa-stack fa-xs">
69
70
  <i class="fa fa-comment-o fa-stack-2x"></i>
70
- <i class="fa fa-stack-1x">#{discussion.messages.size}</i>
71
+ <i class="fa fa-stack-1x">#{discussion.validated_messages_count}</i>
71
72
  </span>
72
73
  }.html_safe
73
74
  end
@@ -91,22 +92,16 @@ module DiscussionsHelper
91
92
  end
92
93
 
93
94
  def new_discussion_link(teaser_text, link_text)
94
- return '' unless Organization.current.can_create_discussions?(current_user)
95
-
96
95
  %Q{
97
96
  <h4>
98
97
  <span>#{t(teaser_text)}</span>
99
- <a>
100
- <span class="discussion-create">
101
- #{t(link_text)}
102
- </span>
103
- </a>
98
+ #{link_to t(link_text), new_exercise_discussion_path(@debatable, anchor: 'new-discussion-description-container') }
104
99
  </h4>
105
100
  }.html_safe
106
101
  end
107
102
 
108
103
  def discussion_count_for_status(status, discussions)
109
- discussions.scoped_query_by(discussion_filter_params, :status).by_status(status).count
104
+ discussions.scoped_query_by(discussion_filter_params, excluded_params: [:status], excluded_methods: [:page]).by_status(status).count
110
105
  end
111
106
 
112
107
  def discussions_reset_query_link
@@ -117,8 +112,10 @@ module DiscussionsHelper
117
112
  Mumuki::Domain::Status::Discussion::STATUSES
118
113
  end
119
114
 
115
+ #TODO: this one uses a long method chain in order to take advantage of eager load
116
+ # Delegate it once again when polymorphic association is removed
120
117
  def discussions_languages(discussions)
121
- discussions.map { |it| it.language.name }.uniq
118
+ @languages ||= discussions.map { |it| it.exercise.language.name }.uniq
122
119
  end
123
120
 
124
121
  def discussion_status_filter_link(status, discussions)
@@ -132,14 +129,14 @@ module DiscussionsHelper
132
129
 
133
130
  def discussion_status_filter(status, discussions_count)
134
131
  %Q{
135
- #{discussion_status_fa_icon(status)}
132
+ #{discussion_status_fa_icon(status)}
136
133
  <span>
137
134
  #{t("#{status}_count", count: discussions_count)}
138
135
  </span>
139
136
  }.html_safe
140
137
  end
141
138
 
142
- def discussion_dropdown_filter(label, filters, &block)
139
+ def discussion_dropdown_filter(label, filters, can_select_all = false, &block)
143
140
  if filters.present?
144
141
  %Q{
145
142
  <div class="dropdown discussions-toolbar-filter">
@@ -147,6 +144,7 @@ module DiscussionsHelper
147
144
  #{t label} #{fa_icon :'caret-down', class: 'fa-xs'}
148
145
  </a>
149
146
  <ul class="dropdown-menu" aria-labelledby="dropdown-#{label}">
147
+ #{discussion_filter_unselect_item(label, can_select_all)}
150
148
  #{discussion_filter_list(label, filters, &block)}
151
149
  </ul>
152
150
  </div>
@@ -159,7 +157,15 @@ module DiscussionsHelper
159
157
  end
160
158
 
161
159
  def discussion_filter_item(label, filter, &block)
162
- content_tag(:li, discussion_filter_link(label, filter, &block), class: "#{'selected' if discussion_filter_selected?(label, filter)}")
160
+ content_tag(:li, discussion_filter_link(label, filter, &block), class: ('selected' if discussion_filter_selected?(label, filter)))
161
+ end
162
+
163
+ def discussion_filter_unselect_item(label, can_select_all)
164
+ if can_select_all
165
+ content_tag(:li,
166
+ link_to(t(:all), discussion_filter_params_without_page.except(label)),
167
+ class: ('selected' unless discussion_filter_params.include?(label)))
168
+ end
163
169
  end
164
170
 
165
171
  def discussion_filter_selected?(label, filter)
@@ -167,10 +173,18 @@ module DiscussionsHelper
167
173
  end
168
174
 
169
175
  def discussion_filter_link(label, filter, &block)
170
- link_to capture(filter, &block), discussion_filter_params.merge(Hash[label, filter])
176
+ link_to capture(filter, &block), discussion_filter_params_without_page.merge(Hash[label, filter])
171
177
  end
172
178
 
173
179
  def discussion_info(discussion)
174
- "#{t(:time_since, time: time_ago_in_words(discussion.created_at))} · #{t(:message_count, count: discussion.messages.size)}"
180
+ "#{t(:time_since, time: time_ago_in_words(discussion.created_at))} · #{t(:message_count, count: discussion.messages.size)}"
181
+ end
182
+
183
+ def discussion_filter_params_without_page
184
+ discussion_filter_params.except(:page)
185
+ end
186
+
187
+ def should_show_approved_for?(user, message)
188
+ !user&.moderator_here? && message.approved? && !message.from_moderator?
175
189
  end
176
190
  end