mumuki-domain 7.6.1 → 7.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/concerns/submittable/solvable.rb +8 -2
  3. data/app/models/concerns/with_discussions.rb +8 -5
  4. data/app/models/concerns/with_layout.rb +5 -1
  5. data/app/models/concerns/with_scoped_queries.rb +9 -9
  6. data/app/models/concerns/with_scoped_queries/page.rb +1 -1
  7. data/app/models/concerns/with_scoped_queries/sort.rb +6 -1
  8. data/app/models/course.rb +13 -7
  9. data/app/models/discussion.rb +47 -9
  10. data/app/models/exam.rb +53 -23
  11. data/app/models/exam/passing_criterion.rb +53 -0
  12. data/app/models/exercise/challenge.rb +1 -1
  13. data/app/models/exercise/problem.rb +6 -0
  14. data/app/models/exercise/reading.rb +4 -0
  15. data/app/models/invitation.rb +7 -1
  16. data/app/models/message.rb +28 -4
  17. data/app/models/user.rb +4 -0
  18. data/db/migrate/20200601203033_add_course_to_exam.rb +5 -0
  19. data/db/migrate/20200605161350_add_passing_criterions_to_exam.rb +6 -0
  20. data/db/migrate/20200702165503_add_messages_count_to_discussion.rb +6 -0
  21. data/db/migrate/20200728162727_add_not_actually_a_question_field_to_messages.rb +5 -0
  22. data/db/migrate/20200728163038_add_requires_moderator_response_to_discussions.rb +5 -0
  23. data/db/migrate/20200731081757_add_last_moderator_access_fields_to_discussion.rb +6 -0
  24. data/lib/mumuki/domain/factories/course_factory.rb +2 -0
  25. data/lib/mumuki/domain/factories/exam_factory.rb +1 -0
  26. data/lib/mumuki/domain/organization/settings.rb +2 -1
  27. data/lib/mumuki/domain/status/discussion/opened.rb +0 -8
  28. data/lib/mumuki/domain/submission/base.rb +6 -0
  29. data/lib/mumuki/domain/submission/solution.rb +3 -1
  30. data/lib/mumuki/domain/version.rb +1 -1
  31. metadata +10 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdb048ee9a17b03d534fd49140b3a30a014af1110ac0f50fc0b25243a6a069ee
4
- data.tar.gz: 3790eb4813fdae432c358ceaee88047a4f45a63cbe89c10a22e48f10ddde0d91
3
+ metadata.gz: 9983a4a1b2d29d329606c4ccfe991bd979a5717a8562678316e143638857622b
4
+ data.tar.gz: 3115a2b010be3a7536bb835e36751595d510cfbc009093d6ac77c08e973de5e8
5
5
  SHA512:
6
- metadata.gz: a34d18802a4a8178bbacfcc8e7e2c403a8da4493062639309bc6fdabc557d8d6655aa0f173530113859633bac7de69d8426c39934cfc6c9bee4ce62e8cffaf49
7
- data.tar.gz: 71852152c330213e65cacebfa5ee1256f1efbb940088cf5a2e89081d74fbaaf27b02af844f167fffdab9a9514016c3defa060e0665c08f9d85c7cf4e2b79012c
6
+ metadata.gz: 46cb4964c07e81cb2568d10a96cc83ca3ab29417abc3012ea7898dfd0712e375ec4a37f7852bd827725e73a94dcc153766948c06cc91e2f1b8f3d91629fcb2d4
7
+ data.tar.gz: 97bc43d01513919f1a3ec4b053261c7f43590399710f3ba0c7ce026daa243547e0bda2a17508372d5ef5a33fa1b36b9c06e90f31a2fe5adb2faed791a22b2933
@@ -1,6 +1,6 @@
1
1
  module Solvable
2
- def submit_solution!(user, attributes={})
3
- assignment, _ = find_assignment_and_submit! user, attributes[:content].to_mumuki_solution(language)
2
+ def submit_solution!(user, submission_attributes={})
3
+ assignment, _ = find_assignment_and_submit! user, solution_for(submission_attributes)
4
4
  try_solve_discussions(user) if assignment.solved?
5
5
  assignment
6
6
  end
@@ -8,6 +8,12 @@ module Solvable
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
@@ -7,11 +7,7 @@ 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(_)
@@ -21,4 +17,11 @@ module WithDiscussions
21
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
@@ -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)
@@ -125,14 +125,52 @@ class Discussion < ApplicationRecord
125
125
  responses_count > 0
126
126
  end
127
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
+ requires_moderator_response = messages_query.find { |it| it.validated? || it.question? }&.from_initiator?
142
+ update! messages_count: messages_query.count,
143
+ validated_messages_count: validated_messages.count,
144
+ requires_moderator_response: requires_moderator_response
145
+ end
146
+
147
+ def update_last_moderator_access!(user)
148
+ unless 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
@@ -0,0 +1,53 @@
1
+ class Exam::PassingCriterion
2
+
3
+ attr_reader :value
4
+
5
+ def initialize(value)
6
+ @value = value
7
+ end
8
+
9
+ def type
10
+ self.class.name.demodulize.underscore
11
+ end
12
+
13
+ def as_json
14
+ {type: type, value: value}
15
+ end
16
+
17
+ def ensure_valid!
18
+ raise "Invalid criterion value #{value} for #{type}" unless valid_passing_grade?
19
+ end
20
+
21
+ def self.parse(type, value)
22
+ parse_criterion_type(type, value)
23
+ end
24
+
25
+ def self.parse_criterion_type(type, value)
26
+ "Exam::PassingCriterion::#{type.camelize}".constantize.new(value)
27
+ rescue
28
+ raise "Invalid criterion type #{type}"
29
+ end
30
+
31
+ end
32
+
33
+ class Exam::PassingCriterion::None < Exam::PassingCriterion
34
+ def initialize(_)
35
+ @value = nil
36
+ end
37
+
38
+ def valid_passing_grade?
39
+ !value
40
+ end
41
+ end
42
+
43
+ class Exam::PassingCriterion::Percentage < Exam::PassingCriterion
44
+ def valid_passing_grade?
45
+ value.between? 0, 100
46
+ end
47
+ end
48
+
49
+ class Exam::PassingCriterion::PassedExercises < Exam::PassingCriterion
50
+ def valid_passing_grade?
51
+ value >= 0
52
+ end
53
+ end
@@ -20,6 +20,6 @@ class Challenge < Exercise
20
20
 
21
21
  def defaults
22
22
  super
23
- self.layout = self.class.default_layout
23
+ self.layout ||= self.class.default_layout
24
24
  end
25
25
  end
@@ -45,6 +45,12 @@ class Problem < QueriableChallenge
45
45
  own_expectations.present? || own_custom_expectations.present?
46
46
  end
47
47
 
48
+ # Sets the layout. This method accepts input_kids as a synonym of input_primary
49
+ # for historical reasons
50
+ def layout=(layout)
51
+ self[:layout] = layout.like?(:input_kids) ? :input_primary : layout
52
+ end
53
+
48
54
  private
49
55
 
50
56
  def ensure_evaluation_criteria
@@ -11,6 +11,10 @@ class Reading < Exercise
11
11
  false
12
12
  end
13
13
 
14
+ def layout=(layout)
15
+ raise 'can not set a layout different to input_bottom on readings' unless layout.like? :input_bottom
16
+ end
17
+
14
18
  def queriable?
15
19
  false
16
20
  end
@@ -2,12 +2,18 @@ class Invitation < ApplicationRecord
2
2
  include Mumuki::Domain::Syncable
3
3
 
4
4
  belongs_to :course
5
+
6
+ validate :ensure_not_expired, on: :create
5
7
  validates_uniqueness_of :code
6
8
 
7
9
  defaults do
8
10
  self.code ||= self.class.generate_code
9
11
  end
10
12
 
13
+ def ensure_not_expired
14
+ errors.add(:base, :invitation_expired) if expired?
15
+ end
16
+
11
17
  def import_from_resource_h!(json)
12
18
  update! json.merge(course: Course.locate!(json[:course]))
13
19
  end
@@ -25,7 +31,7 @@ class Invitation < ApplicationRecord
25
31
  end
26
32
 
27
33
  def to_resource_h
28
- { code: code, course: course_slug, expiration_date: expiration_date }
34
+ {code: code, course: course_slug, expiration_date: expiration_date}
29
35
  end
30
36
 
31
37
  def navigation_end?
@@ -6,6 +6,10 @@ class Message < ApplicationRecord
6
6
 
7
7
  validates_presence_of :content, :sender
8
8
  validates_presence_of :submission_id, :unless => :discussion_id?
9
+
10
+ after_save :update_counters_cache!
11
+ after_destroy :update_counters_cache!
12
+
9
13
  markdown_on :content
10
14
 
11
15
  def notify!
@@ -16,6 +20,10 @@ class Message < ApplicationRecord
16
20
  sender_user == discussion&.initiator
17
21
  end
18
22
 
23
+ def from_moderator?
24
+ sender_user.moderator_here?
25
+ end
26
+
19
27
  def from_user?(user)
20
28
  sender_user == user
21
29
  end
@@ -33,9 +41,9 @@ class Message < ApplicationRecord
33
41
  end
34
42
 
35
43
  def to_resource_h
36
- as_json(except: [:id, :type, :discussion_id, :approved],
44
+ as_json(except: [:id, :type, :discussion_id, :approved, :not_actually_a_question],
37
45
  include: {exercise: {only: [:bibliotheca_id]}})
38
- .merge(organization: Organization.current.name)
46
+ .merge(organization: Organization.current.name)
39
47
  end
40
48
 
41
49
  def read!
@@ -46,11 +54,27 @@ class Message < ApplicationRecord
46
54
  toggle! :approved
47
55
  end
48
56
 
57
+ def toggle_not_actually_a_question!
58
+ toggle! :not_actually_a_question
59
+ end
60
+
61
+ def validated?
62
+ approved? || from_moderator?
63
+ end
64
+
65
+ def update_counters_cache!
66
+ discussion&.update_counters!
67
+ end
68
+
69
+ def question?
70
+ from_initiator? && !not_actually_a_question?
71
+ end
72
+
49
73
  def self.parse_json(json)
50
74
  message = json.delete 'message'
51
75
  json
52
- .except('uid', 'exercise_id')
53
- .merge(message)
76
+ .except('uid', 'exercise_id')
77
+ .merge(message)
54
78
  end
55
79
 
56
80
  def self.read_all!
@@ -169,6 +169,10 @@ class User < ApplicationRecord
169
169
  sequence[0..count + lookahead - 1]
170
170
  end
171
171
 
172
+ def name_initials
173
+ name.split.map(&:first).map(&:capitalize).join(' ')
174
+ end
175
+
172
176
  private
173
177
 
174
178
  def set_uid!
@@ -0,0 +1,5 @@
1
+ class AddCourseToExam < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_reference :exams, :course
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddPassingCriterionsToExam < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :exams, :passing_criterion_type, :integer, default: 0
4
+ add_column :exams, :passing_criterion_value, :integer
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class AddMessagesCountToDiscussion < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :discussions, :messages_count, :integer, default: 0
4
+ add_column :discussions, :validated_messages_count, :integer, default: 0
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddNotActuallyAQuestionFieldToMessages < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :messages, :not_actually_a_question, :boolean, default: false
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddRequiresModeratorResponseToDiscussions < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :discussions, :requires_moderator_response, :boolean, default: true
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddLastModeratorAccessFieldsToDiscussion < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :discussions, :last_moderator_access_by_id, :string
4
+ add_column :discussions, :last_moderator_access_at, :datetime
5
+ end
6
+ end
@@ -2,8 +2,10 @@ FactoryBot.define do
2
2
  factory :course do
3
3
  period { '2016' }
4
4
  shifts { %w(morning) }
5
+ code { "k1234-#{SecureRandom.uuid}" }
5
6
  days { %w(monday wednesday) }
6
7
  description { 'test' }
7
8
  organization_id { 1 }
9
+ slug { "#{Organization.current.name}/#{period}-#{code}" }
8
10
  end
9
11
  end
@@ -3,6 +3,7 @@ FactoryBot.define do
3
3
  factory :exam, traits: [:guide_container] do
4
4
  duration { Faker::Number.between(from: 10, to:60).minutes }
5
5
  organization { Organization.current }
6
+ course { create(:course) }
6
7
  start_time { 5.minutes.ago }
7
8
  end_time { 10.minutes.since }
8
9
  end
@@ -13,7 +13,8 @@ class Mumuki::Domain::Organization::Settings < Mumukit::Platform::Model
13
13
  :forum_enabled?,
14
14
  :report_issue_enabled?,
15
15
  :disabled_from,
16
- :in_preparation_until
16
+ :in_preparation_until,
17
+ :gamification_enabled?
17
18
 
18
19
  def private?
19
20
  !public?
@@ -5,14 +5,6 @@ module Mumuki::Domain::Status::Discussion::Opened
5
5
  true
6
6
  end
7
7
 
8
- def self.reachable_statuses_for_initiator(discussion)
9
- if discussion.has_responses?
10
- [Mumuki::Domain::Status::Discussion::PendingReview]
11
- else
12
- [Mumuki::Domain::Status::Discussion::Closed]
13
- end
14
- end
15
-
16
8
  def self.reachable_statuses_for_moderator(discussion)
17
9
  if discussion.has_responses?
18
10
  [Mumuki::Domain::Status::Discussion::Closed, Mumuki::Domain::Status::Discussion::Solved]
@@ -9,6 +9,7 @@ class Mumuki::Domain::Submission::Base
9
9
  :submission_id, :queries, :query_results, :manual_evaluation_comment]
10
10
 
11
11
  attr_accessor *ATTRIBUTES
12
+ attr_accessor :client_result
12
13
 
13
14
  def self.from_attributes(*args)
14
15
  new ATTRIBUTES.zip(args).to_h
@@ -26,6 +27,11 @@ class Mumuki::Domain::Submission::Base
26
27
  results
27
28
  end
28
29
 
30
+ def with_client_result(result)
31
+ self.client_result = result if result.present?
32
+ self
33
+ end
34
+
29
35
  def evaluate!(assignment)
30
36
  try_evaluate! assignment
31
37
  rescue => e
@@ -2,6 +2,8 @@ class Mumuki::Domain::Submission::Solution < Mumuki::Domain::Submission::Persist
2
2
  attr_accessor :content
3
3
 
4
4
  def try_evaluate!(assignment)
5
- assignment.run_tests!(content: content).except(:response_type)
5
+ assignment
6
+ .run_tests!({client_result: client_result}.compact.merge(content: content))
7
+ .except(:response_type)
6
8
  end
7
9
  end
@@ -1,5 +1,5 @@
1
1
  module Mumuki
2
2
  module Domain
3
- VERSION = '7.6.1'
3
+ VERSION = '7.7.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mumuki-domain
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.6.1
4
+ version: 7.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Franco Leonardo Bulgarelli
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-19 00:00:00.000000000 Z
11
+ date: 2020-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -299,6 +299,7 @@ files:
299
299
  - app/models/discussion.rb
300
300
  - app/models/event.rb
301
301
  - app/models/exam.rb
302
+ - app/models/exam/passing_criterion.rb
302
303
  - app/models/exam_authorization.rb
303
304
  - app/models/exercise.rb
304
305
  - app/models/exercise/challenge.rb
@@ -604,7 +605,13 @@ files:
604
605
  - db/migrate/20200508191543_create_avatars.rb
605
606
  - db/migrate/20200518135658_add_avatar_to_users.rb
606
607
  - db/migrate/20200527180729_add_disabled_at_to_users.rb
608
+ - db/migrate/20200601203033_add_course_to_exam.rb
609
+ - db/migrate/20200605161350_add_passing_criterions_to_exam.rb
607
610
  - db/migrate/20200608132959_add_progressive_display_lookahead_to_organizations.rb
611
+ - db/migrate/20200702165503_add_messages_count_to_discussion.rb
612
+ - db/migrate/20200728162727_add_not_actually_a_question_field_to_messages.rb
613
+ - db/migrate/20200728163038_add_requires_moderator_response_to_discussions.rb
614
+ - db/migrate/20200731081757_add_last_moderator_access_fields_to_discussion.rb
608
615
  - lib/mumuki/domain.rb
609
616
  - lib/mumuki/domain/area.rb
610
617
  - lib/mumuki/domain/engine.rb
@@ -714,8 +721,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
714
721
  - !ruby/object:Gem::Version
715
722
  version: '0'
716
723
  requirements: []
717
- rubyforge_project:
718
- rubygems_version: 2.7.7
724
+ rubygems_version: 3.0.8
719
725
  signing_key:
720
726
  specification_version: 4
721
727
  summary: Mumuki Platform's Domain Model