mumuki-laboratory 7.7.6 → 7.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -3
  3. data/Rakefile +7 -1
  4. data/app/assets/javascripts/mumuki_laboratory/application/alias-modes.js +1 -1
  5. data/app/assets/javascripts/mumuki_laboratory/application/bridge.js +66 -57
  6. data/app/assets/javascripts/mumuki_laboratory/application/codemirror-builder.js +28 -25
  7. data/app/assets/javascripts/mumuki_laboratory/application/codemirror.js +8 -10
  8. data/app/assets/javascripts/mumuki_laboratory/application/confirmation.js +2 -2
  9. data/app/assets/javascripts/mumuki_laboratory/application/console.js +41 -43
  10. data/app/assets/javascripts/mumuki_laboratory/application/csrf-token.js +9 -12
  11. data/app/assets/javascripts/mumuki_laboratory/application/custom-editor.js +11 -15
  12. data/app/assets/javascripts/mumuki_laboratory/application/discussions.js +1 -3
  13. data/app/assets/javascripts/mumuki_laboratory/application/editors.js +104 -0
  14. data/app/assets/javascripts/mumuki_laboratory/application/elipsis.js +5 -4
  15. data/app/assets/javascripts/mumuki_laboratory/application/events.js +51 -0
  16. data/app/assets/javascripts/mumuki_laboratory/application/exercise.js +68 -0
  17. data/app/assets/javascripts/mumuki_laboratory/application/inputs.js +4 -2
  18. data/app/assets/javascripts/mumuki_laboratory/application/interval.js +2 -4
  19. data/app/assets/javascripts/mumuki_laboratory/application/kids.js +1 -1
  20. data/app/assets/javascripts/mumuki_laboratory/application/load-analytics.js +1 -1
  21. data/app/assets/javascripts/mumuki_laboratory/application/load-error-svg.js +1 -1
  22. data/app/assets/javascripts/mumuki_laboratory/application/messages.js +2 -2
  23. data/app/assets/javascripts/mumuki_laboratory/application/multiple-choice.js +1 -1
  24. data/app/assets/javascripts/mumuki_laboratory/application/multiple-scenarios.js +3 -6
  25. data/app/assets/javascripts/mumuki_laboratory/application/pin.js +3 -5
  26. data/app/assets/javascripts/mumuki_laboratory/application/profile.js +71 -0
  27. data/app/assets/javascripts/mumuki_laboratory/application/progress.js +24 -6
  28. data/app/assets/javascripts/mumuki_laboratory/application/results-renderer.js +20 -11
  29. data/app/assets/javascripts/mumuki_laboratory/application/speech-bubble-renderer.js +12 -5
  30. data/app/assets/javascripts/mumuki_laboratory/application/submission.js +19 -101
  31. data/app/assets/javascripts/mumuki_laboratory/application/submissions-store.js +93 -0
  32. data/app/assets/javascripts/mumuki_laboratory/application/sync-mode.js +75 -0
  33. data/app/assets/javascripts/mumuki_laboratory/application/timer.js +5 -6
  34. data/app/assets/javascripts/mumuki_laboratory/application/tooltip.js +1 -1
  35. data/app/assets/javascripts/mumuki_laboratory/application/upload.js +1 -1
  36. data/app/assets/javascripts/mumuki_laboratory/application/user.js +1 -1
  37. data/app/assets/stylesheets/mumuki_laboratory/application.scss +1 -1
  38. data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +19 -17
  39. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_avatar.scss +41 -0
  40. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_gs-board.scss +3 -0
  41. data/app/assets/stylesheets/mumuki_laboratory/application/modules/{guide-corollary.scss → _guide_corollary.scss} +0 -0
  42. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kids.scss +1 -2
  43. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kindergarten.scss +2 -1
  44. data/app/assets/stylesheets/mumuki_laboratory/application/modules/{popover.scss → _popover.scss} +0 -0
  45. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_profile.scss +36 -0
  46. data/app/controllers/application_controller.rb +2 -1
  47. data/app/controllers/users_controller.rb +5 -1
  48. data/app/helpers/application_helper.rb +6 -4
  49. data/app/helpers/avatar_helper.rb +9 -0
  50. data/app/helpers/discussions_helper.rb +2 -2
  51. data/app/helpers/{locale_helper.rb → globals_helper.rb} +6 -2
  52. data/app/helpers/profile_helper.rb +5 -0
  53. data/app/mailers/user_mailer.rb +24 -11
  54. data/app/views/book/show.html.erb +1 -1
  55. data/app/views/exercises/show.html.erb +3 -0
  56. data/app/views/layouts/_main.html.erb +1 -2
  57. data/app/views/layouts/_progress.html.erb +1 -1
  58. data/app/views/layouts/_progress_bar.html.erb +7 -1
  59. data/app/views/layouts/_runner_assets.html.erb +1 -2
  60. data/app/views/layouts/application.html.erb +2 -2
  61. data/app/views/layouts/modals/_avatar_picker.html.erb +16 -0
  62. data/app/views/users/_avatar_list.html.erb +11 -0
  63. data/app/views/users/_edit_user_form.html.erb +22 -0
  64. data/app/views/users/_user_form.html.erb +21 -8
  65. data/app/views/users/edit.html.erb +5 -0
  66. data/app/views/users/show.html.erb +0 -4
  67. data/config/routes.rb +1 -1
  68. data/lib/mumuki/laboratory/controllers.rb +1 -0
  69. data/lib/mumuki/laboratory/controllers/incognito_mode.rb +28 -0
  70. data/lib/mumuki/laboratory/locales/datetime.es.yml +14 -14
  71. data/lib/mumuki/laboratory/locales/en.yml +12 -4
  72. data/lib/mumuki/laboratory/locales/es.yml +12 -4
  73. data/lib/mumuki/laboratory/locales/pt.yml +10 -2
  74. data/lib/mumuki/laboratory/version.rb +1 -1
  75. data/spec/dummy/db/schema.rb +13 -1
  76. data/spec/features/chapter_spec.rb +17 -0
  77. data/spec/features/exercise_flow_spec.rb +54 -6
  78. data/spec/features/home_public_flow_spec.rb +16 -0
  79. data/spec/helpers/avatar_helper_spec.rb +26 -0
  80. data/spec/javascripts/editors-spec.js +54 -0
  81. data/spec/javascripts/events-spec.js +33 -0
  82. data/spec/javascripts/exercise-spec.js +41 -0
  83. data/spec/javascripts/global-spec.js +6 -0
  84. data/spec/javascripts/spec-helper.js +4 -0
  85. data/spec/javascripts/submissions-store-spec.js +44 -0
  86. data/spec/javascripts/sync-mode-spec.js +15 -0
  87. data/spec/mailers/user_mailer_spec.rb +23 -3
  88. data/spec/teaspoon_env.rb +8 -2
  89. data/vendor/assets/javascripts/codemirror-modes/gobstones.js +3 -3
  90. metadata +38 -11
  91. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_follow_us.scss +0 -16
  92. data/app/helpers/version_helper.rb +0 -5
@@ -0,0 +1,93 @@
1
+ mumuki.SubmissionsStore = (() => {
2
+ const SubmissionsStore = new class {
3
+ /**
4
+ * Returns the submission's result status for the last submission to
5
+ * the given exercise, or pending, if not present
6
+ *
7
+ * @param {number} exerciseId
8
+ * @returns {SubmissionStatus}
9
+ */
10
+ getLastSubmissionStatus(exerciseId) {
11
+ const submission = this.getLastSubmissionAndResult(exerciseId);
12
+ return submission ? submission.result.status : 'pending';
13
+ }
14
+
15
+ /**
16
+ * Returns the submission and result for the last submission to
17
+ * the given exercise
18
+ *
19
+ * @param {number} exerciseId
20
+ * @returns {SubmissionAndResult}
21
+ */
22
+ getLastSubmissionAndResult(exerciseId) {
23
+ const submissionAndResult = window.localStorage.getItem(this._keyFor(exerciseId));
24
+ if (!submissionAndResult) return null;
25
+ return JSON.parse(submissionAndResult);
26
+ }
27
+
28
+ /**
29
+ * Saves the result for the given exercise
30
+ *
31
+ * @param {number} exerciseId
32
+ * @param {SubmissionAndResult} submissionAndResult
33
+ */
34
+ setSubmissionResultFor(exerciseId, submissionAndResult) {
35
+ window.localStorage.setItem(this._keyFor(exerciseId), this._asString(submissionAndResult));
36
+ }
37
+
38
+ /**
39
+ * Retrieves the last cached, non-aborted result for the given submission of the given exercise
40
+ *
41
+ * @param {number} exerciseId
42
+ * @param {Submission} submission
43
+ * @returns {SubmissionResult} the cached result for this submission
44
+ */
45
+ getSubmissionResultFor(exerciseId, submission) {
46
+ const last = this.getLastSubmissionAndResult(exerciseId);
47
+ if (!last
48
+ || last.result.status === 'aborted'
49
+ || !this.submissionSolutionEquals(last.submission, submission)) {
50
+ return null;
51
+ }
52
+ return last.result;
53
+ }
54
+
55
+ /**
56
+ * Extract the submission's solution content
57
+ *
58
+ * @param {Submission} submission
59
+ * @returns {string}
60
+ */
61
+ submissionSolutionContent(submission) {
62
+ if (submission.solution) {
63
+ return submission.solution.content;
64
+ } else {
65
+ return submission['solution[content]'];
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Compares two solutions to determine if they are equivalent
71
+ * from the point of view of the code evaluation
72
+ *
73
+ * @param {Submission} one
74
+ * @param {Submission} other
75
+ * @returns {boolean}
76
+ */
77
+ submissionSolutionEquals(one, other) {
78
+ return this.submissionSolutionContent(one) === this.submissionSolutionContent(other);
79
+ }
80
+
81
+ // private API
82
+
83
+ _asString(object) {
84
+ return JSON.stringify(object);
85
+ }
86
+
87
+ _keyFor(exerciseId) {
88
+ return `/exercise/${exerciseId}/submission`;
89
+ }
90
+ };
91
+
92
+ return SubmissionsStore;
93
+ })();
@@ -0,0 +1,75 @@
1
+ /** @type {boolean} */
2
+ mumuki.incognitoUser;
3
+ mumuki.syncMode = (() => {
4
+
5
+ /**
6
+ * Syncs progress and solutions
7
+ * from local storage
8
+ */
9
+ class ClientSyncMode {
10
+ syncProgress() {
11
+ mumuki.progress.updateWholeProgressBar($anchor => this._getProgressListItemClass($anchor));
12
+ }
13
+
14
+ syncEditorContent() {
15
+ const lastSubmission = mumuki.SubmissionsStore.getLastSubmissionAndResult(mumuki.exercise.id);
16
+ if (lastSubmission) {
17
+ /** @todo extract core module */
18
+ const content = mumuki.SubmissionsStore.submissionSolutionContent(lastSubmission.submission);
19
+ mumuki.editors.setContent(content);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * @param {JQuery} $anchor
25
+ */
26
+ _getProgressListItemClass($anchor) {
27
+ const exerciseId = $anchor.data('mu-exercise-id');
28
+ const status = mumuki.SubmissionsStore.getLastSubmissionStatus(exerciseId);
29
+ return mumuki.renderers.progressListItemClassForStatus(status, exerciseId == mumuki.exercise.id);
30
+ }
31
+
32
+ }
33
+
34
+ /**
35
+ * Syncs progress and solutions from server.
36
+ *
37
+ * This class does nothing actually, since a server-side behaviour is the default one
38
+ * and no additional actions are needed.
39
+ */
40
+ class ServerSyncMode {
41
+ syncProgress() {
42
+ // nothing
43
+ }
44
+
45
+ syncEditorContent() {
46
+ // nothing
47
+ }
48
+ }
49
+
50
+
51
+ /** Selects the most appropriate sync mode */
52
+ function _selectSyncMode() {
53
+ if (mumuki.incognitoUser) {
54
+ mumuki.syncMode._current = new ClientSyncMode();
55
+ } else {
56
+ mumuki.syncMode._current = new ServerSyncMode();
57
+ }
58
+ }
59
+
60
+ return {
61
+ ServerSyncMode,
62
+ ClientSyncMode,
63
+
64
+ _selectSyncMode,
65
+
66
+ /** @type {ClientSyncMode|ServerSyncMode}*/
67
+ _current: null
68
+ }
69
+ })();
70
+
71
+ mumuki.load(() => {
72
+ mumuki.syncMode._selectSyncMode();
73
+ mumuki.syncMode._current.syncProgress();
74
+ mumuki.syncMode._current.syncEditorContent();
75
+ })
@@ -1,7 +1,5 @@
1
- var mumuki = mumuki || {};
2
-
3
- (function (mumuki) {
4
- mumuki.startTimer = function (endDate) {
1
+ mumuki.startTimer = (() => {
2
+ function startTimer(endDate) {
5
3
  var endTime = new Date(endDate).getTime();
6
4
  var currentTime = Date.now();
7
5
  var diffTime = endTime - currentTime;
@@ -17,5 +15,6 @@ var mumuki = mumuki || {};
17
15
  $('#timer').text(duration.format("HH:mm:ss"));
18
16
  }
19
17
  }, intervalDuration);
20
- };
21
- })(mumuki);
18
+ }
19
+ return startTimer
20
+ })();
@@ -1,3 +1,3 @@
1
- mumuki.load(function () {
1
+ mumuki.load(() => {
2
2
  $('[title]').tooltip();
3
3
  });
@@ -1,4 +1,4 @@
1
- mumuki.load(function() {
1
+ mumuki.load(() => {
2
2
  $('#upload-input').change(function (evt) {
3
3
  var file = evt.target.files[0];
4
4
  if (!file) return;
@@ -1,4 +1,4 @@
1
- mumuki.load(function () {
1
+ mumuki.load(() => {
2
2
  var hash = document.location.hash;
3
3
  if (hash) {
4
4
  $(".nav-tabs a[data-target='" + hash + "']").tab('show');
@@ -1,4 +1,4 @@
1
- $icon-font-path: asset-path('assets/bootstrap');
1
+ $icon-font-path: asset-path('assets/bootstrap/');
2
2
  $fa-font-path: asset-path('assets');
3
3
  $da-font-path: asset-path('assets');
4
4
 
@@ -1,26 +1,28 @@
1
- @import "modules/chapter_show";
2
- @import "modules/exercise_assignment";
3
- @import "modules/exercise_results";
4
- @import "modules/modal";
5
- @import "modules/upload";
6
- @import "modules/console";
7
- @import "modules/progress_listing";
1
+ @import "modules/avatar";
8
2
  @import "modules/book_header";
9
- @import "modules/progress_bar";
10
- @import "modules/timer";
11
- @import "modules/overlap";
12
- @import "modules/editor";
13
- @import "modules/organization_chooser";
14
- @import "modules/guide-corollary";
3
+ @import "modules/breadcrumb";
4
+ @import "modules/chapter_show";
15
5
  @import "modules/checkboxes";
6
+ @import "modules/console";
7
+ @import "modules/datepicker";
8
+ @import "modules/discussion";
16
9
  @import "modules/dropdown";
10
+ @import "modules/editor";
11
+ @import "modules/exercise_assignment";
12
+ @import "modules/exercise_results";
17
13
  @import "modules/flash";
18
- @import "modules/highlight";
19
- @import "modules/breadcrumb";
20
14
  @import "modules/gs-board";
15
+ @import "modules/guide_corollary";
16
+ @import "modules/highlight";
21
17
  @import "modules/kids";
22
18
  @import "modules/kindergarten";
23
19
  @import "modules/kids_results";
24
- @import "modules/discussion";
20
+ @import "modules/modal";
21
+ @import "modules/organization_chooser";
22
+ @import "modules/overlap";
25
23
  @import "modules/popover";
26
- @import "modules/datepicker";
24
+ @import "modules/progress_bar";
25
+ @import "modules/progress_listing";
26
+ @import "modules/timer";
27
+ @import "modules/upload";
28
+ @import "modules/user_profile";
@@ -0,0 +1,41 @@
1
+ .mu-user-avatar {
2
+ width: 250px;
3
+ height: 250px;
4
+
5
+ border: 2px solid $mu-color-dark-separator;
6
+ border-radius: 50%;
7
+
8
+ padding: 2px;
9
+ }
10
+
11
+ .mu-edit-avatar {
12
+ position: relative;
13
+ top: 100px;
14
+ right: 15px;
15
+ }
16
+
17
+ .mu-avatar-list {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: space-evenly;
21
+ flex-wrap: wrap;
22
+
23
+ padding: 15px;
24
+ }
25
+
26
+ .mu-avatar-item {
27
+ width: 150px;
28
+ height: 150px;
29
+
30
+ border: 2px solid $mu-color-dark-separator;
31
+
32
+ padding: 2px;
33
+ margin: 10px;
34
+
35
+ cursor: pointer;
36
+
37
+ &:hover {
38
+ box-shadow: -3px 3px 10px -3px $mu-color-primary;
39
+ transform: scale(1.025);
40
+ }
41
+ }
@@ -4,5 +4,8 @@ gs-board {
4
4
  span {
5
5
  vertical-align: middle;
6
6
  }
7
+ &.gs-board {
8
+ background-color: #FFFFFF;
9
+ }
7
10
  }
8
11
  }
@@ -96,7 +96,7 @@ $kids-speech-tabs-width: 40px;
96
96
  }
97
97
 
98
98
  .mu-kids-character-animation {
99
- width: 120px;
99
+ width: $kids-characters-height;
100
100
  }
101
101
 
102
102
  .mu-kids-character {
@@ -338,7 +338,6 @@ $kids-speech-tabs-width: 40px;
338
338
  td, th {
339
339
  &.gs-board {
340
340
  border: $kids-speech-border !important;
341
- background-color: #FFFFFF;
342
341
  }
343
342
  &.gbs_lx,
344
343
  &.gbs_lh,
@@ -8,7 +8,7 @@
8
8
  .mu-kids-exercise-workspace {
9
9
  display: flex;
10
10
  flex-flow: row;
11
- height: 100%;
11
+ height: calc(100% - #{$kids-characters-height});
12
12
  width: 100%;
13
13
  }
14
14
 
@@ -41,6 +41,7 @@
41
41
 
42
42
  .mu-kids-state.mu-state-initial {
43
43
  height: 100%;
44
+ width: 100%;
44
45
  }
45
46
 
46
47
  .mu-kids-state.mu-state-final {
@@ -0,0 +1,36 @@
1
+ .mu-user-header {
2
+ padding: 10px 0px;
3
+
4
+ display: flex;
5
+ align-items: center;
6
+ }
7
+
8
+ .mu-profile-actions {
9
+ vertical-align: middle;
10
+ margin-left: auto;
11
+
12
+ .btn {
13
+ width: 150px;
14
+ height: 55px;
15
+ margin-left: 15px;
16
+ transition: background-color 0.3s, border-color 0.3s;
17
+
18
+ &[disabled], &[disabled]:hover {
19
+ background-color: lighten($mu-color-complementary, 15%);
20
+ border-color: lighten($mu-color-complementary, 15%);
21
+ }
22
+ }
23
+ }
24
+
25
+ .mu-profile-info {
26
+ font-size: 20px;
27
+ margin-top: 5px;
28
+ div {
29
+ margin-bottom: 15px;
30
+ .italic {
31
+ font-style: italic;
32
+ }
33
+ }
34
+
35
+
36
+ }
@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
11
11
  include Mumuki::Laboratory::Controllers::Notifications
12
12
  include Mumuki::Laboratory::Controllers::DynamicErrors
13
13
  include Mumuki::Laboratory::Controllers::EmbeddedMode
14
+ include Mumuki::Laboratory::Controllers::IncognitoMode
14
15
 
15
16
  before_action :set_current_organization!
16
17
  before_action :set_locale!
@@ -92,7 +93,7 @@ class ApplicationController < ActionController::Base
92
93
  def validate_user_profile!
93
94
  unless current_user.profile_completed?
94
95
  flash.notice = I18n.t :please_fill_profile_data
95
- redirect_to user_path
96
+ redirect_to edit_user_path
96
97
  end
97
98
  end
98
99
 
@@ -11,7 +11,7 @@ class UsersController < ApplicationController
11
11
 
12
12
  def update
13
13
  current_user.update_and_notify! user_params
14
- redirect_to root_path, notice: I18n.t(:user_data_updated)
14
+ redirect_to user_path, notice: I18n.t(:user_data_updated)
15
15
  end
16
16
 
17
17
  def unsubscribe
@@ -21,6 +21,10 @@ class UsersController < ApplicationController
21
21
  redirect_to root_path, notice: t(:unsubscribed_successfully)
22
22
  end
23
23
 
24
+ def permissible_params
25
+ super << :avatar_id
26
+ end
27
+
24
28
  private
25
29
 
26
30
  def validate_user_profile!
@@ -6,12 +6,14 @@ module ApplicationHelper
6
6
  html_escape html.to_str
7
7
  end
8
8
 
9
- def profile_picture
10
- profile_picture_for current_user
9
+ def profile_picture_for(user, **options)
10
+ options.merge!(height: 40, onError: "this.onerror = null; this.src = '#{image_url(user.placeholder_image_url)}'")
11
+ avatar_image(user.profile_picture, options)
11
12
  end
12
13
 
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')}'")
14
+ def avatar_image(avatar_url, **options)
15
+ options.merge!(class: "img-circle #{options[:class]}")
16
+ image_tag(image_url(avatar_url), options)
15
17
  end
16
18
 
17
19
  def paginate(object, options = {})