fmrest 0.10.0 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +2 -0
- data/CHANGELOG.md +38 -0
- data/README.md +194 -763
- metadata +70 -97
- data/.gitignore +0 -26
- data/.rspec +0 -3
- data/.travis.yml +0 -5
- data/Gemfile +0 -3
- data/Rakefile +0 -6
- data/fmrest.gemspec +0 -38
- data/lib/fmrest.rb +0 -29
- data/lib/fmrest/errors.rb +0 -28
- data/lib/fmrest/spyke.rb +0 -21
- data/lib/fmrest/spyke/base.rb +0 -23
- data/lib/fmrest/spyke/container_field.rb +0 -59
- data/lib/fmrest/spyke/model.rb +0 -36
- data/lib/fmrest/spyke/model/associations.rb +0 -82
- data/lib/fmrest/spyke/model/attributes.rb +0 -171
- data/lib/fmrest/spyke/model/auth.rb +0 -35
- data/lib/fmrest/spyke/model/connection.rb +0 -74
- data/lib/fmrest/spyke/model/container_fields.rb +0 -25
- data/lib/fmrest/spyke/model/global_fields.rb +0 -40
- data/lib/fmrest/spyke/model/http.rb +0 -37
- data/lib/fmrest/spyke/model/orm.rb +0 -212
- data/lib/fmrest/spyke/model/serialization.rb +0 -91
- data/lib/fmrest/spyke/model/uri.rb +0 -30
- data/lib/fmrest/spyke/portal.rb +0 -55
- data/lib/fmrest/spyke/relation.rb +0 -359
- data/lib/fmrest/spyke/spyke_formatter.rb +0 -273
- data/lib/fmrest/spyke/validation_error.rb +0 -25
- data/lib/fmrest/string_date.rb +0 -220
- data/lib/fmrest/token_store.rb +0 -6
- data/lib/fmrest/token_store/active_record.rb +0 -74
- data/lib/fmrest/token_store/base.rb +0 -25
- data/lib/fmrest/token_store/memory.rb +0 -26
- data/lib/fmrest/token_store/moneta.rb +0 -41
- data/lib/fmrest/token_store/redis.rb +0 -45
- data/lib/fmrest/v1.rb +0 -21
- data/lib/fmrest/v1/connection.rb +0 -89
- data/lib/fmrest/v1/container_fields.rb +0 -114
- data/lib/fmrest/v1/dates.rb +0 -81
- data/lib/fmrest/v1/paths.rb +0 -47
- data/lib/fmrest/v1/raise_errors.rb +0 -57
- data/lib/fmrest/v1/token_session.rb +0 -142
- data/lib/fmrest/v1/token_store/active_record.rb +0 -13
- data/lib/fmrest/v1/token_store/memory.rb +0 -13
- data/lib/fmrest/v1/type_coercer.rb +0 -192
- data/lib/fmrest/v1/utils.rb +0 -95
- data/lib/fmrest/version.rb +0 -5
data/lib/fmrest/spyke.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
begin
|
4
|
-
require "spyke"
|
5
|
-
rescue LoadError => e
|
6
|
-
e.message << " (Did you include Spyke in your Gemfile?)" unless e.message.frozen?
|
7
|
-
raise e
|
8
|
-
end
|
9
|
-
|
10
|
-
require "fmrest"
|
11
|
-
require "fmrest/spyke/spyke_formatter"
|
12
|
-
require "fmrest/spyke/model"
|
13
|
-
require "fmrest/spyke/base"
|
14
|
-
|
15
|
-
module FmRest
|
16
|
-
module Spyke
|
17
|
-
def self.included(base)
|
18
|
-
base.include Model
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
data/lib/fmrest/spyke/base.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module FmRest
|
4
|
-
module Spyke
|
5
|
-
class Base < ::Spyke::Base
|
6
|
-
include FmRest::Spyke::Model
|
7
|
-
end
|
8
|
-
|
9
|
-
class << self
|
10
|
-
def Base(config = nil)
|
11
|
-
warn "[DEPRECATION] Inheriting from `FmRest::Spyke::Base(config)` is deprecated and will be removed, inherit from `FmRest::Spyke::Base` (without arguments) and use `fmrest_config=` instead"
|
12
|
-
|
13
|
-
if config
|
14
|
-
return Class.new(::FmRest::Spyke::Base) do
|
15
|
-
self.fmrest_config = config
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
::FmRest::Spyke::Base
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,59 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module FmRest
|
4
|
-
module Spyke
|
5
|
-
class ContainerField
|
6
|
-
|
7
|
-
# @return [String] the name of the container field
|
8
|
-
attr_reader :name
|
9
|
-
|
10
|
-
# @param base [FmRest::Spyke::Base] the record this container belongs to
|
11
|
-
# @param name [Symbol] the name of the container field
|
12
|
-
def initialize(base, name)
|
13
|
-
@base = base
|
14
|
-
@name = name
|
15
|
-
end
|
16
|
-
|
17
|
-
# @return [String] the URL for the container
|
18
|
-
def url
|
19
|
-
@base.attributes[name]
|
20
|
-
end
|
21
|
-
|
22
|
-
# @return (see FmRest::V1::ContainerFields#fetch_container_data)
|
23
|
-
def download
|
24
|
-
FmRest::V1.fetch_container_data(url, @base.class.connection)
|
25
|
-
end
|
26
|
-
|
27
|
-
# @param filename_or_io [String, IO] a path to the file to upload or an
|
28
|
-
# IO object
|
29
|
-
# @param options [Hash]
|
30
|
-
# @option options [Integer] :repetition (1) The repetition to pass to the
|
31
|
-
# upload URL
|
32
|
-
# @option (see FmRest::V1::ContainerFields#upload_container_data)
|
33
|
-
def upload(filename_or_io, options = {})
|
34
|
-
raise ArgumentError, "Record needs to be saved before uploading to a container field" unless @base.persisted?
|
35
|
-
|
36
|
-
response =
|
37
|
-
FmRest::V1.upload_container_data(
|
38
|
-
@base.class.connection,
|
39
|
-
upload_path(options[:repetition] || 1),
|
40
|
-
filename_or_io,
|
41
|
-
options
|
42
|
-
)
|
43
|
-
|
44
|
-
# Update mod id on record
|
45
|
-
@base.mod_id = response.body[:data][:mod_id]
|
46
|
-
|
47
|
-
true
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
# @param repetition [Integer]
|
53
|
-
# @return [String] the path for uploading a file to the container
|
54
|
-
def upload_path(repetition)
|
55
|
-
FmRest::V1.container_field_path(@base.class.layout, @base.id, name, repetition)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
data/lib/fmrest/spyke/model.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "fmrest/spyke/model/connection"
|
4
|
-
require "fmrest/spyke/model/uri"
|
5
|
-
require "fmrest/spyke/model/attributes"
|
6
|
-
require "fmrest/spyke/model/serialization"
|
7
|
-
require "fmrest/spyke/model/associations"
|
8
|
-
require "fmrest/spyke/model/orm"
|
9
|
-
require "fmrest/spyke/model/container_fields"
|
10
|
-
require "fmrest/spyke/model/global_fields"
|
11
|
-
require "fmrest/spyke/model/http"
|
12
|
-
require "fmrest/spyke/model/auth"
|
13
|
-
|
14
|
-
module FmRest
|
15
|
-
module Spyke
|
16
|
-
module Model
|
17
|
-
extend ::ActiveSupport::Concern
|
18
|
-
|
19
|
-
include Connection
|
20
|
-
include Uri
|
21
|
-
include Attributes
|
22
|
-
include Serialization
|
23
|
-
include Associations
|
24
|
-
include Orm
|
25
|
-
include ContainerFields
|
26
|
-
include GlobalFields
|
27
|
-
include Http
|
28
|
-
include Auth
|
29
|
-
|
30
|
-
included do
|
31
|
-
# @return [Integer] the record's modId
|
32
|
-
attr_accessor :mod_id
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
@@ -1,82 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "fmrest/spyke/portal"
|
4
|
-
|
5
|
-
module FmRest
|
6
|
-
module Spyke
|
7
|
-
module Model
|
8
|
-
module Associations
|
9
|
-
extend ::ActiveSupport::Concern
|
10
|
-
|
11
|
-
included do
|
12
|
-
# Keep track of portal options by their FM keys as we could need it
|
13
|
-
# to parse the portalData JSON in SpykeFormatter
|
14
|
-
class_attribute :portal_options, instance_accessor: false, instance_predicate: false
|
15
|
-
|
16
|
-
# class_attribute supports a :default option since ActiveSupport 5.2,
|
17
|
-
# but we want to support previous versions too so we set the default
|
18
|
-
# manually instead
|
19
|
-
self.portal_options = {}.freeze
|
20
|
-
|
21
|
-
class << self; private :portal_options=; end
|
22
|
-
end
|
23
|
-
|
24
|
-
class_methods do
|
25
|
-
# Based on +has_many+, but creates a special Portal association
|
26
|
-
# instead.
|
27
|
-
#
|
28
|
-
# Custom options:
|
29
|
-
#
|
30
|
-
# * <tt>:portal_key</tt> - The key used for the portal in the FM Data JSON portalData.
|
31
|
-
# * <tt>:attribute_prefix</tt> - The prefix used for portal attributes in the FM Data JSON.
|
32
|
-
#
|
33
|
-
# Example:
|
34
|
-
#
|
35
|
-
# has_portal :jobs, portal_key: "JobsTable", attribute_prefix: "Job"
|
36
|
-
#
|
37
|
-
def has_portal(name, options = {})
|
38
|
-
create_association(name, Portal, options)
|
39
|
-
|
40
|
-
# Store options for SpykeFormatter to use if needed
|
41
|
-
portal_key = options[:portal_key] || name
|
42
|
-
self.portal_options = portal_options.merge(portal_key.to_s => options.dup.merge(name: name.to_s)).freeze
|
43
|
-
|
44
|
-
define_method "#{name.to_s.singularize}_ids" do
|
45
|
-
association(name).map(&:id)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Override Spyke's association reader to keep a cache of loaded
|
51
|
-
# portals. Spyke's default behavior is to reload the association
|
52
|
-
# each time.
|
53
|
-
#
|
54
|
-
def association(name)
|
55
|
-
@loaded_portals ||= {}
|
56
|
-
|
57
|
-
if @loaded_portals.has_key?(name.to_sym)
|
58
|
-
return @loaded_portals[name.to_sym]
|
59
|
-
end
|
60
|
-
|
61
|
-
super.tap do |assoc|
|
62
|
-
next unless assoc.kind_of?(FmRest::Spyke::Portal)
|
63
|
-
@loaded_portals[name.to_sym] = assoc
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def reload(*_)
|
68
|
-
super.tap { @loaded_portals = nil }
|
69
|
-
end
|
70
|
-
|
71
|
-
def portals
|
72
|
-
self.class.associations.each_with_object([]) do |(key, _), portals|
|
73
|
-
candidate = association(key)
|
74
|
-
next unless candidate.kind_of?(FmRest::Spyke::Portal)
|
75
|
-
portals << candidate
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
@@ -1,171 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "fmrest/spyke/model/orm"
|
4
|
-
|
5
|
-
module FmRest
|
6
|
-
module Spyke
|
7
|
-
module Model
|
8
|
-
module Attributes
|
9
|
-
extend ::ActiveSupport::Concern
|
10
|
-
|
11
|
-
include Orm # Needed to extend custom save and reload
|
12
|
-
|
13
|
-
include ::ActiveModel::Dirty
|
14
|
-
include ::ActiveModel::ForbiddenAttributesProtection
|
15
|
-
|
16
|
-
included do
|
17
|
-
# Prevent the creation of plain (no prefix/suffix) attribute methods
|
18
|
-
# when calling ActiveModels' define_attribute_method, otherwise it
|
19
|
-
# will define an `attribute` method which overrides the one provided
|
20
|
-
# by Spyke
|
21
|
-
self.attribute_method_matchers.shift
|
22
|
-
|
23
|
-
# ActiveModel::Dirty methods for id
|
24
|
-
define_attribute_method(:id)
|
25
|
-
|
26
|
-
# Keep track of attribute mappings so we can get the FM field names
|
27
|
-
# for changed attributes
|
28
|
-
class_attribute :mapped_attributes, instance_writer: false, instance_predicate: false
|
29
|
-
|
30
|
-
# class_attribute supports a :default option since ActiveSupport 5.2,
|
31
|
-
# but we want to support previous versions too so we set the default
|
32
|
-
# manually instead
|
33
|
-
self.mapped_attributes = ::ActiveSupport::HashWithIndifferentAccess.new.freeze
|
34
|
-
|
35
|
-
class << self; private :mapped_attributes=; end
|
36
|
-
end
|
37
|
-
|
38
|
-
class_methods do
|
39
|
-
# Similar to Spyke::Base.attributes, but allows defining attribute
|
40
|
-
# methods that map to FM attributes with different names.
|
41
|
-
#
|
42
|
-
# Example:
|
43
|
-
#
|
44
|
-
# class Person < Spyke::Base
|
45
|
-
# include FmRest::Spyke::Model
|
46
|
-
#
|
47
|
-
# attributes first_name: "FstName", last_name: "LstName"
|
48
|
-
# end
|
49
|
-
#
|
50
|
-
# p = Person.new
|
51
|
-
# p.first_name = "Jojo"
|
52
|
-
# p.attributes # => { "FstName" => "Jojo" }
|
53
|
-
#
|
54
|
-
def attributes(*attrs)
|
55
|
-
if attrs.length == 1 && attrs.first.kind_of?(Hash)
|
56
|
-
attrs.first.each { |from, to| _fmrest_define_attribute(from, to) }
|
57
|
-
else
|
58
|
-
attrs.each { |attr| _fmrest_define_attribute(attr, attr) }
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
private
|
63
|
-
|
64
|
-
# Override Spyke::Base.new_or_return (private), called whenever
|
65
|
-
# loading records from the HTTP API, so we can reset dirty info on
|
66
|
-
# freshly loaded records
|
67
|
-
#
|
68
|
-
# See: https://github.com/balvig/spyke/blob/master/lib/spyke/http.rb
|
69
|
-
#
|
70
|
-
def new_or_return(attributes_or_object, *_)
|
71
|
-
# In case of an existing Spyke object return it as is so that we
|
72
|
-
# don't accidentally remove dirty data from associations
|
73
|
-
return super if attributes_or_object.is_a?(::Spyke::Base)
|
74
|
-
super.tap do |record|
|
75
|
-
# In ActiveModel 4.x #clear_changes_information is a private
|
76
|
-
# method, so we need to call it with send() in that case, but
|
77
|
-
# keep calling it normally for AM5+
|
78
|
-
if record.respond_to?(:clear_changes_information)
|
79
|
-
record.clear_changes_information
|
80
|
-
else
|
81
|
-
record.send(:clear_changes_information)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def _fmrest_attribute_methods_container
|
87
|
-
@fmrest_attribute_methods_container ||= Module.new.tap { |mod| include mod }
|
88
|
-
end
|
89
|
-
|
90
|
-
def _fmrest_define_attribute(from, to)
|
91
|
-
raise ArgumentError, "attribute name `id' is reserved for the recordId" if from.to_s == "id"
|
92
|
-
|
93
|
-
# We use a setter here instead of injecting the hash key/value pair
|
94
|
-
# directly with #[]= so that we don't change the mapped_attributes
|
95
|
-
# hash on the parent class. The resulting hash is frozen for the
|
96
|
-
# same reason.
|
97
|
-
self.mapped_attributes = mapped_attributes.merge(from => to).freeze
|
98
|
-
|
99
|
-
_fmrest_attribute_methods_container.module_eval do
|
100
|
-
define_method(from) do
|
101
|
-
attribute(to)
|
102
|
-
end
|
103
|
-
|
104
|
-
define_method(:"#{from}=") do |value|
|
105
|
-
send("#{from}_will_change!") unless value == send(from)
|
106
|
-
set_attribute(to, value)
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
# Define ActiveModel::Dirty's methods
|
111
|
-
define_attribute_method(from)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def id=(value)
|
116
|
-
id_will_change! unless value == id
|
117
|
-
super
|
118
|
-
end
|
119
|
-
|
120
|
-
def reload(*args)
|
121
|
-
super.tap { |r| clear_changes_information }
|
122
|
-
end
|
123
|
-
|
124
|
-
def save(*args)
|
125
|
-
super.tap { |r| changes_applied_after_save if r }
|
126
|
-
end
|
127
|
-
|
128
|
-
# ActiveModel::Dirty since version 5.2 assumes that if there's an
|
129
|
-
# @attributes instance variable set we must be using ActiveRecord, so
|
130
|
-
# we override the instance variable name used by Spyke to avoid issues.
|
131
|
-
#
|
132
|
-
# TODO: Submit a pull request to Spyke so this isn't needed
|
133
|
-
#
|
134
|
-
def attributes
|
135
|
-
@_spyke_attributes
|
136
|
-
end
|
137
|
-
|
138
|
-
# In addition to the comments above on `attributes`, this also adds
|
139
|
-
# support for forbidden attributes
|
140
|
-
#
|
141
|
-
def attributes=(new_attributes)
|
142
|
-
@_spyke_attributes ||= ::Spyke::Attributes.new(scope.params)
|
143
|
-
use_setters(sanitize_for_mass_assignment(new_attributes)) if new_attributes && !new_attributes.empty?
|
144
|
-
end
|
145
|
-
|
146
|
-
private
|
147
|
-
|
148
|
-
def changed_params
|
149
|
-
attributes.to_params.slice(*mapped_changed)
|
150
|
-
end
|
151
|
-
|
152
|
-
def mapped_changed
|
153
|
-
mapped_attributes.values_at(*changed)
|
154
|
-
end
|
155
|
-
|
156
|
-
# Use known mapped_attributes for inspect
|
157
|
-
#
|
158
|
-
def inspect_attributes
|
159
|
-
mapped_attributes.except(primary_key).map do |k, v|
|
160
|
-
"#{k}: #{attribute(v).inspect}"
|
161
|
-
end.join(', ')
|
162
|
-
end
|
163
|
-
|
164
|
-
def changes_applied_after_save
|
165
|
-
changes_applied
|
166
|
-
portals.each(&:parent_changes_applied)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module FmRest
|
4
|
-
module Spyke
|
5
|
-
module Model
|
6
|
-
module Auth
|
7
|
-
extend ::ActiveSupport::Concern
|
8
|
-
|
9
|
-
class_methods do
|
10
|
-
# Logs out the database session for this model (and other models
|
11
|
-
# using the same credentials).
|
12
|
-
#
|
13
|
-
# @raise [FmRest::V1::TokenSession::NoSessionTokenSet] if no session
|
14
|
-
# token was set (and no request is sent).
|
15
|
-
def logout!
|
16
|
-
connection.delete(FmRest::V1.session_path("dummy-token"))
|
17
|
-
end
|
18
|
-
|
19
|
-
# Logs out the database session for this model (and other models
|
20
|
-
# using the same credentials). Unlike `logout!`, no exception is
|
21
|
-
# raised in case of missing session token.
|
22
|
-
#
|
23
|
-
# @return [Boolean] Whether the logout request was sent (it's only
|
24
|
-
# sent if a session token was previously set)
|
25
|
-
def logout
|
26
|
-
logout!
|
27
|
-
true
|
28
|
-
rescue FmRest::V1::TokenSession::NoSessionTokenSet
|
29
|
-
false
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
@@ -1,74 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module FmRest
|
4
|
-
module Spyke
|
5
|
-
module Model
|
6
|
-
module Connection
|
7
|
-
extend ActiveSupport::Concern
|
8
|
-
|
9
|
-
included do
|
10
|
-
class_attribute :fmrest_config, instance_writer: false, instance_predicate: false
|
11
|
-
|
12
|
-
# Overrides the fmrest_config reader created by class_attribute so we
|
13
|
-
# can default set the default at call time.
|
14
|
-
#
|
15
|
-
# This method gets overwriten in subclasses if self.fmrest_config= is
|
16
|
-
# called.
|
17
|
-
define_singleton_method(:fmrest_config) do
|
18
|
-
FmRest.default_connection_settings
|
19
|
-
end
|
20
|
-
|
21
|
-
class_attribute :faraday_block, instance_accessor: false, instance_predicate: false
|
22
|
-
class << self; private :faraday_block, :faraday_block=; end
|
23
|
-
|
24
|
-
# FM Data API expects PATCH for updates (Spyke's default was PUT)
|
25
|
-
self.callback_methods = { create: :post, update: :patch }.freeze
|
26
|
-
end
|
27
|
-
|
28
|
-
class_methods do
|
29
|
-
def connection
|
30
|
-
super || fmrest_connection
|
31
|
-
end
|
32
|
-
|
33
|
-
# Sets a block for injecting custom middleware into the Faraday
|
34
|
-
# connection. Example usage:
|
35
|
-
#
|
36
|
-
# class MyModel < FmRest::Spyke::Base
|
37
|
-
# faraday do |conn|
|
38
|
-
# # Set up a custom logger for the model
|
39
|
-
# conn.response :logger, MyApp.logger, bodies: true
|
40
|
-
# end
|
41
|
-
# end
|
42
|
-
#
|
43
|
-
def faraday(&block)
|
44
|
-
self.faraday_block = block
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
def fmrest_connection
|
50
|
-
@fmrest_connection ||=
|
51
|
-
begin
|
52
|
-
config = fmrest_config
|
53
|
-
|
54
|
-
FmRest::V1.build_connection(config) do |conn|
|
55
|
-
faraday_block.call(conn) if faraday_block
|
56
|
-
|
57
|
-
# Pass the class to SpykeFormatter's initializer so it can have
|
58
|
-
# access to extra context defined in the model, e.g. a portal
|
59
|
-
# where name of the portal and the attributes prefix don't match
|
60
|
-
# and need to be specified as options to `portal`
|
61
|
-
conn.use FmRest::Spyke::SpykeFormatter, self
|
62
|
-
|
63
|
-
conn.use FmRest::V1::TypeCoercer, config
|
64
|
-
|
65
|
-
# FmRest::Spyke::JsonParse expects symbol keys
|
66
|
-
conn.response :json, parser_options: { symbolize_names: true }
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|