ditty 0.9.1 → 0.10.1
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 +4 -4
- data/.env.test +2 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +23 -4
- data/.travis.yml +2 -7
- data/Gemfile.ci +0 -15
- data/ditty.gemspec +19 -15
- data/lib/ditty.rb +2 -2
- data/lib/ditty/cli.rb +5 -0
- data/lib/ditty/components/ditty.rb +4 -7
- data/lib/ditty/controllers/application_controller.rb +6 -5
- data/lib/ditty/controllers/audit_logs_controller.rb +2 -0
- data/lib/ditty/controllers/auth_controller.rb +5 -2
- data/lib/ditty/controllers/component_controller.rb +3 -3
- data/lib/ditty/controllers/user_login_traits_controller.rb +28 -1
- data/lib/ditty/controllers/users_controller.rb +3 -2
- data/lib/ditty/db.rb +4 -3
- data/lib/ditty/emails/base.rb +32 -30
- data/lib/ditty/generators/crud_generator.rb +51 -41
- data/lib/ditty/generators/project_generator.rb +1 -0
- data/lib/ditty/helpers/pundit.rb +4 -4
- data/lib/ditty/helpers/response.rb +6 -11
- data/lib/ditty/helpers/views.rb +21 -3
- data/lib/ditty/listener.rb +1 -1
- data/lib/ditty/models/base.rb +5 -0
- data/lib/ditty/models/identity.rb +7 -7
- data/lib/ditty/models/user.rb +9 -1
- data/lib/ditty/policies/user_policy.rb +1 -1
- data/lib/ditty/services/authentication.rb +19 -9
- data/lib/ditty/services/email.rb +13 -13
- data/lib/ditty/services/logger.rb +26 -20
- data/lib/ditty/services/pagination_wrapper.rb +7 -5
- data/lib/ditty/services/settings.rb +7 -6
- data/lib/ditty/tasks/ditty.rake +2 -1
- data/lib/ditty/templates/application.rb +1 -1
- data/lib/ditty/templates/config.ru +2 -2
- data/lib/ditty/templates/controller.rb.erb +7 -1
- data/{public → lib/ditty/templates/public}/browserconfig.xml +0 -0
- data/{public → lib/ditty/templates/public}/css/styles.css +0 -0
- data/lib/ditty/templates/public/favicon.ico +0 -0
- data/{public → lib/ditty/templates/public}/images/apple-icon.png +0 -0
- data/{public → lib/ditty/templates/public}/images/favicon-16x16.png +0 -0
- data/{public → lib/ditty/templates/public}/images/favicon-32x32.png +0 -0
- data/{public → lib/ditty/templates/public}/images/launcher-icon-1x.png +0 -0
- data/{public → lib/ditty/templates/public}/images/launcher-icon-2x.png +0 -0
- data/{public → lib/ditty/templates/public}/images/launcher-icon-4x.png +0 -0
- data/{public → lib/ditty/templates/public}/images/mstile-150x150.png +0 -0
- data/{public → lib/ditty/templates/public}/images/safari-pinned-tab.svg +0 -0
- data/{public → lib/ditty/templates/public}/js/scripts.js +0 -0
- data/{public/manifest.json → lib/ditty/templates/public/manifest.json.erb} +2 -2
- data/lib/ditty/templates/settings.yml.erb +1 -0
- data/lib/ditty/templates/spec_helper.rb +1 -1
- data/lib/ditty/templates/views/display.haml.tt +1 -1
- data/lib/ditty/templates/views/edit.haml.tt +1 -1
- data/lib/ditty/templates/views/index.haml.tt +1 -1
- data/lib/ditty/templates/views/new.haml.tt +1 -1
- data/lib/ditty/version.rb +1 -1
- data/spec/ditty/api_spec.rb +1 -1
- data/spec/ditty/emails/base_spec.rb +3 -3
- data/spec/ditty/emails/forgot_password_spec.rb +3 -2
- data/spec/ditty/models/user_spec.rb +3 -3
- data/spec/ditty/services/logger_spec.rb +7 -6
- data/spec/ditty/services/settings_spec.rb +2 -2
- data/spec/factories.rb +4 -4
- data/spec/spec_helper.rb +5 -1
- data/views/403.haml +1 -1
- data/views/500.haml +11 -0
- data/views/audit_logs/index.haml +12 -11
- data/views/auth/forgot_password.haml +29 -24
- data/views/auth/ldap.haml +1 -1
- data/views/auth/login.haml +3 -2
- data/views/auth/register.haml +3 -2
- data/views/auth/reset_password.haml +36 -19
- data/views/blank.haml +1 -0
- data/views/embedded.haml +17 -11
- data/views/layout.haml +16 -8
- data/views/partials/actions.haml +15 -14
- data/views/partials/filter_control.haml +1 -1
- data/views/partials/footer.haml +10 -2
- data/views/partials/form_tag.haml +1 -1
- data/views/partials/navitems.haml +25 -27
- data/views/partials/pager.haml +44 -25
- data/views/partials/search.haml +14 -9
- data/views/partials/sidebar.haml +2 -2
- data/views/partials/sort_ui.haml +2 -0
- data/views/partials/timespan_selector.haml +64 -0
- data/views/partials/topbar.haml +0 -15
- data/views/partials/user_associations.haml +32 -0
- data/views/quick_start.haml +23 -0
- data/views/roles/display.haml +3 -3
- data/views/roles/edit.haml +1 -1
- data/views/roles/index.haml +2 -2
- data/views/roles/new.haml +1 -1
- data/views/user_login_traits/display.haml +1 -1
- data/views/user_login_traits/edit.haml +1 -1
- data/views/user_login_traits/index.haml +23 -25
- data/views/user_login_traits/new.haml +1 -1
- data/views/users/display.haml +5 -5
- data/views/users/edit.haml +1 -1
- data/views/users/index.haml +5 -5
- data/views/users/login_traits.haml +2 -2
- data/views/users/new.haml +1 -1
- data/views/users/profile.haml +4 -4
- data/views/users/user.haml +1 -1
- metadata +116 -54
|
@@ -30,40 +30,46 @@ module Ditty
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# TODO: REfac this so that you can log something like ES to a separate logger
|
|
34
|
+
|
|
33
35
|
def method_missing(method, *args, &block)
|
|
34
36
|
loggers.each { |logger| logger.send(method, *args, &block) }
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def respond_to_missing?(method, _include_private = false)
|
|
38
|
-
loggers.any? { |logger| logger.respond_to?(method) }
|
|
40
|
+
return true if loggers.any? { |logger| logger.respond_to?(method) }
|
|
41
|
+
|
|
42
|
+
super
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
private
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def tr(val)
|
|
48
|
-
{
|
|
49
|
-
'$stdout' => $stdout,
|
|
50
|
-
'$stderr' => $stderr
|
|
51
|
-
}[val] || val
|
|
52
|
-
end
|
|
47
|
+
def config
|
|
48
|
+
default.merge ::Ditty::Services::Settings.values(:logger) || {}
|
|
49
|
+
end
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
def tr(val)
|
|
52
|
+
{
|
|
53
|
+
'$stdout' => $stdout,
|
|
54
|
+
'$stderr' => $stderr
|
|
55
|
+
}[val] || val
|
|
56
|
+
end
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
instance.send(method, *args, &block)
|
|
58
|
+
def default
|
|
59
|
+
{ loggers: [{ name: 'default', class: 'Logger' }] }
|
|
61
60
|
end
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
class << self
|
|
63
|
+
def method_missing(method, *args, &block)
|
|
64
|
+
instance.send(method, *args, &block)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def respond_to_missing?(method, _include_private)
|
|
68
|
+
return true if instance.respond_to?(method)
|
|
69
|
+
|
|
70
|
+
super
|
|
71
|
+
end
|
|
65
72
|
end
|
|
66
|
-
end
|
|
67
73
|
end
|
|
68
74
|
end
|
|
69
75
|
end
|
|
@@ -10,18 +10,18 @@ module Ditty
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def last_page?
|
|
13
|
-
if list.respond_to? :
|
|
13
|
+
if list.respond_to? :last_page?
|
|
14
14
|
list.last_page?
|
|
15
15
|
else
|
|
16
|
-
list.
|
|
16
|
+
list.next_page.nil?
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def first_page?
|
|
21
|
-
if list.respond_to? :
|
|
21
|
+
if list.respond_to? :first_page?
|
|
22
22
|
list.first_page?
|
|
23
23
|
else
|
|
24
|
-
list.
|
|
24
|
+
list.previous_page.nil?
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -64,7 +64,9 @@ module Ditty
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def respond_to_missing?(method, _include_private = false)
|
|
67
|
-
list.respond_to?
|
|
67
|
+
return true if list.respond_to?(method)
|
|
68
|
+
|
|
69
|
+
super
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
def current_page_record_range
|
|
@@ -25,20 +25,21 @@ module Ditty
|
|
|
25
25
|
from = values
|
|
26
26
|
if keys.count > 1 && scope?(keys.first)
|
|
27
27
|
from = values(keys.first)
|
|
28
|
-
keys = keys[1
|
|
28
|
+
keys = keys[1..]
|
|
29
29
|
key = keys.join('.')
|
|
30
30
|
end
|
|
31
|
-
|
|
31
|
+
key = key.to_sym if key.respond_to?(:to_sym)
|
|
32
|
+
from[key] || from.dig(*keys)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def values(scope = :settings)
|
|
35
36
|
@values ||= begin
|
|
36
37
|
v = Hash.new do |h, k|
|
|
37
38
|
h[k] = if File.file?("#{CONFIG_FOLDER}/#{k}.yml")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
read("#{CONFIG_FOLDER}/#{k}.yml")
|
|
40
|
+
elsif k != :settings && h[:settings].key?(k)
|
|
41
|
+
h[:settings][k]
|
|
42
|
+
end
|
|
42
43
|
h[k]
|
|
43
44
|
end
|
|
44
45
|
v[:settings] = File.file?(CONFIG_FILE) ? read(CONFIG_FILE) : {}
|
data/lib/ditty/tasks/ditty.rake
CHANGED
|
@@ -38,6 +38,7 @@ namespace :ditty do
|
|
|
38
38
|
task :folders do
|
|
39
39
|
puts 'Prepare the Ditty folders'
|
|
40
40
|
Dir.mkdir 'pids' unless File.exist?('pids')
|
|
41
|
+
Dir.mkdir 'logs' unless File.exist?('logs')
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
desc 'Check that the public folder is present and populated'
|
|
@@ -60,7 +61,7 @@ namespace :ditty do
|
|
|
60
61
|
FileUtils.cp_r path, 'migrations' unless File.expand_path(path).eql? File.expand_path('migrations')
|
|
61
62
|
end
|
|
62
63
|
puts 'Migrations added:'
|
|
63
|
-
Dir.foreach('migrations').sort.each { |x| puts x if File.file?("migrations/#{x}") && x[-3
|
|
64
|
+
Dir.foreach('migrations').sort.each { |x| puts x if File.file?("migrations/#{x}") && x[-3..] == '.rb' }
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
67
|
|
|
@@ -19,8 +19,8 @@ use Rack::Session::Cookie,
|
|
|
19
19
|
require './application'
|
|
20
20
|
require 'ditty/services/authentication'
|
|
21
21
|
use OmniAuth::Builder do
|
|
22
|
-
::Ditty::Services::Authentication.
|
|
23
|
-
provider prov,
|
|
22
|
+
::Ditty::Services::Authentication.providers.each do |prov|
|
|
23
|
+
provider prov, *::Ditty::Services::Authentication.config[prov][:arguments]
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -14,7 +14,7 @@ module <%= namespace %>
|
|
|
14
14
|
# Add field filter definitions here
|
|
15
15
|
FILTERS = [
|
|
16
16
|
<%- many_to_ones.each do |key| -%>
|
|
17
|
-
{ name: :<%= key[:table].to_s.singularize %>, field: :<%= key[:
|
|
17
|
+
{ name: :<%= key[:table].to_s.singularize %>, field: :<%= key[:columns].first || 'id' %> },
|
|
18
18
|
<%- end -%>
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
@@ -45,6 +45,12 @@ module <%= namespace %>
|
|
|
45
45
|
<%- end -%>
|
|
46
46
|
<%- if many_to_ones.count.positive? -%>
|
|
47
47
|
|
|
48
|
+
before '/' do
|
|
49
|
+
<%- many_to_ones.each do |key| -%>
|
|
50
|
+
param(:<%= key[:table].to_s.singularize %>, Integer) unless params[:<%= key[:table].to_s.singularize %>].blank?
|
|
51
|
+
<%- end -%>
|
|
52
|
+
end
|
|
53
|
+
|
|
48
54
|
before '*', provides: 'html' do
|
|
49
55
|
<%- many_to_ones.each do |key| -%>
|
|
50
56
|
if <%= key[:table].to_s.classify %>.count.zero?
|
|
File without changes
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -13,7 +13,7 @@ require 'factory_bot'
|
|
|
13
13
|
require 'database_cleaner'
|
|
14
14
|
|
|
15
15
|
if ENV['DATABASE_URL'] == 'sqlite::memory:'
|
|
16
|
-
folder = File.expand_path(File.dirname(__FILE__)
|
|
16
|
+
folder = File.expand_path("#{File.dirname(__FILE__)}/../migrate")
|
|
17
17
|
Sequel.extension :migration
|
|
18
18
|
Sequel::Migrator.apply(DB, folder)
|
|
19
19
|
|
data/lib/ditty/version.rb
CHANGED
data/spec/ditty/api_spec.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
-
Dir.glob('./lib/ditty/controllers/*.rb').each { |f| require f }
|
|
4
|
+
Dir.glob('./lib/ditty/controllers/*.rb').sort.each { |f| require f }
|
|
5
5
|
require 'support/api_shared_examples'
|
|
6
6
|
|
|
7
7
|
describe ::Ditty::RolesController, type: :controller do
|
|
@@ -11,7 +11,7 @@ describe ::Ditty::Emails::Base do
|
|
|
11
11
|
mail
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
describe '.new' do
|
|
15
15
|
it 'defaults to base options' do
|
|
16
16
|
expect(subject.options).to include subject: '(No Subject)', from: 'no-reply@ditty.io', view: :base
|
|
17
17
|
end
|
|
@@ -24,7 +24,7 @@ describe ::Ditty::Emails::Base do
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
describe '.deliver!' do
|
|
28
28
|
it 'delivers the email to the specified email address' do
|
|
29
29
|
expect(mail).to receive(:to).with('test@email.com')
|
|
30
30
|
expect(mail).to receive(:deliver!)
|
|
@@ -44,7 +44,7 @@ describe ::Ditty::Emails::Base do
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
describe '#deliver!' do
|
|
48
48
|
it 'delivers the email to the specified email address' do
|
|
49
49
|
expect(mail).to receive(:to).with('test2@email.com')
|
|
50
50
|
base = described_class.new(mail: mail)
|
|
@@ -11,9 +11,10 @@ describe ::Ditty::Emails::ForgotPassword do
|
|
|
11
11
|
mail
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
describe '.new' do
|
|
15
15
|
it 'defaults to base options' do
|
|
16
|
-
expect(subject.options).to include subject: 'Request to reset password', from: 'no-reply@ditty.io',
|
|
16
|
+
expect(subject.options).to include subject: 'Request to reset password', from: 'no-reply@ditty.io',
|
|
17
|
+
view: :forgot_password
|
|
17
18
|
end
|
|
18
19
|
end
|
|
19
20
|
end
|
|
@@ -15,12 +15,12 @@ describe ::Ditty::User, type: :model do
|
|
|
15
15
|
describe '#role?(check)' do
|
|
16
16
|
context 'when a user has a role without a parent' do
|
|
17
17
|
it 'returns true only for specific role' do
|
|
18
|
-
expect(user.
|
|
18
|
+
expect(user).to be_role('user')
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
it 'returns false for other roles' do
|
|
22
22
|
%w[admin super_admin].each do |role|
|
|
23
|
-
expect(user.
|
|
23
|
+
expect(user).not_to be_role(role)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -28,7 +28,7 @@ describe ::Ditty::User, type: :model do
|
|
|
28
28
|
context 'when a user has a role with descendants' do
|
|
29
29
|
it 'returns true for all descendants' do
|
|
30
30
|
%w[user admin super_admin].each do |role|
|
|
31
|
-
expect(super_admin.
|
|
31
|
+
expect(super_admin).to be_role(role)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -7,6 +7,7 @@ require 'ditty/services/logger'
|
|
|
7
7
|
class TestLogger
|
|
8
8
|
WARN = 2
|
|
9
9
|
attr_accessor :level
|
|
10
|
+
|
|
10
11
|
def initialize(options = {})
|
|
11
12
|
@options = options
|
|
12
13
|
end
|
|
@@ -27,8 +28,8 @@ describe ::Ditty::Services::Logger, type: :service do
|
|
|
27
28
|
|
|
28
29
|
it 'reads config from file and creates an array of loggers' do
|
|
29
30
|
::Ditty::Services::Settings.values = nil
|
|
30
|
-
allow(File).to receive(:
|
|
31
|
-
allow(File).to receive(:
|
|
31
|
+
allow(File).to receive(:file?).and_return(false)
|
|
32
|
+
allow(File).to receive(:file?).with('./config/logger.yml').and_return(true)
|
|
32
33
|
allow(File).to receive(:read).and_return(config_file)
|
|
33
34
|
|
|
34
35
|
expect(subject.instance.loggers.size).to eq 4
|
|
@@ -38,8 +39,8 @@ describe ::Ditty::Services::Logger, type: :service do
|
|
|
38
39
|
|
|
39
40
|
it 'sets the correct logging level' do
|
|
40
41
|
::Ditty::Services::Settings.values = nil
|
|
41
|
-
allow(File).to receive(:
|
|
42
|
-
allow(File).to receive(:
|
|
42
|
+
allow(File).to receive(:file?).and_return(false)
|
|
43
|
+
allow(File).to receive(:file?).with('./config/logger.yml').and_return(true)
|
|
43
44
|
allow(File).to receive(:read).and_return(config_file)
|
|
44
45
|
expect(subject.instance.loggers[0].level).to eq Logger::DEBUG
|
|
45
46
|
expect(subject.instance.loggers[2].level).to eq Logger::INFO
|
|
@@ -50,8 +51,8 @@ describe ::Ditty::Services::Logger, type: :service do
|
|
|
50
51
|
context 'send messages' do
|
|
51
52
|
it 'receives message and passes it to the loggers' do
|
|
52
53
|
::Ditty::Services::Settings.values = nil
|
|
53
|
-
allow(File).to receive(:
|
|
54
|
-
allow(File).to receive(:
|
|
54
|
+
allow(File).to receive(:file?).and_return(false)
|
|
55
|
+
allow(File).to receive(:file?).with('./config/logger.yml').and_return(true)
|
|
55
56
|
allow(File).to receive(:read).and_return(config_file)
|
|
56
57
|
allow(Logger).to receive(:warn).with('Some message')
|
|
57
58
|
allow(TestLogger).to receive(:warn).with('Some message')
|
|
@@ -16,7 +16,7 @@ describe ::Ditty::Services::Settings do
|
|
|
16
16
|
allow(File).to receive(:read).with('./config/section.yml').and_return(section)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
describe '#[]' do
|
|
20
20
|
before do
|
|
21
21
|
setup_files
|
|
22
22
|
described_class.values = nil
|
|
@@ -35,7 +35,7 @@ describe ::Ditty::Services::Settings do
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
describe '#values' do
|
|
39
39
|
context 'uses the global file' do
|
|
40
40
|
before do
|
|
41
41
|
setup_files
|
data/spec/factories.rb
CHANGED
|
@@ -12,7 +12,7 @@ FactoryBot.define do
|
|
|
12
12
|
sequence(:email) { |n| "person-#{n}@example.com" }
|
|
13
13
|
sequence(:name) { |n| "Name-#{n}" }
|
|
14
14
|
|
|
15
|
-
factory :user, class: Ditty::User, aliases: [:'Ditty::User'] do
|
|
15
|
+
factory :user, class: 'Ditty::User', aliases: [:'Ditty::User'] do
|
|
16
16
|
email
|
|
17
17
|
|
|
18
18
|
after(:create) do |user, _evaluator|
|
|
@@ -26,17 +26,17 @@ FactoryBot.define do
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
factory :identity, class: Ditty::Identity, aliases: [:'Ditty::Identity'] do
|
|
29
|
+
factory :identity, class: 'Ditty::Identity', aliases: [:'Ditty::Identity'] do
|
|
30
30
|
username { generate :email }
|
|
31
31
|
crypted_password { 'som3Password!' }
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
factory :role, class: Ditty::Role, aliases: [:'Ditty::Role'] do
|
|
34
|
+
factory :role, class: 'Ditty::Role', aliases: [:'Ditty::Role'] do
|
|
35
35
|
name { "Role #{generate(:name)}" }
|
|
36
36
|
parent_id { nil }
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
factory :user_login_trait, class: Ditty::UserLoginTrait, aliases: [:'Ditty::UserLoginTrait'] do
|
|
39
|
+
factory :user_login_trait, class: 'Ditty::UserLoginTrait', aliases: [:'Ditty::UserLoginTrait'] do
|
|
40
40
|
association :user, strategy: :create, factory: :user
|
|
41
41
|
ip_address { Faker::Internet.ip_v4_address }
|
|
42
42
|
platform { Faker::Device.platform }
|