mumuki-domain 8.1.3 → 8.6.0

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/Rakefile +2 -0
  3. data/app/models/application_record.rb +6 -0
  4. data/app/models/assignment.rb +10 -1
  5. data/app/models/chapter.rb +3 -0
  6. data/app/models/concerns/guide_container.rb +2 -1
  7. data/app/models/concerns/with_assignments.rb +1 -0
  8. data/app/models/concerns/with_assignments_batch.rb +31 -0
  9. data/app/models/concerns/with_notifications.rb +17 -0
  10. data/app/models/concerns/with_preferences.rb +7 -0
  11. data/app/models/concerns/with_timed_enablement.rb +11 -0
  12. data/app/models/discussion.rb +4 -0
  13. data/app/models/exam.rb +4 -8
  14. data/app/models/exam_authorization_request.rb +26 -0
  15. data/app/models/exam_registration.rb +46 -0
  16. data/app/models/exam_registration/authorization_criterion.rb +61 -0
  17. data/app/models/exercise.rb +5 -0
  18. data/app/models/guide.rb +7 -2
  19. data/app/models/indicator.rb +35 -0
  20. data/app/models/message.rb +4 -0
  21. data/app/models/notification.rb +9 -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 +8 -5
  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/factories.rb +3 -0
  37. data/lib/mumuki/domain/factories/exam_authorization_request_factory.rb +6 -0
  38. data/lib/mumuki/domain/factories/exam_registration_factory.rb +8 -0
  39. data/lib/mumuki/domain/factories/notification_factory.rb +5 -0
  40. data/lib/mumuki/domain/helpers/organization.rb +4 -0
  41. data/lib/mumuki/domain/incognito.rb +1 -1
  42. data/lib/mumuki/domain/progress_transfer.rb +8 -0
  43. data/lib/mumuki/domain/progress_transfer/base.rb +44 -0
  44. data/lib/mumuki/domain/progress_transfer/copy.rb +9 -0
  45. data/lib/mumuki/domain/progress_transfer/move.rb +14 -0
  46. data/lib/mumuki/domain/status/submission/manual_evaluation_pending.rb +1 -1
  47. data/lib/mumuki/domain/status/submission/submission.rb +4 -0
  48. data/lib/mumuki/domain/version.rb +1 -1
  49. metadata +28 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9495d00fe4f85bdc27f5cf67b94e5f09de465a31a795e9e3d56b3f4cd91433e1
4
- data.tar.gz: 209256d417ff7c98c309b517cd7076a9743ea00de79124b6bec3e735a49a5238
3
+ metadata.gz: 0fc67d5d416345806e5d570dae375114d9bd32e3be5af26485a926c9957d10f2
4
+ data.tar.gz: cb223b383c8f176ac852091b8cf79ed2f496404bc75d1d7dcc3ccfbde70e49f2
5
5
  SHA512:
6
- metadata.gz: 88ccf91cbccb01f15ec513e232d48832e69a0ec9f5153992e91eab56c44183f10a10dc2a4bcd894509708ab97461b4698ecb73485da8d7dfa8826196837e0c3d
7
- data.tar.gz: 80b0c3ec21c9444dedb05c1c1060035cdd0d5f0f731635ae79e4738c0a0d23c6fbf4ac67d48c6bb0ea70a76ee7e825a05a2afaaad95e8d2be54096636e0e2af0
6
+ metadata.gz: 3a891889e37c9fd9a55c383f2ffe6aa2f9368193bb7550ba34d9253fb8fbab3534be85ac6acc8a162f02907059acc57da14658ee6df7ad19ad93523ee1ef998d
7
+ data.tar.gz: e830934dd132b0bada6f45348249f06d170438c98fbb07528fa1e608397b40945485adeb4a79e732d701371e4196a9fae22c5e4ba68ef983a71dc7ae40236704
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
@@ -129,6 +129,12 @@ class ApplicationRecord < ActiveRecord::Base
129
129
  end
130
130
  end
131
131
 
132
+ def self.enum_prefixed_translations_for(selector)
133
+ send(selector.to_s.pluralize).map do |key, _|
134
+ [I18n.t("#{selector}_#{key}", default: key.to_sym), key]
135
+ end
136
+ end
137
+
132
138
  private
133
139
 
134
140
  def raise_foreign_key_error!
@@ -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
 
@@ -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
@@ -0,0 +1,17 @@
1
+ module WithNotifications
2
+ extend ActiveSupport::Concern
3
+
4
+ def unread_messages
5
+ messages.where read: false
6
+ end
7
+
8
+ def unread_notifications
9
+ # TODO: message and discussion should trigger a notification instead of being one
10
+ all = notifications.where(read: false) + unread_messages + unread_discussions
11
+ all.sort_by(&:created_at).reverse
12
+ end
13
+
14
+ def read_notification!(target)
15
+ notifications.find_by(target: target)&.mark_as_read!
16
+ end
17
+ 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
@@ -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
@@ -48,6 +48,10 @@ class Discussion < ApplicationRecord
48
48
  nil
49
49
  end
50
50
 
51
+ def target
52
+ self
53
+ end
54
+
51
55
  def used_in?(organization)
52
56
  organization == self.organization
53
57
  end
data/app/models/exam.rb CHANGED
@@ -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,26 @@
1
+ class ExamAuthorizationRequest < ApplicationRecord
2
+ include TerminalNavigation
3
+
4
+ belongs_to :exam
5
+ belongs_to :user
6
+ belongs_to :organization
7
+ belongs_to :exam_registration
8
+
9
+ enum status: %i(pending approved rejected)
10
+
11
+ after_update :notify_user!
12
+
13
+ def try_authorize!
14
+ exam.authorize! user if approved?
15
+ end
16
+
17
+ def name
18
+ exam_registration.description
19
+ end
20
+
21
+ private
22
+
23
+ def notify_user!
24
+ Notification.create! organization: organization, user: user, target: self if saved_change_to_status?
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ class ExamRegistration < ApplicationRecord
2
+ include WithTimedEnablement
3
+ include TerminalNavigation
4
+
5
+ belongs_to :organization
6
+ has_and_belongs_to_many :exams
7
+ has_many :authorization_requests, class_name: 'ExamAuthorizationRequest'
8
+
9
+ enum authorization_criterion_type: %i(none passed_exercises), _prefix: :authorization_criterion
10
+
11
+ before_save :ensure_valid_authorization_criterion!
12
+
13
+ delegate :meets_authorization_criteria?, :process_request!, to: :authorization_criterion
14
+
15
+ alias_attribute :name, :description
16
+
17
+ def authorization_criterion
18
+ @authorization_criterion ||= ExamRegistration::AuthorizationCriterion.parse(authorization_criterion_type, authorization_criterion_value)
19
+ end
20
+
21
+ def ensure_valid_authorization_criterion!
22
+ authorization_criterion.ensure_valid!
23
+ end
24
+
25
+ def start!(users)
26
+ users.each &method(:notify_user!)
27
+ end
28
+
29
+ def process_requests!
30
+ authorization_requests.each do |it|
31
+ process_request! it
32
+ it.try_authorize!
33
+ end
34
+ end
35
+
36
+ def authorization_request_for(user)
37
+ authorization_requests.find_by(user: user) ||
38
+ ExamAuthorizationRequest.new(exam_registration: self, organization: organization)
39
+ end
40
+
41
+ private
42
+
43
+ def notify_user!(user)
44
+ Notification.create! organization: organization, user: user, target: self
45
+ end
46
+ 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
@@ -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?
data/app/models/guide.rb CHANGED
@@ -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
@@ -70,6 +70,10 @@ class Message < ApplicationRecord
70
70
  from_initiator? && !not_actually_a_question?
71
71
  end
72
72
 
73
+ def target
74
+ self
75
+ end
76
+
73
77
  def self.parse_json(json)
74
78
  message = json.delete 'message'
75
79
  json
@@ -0,0 +1,9 @@
1
+ class Notification < ApplicationRecord
2
+ belongs_to :user
3
+ belongs_to :organization
4
+ belongs_to :target, polymorphic: true
5
+
6
+ def mark_as_read!
7
+ update read: true
8
+ end
9
+ 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
data/app/models/stats.rb CHANGED
@@ -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
data/app/models/topic.rb CHANGED
@@ -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
data/app/models/user.rb CHANGED
@@ -3,15 +3,18 @@ class User < ApplicationRecord
3
3
  include WithProfile,
4
4
  WithUserNavigation,
5
5
  WithReminders,
6
+ WithNotifications,
6
7
  WithDiscussionCreation,
7
8
  Awardee,
8
9
  Disabling,
9
10
  WithTermsAcceptance,
11
+ WithPreferences,
10
12
  Mumuki::Domain::Helpers::User
11
13
 
12
14
  serialize :permissions, Mumukit::Auth::Permissions
13
15
 
14
16
 
17
+ has_many :notifications
15
18
  has_many :assignments, foreign_key: :submitter_id
16
19
  has_many :messages, -> { order(created_at: :desc) }, through: :assignments
17
20
 
@@ -49,6 +52,10 @@ class User < ApplicationRecord
49
52
  last_guide.try(:lesson)
50
53
  end
51
54
 
55
+ def passed_submissions_count_in(organization)
56
+ assignments.where(top_submission_status: Mumuki::Domain::Status::Submission::Passed.to_i, organization: organization).count
57
+ end
58
+
52
59
  def submissions_count
53
60
  assignments.pluck(:submissions_count).sum
54
61
  end
@@ -69,10 +76,6 @@ class User < ApplicationRecord
69
76
  assignments.where(status: Mumuki::Domain::Status::Submission::Passed.to_i)
70
77
  end
71
78
 
72
- def unread_messages
73
- messages.where read: false
74
- end
75
-
76
79
  def visit!(organization)
77
80
  update!(last_organization: organization) if organization != last_organization
78
81
  end
@@ -206,7 +209,7 @@ class User < ApplicationRecord
206
209
  end
207
210
 
208
211
  def build_assignment(exercise, organization)
209
- assignments.build(exercise: exercise, organization: organization)
212
+ Assignment.build_for(self, exercise, organization)
210
213
  end
211
214
 
212
215
  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
data/lib/mumuki/domain.rb CHANGED
@@ -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)
@@ -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'
@@ -14,6 +16,7 @@ require_relative './factories/lesson_factory'
14
16
  require_relative './factories/login_settings_factory'
15
17
  require_relative './factories/medal_factory'
16
18
  require_relative './factories/message_factory'
19
+ require_relative './factories/notification_factory'
17
20
  require_relative './factories/organization_factory'
18
21
  require_relative './factories/term_factory'
19
22
  require_relative './factories/topic_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
@@ -0,0 +1,5 @@
1
+ FactoryBot.define do
2
+ factory :notification do
3
+ organization { Organization.current }
4
+ end
5
+ 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)
@@ -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.3'
3
+ VERSION = '8.6.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.3
4
+ version: 8.6.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-18 00:00:00.000000000 Z
11
+ date: 2021-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -170,28 +170,28 @@ dependencies:
170
170
  requirements:
171
171
  - - "~>"
172
172
  - !ruby/object:Gem::Version
173
- version: '5.0'
173
+ version: '6.0'
174
174
  type: :runtime
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
- version: '5.0'
180
+ version: '6.0'
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: mumukit-inspection
183
183
  requirement: !ruby/object:Gem::Requirement
184
184
  requirements:
185
185
  - - "~>"
186
186
  - !ruby/object:Gem::Version
187
- version: '5.1'
187
+ version: '6.0'
188
188
  type: :runtime
189
189
  prerelease: false
190
190
  version_requirements: !ruby/object:Gem::Requirement
191
191
  requirements:
192
192
  - - "~>"
193
193
  - !ruby/object:Gem::Version
194
- version: '5.1'
194
+ version: '6.0'
195
195
  - !ruby/object:Gem::Dependency
196
196
  name: sprockets
197
197
  requirement: !ruby/object:Gem::Requirement
@@ -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
@@ -285,7 +286,9 @@ files:
285
286
  - app/models/concerns/with_medal.rb
286
287
  - app/models/concerns/with_messages.rb
287
288
  - app/models/concerns/with_name.rb
289
+ - app/models/concerns/with_notifications.rb
288
290
  - app/models/concerns/with_number.rb
291
+ - app/models/concerns/with_preferences.rb
289
292
  - app/models/concerns/with_profile.rb
290
293
  - app/models/concerns/with_progress.rb
291
294
  - app/models/concerns/with_randomizations.rb
@@ -297,6 +300,7 @@ files:
297
300
  - app/models/concerns/with_slug.rb
298
301
  - app/models/concerns/with_target_audience.rb
299
302
  - app/models/concerns/with_terms_acceptance.rb
303
+ - app/models/concerns/with_timed_enablement.rb
300
304
  - app/models/concerns/with_usages.rb
301
305
  - app/models/concerns/with_user_navigation.rb
302
306
  - app/models/content.rb
@@ -306,6 +310,9 @@ files:
306
310
  - app/models/exam.rb
307
311
  - app/models/exam/passing_criterion.rb
308
312
  - app/models/exam_authorization.rb
313
+ - app/models/exam_authorization_request.rb
314
+ - app/models/exam_registration.rb
315
+ - app/models/exam_registration/authorization_criterion.rb
309
316
  - app/models/exercise.rb
310
317
  - app/models/exercise/challenge.rb
311
318
  - app/models/exercise/interactive.rb
@@ -320,7 +327,9 @@ files:
320
327
  - app/models/lesson.rb
321
328
  - app/models/medal.rb
322
329
  - app/models/message.rb
330
+ - app/models/notification.rb
323
331
  - app/models/organization.rb
332
+ - app/models/preferences.rb
324
333
  - app/models/progress.rb
325
334
  - app/models/stats.rb
326
335
  - app/models/subscription.rb
@@ -637,6 +646,12 @@ files:
637
646
  - db/migrate/20201027134205_add_immersible_to_organization.rb
638
647
  - db/migrate/20201027152806_create_terms.rb
639
648
  - db/migrate/20201130163114_add_banned_from_forum_to_users.rb
649
+ - db/migrate/20210111125810_add_uppercase_mode_to_user.rb
650
+ - db/migrate/20210114200545_create_exam_registrations.rb
651
+ - db/migrate/20210118180941_create_exam_authorization_request.rb
652
+ - db/migrate/20210118194904_create_notification.rb
653
+ - db/migrate/20210119160440_add_prevent_manual_evaluation_content_to_organizations.rb
654
+ - db/migrate/20210119190204_create_exam_registration_exam_join_table.rb
640
655
  - lib/mumuki/domain.rb
641
656
  - lib/mumuki/domain/area.rb
642
657
  - lib/mumuki/domain/engine.rb
@@ -669,7 +684,9 @@ files:
669
684
  - lib/mumuki/domain/factories/complement_factory.rb
670
685
  - lib/mumuki/domain/factories/course_factory.rb
671
686
  - lib/mumuki/domain/factories/discussion_factory.rb
687
+ - lib/mumuki/domain/factories/exam_authorization_request_factory.rb
672
688
  - lib/mumuki/domain/factories/exam_factory.rb
689
+ - lib/mumuki/domain/factories/exam_registration_factory.rb
673
690
  - lib/mumuki/domain/factories/exercise_factory.rb
674
691
  - lib/mumuki/domain/factories/guide_factory.rb
675
692
  - lib/mumuki/domain/factories/invitation_factory.rb
@@ -677,6 +694,7 @@ files:
677
694
  - lib/mumuki/domain/factories/login_settings_factory.rb
678
695
  - lib/mumuki/domain/factories/medal_factory.rb
679
696
  - lib/mumuki/domain/factories/message_factory.rb
697
+ - lib/mumuki/domain/factories/notification_factory.rb
680
698
  - lib/mumuki/domain/factories/organization_factory.rb
681
699
  - lib/mumuki/domain/factories/term_factory.rb
682
700
  - lib/mumuki/domain/factories/topic_factory.rb
@@ -704,6 +722,10 @@ files:
704
722
  - lib/mumuki/domain/organization/profile.rb
705
723
  - lib/mumuki/domain/organization/settings.rb
706
724
  - lib/mumuki/domain/organization/theme.rb
725
+ - lib/mumuki/domain/progress_transfer.rb
726
+ - lib/mumuki/domain/progress_transfer/base.rb
727
+ - lib/mumuki/domain/progress_transfer/copy.rb
728
+ - lib/mumuki/domain/progress_transfer/move.rb
707
729
  - lib/mumuki/domain/seed.rb
708
730
  - lib/mumuki/domain/status.rb
709
731
  - lib/mumuki/domain/status/discussion/closed.rb