skinny_controllers 0.10.6 → 0.10.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -433
- data/lib/skinny_controllers/diet.rb +21 -9
- data/lib/skinny_controllers/policy/base.rb +0 -4
- data/lib/skinny_controllers/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec8abdfb85c06f6eb9502eed56d1c9aaefe44dae
|
4
|
+
data.tar.gz: 1cb87906f028c8e73e8c6cffb0d1c4e4ede94b4f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93867fac825426ab77574c718b2a8b4027dc3c32a2c0e72dd7764757c49e2a612e87aa1445f217e1a160534cb7f72859e72c4e771e3a5e56a3859f60f3300eab
|
7
|
+
data.tar.gz: 89bc46531b3fa9fe7a0c1dac5cc7f877181814a0a457460d88fe8477ce145d782ecd466d7009c8ec546889a76a3fde7706f6fd6d01ab723de6462ad59639ef7a
|
data/README.md
CHANGED
@@ -38,20 +38,6 @@ or
|
|
38
38
|
gem install skinny_controllers
|
39
39
|
```
|
40
40
|
|
41
|
-
|
42
|
-
## Generators
|
43
|
-
|
44
|
-
```bash
|
45
|
-
rails g operation event_summary
|
46
|
-
# => create app/operations/event_summary_operations.rb
|
47
|
-
|
48
|
-
rails g policy event_summary
|
49
|
-
# create app/policies/event_summary_policy.rb
|
50
|
-
|
51
|
-
rails g skinny_controller event_summaries
|
52
|
-
# create app/controllers/event_summaries_controller.rb
|
53
|
-
```
|
54
|
-
|
55
41
|
# Usage
|
56
42
|
|
57
43
|
## In a controller:
|
@@ -65,117 +51,6 @@ render json: model
|
|
65
51
|
|
66
52
|
and that's it!
|
67
53
|
|
68
|
-
The above does a multitude of assumptions to make sure that you can type the least amount code possible.
|
69
|
-
|
70
|
-
1. Your controller name is the name of your _resource_.
|
71
|
-
2. Any defined policies or operations follow the formats (though they don't have to exist):
|
72
|
-
- `class #{resource_name}Policy`
|
73
|
-
- `module #{resource_name}Operations`
|
74
|
-
3. Your model responds to `find`, and `where`
|
75
|
-
4. If relying on the default / implicit operations for create and update, the params key for your model's changes much be formatted as `{ Model.name.underscore => { attributes }}`
|
76
|
-
5. If using strong parameters, SkinnyControllers will look for `{action}_{model}_params` then `{model}_params` and then `params`. See the `strong_parameters_spec.rb` test to see an example.
|
77
|
-
|
78
|
-
### Per Controller Configuration
|
79
|
-
|
80
|
-
```ruby
|
81
|
-
skinny_controllers_config model_class: AClass,
|
82
|
-
parent_class: ParentClass,
|
83
|
-
asociation_name: :association_aclasses,
|
84
|
-
model_params_key: :aclass
|
85
|
-
```
|
86
|
-
|
87
|
-
#### model_class
|
88
|
-
Lets say you have a JSON API resource that you'd like to render that has some additional/subset of data.
|
89
|
-
Maybe the model is an `Event`, and the resource an `EventSummary` (which could do some aggregation of `Event` data).
|
90
|
-
|
91
|
-
The naming of all the objects should be as follows:
|
92
|
-
- `EventSummariesController`
|
93
|
-
- `EventSummaryOperations::*`
|
94
|
-
- `EventSummaryPolicy`
|
95
|
-
- and the model is still `Event`
|
96
|
-
|
97
|
-
In `EventSummariesController`, you would make the following additions:
|
98
|
-
```ruby
|
99
|
-
class EventSummariesController < ApiController # or whatever your superclass is
|
100
|
-
include SkinnyControllers::Diet
|
101
|
-
|
102
|
-
skinny_controllers_config model_class: Event
|
103
|
-
|
104
|
-
def index
|
105
|
-
render json: model, each_serializer: EventSummariesSerializer
|
106
|
-
end
|
107
|
-
|
108
|
-
def show
|
109
|
-
render json: model, serializer: EventSummariesSerializer
|
110
|
-
end
|
111
|
-
end
|
112
|
-
```
|
113
|
-
|
114
|
-
Note that `each_serializer` and `serializer` is not part of `SkinnyControllers`, and is part of [ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers).
|
115
|
-
|
116
|
-
Also note that setting `model_class` may be required if your model is namespaced.
|
117
|
-
|
118
|
-
#### parent_class and association_name
|
119
|
-
|
120
|
-
If you want to scope the finding of a resource to a parent object, `parent_class` must be set
|
121
|
-
|
122
|
-
```ruby
|
123
|
-
skinny_controllers_config parent_class: ParentClass,
|
124
|
-
assaciation_name: :children,
|
125
|
-
model_class: Child
|
126
|
-
```
|
127
|
-
|
128
|
-
Given the above configuration in a controller, and a request with the params:
|
129
|
-
|
130
|
-
```
|
131
|
-
{
|
132
|
-
id: 2,
|
133
|
-
parent_id: 78
|
134
|
-
}
|
135
|
-
```
|
136
|
-
|
137
|
-
The following query will be made:
|
138
|
-
|
139
|
-
```ruby
|
140
|
-
Parent.find(78).children.find(2)
|
141
|
-
```
|
142
|
-
|
143
|
-
#### model_params_key
|
144
|
-
|
145
|
-
Date stored another a different key in `params`?
|
146
|
-
|
147
|
-
```ruby
|
148
|
-
skinny_controllers_config model_class: Child,
|
149
|
-
model_params_key: :progeny
|
150
|
-
```
|
151
|
-
|
152
|
-
Given the above configuration in a controller, and a request with the params:
|
153
|
-
|
154
|
-
```
|
155
|
-
{
|
156
|
-
progeny: {
|
157
|
-
attribute1: 'value'
|
158
|
-
}
|
159
|
-
}
|
160
|
-
```
|
161
|
-
|
162
|
-
The attributes inside the `progeny` sub hash will be used instead of the default, `child`.
|
163
|
-
|
164
|
-
### What if your model is namespaced?
|
165
|
-
|
166
|
-
All you have to do is set the `model_class`, and `model_key`.
|
167
|
-
|
168
|
-
```ruby
|
169
|
-
class ItemsController < ApiController # or whatever your superclass is
|
170
|
-
include SkinnyControllers::Diet
|
171
|
-
|
172
|
-
skinny_controllers_config model_class: NameSpace::Item
|
173
|
-
model_params_key: :item
|
174
|
-
end
|
175
|
-
```
|
176
|
-
`model_key` specifies the key to look for params when creating / updating the model.
|
177
|
-
|
178
|
-
Note that while `model_key` doesn't *have* to be specified, it would default to name_space/item. So, just keep that in mind.
|
179
54
|
|
180
55
|
### What if you want to call your own operations?
|
181
56
|
|
@@ -227,314 +102,6 @@ end
|
|
227
102
|
|
228
103
|
Note that we don't need the id under the data hash, because in a RESTful api, the id will be available to us through the top level params hash.
|
229
104
|
|
230
|
-
|
231
|
-
## Defining Operations
|
232
|
-
|
233
|
-
Operations should be placed in `app/operations` of your rails app.
|
234
|
-
|
235
|
-
For operations concerning an `Event`, they should be under `app/operations/event_operations/`.
|
236
|
-
|
237
|
-
Using the example from the specs:
|
238
|
-
```ruby
|
239
|
-
module EventOperations
|
240
|
-
class Read < SkinnyControllers::Operation::Base
|
241
|
-
def run
|
242
|
-
model if allowed?
|
243
|
-
end
|
244
|
-
end
|
245
|
-
end
|
246
|
-
```
|
247
|
-
|
248
|
-
alternatively, all operation verbs can be stored in the same file under (for example) `app/operations/user_operations.rb`
|
249
|
-
|
250
|
-
```ruby
|
251
|
-
module UserOperations
|
252
|
-
class Read < SkinnyControllers::Operation::Base
|
253
|
-
def run
|
254
|
-
model if allowed?
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
class ReadAll < SkinnyControllers::Operation::Base
|
259
|
-
def run
|
260
|
-
model if allowed?
|
261
|
-
end
|
262
|
-
end
|
263
|
-
end
|
264
|
-
```
|
265
|
-
|
266
|
-
### Creating
|
267
|
-
|
268
|
-
To achieve default functionality, this operation *may* be defined -- though, it is implicitly assumed to function this way if not defined.
|
269
|
-
```ruby
|
270
|
-
module UserOperations
|
271
|
-
class Create < SkinnyControllers::Operation::Base
|
272
|
-
def run
|
273
|
-
@model = User.new(model_params)
|
274
|
-
|
275
|
-
# raising an exception here allows the corresponding resource controller to
|
276
|
-
# `rescue_from SkinnyControllers::DeniedByPolicy` and have a uniform error
|
277
|
-
# returned to the frontend
|
278
|
-
raise SkinnyControllers::DeniedByPolicy.new('Something Horrible') unless allowed?
|
279
|
-
|
280
|
-
@model.save
|
281
|
-
return @model # or just `model`
|
282
|
-
end
|
283
|
-
end
|
284
|
-
end
|
285
|
-
```
|
286
|
-
|
287
|
-
### Updating
|
288
|
-
```ruby
|
289
|
-
module UserOperations
|
290
|
-
class Update < SkinnyControllers::Operation::Base
|
291
|
-
def run
|
292
|
-
# this throws a DeniedByPolicy exception if `allowed?` returns false
|
293
|
-
check_allowed!
|
294
|
-
|
295
|
-
model.update(model_params)
|
296
|
-
model
|
297
|
-
end
|
298
|
-
end
|
299
|
-
end
|
300
|
-
```
|
301
|
-
|
302
|
-
### Deleting
|
303
|
-
|
304
|
-
Goal: Users should only be able to delete themselves
|
305
|
-
|
306
|
-
To achieve default functionality, this operation *may* be defined -- though, it is implicitly assumed to function this way if not defined.
|
307
|
-
```ruby
|
308
|
-
module UserOperations
|
309
|
-
class Delete < SkinnyControllers::Operation::Base
|
310
|
-
def run
|
311
|
-
model.destroy if allowed?
|
312
|
-
end
|
313
|
-
end
|
314
|
-
end
|
315
|
-
```
|
316
|
-
|
317
|
-
NOTE: `allowed?` is `true` by default via the `SkinnyControllers.allow_by_default` option.
|
318
|
-
|
319
|
-
## Defining Policies
|
320
|
-
|
321
|
-
Policies should be placed in `app/policies` of your rails app.
|
322
|
-
These are where you define your access logic, and how to decide if a user has access to the `object`
|
323
|
-
|
324
|
-
```ruby
|
325
|
-
class EventPolicy < SkinnyControllers::Policy::Base
|
326
|
-
def read?(event = object)
|
327
|
-
event.user == user
|
328
|
-
end
|
329
|
-
end
|
330
|
-
```
|
331
|
-
|
332
|
-
|
333
|
-
## More Advanced Usage
|
334
|
-
|
335
|
-
These are snippets taking from other projects.
|
336
|
-
|
337
|
-
### Using ransack
|
338
|
-
|
339
|
-
```ruby
|
340
|
-
# config/initializers/skinny_controllers.rb
|
341
|
-
SkinnyControllers.search_proc = lambda do |relation|
|
342
|
-
relation.ransack(params[:q]).result
|
343
|
-
end
|
344
|
-
```
|
345
|
-
|
346
|
-
### Finding a record when the id parameter isn't passed
|
347
|
-
|
348
|
-
```ruby
|
349
|
-
module HostOperations
|
350
|
-
class Read < SkinnyControllers::Operation::Base
|
351
|
-
def run
|
352
|
-
# always allowed, never restricted
|
353
|
-
# (because there is now call to allowed?)
|
354
|
-
model
|
355
|
-
end
|
356
|
-
|
357
|
-
# the params to this method should include the subdomain
|
358
|
-
# e.g.: { subdomain: 'swingin2015' }
|
359
|
-
def model_from_params
|
360
|
-
subdomain = params[:subdomain]
|
361
|
-
host = Host.find_by_subdomain(subdomain)
|
362
|
-
end
|
363
|
-
end
|
364
|
-
end
|
365
|
-
```
|
366
|
-
|
367
|
-
### The built in model-finding methods can be completely ignored
|
368
|
-
|
369
|
-
The `model` method does not need to be overridden. `run` is what is called on the operation.
|
370
|
-
|
371
|
-
```ruby
|
372
|
-
module MembershipRenewalOperations
|
373
|
-
# MembershipRenewalsController#index
|
374
|
-
class ReadAll < SkinnyControllers::Operation::Base
|
375
|
-
|
376
|
-
def run
|
377
|
-
# default 'model' functionality is avoided
|
378
|
-
latest_renewals
|
379
|
-
end
|
380
|
-
|
381
|
-
private
|
382
|
-
|
383
|
-
def organization
|
384
|
-
id = params[:organization_id]
|
385
|
-
Organization.find(id)
|
386
|
-
end
|
387
|
-
|
388
|
-
def renewals
|
389
|
-
options = organization.membership_options.includes(renewals: [:user, :membership_option])
|
390
|
-
options.map(&:renewals).flatten
|
391
|
-
end
|
392
|
-
|
393
|
-
def latest_renewals
|
394
|
-
sorted_renewals = renewals.sort_by{|r| [r.user_id,r.updated_at]}.reverse
|
395
|
-
|
396
|
-
# unique picks the first option.
|
397
|
-
# so, because the list is sorted by user id, then updated at,
|
398
|
-
# for each user, the first renewal will be chosen...
|
399
|
-
# and because it is descending, that means the most recent renewal
|
400
|
-
sorted_renewals.uniq { |r| r.user_id }
|
401
|
-
end
|
402
|
-
end
|
403
|
-
|
404
|
-
end
|
405
|
-
```
|
406
|
-
|
407
|
-
### Updating / Deleting the current_user
|
408
|
-
|
409
|
-
This is something you could do if you always know your model ahead of time.
|
410
|
-
|
411
|
-
```ruby
|
412
|
-
module UserOperations
|
413
|
-
class Update < SkinnyControllers::Operation::Base
|
414
|
-
def run
|
415
|
-
return unless allowed_for?(current_user)
|
416
|
-
# update with password provided by Devise
|
417
|
-
current_user.update_with_password(model_params)
|
418
|
-
current_user
|
419
|
-
end
|
420
|
-
end
|
421
|
-
|
422
|
-
class Delete < SkinnyControllers::Operation::Base
|
423
|
-
def run
|
424
|
-
if allowed_for?(current_user)
|
425
|
-
if current_user.upcoming_events.count > 0
|
426
|
-
current_user.errors.add(
|
427
|
-
:base,
|
428
|
-
"You cannot delete your account when you are about to attend an event."
|
429
|
-
)
|
430
|
-
else
|
431
|
-
current_user.destroy
|
432
|
-
end
|
433
|
-
|
434
|
-
current_user
|
435
|
-
end
|
436
|
-
end
|
437
|
-
end
|
438
|
-
|
439
|
-
end
|
440
|
-
```
|
441
|
-
|
442
|
-
## Testing
|
443
|
-
|
444
|
-
The whole goal of this project is to minimize the complexity or existence of controller tests, and provide
|
445
|
-
a way to unit test business logic.
|
446
|
-
|
447
|
-
In the following examples, I'll be using RSpec -- but there isn't anything that would prevent you from using a different testing framework, if you so choose.
|
448
|
-
|
449
|
-
### Operations
|
450
|
-
|
451
|
-
```ruby
|
452
|
-
describe HostOperations do
|
453
|
-
describe HostOperations::Read do
|
454
|
-
context 'model_from_params' do
|
455
|
-
let(:subdomain){ 'subdomain' }
|
456
|
-
# an operation takes a user, and a list of params
|
457
|
-
# there are optional parameters as well, but generally may not be required.
|
458
|
-
# see: `SkinnyControllers:::Operation::Base`
|
459
|
-
let(:operation){ HostOperations::Read.new(nil, { subdomain: subdomain }) }
|
460
|
-
|
461
|
-
it 'finds an event' do
|
462
|
-
host = create(:host, domain: subdomain)
|
463
|
-
model = operation.run
|
464
|
-
expect(model).to eq host
|
465
|
-
end
|
466
|
-
#...
|
467
|
-
```
|
468
|
-
|
469
|
-
### Policies
|
470
|
-
|
471
|
-
With policies, I like to test using Procs, because the setup is the same for most actions, and it's easier to set up different scenarios.
|
472
|
-
|
473
|
-
```ruby
|
474
|
-
describe PackagePolicy do
|
475
|
-
# will test if the owner of this object can access it
|
476
|
-
let(:by_owner){
|
477
|
-
->(method){
|
478
|
-
package = create(:package)
|
479
|
-
# a policy takes a user and an object
|
480
|
-
policy = PackagePolicy.new(package.event.hosted_by, package)
|
481
|
-
policy.send(method)
|
482
|
-
}
|
483
|
-
}
|
484
|
-
|
485
|
-
# will test if the person registering with this package has permission
|
486
|
-
let(:by_registrant){
|
487
|
-
->(method){
|
488
|
-
event = create(:event)
|
489
|
-
package = create(:package, event: event)
|
490
|
-
attendance = create(:attendance, host: event, package: package)
|
491
|
-
# a policy takes a user and an object
|
492
|
-
policy = PackagePolicy.new(attendance.attendee, package)
|
493
|
-
policy.send(method)
|
494
|
-
}
|
495
|
-
}
|
496
|
-
|
497
|
-
context 'can be read?' do
|
498
|
-
it 'by the event owner' do
|
499
|
-
result = by_owner.call(:read?)
|
500
|
-
expect(result).to eq true
|
501
|
-
end
|
502
|
-
|
503
|
-
it 'by a registrant' do
|
504
|
-
result = by_registrant.call(:read?)
|
505
|
-
expect(result).to eq true
|
506
|
-
end
|
507
|
-
end
|
508
|
-
|
509
|
-
context 'can be updated?' do
|
510
|
-
it 'by the event owner' do
|
511
|
-
result = by_owner.call(:update?)
|
512
|
-
expect(result).to eq true
|
513
|
-
end
|
514
|
-
|
515
|
-
it 'by a registrant' do
|
516
|
-
result = by_registrant.call(:update?)
|
517
|
-
expect(result).to eq false
|
518
|
-
end
|
519
|
-
end
|
520
|
-
```
|
521
|
-
|
522
|
-
|
523
|
-
## Globally Configurable Options
|
524
|
-
|
525
|
-
The following options are available:
|
526
|
-
|
527
|
-
|Option|Default|Note|
|
528
|
-
|------|-------|----|
|
529
|
-
|`operations_namespace` | '' | Optional namespace to put all the operations in. |
|
530
|
-
|`operations_suffix`|`'Operations'` | Default suffix for the operations namespaces. |
|
531
|
-
|`policy_suffix`|`'Policy'` | Default suffix for policies classes. |
|
532
|
-
|`controller_namespace`|`''`| Global Namespace for all controllers (e.g.: `'API'`) |
|
533
|
-
|`allow_by_default`| `true` | Default permission |
|
534
|
-
|`action_map`| see [skinny_controllers.rb](./lib/skinny_controllers.rb#L61)| |
|
535
|
-
| `search_proc`| passthrough | can be used to filter results, such as with using ransack |
|
536
|
-
|
537
|
-
|
538
105
|
-------------------------------------------------------
|
539
106
|
|
540
107
|
## How is this different from trailblazer?
|
@@ -549,3 +116,14 @@ This may not be horribly apparent, but here is a table overviewing some highleve
|
|
549
116
|
| Additional objects|-| none | contacts, representers, callbacks, cells |
|
550
117
|
| Rendering |-| done in the controller, and up to the dev to decide how that is done. `ActiveModel::Serializers` with JSON-API is highly recommended |-| representers provide a way to define serializers for json, xml, json-api, etc |
|
551
118
|
| App Structure |-| same as rails. `app/operations` and `app/policies` are added | encourages a new structure 'concepts', where cells, view templates, assets, operations, etc are all under `concepts/{model-name}` |
|
119
|
+
|
120
|
+
|
121
|
+
# Contributing
|
122
|
+
|
123
|
+
Please refer to each project's style guidelines and guidelines for submitting patches and additions. In general, we follow the "fork-and-pull" Git workflow.
|
124
|
+
|
125
|
+
1. **Fork** the repo on GitHub
|
126
|
+
2. **Clone** the project to your own machine
|
127
|
+
3. **Commit** changes to your own branch
|
128
|
+
4. **Push** your work back up to your fork
|
129
|
+
5. Submit a **Pull request** so that we can review your changes
|
@@ -77,7 +77,9 @@ module SkinnyControllers
|
|
77
77
|
|
78
78
|
# In order of most specific, to least specific:
|
79
79
|
# - {action}_{model_name}_params
|
80
|
-
# - {
|
80
|
+
# - {action}_params
|
81
|
+
# - {model_key}_params
|
82
|
+
# - resource_params
|
81
83
|
# - params
|
82
84
|
#
|
83
85
|
# It's recommended to use whitelisted strong parameters on
|
@@ -100,16 +102,26 @@ module SkinnyControllers
|
|
100
102
|
_lookup.model_name.underscore
|
101
103
|
end
|
102
104
|
|
103
|
-
|
104
|
-
|
105
|
+
params_lookups = [
|
106
|
+
# e.g.: create_post_params
|
107
|
+
"#{action_name}_#{model_key}_params",
|
108
|
+
# generic for action
|
109
|
+
"#{action_name}_params",
|
110
|
+
# e.g.: post_params
|
111
|
+
"#{model_key}_params",
|
112
|
+
# most generic
|
113
|
+
'resource_params'
|
114
|
+
]
|
115
|
+
|
116
|
+
lookup_params_for_action(params_lookups)
|
117
|
+
end
|
105
118
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
send(model_params_method)
|
110
|
-
else
|
111
|
-
params
|
119
|
+
def lookup_params_for_action(lookups)
|
120
|
+
lookups.each do |method_name|
|
121
|
+
return send(method_name) if respond_to?(method_name, true)
|
112
122
|
end
|
123
|
+
|
124
|
+
params
|
113
125
|
end
|
114
126
|
|
115
127
|
# action name is inherited from ActionController::Base
|
@@ -64,10 +64,6 @@ module SkinnyControllers
|
|
64
64
|
# TODO: think of a way to override the authorized_via_parent functionality
|
65
65
|
def read_all?
|
66
66
|
return true if authorized_via_parent
|
67
|
-
|
68
|
-
# Might be deceptive...
|
69
|
-
return true if object.nil? || object.empty?
|
70
|
-
|
71
67
|
# This is expensive, so try to avoid it
|
72
68
|
# TODO: look in to creating a cache for
|
73
69
|
# these look ups that's invalidated upon
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: skinny_controllers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.10.
|
4
|
+
version: 0.10.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- L. Preston Sego III
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-07-
|
11
|
+
date: 2017-07-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -235,8 +235,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
235
235
|
version: '0'
|
236
236
|
requirements: []
|
237
237
|
rubyforge_project:
|
238
|
-
rubygems_version: 2.6.
|
238
|
+
rubygems_version: 2.6.8
|
239
239
|
signing_key:
|
240
240
|
specification_version: 4
|
241
|
-
summary: SkinnyControllers-0.10.
|
241
|
+
summary: SkinnyControllers-0.10.7
|
242
242
|
test_files: []
|