mumuki-laboratory 7.8.0 → 7.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +70 -0
  3. data/app/assets/javascripts/mumuki_laboratory/application/events.js +51 -0
  4. data/app/assets/javascripts/mumuki_laboratory/application/exercise.js +45 -9
  5. data/app/assets/javascripts/mumuki_laboratory/application/profile.js +71 -0
  6. data/app/assets/stylesheets/mumuki_laboratory/application.scss +1 -1
  7. data/app/assets/stylesheets/mumuki_laboratory/application/_modules.scss +19 -17
  8. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_avatar.scss +41 -0
  9. data/app/assets/stylesheets/mumuki_laboratory/application/modules/{guide-corollary.scss → _guide_corollary.scss} +0 -0
  10. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kids.scss +1 -1
  11. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_kindergarten.scss +2 -1
  12. data/app/assets/stylesheets/mumuki_laboratory/application/modules/{popover.scss → _popover.scss} +0 -0
  13. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_user_profile.scss +36 -0
  14. data/app/controllers/application_controller.rb +1 -1
  15. data/app/controllers/users_controller.rb +5 -1
  16. data/app/helpers/application_helper.rb +6 -4
  17. data/app/helpers/avatar_helper.rb +9 -0
  18. data/app/helpers/discussions_helper.rb +2 -2
  19. data/app/helpers/profile_helper.rb +5 -0
  20. data/app/mailers/user_mailer.rb +6 -6
  21. data/app/views/exercises/show.html.erb +1 -0
  22. data/app/views/layouts/_runner_assets.html.erb +1 -2
  23. data/app/views/layouts/application.html.erb +1 -1
  24. data/app/views/layouts/modals/_avatar_picker.html.erb +16 -0
  25. data/app/views/users/_avatar_list.html.erb +11 -0
  26. data/app/views/users/_edit_user_form.html.erb +22 -0
  27. data/app/views/users/_user_form.html.erb +21 -8
  28. data/app/views/users/edit.html.erb +5 -0
  29. data/app/views/users/show.html.erb +0 -4
  30. data/config/routes.rb +1 -1
  31. data/lib/mumuki/laboratory/locales/datetime.es.yml +14 -14
  32. data/lib/mumuki/laboratory/locales/en.yml +6 -0
  33. data/lib/mumuki/laboratory/locales/es.yml +7 -1
  34. data/lib/mumuki/laboratory/locales/pt.yml +6 -0
  35. data/lib/mumuki/laboratory/version.rb +1 -1
  36. data/spec/dummy/db/schema.rb +11 -0
  37. data/spec/features/exercise_flow_spec.rb +8 -5
  38. data/spec/helpers/avatar_helper_spec.rb +26 -0
  39. data/spec/javascripts/events-spec.js +33 -0
  40. data/spec/javascripts/exercise-spec.js +23 -4
  41. data/spec/mailers/user_mailer_spec.rb +11 -6
  42. data/vendor/assets/javascripts/codemirror-modes/gobstones.js +3 -2
  43. metadata +22 -9
  44. data/app/assets/stylesheets/mumuki_laboratory/application/modules/_follow_us.scss +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf3186d3e4ad3367ccf34396dfde31138bb255456b8cf5e5593a76e908545434
4
- data.tar.gz: 7e3aa354499be05940fe21355f7e4a45ae933dab0a989cd8db99b48e564c8f12
3
+ metadata.gz: 949f8977b6b6dbf69493e33b7cfc107ca0a17fb02126d0691ade60448ed6ba84
4
+ data.tar.gz: 3745b78726fd18397df226c439376f0ba8fa5dabf3a7800ef016c60558c7267d
5
5
  SHA512:
6
- metadata.gz: e91791ad61d5843960950276383f74330b0eec046f475400c21fdb89af80f044fa3902a7a86e82d1dc3e130f30716c62e23d79f5ea87c8416c9140e550414819
7
- data.tar.gz: 528f209da31776d4c01be41b92c2218bcbb6dc98175809a05062c3b077e1dc0ca8951b034e44a9a0f3cca93bbba371fd0f14da18bb56e15fac9ddb3b2e211455
6
+ metadata.gz: 5b0769fbe726708b181d7e710e34925e931adc7da7783a3a31d0764c1ba041ccbb0917e2ec7194f7a07f8ab50f38c9880ca786a2c9258a44a8e10b794435723b
7
+ data.tar.gz: f253812012281ebd1ccfd877f1aaa3ce46b5098ea1d49a313267d4b1747c1cbcc3d47e9c3c28832e27ede19041e44eb660b079dffdf8a523b3b05c4e7cbf1dbc
data/README.md CHANGED
@@ -150,6 +150,44 @@ MOZ_HEADLESS=1 bundle exec rake teaspoon
150
150
  yarn run lint
151
151
  ```
152
152
 
153
+ ## Using a local runner
154
+
155
+ Sometimes you will need to check `laboratory` against a local runner. Run the following code in you `rails console`:
156
+
157
+ ```ruby
158
+ require 'mumuki/domain/seed'
159
+
160
+ # import a new language
161
+ Mumuki::Domain::Seed.languages_syncer.locate_and_import! Language, 'http://localhost:9292'
162
+
163
+ # update an existing language object
164
+ Mumuki::Domain::Seed.languages_syncer.import! Mumukit::Sync.key(Language, 'http://localhost:9292'), language
165
+ ```
166
+
167
+ ## Using a remote content
168
+
169
+ Likewise, you will sometimes require a guide that is not locally available. Run the following code in `rails console`:
170
+
171
+ ```ruby
172
+ require 'mumuki/domain/seed'
173
+
174
+ # import a new guide
175
+ Mumuki::Domain::Seed.contents_syncer.locate_and_import! Guide, slug)
176
+
177
+ # update an existing guide object
178
+ Mumuki::Domain::Seed.contents_syncer.import! Mumukit::Sync.key(Guide, slug), guide
179
+ ```
180
+
181
+ After that you will probably to add it somewhere. The easiest way is to create a complement of `central`:
182
+
183
+ ```ruby
184
+ o = Organization.central
185
+ o.book.complements << Guide.locate!(slug).as_complement_of(o.book)
186
+ o.reindex_usages!
187
+ ```
188
+
189
+ Now you will be able to visit that guide at `http://localhost:3000/central/guides/#{slug}`
190
+
153
191
  ## JavaScript API Docs
154
192
 
155
193
  In order to be customized by runners, Laboratory exposes the following selectors and methods
@@ -268,6 +306,38 @@ which are granted to be safe and stable.
268
306
  2. Laboratory Kids Layout Initialization
269
307
  3. Runner Editor HTML
270
308
 
309
+ ## Generic event system
310
+
311
+ Laboartory provides the `mumuki.events` object, which acts as minimal, generic event system, which is mostly designed for third party components built on top of laboratory and runners. It does nothing by default.
312
+
313
+ This API has two parts: consumers API and producers API.
314
+
315
+ ```javascript
316
+ // ======
317
+ // producer
318
+ // ======
319
+
320
+ // you need to call this method in order to enable registration of event handlers
321
+ // otherwise, it will be ignored
322
+ mumuki.events.enable('myEvent');
323
+
324
+ // fire the event, with an optional event object as payload
325
+ mumuki.events.fire('myEvent', aPlainOldObject);
326
+
327
+ // clear all the registered event handlers
328
+ mumuki.events.clear('myEvent');
329
+
330
+ // ========
331
+ // consumer
332
+ // ========
333
+
334
+ // register an event handler
335
+ mumuki.events.on('myEvent', (anEventObject) => {
336
+ // do stuff
337
+ });
338
+ ```
339
+
340
+
271
341
  ## Custom editors
272
342
 
273
343
  Mumuki provides several editor types: code editors, multiple choice, file upload, and so on.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A general-purpuose event system
3
+ */
4
+ mumuki.events = {
5
+ _handlers: {},
6
+
7
+ /**
8
+ * Enables registration of event handlers for the given event name
9
+ *
10
+ * @param {string} eventName
11
+ */
12
+ enable(eventName) {
13
+ this._handlers[eventName] = this._handlers[eventName] || [];
14
+ },
15
+
16
+ /**
17
+ * Registers a listener that will be called whenever the given event is produced.
18
+ * If the event is not enabled, the given handler is simply ignored.
19
+ *
20
+ * @param {string} eventName the event to listen to
21
+ * @param {(event: any) => void} handler
22
+ */
23
+ on(eventName, handler) {
24
+ if (this._handlers[eventName]) {
25
+ this._handlers[eventName].push(handler);
26
+ }
27
+ },
28
+
29
+ /**
30
+ * Fires a given event
31
+ *
32
+ * @param {string} eventName
33
+ * @param {any} [value]
34
+ */
35
+ fire(eventName, value = null) {
36
+ if (this._handlers[eventName]) {
37
+ this._handlers[eventName].forEach(it => it(value))
38
+ }
39
+ },
40
+
41
+ /**
42
+ * Clears handlers of the given event
43
+ *
44
+ * @param {string} eventName
45
+ */
46
+ clear(eventName) {
47
+ if (this._handlers[eventName]) {
48
+ this._handlers[eventName] = [];
49
+ }
50
+ }
51
+ }
@@ -1,17 +1,50 @@
1
+ /**
2
+ * @typedef {"input_right" | "input_bottom" | "input_primary" | "input_kindergarten"} Layout
3
+ * @typedef {{id: number, layout: Layout, settings: any}} Exercise
4
+ */
5
+
1
6
  mumuki.exercise = {
7
+
8
+ /**
9
+ * The current exercise's id
10
+ *
11
+ * @type {Exercise?}
12
+ * */
13
+ _current: null,
14
+
2
15
  /**
3
16
  * The current exercise's id
4
17
  *
5
- * @type {number}
18
+ * @type {number?}
6
19
  * */
7
- id: null,
20
+ get id() {
21
+ return this._current ? this._current.id : null;
22
+ },
8
23
 
9
24
  /**
10
25
  * The current exercise's layout
11
26
  *
12
- * @type {"input_right" | "input_bottom" | "input_primary" | "input_kindergarten"}
27
+ * @type {Layout?}
13
28
  * */
14
- layout: null,
29
+ get layout() {
30
+ return this._current ? this._current.layout : null;
31
+ },
32
+
33
+ /**
34
+ * The current exercise's settings
35
+ *
36
+ * @type {any?}
37
+ * */
38
+ get settings() {
39
+ return this._current ? this._current.settings : null;
40
+ },
41
+
42
+ /**
43
+ * @type {Exercise?}
44
+ */
45
+ get current() {
46
+ return this._current;
47
+ },
15
48
 
16
49
  /**
17
50
  * Set global current exercise information
@@ -19,12 +52,15 @@ mumuki.exercise = {
19
52
  load() {
20
53
  const $muExerciseId = $('#mu-exercise-id');
21
54
  if ($muExerciseId.length) {
22
- this.id = Number($muExerciseId.val());
23
- // @ts-ignore
24
- this.layout = $('#mu-exercise-layout').val();
55
+ this._current = {
56
+ id: Number($muExerciseId.val()),
57
+ // @ts-ignore
58
+ layout: $('#mu-exercise-layout').val(),
59
+ // @ts-ignore
60
+ settings: JSON.parse($('#mu-exercise-settings').val())
61
+ };
25
62
  } else {
26
- this.id = null;
27
- this.layout = null;
63
+ this._current = null;
28
64
  }
29
65
  }
30
66
  }
@@ -0,0 +1,71 @@
1
+ mumuki.load(function() {
2
+ let $userForm = $("#mu-user-form");
3
+ let $userAvatar = $('#mu-user-avatar');
4
+ let $editButton = $('#mu-edit-profile-btn');
5
+ let $avatarPicker = $('#mu-avatar-picker');
6
+ let $avatarItem = $('.mu-avatar-item');
7
+
8
+ let userImage = "";
9
+ let avatarId = "";
10
+
11
+ let originalData = $userForm.serialize();
12
+ let originalProfilePicture = $userAvatar.attr('src');
13
+
14
+ $userForm.on('change keyup', function() {
15
+ toggleEditButtonIfThereAreChanges();
16
+ });
17
+
18
+ $avatarItem.on('click', function() {
19
+ $userAvatar.attr('src', $(this).attr('src'));
20
+ $avatarPicker.modal('hide');
21
+
22
+ const clickedAvatarId = $(this).attr('mu-avatar-id');
23
+ avatarId = clickedAvatarId || "";
24
+
25
+ toggleEditButtonIfThereAreChanges();
26
+ });
27
+
28
+ function toggleEditButtonIfThereAreChanges() {
29
+ let shouldEnable = requiredFieldsAreFilled() && (dataChanged() || avatarChanged());
30
+
31
+ $editButton.prop('disabled', !shouldEnable);
32
+ }
33
+
34
+ const requiredFieldsAreFilled = () =>
35
+ $userForm.find('select, textarea, input').toArray().every(elem => {
36
+ const $elem = $(elem);
37
+ return !($elem.prop('required')) || !!$elem.val();
38
+ });
39
+
40
+ const dataChanged = () => $userForm.serialize() !== originalData;
41
+
42
+ const avatarChanged = () => $userAvatar.attr('src') !== originalProfilePicture;
43
+
44
+ $('#mu-user-image').on('click', function(){
45
+ userImage = $userAvatar.attr('src');
46
+ });
47
+
48
+ $userForm.on('submit', function(){
49
+ if (userImage) {
50
+ setImageUrl($(this), userImage);
51
+ setAvatarId($(this), "");
52
+ }
53
+
54
+ if (avatarId) {
55
+ setAvatarId($(this), avatarId);
56
+ }
57
+ });
58
+
59
+ function setImageUrl(form, url) {
60
+ form.append(`<input type="hidden" name="user[image_url]" value="${url}"/>`);
61
+ }
62
+
63
+ function setAvatarId(form, id) {
64
+ form.append(`<input type="hidden" name="user[avatar_id]" value=${id}/>`);
65
+ }
66
+
67
+ $("#mu-edit-avatar-icon").on('click', function(){
68
+ $avatarPicker.modal();
69
+ });
70
+
71
+ });
@@ -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
+ }
@@ -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 {
@@ -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
+ }