eco-helpers 3.2.2 → 3.2.3
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/.rubocop.yml +1 -1
- data/CHANGELOG.md +16 -1
- data/eco-helpers.gemspec +2 -2
- data/lib/eco/api/common/loaders/config/workflow/mailer.rb +1 -0
- data/lib/eco/api/common/people/person_entry.rb +1 -1
- data/lib/eco/api/common/people/person_parser.rb +1 -0
- data/lib/eco/api/organization/tag_tree.rb +11 -4
- data/lib/eco/api/usecases/default/locations/tagtree_upload_case.rb +20 -2
- data/lib/eco/api/usecases/graphql/helpers/location/base/tree_tracking.rb +14 -7
- data/lib/eco/api/usecases/graphql/helpers/location/base.rb +1 -1
- data/lib/eco/api/usecases/graphql/helpers/location/command/diff/as_update.rb +0 -1
- data/lib/eco/api/usecases/graphql/helpers/location/command/diffs/stages/diff_sortable/for_archive.rb +0 -1
- data/lib/eco/api/usecases/graphql/helpers/location/command/end_points/optimizations.rb +64 -0
- data/lib/eco/api/usecases/graphql/helpers/location/command/end_points.rb +96 -0
- data/lib/eco/api/usecases/graphql/helpers/location/command/input_unit_response.rb +69 -0
- data/lib/eco/api/usecases/graphql/helpers/location/command/result.rb +11 -10
- data/lib/eco/api/usecases/graphql/helpers/location/command/results.rb +120 -63
- data/lib/eco/api/usecases/graphql/helpers/location/command.rb +26 -26
- data/lib/eco/api/usecases/graphql/samples/location/command/dsl.rb +195 -37
- data/lib/eco/api/usecases/graphql/samples/location/command/results.rb +45 -13
- data/lib/eco/api/usecases/graphql/samples/location/command/service/tree_update.rb +0 -51
- data/lib/eco/api/usecases/graphql/samples/location/command/track_changed_ids.rb +6 -14
- data/lib/eco/api/usecases/graphql/samples/location/command.rb +1 -1
- data/lib/eco/api/usecases/graphql/samples/location/service/tree_diff/convertible/inputable.rb +4 -1
- data/lib/eco/api/usecases/graphql/samples/location/service/tree_diff.rb +6 -0
- data/lib/eco/api/usecases/graphql/samples/location/service/tree_to_list/converter/input.rb +1 -0
- data/lib/eco/api/usecases/lib/error_handling.rb +1 -1
- data/lib/eco/api/usecases/ooze_samples/helpers/creatable.rb +1 -0
- data/lib/eco/api/usecases/ooze_samples/helpers/rescuable.rb +1 -0
- data/lib/eco/data/hashes/array_diff.rb +14 -4
- data/lib/eco/data/locations/node_base/csv_convert.rb +9 -1
- data/lib/eco/data/locations/node_diff/nodes_diff.rb +6 -0
- data/lib/eco/version.rb +1 -1
- metadata +9 -7
- data/lib/eco/api/usecases/graphql/helpers/location/command/optimizations.rb +0 -84
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3323b0394649126c76ec4239eade6cca337049bd8f761f96aa7c66034717bd0
|
4
|
+
data.tar.gz: 3ddedb00b3b1e7674e014143e54e9cb46e52abb9482e9dfbc65dd4059fd0a70e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b6126ec90b13ef1d2d985b384215a529b5818ff5d8139d96fe80d5c21c6a4a0b367cb9bb7bf8b92ff6ed61f3df525f35e2639b5160693cc0e2fd6109bfb02cb
|
7
|
+
data.tar.gz: d4b799b66943fda1cf7340f358520d1f30715e27866ba900615cedc5bab593200a53cb2ef72bb6ff04c8e8b4e7c4b1b6bd1b0240145240364f67f633fbb3fbd6
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
|
5
|
-
## [3.
|
5
|
+
## [3.2.4] - 2025-08-xx
|
6
6
|
|
7
7
|
### Added
|
8
8
|
|
@@ -10,6 +10,21 @@ All notable changes to this project will be documented in this file.
|
|
10
10
|
|
11
11
|
### Fixed
|
12
12
|
|
13
|
+
## [3.2.3] - 2025-08-21
|
14
|
+
|
15
|
+
### Added
|
16
|
+
|
17
|
+
- `Person#phone_number`
|
18
|
+
|
19
|
+
### Changed
|
20
|
+
|
21
|
+
- upgraded dependencies
|
22
|
+
- `ecoportal-api` gem
|
23
|
+
|
24
|
+
### Fixed
|
25
|
+
|
26
|
+
- Switched to use DRAFTs on RS updates
|
27
|
+
|
13
28
|
## [3.2.2] - 2025-06-11
|
14
29
|
|
15
30
|
### Added
|
data/eco-helpers.gemspec
CHANGED
@@ -41,8 +41,8 @@ Gem::Specification.new do |spec|
|
|
41
41
|
spec.add_dependency 'bcrypt_pbkdf', '~> 1.0'
|
42
42
|
spec.add_dependency 'docx', '>= 0.8.0', '< 0.9'
|
43
43
|
spec.add_dependency 'dotenv', '~> 3'
|
44
|
-
spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.
|
45
|
-
spec.add_dependency 'ecoportal-api-graphql', '~> 1.3', '>= 1.3.
|
44
|
+
spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.14'
|
45
|
+
spec.add_dependency 'ecoportal-api-graphql', '~> 1.3', '>= 1.3.2'
|
46
46
|
spec.add_dependency 'ecoportal-api-v2', '~> 3.3', '>= 3.3.1'
|
47
47
|
spec.add_dependency 'ed25519', '~> 1.2'
|
48
48
|
spec.add_dependency 'fast_excel', '>= 0.5.0', '< 0.6'
|
@@ -68,7 +68,13 @@ module Eco
|
|
68
68
|
@path.push(self.id) unless top?
|
69
69
|
|
70
70
|
@nodes = @raw_nodes.map.with_index do |cnode, idx|
|
71
|
-
self.class.new(
|
71
|
+
self.class.new(
|
72
|
+
cnode,
|
73
|
+
depth: depth + 1,
|
74
|
+
path: @path.dup,
|
75
|
+
parent: self,
|
76
|
+
_weight: idx
|
77
|
+
)
|
72
78
|
end
|
73
79
|
|
74
80
|
init_hashes
|
@@ -199,8 +205,9 @@ module Eco
|
|
199
205
|
# @param max_depth [Boolean] up to what level `depth` nodes should be included.
|
200
206
|
# @return [Array[Hash]] where `Hash` is a `node` (i.e. `{"tag" => TAG, "nodes": Array[Hash]}`)
|
201
207
|
def as_json( # rubocop:disable Metrics/AbcSize
|
202
|
-
include_children: true,
|
203
|
-
|
208
|
+
include_children: true,
|
209
|
+
include_archived: true,
|
210
|
+
max_depth: total_depth,
|
204
211
|
&block
|
205
212
|
)
|
206
213
|
max_depth ||= total_depth
|
@@ -230,7 +237,7 @@ module Eco
|
|
230
237
|
depth + 1
|
231
238
|
]
|
232
239
|
node_json = self.class::HEADER.zip(values).to_h
|
233
|
-
node_json[
|
240
|
+
node_json['nodes'] = children_json if include_children
|
234
241
|
node_json = yield(node_json, self) if block_given?
|
235
242
|
node_json
|
236
243
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class Eco::API::UseCases::Default::Locations::TagtreeUpload < Eco::API::UseCases::GraphQL::Samples::Location::Command
|
2
2
|
require_relative 'cli/tagtree_upload_cli'
|
3
3
|
|
4
|
-
name
|
4
|
+
name 'tagtree-upload'
|
5
5
|
type :other
|
6
6
|
|
7
7
|
COMMANDS_PER_PAGE = 45
|
@@ -11,10 +11,28 @@ class Eco::API::UseCases::Default::Locations::TagtreeUpload < Eco::API::UseCases
|
|
11
11
|
false
|
12
12
|
end
|
13
13
|
|
14
|
+
def compare_live_with_file?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
# # With given the commands, it generates the input of the endpoint mutation.
|
19
|
+
# # @param commands [Array<Hash>]
|
20
|
+
# def input(commands, force_continue: force_continue?)
|
21
|
+
# {
|
22
|
+
# clientMutationId: '',
|
23
|
+
# id: target_structure_id,
|
24
|
+
# force: force_continue,
|
25
|
+
# commands: commands
|
26
|
+
# }
|
27
|
+
# end
|
28
|
+
|
14
29
|
# We only define the `:insert` stage
|
15
30
|
def inputs(force_continue: force_continue?)
|
16
31
|
{}.tap do |stages|
|
17
|
-
stages[:insert] = input(
|
32
|
+
stages[:insert] = input(
|
33
|
+
insert_commands,
|
34
|
+
force_continue: force_continue
|
35
|
+
)
|
18
36
|
end.each do |stage, input|
|
19
37
|
yield(input, stage) if block_given?
|
20
38
|
end
|
@@ -29,16 +29,20 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
|
|
29
29
|
# At any moment we want to know how the live tree is
|
30
30
|
# @note it also does a backup
|
31
31
|
# @return [Eco::API::Organization::TagTree] the latest tree (`current_tree`)
|
32
|
-
def track_current_tree(tree)
|
32
|
+
def track_current_tree(tree) # rubocop:disable Metrics/AbcSize
|
33
|
+
return unless track_current_tree?
|
33
34
|
return if simulate?
|
34
35
|
return if tree.nil?
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
tree_id = current_tree&.id
|
38
|
+
tree_name = current_tree&.name
|
39
|
+
tree_id = tree.id if tree.respond_to?(:id)
|
40
|
+
tree_name = tree.name if tree.respond_to?(:name)
|
41
|
+
latest_tree = tree if tree.is_a?(Eco::API::Organization::TagTree)
|
42
|
+
|
43
|
+
if tree.respond_to?(:treeify)
|
38
44
|
latest_tree ||= Eco::API::Organization::TagTree.new(
|
39
|
-
tree.treeify
|
40
|
-
id: tree.id,
|
41
|
-
name: tree.name
|
45
|
+
tree.treeify
|
42
46
|
)
|
43
47
|
end
|
44
48
|
|
@@ -46,6 +50,9 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
|
|
46
50
|
next unless latest_tree.is_a?(Eco::API::Organization::TagTree)
|
47
51
|
next if latest_tree.empty?
|
48
52
|
|
53
|
+
latest_tree.id = tree_id if latest_tree.id.nil?
|
54
|
+
latest_tree.name = tree_name if latest_tree.name.nil?
|
55
|
+
|
49
56
|
# a backup happens:
|
50
57
|
self.current_tree = latest_tree
|
51
58
|
end
|
@@ -53,7 +60,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
|
|
53
60
|
|
54
61
|
# @param tree [Eco::API::Organization::TagTree, Hash, Array]
|
55
62
|
# @return [Boolean] whether or not the backup was created
|
56
|
-
def backup_tree(tree = current_tree || live_tree)
|
63
|
+
def backup_tree(tree = current_tree || live_tree) # rubocop:disable Naming/PredicateMethod
|
57
64
|
return false if simulate?
|
58
65
|
case tree
|
59
66
|
when Eco::API::Organization::TagTree
|
@@ -22,7 +22,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
|
|
22
22
|
log(:warn) { msg }
|
23
23
|
|
24
24
|
# a backup happens:
|
25
|
-
return
|
25
|
+
return unless (self.current_tree = session_live_tree)
|
26
26
|
|
27
27
|
@target_structure_id = current_tree.id
|
28
28
|
end
|
@@ -18,7 +18,6 @@ class Eco::API::UseCases::GraphQL::Helpers::Location::Command::Diff
|
|
18
18
|
class_ids = class_ids.compact.map {|val| val.split('|')}.flatten.uniq
|
19
19
|
h['classificationIds'] = class_ids
|
20
20
|
end
|
21
|
-
|
22
21
|
if archive? || insert?
|
23
22
|
# We assume archives do not have `move` nor update `id` or `name`
|
24
23
|
h['nodeId'] = node_id || node_id_prev
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers::Location
|
2
|
+
module Command::EndPoints
|
3
|
+
module Optimizations
|
4
|
+
DEFAULT_COMMANDS_PER_PAGE = 45
|
5
|
+
|
6
|
+
# Available options are:
|
7
|
+
# - :per_request -> on each request
|
8
|
+
# - :per_batch -> at the end of each batch / stage (on last page)
|
9
|
+
# - :once -> only when the script starts to run
|
10
|
+
# @return [Symbol] the default tree tracking mode
|
11
|
+
def default_tree_tracking_mode
|
12
|
+
:per_request # :per_batch @todo per batch/stage is more plausible
|
13
|
+
end
|
14
|
+
|
15
|
+
# Helper to identify the commands payload block
|
16
|
+
# @note
|
17
|
+
# 1. Gives flexibility on at what time the structure should be retrieved
|
18
|
+
# 2. This increases performacne, as we don't retrieve the full tree on
|
19
|
+
# each request unless necessary/specified
|
20
|
+
# 3. `nil` falls the block back to the `ecoportal-api-graphql` gem, which
|
21
|
+
# has a default block that retrieves the structure
|
22
|
+
# @return [Proc, NilClass] the payload block
|
23
|
+
def scope_commands_block(track_tree_mode: default_tree_tracking_mode)
|
24
|
+
case track_tree_mode
|
25
|
+
when :per_request
|
26
|
+
nil
|
27
|
+
when :once, :per_batch
|
28
|
+
commands_payload_without_structure_block
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Prevents each request from timing out
|
33
|
+
def commands_per_page
|
34
|
+
if self.class.const_defined?(:COMMANDS_PER_PAGE)
|
35
|
+
self.class::COMMANDS_PER_PAGE
|
36
|
+
else
|
37
|
+
DEFAULT_COMMANDS_PER_PAGE
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Whether to stop or continue on command fail
|
42
|
+
def force_continue?
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
# Commands payload without querying Structure
|
47
|
+
# @note this servces the purpose of optimizing/speeding up the requests.
|
48
|
+
def commands_payload_without_structure_block
|
49
|
+
proc {
|
50
|
+
clientMutationId
|
51
|
+
ok
|
52
|
+
errors { # rubocop:disable Style/BlockDelimiters
|
53
|
+
details
|
54
|
+
fullMessages
|
55
|
+
messages
|
56
|
+
}
|
57
|
+
draft { # rubocop:disable Style/BlockDelimiters
|
58
|
+
___Ecoportal__API__GraphQL__Fragment__LocationDraft
|
59
|
+
}
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers::Location
|
2
|
+
module Command::EndPoints
|
3
|
+
include Eco::Language::AuxiliarLogger
|
4
|
+
include Eco::API::UseCases::GraphQL::Helpers::Location::Base
|
5
|
+
|
6
|
+
require_relative 'end_points/optimizations'
|
7
|
+
|
8
|
+
include Optimizations
|
9
|
+
|
10
|
+
def create_draft(
|
11
|
+
structure_id,
|
12
|
+
name: Time.now.iso8601,
|
13
|
+
notes: '',
|
14
|
+
include_archived_nodes: true
|
15
|
+
)
|
16
|
+
log(:info) { "Going to create draft (for #{structure_id})..." }
|
17
|
+
|
18
|
+
graphql.locationStructure.draft.create(
|
19
|
+
input: {
|
20
|
+
structureId: structure_id,
|
21
|
+
name: name,
|
22
|
+
notes: notes
|
23
|
+
},
|
24
|
+
includeArchivedNodes: include_archived_nodes
|
25
|
+
).tap do |payload|
|
26
|
+
unless payload.error?
|
27
|
+
log(:info) { "Created draft '#{payload.draft&.id}' (for #{structure_id})" }
|
28
|
+
next
|
29
|
+
end
|
30
|
+
|
31
|
+
msg = "Could not create draft for #{structure_id}:\n"
|
32
|
+
msg << JSON.pretty_generate(payload.error_doc)
|
33
|
+
|
34
|
+
raise StandardError, msg
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete_draft(draft_id)
|
39
|
+
graphql.locationStructure.draft.delete(
|
40
|
+
input: {
|
41
|
+
id: draft_id
|
42
|
+
}
|
43
|
+
).tap do |payload|
|
44
|
+
unless payload.error?
|
45
|
+
log(:info) { "Deleted draft #{draft_id}" }
|
46
|
+
next
|
47
|
+
end
|
48
|
+
|
49
|
+
msg = "Could not delete draft #{draft_id}:\n"
|
50
|
+
msg << JSON.pretty_generate(payload.error_doc)
|
51
|
+
|
52
|
+
raise StandardError, msg
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def publish_draft(
|
57
|
+
draft_id,
|
58
|
+
preview: false,
|
59
|
+
force: false,
|
60
|
+
include_archived_nodes: true
|
61
|
+
)
|
62
|
+
log(:info) { "Going to publish draft #{draft_id}..." }
|
63
|
+
|
64
|
+
graphql.locationStructure.draft.publish(
|
65
|
+
input: {
|
66
|
+
id: draft_id,
|
67
|
+
preview: preview,
|
68
|
+
force: force
|
69
|
+
},
|
70
|
+
includeArchivedNodes: include_archived_nodes
|
71
|
+
).tap do |payload|
|
72
|
+
unless payload.error?
|
73
|
+
log(:info) { "Published draft #{draft_id}" }
|
74
|
+
next
|
75
|
+
end
|
76
|
+
|
77
|
+
msg = "Could not publish draft #{draft_id}:\n"
|
78
|
+
msg << JSON.pretty_generate(payload.error_doc)
|
79
|
+
|
80
|
+
raise StandardError, msg
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def apply_commands(
|
85
|
+
input,
|
86
|
+
final: false,
|
87
|
+
track_tree_mode: nil,
|
88
|
+
&block
|
89
|
+
)
|
90
|
+
block ||= scope_commands_block(track_tree_mode: track_tree_mode)
|
91
|
+
block = nil if final
|
92
|
+
|
93
|
+
graphql.locationStructure.draft.addCommands(input: input, &block)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers::Location::Command
|
2
|
+
class InputUnitResponse
|
3
|
+
attr_reader :input, :response
|
4
|
+
|
5
|
+
def initialize(input, response)
|
6
|
+
@input = input
|
7
|
+
@response = response
|
8
|
+
end
|
9
|
+
|
10
|
+
def draft
|
11
|
+
response&.draft
|
12
|
+
end
|
13
|
+
|
14
|
+
def structure
|
15
|
+
draft&.structure
|
16
|
+
end
|
17
|
+
|
18
|
+
def commands
|
19
|
+
input[:commands]
|
20
|
+
end
|
21
|
+
|
22
|
+
# brief summary of the error
|
23
|
+
def error_doc
|
24
|
+
if (err_doc = response&.error_doc)
|
25
|
+
return err_doc
|
26
|
+
end
|
27
|
+
|
28
|
+
conflictingIds if conflictingIds?
|
29
|
+
end
|
30
|
+
|
31
|
+
def error?
|
32
|
+
return false if response.nil?
|
33
|
+
return true if conflictingIds?
|
34
|
+
|
35
|
+
response.error? # draft might have previous, but we will stop anyway!
|
36
|
+
end
|
37
|
+
alias_method :errors?, :error?
|
38
|
+
|
39
|
+
def ok?
|
40
|
+
!errors?
|
41
|
+
end
|
42
|
+
|
43
|
+
def conflictingIds? # rubocop:disable Naming/MethodName
|
44
|
+
conflictingIds.any?
|
45
|
+
end
|
46
|
+
|
47
|
+
def conflictingIds # rubocop:disable Naming/MethodName
|
48
|
+
draft_conflicting_ids & target_ids
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def draft_conflicting_ids
|
54
|
+
return [] unless draft
|
55
|
+
|
56
|
+
draft.conflictingIds || []
|
57
|
+
end
|
58
|
+
|
59
|
+
def target_ids
|
60
|
+
return [] unless commands.is_a?(Array)
|
61
|
+
|
62
|
+
commands.map do |cmd|
|
63
|
+
next cmd.target_ids if cmd.respond_to?(:target_ids)
|
64
|
+
|
65
|
+
cmd.values_at(:nodeId, :newId, :id)
|
66
|
+
end.flatten.compact.uniq
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -7,14 +7,11 @@ module Eco::API::UseCases::GraphQL::Helpers::Location::Command
|
|
7
7
|
@result = result
|
8
8
|
end
|
9
9
|
|
10
|
-
|
10
|
+
# @return [Symbol] the command type
|
11
|
+
def command_type
|
11
12
|
input.keys.first
|
12
13
|
end
|
13
14
|
|
14
|
-
def command_input_data
|
15
|
-
input[command]
|
16
|
-
end
|
17
|
-
|
18
15
|
def node_id
|
19
16
|
command_input_data[:nodeId]
|
20
17
|
end
|
@@ -24,7 +21,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location::Command
|
|
24
21
|
end
|
25
22
|
|
26
23
|
def applied?
|
27
|
-
!pending?
|
24
|
+
!pending? && success?
|
28
25
|
end
|
29
26
|
|
30
27
|
def success?
|
@@ -47,20 +44,24 @@ module Eco::API::UseCases::GraphQL::Helpers::Location::Command
|
|
47
44
|
|
48
45
|
feed = []
|
49
46
|
feed.concat(error.validationErrors.map(&:message)) unless error.validationErrors.empty?
|
50
|
-
feed << "Command: #{command_input_data
|
47
|
+
feed << "Command: #{JSON.pretty_generate(command_input_data)}"
|
51
48
|
|
52
49
|
msg << " * #{feed.join("\n * ")}"
|
53
50
|
msg.join("\n")
|
54
51
|
end
|
55
52
|
|
56
|
-
def
|
53
|
+
def command_result_data
|
57
54
|
result&.command
|
58
55
|
end
|
59
56
|
|
57
|
+
def command_input_data
|
58
|
+
input[command]
|
59
|
+
end
|
60
|
+
|
60
61
|
def command_id
|
61
|
-
return unless
|
62
|
+
return unless command_result_data
|
62
63
|
|
63
|
-
|
64
|
+
command_result_data['id']
|
64
65
|
end
|
65
66
|
|
66
67
|
def as_json
|