mumuki-domain 8.1.1 → 8.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +2 -0
  3. data/app/models/assignment.rb +11 -2
  4. data/app/models/chapter.rb +3 -0
  5. data/app/models/concerns/guide_container.rb +2 -1
  6. data/app/models/concerns/with_assignments.rb +1 -0
  7. data/app/models/concerns/with_assignments_batch.rb +31 -0
  8. data/app/models/concerns/with_expectations.rb +3 -7
  9. data/app/models/concerns/with_preferences.rb +7 -0
  10. data/app/models/concerns/with_randomizations.rb +1 -2
  11. data/app/models/concerns/with_timed_enablement.rb +11 -0
  12. data/app/models/exam.rb +4 -8
  13. data/app/models/exam_authorization_request.rb +19 -0
  14. data/app/models/exam_registration.rb +38 -0
  15. data/app/models/exam_registration/authorization_criterion.rb +61 -0
  16. data/app/models/exercise.rb +8 -5
  17. data/app/models/exercise/challenge.rb +4 -2
  18. data/app/models/exercise/problem.rb +5 -10
  19. data/app/models/guide.rb +7 -2
  20. data/app/models/indicator.rb +35 -0
  21. data/app/models/notification.rb +5 -0
  22. data/app/models/organization.rb +1 -1
  23. data/app/models/preferences.rb +17 -0
  24. data/app/models/progress.rb +35 -0
  25. data/app/models/stats.rb +4 -0
  26. data/app/models/topic.rb +16 -7
  27. data/app/models/user.rb +7 -1
  28. data/app/models/with_stats.rb +2 -1
  29. data/db/migrate/20210111125810_add_uppercase_mode_to_user.rb +5 -0
  30. data/db/migrate/20210114200545_create_exam_registrations.rb +14 -0
  31. data/db/migrate/20210118180941_create_exam_authorization_request.rb +13 -0
  32. data/db/migrate/20210118194904_create_notification.rb +13 -0
  33. data/db/migrate/20210119160440_add_prevent_manual_evaluation_content_to_organizations.rb +5 -0
  34. data/db/migrate/20210119190204_create_exam_registration_exam_join_table.rb +8 -0
  35. data/lib/mumuki/domain.rb +1 -0
  36. data/lib/mumuki/domain/extensions.rb +2 -0
  37. data/lib/mumuki/domain/extensions/array.rb +2 -4
  38. data/lib/mumuki/domain/extensions/hash.rb +4 -0
  39. data/lib/mumuki/domain/extensions/nil.rb +17 -0
  40. data/lib/mumuki/domain/extensions/string.rb +2 -9
  41. data/lib/mumuki/domain/extensions/symbol.rb +5 -0
  42. data/lib/mumuki/domain/factories.rb +2 -0
  43. data/lib/mumuki/domain/factories/exam_authorization_request_factory.rb +6 -0
  44. data/lib/mumuki/domain/factories/exam_registration_factory.rb +8 -0
  45. data/lib/mumuki/domain/helpers/organization.rb +4 -0
  46. data/lib/mumuki/domain/incognito.rb +1 -1
  47. data/lib/mumuki/domain/locales/activerecord/en.yml +1 -1
  48. data/lib/mumuki/domain/locales/activerecord/es-CL.yml +1 -1
  49. data/lib/mumuki/domain/locales/activerecord/es.yml +1 -1
  50. data/lib/mumuki/domain/locales/activerecord/pt.yml +1 -1
  51. data/lib/mumuki/domain/progress_transfer.rb +8 -0
  52. data/lib/mumuki/domain/progress_transfer/base.rb +44 -0
  53. data/lib/mumuki/domain/progress_transfer/copy.rb +9 -0
  54. data/lib/mumuki/domain/progress_transfer/move.rb +14 -0
  55. data/lib/mumuki/domain/status/submission/manual_evaluation_pending.rb +1 -1
  56. data/lib/mumuki/domain/status/submission/submission.rb +4 -0
  57. data/lib/mumuki/domain/version.rb +1 -1
  58. metadata +25 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd4418eebed893926b600b43b22080be43205ef4b857e16df1be7e50ea625e9b
4
- data.tar.gz: 69d81e7072172101767cc3655cfad7bafdcf9fc726c3438432a9777792e744c0
3
+ metadata.gz: 945a8212dc38bd06d9ce3a7ba70f58fd5ea3fc2ae7076c81727ef2bdfee05979
4
+ data.tar.gz: 79dbe9e02af53932d78b403777c3d03f92dff40844047ce40052a43dbbb361c4
5
5
  SHA512:
6
- metadata.gz: d28c8e9e981e2669f11a5f581f25639c318df1dc17bdf1c94bcd127010426f2b846ff801f7ee2362941f2fb42ea188a36694b91add542555085f5d363603cc11
7
- data.tar.gz: 2e95982034e482f748425c8445791349b5eb9c206a962b6620aea4412d4ef01a17ecf2d79ac0ca12cca75a458a51e0f4ce671951213ac434381475592e235ac2
6
+ metadata.gz: a59255b4f0dcf4bb464dea4d0c1efb86cd28b3ce85b7e3f0ba2d74ad735505d82d795425bc704dd7b8e2e0bcb45a4a6ebd6e26d6f942971b4149ee0b09dd21c2
7
+ data.tar.gz: a904b0a41d0c04b09f3dc5afbc4abaf4a228112f57d4f7279798be91a9405b2a9a5bf4d7f0962c55773f3be0add354fecd5ba0e5cc0d5c2e3c91c2b3cea3a38f
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ ENV['RAILS_ENV'] = 'test'
2
+
1
3
  begin
2
4
  require 'bundler/setup'
3
5
  rescue LoadError
@@ -23,6 +23,8 @@ class Assignment < Progress
23
23
 
24
24
  delegate :completed?, :solved?, to: :submission_status
25
25
 
26
+ delegate :content_available_in?, to: :parent
27
+
26
28
  alias_attribute :status, :submission_status
27
29
  alias_attribute :attempts_count, :attemps_count
28
30
 
@@ -126,7 +128,7 @@ class Assignment < Progress
126
128
  end
127
129
 
128
130
  def content=(content)
129
- unless exercise.reading?
131
+ if exercise.solvable?
130
132
  self.solution = exercise.single_choice? ? exercise.choice_index_for(content) : content
131
133
  end
132
134
  end
@@ -261,8 +263,16 @@ class Assignment < Progress
261
263
  update! misplaced: value if value != misplaced?
262
264
  end
263
265
 
266
+ def self.build_for(user, exercise, organization)
267
+ Assignment.new submitter: user, exercise: exercise, organization: organization
268
+ end
269
+
264
270
  private
265
271
 
272
+ def duplicates_key
273
+ { exercise: exercise, submitter: submitter }
274
+ end
275
+
266
276
  def update_submissions_count!
267
277
  self.class.connection.execute(
268
278
  "update public.exercises
@@ -278,5 +288,4 @@ class Assignment < Progress
278
288
  def update_last_submission!
279
289
  submitter.update!(last_submission_date: DateTime.current, last_exercise: exercise)
280
290
  end
281
-
282
291
  end
@@ -13,6 +13,9 @@ class Chapter < ApplicationRecord
13
13
 
14
14
  has_many :exercises, through: :topic
15
15
 
16
+ delegate :monolesson?, :monolesson, :first_lesson, to: :topic
17
+
18
+ delegate :next_exercise, :stats_for, to: :monolesson, allow_nil: true
16
19
 
17
20
  def used_in?(organization)
18
21
  organization.book == self.book
@@ -13,7 +13,8 @@ module GuideContainer
13
13
  :first_exercise,
14
14
  :next_exercise,
15
15
  :stats_for,
16
- :exercises_count, to: :guide
16
+ :exercises_count,
17
+ :assignments_for, to: :guide
17
18
  end
18
19
 
19
20
  def index_usage!(organization = Organization.current)
@@ -19,6 +19,7 @@ module WithAssignments
19
19
  end
20
20
 
21
21
  # TODO: When the organization is used in this one, please change guide.pending_exercises
22
+ # TODO: Please do the same on WithAssignmentsBatch
22
23
  def find_assignment_for(user, _organization)
23
24
  assignments.find_by(submitter: user)
24
25
  end
@@ -0,0 +1,31 @@
1
+ # WithAssignmentsBatch mirrors the WithAssignment mixin
2
+ # but implements operations in batches, so that they outperform
3
+ # their counterparts
4
+ module WithAssignmentsBatch
5
+ extend ActiveSupport::Concern
6
+
7
+ def find_assignments_for(user, _organization = Organization.current, &block)
8
+ block = block_given? ? block : lambda { |it, _e| it }
9
+
10
+ return exercises.map { |it| block.call nil, it } unless user
11
+
12
+ pairs = exercises.map { |it| [it.id, [nil, it]] }.to_h
13
+ Assignment.where(submitter: user, exercise: exercises).each do |it|
14
+ pairs[it.exercise_id][0] = it
15
+ end
16
+
17
+ pairs.values.map { |assignment, exercise| block.call assignment, exercise }
18
+ end
19
+
20
+ def statuses_for(user, organization = Organization.current)
21
+ find_assignments_for user, organization do |it|
22
+ it&.status || Mumuki::Domain::Status::Submission::Pending
23
+ end
24
+ end
25
+
26
+ def assignments_for(user, organization = Organization.current)
27
+ find_assignments_for user, organization do |it, exercise|
28
+ it || Assignment.build_for(user, exercise, organization)
29
+ end
30
+ end
31
+ end
@@ -18,16 +18,12 @@ module WithExpectations
18
18
  self[:expectations] = expectations.map(&:stringify_keys)
19
19
  end
20
20
 
21
- def own_expectations
21
+ def raw_expectations
22
22
  self[:expectations]
23
23
  end
24
24
 
25
- def own_custom_expectations
26
- self[:custom_expectations]
27
- end
28
-
29
25
  def ensure_expectations_format
30
- errors.add :own_expectations,
31
- :invalid_format unless own_expectations.to_a.all? { |it| Mulang::Expectation.valid? it }
26
+ errors.add :raw_expectations,
27
+ :invalid_format unless raw_expectations.to_a.all? { |it| Mulang::Expectation.valid? it }
32
28
  end
33
29
  end
@@ -0,0 +1,7 @@
1
+ module WithPreferences
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ composed_of :preferences, mapping: %w(uppercase_mode uppercase_mode), constructor: :from_attributes
6
+ end
7
+ end
@@ -35,8 +35,7 @@ module WithRandomizations
35
35
 
36
36
  def randomize_field(selector)
37
37
  define_method(selector) do |*args|
38
- return unless super(*args)
39
- randomizer.randomize!(super(*args), seed)
38
+ super(*args).randomize_with randomizer, seed
40
39
  end
41
40
  end
42
41
  end
@@ -0,0 +1,11 @@
1
+ module WithTimedEnablement
2
+ extend ActiveSupport::Concern
3
+
4
+ def enabled?
5
+ enabled_range.cover? DateTime.current
6
+ end
7
+
8
+ def enabled_range
9
+ start_time..end_time
10
+ end
11
+ end
@@ -3,13 +3,17 @@ class Exam < ApplicationRecord
3
3
  include GuideContainer
4
4
  include FriendlyName
5
5
  include TerminalNavigation
6
+ include WithTimedEnablement
6
7
 
7
8
  belongs_to :organization
8
9
  belongs_to :course
9
10
 
10
11
  has_many :authorizations, class_name: 'ExamAuthorization', dependent: :destroy
12
+ has_many :authorization_requests, class_name: 'ExamAuthorizationRequest', dependent: :destroy
11
13
  has_many :users, through: :authorizations
12
14
 
15
+ has_and_belongs_to_many :exam_registrations
16
+
13
17
  enum passing_criterion_type: [:none, :percentage, :passed_exercises], _prefix: :passing_criterion
14
18
 
15
19
  validates_presence_of :start_time, :end_time
@@ -27,14 +31,6 @@ class Exam < ApplicationRecord
27
31
  organization == self.organization
28
32
  end
29
33
 
30
- def enabled?
31
- enabled_range.cover? DateTime.current
32
- end
33
-
34
- def enabled_range
35
- start_time..end_time
36
- end
37
-
38
34
  def enabled_for?(user)
39
35
  enabled_range_for(user).cover? DateTime.current
40
36
  end
@@ -0,0 +1,19 @@
1
+ class ExamAuthorizationRequest < ApplicationRecord
2
+ belongs_to :exam
3
+ belongs_to :user
4
+ belongs_to :organization
5
+
6
+ enum status: %i(pending approved rejected)
7
+
8
+ after_update :notify_user!
9
+
10
+ def try_authorize!
11
+ exam.authorize! user if approved?
12
+ end
13
+
14
+ private
15
+
16
+ def notify_user!
17
+ Notification.create! organization: organization, user: user, target: self if saved_change_to_status?
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ class ExamRegistration < ApplicationRecord
2
+ include WithTimedEnablement
3
+
4
+ belongs_to :organization
5
+ has_and_belongs_to_many :exams
6
+ has_many :authorization_requests, class_name: 'ExamAuthorizationRequest', through: :exams
7
+
8
+ enum authorization_criterion_type: %i(none passed_exercises), _prefix: :authorization_criterion
9
+
10
+ before_save :ensure_valid_authorization_criterion!
11
+
12
+ delegate :meets_authorization_criteria?, :process_request!, to: :authorization_criterion
13
+
14
+ def authorization_criterion
15
+ @authorization_criterion ||= ExamRegistration::AuthorizationCriterion.parse(authorization_criterion_type, authorization_criterion_value)
16
+ end
17
+
18
+ def ensure_valid_authorization_criterion!
19
+ authorization_criterion.ensure_valid!
20
+ end
21
+
22
+ def start!(users)
23
+ users.each &method(:notify_user!)
24
+ end
25
+
26
+ def process_requests!
27
+ authorization_requests.each do |it|
28
+ process_request! it
29
+ it.try_authorize!
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def notify_user!(user)
36
+ Notification.create! organization: organization, user: user, target: self
37
+ end
38
+ end
@@ -0,0 +1,61 @@
1
+ class ExamRegistration::AuthorizationCriterion
2
+ attr_reader :value
3
+
4
+ def initialize(value)
5
+ @value = value
6
+ end
7
+
8
+ def type
9
+ self.class.name.demodulize.underscore
10
+ end
11
+
12
+ def as_json
13
+ { type: type, value: value }
14
+ end
15
+
16
+ def ensure_valid!
17
+ raise "Invalid criterion value #{value} for #{type}" unless valid?
18
+ end
19
+
20
+ def process_request!(authorization_request)
21
+ authorization_request.update! status: authorization_status_for(authorization_request)
22
+ end
23
+
24
+ def authorization_status_for(authorization_request)
25
+ meets_authorization_criteria?(authorization_request) ? :approved : :rejected
26
+ end
27
+
28
+ def self.parse(type, value)
29
+ parse_criterion_type(type, value)
30
+ end
31
+
32
+ def self.parse_criterion_type(type, value)
33
+ "ExamRegistration::AuthorizationCriterion::#{type.camelize}".constantize.new(value)
34
+ rescue
35
+ raise "Invalid criterion type #{type}"
36
+ end
37
+ end
38
+
39
+ class ExamRegistration::AuthorizationCriterion::None < ExamRegistration::AuthorizationCriterion
40
+ def initialize(_)
41
+ @value = nil
42
+ end
43
+
44
+ def valid?
45
+ !value
46
+ end
47
+
48
+ def meets_authorization_criteria?(_authorization_request)
49
+ true
50
+ end
51
+ end
52
+
53
+ class ExamRegistration::AuthorizationCriterion::PassedExercises < ExamRegistration::AuthorizationCriterion
54
+ def valid?
55
+ value.positive?
56
+ end
57
+
58
+ def meets_authorization_criteria?(authorization_request)
59
+ authorization_request.user.passed_submissions_count_in(authorization_request.organization) >= value
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  class Exercise < ApplicationRecord
2
- RANDOMIZED_FIELDS = [:default_content, :description, :extra, :hint, :test]
2
+ RANDOMIZED_FIELDS = [:default_content, :description, :extra, :hint, :test, :expectations, :custom_expectations]
3
3
  BASIC_RESOURCE_FIELDS = %i(
4
4
  name layout editor corollary teacher_info manual_evaluation locale
5
5
  choices assistance_rules randomizations tag_list extra_visible goal
@@ -28,6 +28,10 @@ class Exercise < ApplicationRecord
28
28
 
29
29
  defaults { self.submissions_count = 0 }
30
30
 
31
+ def self.default_scope
32
+ where(manual_evaluation: false) if Organization.safe_current&.prevent_manual_evaluation_content
33
+ end
34
+
31
35
  alias_method :progress_for, :assignment_for
32
36
 
33
37
  serialize :choices, Array
@@ -38,6 +42,7 @@ class Exercise < ApplicationRecord
38
42
 
39
43
  randomize(*RANDOMIZED_FIELDS)
40
44
  delegate :timed?, to: :navigable_parent
45
+ delegate :stats_for, to: :guide
41
46
 
42
47
  def console?
43
48
  queriable?
@@ -135,8 +140,6 @@ class Exercise < ApplicationRecord
135
140
  language_resource_h = language.to_embedded_resource_h if language != guide.language
136
141
  as_json(only: BASIC_RESOURCE_FIELDS)
137
142
  .merge(id: bibliotheca_id, language: language_resource_h, type: type.underscore)
138
- .merge(expectations: self[:expectations])
139
- .merge(custom_expectations: self[:custom_expectations])
140
143
  .merge(settings: self[:settings])
141
144
  .merge(RANDOMIZED_FIELDS.map { |it| [it, self[it]] }.to_h)
142
145
  .symbolize_keys
@@ -239,8 +242,8 @@ class Exercise < ApplicationRecord
239
242
  guide.pending_exercises(user)
240
243
  end
241
244
 
242
- def reading?
243
- is_a? ::Reading
245
+ def solvable?
246
+ is_a? ::Problem
244
247
  end
245
248
 
246
249
  private
@@ -8,8 +8,10 @@ class Challenge < Exercise
8
8
  self.layout = self.class.default_layout
9
9
  end
10
10
 
11
- def extra(*)
12
- [guide.extra, super]
11
+ alias_method :own_extra, :extra
12
+
13
+ def extra
14
+ [guide.extra, own_extra]
13
15
  .compact
14
16
  .join("\n")
15
17
  .strip
@@ -21,20 +21,15 @@ class Problem < QueriableChallenge
21
21
  self.expectations = []
22
22
  end
23
23
 
24
+ alias_method :own_expectations, :expectations
25
+ alias_method :own_custom_expectations, :custom_expectations
26
+
24
27
  def expectations
25
- own_expectations + guide_expectations
28
+ own_expectations + guide.expectations
26
29
  end
27
30
 
28
31
  def custom_expectations
29
- "#{own_custom_expectations}\n#{guide_custom_expectations}"
30
- end
31
-
32
- def guide_expectations
33
- guide.expectations
34
- end
35
-
36
- def guide_custom_expectations
37
- guide.custom_expectations
32
+ "#{own_custom_expectations}\n#{guide.custom_expectations}"
38
33
  end
39
34
 
40
35
  def evaluation_criteria?
@@ -6,7 +6,8 @@ class Guide < Content
6
6
 
7
7
  include WithStats,
8
8
  WithExpectations,
9
- WithLanguage
9
+ WithLanguage,
10
+ WithAssignmentsBatch
10
11
 
11
12
  markdown_on :corollary, :sources, :learn_more, :teacher_info
12
13
 
@@ -42,7 +43,11 @@ class Guide < Content
42
43
  end
43
44
 
44
45
  def next_exercise(user)
45
- user.next_exercise_at(self)
46
+ if user.present?
47
+ user.next_exercise_at(self)
48
+ else
49
+ first_exercise
50
+ end
46
51
  end
47
52
 
48
53
  # TODO: Make use of pending_siblings logic
@@ -66,8 +66,43 @@ class Indicator < Progress
66
66
  where(content: content, organization: organization).delete_all
67
67
  end
68
68
 
69
+ def _move_to!(organization)
70
+ move_children_to!(organization)
71
+ super
72
+ end
73
+
74
+ def _copy_to!(organization)
75
+ progress_item = super
76
+ children.each { |it| it._copy_to! organization }
77
+ progress_item
78
+ end
79
+
80
+ def move_children_to!(organization)
81
+ children.update_all(organization_id: organization.id)
82
+
83
+ indicators.each { |it| it.move_children_to!(organization) }
84
+ end
85
+
86
+ def cascade_delete_children!
87
+ indicators.each(&:cascade_delete_children!)
88
+ children.delete_all(:delete_all)
89
+ end
90
+
91
+ def content_available_in?(organization)
92
+ content.usage_in_organization(organization).present?
93
+ end
94
+
95
+ def delete_duplicates_in!(organization)
96
+ duplicates_in(organization).each(&:cascade_delete_children!)
97
+ super
98
+ end
99
+
69
100
  private
70
101
 
102
+ def duplicates_key
103
+ { content: content, user: user }
104
+ end
105
+
71
106
  def children
72
107
  indicators.presence || assignments
73
108
  end
@@ -0,0 +1,5 @@
1
+ class Notification < ApplicationRecord
2
+ belongs_to :user
3
+ belongs_to :organization
4
+ belongs_to :target, polymorphic: true
5
+ end
@@ -207,7 +207,7 @@ class Organization < ApplicationRecord
207
207
  end
208
208
 
209
209
  def silenced?
210
- !Mumukit::Platform::Organization.current? || current.silent?
210
+ !current? || current.silent?
211
211
  end
212
212
 
213
213
  def sync_key_id_field
@@ -0,0 +1,17 @@
1
+ class Preferences
2
+ include ActiveModel::Model
3
+
4
+ def self.attributes
5
+ [:uppercase_mode]
6
+ end
7
+
8
+ attr_accessor *self.attributes
9
+
10
+ def self.from_attributes(*args)
11
+ new self.attributes.zip(args).to_h
12
+ end
13
+
14
+ def uppercase?
15
+ uppercase_mode
16
+ end
17
+ end
@@ -12,4 +12,39 @@ class Progress < ApplicationRecord
12
12
  def dirty_parent_by_submission!
13
13
  parent&.dirty_by_submission!
14
14
  end
15
+
16
+ def _copy_to!(organization)
17
+ dup.transfer_to!(organization)
18
+ end
19
+
20
+ def transfer_to!(organization)
21
+ update! organization: organization, parent: nil
22
+ self
23
+ end
24
+
25
+ alias_method :_move_to!, :transfer_to!
26
+
27
+ %w(copy move).each do |transfer_type|
28
+ define_method "#{transfer_type}_to!" do |organization|
29
+ "Mumuki::Domain::ProgressTransfer::#{transfer_type.camelize}".constantize.new(self, organization).execute!
30
+ end
31
+ end
32
+
33
+ def guide_indicator?
34
+ is_a?(Indicator) && content_type == 'Guide'
35
+ end
36
+
37
+ def has_duplicates_in?(organization)
38
+ duplicates_in(organization).present?
39
+ end
40
+
41
+ def delete_duplicates_in!(organization)
42
+ duplicates_in(organization).delete_all
43
+ end
44
+
45
+ private
46
+
47
+ def duplicates_in(organization)
48
+ self.class.where(duplicates_key.merge(organization: organization)).where.not(id: id)
49
+ end
15
50
  end
@@ -11,6 +11,10 @@ class Stats
11
11
  failed + pending == 0
12
12
  end
13
13
 
14
+ def almost_done?
15
+ failed + pending <= 1
16
+ end
17
+
14
18
  def started?
15
19
  submitted > 0
16
20
  end
@@ -35,6 +35,14 @@ class Topic < Content
35
35
  Chapter.where(topic: self).map(&:book).each(&:reindex_usages!)
36
36
  end
37
37
 
38
+ def monolesson
39
+ @monolesson ||= lessons.to_a.single
40
+ end
41
+
42
+ def monolesson?
43
+ monolesson.present?
44
+ end
45
+
38
46
  ## Forking
39
47
 
40
48
  def fork_children_into!(dup, organization, syncer)
@@ -42,18 +50,19 @@ class Topic < Content
42
50
  end
43
51
 
44
52
  def pending_lessons(user)
45
- guides.
46
- joins('left join public.exercises exercises
47
- on exercises.guide_id = guides.id').
48
- joins("left join public.assignments assignments
53
+ lessons
54
+ .includes(:guide)
55
+ .references(:guide)
56
+ .joins('left join exercises exercises on exercises.guide_id = guides.id')
57
+ .joins("left join assignments assignments
49
58
  on assignments.exercise_id = exercises.id
50
59
  and assignments.submitter_id = #{user.id}
51
60
  and assignments.submission_status in (
52
61
  #{Mumuki::Domain::Status::Submission::Passed.to_i},
53
62
  #{Mumuki::Domain::Status::Submission::ManualEvaluationPending.to_i}
54
- )").
55
- where('assignments.id is null').
56
- group('public.guides.id', 'lessons.number').map(&:lesson)
63
+ )")
64
+ .where('assignments.id is null')
65
+ .group('guides.id', 'lessons.number', 'lessons.id')
57
66
  end
58
67
 
59
68
  private
@@ -7,11 +7,13 @@ class User < ApplicationRecord
7
7
  Awardee,
8
8
  Disabling,
9
9
  WithTermsAcceptance,
10
+ WithPreferences,
10
11
  Mumuki::Domain::Helpers::User
11
12
 
12
13
  serialize :permissions, Mumukit::Auth::Permissions
13
14
 
14
15
 
16
+ has_many :notifications
15
17
  has_many :assignments, foreign_key: :submitter_id
16
18
  has_many :messages, -> { order(created_at: :desc) }, through: :assignments
17
19
 
@@ -49,6 +51,10 @@ class User < ApplicationRecord
49
51
  last_guide.try(:lesson)
50
52
  end
51
53
 
54
+ def passed_submissions_count_in(organization)
55
+ assignments.where(top_submission_status: Mumuki::Domain::Status::Submission::Passed.to_i, organization: organization).count
56
+ end
57
+
52
58
  def submissions_count
53
59
  assignments.pluck(:submissions_count).sum
54
60
  end
@@ -206,7 +212,7 @@ class User < ApplicationRecord
206
212
  end
207
213
 
208
214
  def build_assignment(exercise, organization)
209
- assignments.build(exercise: exercise, organization: organization)
215
+ Assignment.build_for(self, exercise, organization)
210
216
  end
211
217
 
212
218
  def pending_siblings_at(content)
@@ -1,6 +1,7 @@
1
1
  module WithStats
2
2
  def stats_for(user)
3
- Stats.from_statuses exercises.map { |it| it.status_for(user) }
3
+ return unless user.present?
4
+ Stats.from_statuses statuses_for(user)
4
5
  end
5
6
 
6
7
  def started?(user)
@@ -0,0 +1,5 @@
1
+ class AddUppercaseModeToUser < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :users, :uppercase_mode, :boolean
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ class CreateExamRegistrations < ActiveRecord::Migration[5.1]
2
+ def change
3
+ create_table :exam_registrations do |t|
4
+ t.string :description
5
+ t.datetime :start_time, null: false
6
+ t.datetime :end_time, null: false
7
+ t.integer :authorization_criterion_type, default: 0
8
+ t.integer :authorization_criterion_value
9
+ t.references :organization, index: true
10
+
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ class CreateExamAuthorizationRequest < ActiveRecord::Migration[5.1]
2
+ def change
3
+ create_table :exam_authorization_requests do |t|
4
+ t.integer :status, default: 0
5
+ t.references :exam, index: true
6
+ t.references :exam_registration, index: true
7
+ t.references :user, index: true
8
+ t.references :organization, index: true
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ class CreateNotification < ActiveRecord::Migration[5.1]
2
+ def change
3
+ create_table :notifications do |t|
4
+ t.integer :priority, default: 100
5
+ t.boolean :read, default: false
6
+ t.references :target, polymorphic: true
7
+ t.references :user, index: true
8
+ t.references :organization, index: true
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ class AddPreventManualEvaluationContentToOrganizations < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :organizations, :prevent_manual_evaluation_content, :boolean
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ class CreateExamRegistrationExamJoinTable < ActiveRecord::Migration[5.1]
2
+ def change
3
+ create_join_table :exams, :exam_registrations do |t|
4
+ t.index :exam_id
5
+ t.index :exam_registration_id
6
+ end
7
+ end
8
+ end
@@ -38,6 +38,7 @@ require_relative './domain/workspace'
38
38
  require_relative './domain/helpers'
39
39
  require_relative './domain/syncable'
40
40
  require_relative './domain/store'
41
+ require_relative './domain/progress_transfer'
41
42
 
42
43
  class Mumukit::Assistant
43
44
  def self.valid?(rules)
@@ -1,5 +1,7 @@
1
1
  require_relative './extensions/string'
2
+ require_relative './extensions/symbol'
2
3
  require_relative './extensions/array'
3
4
  require_relative './extensions/module'
4
5
  require_relative './extensions/hash'
6
+ require_relative './extensions/nil'
5
7
  require_relative './extensions/time'
@@ -19,10 +19,8 @@ class Array
19
19
  def multiple?
20
20
  size > 1
21
21
  end
22
- end
23
22
 
24
- class NilClass
25
- def insert_last(element)
26
- [element]
23
+ def randomize_with(randomizer, seed)
24
+ map { |it| it.randomize_with randomizer, seed }
27
25
  end
28
26
  end
@@ -11,4 +11,8 @@ class Hash
11
11
  def markdownified(*keys, **options)
12
12
  map { |k, v| key.in?(keys) ? v.markdownified(options) : v }.to_h
13
13
  end
14
+
15
+ def randomize_with(randomizer, seed)
16
+ transform_values { |v| v.randomize_with randomizer, seed }
17
+ end
14
18
  end
@@ -0,0 +1,17 @@
1
+ class NilClass
2
+ def affable
3
+ end
4
+
5
+ def markdownified(**options)
6
+ end
7
+
8
+ def sanitized
9
+ end
10
+
11
+ def randomize_with(randomizer, seed)
12
+ end
13
+
14
+ def insert_last(element)
15
+ [element]
16
+ end
17
+ end
@@ -59,15 +59,8 @@ class String
59
59
  def sanitized
60
60
  Mumukit::ContentType::Sanitizer.sanitize self
61
61
  end
62
- end
63
-
64
- class NilClass
65
- def affable
66
- end
67
62
 
68
- def markdownified(**options)
69
- end
70
-
71
- def sanitized
63
+ def randomize_with(randomizer, seed)
64
+ randomizer.randomize!(self, seed)
72
65
  end
73
66
  end
@@ -0,0 +1,5 @@
1
+ class Symbol
2
+ def randomize_with(randomizer, seed)
3
+ self.to_s.randomize_with(randomizer, seed).to_sym
4
+ end
5
+ end
@@ -7,6 +7,8 @@ require_relative './factories/complement_factory'
7
7
  require_relative './factories/course_factory'
8
8
  require_relative './factories/discussion_factory'
9
9
  require_relative './factories/exam_factory'
10
+ require_relative './factories/exam_authorization_request_factory'
11
+ require_relative './factories/exam_registration_factory'
10
12
  require_relative './factories/exercise_factory'
11
13
  require_relative './factories/guide_factory'
12
14
  require_relative './factories/invitation_factory'
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ factory :exam_authorization_request do
3
+ organization { Organization.current }
4
+ exam { create(:exam) }
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ FactoryBot.define do
2
+ factory :exam_registration do
3
+ description { Faker::Lorem.sentence(word_count: 5) }
4
+ organization { Organization.current }
5
+ start_time { 5.minutes.ago }
6
+ end_time { 10.minutes.since }
7
+ end
8
+ end
@@ -74,6 +74,10 @@ module Mumuki::Domain::Helpers::Organization
74
74
  Mumukit::Platform::Organization.current?
75
75
  end
76
76
 
77
+ def safe_current
78
+ current if current?
79
+ end
80
+
77
81
  def parse(json)
78
82
  json
79
83
  .slice(:name)
@@ -107,7 +107,7 @@ module Mumuki::Domain
107
107
  end
108
108
 
109
109
  def build_assignment(exercise, organization)
110
- Assignment.new exercise: exercise, organization: organization, submitter: self
110
+ Assignment.build_for(self, exercise, organization)
111
111
  end
112
112
 
113
113
  def pending_siblings_at(content)
@@ -15,7 +15,7 @@ en:
15
15
  attributes:
16
16
  randomizations:
17
17
  invalid_format: 'format is invalid'
18
- own_expectations:
18
+ raw_expectations:
19
19
  invalid_format: 'format is invalid'
20
20
  assistance_rules:
21
21
  invalid_format: 'format is invalid'
@@ -32,7 +32,7 @@ es-CL:
32
32
  attributes:
33
33
  randomizations:
34
34
  invalid_format: 'formato inválido'
35
- own_expectations:
35
+ raw_expectations:
36
36
  invalid_format: 'formato inválido'
37
37
  assistance_rules:
38
38
  invalid_format: 'formato inválido'
@@ -32,7 +32,7 @@ es:
32
32
  attributes:
33
33
  randomizations:
34
34
  invalid_format: 'formato inválido'
35
- own_expectations:
35
+ raw_expectations:
36
36
  invalid_format: 'formato inválido'
37
37
  assistance_rules:
38
38
  invalid_format: 'formato inválido'
@@ -15,7 +15,7 @@ pt:
15
15
  attributes:
16
16
  randomizations:
17
17
  invalid_format: 'o formato é inválido'
18
- own_expectations:
18
+ raw_expectations:
19
19
  invalid_format: 'o formato é inválido'
20
20
  assistance_rules:
21
21
  invalid_format: 'o formato é inválido'
@@ -0,0 +1,8 @@
1
+ module Mumuki::Domain
2
+ module ProgressTransfer
3
+ end
4
+ end
5
+
6
+ require_relative './progress_transfer/base'
7
+ require_relative './progress_transfer/move'
8
+ require_relative './progress_transfer/copy'
@@ -0,0 +1,44 @@
1
+ class Mumuki::Domain::ProgressTransfer::Base
2
+ attr_reader *%i(source_organization destination_organization progress_item transferred_item)
3
+
4
+ delegate :user, to: :progress_item
5
+
6
+ def initialize(progress_item, destination_organization)
7
+ @progress_item = progress_item
8
+ @destination_organization = destination_organization
9
+ end
10
+
11
+ def execute!
12
+ ActiveRecord::Base.transaction do
13
+ pre_transfer!
14
+ transfer!
15
+ post_transfer!
16
+ end
17
+ end
18
+
19
+ def pre_transfer!
20
+ validate_transferrable!
21
+ @source_organization = progress_item.organization
22
+ progress_item.delete_duplicates_in!(destination_organization)
23
+ end
24
+
25
+ def transfer!
26
+ @transferred_item = do_transfer!
27
+ end
28
+
29
+ def post_transfer!
30
+ transferred_item.dirty_parent_by_submission!
31
+ notify_transfer!
32
+ transferred_item
33
+ end
34
+
35
+ def validate_transferrable!
36
+ raise "Transferred progress' content must be available in destination!" unless progress_item.content_available_in?(destination_organization)
37
+ raise 'User must be student in destination organization' unless user.student_of?(destination_organization)
38
+ raise 'Transfer only supported for guide indicators' unless progress_item.guide_indicator?
39
+ end
40
+
41
+ def notify_transfer!
42
+ Mumukit::Nuntius.notify! 'progress-transfers', { from: source_organization.name, to: destination_organization.name, item_type: transferred_item.class.to_s, item_id: transferred_item.id, transfer_type: transfer_type }
43
+ end
44
+ end
@@ -0,0 +1,9 @@
1
+ class Mumuki::Domain::ProgressTransfer::Copy < Mumuki::Domain::ProgressTransfer::Base
2
+ def transfer_type
3
+ :copy
4
+ end
5
+
6
+ def do_transfer!
7
+ progress_item._copy_to!(destination_organization)
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ class Mumuki::Domain::ProgressTransfer::Move < Mumuki::Domain::ProgressTransfer::Base
2
+ def transfer_type
3
+ :move
4
+ end
5
+
6
+ def pre_transfer!
7
+ super
8
+ progress_item.dirty_parent_by_submission!
9
+ end
10
+
11
+ def do_transfer!
12
+ progress_item._move_to!(destination_organization)
13
+ end
14
+ end
@@ -10,6 +10,6 @@ module Mumuki::Domain::Status::Submission::ManualEvaluationPending
10
10
  end
11
11
 
12
12
  def self.iconize
13
- {class: :info, type: 'clock-o'}
13
+ {class: :info, type: 'clock'}
14
14
  end
15
15
  end
@@ -50,4 +50,8 @@ module Mumuki::Domain::Status::Submission
50
50
  def exp_given
51
51
  0
52
52
  end
53
+
54
+ def dup
55
+ self
56
+ end
53
57
  end
@@ -1,5 +1,5 @@
1
1
  module Mumuki
2
2
  module Domain
3
- VERSION = '8.1.1'
3
+ VERSION = '8.4.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: 8.1.1
4
+ version: 8.4.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-12-03 00:00:00.000000000 Z
11
+ date: 2021-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -270,6 +270,7 @@ files:
270
270
  - app/models/concerns/submittable/triable.rb
271
271
  - app/models/concerns/topic_container.rb
272
272
  - app/models/concerns/with_assignments.rb
273
+ - app/models/concerns/with_assignments_batch.rb
273
274
  - app/models/concerns/with_case_insensitive_search.rb
274
275
  - app/models/concerns/with_description.rb
275
276
  - app/models/concerns/with_discussion_creation.rb
@@ -286,6 +287,7 @@ files:
286
287
  - app/models/concerns/with_messages.rb
287
288
  - app/models/concerns/with_name.rb
288
289
  - app/models/concerns/with_number.rb
290
+ - app/models/concerns/with_preferences.rb
289
291
  - app/models/concerns/with_profile.rb
290
292
  - app/models/concerns/with_progress.rb
291
293
  - app/models/concerns/with_randomizations.rb
@@ -297,6 +299,7 @@ files:
297
299
  - app/models/concerns/with_slug.rb
298
300
  - app/models/concerns/with_target_audience.rb
299
301
  - app/models/concerns/with_terms_acceptance.rb
302
+ - app/models/concerns/with_timed_enablement.rb
300
303
  - app/models/concerns/with_usages.rb
301
304
  - app/models/concerns/with_user_navigation.rb
302
305
  - app/models/content.rb
@@ -306,6 +309,9 @@ files:
306
309
  - app/models/exam.rb
307
310
  - app/models/exam/passing_criterion.rb
308
311
  - app/models/exam_authorization.rb
312
+ - app/models/exam_authorization_request.rb
313
+ - app/models/exam_registration.rb
314
+ - app/models/exam_registration/authorization_criterion.rb
309
315
  - app/models/exercise.rb
310
316
  - app/models/exercise/challenge.rb
311
317
  - app/models/exercise/interactive.rb
@@ -320,7 +326,9 @@ files:
320
326
  - app/models/lesson.rb
321
327
  - app/models/medal.rb
322
328
  - app/models/message.rb
329
+ - app/models/notification.rb
323
330
  - app/models/organization.rb
331
+ - app/models/preferences.rb
324
332
  - app/models/progress.rb
325
333
  - app/models/stats.rb
326
334
  - app/models/subscription.rb
@@ -637,6 +645,12 @@ files:
637
645
  - db/migrate/20201027134205_add_immersible_to_organization.rb
638
646
  - db/migrate/20201027152806_create_terms.rb
639
647
  - db/migrate/20201130163114_add_banned_from_forum_to_users.rb
648
+ - db/migrate/20210111125810_add_uppercase_mode_to_user.rb
649
+ - db/migrate/20210114200545_create_exam_registrations.rb
650
+ - db/migrate/20210118180941_create_exam_authorization_request.rb
651
+ - db/migrate/20210118194904_create_notification.rb
652
+ - db/migrate/20210119160440_add_prevent_manual_evaluation_content_to_organizations.rb
653
+ - db/migrate/20210119190204_create_exam_registration_exam_join_table.rb
640
654
  - lib/mumuki/domain.rb
641
655
  - lib/mumuki/domain/area.rb
642
656
  - lib/mumuki/domain/engine.rb
@@ -656,7 +670,9 @@ files:
656
670
  - lib/mumuki/domain/extensions/array.rb
657
671
  - lib/mumuki/domain/extensions/hash.rb
658
672
  - lib/mumuki/domain/extensions/module.rb
673
+ - lib/mumuki/domain/extensions/nil.rb
659
674
  - lib/mumuki/domain/extensions/string.rb
675
+ - lib/mumuki/domain/extensions/symbol.rb
660
676
  - lib/mumuki/domain/extensions/time.rb
661
677
  - lib/mumuki/domain/factories.rb
662
678
  - lib/mumuki/domain/factories/api_client_factory.rb
@@ -667,7 +683,9 @@ files:
667
683
  - lib/mumuki/domain/factories/complement_factory.rb
668
684
  - lib/mumuki/domain/factories/course_factory.rb
669
685
  - lib/mumuki/domain/factories/discussion_factory.rb
686
+ - lib/mumuki/domain/factories/exam_authorization_request_factory.rb
670
687
  - lib/mumuki/domain/factories/exam_factory.rb
688
+ - lib/mumuki/domain/factories/exam_registration_factory.rb
671
689
  - lib/mumuki/domain/factories/exercise_factory.rb
672
690
  - lib/mumuki/domain/factories/guide_factory.rb
673
691
  - lib/mumuki/domain/factories/invitation_factory.rb
@@ -702,6 +720,10 @@ files:
702
720
  - lib/mumuki/domain/organization/profile.rb
703
721
  - lib/mumuki/domain/organization/settings.rb
704
722
  - lib/mumuki/domain/organization/theme.rb
723
+ - lib/mumuki/domain/progress_transfer.rb
724
+ - lib/mumuki/domain/progress_transfer/base.rb
725
+ - lib/mumuki/domain/progress_transfer/copy.rb
726
+ - lib/mumuki/domain/progress_transfer/move.rb
705
727
  - lib/mumuki/domain/seed.rb
706
728
  - lib/mumuki/domain/status.rb
707
729
  - lib/mumuki/domain/status/discussion/closed.rb
@@ -757,7 +779,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
757
779
  - !ruby/object:Gem::Version
758
780
  version: '0'
759
781
  requirements: []
760
- rubygems_version: 3.1.2
782
+ rubygems_version: 3.0.3
761
783
  signing_key:
762
784
  specification_version: 4
763
785
  summary: Mumuki Platform's Domain Model