foreman-tasks 3.0.4 → 4.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby_tests.yml +5 -3
- data/app/controllers/foreman_tasks/api/tasks_controller.rb +2 -5
- data/app/lib/actions/entry_action.rb +8 -4
- data/app/lib/actions/helpers/lock.rb +11 -5
- data/app/lib/actions/middleware/keep_current_request_id.rb +4 -1
- data/app/lib/actions/middleware/keep_current_user.rb +11 -1
- data/app/lib/actions/observable_action.rb +80 -0
- data/app/lib/actions/proxy_action.rb +2 -4
- data/app/models/foreman_tasks/concerns/action_subject.rb +0 -6
- data/app/models/foreman_tasks/link.rb +60 -0
- data/app/models/foreman_tasks/lock.rb +30 -128
- data/app/models/foreman_tasks/task.rb +20 -7
- data/app/models/foreman_tasks/task/search.rb +7 -6
- data/app/views/foreman_tasks/api/locks/show.json.rabl +4 -0
- data/app/views/foreman_tasks/api/tasks/details.json.rabl +5 -3
- data/app/views/foreman_tasks/tasks/_lock_card.html.erb +10 -0
- data/db/migrate/20180927120509_add_user_id.foreman_tasks.rb +4 -2
- data/db/migrate/20181206123910_create_foreman_tasks_links.foreman_tasks.rb +26 -0
- data/db/migrate/20181206124952_migrate_non_exclusive_locks_to_links.foreman_tasks.rb +14 -0
- data/db/migrate/20181206131436_drop_old_locks.foreman_tasks.rb +20 -0
- data/db/migrate/20181206131627_make_locks_exclusive.foreman_tasks.rb +25 -0
- data/lib/foreman_tasks/cleaner.rb +10 -0
- data/lib/foreman_tasks/engine.rb +5 -2
- data/lib/foreman_tasks/tasks/export_tasks.rake +2 -2
- data/lib/foreman_tasks/version.rb +1 -1
- data/package.json +6 -6
- data/test/controllers/api/tasks_controller_test.rb +1 -1
- data/test/controllers/tasks_controller_test.rb +3 -3
- data/test/core/unit/runner_test.rb +4 -17
- data/test/factories/task_factory.rb +31 -4
- data/test/unit/actions/action_with_sub_plans_test.rb +5 -2
- data/test/unit/actions/proxy_action_test.rb +4 -1
- data/test/unit/cleaner_test.rb +4 -4
- data/test/unit/locking_test.rb +85 -0
- data/test/unit/task_test.rb +15 -13
- data/test/unit/triggering_test.rb +2 -2
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Locks.js +2 -2
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +3 -2
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Locks.test.js.snap +4 -4
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +2 -0
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +4 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +5 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +3 -0
- data/webpack/ForemanTasks/Components/TaskDetails/index.js +2 -0
- data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.scss +4 -3
- data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +2 -2
- data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionNameCellFormatter.test.js.snap +2 -3
- data/webpack/ForemanTasks/Components/TasksTable/formatters/actionNameCellFormatter.js +2 -3
- metadata +12 -4
- data/test/unit/lock_test.rb +0 -22
@@ -34,8 +34,9 @@ module ForemanTasks
|
|
34
34
|
describe Actions::ActionWithSubPlans do
|
35
35
|
include ForemanTasks::TestHelpers::WithInThreadExecutor
|
36
36
|
|
37
|
+
let(:user) { FactoryBot.create(:user) }
|
38
|
+
|
37
39
|
let(:task) do
|
38
|
-
user = FactoryBot.create(:user)
|
39
40
|
triggered = ForemanTasks.trigger(ParentAction, user)
|
40
41
|
raise triggered.error if triggered.respond_to?(:error)
|
41
42
|
triggered.finished.wait(30)
|
@@ -49,7 +50,9 @@ module ForemanTasks
|
|
49
50
|
|
50
51
|
specify "the locks of the sub-plan don't colide with the locks of its parent" do
|
51
52
|
child_task = task.sub_tasks.first
|
52
|
-
|
53
|
+
assert_not(child_task.locks.any?, "the lock is ensured by the parent")
|
54
|
+
found = ForemanTasks::Link.for_resource(user).where(:task_id => child_task.id).any?
|
55
|
+
assert(found, "the action is linked properly")
|
53
56
|
end
|
54
57
|
end
|
55
58
|
end
|
@@ -14,6 +14,9 @@ module ForemanTasks
|
|
14
14
|
Support::DummyProxyAction.any_instance.stubs(:with_batch_triggering?).returns(batch_triggering)
|
15
15
|
Support::DummyProxyAction.reset
|
16
16
|
RemoteTask.any_instance.stubs(:proxy).returns(Support::DummyProxyAction.proxy)
|
17
|
+
Setting.stubs(:[]).with('foreman_tasks_proxy_action_retry_interval')
|
18
|
+
Setting.stubs(:[]).with('foreman_tasks_proxy_action_retry_count')
|
19
|
+
Setting.stubs(:[]).with('foreman_tasks_proxy_batch_trigger')
|
17
20
|
@action = create_and_plan_action(Support::DummyProxyAction,
|
18
21
|
Support::DummyProxyAction.proxy,
|
19
22
|
'Proxy::DummyAction',
|
@@ -107,7 +110,7 @@ module ForemanTasks
|
|
107
110
|
_(action.world.clock.pending_pings.length).must_equal 1
|
108
111
|
_(action.output[:metadata][:failed_proxy_tasks].length).must_equal 1
|
109
112
|
2.times { action.output[:metadata][:failed_proxy_tasks] << {} }
|
110
|
-
_
|
113
|
+
_(proc { action = run_stubbed_action.call action }).must_raise(Errno::ECONNREFUSED)
|
111
114
|
_(action.state).must_equal :error
|
112
115
|
end
|
113
116
|
|
data/test/unit/cleaner_test.rb
CHANGED
@@ -60,18 +60,18 @@ class TasksTest < ActiveSupport::TestCase
|
|
60
60
|
task.started_at = task.ended_at = Time.zone.now
|
61
61
|
task.save
|
62
62
|
end]
|
63
|
-
|
63
|
+
link_to_delete = tasks_to_delete.first.links.create(:resource => User.current)
|
64
64
|
|
65
65
|
tasks_to_keep = [FactoryBot.create(:dynflow_task, :product_create_task)]
|
66
|
-
|
66
|
+
link_to_keep = tasks_to_keep.first.links.create(:resource => User.current)
|
67
67
|
|
68
68
|
cleaner.expects(:tasks_to_csv)
|
69
69
|
cleaner.delete
|
70
70
|
_(ForemanTasks::Task.where(id: tasks_to_delete)).must_be_empty
|
71
71
|
_(ForemanTasks::Task.where(id: tasks_to_keep)).must_equal tasks_to_keep
|
72
72
|
|
73
|
-
_(ForemanTasks::
|
74
|
-
_(ForemanTasks::
|
73
|
+
_(ForemanTasks::Link.find_by(id: link_to_delete.id)).must_be_nil
|
74
|
+
_(ForemanTasks::Link.find_by(id: link_to_keep.id)).wont_be_nil
|
75
75
|
end
|
76
76
|
|
77
77
|
it 'supports passing empty filter (just delete all)' do
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'foreman_tasks_test_helper'
|
3
|
+
|
4
|
+
module ForemanTasks
|
5
|
+
class LockTest < ::ActiveSupport::TestCase
|
6
|
+
describe ::ForemanTasks::Lock::LockConflict do
|
7
|
+
class FakeLockConflict < ForemanTasks::Lock::LockConflict
|
8
|
+
def _(val)
|
9
|
+
val.freeze
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'does not modify frozen strings' do
|
14
|
+
required_lock = OpenStruct.new(:name => 'my_lock')
|
15
|
+
# Before #21770 the next line would raise
|
16
|
+
# RuntimeError: can't modify frozen String
|
17
|
+
conflict = FakeLockConflict.new(required_lock, [])
|
18
|
+
assert conflict._('this should be frozen').frozen?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'locking and linking' do
|
23
|
+
before { [Lock, Link].each(&:destroy_all) }
|
24
|
+
let(:task1) { FactoryBot.create(:some_task) }
|
25
|
+
let(:task2) { FactoryBot.create(:some_task) }
|
26
|
+
let(:resource) { FactoryBot.create(:user) }
|
27
|
+
|
28
|
+
describe Lock do
|
29
|
+
it 'can lock a resource for a single task' do
|
30
|
+
Lock.lock!(resource, task1)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'can lock a resource for a single task only once' do
|
34
|
+
Lock.lock!(resource, task1)
|
35
|
+
_(Lock.for_resource(resource).count).must_equal 1
|
36
|
+
Lock.lock!(resource, task1)
|
37
|
+
_(Lock.for_resource(resource).count).must_equal 1
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'cannot lock a resource for multiple tasks' do
|
41
|
+
lock = Lock.lock!(resource, task1)
|
42
|
+
_(Lock.colliding_locks(resource, task2)).must_equal [lock]
|
43
|
+
assert_raises Lock::LockConflict do
|
44
|
+
Lock.lock!(resource, task2)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'raises LockConflict when enforced by db' do
|
49
|
+
lock = Lock.lock!(resource, task1)
|
50
|
+
Lock.any_instance
|
51
|
+
.expects(:colliding_locks).twice.returns([], [lock])
|
52
|
+
exception = assert_raises Lock::LockConflict do
|
53
|
+
Lock.lock!(resource, task2)
|
54
|
+
end
|
55
|
+
_(exception.message).must_match(/#{lock.task_id}/)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'creates a link when creating a lock for a resource' do
|
59
|
+
Lock.lock!(resource, task1)
|
60
|
+
link = Link.for_resource(resource).first
|
61
|
+
_(link.task_id).must_equal task1.id
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe Link do
|
66
|
+
it 'can link a resource to a single task' do
|
67
|
+
Link.link!(resource, task1)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'can link a resource for a single task only once' do
|
71
|
+
Link.link!(resource, task1)
|
72
|
+
_(Link.for_resource(resource).count).must_equal 1
|
73
|
+
Link.link!(resource, task1)
|
74
|
+
_(Link.for_resource(resource).count).must_equal 1
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'can link a resource to multiple tasks' do
|
78
|
+
Link.link!(resource, task1)
|
79
|
+
Link.link!(resource, task2)
|
80
|
+
_(Link.for_resource(resource).count).must_equal 2
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/test/unit/task_test.rb
CHANGED
@@ -31,9 +31,9 @@ class TasksTest < ActiveSupport::TestCase
|
|
31
31
|
assert_equal [@task_one], ForemanTasks::Task.search_for("user = #{@user_one.login}")
|
32
32
|
end
|
33
33
|
|
34
|
-
|
35
|
-
_
|
36
|
-
_
|
34
|
+
it 'cannot search by arbitrary key' do
|
35
|
+
_(proc { ForemanTasks::Task.search_for('user.my_key ~ 5') }).must_raise(ScopedSearch::QueryNotSupported)
|
36
|
+
_(proc { ForemanTasks::Task.search_for('user. = 5') }).must_raise(ScopedSearch::QueryNotSupported)
|
37
37
|
end
|
38
38
|
|
39
39
|
test 'can search the tasks by negated user' do
|
@@ -59,8 +59,8 @@ class TasksTest < ActiveSupport::TestCase
|
|
59
59
|
end
|
60
60
|
|
61
61
|
test 'cannot glob on user\'s id' do
|
62
|
-
_
|
63
|
-
_
|
62
|
+
_(proc { ForemanTasks::Task.search_for("user.id ~ something") }).must_raise(ScopedSearch::QueryNotSupported)
|
63
|
+
_(proc { ForemanTasks::Task.search_for("user.id ~ 5") }).must_raise(ScopedSearch::QueryNotSupported)
|
64
64
|
end
|
65
65
|
|
66
66
|
test 'can search the tasks by user with wildcards' do
|
@@ -126,15 +126,17 @@ class TasksTest < ActiveSupport::TestCase
|
|
126
126
|
end
|
127
127
|
|
128
128
|
it 'raises an exception if duration is unknown' do
|
129
|
-
_
|
129
|
+
_(proc { ForemanTasks::Task.search_for('duration = "25 potatoes"') }).must_raise ScopedSearch::QueryNotSupported
|
130
130
|
end
|
131
131
|
end
|
132
132
|
|
133
133
|
context 'by taxonomies' do
|
134
134
|
test 'can search by taxonomies using IN' do
|
135
|
-
assert_nothing_raised
|
136
|
-
assert_nothing_raised
|
137
|
-
assert_nothing_raised
|
135
|
+
assert_nothing_raised { ForemanTasks::Task.search_for('location_id ^ (1)').first }
|
136
|
+
assert_nothing_raised { ForemanTasks::Task.search_for('organization_id ^ (1)').first }
|
137
|
+
assert_nothing_raised { ForemanTasks::Task.search_for('location_id ^ (1,2)').first }
|
138
|
+
assert_nothing_raised { ForemanTasks::Task.search_for('organization_id ^ (1,2)').first }
|
139
|
+
assert_nothing_raised { ForemanTasks::Task.search_for('organization_id = 1').first }
|
138
140
|
end
|
139
141
|
end
|
140
142
|
end
|
@@ -300,7 +302,7 @@ class TasksTest < ActiveSupport::TestCase
|
|
300
302
|
resource_type = 'restype1'
|
301
303
|
|
302
304
|
task1_old = FactoryBot.create(
|
303
|
-
:
|
305
|
+
:task_with_links,
|
304
306
|
started_at: '2019-10-01 11:15:55',
|
305
307
|
ended_at: '2019-10-01 11:15:57',
|
306
308
|
resource_id: 1,
|
@@ -308,7 +310,7 @@ class TasksTest < ActiveSupport::TestCase
|
|
308
310
|
resource_type: resource_type
|
309
311
|
)
|
310
312
|
task1_new = FactoryBot.create(
|
311
|
-
:
|
313
|
+
:task_with_links,
|
312
314
|
started_at: '2019-10-02 11:15:55',
|
313
315
|
ended_at: '2019-10-02 11:15:57',
|
314
316
|
resource_id: 1,
|
@@ -316,7 +318,7 @@ class TasksTest < ActiveSupport::TestCase
|
|
316
318
|
resource_type: resource_type
|
317
319
|
)
|
318
320
|
task2 = FactoryBot.create(
|
319
|
-
:
|
321
|
+
:task_with_links,
|
320
322
|
started_at: '2019-10-03 11:15:55',
|
321
323
|
ended_at: '2019-10-03 11:15:57',
|
322
324
|
resource_id: 2,
|
@@ -324,7 +326,7 @@ class TasksTest < ActiveSupport::TestCase
|
|
324
326
|
resource_type: resource_type
|
325
327
|
)
|
326
328
|
task3 = FactoryBot.create(
|
327
|
-
:
|
329
|
+
:task_with_links,
|
328
330
|
started_at: '2019-10-03 11:15:55',
|
329
331
|
ended_at: '2019-10-03 11:15:57',
|
330
332
|
resource_id: 3,
|
@@ -24,7 +24,7 @@ class TriggeringTest < ActiveSupport::TestCase
|
|
24
24
|
it 'cannot have mode set to arbitrary value' do
|
25
25
|
triggering = FactoryBot.build(:triggering)
|
26
26
|
_(triggering).must_be :valid?
|
27
|
-
_
|
28
|
-
_
|
27
|
+
_(proc { triggering.mode = 'bogus' }).must_raise ArgumentError
|
28
|
+
_(proc { triggering.mode = 27 }).must_raise ArgumentError
|
29
29
|
end
|
30
30
|
end
|
@@ -21,10 +21,10 @@ const Locks = ({ locks }) => (
|
|
21
21
|
lock.exclusive ? 'fa-lock' : 'fa-unlock-alt'
|
22
22
|
}`}
|
23
23
|
/>
|
24
|
-
{lock.
|
24
|
+
{lock.resource_type}
|
25
25
|
</Card.Title>
|
26
26
|
<Card.Body>
|
27
|
-
{
|
27
|
+
{`id:${lock.resource_id}`}
|
28
28
|
<br />
|
29
29
|
</Card.Body>
|
30
30
|
</Card>
|
@@ -69,6 +69,7 @@ class TaskInfo extends Component {
|
|
69
69
|
{
|
70
70
|
title: 'Name',
|
71
71
|
value: action || __('N/A'),
|
72
|
+
className: 'details-name',
|
72
73
|
},
|
73
74
|
{
|
74
75
|
title: 'Start at',
|
@@ -129,7 +130,7 @@ class TaskInfo extends Component {
|
|
129
130
|
{__(items[0].title)}:
|
130
131
|
</span>
|
131
132
|
</Col>
|
132
|
-
<Col md={5} sm={6}>
|
133
|
+
<Col md={5} sm={6} className={items[0].className}>
|
133
134
|
<span>{items[0].value}</span>
|
134
135
|
</Col>
|
135
136
|
<Col md={2} sm={6}>
|
@@ -137,7 +138,7 @@ class TaskInfo extends Component {
|
|
137
138
|
{__(items[1].title)}:
|
138
139
|
</span>
|
139
140
|
</Col>
|
140
|
-
<Col md={3} sm={6}>
|
141
|
+
<Col md={3} sm={6} className={items[1].className}>
|
141
142
|
{items[1].value}
|
142
143
|
</Col>
|
143
144
|
</Row>
|
@@ -39,12 +39,12 @@ exports[`Locks rendering render with Props 1`] = `
|
|
39
39
|
<span
|
40
40
|
className="fa fa-unlock-alt"
|
41
41
|
/>
|
42
|
-
|
42
|
+
User
|
43
43
|
</CardTitle>
|
44
44
|
<CardBody
|
45
45
|
className=""
|
46
46
|
>
|
47
|
-
|
47
|
+
id:4
|
48
48
|
<br />
|
49
49
|
</CardBody>
|
50
50
|
</Card>
|
@@ -71,12 +71,12 @@ exports[`Locks rendering render with Props 1`] = `
|
|
71
71
|
<span
|
72
72
|
className="fa fa-unlock-alt"
|
73
73
|
/>
|
74
|
-
|
74
|
+
User
|
75
75
|
</CardTitle>
|
76
76
|
<CardBody
|
77
77
|
className=""
|
78
78
|
>
|
79
|
-
|
79
|
+
id:2
|
80
80
|
<br />
|
81
81
|
</CardBody>
|
82
82
|
</Card>
|
@@ -27,6 +27,7 @@ exports[`TaskInfo rendering render with Props 1`] = `
|
|
27
27
|
</Col>
|
28
28
|
<Col
|
29
29
|
bsClass="col"
|
30
|
+
className="details-name"
|
30
31
|
componentClass="div"
|
31
32
|
md={5}
|
32
33
|
sm={6}
|
@@ -330,6 +331,7 @@ exports[`TaskInfo rendering render without Props 1`] = `
|
|
330
331
|
</Col>
|
331
332
|
<Col
|
332
333
|
bsClass="col"
|
334
|
+
className="details-name"
|
333
335
|
componentClass="div"
|
334
336
|
md={5}
|
335
337
|
sm={6}
|
@@ -19,6 +19,7 @@ const TaskDetails = ({
|
|
19
19
|
failedSteps,
|
20
20
|
runningSteps,
|
21
21
|
locks,
|
22
|
+
links,
|
22
23
|
cancelStep,
|
23
24
|
taskReloadStart,
|
24
25
|
taskReloadStop,
|
@@ -87,7 +88,7 @@ const TaskDetails = ({
|
|
87
88
|
<Errors executionPlan={executionPlan} failedSteps={failedSteps} />
|
88
89
|
</Tab>
|
89
90
|
<Tab eventKey={4} disabled={isLoading} title={__('Locks')}>
|
90
|
-
<Locks locks={locks} />
|
91
|
+
<Locks locks={locks.concat(links)} />
|
91
92
|
</Tab>
|
92
93
|
<Tab eventKey={5} disabled={isLoading} title={__('Raw')}>
|
93
94
|
<Raw
|
@@ -114,6 +115,7 @@ TaskDetails.propTypes = {
|
|
114
115
|
APIerror: PropTypes.object,
|
115
116
|
taskReloadStop: PropTypes.func.isRequired,
|
116
117
|
taskReloadStart: PropTypes.func.isRequired,
|
118
|
+
links: PropTypes.array,
|
117
119
|
...Task.propTypes,
|
118
120
|
...Errors.propTypes,
|
119
121
|
...Locks.propTypes,
|
@@ -124,6 +126,7 @@ TaskDetails.defaultProps = {
|
|
124
126
|
runningSteps: [],
|
125
127
|
APIerror: null,
|
126
128
|
status: STATUS.PENDING,
|
129
|
+
links: [],
|
127
130
|
...Task.defaultProps,
|
128
131
|
...RunningSteps.defaultProps,
|
129
132
|
...Errors.defaultProps,
|
@@ -70,6 +70,9 @@ export const selectHasSubTasks = state =>
|
|
70
70
|
export const selectLocks = state =>
|
71
71
|
selectTaskDetailsResponse(state).locks || [];
|
72
72
|
|
73
|
+
export const selectLinks = state =>
|
74
|
+
selectTaskDetailsResponse(state).links || [];
|
75
|
+
|
73
76
|
export const selectUsernamePath = state =>
|
74
77
|
selectTaskDetailsResponse(state)?.username_path;
|
75
78
|
|
@@ -9,6 +9,7 @@ import {
|
|
9
9
|
selectStartBefore,
|
10
10
|
selectStartedAt,
|
11
11
|
selectLocks,
|
12
|
+
selectLinks,
|
12
13
|
selectInput,
|
13
14
|
selectOutput,
|
14
15
|
selectResumable,
|
@@ -56,6 +57,7 @@ const mapStateToProps = state => ({
|
|
56
57
|
help: selectHelp(state),
|
57
58
|
hasSubTasks: selectHasSubTasks(state),
|
58
59
|
locks: selectLocks(state),
|
60
|
+
links: selectLinks(state),
|
59
61
|
usernamePath: selectUsernamePath(state),
|
60
62
|
action: selectAction(state),
|
61
63
|
state: selectState(state),
|
data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap
CHANGED
@@ -17,7 +17,7 @@ exports[`TasksTablePage rendering render with Breadcrubs and edit permissions 1`
|
|
17
17
|
/>
|
18
18
|
<PageLayout
|
19
19
|
beforeToolbarComponent={
|
20
|
-
<
|
20
|
+
<Memo(Connect(TasksDashboard))
|
21
21
|
history={
|
22
22
|
Object {
|
23
23
|
"location": Object {
|
@@ -169,7 +169,7 @@ exports[`TasksTablePage rendering render with minimal props 1`] = `
|
|
169
169
|
/>
|
170
170
|
<PageLayout
|
171
171
|
beforeToolbarComponent={
|
172
|
-
<
|
172
|
+
<Memo(Connect(TasksDashboard))
|
173
173
|
history={
|
174
174
|
Object {
|
175
175
|
"location": Object {
|