mumuki-domain 7.4.1 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68dc581849f150d3c0a958f3a1df2b25b4959b3ee71a595364cf2ac7533fbd25
4
- data.tar.gz: dc6a6e467c80e08d035ccc70c7ace81ffdabb41088564350220f76706738332a
3
+ metadata.gz: 4e4bc7628cef1eb1a3d8ed748fc2fd8c49830ffd9814058360aa324aa6bcde35
4
+ data.tar.gz: c32ffaac2a2cef4a4e7f71ded9c4f81f233db86f55a1a0d7a67bdda0792c9489
5
5
  SHA512:
6
- metadata.gz: d852df438a75e07817d1b01769559faf0a68b7c892a4423b7ee0a97a04153439388904b544a19562597015c2f613596a8236d87205aedaf86304d9141f7992ea
7
- data.tar.gz: aeb1db5771206c8de35da5e9d03395f5029ce7dcec4d5b643d65365f4d1ef01e5159ffc66038bc056300bc9f9f103549ff029bcbb7b22c08fc2b47270b7aecb8
6
+ metadata.gz: ce2e45ade3b54b53eec50718f7e42ef1f6cc51d9aa39706651c3cbd6e9d156b043c300d9d19e333e4ecf5955b5d2e915fe4aa0162830983e92d37892bca5f952
7
+ data.tar.gz: f3b7a44f946871521a943efbccd3d3d24594cfda1eb2493f6be706ab65738ca90334da2b3c1c5521a9936ad9a58c2856bb132c2e71dd0a440a72ed9914832dd1
@@ -168,7 +168,7 @@ class Assignment < Progress
168
168
  language: {only: [:name]}},
169
169
  },
170
170
  exercise: {only: [:name, :number]},
171
- submitter: {only: [:email, :image_url, :social_id, :uid], methods: [:name]}}).
171
+ submitter: {only: [:email, :social_id, :uid], methods: [:name, :profile_picture]}}).
172
172
  deep_merge(
173
173
  'organization' => Organization.current.name,
174
174
  'sid' => submission_id,
@@ -0,0 +1,5 @@
1
+ class Avatar < ApplicationRecord
2
+ def self.sample
3
+ Avatar.order('RANDOM()').first
4
+ end
5
+ end
@@ -35,12 +35,34 @@ 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
50
+ end
51
+
52
+ def single_visible_test_result?
53
+ test_results.size == 1 && visible_success_output?
54
+ end
55
+
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]
44
66
  end
45
67
 
46
68
  def results_body_hidden?
@@ -76,6 +98,7 @@ module Contextualization
76
98
  end
77
99
 
78
100
  def humanized_expectation_results
101
+ warn "Don't use humanized_expectation_results. Use affable_expectation_results, which also handles markdown and sanitization"
79
102
  visible_expectation_results.map do |it|
80
103
  {
81
104
  result: it[:result],
@@ -83,4 +106,32 @@ module Contextualization
83
106
  }
84
107
  end
85
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
86
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
+
@@ -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
 
@@ -132,7 +132,7 @@ class Exercise < ApplicationRecord
132
132
  .merge(settings: self[:settings])
133
133
  .merge(RANDOMIZED_FIELDS.map { |it| [it, self[it]] }.to_h)
134
134
  .symbolize_keys
135
- .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] }
136
136
  end
137
137
 
138
138
  def reset!
@@ -166,7 +166,7 @@ class Exercise < ApplicationRecord
166
166
  end
167
167
 
168
168
  def description_context
169
- Mumukit::ContentType::Markdown.to_html splitted_description.first
169
+ splitted_description.first.markdownified
170
170
  end
171
171
 
172
172
  def splitted_description
@@ -174,7 +174,7 @@ class Exercise < ApplicationRecord
174
174
  end
175
175
 
176
176
  def description_task
177
- Mumukit::ContentType::Markdown.to_html splitted_description.drop(1).join("\n")
177
+ splitted_description.drop(1).join("\n").markdownified
178
178
  end
179
179
 
180
180
  def custom?
@@ -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
@@ -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
@@ -33,10 +34,12 @@ class User < ApplicationRecord
33
34
 
34
35
  enum gender: %i(female male other)
35
36
 
37
+ belongs_to :avatar, optional: true
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,15 @@ 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
+
141
157
  private
142
158
 
143
159
  def set_uid!
@@ -145,6 +161,9 @@ class User < ApplicationRecord
145
161
  end
146
162
 
147
163
  def init
164
+ # Temporarily keep using image_url until avatars are created
165
+ # self.avatar = Avatar.sample unless profile_picture.present?
166
+
148
167
  self.image_url ||= "user_shape.png"
149
168
  end
150
169
 
@@ -160,4 +179,14 @@ class User < ApplicationRecord
160
179
  user[:uid] ||= user[:email]
161
180
  where(uid: user[:uid]).first_or_create(user)
162
181
  end
182
+
183
+ # Call this method once as part of application initialization
184
+ # in order to enable user profile override as part of disabling process
185
+ def self.configure_buried_profile!(profile)
186
+ @buried_profile = profile
187
+ end
188
+
189
+ def self.buried_profile
190
+ (@buried_profile || {}).slice(:first_name, :last_name, :email)
191
+ end
163
192
  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
@@ -2,4 +2,5 @@ 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'
@@ -0,0 +1,2 @@
1
+ class Mumuki::Domain::DisabledError < 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 }
@@ -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
@@ -58,7 +58,7 @@ module Mumuki::Domain::Helpers::User
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
@@ -1,5 +1,5 @@
1
1
  module Mumuki
2
2
  module Domain
3
- VERSION = '7.4.1'
3
+ VERSION = '7.5.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: 7.4.1
4
+ version: 7.5.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-23 00:00:00.000000000 Z
11
+ date: 2020-06-02 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,13 @@ 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
254
255
  - app/models/concerns/contextualization.rb
256
+ - app/models/concerns/disabling.rb
255
257
  - app/models/concerns/friendly_name.rb
256
258
  - app/models/concerns/guide_container.rb
257
259
  - app/models/concerns/navigation/parent_navigation.rb
@@ -599,6 +601,9 @@ files:
599
601
  - db/migrate/20200127142401_add_private_to_topics_and_books.rb
600
602
  - db/migrate/20200213175736_add_verified_names_to_users.rb
601
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
602
607
  - lib/mumuki/domain.rb
603
608
  - lib/mumuki/domain/engine.rb
604
609
  - lib/mumuki/domain/evaluation.rb
@@ -606,6 +611,7 @@ files:
606
611
  - lib/mumuki/domain/evaluation/manual.rb
607
612
  - lib/mumuki/domain/exceptions.rb
608
613
  - lib/mumuki/domain/exceptions/blocked_forum_error.rb
614
+ - lib/mumuki/domain/exceptions/disabled_error.rb
609
615
  - lib/mumuki/domain/exceptions/forbidden_error.rb
610
616
  - lib/mumuki/domain/exceptions/gone_error.rb
611
617
  - lib/mumuki/domain/exceptions/not_found_error.rb