mumuki-domain 7.5.1 → 7.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/assignment.rb +2 -0
  3. data/app/models/book.rb +17 -2
  4. data/app/models/concerns/{with_content.rb → container.rb} +1 -1
  5. data/app/models/concerns/contextualization.rb +1 -1
  6. data/app/models/concerns/guide_container.rb +1 -1
  7. data/app/models/concerns/submittable/solvable.rb +9 -3
  8. data/app/models/concerns/topic_container.rb +1 -1
  9. data/app/models/concerns/with_discussions.rb +9 -6
  10. data/app/models/concerns/with_layout.rb +5 -1
  11. data/app/models/concerns/with_progress.rb +4 -0
  12. data/app/models/concerns/with_scoped_queries.rb +9 -9
  13. data/app/models/concerns/with_scoped_queries/page.rb +1 -1
  14. data/app/models/concerns/with_scoped_queries/sort.rb +6 -1
  15. data/app/models/course.rb +18 -7
  16. data/app/models/discussion.rb +51 -13
  17. data/app/models/exam.rb +53 -23
  18. data/app/models/exam/passing_criterion.rb +53 -0
  19. data/app/models/exercise/challenge.rb +1 -1
  20. data/app/models/exercise/problem.rb +6 -0
  21. data/app/models/exercise/reading.rb +4 -0
  22. data/app/models/invitation.rb +7 -1
  23. data/app/models/message.rb +28 -4
  24. data/app/models/organization.rb +18 -11
  25. data/app/models/user.rb +36 -4
  26. data/db/migrate/20200601203033_add_course_to_exam.rb +5 -0
  27. data/db/migrate/20200605161350_add_passing_criterions_to_exam.rb +6 -0
  28. data/db/migrate/20200608132959_add_progressive_display_lookahead_to_organizations.rb +5 -0
  29. data/db/migrate/20200702165503_add_messages_count_to_discussion.rb +6 -0
  30. data/db/migrate/20200728162727_add_not_actually_a_question_field_to_messages.rb +5 -0
  31. data/db/migrate/20200728163038_add_requires_moderator_response_to_discussions.rb +5 -0
  32. data/db/migrate/20200730221001_add_trusted_for_forum_to_user.rb +5 -0
  33. data/db/migrate/20200731081757_add_last_moderator_access_fields_to_discussion.rb +6 -0
  34. data/lib/mumuki/domain.rb +2 -0
  35. data/lib/mumuki/domain/area.rb +13 -0
  36. data/lib/mumuki/domain/exceptions.rb +2 -0
  37. data/lib/mumuki/domain/exceptions/disabled_organization_error.rb +2 -0
  38. data/lib/mumuki/domain/exceptions/unprepared_organization_error.rb +2 -0
  39. data/lib/mumuki/domain/factories/course_factory.rb +2 -0
  40. data/lib/mumuki/domain/factories/discussion_factory.rb +2 -2
  41. data/lib/mumuki/domain/factories/exam_factory.rb +1 -0
  42. data/lib/mumuki/domain/factories/message_factory.rb +1 -6
  43. data/lib/mumuki/domain/helpers/organization.rb +6 -1
  44. data/lib/mumuki/domain/helpers/user.rb +6 -6
  45. data/lib/mumuki/domain/locales/activerecord/en.yml +1 -0
  46. data/lib/mumuki/domain/locales/activerecord/es.yml +1 -0
  47. data/lib/mumuki/domain/locales/activerecord/pt.yml +1 -0
  48. data/lib/mumuki/domain/organization/settings.rb +19 -7
  49. data/lib/mumuki/domain/status/discussion/discussion.rb +2 -6
  50. data/lib/mumuki/domain/submission/base.rb +6 -0
  51. data/lib/mumuki/domain/submission/solution.rb +3 -1
  52. data/lib/mumuki/domain/version.rb +1 -1
  53. data/lib/mumuki/domain/workspace.rb +38 -0
  54. metadata +17 -5
@@ -0,0 +1,53 @@
1
+ class Exam::PassingCriterion
2
+
3
+ attr_reader :value
4
+
5
+ def initialize(value)
6
+ @value = value
7
+ end
8
+
9
+ def type
10
+ self.class.name.demodulize.underscore
11
+ end
12
+
13
+ def as_json
14
+ {type: type, value: value}
15
+ end
16
+
17
+ def ensure_valid!
18
+ raise "Invalid criterion value #{value} for #{type}" unless valid_passing_grade?
19
+ end
20
+
21
+ def self.parse(type, value)
22
+ parse_criterion_type(type, value)
23
+ end
24
+
25
+ def self.parse_criterion_type(type, value)
26
+ "Exam::PassingCriterion::#{type.camelize}".constantize.new(value)
27
+ rescue
28
+ raise "Invalid criterion type #{type}"
29
+ end
30
+
31
+ end
32
+
33
+ class Exam::PassingCriterion::None < Exam::PassingCriterion
34
+ def initialize(_)
35
+ @value = nil
36
+ end
37
+
38
+ def valid_passing_grade?
39
+ !value
40
+ end
41
+ end
42
+
43
+ class Exam::PassingCriterion::Percentage < Exam::PassingCriterion
44
+ def valid_passing_grade?
45
+ value.between? 0, 100
46
+ end
47
+ end
48
+
49
+ class Exam::PassingCriterion::PassedExercises < Exam::PassingCriterion
50
+ def valid_passing_grade?
51
+ value >= 0
52
+ end
53
+ end
@@ -20,6 +20,6 @@ class Challenge < Exercise
20
20
 
21
21
  def defaults
22
22
  super
23
- self.layout = self.class.default_layout
23
+ self.layout ||= self.class.default_layout
24
24
  end
25
25
  end
@@ -45,6 +45,12 @@ class Problem < QueriableChallenge
45
45
  own_expectations.present? || own_custom_expectations.present?
46
46
  end
47
47
 
48
+ # Sets the layout. This method accepts input_kids as a synonym of input_primary
49
+ # for historical reasons
50
+ def layout=(layout)
51
+ self[:layout] = layout.like?(:input_kids) ? :input_primary : layout
52
+ end
53
+
48
54
  private
49
55
 
50
56
  def ensure_evaluation_criteria
@@ -11,6 +11,10 @@ class Reading < Exercise
11
11
  false
12
12
  end
13
13
 
14
+ def layout=(layout)
15
+ raise 'can not set a layout different to input_bottom on readings' unless layout.like? :input_bottom
16
+ end
17
+
14
18
  def queriable?
15
19
  false
16
20
  end
@@ -2,12 +2,18 @@ class Invitation < ApplicationRecord
2
2
  include Mumuki::Domain::Syncable
3
3
 
4
4
  belongs_to :course
5
+
6
+ validate :ensure_not_expired, on: :create
5
7
  validates_uniqueness_of :code
6
8
 
7
9
  defaults do
8
10
  self.code ||= self.class.generate_code
9
11
  end
10
12
 
13
+ def ensure_not_expired
14
+ errors.add(:base, :invitation_expired) if expired?
15
+ end
16
+
11
17
  def import_from_resource_h!(json)
12
18
  update! json.merge(course: Course.locate!(json[:course]))
13
19
  end
@@ -25,7 +31,7 @@ class Invitation < ApplicationRecord
25
31
  end
26
32
 
27
33
  def to_resource_h
28
- { code: code, course: course_slug, expiration_date: expiration_date }
34
+ {code: code, course: course_slug, expiration_date: expiration_date}
29
35
  end
30
36
 
31
37
  def navigation_end?
@@ -6,6 +6,10 @@ class Message < ApplicationRecord
6
6
 
7
7
  validates_presence_of :content, :sender
8
8
  validates_presence_of :submission_id, :unless => :discussion_id?
9
+
10
+ after_save :update_counters_cache!
11
+ after_destroy :update_counters_cache!
12
+
9
13
  markdown_on :content
10
14
 
11
15
  def notify!
@@ -16,6 +20,10 @@ class Message < ApplicationRecord
16
20
  sender_user == discussion&.initiator
17
21
  end
18
22
 
23
+ def from_moderator?
24
+ sender_user.moderator_here?
25
+ end
26
+
19
27
  def from_user?(user)
20
28
  sender_user == user
21
29
  end
@@ -33,9 +41,9 @@ class Message < ApplicationRecord
33
41
  end
34
42
 
35
43
  def to_resource_h
36
- as_json(except: [:id, :type, :discussion_id, :approved],
44
+ as_json(except: [:id, :type, :discussion_id, :approved, :not_actually_a_question],
37
45
  include: {exercise: {only: [:bibliotheca_id]}})
38
- .merge(organization: Organization.current.name)
46
+ .merge(organization: Organization.current.name)
39
47
  end
40
48
 
41
49
  def read!
@@ -46,11 +54,27 @@ class Message < ApplicationRecord
46
54
  toggle! :approved
47
55
  end
48
56
 
57
+ def toggle_not_actually_a_question!
58
+ toggle! :not_actually_a_question
59
+ end
60
+
61
+ def validated?
62
+ approved? || from_moderator?
63
+ end
64
+
65
+ def update_counters_cache!
66
+ discussion&.update_counters!
67
+ end
68
+
69
+ def question?
70
+ from_initiator? && !not_actually_a_question?
71
+ end
72
+
49
73
  def self.parse_json(json)
50
74
  message = json.delete 'message'
51
75
  json
52
- .except('uid', 'exercise_id')
53
- .merge(message)
76
+ .except('uid', 'exercise_id')
77
+ .merge(message)
54
78
  end
55
79
 
56
80
  def self.read_all!
@@ -1,6 +1,7 @@
1
1
  class Organization < ApplicationRecord
2
2
  include Mumuki::Domain::Syncable
3
3
  include Mumuki::Domain::Helpers::Organization
4
+ include Mumuki::Domain::Area
4
5
 
5
6
  include Mumukit::Login::OrganizationHelpers
6
7
 
@@ -11,6 +12,7 @@ class Organization < ApplicationRecord
11
12
  markdown_on :description
12
13
 
13
14
  validate :ensure_consistent_public_login
15
+ validate :ensure_valid_activity_range
14
16
 
15
17
  belongs_to :book
16
18
  has_many :usages
@@ -103,17 +105,8 @@ class Organization < ApplicationRecord
103
105
  # ask for help in this organization
104
106
  #
105
107
  # Warning: this method does not strictly check user's permission
106
- def ask_for_help_enabled?(user = nil)
107
- report_issue_enabled? || community_link.present? || can_create_discussions?(user)
108
- end
109
-
110
- # Tells if the given user can
111
- # create discussion in this organization
112
- #
113
- # This is true only when this organization has a forum and the user
114
- # has the discusser pseudo-permission
115
- def can_create_discussions?(user = nil)
116
- forum_enabled? && (!user || user.discusser_of?(self))
108
+ def ask_for_help_enabled?(user)
109
+ report_issue_enabled? || community_link.present? || user.can_discuss_in?(self)
117
110
  end
118
111
 
119
112
  def import_from_resource_h!(resource_h)
@@ -126,12 +119,26 @@ class Organization < ApplicationRecord
126
119
  super.merge(book: book.slug)
127
120
  end
128
121
 
122
+ def to_organization
123
+ self
124
+ end
125
+
126
+ def enable_progressive_display!(lookahead: 1)
127
+ update! progressive_display_lookahead: lookahead
128
+ end
129
+
129
130
  private
130
131
 
131
132
  def ensure_consistent_public_login
132
133
  errors.add(:base, :consistent_public_login) if settings.customized_login_methods? && public?
133
134
  end
134
135
 
136
+ def ensure_valid_activity_range
137
+ if in_preparation_until.present? && disabled_from.present?
138
+ errors.add(:base, :invalid_activity_range) if in_preparation_until.to_datetime >= disabled_from.to_datetime
139
+ end
140
+ end
141
+
135
142
  def notify_assignments!(assignments)
136
143
  assignments.each { |assignment| assignment.notify! }
137
144
  end
@@ -154,6 +154,41 @@ class User < ApplicationRecord
154
154
  update! self.class.buried_profile.merge(accepts_reminders: false, gender: nil, birthdate: nil)
155
155
  end
156
156
 
157
+ # Takes a didactic - ordered - sequence of content containers
158
+ # and returns those that have been completed
159
+ def completed_containers(sequence, organization)
160
+ sequence.take_while { |it| it.content.completed_for?(self, organization) }
161
+ end
162
+
163
+ # Like `completed_containers`, returns a slice of the completed containers
164
+ # in the sequence, but adding a configurable number of trailing, non-completed contaienrs
165
+ def completed_containers_with_lookahead(sequence, organization, lookahead: 1)
166
+ raise 'invalid lookahead' if lookahead < 1
167
+
168
+ count = completed_containers(sequence, organization).size
169
+ sequence[0..count + lookahead - 1]
170
+ end
171
+
172
+ # Tells if the given user can discuss in an organization
173
+ #
174
+ # This is true only when this organization has the forum enabled and the user
175
+ # has the discusser pseudo-permission and the discusser is trusted
176
+ def can_discuss_in?(organization)
177
+ organization.forum_enabled? && discusser_of?(organization) && trusted_as_discusser_in?(organization)
178
+ end
179
+
180
+ def trusted_as_discusser_in?(organization)
181
+ trusted_for_forum? || !organization.forum_only_for_trusted?
182
+ end
183
+
184
+ def can_discuss_here?
185
+ can_discuss_in? Organization.current
186
+ end
187
+
188
+ def name_initials
189
+ name.split.map(&:first).map(&:capitalize).join(' ')
190
+ end
191
+
157
192
  private
158
193
 
159
194
  def set_uid!
@@ -161,10 +196,7 @@ class User < ApplicationRecord
161
196
  end
162
197
 
163
198
  def init
164
- # Temporarily keep using image_url until avatars are created
165
- # self.avatar = Avatar.sample unless profile_picture.present?
166
-
167
- self.image_url ||= "user_shape.png"
199
+ self.avatar = Avatar.sample unless profile_picture.present?
168
200
  end
169
201
 
170
202
  def self.sync_key_id_field
@@ -0,0 +1,5 @@
1
+ class AddCourseToExam < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_reference :exams, :course
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddPassingCriterionsToExam < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :exams, :passing_criterion_type, :integer, default: 0
4
+ add_column :exams, :passing_criterion_value, :integer
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddProgressiveDisplayLookaheadToOrganizations < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :organizations, :progressive_display_lookahead, :integer
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddMessagesCountToDiscussion < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :discussions, :messages_count, :integer, default: 0
4
+ add_column :discussions, :validated_messages_count, :integer, default: 0
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddNotActuallyAQuestionFieldToMessages < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :messages, :not_actually_a_question, :boolean, default: false
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddRequiresModeratorResponseToDiscussions < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :discussions, :requires_moderator_response, :boolean, default: true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddTrustedForForumToUser < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :users, :trusted_for_forum, :boolean
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddLastModeratorAccessFieldsToDiscussion < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :discussions, :last_moderator_access_by_id, :string
4
+ add_column :discussions, :last_moderator_access_at, :datetime
5
+ end
6
+ end
@@ -25,6 +25,7 @@ Mumukit::Platform.configure do |config|
25
25
  config.organization_class_name = 'Organization'
26
26
  end
27
27
 
28
+ require_relative './domain/area'
28
29
  require_relative './domain/evaluation'
29
30
  require_relative './domain/submission'
30
31
  require_relative './domain/status'
@@ -32,6 +33,7 @@ require_relative './domain/exceptions'
32
33
  require_relative './domain/file'
33
34
  require_relative './domain/extensions'
34
35
  require_relative './domain/organization'
36
+ require_relative './domain/workspace'
35
37
  require_relative './domain/helpers'
36
38
  require_relative './domain/syncable'
37
39
  require_relative './domain/store'
@@ -0,0 +1,13 @@
1
+ module Mumuki::Domain::Area
2
+ extend ActiveSupport::Concern
3
+
4
+ def to_mumukit_grant
5
+ slug.to_mumukit_grant
6
+ end
7
+
8
+ def to_mumukit_slug
9
+ slug
10
+ end
11
+
12
+ required :to_organization
13
+ end
@@ -4,3 +4,5 @@ require_relative './exceptions/not_found_error'
4
4
  require_relative './exceptions/unauthorized_error'
5
5
  require_relative './exceptions/disabled_error'
6
6
  require_relative './exceptions/blocked_forum_error'
7
+ require_relative './exceptions/unprepared_organization_error'
8
+ require_relative './exceptions/disabled_organization_error'
@@ -0,0 +1,2 @@
1
+ class Mumuki::Domain::DisabledOrganizationError < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class Mumuki::Domain::UnpreparedOrganizationError < StandardError
2
+ end
@@ -2,8 +2,10 @@ FactoryBot.define do
2
2
  factory :course do
3
3
  period { '2016' }
4
4
  shifts { %w(morning) }
5
+ code { "k1234-#{SecureRandom.uuid}" }
5
6
  days { %w(monday wednesday) }
6
7
  description { 'test' }
7
8
  organization_id { 1 }
9
+ slug { "#{Organization.current.name}/#{period}-#{code}" }
8
10
  end
9
11
  end
@@ -1,7 +1,7 @@
1
1
  FactoryBot.define do
2
2
  factory :discussion do
3
- title { 'A discussion' }
4
- description { 'A discussion description' }
3
+ title { Faker::Lorem.sentence(word_count: 2) }
4
+ description { Faker::Lorem.sentence(word_count: 5) }
5
5
  initiator { create(:user) }
6
6
  item { create(:exercise) }
7
7
  organization { Organization.current rescue nil }
@@ -3,6 +3,7 @@ FactoryBot.define do
3
3
  factory :exam, traits: [:guide_container] do
4
4
  duration { Faker::Number.between(from: 10, to:60).minutes }
5
5
  organization { Organization.current }
6
+ course { create(:course) }
6
7
  start_time { 5.minutes.ago }
7
8
  end_time { 10.minutes.since }
8
9
  end
@@ -1,11 +1,6 @@
1
1
  FactoryBot.define do
2
2
 
3
3
  factory :message do
4
- exercise_id { Faker::Internet.number(2) }
5
- assignment
6
- submission_id { assignment.id }
7
- sender { Faker::Internet.email }
8
- type { 'success' }
9
- content { Faker::Lorem.sentence(3) }
4
+ content { Faker::Lorem.sentence(word_count: 3) }
10
5
  end
11
6
  end
@@ -4,7 +4,7 @@ module Mumuki::Domain::Helpers::Organization
4
4
 
5
5
  included do
6
6
  delegate *Mumuki::Domain::Organization::Theme.accessors, to: :theme
7
- delegate *Mumuki::Domain::Organization::Settings.accessors, :private?, :login_settings, to: :settings
7
+ delegate *Mumuki::Domain::Organization::Settings.accessors, :private?, :login_settings, :in_preparation?, :disabled?, to: :settings
8
8
  delegate *Mumuki::Domain::Organization::Profile.accessors, :locale_json, to: :profile
9
9
  end
10
10
 
@@ -48,6 +48,11 @@ module Mumuki::Domain::Helpers::Organization
48
48
  Mumukit::Platform.application.organic_domain(name)
49
49
  end
50
50
 
51
+ def validate_active!
52
+ raise Mumuki::Domain::DisabledOrganizationError if disabled?
53
+ raise Mumuki::Domain::UnpreparedOrganizationError if in_preparation?
54
+ end
55
+
51
56
  ## API Exposure
52
57
 
53
58
  def to_param