mumuki-domain 8.2.0 → 8.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) 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 +4 -0
  5. data/app/models/book.rb +1 -1
  6. data/app/models/chapter.rb +3 -0
  7. data/app/models/concerns/guide_container.rb +2 -1
  8. data/app/models/concerns/with_assignments.rb +1 -0
  9. data/app/models/concerns/with_assignments_batch.rb +31 -0
  10. data/app/models/concerns/with_notifications.rb +17 -0
  11. data/app/models/concerns/with_preferences.rb +7 -0
  12. data/app/models/concerns/with_timed_enablement.rb +11 -0
  13. data/app/models/discussion.rb +4 -0
  14. data/app/models/exam.rb +4 -8
  15. data/app/models/exam_authorization_request.rb +26 -0
  16. data/app/models/exam_registration.rb +46 -0
  17. data/app/models/exam_registration/authorization_criterion.rb +61 -0
  18. data/app/models/exercise.rb +5 -0
  19. data/app/models/guide.rb +7 -2
  20. data/app/models/message.rb +4 -0
  21. data/app/models/notification.rb +9 -0
  22. data/app/models/organization.rb +5 -1
  23. data/app/models/preferences.rb +17 -0
  24. data/app/models/stats.rb +4 -0
  25. data/app/models/topic.rb +16 -7
  26. data/app/models/user.rb +8 -5
  27. data/app/models/user_stats.rb +32 -0
  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/factories.rb +3 -0
  36. data/lib/mumuki/domain/factories/book_factory.rb +13 -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 +4 -1
  42. data/lib/mumuki/domain/version.rb +1 -1
  43. metadata +24 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ea9b10953e5031cfcfa680763efb0eac02e63b91219c3dbf71b0d30b5dd3250
4
- data.tar.gz: 8fda448216e9d80cb4de41a7f43825882e043895705c66c1c6b9b4bc68c5bfec
3
+ metadata.gz: 339d512e2040015c6556f9671fd00281edf9b651a5dc9db8f860d7607989cd65
4
+ data.tar.gz: a03fe1fe26593986af16d89c1df6cc6e9b8f21c3e9e240de41f884482e75c6d7
5
5
  SHA512:
6
- metadata.gz: f2f8f341502367fa4a7ba037eb086f0506c19fede8290aba4921430fb22e33cc338faf01bc85d23d9df89fca006dba97705e377f48c3eae789170291644562e5
7
- data.tar.gz: e5044aa8475aba10ea9d1140eceefccd9fc36de384cf215007f4f239568aebf9f06583074249af630fff81db9da8d429604fa428db0c509a55c4f9092b25965d
6
+ metadata.gz: ea2475a456f0558a61e798d27e88921a7a69b134dc38431c1244033d778dbc1b91ce4a974acaa6abbf77cceed03134430cb378f2e4d4561f39d297a7040a9745
7
+ data.tar.gz: 1321dbd879300fb21e87ef044bae0a7bcbca62ff9fc27cf55bfac1e02cc122273952add367a94381a54e1b730e68549659e5abb9762795071e8c7fa89d6a3699
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!
@@ -263,6 +263,10 @@ class Assignment < Progress
263
263
  update! misplaced: value if value != misplaced?
264
264
  end
265
265
 
266
+ def self.build_for(user, exercise, organization)
267
+ Assignment.new submitter: user, exercise: exercise, organization: organization
268
+ end
269
+
266
270
  private
267
271
 
268
272
  def duplicates_key
data/app/models/book.rb CHANGED
@@ -18,7 +18,7 @@ class Book < Content
18
18
  end
19
19
 
20
20
  def discussions_in_organization(organization = Organization.current)
21
- Discussion.where(organization: organization).includes(exercise: [:language, :guide])
21
+ Discussion.where(organization: organization, item: organization.exercises).includes(exercise: [:language, :guide])
22
22
  end
23
23
 
24
24
  def first_chapter
@@ -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
@@ -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
@@ -138,6 +138,10 @@ class Organization < ApplicationRecord
138
138
  self[:progressive_display_lookahead] = lookahead.to_i.positive? ? lookahead : nil
139
139
  end
140
140
 
141
+ def activity_start_date(default_date)
142
+ [default_date, in_preparation_until&.to_date].compact.max
143
+ end
144
+
141
145
  # ==============
142
146
  # Display fields
143
147
  # ==============
@@ -207,7 +211,7 @@ class Organization < ApplicationRecord
207
211
  end
208
212
 
209
213
  def silenced?
210
- !Mumukit::Platform::Organization.current? || current.silent?
214
+ !current? || current.silent?
211
215
  end
212
216
 
213
217
  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
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)
@@ -10,7 +10,39 @@ class UserStats < ApplicationRecord
10
10
  self.stats_for(user).exp
11
11
  end
12
12
 
13
+ def activity(date_range = nil)
14
+ date_filter = { submitted_at: date_range }.compact
15
+ {
16
+ exercises: {
17
+ solved_count: organization_exercises
18
+ .joins(:assignments)
19
+ .where(assignments: { top_submission_status: [:passed, :skipped], submitter: user }.merge(date_filter))
20
+ .count,
21
+ count: organization_exercises.count},
22
+
23
+ messages: messages_in_discussions_count(date_range)
24
+ }
25
+ end
26
+
13
27
  def add_exp!(points)
14
28
  self.exp += points
15
29
  end
30
+
31
+ private
32
+
33
+ def messages_in_discussions_count(date_range = nil)
34
+ date_filter = { date: date_range }.compact
35
+ result = Message.joins(:discussion)
36
+ .where({sender: user.uid, discussions: { organization: organization }}.merge(date_filter))
37
+ .group(:approved)
38
+ .count
39
+ unapproved = result[false] || 0
40
+ approved = result[true] || 0
41
+
42
+ { count: unapproved + approved, approved: approved }
43
+ end
44
+
45
+ def organization_exercises
46
+ @organization_exercises ||= organization.exercises
47
+ end
16
48
  end
@@ -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
@@ -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'
@@ -4,4 +4,17 @@ FactoryBot.define do
4
4
  description { Faker::Lorem.sentence(word_count: 30) }
5
5
  slug { "mumuki/mumuki-test-book-#{SecureRandom.uuid}" }
6
6
  end
7
+
8
+ factory :book_with_full_tree, parent: :book do
9
+ transient do
10
+ children_factor { 3 }
11
+ exercises { create_list(:exercise, children_factor) }
12
+ lessons { create_list(:lesson, children_factor, exercises: exercises) }
13
+ chapters { create_list(:chapter, children_factor, lessons: lessons) }
14
+ end
15
+
16
+ after(:build) do |book, evaluator|
17
+ book.chapters = evaluator.chapters
18
+ end
19
+ end
7
20
  end
@@ -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)
@@ -90,6 +90,9 @@ module Mumuki::Domain
90
90
  def visit!(*)
91
91
  end
92
92
 
93
+ def currently_in_exam?
94
+ false
95
+ end
93
96
  # ========
94
97
  # Progress
95
98
  # ========
@@ -107,7 +110,7 @@ module Mumuki::Domain
107
110
  end
108
111
 
109
112
  def build_assignment(exercise, organization)
110
- Assignment.new exercise: exercise, organization: organization, submitter: self
113
+ Assignment.build_for(self, exercise, organization)
111
114
  end
112
115
 
113
116
  def pending_siblings_at(content)
@@ -1,5 +1,5 @@
1
1
  module Mumuki
2
2
  module Domain
3
- VERSION = '8.2.0'
3
+ VERSION = '8.6.1'
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.2.0
4
+ version: 8.6.1
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: 2021-01-07 00:00:00.000000000 Z
11
+ date: 2021-02-26 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