mumuki-domain 7.3.2 → 7.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/assignment.rb +11 -3
  3. data/app/models/avatar.rb +5 -0
  4. data/app/models/book.rb +13 -0
  5. data/app/models/concerns/{with_content.rb → container.rb} +1 -1
  6. data/app/models/concerns/contextualization.rb +63 -8
  7. data/app/models/concerns/disabling.rb +37 -0
  8. data/app/models/concerns/guide_container.rb +8 -2
  9. data/app/models/concerns/submittable/solvable.rb +1 -1
  10. data/app/models/concerns/topic_container.rb +1 -1
  11. data/app/models/concerns/with_editor.rb +1 -1
  12. data/app/models/concerns/with_progress.rb +4 -0
  13. data/app/models/course.rb +5 -0
  14. data/app/models/exam.rb +4 -0
  15. data/app/models/exercise.rb +15 -4
  16. data/app/models/guide.rb +1 -1
  17. data/app/models/organization.rb +16 -0
  18. data/app/models/stats.rb +2 -2
  19. data/app/models/user.rb +46 -2
  20. data/db/migrate/20200312181842_add_results_hidden_for_choices_to_exam.rb +5 -0
  21. data/db/migrate/20200508191543_create_avatars.rb +8 -0
  22. data/db/migrate/20200518135658_add_avatar_to_users.rb +5 -0
  23. data/db/migrate/20200527180729_add_disabled_at_to_users.rb +6 -0
  24. data/db/migrate/20200608132959_add_progressive_display_lookahead_to_organizations.rb +5 -0
  25. data/lib/mumuki/domain.rb +2 -0
  26. data/lib/mumuki/domain/area.rb +13 -0
  27. data/lib/mumuki/domain/exceptions.rb +3 -0
  28. data/lib/mumuki/domain/exceptions/disabled_error.rb +2 -0
  29. data/lib/mumuki/domain/exceptions/disabled_organization_error.rb +2 -0
  30. data/lib/mumuki/domain/exceptions/unprepared_organization_error.rb +2 -0
  31. data/lib/mumuki/domain/extensions/hash.rb +11 -2
  32. data/lib/mumuki/domain/extensions/string.rb +44 -0
  33. data/lib/mumuki/domain/factories/book_factory.rb +2 -2
  34. data/lib/mumuki/domain/factories/chapter_factory.rb +2 -2
  35. data/lib/mumuki/domain/factories/exam_factory.rb +1 -1
  36. data/lib/mumuki/domain/factories/exercise_factory.rb +7 -0
  37. data/lib/mumuki/domain/factories/guide_factory.rb +2 -2
  38. data/lib/mumuki/domain/factories/invitation_factory.rb +1 -1
  39. data/lib/mumuki/domain/factories/topic_factory.rb +2 -2
  40. data/lib/mumuki/domain/factories/user_factory.rb +1 -0
  41. data/lib/mumuki/domain/helpers/organization.rb +6 -1
  42. data/lib/mumuki/domain/helpers/user.rb +7 -7
  43. data/lib/mumuki/domain/locales/activerecord/en.yml +1 -0
  44. data/lib/mumuki/domain/locales/activerecord/es.yml +1 -0
  45. data/lib/mumuki/domain/locales/activerecord/pt.yml +1 -0
  46. data/lib/mumuki/domain/organization/settings.rb +11 -1
  47. data/lib/mumuki/domain/status/discussion/discussion.rb +4 -0
  48. data/lib/mumuki/domain/status/submission/skipped.rb +0 -4
  49. data/lib/mumuki/domain/status/submission/submission.rb +5 -3
  50. data/lib/mumuki/domain/version.rb +1 -1
  51. data/lib/mumuki/domain/workspace.rb +38 -0
  52. metadata +17 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39020a28808b40cdfdfcbbacb3f5548a56ba6886ae4ffec37e46501a9e211ae9
4
- data.tar.gz: 80eebf536803172e019da3af5e4e480e4613716db2dff9cb2bd51a09dee20340
3
+ metadata.gz: 40391bc2dcc0ab54965e1d82a289c8960dac3f68cf4c8fb2a3d097e774b899b4
4
+ data.tar.gz: 205ce356bcc3365491551226eaa2b076ae58d7ac1255fb51294424b419ea4ffb
5
5
  SHA512:
6
- metadata.gz: 2b92101a2f8af04af9f29722c8581f645533da33146deb68b16af0013babfa86786f6210929ea4d42857cfdb1be871fd8671b76b210321a7c78110a02fbee750
7
- data.tar.gz: dcd486ae7a1cbe1500d2ad16b06856ee3ef5ee114ec5d54280754c1ad49c627eba0290f1af0b59d7a11a21e1df9313cdf5f948bef1849b180074c363399519a0
6
+ metadata.gz: 3741ef7ad7daf3f6cfc2d829d8fbf336512e4ec719257734d2e3bcc0b960081a6c50bdc3f31e2c82dc59abbc1d2d1719ab68ace80ec8c7ff1a908613b1123409
7
+ data.tar.gz: cf4e192e33fcdd94f68398f074715b22088169d50b88acc9d1aa0457bb15850a8288f2280ecbcf5aec874dd8c85f01c952a430762113fe5a383c19816bcbb5c2
@@ -18,7 +18,7 @@ class Assignment < Progress
18
18
  validates_presence_of :exercise, :submitter
19
19
 
20
20
  delegate :language, :name, :navigable_parent, :settings,
21
- :limited?, :input_kids?, :choice?, to: :exercise
21
+ :limited?, :input_kids?, :choice?, :results_hidden?, to: :exercise
22
22
 
23
23
  alias_attribute :status, :submission_status
24
24
  alias_attribute :attempts_count, :attemps_count
@@ -59,6 +59,14 @@ class Assignment < Progress
59
59
  update! status: teacher_evaluation[:status], manual_evaluation_comment: teacher_evaluation[:manual_evaluation]
60
60
  end
61
61
 
62
+ def visible_status
63
+ if results_hidden? && !pending?
64
+ :manual_evaluation_pending.to_submission_status
65
+ else
66
+ super
67
+ end
68
+ end
69
+
62
70
  def persist_submission!(submission)
63
71
  transaction do
64
72
  messages.destroy_all if submission_id.present?
@@ -160,7 +168,7 @@ class Assignment < Progress
160
168
  language: {only: [:name]}},
161
169
  },
162
170
  exercise: {only: [:name, :number]},
163
- submitter: {only: [:email, :image_url, :social_id, :uid], methods: [:name]}}).
171
+ submitter: {only: [:email, :social_id, :uid], methods: [:name, :profile_picture]}}).
164
172
  deep_merge(
165
173
  'organization' => Organization.current.name,
166
174
  'sid' => submission_id,
@@ -190,7 +198,7 @@ class Assignment < Progress
190
198
  navigable_parent.attempts_left_for(self)
191
199
  end
192
200
 
193
- # Tells wether the submitter of this
201
+ # Tells whether the submitter of this
194
202
  # assignment can keep on sending submissions
195
203
  # which is true for non limited or for assignments
196
204
  # that have not reached their submissions limit
@@ -0,0 +1,5 @@
1
+ class Avatar < ApplicationRecord
2
+ def self.sample
3
+ Avatar.order('RANDOM()').first
4
+ end
5
+ end
@@ -62,4 +62,17 @@ class Book < Content
62
62
  def structural_parent
63
63
  nil
64
64
  end
65
+
66
+ ## progressive display
67
+
68
+ def enabled_chapters_in(workspace)
69
+ workspace.enabled_containers(chapters)
70
+ end
71
+
72
+ # experimental API - it may change in the future.
73
+ # This method assumes no gaps in the sequences are introduced
74
+ # by enabled_chapters_in
75
+ def chapter_visibilities_in(workspace)
76
+ chapters.zip(enabled_chapters_in(workspace)).map { |chapter, enabled| [chapter, !enabled.nil?] }
77
+ end
65
78
  end
@@ -1,4 +1,4 @@
1
- module WithContent
1
+ module Container
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
@@ -25,7 +25,7 @@ module Contextualization
25
25
 
26
26
  delegate :visible_success_output?, to: :exercise
27
27
  delegate :output_content_type, to: :language
28
- delegate :should_retry?, :to_submission_status, :completed?, *Mumuki::Domain::Status::Submission.test_selectors, to: :submission_status
28
+ delegate :should_retry?, :to_submission_status, :completed?, :solved?, *Mumuki::Domain::Status::Submission.test_selectors, to: :submission_status
29
29
  delegate :inspection_keywords, to: :exercise
30
30
  end
31
31
 
@@ -35,20 +35,46 @@ module Contextualization
35
35
  end
36
36
  end
37
37
 
38
+ # deprecated: this method does hidden assumptions about the UI not wanting
39
+ # non-empty titles to not be displayed. Also it incorrectly uses the term `visual` instead of `visible`
38
40
  def single_visual_result?
39
- test_results.size == 1 && test_results.first[:title].blank? && visible_success_output?
41
+ warn 'use single_visible_test_result? instead'
42
+ single_visible_test_result? && first_test_result[:title].blank?
40
43
  end
41
44
 
45
+ # deprecated: this method does not validate nor depends on any `visible` condition
46
+ # Also, it incorrectly uses the term `visual` instead of `visible`
42
47
  def single_visual_result_html
43
- output_content_type.to_html test_results.first[:result]
48
+ warn 'use first_test_result_html intead'
49
+ first_test_result_html
44
50
  end
45
51
 
46
- def results_visible?
47
- (visible_success_output? || !passed?) && !exercise.choices? && !manual_evaluation_pending?
52
+ def single_visible_test_result?
53
+ test_results.size == 1 && visible_success_output?
48
54
  end
49
55
 
50
- def result_preview
51
- result.truncate(100) unless passed?
56
+ def first_test_result
57
+ test_results.first
58
+ end
59
+
60
+ def first_test_result_html
61
+ test_result_html first_test_result
62
+ end
63
+
64
+ def test_result_html(test_result)
65
+ output_content_type.to_html test_result[:result]
66
+ end
67
+
68
+ def results_body_hidden?
69
+ (passed? && !visible_success_output?) || exercise.choice? || manual_evaluation_pending? || skipped?
70
+ end
71
+
72
+ def visible_status
73
+ status
74
+ end
75
+
76
+ def iconize
77
+ visible_status.iconize
52
78
  end
53
79
 
54
80
  def result_html
@@ -60,7 +86,7 @@ module Contextualization
60
86
  end
61
87
 
62
88
  def failed_expectation_results
63
- expectation_results.to_a.select { |it| it[:result].failed? }
89
+ expectation_results.to_a.select { |it| it[:result].failed? }.uniq
64
90
  end
65
91
 
66
92
  def expectation_results_visible?
@@ -72,6 +98,7 @@ module Contextualization
72
98
  end
73
99
 
74
100
  def humanized_expectation_results
101
+ warn "Don't use humanized_expectation_results. Use affable_expectation_results, which also handles markdown and sanitization"
75
102
  visible_expectation_results.map do |it|
76
103
  {
77
104
  result: it[:result],
@@ -79,4 +106,32 @@ module Contextualization
79
106
  }
80
107
  end
81
108
  end
109
+
110
+ ####################
111
+ ## Affable results
112
+ ####################
113
+
114
+ def affable_expectation_results
115
+ visible_expectation_results.map do |it|
116
+ {
117
+ result: it[:result],
118
+ explanation: Mulang::Expectation.parse(it).translate(inspection_keywords).affable
119
+ }
120
+ end
121
+ end
122
+
123
+ def affable_tips
124
+ tips.map(&:affable)
125
+ end
126
+
127
+ def affable_test_results
128
+ test_results.to_a.map do |it|
129
+ { summary: it.dig(:summary, :message).affable }
130
+ .compact
131
+ .merge(
132
+ title: it[:title].affable,
133
+ result: it[:result].sanitized,
134
+ status: it[:status])
135
+ end
136
+ end
82
137
  end
@@ -0,0 +1,37 @@
1
+ # The disposable module is a soft-delete helper that:
2
+ #
3
+ # * adds `disable!` method that set a `disabled_at` attribute and then _buries_ the object
4
+ # * adds a `bury!` hook method that allows further modification when disabling
5
+ # * aliases `destroy!` and `destroy` to `disable!`, but still keeps `delete` and friends
6
+ #
7
+ module Disabling
8
+ extend ActiveSupport::Concern
9
+
10
+ def disable!
11
+ transaction do
12
+ update_attribute :disabled_at, Time.current
13
+ bury!
14
+ end
15
+ end
16
+
17
+ def disabled?
18
+ disabled_at.present?
19
+ end
20
+
21
+ def enabled?
22
+ !disabled?
23
+ end
24
+
25
+ # override to perform additional
26
+ # post-disable actions
27
+ def bury!
28
+ end
29
+
30
+ def ensure_enabled!
31
+ raise Mumuki::Domain::DisabledError if disabled?
32
+ end
33
+
34
+ alias_method :destroy!, :disable!
35
+ alias_method :destroy, :disable!
36
+ end
37
+
@@ -1,6 +1,6 @@
1
1
  module GuideContainer
2
2
  extend ActiveSupport::Concern
3
- include WithContent
3
+ include Container
4
4
 
5
5
  included do
6
6
  associated_content :guide
@@ -38,7 +38,13 @@ module GuideContainer
38
38
  # Tells if this guide container
39
39
  # imposes any kind of limit to the number of submission
40
40
  # to its assignments, which may depend on the exercise's type
41
- def limited_for?(exercise)
41
+ def limited_for?(_exercise)
42
+ false
43
+ end
44
+
45
+ # Tells if this guide container
46
+ # hides the results for students
47
+ def results_hidden_for?(_exercise)
42
48
  false
43
49
  end
44
50
 
@@ -1,7 +1,7 @@
1
1
  module Solvable
2
2
  def submit_solution!(user, attributes={})
3
3
  assignment, _ = find_assignment_and_submit! user, attributes[:content].to_mumuki_solution(language)
4
- try_solve_discussions(user) if assignment.passed?
4
+ try_solve_discussions(user) if assignment.solved?
5
5
  assignment
6
6
  end
7
7
 
@@ -1,6 +1,6 @@
1
1
  module TopicContainer
2
2
  extend ActiveSupport::Concern
3
- include WithContent
3
+ include Container
4
4
 
5
5
  included do
6
6
  associated_content :topic
@@ -19,7 +19,7 @@ module WithEditor
19
19
  struct id: "content_choice_#{index}",
20
20
  index: index,
21
21
  value: choice,
22
- text: Mumukit::ContentType::Markdown.to_html(choice_text(choice))
22
+ text: choice_text(choice).markdownified
23
23
  end
24
24
  end
25
25
 
@@ -19,6 +19,10 @@ module WithProgress
19
19
  self
20
20
  end
21
21
 
22
+ def completed_for?(user, organization)
23
+ progress_for(user, organization).completed?
24
+ end
25
+
22
26
  private
23
27
 
24
28
  def structural_children_changed?(old_structural_children)
@@ -1,6 +1,7 @@
1
1
  class Course < ApplicationRecord
2
2
  include Mumuki::Domain::Syncable
3
3
  include Mumuki::Domain::Helpers::Course
4
+ include Mumuki::Domain::Area
4
5
 
5
6
  validates_presence_of :slug, :shifts, :code, :days, :period, :description, :organization_id
6
7
  validates_uniqueness_of :slug
@@ -48,4 +49,8 @@ class Course < ApplicationRecord
48
49
  def self.sync_key_id_field
49
50
  :slug
50
51
  end
52
+
53
+ def to_organization
54
+ organization
55
+ end
51
56
  end
@@ -161,6 +161,10 @@ class Exam < ApplicationRecord
161
161
  max_attempts_for(exercise).present?
162
162
  end
163
163
 
164
+ def results_hidden_for?(exercise)
165
+ exercise.choice? && results_hidden_for_choices?
166
+ end
167
+
164
168
  def resettable?
165
169
  false
166
170
  end
@@ -55,6 +55,10 @@ class Exercise < ApplicationRecord
55
55
  guide.done_for?(user)
56
56
  end
57
57
 
58
+ def choice?
59
+ false
60
+ end
61
+
58
62
  def previous
59
63
  sibling_at number.pred
60
64
  end
@@ -128,7 +132,7 @@ class Exercise < ApplicationRecord
128
132
  .merge(settings: self[:settings])
129
133
  .merge(RANDOMIZED_FIELDS.map { |it| [it, self[it]] }.to_h)
130
134
  .symbolize_keys
131
- .tap { |it| it.markdownify!(:hint, :corollary, :description, :teacher_info) if options[:markdownified] }
135
+ .tap { |it| it.markdownified!(:hint, :corollary, :description, :teacher_info) if options[:markdownified] }
132
136
  end
133
137
 
134
138
  def reset!
@@ -162,7 +166,7 @@ class Exercise < ApplicationRecord
162
166
  end
163
167
 
164
168
  def description_context
165
- Mumukit::ContentType::Markdown.to_html splitted_description.first
169
+ splitted_description.first.markdownified
166
170
  end
167
171
 
168
172
  def splitted_description
@@ -170,7 +174,7 @@ class Exercise < ApplicationRecord
170
174
  end
171
175
 
172
176
  def description_task
173
- Mumukit::ContentType::Markdown.to_html splitted_description.drop(1).join("\n")
177
+ splitted_description.drop(1).join("\n").markdownified
174
178
  end
175
179
 
176
180
  def custom?
@@ -192,8 +196,15 @@ class Exercise < ApplicationRecord
192
196
  end
193
197
  end
194
198
 
199
+ # An exercise with hidden results cannot be limited
200
+ # as those exercises can be submitted as many times as the
201
+ # student wants because no result output is given
195
202
  def limited?
196
- navigable_parent.limited_for?(self)
203
+ !results_hidden? && navigable_parent.limited_for?(self)
204
+ end
205
+
206
+ def results_hidden?
207
+ navigable_parent&.results_hidden_for?(self)
197
208
  end
198
209
 
199
210
  def files_for(current_content)
@@ -104,7 +104,7 @@ class Guide < Content
104
104
  .merge(super)
105
105
  .merge(exercises: exercises.map { |it| it.to_resource_h(options) })
106
106
  .merge(language: language.to_embedded_resource_h)
107
- .tap { |it| it.markdownify!(:corollary, :description, :teacher_info) if options[:markdownified] }
107
+ .tap { |it| it.markdownified!(:corollary, :description, :teacher_info) if options[:markdownified] }
108
108
  end
109
109
 
110
110
  def to_markdownified_resource_h
@@ -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
@@ -126,12 +128,26 @@ class Organization < ApplicationRecord
126
128
  super.merge(book: book.slug)
127
129
  end
128
130
 
131
+ def to_organization
132
+ self
133
+ end
134
+
135
+ def enable_progressive_display!(lookahead: 1)
136
+ update! progressive_display_lookahead: lookahead
137
+ end
138
+
129
139
  private
130
140
 
131
141
  def ensure_consistent_public_login
132
142
  errors.add(:base, :consistent_public_login) if settings.customized_login_methods? && public?
133
143
  end
134
144
 
145
+ def ensure_valid_activity_range
146
+ if in_preparation_until.present? && disabled_from.present?
147
+ errors.add(:base, :invalid_activity_range) if in_preparation_until.to_datetime >= disabled_from.to_datetime
148
+ end
149
+ end
150
+
135
151
  def notify_assignments!(assignments)
136
152
  assignments.each { |assignment| assignment.notify! }
137
153
  end
@@ -1,7 +1,7 @@
1
1
  class Stats
2
2
  include ActiveModel::Model
3
3
 
4
- attr_accessor :passed, :passed_with_warnings, :failed, :pending
4
+ attr_accessor :passed, :passed_with_warnings, :failed, :pending, :skipped
5
5
 
6
6
  def submitted
7
7
  passed + passed_with_warnings + failed
@@ -16,7 +16,7 @@ class Stats
16
16
  end
17
17
 
18
18
  def self.from_statuses(statuses)
19
- Stats.new(statuses.inject({passed: 0, passed_with_warnings: 0, failed: 0, pending: 0}) do |accum, status|
19
+ Stats.new(statuses.inject({passed: 0, passed_with_warnings: 0, failed: 0, pending: 0, skipped: 0}) do |accum, status|
20
20
  accum[status.group.to_sym] += 1
21
21
  accum
22
22
  end)
@@ -4,6 +4,7 @@ class User < ApplicationRecord
4
4
  WithUserNavigation,
5
5
  WithReminders,
6
6
  WithDiscussionCreation,
7
+ Disabling,
7
8
  Mumuki::Domain::Helpers::User
8
9
 
9
10
  serialize :permissions, Mumukit::Auth::Permissions
@@ -31,12 +32,14 @@ class User < ApplicationRecord
31
32
 
32
33
  after_initialize :init
33
34
 
34
- enum gender: %i(female male other)
35
+ enum gender: %i(female male other unspecified)
36
+
37
+ belongs_to :avatar, optional: true
35
38
 
36
39
  before_validation :set_uid!
37
40
  validates :uid, presence: true
38
41
 
39
- resource_fields :uid, :social_id, :image_url, :email, :permissions, :verified_first_name, :verified_last_name, *profile_fields
42
+ resource_fields :uid, :social_id, :email, :permissions, :verified_first_name, :verified_last_name, *profile_fields
40
43
 
41
44
  def last_lesson
42
45
  last_guide.try(:lesson)
@@ -105,6 +108,10 @@ class User < ApplicationRecord
105
108
  update! self.class.slice_resource_h json
106
109
  end
107
110
 
111
+ def to_resource_h
112
+ super.merge(image_url: profile_picture)
113
+ end
114
+
108
115
  def verify_name!
109
116
  self.verified_first_name ||= first_name
110
117
  self.verified_last_name ||= last_name
@@ -138,6 +145,30 @@ class User < ApplicationRecord
138
145
  exams.any? { |e| e.in_progress_for? self }
139
146
  end
140
147
 
148
+ def profile_picture
149
+ avatar&.image_url || image_url
150
+ end
151
+
152
+ def bury!
153
+ # TODO change avatar
154
+ update! self.class.buried_profile.merge(accepts_reminders: false, gender: nil, birthdate: nil)
155
+ end
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
+
141
172
  private
142
173
 
143
174
  def set_uid!
@@ -145,6 +176,9 @@ class User < ApplicationRecord
145
176
  end
146
177
 
147
178
  def init
179
+ # Temporarily keep using image_url until avatars are created
180
+ # self.avatar = Avatar.sample unless profile_picture.present?
181
+
148
182
  self.image_url ||= "user_shape.png"
149
183
  end
150
184
 
@@ -160,4 +194,14 @@ class User < ApplicationRecord
160
194
  user[:uid] ||= user[:email]
161
195
  where(uid: user[:uid]).first_or_create(user)
162
196
  end
197
+
198
+ # Call this method once as part of application initialization
199
+ # in order to enable user profile override as part of disabling process
200
+ def self.configure_buried_profile!(profile)
201
+ @buried_profile = profile
202
+ end
203
+
204
+ def self.buried_profile
205
+ (@buried_profile || {}).slice(:first_name, :last_name, :email)
206
+ end
163
207
  end
@@ -0,0 +1,5 @@
1
+ class AddResultsHiddenForChoicesToExam < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :exams, :results_hidden_for_choices, :boolean, default: false
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ class CreateAvatars < ActiveRecord::Migration[5.1]
2
+ def change
3
+ create_table :avatars do |t|
4
+ t.string :image_url
5
+ t.string :description
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ class AddAvatarToUsers < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_reference :users, :avatar, index: false
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddDisabledAtToUsers < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :users, :disabled_at, :datetime
4
+ add_index :users, :disabled_at
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
@@ -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
@@ -2,4 +2,7 @@ require_relative './exceptions/forbidden_error'
2
2
  require_relative './exceptions/gone_error'
3
3
  require_relative './exceptions/not_found_error'
4
4
  require_relative './exceptions/unauthorized_error'
5
+ require_relative './exceptions/disabled_error'
5
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::DisabledError < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class Mumuki::Domain::DisabledOrganizationError < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class Mumuki::Domain::UnpreparedOrganizationError < StandardError
2
+ end
@@ -1,5 +1,14 @@
1
1
  class Hash
2
- def markdownify!(*keys)
3
- keys.each { |it| self[it] = Mumukit::ContentType::Markdown.to_html(self[it]) }
2
+ def markdownify!(*keys, **options)
3
+ warn "Don't use markdownify. Use markdownified! instead"
4
+ markdownified! *keys, **options
5
+ end
6
+
7
+ def markdownified!(*keys, **options)
8
+ keys.each { |it| self[it] = self[it].markdownified(**options) }
9
+ end
10
+
11
+ def markdownified(*keys, **options)
12
+ map { |k, v| key.in?(keys) ? v.markdownified(options) : v }.to_h
4
13
  end
5
14
  end
@@ -27,3 +27,47 @@ class String
27
27
  File.extname(self).delete '.'
28
28
  end
29
29
  end
30
+
31
+
32
+ # The nil-safe affable pipeline goes as follow:
33
+ #
34
+ # i18n > markdownified > sanitized > affable
35
+ #
36
+ # Where:
37
+ # * i18n: translates to current locale
38
+ # * markdownified: interpretes markdown in message and generates HTML
39
+ # * sanitized: sanitizes results HTML
40
+ # * affable: changes structure to hide low level details
41
+ #
42
+ # Other classes may polymorphically implement their own
43
+ # markdownified, sanitized and affable methods with similar semantics
44
+ # to extend this pipeline to non-strings
45
+ class String
46
+
47
+ # Creates a humman representation - but not necessary UI - representation
48
+ # of this string by interpreting its markdown as a one-liner and sanitizing it
49
+ def affable
50
+ markdownified(one_liner: true).sanitized
51
+ end
52
+
53
+ # Interprets the markdown on this string, and converts it into HTML
54
+ def markdownified(**options)
55
+ Mumukit::ContentType::Markdown.to_html self, options
56
+ end
57
+
58
+ # Sanitizes this string, escaping unsafe HTML sequences
59
+ def sanitized
60
+ Mumukit::ContentType::Sanitizer.sanitize self
61
+ end
62
+ end
63
+
64
+ class NilClass
65
+ def affable
66
+ end
67
+
68
+ def markdownified(**options)
69
+ end
70
+
71
+ def sanitized
72
+ end
73
+ end
@@ -1,7 +1,7 @@
1
1
  FactoryBot.define do
2
2
  factory :book do
3
- name { Faker::Lorem.sentence(3) }
4
- description { Faker::Lorem.sentence(30) }
3
+ name { Faker::Lorem.sentence(word_count: 3) }
4
+ description { Faker::Lorem.sentence(word_count: 30) }
5
5
  slug { "mumuki/mumuki-test-book-#{SecureRandom.uuid}" }
6
6
  end
7
7
  end
@@ -1,12 +1,12 @@
1
1
  FactoryBot.define do
2
2
 
3
3
  factory :chapter do
4
- number { Faker::Number.between(1, 40) }
4
+ number { Faker::Number.between(from: 1, to: 40) }
5
5
  book { Organization.current.book rescue nil }
6
6
 
7
7
  transient do
8
8
  lessons { [] }
9
- name { Faker::Lorem.sentence(3) }
9
+ name { Faker::Lorem.sentence(word_count: 3) }
10
10
  slug { "mumuki/mumuki-test-topic-#{SecureRandom.uuid}" }
11
11
  end
12
12
 
@@ -1,7 +1,7 @@
1
1
  FactoryBot.define do
2
2
 
3
3
  factory :exam, traits: [:guide_container] do
4
- duration { Faker::Number.between(10, 60).minutes }
4
+ duration { Faker::Number.between(from: 10, to:60).minutes }
5
5
  organization { Organization.current }
6
6
  start_time { 5.minutes.ago }
7
7
  end_time { 10.minutes.since }
@@ -51,6 +51,13 @@ FactoryBot.define do
51
51
  test { 'dont care' }
52
52
  end
53
53
 
54
+ factory :multiple_choice, parent: :problem do
55
+ name { 'A multiple choice problem' }
56
+ editor { :multiple_choice }
57
+ description { 'Simple multiple choice problem' }
58
+ choices { [{value: 'a', checked: true}, {value: 'b', checked: false }] }
59
+ end
60
+
54
61
  factory :interactive, class: Interactive, parent: :challenge do
55
62
  name { 'An interactive problem' }
56
63
  description { 'Simple interactive problem' }
@@ -11,8 +11,8 @@ FactoryBot.define do
11
11
  trait :guide_container do
12
12
  transient do
13
13
  exercises { [] }
14
- name { Faker::Lorem.sentence(3) }
15
- description { Faker::Lorem.sentence(10) }
14
+ name { Faker::Lorem.sentence(word_count: 3) }
15
+ description { Faker::Lorem.sentence(word_count: 10) }
16
16
  language { create(:language) }
17
17
  slug { "mumuki/mumuki-test-lesson-#{SecureRandom.uuid}" }
18
18
  end
@@ -1,6 +1,6 @@
1
1
  FactoryBot.define do
2
2
  factory :invitation do
3
- code { Faker::Lorem.sentence(6) }
3
+ code { Faker::Lorem.sentence(word_count: 6) }
4
4
  course { "foo/bar" }
5
5
  expiration_date { 5.minutes.since }
6
6
  end
@@ -1,8 +1,8 @@
1
1
  FactoryBot.define do
2
2
 
3
3
  factory :topic do
4
- name { Faker::Lorem::sentence(3) }
5
- description { Faker::Lorem.paragraph(2) }
4
+ name { Faker::Lorem::sentence(word_count: 3) }
5
+ description { Faker::Lorem.paragraph(sentence_count: 2) }
6
6
  slug { "mumuki/mumuki-sample-topic-#{SecureRandom.uuid}" }
7
7
  locale { :en }
8
8
  end
@@ -6,5 +6,6 @@ FactoryBot.define do
6
6
  last_name { Faker::Name.last_name }
7
7
  gender { 1 }
8
8
  birthdate { Date.today }
9
+ avatar { Avatar.new image_url: 'user_shape.png' }
9
10
  end
10
11
  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
@@ -28,9 +28,9 @@ module Mumuki::Domain::Helpers::User
28
28
  role_here = "#{role}_here?"
29
29
 
30
30
  # Tells whether this user has #{role} permissions in
31
- # the given organization
32
- define_method role_of do |organization|
33
- has_permission? role, organization.slug
31
+ # the given `slug_like`
32
+ define_method role_of do |slug_like|
33
+ has_permission? role, slug_like.to_mumukit_slug
34
34
  end
35
35
 
36
36
  # Tells whether this user has #{role} permissions in
@@ -49,16 +49,16 @@ module Mumuki::Domain::Helpers::User
49
49
  (Mumukit::Auth::Roles::ROLES - [:owner]).each do |role|
50
50
 
51
51
  # Assignes the #{role} role to this user
52
- # for the given slug
53
- define_method "make_#{role}_of!" do |slug|
54
- add_permission! role, slug
52
+ # for the given `grant_like`
53
+ define_method "make_#{role}_of!" do |grant_like|
54
+ add_permission! role, grant_like.to_mumukit_grant
55
55
  end
56
56
  end
57
57
 
58
58
  ## Profile
59
59
 
60
60
  def full_name
61
- "#{first_name} #{last_name}"
61
+ "#{first_name} #{last_name}".strip
62
62
  end
63
63
 
64
64
  alias_method :name, :full_name
@@ -27,3 +27,4 @@ en:
27
27
  attributes:
28
28
  base:
29
29
  consistent_public_login: 'A public organization can not restrict login methods'
30
+ invalid_activity_range: 'The organization activity range are not valid'
@@ -44,6 +44,7 @@ es:
44
44
  attributes:
45
45
  base:
46
46
  consistent_public_login: 'Una organización pública no puede restringir los métodos de login'
47
+ invalid_activity_range: 'La fecha de deshabilitación no puede ser anterior a la de inicio del recorrido'
47
48
  models:
48
49
  exercise:
49
50
  one: Ejercicio
@@ -27,3 +27,4 @@ pt:
27
27
  attributes:
28
28
  base:
29
29
  consistent_public_login: 'Uma organização pública não pode restringir métodos de login'
30
+ invalid_activity_range: 'O intervalo de atividades da organização não é válido'
@@ -11,7 +11,9 @@ class Mumuki::Domain::Organization::Settings < Mumukit::Platform::Model
11
11
  :embeddable?,
12
12
  :immersive?,
13
13
  :forum_enabled?,
14
- :report_issue_enabled?
14
+ :report_issue_enabled?,
15
+ :disabled_from,
16
+ :in_preparation_until
15
17
 
16
18
  def private?
17
19
  !public?
@@ -24,4 +26,12 @@ class Mumuki::Domain::Organization::Settings < Mumukit::Platform::Model
24
26
  def forum_discussions_minimal_role
25
27
  (@forum_discussions_minimal_role || 'student').to_sym
26
28
  end
29
+
30
+ def disabled?
31
+ disabled_from.present? && disabled_from.to_datetime < DateTime.now
32
+ end
33
+
34
+ def in_preparation?
35
+ in_preparation_until.present? && in_preparation_until.to_datetime > DateTime.now
36
+ end
27
37
  end
@@ -41,4 +41,8 @@ module Mumuki::Domain::Status::Discussion
41
41
  def allowed_statuses_for(user, discussion)
42
42
  STATUSES.select { |it| it.allowed_for?(user, discussion) }
43
43
  end
44
+
45
+ def as_json(_options={})
46
+ to_s
47
+ end
44
48
  end
@@ -5,10 +5,6 @@ module Mumuki::Domain::Status::Submission::Skipped
5
5
  true
6
6
  end
7
7
 
8
- def self.passed?
9
- true
10
- end
11
-
12
8
  def self.iconize
13
9
  {class: :success, type: 'check-circle'}
14
10
  end
@@ -23,8 +23,6 @@ module Mumuki::Domain::Status::Submission
23
23
  self
24
24
  end
25
25
 
26
- # Tells if a new, different submission should be tried.
27
- # True for `failed`, `errored` and `passed_with_warnings`
28
26
  def should_retry?
29
27
  false
30
28
  end
@@ -38,6 +36,10 @@ module Mumuki::Domain::Status::Submission
38
36
  end
39
37
 
40
38
  def completed?
41
- passed?
39
+ solved?
40
+ end
41
+
42
+ def solved?
43
+ passed? || skipped?
42
44
  end
43
45
  end
@@ -1,5 +1,5 @@
1
1
  module Mumuki
2
2
  module Domain
3
- VERSION = '7.3.2'
3
+ VERSION = '7.6.0'
4
4
  end
5
5
  end
@@ -0,0 +1,38 @@
1
+ class Mumuki::Domain::Workspace
2
+ attr_accessor :user, :area
3
+
4
+ # area is a organization-like or course-like object
5
+ # that can be converted into slugs, has content and access-rules information
6
+ def initialize(user, area)
7
+ @user = user
8
+ @area = area
9
+ end
10
+
11
+ def annonymous?
12
+ user.nil?
13
+ end
14
+
15
+ def teacher?
16
+ user.teacher_of? area
17
+ end
18
+
19
+ # Takes a didactic sequence of containers and retuns the enabled containers
20
+ # for this user in this workspace.
21
+ #
22
+ # This method does not check the user is actually member of the area,
23
+ # you should check that before sending this message
24
+ #
25
+ def enabled_containers(sequence)
26
+ return sequence if annonymous? || teacher?
27
+
28
+ # TODO refactor when introducing access rules
29
+ if area.progressive_display_lookahead
30
+ user.completed_containers_with_lookahead(
31
+ sequence,
32
+ area.to_organization,
33
+ lookahead: area.progressive_display_lookahead)
34
+ else
35
+ sequence
36
+ end
37
+ end
38
+ 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: 7.3.2
4
+ version: 7.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-04-01 00:00:00.000000000 Z
11
+ date: 2020-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '4.0'
61
+ version: '4.1'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '4.0'
68
+ version: '4.1'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: mumukit-content-type
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -247,11 +247,14 @@ files:
247
247
  - app/models/api_client.rb
248
248
  - app/models/application_record.rb
249
249
  - app/models/assignment.rb
250
+ - app/models/avatar.rb
250
251
  - app/models/book.rb
251
252
  - app/models/chapter.rb
252
253
  - app/models/complement.rb
253
254
  - app/models/concerns/assistable.rb
255
+ - app/models/concerns/container.rb
254
256
  - app/models/concerns/contextualization.rb
257
+ - app/models/concerns/disabling.rb
255
258
  - app/models/concerns/friendly_name.rb
256
259
  - app/models/concerns/guide_container.rb
257
260
  - app/models/concerns/navigation/parent_navigation.rb
@@ -266,7 +269,6 @@ files:
266
269
  - app/models/concerns/topic_container.rb
267
270
  - app/models/concerns/with_assignments.rb
268
271
  - app/models/concerns/with_case_insensitive_search.rb
269
- - app/models/concerns/with_content.rb
270
272
  - app/models/concerns/with_description.rb
271
273
  - app/models/concerns/with_discussion_creation.rb
272
274
  - app/models/concerns/with_discussion_creation/subscription.rb
@@ -598,17 +600,26 @@ files:
598
600
  - db/migrate/20191217184525_add_progress_fields_to_indicators.rb
599
601
  - db/migrate/20200127142401_add_private_to_topics_and_books.rb
600
602
  - db/migrate/20200213175736_add_verified_names_to_users.rb
603
+ - db/migrate/20200312181842_add_results_hidden_for_choices_to_exam.rb
604
+ - db/migrate/20200508191543_create_avatars.rb
605
+ - db/migrate/20200518135658_add_avatar_to_users.rb
606
+ - db/migrate/20200527180729_add_disabled_at_to_users.rb
607
+ - db/migrate/20200608132959_add_progressive_display_lookahead_to_organizations.rb
601
608
  - lib/mumuki/domain.rb
609
+ - lib/mumuki/domain/area.rb
602
610
  - lib/mumuki/domain/engine.rb
603
611
  - lib/mumuki/domain/evaluation.rb
604
612
  - lib/mumuki/domain/evaluation/automated.rb
605
613
  - lib/mumuki/domain/evaluation/manual.rb
606
614
  - lib/mumuki/domain/exceptions.rb
607
615
  - lib/mumuki/domain/exceptions/blocked_forum_error.rb
616
+ - lib/mumuki/domain/exceptions/disabled_error.rb
617
+ - lib/mumuki/domain/exceptions/disabled_organization_error.rb
608
618
  - lib/mumuki/domain/exceptions/forbidden_error.rb
609
619
  - lib/mumuki/domain/exceptions/gone_error.rb
610
620
  - lib/mumuki/domain/exceptions/not_found_error.rb
611
621
  - lib/mumuki/domain/exceptions/unauthorized_error.rb
622
+ - lib/mumuki/domain/exceptions/unprepared_organization_error.rb
612
623
  - lib/mumuki/domain/extensions.rb
613
624
  - lib/mumuki/domain/extensions/array.rb
614
625
  - lib/mumuki/domain/extensions/hash.rb
@@ -683,6 +694,7 @@ files:
683
694
  - lib/mumuki/domain/syncable.rb
684
695
  - lib/mumuki/domain/syncable/with_resource_fields.rb
685
696
  - lib/mumuki/domain/version.rb
697
+ - lib/mumuki/domain/workspace.rb
686
698
  homepage: https://mumuki.org
687
699
  licenses:
688
700
  - AGPL-3.0