mumuki-domain 9.0.5 → 9.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46425c74cf597dd17603273a455f7a5956c2f0b80926f81e739f45de0bd4e17e
4
- data.tar.gz: 9241fe5525d3c387e726fda5037044c96564d77c802ec4af9e01de086648018e
3
+ metadata.gz: fc8a965a85ae367436a825f12235e8ce3149baacb9e610d6f2801c97270cf603
4
+ data.tar.gz: d1f59395e72b047031ff987e47e8a48a596aefd1c57d566480c4c7e33737d058
5
5
  SHA512:
6
- metadata.gz: f7ff8c266a27514fac9fe776572efa0ec5d742627bda381b42218364853d75bd51c22aa7aae86be0fb0c8692e9b9b43209e96bc8ac6015cebacce0f3cdb869e7
7
- data.tar.gz: 4ba53f6362fcbbbb38e9be6fc19e8a90561cfb02e6a0cc2ba82e597ca5cae8fc06d768f2e0ec7ef8255366d241f5558b1a0f2409b34bedc074466792d78f27fa
6
+ metadata.gz: b9b535002f54d54cfd9487122943bd44e31a6563eda77f672fb3b78347b13bf2cecacc880c7999efa04ed3a028d1352f899764a0fd95cbc4f53f617e9f010904
7
+ data.tar.gz: dbde9d021212f2fd8f6118df3a9f6585606e317b8b6d14acd0adb1190b834b8fdf63646ba1b2025db204e3424bcefe17c2491aa1c2f63cdeadd4b4b6a7bc798f
@@ -29,7 +29,7 @@ class ApplicationRecord < ActiveRecord::Base
29
29
  def self.serialize_symbolized_hash_array(*keys)
30
30
  keys.each do |field|
31
31
  serialize field
32
- define_method(field) { self[field]&.map { |it| it.symbolize_keys } }
32
+ define_method(field) { self[field]&.map { |it| it.deep_symbolize_keys } }
33
33
  end
34
34
  end
35
35
 
@@ -134,6 +134,19 @@ class ApplicationRecord < ActiveRecord::Base
134
134
  end
135
135
  end
136
136
 
137
+ def self.active_between(start_date_field, end_date_field, **options)
138
+ define_singleton_method(:active) do |actually_filter=true|
139
+ if actually_filter
140
+ self.where("(#{start_date_field} IS NULL OR #{start_date_field} < :now) AND (#{end_date_field} IS NULL OR #{end_date_field} > :now)", now: Time.now)
141
+ else
142
+ all
143
+ end
144
+ end
145
+
146
+ aliased_as = options.delete(:aliased_as)
147
+ singleton_class.send(:alias_method, aliased_as, :active) if aliased_as
148
+ end
149
+
137
150
  ## Partially implements resource-hash protocol, by
138
151
  ## defining `to_resource_h` and helper methods `resource_fields` and `slice_resource_h`
139
152
  ## using the given fields
@@ -2,6 +2,8 @@ class CertificateProgram < ApplicationRecord
2
2
  belongs_to :organization
3
3
  has_many :certificates
4
4
 
5
+ active_between :start_date, :end_date, aliased_as: :ongoing
6
+
5
7
  def friendly
6
8
  title
7
9
  end
@@ -23,8 +25,8 @@ class CertificateProgram < ApplicationRecord
23
25
  }
24
26
  </style>
25
27
  <!-- You can use interpolations like --
26
- <%#= certificate.start_date %>
27
- <%#= certificate.end_date %>
28
+ <%#= certificate.started_at %>
29
+ <%#= certificate.ended_at %>
28
30
  <%#= user.formal_first_name %>
29
31
  <%#= user.formal_last_name %>
30
32
  <%#= user.formal_full_name %>
@@ -2,8 +2,8 @@ class Chapter < ApplicationRecord
2
2
  include WithStats
3
3
  include WithNumber
4
4
 
5
- include SiblingsNavigation
6
5
  include TerminalNavigation
6
+ include SiblingsNavigation
7
7
 
8
8
  include FriendlyName
9
9
 
@@ -29,8 +29,4 @@ class Chapter < ApplicationRecord
29
29
  def structural_parent
30
30
  book
31
31
  end
32
-
33
- def pending_siblings_for(user)
34
- book.pending_chapters(user)
35
- end
36
32
  end
@@ -12,10 +12,9 @@ module SiblingsNavigation
12
12
  structural_parent.structural_children
13
13
  end
14
14
 
15
- #TODO reestablish this after indicators reliably linked to assignments
16
- # def pending_siblings_for(user, organization=Organization.current)
17
- # siblings.reject { |it| it.progress_for(user, organization).completed? }
18
- # end
15
+ def pending_siblings_for(user, organization=Organization.current)
16
+ siblings.reject { |it| it.progress_for(user, organization).completed? }
17
+ end
19
18
 
20
19
  # Names
21
20
 
@@ -18,4 +18,8 @@ module TerminalNavigation
18
18
  def siblings
19
19
  []
20
20
  end
21
+
22
+ def next_for(_user)
23
+ nil
24
+ end
21
25
  end
@@ -0,0 +1,16 @@
1
+ module WithSoftDeletion
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ enum deletion_motive: %i(self_deleted inappropriate_content shares_solution discloses_personal_information)
6
+ belongs_to :deleted_by, class_name: 'User', optional: true
7
+ end
8
+
9
+ def soft_delete!(motive, deleter)
10
+ update! deletion_motive: motive, deleted_by: deleter, deleted_at: Time.now
11
+ end
12
+
13
+ def deleted?
14
+ deleted_at.present?
15
+ end
16
+ end
@@ -28,7 +28,7 @@ class Discussion < ApplicationRecord
28
28
  delegate :language, to: :item
29
29
  delegate :to_discussion_status, to: :status
30
30
 
31
- MODERATOR_REVIEW_AVERAGE_TIME = 10.minutes
31
+ MODERATOR_REVIEW_AVERAGE_TIME = 17.minutes
32
32
 
33
33
  scope :for_user, -> (user) do
34
34
  if user.try(:moderator_here?)
@@ -38,6 +38,10 @@ class Discussion < ApplicationRecord
38
38
  end
39
39
  end
40
40
 
41
+ def visible_messages
42
+ messages.where.not(deletion_motive: :self_deleted).or(messages.where(deletion_motive: nil))
43
+ end
44
+
41
45
  def try_solve!
42
46
  if opened?
43
47
  update! status: reachable_statuses_for(initiator).first
@@ -73,7 +77,7 @@ class Discussion < ApplicationRecord
73
77
  end
74
78
 
75
79
  def last_message_date
76
- messages.last&.created_at || created_at
80
+ visible_messages.last&.created_at || created_at
77
81
  end
78
82
 
79
83
  def friendly
@@ -125,11 +129,11 @@ class Discussion < ApplicationRecord
125
129
  end
126
130
 
127
131
  def has_messages?
128
- messages.exists? || description.present?
132
+ visible_messages.exists? || description.present?
129
133
  end
130
134
 
131
135
  def responses_count
132
- messages.where.not(sender: initiator.uid).count
136
+ visible_messages.where.not(sender: initiator.uid).count
133
137
  end
134
138
 
135
139
  def has_responses?
@@ -186,6 +190,6 @@ class Discussion < ApplicationRecord
186
190
  private
187
191
 
188
192
  def messages_by_updated_at(direction = :desc)
189
- messages.reorder(updated_at: direction)
193
+ messages.where(deletion_motive: nil).reorder(updated_at: direction)
190
194
  end
191
195
  end
@@ -18,6 +18,17 @@ class ExamAuthorizationRequest < ApplicationRecord
18
18
  exam_registration.description
19
19
  end
20
20
 
21
+ def icon
22
+ case status.to_sym
23
+ when :pending
24
+ { class: 'info-circle', type: 'info' }
25
+ when :approved
26
+ { class: 'check-circle', type: 'success' }
27
+ when :rejected
28
+ { class: 'times-circle', type: 'danger' }
29
+ end
30
+ end
31
+
21
32
  private
22
33
 
23
34
  def notify_user!
@@ -5,6 +5,8 @@ class ExamRegistration < ApplicationRecord
5
5
  belongs_to :organization
6
6
  has_and_belongs_to_many :exams
7
7
  has_many :authorization_requests, class_name: 'ExamAuthorizationRequest'
8
+ has_and_belongs_to_many :registrees, class_name: 'User'
9
+ has_many :notifications, as: :target
8
10
 
9
11
  enum authorization_criterion_type: %i(none passed_exercises), _prefix: :authorization_criterion
10
12
 
@@ -22,8 +24,20 @@ class ExamRegistration < ApplicationRecord
22
24
  authorization_criterion.ensure_valid!
23
25
  end
24
26
 
25
- def start!(users)
26
- users.each &method(:notify_user!)
27
+ def notify_unnotified_registrees!
28
+ unnotified_registrees.each { |registree| notify_registree! registree }
29
+ end
30
+
31
+ def unnotified_registrees?
32
+ unnotified_registrees.exists?
33
+ end
34
+
35
+ def register_users!(users)
36
+ users.each { |user| register! user }
37
+ end
38
+
39
+ def unnotified_registrees
40
+ registrees.where.not(id: Notification.notified_users_ids_for(self, self.organization))
27
41
  end
28
42
 
29
43
  def process_requests!
@@ -38,9 +52,17 @@ class ExamRegistration < ApplicationRecord
38
52
  ExamAuthorizationRequest.new(exam_registration: self, organization: organization)
39
53
  end
40
54
 
55
+ def register!(user)
56
+ registrees << user unless registered?(user)
57
+ end
58
+
59
+ def registered?(user)
60
+ registrees.include? user
61
+ end
62
+
41
63
  private
42
64
 
43
- def notify_user!(user)
44
- Notification.create! organization: organization, user: user, target: self
65
+ def notify_registree!(registree)
66
+ Notification.create! organization: organization, user: registree, target: self
45
67
  end
46
68
  end
@@ -249,18 +249,6 @@ class Exercise < ApplicationRecord
249
249
  private
250
250
 
251
251
  def evaluation_class
252
- if manual_evaluation?
253
- manual_evaluation_class
254
- else
255
- automated_evaluation_class
256
- end
257
- end
258
-
259
- def manual_evaluation_class
260
- Mumuki::Domain::Evaluation::Manual
261
- end
262
-
263
- def automated_evaluation_class
264
252
  Mumuki::Domain::Evaluation::Automated
265
253
  end
266
254
 
@@ -33,13 +33,31 @@ class Problem < QueriableChallenge
33
33
  end
34
34
 
35
35
  def evaluation_criteria?
36
- manual_evaluation? || expectations? || test.present?
36
+ manual_evaluation? || automated_evaluation?
37
+ end
38
+
39
+ def mixed_evaluation?
40
+ manual_evaluation? && automated_evaluation?
41
+ end
42
+
43
+ def automated_evaluation?
44
+ expectations? || test.present?
37
45
  end
38
46
 
39
47
  def expectations?
40
48
  own_expectations.present? || own_custom_expectations.present?
41
49
  end
42
50
 
51
+ def evaluation_class
52
+ if mixed_evaluation?
53
+ Mumuki::Domain::Evaluation::Mixed
54
+ elsif manual_evaluation?
55
+ Mumuki::Domain::Evaluation::Manual
56
+ else
57
+ Mumuki::Domain::Evaluation::Automated
58
+ end
59
+ end
60
+
43
61
  # Sets the layout. This method accepts input_kids as a synonym of input_primary
44
62
  # for historical reasons
45
63
  def layout=(layout)
@@ -1,4 +1,5 @@
1
1
  class Message < ApplicationRecord
2
+ include WithSoftDeletion
2
3
 
3
4
  belongs_to :discussion, optional: true
4
5
  belongs_to :assignment, foreign_key: :submission_id, primary_key: :submission_id, optional: true
@@ -10,7 +11,6 @@ class Message < ApplicationRecord
10
11
  validates_presence_of :submission_id, :unless => :discussion_id?
11
12
 
12
13
  after_save :update_counters_cache!
13
- after_destroy :update_counters_cache!
14
14
 
15
15
  markdown_on :content
16
16
 
@@ -43,7 +43,8 @@ class Message < ApplicationRecord
43
43
  end
44
44
 
45
45
  def to_resource_h
46
- as_json(except: [:id, :type, :discussion_id, :approved, :not_actually_a_question],
46
+ as_json(except: [:id, :type, :discussion_id, :approved, :approved_at, :approved_by_id,
47
+ :not_actually_a_question, :deletion_motive, :deleted_at, :deleted_by_id],
47
48
  include: {exercise: {only: [:bibliotheca_id]}})
48
49
  .merge(organization: Organization.current.name)
49
50
  end
@@ -3,6 +3,11 @@ class Notification < ApplicationRecord
3
3
  belongs_to :organization
4
4
  belongs_to :target, polymorphic: true
5
5
 
6
+
7
+ scope :notified_users_ids_for, ->(target, organization=Organization.current) do
8
+ where(target: target, organization: organization).pluck(:user_id)
9
+ end
10
+
6
11
  def mark_as_read!
7
12
  update read: true
8
13
  end
@@ -21,6 +21,8 @@ class Organization < ApplicationRecord
21
21
  belongs_to :book
22
22
  has_many :usages
23
23
 
24
+ has_many :certificate_programs
25
+
24
26
  validates_presence_of :contact_email, :locale
25
27
  validates_presence_of :welcome_email_template, if: :greet_new_users?
26
28
  validates :name, uniqueness: true,
@@ -31,7 +33,7 @@ class Organization < ApplicationRecord
31
33
  after_create :reindex_usages!
32
34
  after_update :reindex_usages!, if: lambda { |user| user.saved_change_to_book_id? }
33
35
 
34
- has_many :guides, through: 'usages', source: 'item', source_type: 'Guide'
36
+ has_many :guides, -> { where 'usages.parent_item_type' => 'Lesson' }, through: 'usages', source: 'item', source_type: 'Guide'
35
37
  has_many :exercises, through: :guides
36
38
  has_many :assignments, through: :exercises
37
39
  has_many :exams
@@ -108,6 +110,10 @@ class Organization < ApplicationRecord
108
110
  name
109
111
  end
110
112
 
113
+ def ongoing_certificate_programs?
114
+ certificate_programs.ongoing.exists?
115
+ end
116
+
111
117
  # Tells if the given user can
112
118
  # ask for help in this organization
113
119
  #
data/app/models/user.rb CHANGED
@@ -211,6 +211,10 @@ class User < ApplicationRecord
211
211
  name.split.map(&:first).map(&:capitalize).join(' ')
212
212
  end
213
213
 
214
+ def abbreviated_name
215
+ "#{first_name} #{last_name.first.capitalize + '.' if last_name.present?}".strip
216
+ end
217
+
214
218
  def progress_at(content, organization)
215
219
  Indicator.find_or_initialize_by(user: self, organization: organization, content: content)
216
220
  end
@@ -283,11 +287,11 @@ class User < ApplicationRecord
283
287
  end
284
288
 
285
289
  def formal_first_name
286
- verified_first_name || first_name
290
+ verified_first_name.presence || first_name
287
291
  end
288
292
 
289
293
  def formal_last_name
290
- verified_last_name || last_name
294
+ verified_last_name.presence || last_name
291
295
  end
292
296
 
293
297
  def formal_full_name
@@ -302,6 +306,12 @@ class User < ApplicationRecord
302
306
  certificates.where(certificate_program: certificate_program).exists?
303
307
  end
304
308
 
309
+ def certificate_in(certificate_program, certificate_h)
310
+ return if certificated_in?(certificate_program)
311
+ certificate = certificates.create certificate_h.merge(certificate_program: certificate_program)
312
+ UserMailer.certificate(certificate).deliver_later
313
+ end
314
+
305
315
  private
306
316
 
307
317
  def welcome_to_new_organizations!
@@ -16,10 +16,10 @@ class UserStats < ApplicationRecord
16
16
  exercises: {
17
17
  solved_count: organization_exercises
18
18
  .joins(:assignments)
19
- .where(assignments: { top_submission_status: [:passed, :skipped], submitter: user }.merge(date_filter))
19
+ .where(assignments: { top_submission_status: [:passed, :skipped], submitter: user, organization: organization }.merge(date_filter))
20
20
  .count,
21
- count: organization_exercises.count},
22
-
21
+ count: organization_exercises.count
22
+ },
23
23
  messages: messages_in_discussions_count(date_range)
24
24
  }
25
25
  end
@@ -0,0 +1,7 @@
1
+ class AddDeletionFieldsToMessage < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :messages, :deletion_motive, :integer
4
+ add_column :messages, :deleted_at, :datetime
5
+ add_reference :messages, :deleted_by, index: true
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ class AddDatesToCertificateProgram < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :certificate_programs, :start_date, :datetime
4
+ add_column :certificate_programs, :end_date, :datetime
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class RenameCertificateDates < ActiveRecord::Migration[5.1]
2
+ def change
3
+ rename_column :certificates, :start_date, :started_at
4
+ rename_column :certificates, :end_date, :ended_at
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ class CreateExamRegistrationUserJoinTable < ActiveRecord::Migration[5.1]
2
+ def change
3
+ create_join_table :exam_registrations, :users do |t|
4
+ t.index :user_id
5
+ t.index :exam_registration_id
6
+ end
7
+ end
8
+ end
@@ -3,3 +3,4 @@ end
3
3
 
4
4
  require_relative './evaluation/manual'
5
5
  require_relative './evaluation/automated'
6
+ require_relative './evaluation/mixed'
@@ -0,0 +1,12 @@
1
+ class Mumuki::Domain::Evaluation::Mixed < Mumuki::Domain::Evaluation::Manual
2
+ def evaluate!(assignment, submission)
3
+ evaluation = submission.evaluate! assignment
4
+ if evaluation[:status].passed?
5
+ super
6
+ elsif evaluation[:status].passed_with_warnings?
7
+ evaluation.merge(status: Mumuki::Domain::Status::Submission::Failed)
8
+ else
9
+ evaluation
10
+ end
11
+ end
12
+ end
@@ -1,7 +1,7 @@
1
1
  FactoryBot.define do
2
2
  factory :certificate do
3
- start_date { 1.month.ago }
4
- end_date { 1.minute.ago }
3
+ started_at { 1.month.ago }
4
+ ended_at { 1.minute.ago }
5
5
  user { build :user, first_name: 'Jane', last_name: 'Doe' }
6
6
  certificate_program { build :certificate_program }
7
7
  end
@@ -1,5 +1,5 @@
1
1
  module Mumuki
2
2
  module Domain
3
- VERSION = '9.0.5'
3
+ VERSION = '9.2.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: 9.0.5
4
+ version: 9.2.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: 2021-03-19 00:00:00.000000000 Z
11
+ date: 2021-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -301,6 +301,7 @@ files:
301
301
  - app/models/concerns/with_scoped_queries/page.rb
302
302
  - app/models/concerns/with_scoped_queries/sort.rb
303
303
  - app/models/concerns/with_slug.rb
304
+ - app/models/concerns/with_soft_deletion.rb
304
305
  - app/models/concerns/with_target_audience.rb
305
306
  - app/models/concerns/with_terms_acceptance.rb
306
307
  - app/models/concerns/with_timed_enablement.rb
@@ -661,12 +662,17 @@ files:
661
662
  - db/migrate/20210302181654_add_faqs_to_organizations.rb
662
663
  - db/migrate/20210308145910_add_approved_by_and_at_to_message.rb
663
664
  - db/migrate/20210310195602_add_delete_account_token_to_user.rb
665
+ - db/migrate/20210318191512_add_deletion_fields_to_message.rb
666
+ - db/migrate/20210318194559_add_dates_to_certificate_program.rb
667
+ - db/migrate/20210318195238_rename_certificate_dates.rb
668
+ - db/migrate/20210330175706_create_exam_registration_user_join_table.rb
664
669
  - lib/mumuki/domain.rb
665
670
  - lib/mumuki/domain/area.rb
666
671
  - lib/mumuki/domain/engine.rb
667
672
  - lib/mumuki/domain/evaluation.rb
668
673
  - lib/mumuki/domain/evaluation/automated.rb
669
674
  - lib/mumuki/domain/evaluation/manual.rb
675
+ - lib/mumuki/domain/evaluation/mixed.rb
670
676
  - lib/mumuki/domain/exceptions.rb
671
677
  - lib/mumuki/domain/exceptions/blocked_forum_error.rb
672
678
  - lib/mumuki/domain/exceptions/disabled_error.rb