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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/application/codemirror-builder.js +16 -8
- data/app/assets/javascripts/application/codemirror.js +7 -9
- data/app/assets/javascripts/application/discussions.js +20 -12
- data/app/assets/javascripts/application/multiple-files.js +222 -0
- data/app/assets/javascripts/application/submission.js +1 -0
- data/app/assets/stylesheets/application/modules/_discussion.scss +12 -0
- data/app/assets/stylesheets/application/modules/_editor.scss +13 -0
- data/app/controllers/discussions_controller.rb +10 -0
- data/app/controllers/discussions_messages_controller.rb +15 -3
- data/app/helpers/application_helper.rb +1 -1
- data/app/helpers/discussions_helper.rb +13 -5
- data/app/helpers/multiple_file_editor_helper.rb +9 -0
- data/app/models/application_record.rb +5 -0
- data/app/models/concerns/with_assignments.rb +3 -21
- data/app/models/discussion.rb +1 -1
- data/app/models/exam.rb +12 -11
- data/app/models/exercise.rb +1 -1
- data/app/models/guide.rb +1 -1
- data/app/models/message.rb +5 -1
- data/app/models/stats.rb +4 -29
- data/app/models/user.rb +6 -0
- data/app/views/discussions/_message.html.erb +3 -0
- data/app/views/errors/forbidden.html.erb +1 -1
- data/app/views/layouts/_authoring.html.erb +5 -0
- data/app/views/layouts/_copyright.html.erb +2 -0
- data/app/views/layouts/_social_media.html.erb +4 -0
- data/app/views/layouts/application.html.erb +4 -6
- data/app/views/layouts/embedded.html.erb +27 -0
- data/app/views/layouts/exercise_inputs/editors/_multiple_files.html.erb +8 -3
- data/config/routes.rb +3 -1
- data/db/migrate/20180802190437_add_approved_to_messages.rb +5 -0
- data/lib/mumuki/laboratory/controllers/dynamic_errors.rb +6 -1
- data/lib/mumuki/laboratory/controllers/notifications.rb +3 -2
- data/lib/mumuki/laboratory/exceptions.rb +1 -0
- data/lib/mumuki/laboratory/exceptions/blocked_forum_error.rb +2 -0
- data/lib/mumuki/laboratory/locales/en.yml +3 -1
- data/lib/mumuki/laboratory/locales/es.yml +3 -2
- data/lib/mumuki/laboratory/locales/pt.yml +3 -2
- data/lib/mumuki/laboratory/mumukit/directives.rb +6 -5
- data/lib/mumuki/laboratory/status/submission/pending.rb +1 -5
- data/lib/mumuki/laboratory/status/submission/running.rb +1 -1
- data/lib/mumuki/laboratory/status/submission/submission.rb +0 -1
- data/lib/mumuki/laboratory/version.rb +1 -1
- data/spec/controllers/discussions_controller_spec.rb +1 -0
- data/spec/controllers/exercise_solutions_controller_spec.rb +1 -1
- data/spec/controllers/organizations_api_controller_spec.rb +1 -1
- data/spec/dummy/db/schema.rb +2 -1
- data/spec/factories/api_client_factory.rb +3 -3
- data/spec/factories/assignments_factory.rb +1 -1
- data/spec/factories/chapter_factory.rb +1 -1
- data/spec/factories/course_factory.rb +5 -5
- data/spec/factories/discussion_factory.rb +2 -2
- data/spec/factories/exercise_factory.rb +24 -26
- data/spec/factories/guide_factory.rb +3 -3
- data/spec/factories/login_settings_factory.rb +1 -1
- data/spec/factories/message_factory.rb +1 -1
- data/spec/factories/organization_factory.rb +9 -9
- data/spec/factories/topic_factory.rb +1 -1
- data/spec/features/choose_organization_spec.rb +49 -42
- data/spec/models/exercise_spec.rb +3 -27
- data/spec/models/query_spec.rb +1 -1
- data/spec/models/question_spec.rb +2 -2
- data/spec/models/stats_spec.rb +2 -9
- data/spec/models/user_spec.rb +13 -0
- metadata +8 -3
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ca436cc42496e79bddfaff35a982a8f86dfbac83a5899152e6870526e8319eb
|
4
|
+
data.tar.gz: 60d05b8275511ab5f817fb6a2d6c10e756a7d362d9937ea071345262d4a088ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
65
|
-
if (
|
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',
|
69
|
+
this.editor.setOption('mode', highlightMode);
|
69
70
|
this.editor.refresh();
|
70
71
|
}
|
72
|
+
|
73
|
+
return this;
|
71
74
|
},
|
72
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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.
|
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
|
-
|
49
|
-
|
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
|
+
});
|
@@ -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
|