mumuki-domain 7.4.1 → 7.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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