mumuki-domain 7.6.1 → 7.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/assignment.rb +2 -0
  3. data/app/models/book.rb +4 -2
  4. data/app/models/concerns/contextualization.rb +1 -1
  5. data/app/models/concerns/navigation/siblings_navigation.rb +2 -2
  6. data/app/models/concerns/submittable/solvable.rb +9 -3
  7. data/app/models/concerns/submittable/submittable.rb +1 -1
  8. data/app/models/concerns/with_assignments.rb +1 -1
  9. data/app/models/concerns/with_discussions.rb +9 -6
  10. data/app/models/concerns/with_layout.rb +5 -1
  11. data/app/models/concerns/with_progress.rb +1 -1
  12. data/app/models/concerns/with_scoped_queries.rb +9 -9
  13. data/app/models/concerns/with_scoped_queries/page.rb +1 -1
  14. data/app/models/concerns/with_scoped_queries/sort.rb +6 -1
  15. data/app/models/course.rb +13 -7
  16. data/app/models/discussion.rb +51 -13
  17. data/app/models/exam.rb +53 -23
  18. data/app/models/exam/passing_criterion.rb +53 -0
  19. data/app/models/exercise/challenge.rb +1 -1
  20. data/app/models/exercise/problem.rb +6 -0
  21. data/app/models/exercise/reading.rb +4 -0
  22. data/app/models/guide.rb +1 -1
  23. data/app/models/invitation.rb +7 -1
  24. data/app/models/message.rb +28 -4
  25. data/app/models/organization.rb +7 -11
  26. data/app/models/user.rb +64 -0
  27. data/db/migrate/20200601203033_add_course_to_exam.rb +5 -0
  28. data/db/migrate/20200605161350_add_passing_criterions_to_exam.rb +6 -0
  29. data/db/migrate/20200702165503_add_messages_count_to_discussion.rb +6 -0
  30. data/db/migrate/20200728162727_add_not_actually_a_question_field_to_messages.rb +5 -0
  31. data/db/migrate/20200728163038_add_requires_moderator_response_to_discussions.rb +5 -0
  32. data/db/migrate/20200730221001_add_trusted_for_forum_to_user.rb +5 -0
  33. data/db/migrate/20200731081757_add_last_moderator_access_fields_to_discussion.rb +6 -0
  34. data/db/migrate/20200804191643_add_incognito_mode_enabled_to_organization.rb +5 -0
  35. data/lib/mumuki/domain.rb +1 -0
  36. data/lib/mumuki/domain/factories/course_factory.rb +2 -0
  37. data/lib/mumuki/domain/factories/discussion_factory.rb +2 -2
  38. data/lib/mumuki/domain/factories/exam_factory.rb +1 -0
  39. data/lib/mumuki/domain/factories/message_factory.rb +1 -6
  40. data/lib/mumuki/domain/incognito.rb +112 -0
  41. data/lib/mumuki/domain/locales/activerecord/es-CL.yml +59 -0
  42. data/lib/mumuki/domain/locales/console_submission/es-CL.yml +3 -0
  43. data/lib/mumuki/domain/organization/profile.rb +2 -1
  44. data/lib/mumuki/domain/organization/settings.rb +12 -9
  45. data/lib/mumuki/domain/status/discussion/discussion.rb +0 -8
  46. data/lib/mumuki/domain/submission/base.rb +11 -1
  47. data/lib/mumuki/domain/submission/solution.rb +3 -1
  48. data/lib/mumuki/domain/version.rb +1 -1
  49. metadata +19 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdb048ee9a17b03d534fd49140b3a30a014af1110ac0f50fc0b25243a6a069ee
4
- data.tar.gz: 3790eb4813fdae432c358ceaee88047a4f45a63cbe89c10a22e48f10ddde0d91
3
+ metadata.gz: 6f501e48aa71e7422741f0223c9ac221b1536bf0171dfd58a5ab2158d4cc215a
4
+ data.tar.gz: fe3c3520675d5b9e066dfeb7682d86dc42522afb88d48e3e81245b3f28a750ea
5
5
  SHA512:
6
- metadata.gz: a34d18802a4a8178bbacfcc8e7e2c403a8da4493062639309bc6fdabc557d8d6655aa0f173530113859633bac7de69d8426c39934cfc6c9bee4ce62e8cffaf49
7
- data.tar.gz: 71852152c330213e65cacebfa5ee1256f1efbb940088cf5a2e89081d74fbaaf27b02af844f167fffdab9a9514016c3defa060e0665c08f9d85c7cf4e2b79012c
6
+ metadata.gz: 7925dac13f2ba1b0e5e5494bd13c167c521fee0dec2ffe5a1024cdc0fc4000ba2ad82786a2d5aa96f354ae147689cc2b763dc03cafa940cd77c1a8fd137e0a71
7
+ data.tar.gz: 3f51ba4d74fbd60a6fe1a95e6602ec1539d5a63c185d9468b9a5915af14103d9f264cd0f1a0dc0d7084db444c6304c6129130deab4a5f80f7a6e264a52398070
@@ -20,6 +20,8 @@ class Assignment < Progress
20
20
  delegate :language, :name, :navigable_parent, :settings,
21
21
  :limited?, :input_kids?, :choice?, :results_hidden?, to: :exercise
22
22
 
23
+ delegate :completed?, :solved?, to: :submission_status
24
+
23
25
  alias_attribute :status, :submission_status
24
26
  alias_attribute :attempts_count, :attemps_count
25
27
 
@@ -8,8 +8,6 @@ class Book < Content
8
8
  has_many :complements, dependent: :destroy
9
9
 
10
10
  has_many :exercises, through: :chapters
11
- has_many :discussions, through: :exercises
12
- organic_on :discussions
13
11
 
14
12
  delegate :first_lesson, to: :first_chapter
15
13
 
@@ -19,6 +17,10 @@ class Book < Content
19
17
  slug
20
18
  end
21
19
 
20
+ def discussions_in_organization(organization = Organization.current)
21
+ Discussion.where(organization: organization).includes(exercise: [:language, :guide])
22
+ end
23
+
22
24
  def first_chapter
23
25
  chapters.first
24
26
  end
@@ -25,7 +25,7 @@ module Contextualization
25
25
 
26
26
  delegate :visible_success_output?, to: :exercise
27
27
  delegate :output_content_type, to: :language
28
- delegate :should_retry?, :to_submission_status, :completed?, :solved?, *Mumuki::Domain::Status::Submission.test_selectors, to: :submission_status
28
+ delegate :should_retry?, :to_submission_status, *Mumuki::Domain::Status::Submission.test_selectors, to: :submission_status
29
29
  delegate :inspection_keywords, to: :exercise
30
30
  end
31
31
 
@@ -1,11 +1,11 @@
1
1
  module SiblingsNavigation
2
2
 
3
3
  def next_for(user)
4
- pending_siblings_for(user).select { |it| it.number > number }.sort_by(&:number).first
4
+ user.pending_siblings_at(self).select { |it| it.number > number }.sort_by(&:number).first
5
5
  end
6
6
 
7
7
  def restart(user)
8
- pending_siblings_for(user).sort_by(&:number).first
8
+ user.pending_siblings_at(self).sort_by(&:number).first
9
9
  end
10
10
 
11
11
  def siblings
@@ -1,13 +1,19 @@
1
1
  module Solvable
2
- def submit_solution!(user, attributes={})
3
- assignment, _ = find_assignment_and_submit! user, attributes[:content].to_mumuki_solution(language)
4
- try_solve_discussions(user) if assignment.solved?
2
+ def submit_solution!(user, submission_attributes={})
3
+ assignment, _ = find_assignment_and_submit! user, solution_for(submission_attributes)
4
+ try_solve_discussions!(user) if assignment.solved?
5
5
  assignment
6
6
  end
7
7
 
8
8
  def run_tests!(params)
9
9
  language.run_tests!(params.merge(locale: locale, expectations: expectations, custom_expectations: custom_expectations))
10
10
  end
11
+
12
+ def solution_for(submission_attributes)
13
+ submission_attributes[:content]
14
+ .to_mumuki_solution(language)
15
+ .with_client_result(submission_attributes[:client_result])
16
+ end
11
17
  end
12
18
 
13
19
  class NilClass
@@ -5,7 +5,7 @@ module Submittable
5
5
 
6
6
  def find_assignment_and_submit!(user, submission)
7
7
  assignment = assignment_for user
8
- results = submission.run! assignment, evaluation_class.new
8
+ results = user.run_submission! submission, assignment, evaluation_class.new
9
9
  [assignment, results]
10
10
  end
11
11
  end
@@ -28,6 +28,6 @@ module WithAssignments
28
28
  end
29
29
 
30
30
  def assignment_for(user, organization=Organization.current)
31
- find_assignment_for(user, organization) || user.assignments.build(exercise: self, organization: organization)
31
+ find_assignment_for(user, organization) || user.build_assignment(self, organization)
32
32
  end
33
33
  end
@@ -7,18 +7,21 @@ module WithDiscussions
7
7
  end
8
8
 
9
9
  def discuss!(user, discussion, organization = Organization.current)
10
- discussion.merge!(initiator_id: user.id, organization: organization)
11
- discussion.merge!(submission: submission_for(user)) if submission_for(user).present?
12
- created_discussion = discussions.create discussion
13
- user.subscribe_to! created_discussion
14
- created_discussion
10
+ new_discussion_for(user, discussion, organization).tap &:save!
15
11
  end
16
12
 
17
13
  def submission_for(_)
18
14
  nil
19
15
  end
20
16
 
21
- def try_solve_discussions(user)
17
+ def try_solve_discussions!(user)
22
18
  discussions.where(initiator: user).map(&:try_solve!)
23
19
  end
20
+
21
+ def new_discussion_for(user, discussion_h = {}, organization = Organization.current)
22
+ discussion_h.merge!(initiator_id: user.id, organization: organization)
23
+ discussion_h.merge!(submission: submission_for(user)) if submission_for(user).present?
24
+ discussions.new discussion_h
25
+ end
26
+
24
27
  end
@@ -2,6 +2,10 @@ module WithLayout
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
- enum layout: [:input_right, :input_bottom, :input_kids]
5
+ enum layout: [:input_right, :input_bottom, :input_primary, :input_kindergarten]
6
+ end
7
+
8
+ def input_kids?
9
+ input_primary? || input_kindergarten?
6
10
  end
7
11
  end
@@ -1,6 +1,6 @@
1
1
  module WithProgress
2
2
  def progress_for(user, organization)
3
- Indicator.find_or_initialize_by(user: user, organization: organization, content: self)
3
+ user.progress_at(self, organization)
4
4
  end
5
5
 
6
6
  def completion_percentage_for(user, organization=Organization.current)
@@ -16,8 +16,8 @@ module WithScopedQueries
16
16
  end
17
17
 
18
18
  class_methods do
19
- def query_methods
20
- queriable_attributes.keys
19
+ def query_methods(excluded_methods)
20
+ queriable_attributes.keys - excluded_methods.to_a
21
21
  end
22
22
 
23
23
  def scoped_query_module(method)
@@ -28,19 +28,19 @@ module WithScopedQueries
28
28
  queriable_attributes.values.flatten
29
29
  end
30
30
 
31
- def actual_params(params, excluded_param)
32
- params.reject { |it| it == excluded_param.to_s }
31
+ def actual_params(params, excluded_params)
32
+ params.except(*excluded_params)
33
33
  end
34
34
 
35
- def scoped_query_by(params, excluded_param=nil)
36
- query_methods.inject(all) do |scope, method|
37
- valid_params = valid_params_for(method, params, excluded_param)
35
+ def scoped_query_by(params, **options)
36
+ query_methods(options[:excluded_methods]).inject(all) do |scope, method|
37
+ valid_params = valid_params_for(method, params, options[:excluded_params])
38
38
  scoped_query_module(method).query_by valid_params, scope, self
39
39
  end
40
40
  end
41
41
 
42
- def valid_params_for(method, params, excluded_param)
43
- actual_params = actual_params(params, excluded_param)
42
+ def valid_params_for(method, params, excluded_params)
43
+ actual_params = actual_params(params, excluded_params)
44
44
  actual_params.permit queriable_attributes[method]
45
45
  end
46
46
  end
@@ -1,7 +1,7 @@
1
1
  module WithScopedQueries::Page
2
2
  def self.query_by(params, current_scope, _)
3
3
  page_param = params[:page] || 1
4
- current_scope.page(page_param)
4
+ current_scope.page(page_param).per(10)
5
5
  end
6
6
 
7
7
  def self.add_queriable_attributes_to(klass, _)
@@ -34,13 +34,18 @@ module WithScopedQueries::Sort
34
34
  end
35
35
 
36
36
  class_methods do
37
+ def opposite(direction)
38
+ dir = direction.to_s.downcase.to_sym
39
+ [:asc, :desc].find { |it| it != dir }
40
+ end
41
+
37
42
  def sorting_filters
38
43
  sorting_fields.product([:asc, :desc]).map do |it|
39
44
  "#{it.first}_#{it.second}"
40
45
  end
41
46
  end
42
47
 
43
- def sorting_params_allowed?(sort_param, direction_param=nil)
48
+ def sorting_params_allowed?(sort_param, direction_param = nil)
44
49
  sorting_fields.include?(sort_param.to_sym) && [:asc, :desc].include?(direction_param&.to_sym)
45
50
  end
46
51
  end
@@ -14,7 +14,7 @@ class Course < ApplicationRecord
14
14
  resource_fields :slug, :shifts, :code, :days, :period, :description
15
15
 
16
16
  def current_invitation
17
- invitations.where('expiration_date > ?', Time.now).take
17
+ invitations.where('expiration_date > ?', Time.now).first
18
18
  end
19
19
 
20
20
  def import_from_resource_h!(resource_h)
@@ -22,10 +22,8 @@ class Course < ApplicationRecord
22
22
  end
23
23
 
24
24
  def slug=(slug)
25
- s = Mumukit::Auth::Slug.parse(slug)
26
-
27
- self[:slug] = slug
28
- self[:code] = s.course
25
+ s = slug.to_mumukit_slug
26
+ self[:slug] = slug.to_s
29
27
  self[:organization_id] = Organization.locate!(s.organization).id
30
28
  end
31
29
 
@@ -42,8 +40,8 @@ class Course < ApplicationRecord
42
40
  end
43
41
 
44
42
  def generate_invitation!(expiration_date)
45
- invitation = invitations.build expiration_date: expiration_date, course: self
46
- invitation.save_and_notify!
43
+ invitations.create expiration_date: expiration_date, course: self
44
+ current_invitation
47
45
  end
48
46
 
49
47
  def self.sync_key_id_field
@@ -53,4 +51,12 @@ class Course < ApplicationRecord
53
51
  def to_organization
54
52
  organization
55
53
  end
54
+
55
+ def to_s
56
+ slug.to_s
57
+ end
58
+
59
+ def self.allowed_for(user, organization = Organization.current)
60
+ where(organization: organization).select { |course| user.teacher_of? course.slug }
61
+ end
56
62
  end
@@ -4,25 +4,29 @@ class Discussion < ApplicationRecord
4
4
  belongs_to :item, polymorphic: true
5
5
  has_many :messages, -> { order(:created_at) }, dependent: :destroy
6
6
  belongs_to :initiator, class_name: 'User'
7
+ belongs_to :last_moderator_access_by, class_name: 'User', optional: true
7
8
  belongs_to :exercise, foreign_type: :exercise, foreign_key: 'item_id'
8
9
  belongs_to :organization
9
10
  has_many :subscriptions
10
11
  has_many :upvotes
11
12
 
12
13
  scope :by_language, -> (language) { includes(:exercise).joins(exercise: :language).where(languages: {name: language}) }
14
+ scope :order_by_responses_count, -> (direction) { reorder(validated_messages_count: direction, messages_count: opposite(direction)) }
15
+ scope :by_requires_moderator_response, -> (boolean) { where(requires_moderator_response: boolean.to_boolean) }
13
16
 
14
- before_save :capitalize_title
15
- validates_presence_of :title
17
+ after_create :subscribe_initiator!
16
18
 
17
19
  markdown_on :description
18
20
 
19
- sortable :created_at, :upvotes_count, default: :created_at_desc
20
- filterable :status, :language
21
+ sortable :responses_count, :upvotes_count, :created_at, default: :created_at_desc
22
+ filterable :status, :language, :requires_moderator_response
21
23
  pageable
22
24
 
23
25
  delegate :language, to: :item
24
26
  delegate :to_discussion_status, to: :status
25
27
 
28
+ MODERATOR_REVIEW_AVERAGE_TIME = 10.minutes
29
+
26
30
  scope :for_user, -> (user) do
27
31
  if user.try(:moderator_here?)
28
32
  all
@@ -37,10 +41,6 @@ class Discussion < ApplicationRecord
37
41
  end
38
42
  end
39
43
 
40
- def capitalize_title
41
- title.capitalize!
42
- end
43
-
44
44
  def used_in?(organization)
45
45
  organization == self.organization
46
46
  end
@@ -66,7 +66,7 @@ class Discussion < ApplicationRecord
66
66
  end
67
67
 
68
68
  def friendly
69
- title
69
+ initiator.name
70
70
  end
71
71
 
72
72
  def subscription_for(user)
@@ -105,10 +105,6 @@ class Discussion < ApplicationRecord
105
105
  reachable_statuses_for(user).include? status
106
106
  end
107
107
 
108
- def allowed_statuses_for(user)
109
- status.allowed_statuses_for(user, self)
110
- end
111
-
112
108
  def update_status!(status, user)
113
109
  update!(status: status) if reachable_status_for?(user, status)
114
110
  end
@@ -125,14 +121,56 @@ class Discussion < ApplicationRecord
125
121
  responses_count > 0
126
122
  end
127
123
 
124
+ def has_validated_responses?
125
+ validated_messages_count > 0
126
+ end
127
+
128
+ def subscribe_initiator!
129
+ initiator.subscribe_to! self
130
+ end
131
+
128
132
  def extra_preview_html
129
133
  # FIXME this is buggy, because the extra
130
134
  # may have changed since the submission of this discussion
131
135
  exercise.assignment_for(initiator).extra_preview_html
132
136
  end
133
137
 
138
+ def update_counters!
139
+ messages_query = messages_by_updated_at
140
+ validated_messages = messages_query.select &:validated?
141
+ has_moderator_response = messages_query.find { |it| it.validated? || it.question? }&.validated?
142
+ update! messages_count: messages_query.count,
143
+ validated_messages_count: validated_messages.count,
144
+ requires_moderator_response: !has_moderator_response
145
+ end
146
+
147
+ def update_last_moderator_access!(user)
148
+ if user&.moderator_here? && !last_moderator_access_visible_for?(user)
149
+ update! last_moderator_access_at: Time.now,
150
+ last_moderator_access_by: user
151
+ end
152
+ end
153
+
154
+ def being_accessed_by_moderator?
155
+ last_moderator_access_at.present? && last_moderator_access_at > Time.now - MODERATOR_REVIEW_AVERAGE_TIME
156
+ end
157
+
158
+ def last_moderator_access_visible_for?(user)
159
+ show_last_moderator_access_for?(user) && being_accessed_by_moderator?
160
+ end
161
+
162
+ def show_last_moderator_access_for?(user)
163
+ user&.moderator_here? && last_moderator_access_by != user
164
+ end
165
+
134
166
  def self.debatable_for(klazz, params)
135
167
  debatable_id = params[:"#{klazz.underscore}_id"]
136
168
  klazz.constantize.find(debatable_id)
137
169
  end
170
+
171
+ private
172
+
173
+ def messages_by_updated_at(direction = :desc)
174
+ messages.reorder(updated_at: direction)
175
+ end
138
176
  end
@@ -1,19 +1,28 @@
1
1
  class Exam < ApplicationRecord
2
+
2
3
  include GuideContainer
3
4
  include FriendlyName
4
-
5
- validates_presence_of :start_time, :end_time
5
+ include TerminalNavigation
6
6
 
7
7
  belongs_to :organization
8
+ belongs_to :course, optional: true
8
9
 
9
10
  has_many :authorizations, class_name: 'ExamAuthorization', dependent: :destroy
10
11
  has_many :users, through: :authorizations
11
12
 
13
+ enum passing_criterion_type: [:none, :percentage, :passed_exercises], _prefix: :passing_criterion
14
+
15
+ validates_presence_of :start_time, :end_time
16
+ validates_numericality_of :max_problem_submissions, :max_choice_submissions, greater_than_or_equal_to: 1, allow_nil: true
17
+
18
+ before_save :set_default_criterion_type!
19
+ before_save :ensure_valid_passing_criterion!
20
+
21
+ before_create :set_classroom_id!
22
+
12
23
  after_destroy { |record| Usage.destroy_usages_for record }
13
24
  after_create :reindex_usages!
14
25
 
15
- include TerminalNavigation
16
-
17
26
  def used_in?(organization)
18
27
  organization == self.organization
19
28
  end
@@ -116,10 +125,41 @@ class Exam < ApplicationRecord
116
125
  index_usage! organization
117
126
  end
118
127
 
128
+ def attempts_left_for(assignment)
129
+ max_attempts_for(assignment.exercise) - (assignment.attempts_count || 0)
130
+ end
131
+
132
+ def limited_for?(exercise)
133
+ max_attempts_for(exercise).present?
134
+ end
135
+
136
+ def results_hidden_for?(exercise)
137
+ exercise.choice? && results_hidden_for_choices?
138
+ end
139
+
140
+ def resettable?
141
+ false
142
+ end
143
+
144
+ def set_classroom_id!
145
+ self.classroom_id ||= SecureRandom.hex(8)
146
+ end
147
+
148
+ def passing_criterion
149
+ @passing_criterion ||= Exam::PassingCriterion.parse(passing_criterion_type, passing_criterion_value)
150
+ end
151
+
152
+ def ensure_valid_passing_criterion!
153
+ passing_criterion.ensure_valid!
154
+ end
155
+
156
+ def set_default_criterion_type!
157
+ self.passing_criterion_type ||= :none
158
+ end
159
+
119
160
  def self.import_from_resource_h!(json)
120
161
  exam_data = json.with_indifferent_access
121
- organization = Organization.locate! exam_data[:organization]
122
- organization.switch!
162
+ Organization.locate!(exam_data[:organization].to_s).switch!
123
163
  adapt_json_values exam_data
124
164
  remove_previous_version exam_data[:eid], exam_data[:guide_id]
125
165
  exam = where(classroom_id: exam_data[:eid]).update_or_create!(whitelist_attributes(exam_data))
@@ -141,8 +181,13 @@ class Exam < ApplicationRecord
141
181
  def self.adapt_json_values(exam)
142
182
  exam[:guide_id] = Guide.locate!(exam[:slug]).id
143
183
  exam[:organization_id] = Organization.current.id
184
+ exam[:course_id] = Course.locate!(exam[:course].to_s).id
144
185
  exam[:users] = User.where(uid: exam[:uids])
145
- [:start_time, :end_time].each { |param| exam[param] = exam[param].to_time }
186
+ exam[:start_time] = exam[:start_time].to_time
187
+ exam[:end_time] = exam[:end_time].to_time
188
+ exam[:classroom_id] = exam[:eid] if exam[:eid].present?
189
+ exam[:passing_criterion_type] = exam.dig(:passing_criterion, :type)
190
+ exam[:passing_criterion_value] = exam.dig(:passing_criterion, :value)
146
191
  end
147
192
 
148
193
  def self.remove_previous_version(eid, guide_id)
@@ -153,25 +198,10 @@ class Exam < ApplicationRecord
153
198
  end
154
199
  end
155
200
 
156
- def attempts_left_for(assignment)
157
- max_attempts_for(assignment.exercise) - (assignment.attempts_count || 0)
158
- end
159
-
160
- def limited_for?(exercise)
161
- max_attempts_for(exercise).present?
162
- end
163
-
164
- def results_hidden_for?(exercise)
165
- exercise.choice? && results_hidden_for_choices?
166
- end
167
-
168
- def resettable?
169
- false
170
- end
171
-
172
201
  private
173
202
 
174
203
  def max_attempts_for(exercise)
175
204
  exercise.choice? ? max_choice_submissions : max_problem_submissions
176
205
  end
206
+
177
207
  end