mumuki-domain 9.0.0 → 9.0.6

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: 05b62ff1728ff0d8fd4a807ce4a8de4b7f70c532a6748c91af12db484582fad0
4
- data.tar.gz: 25810325f5fa2fb294b0c189bd20f77e85e4722e2557d50b8a760df07edd56bf
3
+ metadata.gz: 03cf2d9f0764fe073de42e72cbb94233949692b8e5ef60b97afeca431d374719
4
+ data.tar.gz: 5db8d1777ae3622bf9673b09f9bc06322ffc46af56780f4a47fc19e5f77462f9
5
5
  SHA512:
6
- metadata.gz: f5795882f6b1ed8f8e7b4b66e3f85767c58f72eda7131aae3034a5abe48a27d27b2789e2f97e135f4713da0667769c58baec5ea2b9408ceacf012470439e3dbd
7
- data.tar.gz: 34514f8787088f9c721fafae973af54a1163d27c00d47029bac4544e78c31d4e8726320ef9d3a81f453090e465c32cd5f10ebb25bad2b4ea9ab6f1227118f270
6
+ metadata.gz: a3abfa44040d80767963218a039bc1eea419921bffb8a4efbd8908bff4b1d5b395cf9c4d10d1844978ee71caa9d2f783d045f4e3f365e68c69a680c930b9b08f
7
+ data.tar.gz: ec63d6c11722ac7dc29c6d58af654fc3dc72ed71fdc387555e60ac71d1711cf893016aaa16c55903d85c5ad961cff7aa0206dd642074f7e5a5fc6f14a246336a
@@ -87,6 +87,22 @@ class ApplicationRecord < ActiveRecord::Base
87
87
  end
88
88
  end
89
89
 
90
+ def self.with_temporary_token(field_name, duration = 2.hours)
91
+ class_eval do
92
+ token_attribute = field_name
93
+ token_date_attribute = "#{field_name}_expiration_date"
94
+
95
+ define_method("generate_#{field_name}!") do
96
+ update!(token_attribute => self.class.generate_secure_token, token_date_attribute => duration.from_now)
97
+ end
98
+
99
+ define_method("#{field_name}_matches?") do |token|
100
+ actual_token = attribute(token_attribute)
101
+ actual_token.present? && token == actual_token && attribute(token_date_attribute)&.future?
102
+ end
103
+ end
104
+ end
105
+
90
106
  def self.numbered(*associations)
91
107
  class_eval do
92
108
  associations.each do |it|
@@ -118,6 +134,19 @@ class ApplicationRecord < ActiveRecord::Base
118
134
  end
119
135
  end
120
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
+
121
150
  ## Partially implements resource-hash protocol, by
122
151
  ## defining `to_resource_h` and helper methods `resource_fields` and `slice_resource_h`
123
152
  ## using the given fields
@@ -140,4 +169,8 @@ class ApplicationRecord < ActiveRecord::Base
140
169
  def raise_foreign_key_error!
141
170
  raise ActiveRecord::InvalidForeignKey.new "#{model_name} is still referenced"
142
171
  end
172
+
173
+ def self.generate_secure_token
174
+ SecureRandom.base58(24)
175
+ end
143
176
  end
@@ -275,11 +275,11 @@ class Assignment < Progress
275
275
 
276
276
  def update_submissions_count!
277
277
  self.class.connection.execute(
278
- "update public.exercises
278
+ "update exercises
279
279
  set submissions_count = submissions_count + 1
280
280
  where id = #{exercise.id}")
281
281
  self.class.connection.execute(
282
- "update public.assignments
282
+ "update assignments
283
283
  set submissions_count = submissions_count + 1
284
284
  where id = #{id}")
285
285
  exercise.reload
@@ -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 %>
data/app/models/course.rb CHANGED
@@ -11,7 +11,7 @@ class Course < ApplicationRecord
11
11
 
12
12
  alias_attribute :name, :code
13
13
 
14
- resource_fields :slug, :shifts, :code, :days, :period, :description
14
+ resource_fields :slug, :shifts, :code, :days, :period, :description, :period_start, :period_end
15
15
 
16
16
  def current_invitation
17
17
  invitations.where('expiration_date > ?', Time.now).first
@@ -35,6 +35,26 @@ class Course < ApplicationRecord
35
35
  end
36
36
  end
37
37
 
38
+ def ended?
39
+ period_end.present? && period_end.past?
40
+ end
41
+
42
+ def started?
43
+ period_start.present? && period_start.past?
44
+ end
45
+
46
+ def infer_period_range!
47
+ return if period_start || period_end
48
+
49
+ period =~ /^(\d{4})?/
50
+ year = $1.to_i
51
+
52
+ return nil unless year.between? 2014, (DateTime.now.year + 1)
53
+
54
+ self.period_start = DateTime.new(year).beginning_of_year
55
+ self.period_end = DateTime.new(year).end_of_year
56
+ end
57
+
38
58
  def canonical_code
39
59
  "#{period}-#{code}".downcase
40
60
  end
@@ -167,7 +167,7 @@ class Discussion < ApplicationRecord
167
167
  end
168
168
 
169
169
  def being_accessed_by_moderator?
170
- last_moderator_access_at.present? && last_moderator_access_at > Time.now - MODERATOR_REVIEW_AVERAGE_TIME
170
+ last_moderator_access_at.present? && (last_moderator_access_at + MODERATOR_REVIEW_AVERAGE_TIME).future?
171
171
  end
172
172
 
173
173
  def last_moderator_access_visible_for?(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,22 +249,23 @@ 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
 
267
255
  def self.default_layout
268
256
  layouts.keys[0]
269
257
  end
258
+
259
+ def self.with_pending_assignments_for(user, relation)
260
+ relation.
261
+ joins("left join assignments assignments
262
+ on assignments.exercise_id = exercises.id
263
+ and assignments.submitter_id = #{user.id}
264
+ and assignments.organization_id = #{Organization.current.id}
265
+ and assignments.submission_status in (
266
+ #{Mumuki::Domain::Status::Submission::Passed.to_i},
267
+ #{Mumuki::Domain::Status::Submission::ManualEvaluationPending.to_i}
268
+ )").
269
+ where('assignments.id is null')
270
+ end
270
271
  end
@@ -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)
data/app/models/guide.rb CHANGED
@@ -50,17 +50,8 @@ class Guide < Content
50
50
  end
51
51
  end
52
52
 
53
- # TODO: Make use of pending_siblings logic
54
53
  def pending_exercises(user)
55
- exercises.
56
- joins("left join public.assignments assignments
57
- on assignments.exercise_id = exercises.id
58
- and assignments.submitter_id = #{user.id}
59
- and assignments.submission_status in (
60
- #{Mumuki::Domain::Status::Submission::Passed.to_i},
61
- #{Mumuki::Domain::Status::Submission::ManualEvaluationPending.to_i}
62
- )").
63
- where('assignments.id is null')
54
+ Exercise.with_pending_assignments_for(user, exercises)
64
55
  end
65
56
 
66
57
  def first_exercise
@@ -39,7 +39,7 @@ class Invitation < ApplicationRecord
39
39
  end
40
40
 
41
41
  def expired?
42
- Time.now > expiration_date
42
+ expiration_date.past?
43
43
  end
44
44
 
45
45
  def unexpired
@@ -2,6 +2,8 @@ class Message < ApplicationRecord
2
2
 
3
3
  belongs_to :discussion, optional: true
4
4
  belongs_to :assignment, foreign_key: :submission_id, primary_key: :submission_id, optional: true
5
+ belongs_to :approved_by, class_name: 'User', optional: true
6
+
5
7
  has_one :exercise, through: :assignment
6
8
 
7
9
  validates_presence_of :content, :sender
@@ -50,14 +52,22 @@ class Message < ApplicationRecord
50
52
  update! read: true
51
53
  end
52
54
 
53
- def toggle_approved!
54
- toggle! :approved
55
+ def toggle_approved!(user)
56
+ if approved?
57
+ disapprove!
58
+ else
59
+ approve!(user)
60
+ end
55
61
  end
56
62
 
57
63
  def toggle_not_actually_a_question!
58
64
  toggle! :not_actually_a_question
59
65
  end
60
66
 
67
+ def approved?
68
+ approved_at?
69
+ end
70
+
61
71
  def validated?
62
72
  approved? || from_moderator?
63
73
  end
@@ -93,4 +103,14 @@ class Message < ApplicationRecord
93
103
  Assignment.find_by(submission_id: message_data.delete('submission_id'))&.receive_answer! message_data
94
104
  end
95
105
  end
106
+
107
+ private
108
+
109
+ def approve!(user)
110
+ update! approved: true, approved_at: Time.now, approved_by: user
111
+ end
112
+
113
+ def disapprove!
114
+ update! approved: false, approved_at: nil, approved_by: nil
115
+ end
96
116
  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
@@ -11,7 +11,7 @@ class Organization < ApplicationRecord
11
11
  serialize :settings, Mumuki::Domain::Organization::Settings
12
12
  serialize :theme, Mumuki::Domain::Organization::Theme
13
13
 
14
- markdown_on :description, :display_description, :page_description
14
+ markdown_on :description, :display_description, :page_description, :faqs
15
15
  teaser_on :display_description
16
16
 
17
17
  validate :ensure_consistent_public_login
@@ -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,
@@ -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/topic.rb CHANGED
@@ -50,18 +50,13 @@ class Topic < Content
50
50
  end
51
51
 
52
52
  def pending_lessons(user)
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
58
- on assignments.exercise_id = exercises.id
59
- and assignments.submitter_id = #{user.id}
60
- and assignments.submission_status in (
61
- #{Mumuki::Domain::Status::Submission::Passed.to_i},
62
- #{Mumuki::Domain::Status::Submission::ManualEvaluationPending.to_i}
63
- )")
64
- .where('assignments.id is null')
53
+ Exercise
54
+ .with_pending_assignments_for(
55
+ user,
56
+ lessons
57
+ .includes(:guide)
58
+ .references(:guide)
59
+ .joins('left join exercises exercises on exercises.guide_id = guides.id'))
65
60
  .group('guides.id', 'lessons.number', 'lessons.id')
66
61
  end
67
62
 
data/app/models/user.rb CHANGED
@@ -49,6 +49,7 @@ class User < ApplicationRecord
49
49
  PLACEHOLDER_IMAGE_URL = 'user_shape.png'.freeze
50
50
 
51
51
  resource_fields :uid, :social_id, :email, :permissions, :verified_first_name, :verified_last_name, *profile_fields
52
+ with_temporary_token :delete_account_token
52
53
 
53
54
  def last_lesson
54
55
  last_guide.try(:lesson)
@@ -223,7 +224,7 @@ class User < ApplicationRecord
223
224
  end
224
225
 
225
226
  def next_exercise_at(guide)
226
- guide.pending_exercises(self).order('public.exercises.number asc').first
227
+ guide.pending_exercises(self).order('exercises.number asc').first
227
228
  end
228
229
 
229
230
  def run_submission!(submission, assignment, evaluation)
@@ -301,6 +302,12 @@ class User < ApplicationRecord
301
302
  certificates.where(certificate_program: certificate_program).exists?
302
303
  end
303
304
 
305
+ def certificate_in(certificate_program, certificate_h)
306
+ return if certificated_in?(certificate_program)
307
+ certificate = certificates.create certificate_h.merge(certificate_program: certificate_program)
308
+ UserMailer.certificate(certificate).deliver_later
309
+ end
310
+
304
311
  private
305
312
 
306
313
  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 AddPeriodStartAndEndToCourse < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :courses, :period_start, :datetime
4
+ add_column :courses, :period_end, :datetime
5
+ end
6
+
7
+ end
@@ -0,0 +1,5 @@
1
+ class AddFAQsToOrganizations < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :organizations, :faqs, :text
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddApprovedByAndAtToMessage < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :messages, :approved_at, :datetime
4
+ add_reference :messages, :approved_by, index: true
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class AddDeleteAccountTokenToUser < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :users, :delete_account_token, :string
4
+ add_column :users, :delete_account_token_expiration_date, :datetime
5
+ end
6
+ 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
@@ -40,10 +40,10 @@ class Mumuki::Domain::Organization::Settings < Mumukit::Platform::Model
40
40
  end
41
41
 
42
42
  def disabled?
43
- disabled_from.present? && disabled_from < Time.now
43
+ disabled_from.present? && disabled_from.past?
44
44
  end
45
45
 
46
46
  def in_preparation?
47
- in_preparation_until.present? && in_preparation_until > Time.now
47
+ in_preparation_until.present? && in_preparation_until.future?
48
48
  end
49
49
  end
@@ -1,5 +1,5 @@
1
1
  module Mumuki
2
2
  module Domain
3
- VERSION = '9.0.0'
3
+ VERSION = '9.0.6'
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.0
4
+ version: 9.0.6
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-04 00:00:00.000000000 Z
11
+ date: 2021-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -657,12 +657,20 @@ files:
657
657
  - db/migrate/20210119174504_create_certificate_programs.rb
658
658
  - db/migrate/20210119174835_create_certificates.rb
659
659
  - db/migrate/20210119190204_create_exam_registration_exam_join_table.rb
660
+ - db/migrate/20210301210530_add_period_start_and_end_to_course.rb
661
+ - db/migrate/20210302181654_add_faqs_to_organizations.rb
662
+ - db/migrate/20210308145910_add_approved_by_and_at_to_message.rb
663
+ - db/migrate/20210310195602_add_delete_account_token_to_user.rb
664
+ - db/migrate/20210318194559_add_dates_to_certificate_program.rb
665
+ - db/migrate/20210318195238_rename_certificate_dates.rb
666
+ - db/migrate/20210330175706_create_exam_registration_user_join_table.rb
660
667
  - lib/mumuki/domain.rb
661
668
  - lib/mumuki/domain/area.rb
662
669
  - lib/mumuki/domain/engine.rb
663
670
  - lib/mumuki/domain/evaluation.rb
664
671
  - lib/mumuki/domain/evaluation/automated.rb
665
672
  - lib/mumuki/domain/evaluation/manual.rb
673
+ - lib/mumuki/domain/evaluation/mixed.rb
666
674
  - lib/mumuki/domain/exceptions.rb
667
675
  - lib/mumuki/domain/exceptions/blocked_forum_error.rb
668
676
  - lib/mumuki/domain/exceptions/disabled_error.rb