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
@@ -1,5 +1,6 @@
1
1
  class DiscussionsMessagesController < AjaxController
2
2
  before_action :set_discussion!, only: [:create, :destroy]
3
+ before_action :authorize!, only: [:destroy, :approve]
3
4
 
4
5
  def create
5
6
  @discussion.submit_message! message_params, current_user
@@ -7,18 +8,29 @@ class DiscussionsMessagesController < AjaxController
7
8
  end
8
9
 
9
10
  def destroy
10
- message = Message.find(params[:id])
11
- message.authorize! current_user
12
- message.destroy!
11
+ current_message.destroy!
13
12
  redirect_back(fallback_location: root_path)
14
13
  end
15
14
 
15
+ def approve
16
+ current_message.toggle_approved!
17
+ head :ok
18
+ end
19
+
16
20
  private
17
21
 
18
22
  def set_discussion!
19
23
  @discussion ||= Discussion.find_by(id: params[:discussion_id])
20
24
  end
21
25
 
26
+ def authorize!
27
+ current_message.authorize! current_user
28
+ end
29
+
30
+ def current_message
31
+ @message ||= Message.find(params[:id])
32
+ end
33
+
22
34
  def message_params
23
35
  params.require(:message).permit(:content)
24
36
  end
@@ -16,7 +16,7 @@ module ApplicationHelper
16
16
  end
17
17
 
18
18
  def profile_picture
19
- image_tag(@current_user.image_url, height: 40, class: 'img-circle', onError: "this.onerror = null; this.src = '#{image_url('user_shape.png')}'")
19
+ image_tag(current_user.image_url, height: 40, class: 'img-circle', onError: "this.onerror = null; this.src = '#{image_url('user_shape.png')}'")
20
20
  end
21
21
 
22
22
  def paginate(object, options={})
@@ -1,18 +1,26 @@
1
1
  module DiscussionsHelper
2
2
  def read_discussions_link(item)
3
- discussions_link t(:solve_your_doubts), item_discussions_path(item, default_discussions_params)
3
+ discussions_link others_discussions_icon(t(:solve_your_doubts)), item_discussions_path(item, default_discussions_params)
4
4
  end
5
5
 
6
6
  def solve_discussions_link
7
- discussions_link t(:solve_doubts), discussions_path(solve_discussion_params_for(current_user))
7
+ discussions_link others_discussions_icon(t(:solve_doubts)), discussions_path(solve_discussion_params_for(current_user))
8
8
  end
9
9
 
10
- def discussions_icon(text)
10
+ def user_discussions_link
11
+ discussions_link user_discussions_icon(t(:my_doubts)), user_path(anchor: 'discussions') if current_user.watched_discussions.present?
12
+ end
13
+
14
+ def others_discussions_icon(text)
11
15
  fixed_fa_icon 'comments', text: text
12
16
  end
13
17
 
14
- def discussions_link(text, path, organization=Organization.current)
15
- link_to discussions_icon(text), path if organization.forum_enabled?
18
+ def user_discussions_icon(text)
19
+ fixed_fa_icon 'comment', text: text
20
+ end
21
+
22
+ def discussions_link(item, path, organization=Organization.current)
23
+ link_to item, path if organization.forum_enabled?
16
24
  end
17
25
 
18
26
  def item_discussion_path(discussion, params={})
@@ -0,0 +1,9 @@
1
+ module MultipleFileEditorHelper
2
+ def highlight_modes
3
+ Language.all.map { |it| { extension: it.extension, highlight_mode: it.highlight_mode } }
4
+ end
5
+
6
+ def multifile_locales
7
+ :insert_file_name.try { |it| { it => t(it) } }
8
+ end
9
+ end
@@ -1,6 +1,8 @@
1
1
  class ApplicationRecord < ActiveRecord::Base
2
2
  self.abstract_class = true
3
3
 
4
+ delegate :whitelist_attributes, to: :class
5
+
4
6
  def self.defaults(&block)
5
7
  after_initialize :defaults, if: :new_record?
6
8
  define_method :defaults, &block
@@ -77,4 +79,7 @@ class ApplicationRecord < ActiveRecord::Base
77
79
  obj
78
80
  end
79
81
 
82
+ def self.whitelist_attributes(a_hash, options={})
83
+ a_hash.slice(*attribute_names).except(*options[:except])
84
+ end
80
85
  end
@@ -6,15 +6,13 @@ module WithAssignments
6
6
  end
7
7
 
8
8
  def current_content_for(user)
9
- assignment_for(user).try(&:solution) ||
10
- default_content_for(user)
9
+ assignment_for(user)&.solution || default_content_for(user)
11
10
  end
12
11
 
13
12
  def files_for(user)
14
13
  language
15
14
  .directives_sections
16
- .split_sections(assignment_for(user)&.solution || default_content_for(user))
17
- .except('content')
15
+ .split_sections(current_content_for user)
18
16
  .map { |name, content| Mumuki::Laboratory::File.new name, content }
19
17
  end
20
18
 
@@ -55,24 +53,8 @@ module WithAssignments
55
53
  assignments.find_by(submitter: user)
56
54
  end
57
55
 
58
- def solved_by?(user)
59
- !!assignment_for(user).try(&:passed?)
60
- end
61
-
62
- def assigned_to?(user)
63
- assignments.exists?(submitter: user)
64
- end
65
-
66
56
  def status_for(user)
67
- assignment_for(user).defaulting(Mumuki::Laboratory::Status::Submission::Unknown, &:status) if user
68
- end
69
-
70
- def last_submission_date_for(user)
71
- assignment_for(user).try(&:updated_at)
72
- end
73
-
74
- def submissions_count_for(user)
75
- assignment_for(user).try(&:submissions_count) || 0
57
+ assignment_for(user).defaulting(Mumuki::Laboratory::Status::Submission::Pending, &:status) if user
76
58
  end
77
59
 
78
60
  def find_or_init_assignment_for(user)
@@ -2,7 +2,7 @@ class Discussion < ApplicationRecord
2
2
  include WithDiscussionStatus, ParentNavigation, WithScopedQueries, Contextualization
3
3
 
4
4
  belongs_to :item, polymorphic: true
5
- has_many :messages
5
+ has_many :messages, -> { order(:created_at) }
6
6
  belongs_to :initiator, class_name: 'User'
7
7
  belongs_to :exercise, foreign_type: :exercise, foreign_key: 'item_id'
8
8
  has_many :subscriptions
data/app/models/exam.rb CHANGED
@@ -30,6 +30,10 @@ class Exam < ApplicationRecord
30
30
  enabled_range_for(user).cover? DateTime.now
31
31
  end
32
32
 
33
+ def in_progress_for?(user)
34
+ accessible_for?(user) && started?(user)
35
+ end
36
+
33
37
  def validate_accessible_for!(user)
34
38
  if user.present?
35
39
  raise Mumuki::Laboratory::ForbiddenError unless authorized?(user)
@@ -97,25 +101,22 @@ class Exam < ApplicationRecord
97
101
  end
98
102
 
99
103
  def self.import_from_json!(json)
100
- json.except!(:social_ids, :sender)
101
- organization = Organization.find_by!(name: json.delete(:organization))
104
+ exam_data = json.with_indifferent_access
105
+ organization = Organization.find_by!(name: exam_data[:organization])
102
106
  organization.switch!
103
- exam_data = parse_json json
107
+ adapt_json_values exam_data
104
108
  remove_previous_version exam_data[:eid], exam_data[:guide_id]
105
- users = exam_data.delete(:users)
106
- exam = where(classroom_id: exam_data.delete(:eid)).update_or_create! exam_data
107
- exam.process_users users
109
+ exam = where(classroom_id: exam_data[:eid]).update_or_create!(whitelist_attributes(exam_data))
110
+ exam.process_users exam_data[:users]
108
111
  exam.index_usage! organization
109
112
  exam
110
113
  end
111
114
 
112
- def self.parse_json(exam_json)
113
- exam = exam_json.except(:name, :language)
114
- exam[:guide_id] = Guide.find_by(slug: exam.delete(:slug)).id
115
+ def self.adapt_json_values(exam)
116
+ exam[:guide_id] = Guide.find_by(slug: exam[:slug]).id
115
117
  exam[:organization_id] = Organization.current.id
116
- exam[:users] = exam.delete(:uids).map { |uid| User.find_by(uid: uid) }.compact
118
+ exam[:users] = exam[:uids].map { |uid| User.find_by(uid: uid) }.compact
117
119
  [:start_time, :end_time].each { |param| exam[param] = exam[param].to_time }
118
- exam
119
120
  end
120
121
 
121
122
  def self.remove_previous_version(eid, guide_id)
@@ -82,7 +82,7 @@ class Exercise < ApplicationRecord
82
82
 
83
83
  reset!
84
84
 
85
- attrs = json.except('type', 'id', 'solution', 'language', 'teacher_info', 'choices')
85
+ attrs = whitelist_attributes(json, except: %w(type id))
86
86
  attrs['choices'] = json['choices'].map { |choice| choice['value'] } if json['choices'].present?
87
87
  attrs['bibliotheca_id'] = json['id']
88
88
  attrs['number'] = number
data/app/models/guide.rb CHANGED
@@ -58,7 +58,7 @@ class Guide < Content
58
58
  end
59
59
 
60
60
  def import_from_json!(json)
61
- self.assign_attributes json.except('exercises', 'language', 'id_format', 'id', 'teacher_info', 'collaborators')
61
+ self.assign_attributes whitelist_attributes(json, except: ['id'])
62
62
  self.language = Language.for_name(json['language'])
63
63
  self.save!
64
64
 
@@ -33,7 +33,7 @@ class Message < ApplicationRecord
33
33
  end
34
34
 
35
35
  def as_platform_json
36
- as_json(except: [:id, :type, :discussion_id],
36
+ as_json(except: [:id, :type, :discussion_id, :approved],
37
37
  include: {exercise: {only: [:bibliotheca_id]}})
38
38
  .merge(organization: Organization.current.name)
39
39
  end
@@ -42,6 +42,10 @@ class Message < ApplicationRecord
42
42
  update! read: true
43
43
  end
44
44
 
45
+ def toggle_approved!
46
+ toggle! :approved
47
+ end
48
+
45
49
  def self.parse_json(json)
46
50
  message = json.delete 'message'
47
51
  json
data/app/models/stats.rb CHANGED
@@ -1,49 +1,24 @@
1
1
  class Stats
2
2
  include ActiveModel::Model
3
3
 
4
- attr_accessor :passed, :passed_with_warnings, :failed, :unknown
5
-
6
- def total
7
- submitted + unknown
8
- end
4
+ attr_accessor :passed, :passed_with_warnings, :failed, :pending
9
5
 
10
6
  def submitted
11
- failed + resolved
12
- end
13
-
14
- def pending
15
- failed + unknown
16
- end
17
-
18
- def resolved
19
- passed + passed_with_warnings
7
+ passed + passed_with_warnings + failed
20
8
  end
21
9
 
22
10
  def done?
23
- pending == 0
11
+ failed + pending == 0
24
12
  end
25
13
 
26
14
  def started?
27
15
  submitted > 0
28
16
  end
29
17
 
30
- def to_h(&key)
31
- {key.call(:passed) => passed,
32
- key.call(:passed_with_warnings) => passed_with_warnings,
33
- key.call(:failed) => failed,
34
- key.call(:unknown) => unknown}
35
- end
36
-
37
18
  def self.from_statuses(statuses)
38
- Stats.new(statuses.inject({passed: 0, passed_with_warnings: 0, failed: 0, unknown: 0}) do |accum, status|
19
+ Stats.new(statuses.inject({passed: 0, passed_with_warnings: 0, failed: 0, pending: 0}) do |accum, status|
39
20
  accum[status.group.to_sym] += 1
40
21
  accum
41
22
  end)
42
23
  end
43
-
44
- private
45
-
46
- def ratio(x)
47
- (100 * x / total.to_f).round(2)
48
- end
49
24
  end
data/app/models/user.rb CHANGED
@@ -26,6 +26,8 @@ class User < ApplicationRecord
26
26
 
27
27
  has_many :exam_authorizations
28
28
 
29
+ has_many :exams, through: :exam_authorizations
30
+
29
31
  after_initialize :init
30
32
 
31
33
  before_validation :set_uid!
@@ -135,6 +137,10 @@ class User < ApplicationRecord
135
137
  assignments.each { |it| it.notify! rescue nil }
136
138
  end
137
139
 
140
+ def currently_in_exam?
141
+ exams.any? { |e| e.in_progress_for? self }
142
+ end
143
+
138
144
  private
139
145
 
140
146
  def set_uid!
@@ -11,6 +11,9 @@
11
11
  <% end %>
12
12
  <% if message.authorized? current_user %>
13
13
  <span class="actions">
14
+ <a class="discussion-message-approved <%= 'selected' if message.approved? %>" onclick="mumuki.Forum.discussionMessageToggleApprove('<%= approve_discussion_message_url(@discussion, message) %>', $(this))">
15
+ <%= fa_icon(:check, class: 'fa-xs') %>
16
+ </a>
14
17
  <%= link_to fa_icon('trash-o'), discussion_message_path(@discussion, message), method: :delete, data: { confirm: t(:are_you_sure, action: t(:destroy_message)) } %>
15
18
  </span>
16
19
  <% end %>
@@ -7,7 +7,7 @@
7
7
  <%= t(:error_description, error: link_to_status_codes(403)).html_safe %>
8
8
  </p>
9
9
  <p>
10
- <%= Organization.current.explain_error(403, :forbidden_explanation).html_safe %>
10
+ <%= Organization.current.explain_error(403, explanation).html_safe %>
11
11
  </p>
12
12
  <p>
13
13
  <%= t(:contact_administrator, link: mail_to_administrator).html_safe %>
@@ -1,6 +1,11 @@
1
1
  <% if guide.authors.present? %>
2
2
  <%= content_for :authoring do %>
3
3
  <p class="small">
4
+ <% if embedded_mode? %>
5
+ <span class="hidden-md hidden-lg hidden-xl">
6
+ <%= render partial: 'layouts/copyright' %> -
7
+ </span>
8
+ <% end %>
4
9
  <%= raw t :authoring_note, authors: guide.authors, collaborators: "https://raw.githubusercontent.com/#{guide.slug}/master/COLLABORATORS.txt" %>
5
10
  </p>
6
11
  <% end %>
@@ -0,0 +1,2 @@
1
+ &copy; Copyright 2015-<%= DateTime.now.year %>
2
+ <a href="http://mumuki.org/" class="mu-org-link"><span class="da da-mumuki-circle"></span> Mumuki Project</a>
@@ -0,0 +1,4 @@
1
+ <a class="fa fa-facebook social-icon" aria-label="Facebook" href="https://www.facebook.com/MumukiProject" target="_blank"></a>
2
+ <a class="fa fa-twitter social-icon" aria-label="Twitter" href="https://twitter.com/MumukiProject" target="_blank"></a>
3
+ <a class="fa fa-github social-icon" aria-label="Github" href="https://github.com/mumuki" target="_blank"></a>
4
+ <a class="fa fa-linkedin social-icon" aria-label="LinkedIn" href="https://www.linkedin.com/company/mumuki-project" target="_blank"></a>
@@ -37,6 +37,7 @@
37
37
  <li><%= link_to_classroom %></li>
38
38
  <li><%= link_to_bibliotheca %></li>
39
39
  <li><%= solve_discussions_link %></li>
40
+ <li><%= user_discussions_link %></li>
40
41
  <li class="divider"></li>
41
42
  <li><%= link_to(t(:sign_out), logout_path(origin: url_for), role: 'menuitem') %></li>
42
43
  </ul>
@@ -62,8 +63,8 @@
62
63
 
63
64
  <div id="footer-copyright" class="row">
64
65
  <div class="col-md-4 text-left">
65
- <p>&copy; Copyright 2015-<%= DateTime.now.year %>
66
- <a href="http://mumuki.org/" class="mu-org-link"><span class="da da-mumuki-circle"></span> Mumuki Project</a>
66
+ <p>
67
+ <%= render partial: 'layouts/copyright' %>
67
68
  </p>
68
69
  </div>
69
70
 
@@ -72,10 +73,7 @@
72
73
  </div>
73
74
 
74
75
  <div id="footer-social" class="col-md-4 text-right" lang="en">
75
- <a class="fa fa-facebook social-icon" aria-label="Facebook" href="https://www.facebook.com/MumukiProject" target="_blank"></a>
76
- <a class="fa fa-twitter social-icon" aria-label="Twitter" href="https://twitter.com/MumukiProject" target="_blank"></a>
77
- <a class="fa fa-github social-icon" aria-label="Github" href="https://github.com/mumuki" target="_blank"></a>
78
- <a class="fa fa-linkedin social-icon" aria-label="LinkedIn" href="https://www.linkedin.com/company/mumuki-project" target="_blank"></a>
76
+ <%= render partial: 'layouts/social_media' %>
79
77
  </div>
80
78
  </div>
81
79
  </div>
@@ -1 +1,28 @@
1
+ <% content_for :footer do %>
2
+ <footer class="footer">
3
+ <div class="<%= exercise_container_type %>">
4
+ <hr>
5
+
6
+ <div class="row">
7
+ <div class="col-md-12">
8
+ <%= yield :authoring %>
9
+ </div>
10
+ </div>
11
+
12
+ <div id="footer-copyright" class="row hidden-xs hidden-sm">
13
+ <div class="col-sm-8 text-left">
14
+ <p>
15
+ <%= render partial: 'layouts/copyright' %>
16
+ </p>
17
+ </div>
18
+
19
+ <div id="footer-social" class="col-sm-4 text-right" lang="en">
20
+ <%= render partial: 'layouts/social_media' %>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </footer>
25
+ <% end %>
26
+
27
+
1
28
  <%= render partial: 'layouts/main' %>
@@ -4,15 +4,20 @@
4
4
  <span class="files-tabs">
5
5
  <ul class="nav nav-tabs">
6
6
  <% @files.each_with_index do |file, index| %>
7
- <li role="presentation" class="<%= 'active' if index == 0 %>" data-target="#editor-file-<%= index %>" tabindex='0' data-toggle='tab'>
8
- <a href="#"><%= file.name %></a>
7
+ <li role="presentation" class="file-tab <%= 'active' if index == 0 %>" data-target="#editor-file-<%= index %>" tabindex='0' data-toggle='tab'>
8
+ <a class="file-name" href="#"><%= file.name %></a> <i class="delete-file-button fa fa-times"></i>
9
9
  </li>
10
10
  <% end %>
11
11
  </ul>
12
+ <i class="add-file-button fa fa-plus"></i>
12
13
  </span>
14
+
15
+ <input id="highlight-modes" type="hidden" value="<%=highlight_modes.to_json%>" />
16
+ <input id="multifile-locales" type="hidden" value="<%=multifile_locales.to_json%>" />
17
+
13
18
  <div class="tab-content">
14
19
  <% @files.each_with_index do |file, index| %>
15
- <div role="tabpanel" class="tab-pane mu-input-panel <%= 'fade in active' if index == 0 %>" id="editor-file-<%= index %>">
20
+ <div role="tabpanel" class="file-editor tab-pane mu-input-panel <%= 'fade in active' if index == 0 %>" id="editor-file-<%= index %>">
16
21
  <%= form.editor "content[#{file.name}]", file.highlight_mode,
17
22
  placeholder: t(:editor_placeholder),
18
23
  class: 'form-control editor',