mumuki-laboratory 5.7.0 → 5.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/application/codemirror-builder.js +16 -8
  3. data/app/assets/javascripts/application/codemirror.js +7 -9
  4. data/app/assets/javascripts/application/discussions.js +20 -12
  5. data/app/assets/javascripts/application/multiple-files.js +222 -0
  6. data/app/assets/javascripts/application/submission.js +1 -0
  7. data/app/assets/stylesheets/application/modules/_discussion.scss +12 -0
  8. data/app/assets/stylesheets/application/modules/_editor.scss +13 -0
  9. data/app/controllers/discussions_controller.rb +10 -0
  10. data/app/controllers/discussions_messages_controller.rb +15 -3
  11. data/app/helpers/application_helper.rb +1 -1
  12. data/app/helpers/discussions_helper.rb +13 -5
  13. data/app/helpers/multiple_file_editor_helper.rb +9 -0
  14. data/app/models/application_record.rb +5 -0
  15. data/app/models/concerns/with_assignments.rb +3 -21
  16. data/app/models/discussion.rb +1 -1
  17. data/app/models/exam.rb +12 -11
  18. data/app/models/exercise.rb +1 -1
  19. data/app/models/guide.rb +1 -1
  20. data/app/models/message.rb +5 -1
  21. data/app/models/stats.rb +4 -29
  22. data/app/models/user.rb +6 -0
  23. data/app/views/discussions/_message.html.erb +3 -0
  24. data/app/views/errors/forbidden.html.erb +1 -1
  25. data/app/views/layouts/_authoring.html.erb +5 -0
  26. data/app/views/layouts/_copyright.html.erb +2 -0
  27. data/app/views/layouts/_social_media.html.erb +4 -0
  28. data/app/views/layouts/application.html.erb +4 -6
  29. data/app/views/layouts/embedded.html.erb +27 -0
  30. data/app/views/layouts/exercise_inputs/editors/_multiple_files.html.erb +8 -3
  31. data/config/routes.rb +3 -1
  32. data/db/migrate/20180802190437_add_approved_to_messages.rb +5 -0
  33. data/lib/mumuki/laboratory/controllers/dynamic_errors.rb +6 -1
  34. data/lib/mumuki/laboratory/controllers/notifications.rb +3 -2
  35. data/lib/mumuki/laboratory/exceptions.rb +1 -0
  36. data/lib/mumuki/laboratory/exceptions/blocked_forum_error.rb +2 -0
  37. data/lib/mumuki/laboratory/locales/en.yml +3 -1
  38. data/lib/mumuki/laboratory/locales/es.yml +3 -2
  39. data/lib/mumuki/laboratory/locales/pt.yml +3 -2
  40. data/lib/mumuki/laboratory/mumukit/directives.rb +6 -5
  41. data/lib/mumuki/laboratory/status/submission/pending.rb +1 -5
  42. data/lib/mumuki/laboratory/status/submission/running.rb +1 -1
  43. data/lib/mumuki/laboratory/status/submission/submission.rb +0 -1
  44. data/lib/mumuki/laboratory/version.rb +1 -1
  45. data/spec/controllers/discussions_controller_spec.rb +1 -0
  46. data/spec/controllers/exercise_solutions_controller_spec.rb +1 -1
  47. data/spec/controllers/organizations_api_controller_spec.rb +1 -1
  48. data/spec/dummy/db/schema.rb +2 -1
  49. data/spec/factories/api_client_factory.rb +3 -3
  50. data/spec/factories/assignments_factory.rb +1 -1
  51. data/spec/factories/chapter_factory.rb +1 -1
  52. data/spec/factories/course_factory.rb +5 -5
  53. data/spec/factories/discussion_factory.rb +2 -2
  54. data/spec/factories/exercise_factory.rb +24 -26
  55. data/spec/factories/guide_factory.rb +3 -3
  56. data/spec/factories/login_settings_factory.rb +1 -1
  57. data/spec/factories/message_factory.rb +1 -1
  58. data/spec/factories/organization_factory.rb +9 -9
  59. data/spec/factories/topic_factory.rb +1 -1
  60. data/spec/features/choose_organization_spec.rb +49 -42
  61. data/spec/models/exercise_spec.rb +3 -27
  62. data/spec/models/query_spec.rb +1 -1
  63. data/spec/models/question_spec.rb +2 -2
  64. data/spec/models/stats_spec.rb +2 -9
  65. data/spec/models/user_spec.rb +13 -0
  66. metadata +8 -3
  67. data/lib/mumuki/laboratory/status/submission/unknown.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eebf90572caa6242083ad55e7af966691ffadd00c9ea6cf4b9ccd5ce3ef89f22
4
- data.tar.gz: a7a348cc2799dbbf19ab01be6f9b4edace17979ce784f124ef695db12a6ba1c2
3
+ metadata.gz: 7ca436cc42496e79bddfaff35a982a8f86dfbac83a5899152e6870526e8319eb
4
+ data.tar.gz: 60d05b8275511ab5f817fb6a2d6c10e756a7d362d9937ea071345262d4a088ee
5
5
  SHA512:
6
- metadata.gz: a02f3ef36c6f80aa9b88b508b579e4d9479c4c43d04c8c120928394ca59a222192a31a7d8ec816d4ac6ccc18fd6e13d36913e819b959b10bbf26875ea95e9ebc
7
- data.tar.gz: bc5ae60058112ffac03761ee34567349a3b34348f3a2f90c334b607fa97bd84e1e82af08886c828f607ac010d1be50eec4f0504fa83bc9ca77ce4506d5aded95
6
+ metadata.gz: 688fd91f7f9ffa2534d164742bb32bdf595ccd9e70638b9e84ba8011b0f5deced7b4154a699788b905577e45d33e4f0af3e53f8945dc3f69296e2545a0fb18e6
7
+ data.tar.gz: 4a6795662d811fc676d3ea281a6862dd1a4c811bd5255bf97f76ef008ddf341af011f30077ca3eab2b8c38f35359a3ef56ae992571350bba80f276a3a27393b5
@@ -29,9 +29,6 @@ var mumuki = mumuki || {};
29
29
  };
30
30
 
31
31
  CodeMirrorBuilder.prototype = {
32
- createEditor: function (customOptions) {
33
- return CodeMirror.fromTextArea(this.textarea, Object.assign({}, codeMirrorDefaults, customOptions));
34
- },
35
32
  setupEditor: function () {
36
33
  this.editor = this.createEditor({
37
34
  lineNumbers: true,
@@ -47,6 +44,8 @@ var mumuki = mumuki || {};
47
44
  }
48
45
  }
49
46
  });
47
+
48
+ return this;
50
49
  },
51
50
  setupSimpleEditor: function () {
52
51
  this.editor = this.createEditor({
@@ -59,21 +58,30 @@ var mumuki = mumuki || {};
59
58
  }
60
59
  }
61
60
  });
61
+
62
+ return this;
62
63
  },
63
- setupLanguage: function () {
64
- var language = this.$textarea.data('editor-language');
65
- if (language === 'dynamic') {
64
+ setupLanguage: function (language) {
65
+ var highlightMode = language || this.$textarea.data('editor-language');
66
+ if (highlightMode === 'dynamic') {
66
67
  mumuki.page.dynamicEditors.push(this.editor);
67
68
  } else {
68
- this.editor.setOption('mode', language);
69
+ this.editor.setOption('mode', highlightMode);
69
70
  this.editor.refresh();
70
71
  }
72
+
73
+ return this;
71
74
  },
72
- setupOptions: function (minLines) {
75
+ setupMinLines: function (minLines) {
73
76
  this.editor.setOption('minLines', minLines);
77
+
78
+ return this;
74
79
  },
75
80
  build: function () {
76
81
  return this.editor;
82
+ },
83
+ createEditor: function (customOptions) {
84
+ return CodeMirror.fromTextArea(this.textarea, Object.assign({}, codeMirrorDefaults, customOptions));
77
85
  }
78
86
  };
79
87
 
@@ -2,16 +2,15 @@ var mumuki = mumuki || {};
2
2
 
3
3
  (function (mumuki) {
4
4
  function createCodeMirrors() {
5
- var editors = $(".editor").map(function (index, textarea) {
5
+ return $(".editor").map(function (index, textarea) {
6
6
  var $textarea = $("#solution_content");
7
- var builder = new mumuki.editor.CodeMirrorBuilder(textarea);
8
- builder.setupEditor();
9
- builder.setupOptions($textarea.data('lines'));
10
- builder.setupLanguage();
11
- return builder.build();
12
- });
13
7
 
14
- return editors;
8
+ return new mumuki.editor.CodeMirrorBuilder(textarea)
9
+ .setupEditor()
10
+ .setupMinLines($textarea.data('lines'))
11
+ .setupLanguage()
12
+ .build();
13
+ });
15
14
  }
16
15
 
17
16
  function onSelectUpdateCodeMirror() {
@@ -62,7 +61,6 @@ var mumuki = mumuki || {};
62
61
  }
63
62
 
64
63
  mumuki.editor = mumuki.editor || {};
65
- mumuki.editor.setupCodeMirrors = setEditorLanguage;
66
64
  mumuki.editor.toggleFullscreen = toggleFullscreen;
67
65
  mumuki.editor.indentWithSpaces = indentWithSpaces;
68
66
  mumuki.editor.syncContent = syncContent;
@@ -18,10 +18,11 @@ mumuki.load(function () {
18
18
  var $textarea = $("#new-discussion-message");
19
19
  var textarea = $textarea[0];
20
20
  if(!textarea) return;
21
- var builder = new mumuki.editor.CodeMirrorBuilder(textarea);
22
- builder.setupSimpleEditor();
23
- builder.setupOptions($textarea.data('lines'));
24
- builder.build();
21
+
22
+ new mumuki.editor.CodeMirrorBuilder(textarea)
23
+ .setupSimpleEditor()
24
+ .setupMinLines($textarea.data('lines'))
25
+ .build();
25
26
  }
26
27
 
27
28
  createNewMessageEditor();
@@ -31,6 +32,16 @@ mumuki.load(function () {
31
32
  spans.toggleClass('hidden');
32
33
  },
33
34
  token: new mumuki.CsrfToken(),
35
+ tokenRequest: function (data) {
36
+ return $.ajax(Forum.token.newRequest(data))
37
+ },
38
+ discussionPost: function (url) {
39
+ return Forum.tokenRequest({
40
+ url: url,
41
+ method: 'POST',
42
+ xhrFields: {withCredentials: true}
43
+ })
44
+ },
34
45
  discussionSubscription: function (url) {
35
46
  Forum.discussionPostAndToggle(url, $subscriptionSpans)
36
47
  },
@@ -38,15 +49,12 @@ mumuki.load(function () {
38
49
  Forum.discussionPostAndToggle(url, $upvoteSpans)
39
50
  },
40
51
  discussionPostAndToggle: function (url, elem) {
41
- Forum.tokenRequest({
42
- url: url,
43
- method: 'POST',
44
- success: Forum.toggleButton(elem),
45
- xhrFields: {withCredentials: true}
46
- })
52
+ Forum.discussionPost(url).done(Forum.toggleButton(elem))
47
53
  },
48
- tokenRequest: function (data) {
49
- $.ajax(Forum.token.newRequest(data))
54
+ discussionMessageToggleApprove : function (url, elem) {
55
+ Forum.discussionPost(url).done(function () {
56
+ elem.toggleClass("selected");
57
+ })
50
58
  }
51
59
  };
52
60
 
@@ -0,0 +1,222 @@
1
+ mumuki.load(() => {
2
+ class File {
3
+ static get NAME_CLASS() { return '.file-name'; }
4
+ static get DELETE_BUTTON_CLASS() { return '.delete-file-button'; }
5
+
6
+ constructor(tab, editor) {
7
+ this.tab = tab;
8
+ this.editor = editor;
9
+ }
10
+
11
+ initialize(name) {
12
+ this.name = name;
13
+ this.unselect();
14
+
15
+ return this;
16
+ }
17
+
18
+ get name() {
19
+ return this.tab.find(File.NAME_CLASS).text();
20
+ }
21
+
22
+ set name(name) {
23
+ return this.tab.find(File.NAME_CLASS).text(name);
24
+ }
25
+
26
+ get isSelected() {
27
+ return this.tab.hasClass("active");
28
+ }
29
+
30
+ setUpOnRemove(handler) {
31
+ this.tab.find(File.DELETE_BUTTON_CLASS).click(() => {
32
+ handler(this);
33
+ });
34
+ }
35
+
36
+ remove() {
37
+ const wasSelected = this.isSelected;
38
+
39
+ this.tab.remove();
40
+ this.editor.remove();
41
+
42
+ return wasSelected;
43
+ }
44
+
45
+ select() {
46
+ this._selectElement(this.tab);
47
+ this._selectElement(this.editor);
48
+ }
49
+
50
+ unselect() {
51
+ this._unselectElement(this.tab);
52
+ this._unselectElement(this.editor);
53
+ }
54
+
55
+ _selectElement(element) {
56
+ element.addClass('active');
57
+ element.addClass('in');
58
+ }
59
+
60
+ _unselectElement(element) {
61
+ element.removeClass('active');
62
+ element.removeClass('in');
63
+ }
64
+ }
65
+
66
+ class MultipleFileEditor {
67
+ constructor(tabsContainer, editorsContainer) {
68
+ this.tabsContainer = tabsContainer;
69
+ this.editorsContainer = editorsContainer;
70
+
71
+ this.MAX_TABS = 5;
72
+
73
+ this._addFileButton = this.tabsContainer.siblings('.add-file-button');
74
+ this._updateButtonsVisibility();
75
+ }
76
+
77
+ get files() {
78
+ const editors = this.editors;
79
+
80
+ return this.tabs.map((i, tab) => {
81
+ return new File($(tab), $(editors[i]));
82
+ });
83
+ }
84
+
85
+ get tabs() {
86
+ return this.tabsContainer.children();
87
+ }
88
+
89
+ get editors() {
90
+ return this.editorsContainer.find('.file-editor')
91
+ }
92
+
93
+ get highlightModes() {
94
+ return this._getDataFromHiddenInput('#highlight-modes')
95
+ }
96
+
97
+ get locales() {
98
+ return this._getDataFromHiddenInput('#multifile-locales')
99
+ }
100
+
101
+ setUpAddFile() {
102
+ this._addFileButton.click(() => {
103
+ this._addFile();
104
+ });
105
+ }
106
+
107
+ setUpDeleteFiles() {
108
+ this.files.each((i, file) => {
109
+ this.setUpDeleteFile(file);
110
+ });
111
+ }
112
+
113
+ setUpDeleteFile(file) {
114
+ file.setUpOnRemove(this._deleteFile.bind(this));
115
+ }
116
+
117
+ _addFile() {
118
+ const name = prompt(this.locales.insert_file_name);
119
+ const alreadyExists = this.files.toArray().some(it => it.name === name);
120
+ if (!name.length || !name.includes('.') || alreadyExists) return;
121
+
122
+ const id = `editor-file-${this._getFilesCount()}`;
123
+ this.tabsContainer.append(this._createTab(name, id));
124
+ this.editors.parent().last().append(this._createEditor(name, id));
125
+ const file = this.files.last().get(0).initialize(name);
126
+ this.setUpDeleteFile(file);
127
+
128
+ this._updateButtonsVisibility();
129
+ }
130
+
131
+ _deleteFile(file) {
132
+ const index = this.files.toArray()
133
+ .map((file) => file.name)
134
+ .indexOf(file.name);
135
+ const previousIndex = Math.max(index - 1, 0);
136
+
137
+ const wasSelected = file.remove();
138
+ if (wasSelected) this.files[previousIndex].select();
139
+
140
+ this._updateButtonsVisibility();
141
+ }
142
+
143
+ _updateButtonsVisibility() {
144
+ const filesCount = this._getFilesCount();
145
+ const deleteButtons = this.tabs.find(File.DELETE_BUTTON_CLASS);
146
+
147
+ this._setVisibility(this._addFileButton, filesCount < this.MAX_TABS);
148
+ this._setVisibility(deleteButtons, filesCount > 1);
149
+ }
150
+
151
+ _createTab(name, id) {
152
+ const tab = this.tabs.last().clone();
153
+ tab.attr('data-target', `#${id}`);
154
+
155
+ return tab;
156
+ }
157
+
158
+ _createEditor(name, id) {
159
+ const editor = this.editors.last().clone();
160
+ editor.attr('id', id);
161
+ editor.find('.CodeMirror').remove();
162
+
163
+ const textarea = editor.children().first();
164
+ this._setUpTextArea(textarea, `solution_content[${name}]`, `solution[content[${name}]]`);
165
+
166
+ const highlightMode = this._getHighlightModeFor(name);
167
+ const codeMirrorEditor = new mumuki.editor.CodeMirrorBuilder(textarea.get(0))
168
+ .setupEditor()
169
+ .setupMinLines(textarea.data('lines'))
170
+ .setupLanguage(highlightMode)
171
+ .build();
172
+
173
+ codeMirrorEditor.on("change", (event) => {
174
+ textarea.val(event.getValue());
175
+ });
176
+
177
+ const solutionTextArea = $('.new_solution').find('textarea').last();
178
+ this._setUpTextArea(solutionTextArea, '', '');
179
+
180
+ return editor;
181
+ }
182
+
183
+ _getFilesCount() {
184
+ return this.files.length;
185
+ }
186
+
187
+ _setUpTextArea(textarea, id, name) {
188
+ textarea.attr('id', id);
189
+ textarea.attr('name', name);
190
+ textarea.text('');
191
+ textarea.val('');
192
+ }
193
+
194
+ _getHighlightModeFor(name) {
195
+ const extension = name.split('.').pop();
196
+ const language = this.highlightModes.find((it) => it.extension === extension);
197
+
198
+ return language && language.highlight_mode || extension;
199
+ }
200
+
201
+ _setVisibility(element, isVisible) {
202
+ if (isVisible) element.show(); else element.hide();
203
+ }
204
+
205
+ _getDataFromHiddenInput(name) {
206
+ return JSON.parse($(name).val());
207
+ }
208
+ }
209
+
210
+
211
+ const setUpTabsBehavior = () => {
212
+ const tabsContainer = $('.nav-tabs');
213
+ if (!tabsContainer.length) return;
214
+ const editorsContainer = $('.tab-content');
215
+
216
+ const multipleFileEditor = new MultipleFileEditor(tabsContainer, editorsContainer);
217
+ multipleFileEditor.setUpAddFile();
218
+ multipleFileEditor.setUpDeleteFiles();
219
+ };
220
+
221
+ $(document).ready(setUpTabsBehavior);
222
+ });
@@ -84,6 +84,7 @@ var mumuki = mumuki || {};
84
84
 
85
85
  function getContent(){
86
86
  var content = {};
87
+
87
88
  $('.new_solution').serializeArray().forEach(function(it) {
88
89
  content[it.name] = it.value;
89
90
  });
@@ -329,6 +329,18 @@ summary.discussion-summary {
329
329
  }
330
330
  .actions {
331
331
  float: right;
332
+ a {
333
+ margin-left: 5px;
334
+ }
335
+ .discussion-message-approved {
336
+ cursor: pointer;
337
+ text-decoration: none;
338
+ &.selected {
339
+ i {
340
+ color: $brand-success;
341
+ }
342
+ }
343
+ }
332
344
  i {
333
345
  color: #aaaaaa
334
346
  }
@@ -88,6 +88,7 @@ body.fullscreen {
88
88
  width: 100%;
89
89
 
90
90
  li {
91
+ position: relative;
91
92
  width: 100%;
92
93
  white-space: nowrap;
93
94
  overflow: hidden;
@@ -99,6 +100,12 @@ body.fullscreen {
99
100
  margin-right: 0;
100
101
  padding: 5px 10px;
101
102
  }
103
+ .delete-file-button {
104
+ position: absolute;
105
+ right: 16px;
106
+ top: 12px;
107
+ cursor: pointer;
108
+ }
102
109
  &.active {
103
110
  a {
104
111
  background-color: #f7f7f7;
@@ -114,5 +121,11 @@ body.fullscreen {
114
121
  }
115
122
  }
116
123
  }
124
+
125
+ .add-file-button {
126
+ margin-left: 16px;
127
+ margin-right: 16px;
128
+ cursor: pointer;
129
+ }
117
130
  }
118
131
 
@@ -1,6 +1,8 @@
1
1
  class DiscussionsController < AjaxController
2
2
  include Mumuki::Laboratory::Controllers::Content
3
3
 
4
+ before_action :validate_forum_enabled!
5
+ before_action :validate_not_in_exam!
4
6
  before_action :set_debatable, except: [:subscription]
5
7
  before_action :authenticate!, only: [:update, :create]
6
8
  before_action :discussion_filter_params, only: :index
@@ -62,4 +64,12 @@ class DiscussionsController < AjaxController
62
64
  def discussion_filter_params
63
65
  @filter_params ||= params.permit(Discussion.permitted_query_params)
64
66
  end
67
+
68
+ def validate_forum_enabled!
69
+ raise Mumuki::Laboratory::NotFoundError unless Organization.current.forum_enabled?
70
+ end
71
+
72
+ def validate_not_in_exam!
73
+ raise Mumuki::Laboratory::BlockedForumError if current_user&.currently_in_exam?
74
+ end
65
75
  end