rest_framework 0.9.15 → 0.10.0
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/VERSION +1 -1
- data/app/views/layouts/rest_framework.html.erb +1 -2
- data/app/views/rest_framework/_head.html.erb +3 -2
- data/app/views/rest_framework/_heading.html.erb +1 -1
- data/app/views/rest_framework/routes_and_forms/_html_form.html.erb +3 -3
- data/app/views/rest_framework/routes_and_forms/_raw_form.html.erb +1 -1
- data/lib/rest_framework/filters/base_filter.rb +1 -1
- data/lib/rest_framework/filters/{model_ordering_filter.rb → ordering_filter.rb} +6 -3
- data/lib/rest_framework/filters/{model_query_filter.rb → query_filter.rb} +10 -7
- data/lib/rest_framework/filters/ransack_filter.rb +1 -1
- data/lib/rest_framework/filters/{model_search_filter.rb → search_filter.rb} +6 -4
- data/lib/rest_framework/filters.rb +3 -3
- data/lib/rest_framework/mixins/base_controller_mixin.rb +42 -62
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +1 -1
- data/lib/rest_framework/mixins/model_controller_mixin.rb +117 -90
- data/lib/rest_framework/serializers/native_serializer.rb +16 -5
- data/lib/rest_framework/utils.rb +38 -18
- data/lib/rest_framework.rb +16 -16
- data/vendor/assets/javascripts/rest_framework/external.min.js +618 -575
- data/vendor/assets/stylesheets/rest_framework/external.min.css +12 -19
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a916afbf32cf43553b8aa952024011639f311ab80db53685163e607e48e1ad0
|
4
|
+
data.tar.gz: 1cc8aa7aed712a63b1ef9586c37b64a87de62fe5dfdcf659e982a1b77c20fd89
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bef1f7c3c52c7a42e8c307ccaf37e4e25f4e663359e26be433f20fd644fca0e59cd3b56613f965b084662046577576728981a320d1d5a5a6517d98556008fc99
|
7
|
+
data.tar.gz: '080f96e20b330a391b896db89a192e415328aa320f8d669260d8d282c0eee709022afa7fa5dd484449eb5bba85544f8974d24764a5e5b45821803d1ccd3c23f0'
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.10.0
|
@@ -3,11 +3,10 @@
|
|
3
3
|
<head>
|
4
4
|
<title><%= @title %></title>
|
5
5
|
|
6
|
-
<%# These dynamic tags cannot be cached, so include directly instead of in `head` partial. %>
|
7
6
|
<%= csrf_meta_tags %>
|
8
7
|
<%= csp_meta_tag rescue nil %>
|
9
8
|
|
10
|
-
<%= render partial: "rest_framework/head"
|
9
|
+
<%= render partial: "rest_framework/head" %>
|
11
10
|
|
12
11
|
<%= yield :head %>
|
13
12
|
</head>
|
@@ -296,8 +296,9 @@
|
|
296
296
|
document.write(content)
|
297
297
|
document.close()
|
298
298
|
|
299
|
-
//
|
300
|
-
|
299
|
+
// It seems that `DOMContentLoaded` is already triggered on `document.close()`.
|
300
|
+
// // Trigger `DOMContentLoaded` manually so our custom JavaScript works.
|
301
|
+
// // document.dispatchEvent(new Event("DOMContentLoaded", {bubbles: true, cancelable: true}))
|
301
302
|
}
|
302
303
|
|
303
304
|
// Refresh the window as a `GET` request.
|
@@ -3,7 +3,7 @@
|
|
3
3
|
<%= render partial: "rest_framework/heading/actions" if @route_groups.present? %>
|
4
4
|
<h1 style="margin: 0"><%= @heading_title || @title %></h1>
|
5
5
|
<% if @description.present? %>
|
6
|
-
<
|
6
|
+
<p style="display: inline-block; margin-bottom: 0; margin-top: 1em"><%= @description %></p>
|
7
7
|
<% end %>
|
8
8
|
</div>
|
9
9
|
</div>
|
@@ -26,14 +26,14 @@
|
|
26
26
|
%>
|
27
27
|
<div class="mb-2">
|
28
28
|
<% if metadata[:kind] == "rich_text" %>
|
29
|
-
<label class="form-label w-100"><%= controller.class.
|
29
|
+
<label class="form-label w-100"><%= controller.class.label_for(f) %></label>
|
30
30
|
<%= form.rich_text_area f %>
|
31
31
|
<% elsif metadata[:kind] == "attachment" %>
|
32
|
-
<label class="form-label w-100"><%= controller.class.
|
32
|
+
<label class="form-label w-100"><%= controller.class.label_for(f) %>
|
33
33
|
<%= form.file_field f, multiple: metadata.dig(:attachment, :macro) == :has_many_attached %>
|
34
34
|
</label>
|
35
35
|
<% else %>
|
36
|
-
<label class="form-label w-100"><%= controller.class.
|
36
|
+
<label class="form-label w-100"><%= controller.class.label_for(f) %>
|
37
37
|
<%= form.text_field f, class: "form-control form-control-sm" %>
|
38
38
|
</label>
|
39
39
|
<% end %>
|
@@ -40,7 +40,7 @@
|
|
40
40
|
local: true,
|
41
41
|
}.compact) do |form| %>
|
42
42
|
<% attachment_reflections.each do |field, ref| %>
|
43
|
-
<label class="form-label w-100"><%= controller.class.
|
43
|
+
<label class="form-label w-100"><%= controller.class.label_for(field) %>
|
44
44
|
<%= form.file_field field, multiple: ref.macro == :has_many_attached %>
|
45
45
|
</label>
|
46
46
|
<% end %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# A filter backend which handles ordering of the recordset.
|
2
|
-
class RESTFramework::Filters::
|
2
|
+
class RESTFramework::Filters::OrderingFilter < RESTFramework::Filters::BaseFilter
|
3
3
|
# Get a list of ordering fields for the current action.
|
4
4
|
def _get_fields
|
5
5
|
return @controller.ordering_fields&.map(&:to_s) || @controller.get_fields
|
@@ -38,7 +38,7 @@ class RESTFramework::Filters::ModelOrderingFilter < RESTFramework::Filters::Base
|
|
38
38
|
end
|
39
39
|
|
40
40
|
# Order data according to the request query parameters.
|
41
|
-
def
|
41
|
+
def filter_data(data)
|
42
42
|
ordering = self._get_ordering
|
43
43
|
reorder = !@controller.ordering_no_reorder
|
44
44
|
|
@@ -51,4 +51,7 @@ class RESTFramework::Filters::ModelOrderingFilter < RESTFramework::Filters::Base
|
|
51
51
|
end
|
52
52
|
|
53
53
|
# Alias for convenience.
|
54
|
-
RESTFramework::
|
54
|
+
RESTFramework::OrderingFilter = RESTFramework::Filters::OrderingFilter
|
55
|
+
|
56
|
+
# TODO: Compatibility; remove in 1.0.
|
57
|
+
RESTFramework::ModelOrderingFilter = RESTFramework::Filters::OrderingFilter
|
@@ -1,14 +1,14 @@
|
|
1
1
|
# A simple filtering backend that supports filtering a recordset based on query parameters.
|
2
|
-
class RESTFramework::Filters::
|
2
|
+
class RESTFramework::Filters::QueryFilter < RESTFramework::Filters::BaseFilter
|
3
3
|
NIL_VALUES = ["nil", "null"].freeze
|
4
4
|
|
5
|
-
# Get a list of
|
5
|
+
# Get a list of filter fields for the current action.
|
6
6
|
def _get_fields
|
7
7
|
# Always return a list of strings; `@controller.get_fields` already does this.
|
8
|
-
return @controller.class.
|
8
|
+
return @controller.class.filter_fields&.map(&:to_s) || @controller.get_fields
|
9
9
|
end
|
10
10
|
|
11
|
-
# Filter params for keys allowed by the current action's
|
11
|
+
# Filter params for keys allowed by the current action's filter_fields/fields config.
|
12
12
|
def _get_filter_params
|
13
13
|
fields = self._get_fields
|
14
14
|
includes = []
|
@@ -22,7 +22,7 @@ class RESTFramework::Filters::ModelQueryFilter < RESTFramework::Filters::BaseFil
|
|
22
22
|
field, sub_field = match[1..2]
|
23
23
|
next false unless field.in?(fields)
|
24
24
|
|
25
|
-
sub_fields = @controller.class.
|
25
|
+
sub_fields = @controller.class.field_config_for(field)[:sub_fields] || []
|
26
26
|
if sub_field.in?(sub_fields)
|
27
27
|
includes << field.to_sym
|
28
28
|
next true
|
@@ -49,7 +49,7 @@ class RESTFramework::Filters::ModelQueryFilter < RESTFramework::Filters::BaseFil
|
|
49
49
|
end
|
50
50
|
|
51
51
|
# Filter data according to the request query parameters.
|
52
|
-
def
|
52
|
+
def filter_data(data)
|
53
53
|
filter_params, includes = self._get_filter_params
|
54
54
|
|
55
55
|
if filter_params.any?
|
@@ -65,4 +65,7 @@ class RESTFramework::Filters::ModelQueryFilter < RESTFramework::Filters::BaseFil
|
|
65
65
|
end
|
66
66
|
|
67
67
|
# Alias for convenience.
|
68
|
-
RESTFramework::
|
68
|
+
RESTFramework::QueryFilter = RESTFramework::Filters::QueryFilter
|
69
|
+
|
70
|
+
# TODO: Compatibility; remove in 1.0.
|
71
|
+
RESTFramework::ModelQueryFilter = RESTFramework::Filters::QueryFilter
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# Adapter for the `ransack` gem.
|
2
2
|
class RESTFramework::Filters::RansackFilter < RESTFramework::Filters::BaseFilter
|
3
3
|
# Filter data according to the request query parameters.
|
4
|
-
def
|
4
|
+
def filter_data(data)
|
5
5
|
q = @controller.request.query_parameters[@controller.ransack_query_param]
|
6
6
|
|
7
7
|
if q.present?
|
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
class RESTFramework::Filters::ModelSearchFilter < RESTFramework::Filters::BaseFilter
|
1
|
+
class RESTFramework::Filters::SearchFilter < RESTFramework::Filters::BaseFilter
|
3
2
|
# Get a list of search fields for the current action.
|
4
3
|
def _get_fields
|
5
4
|
if search_fields = @controller.search_fields
|
@@ -13,7 +12,7 @@ class RESTFramework::Filters::ModelSearchFilter < RESTFramework::Filters::BaseFi
|
|
13
12
|
end
|
14
13
|
|
15
14
|
# Filter data according to the request query parameters.
|
16
|
-
def
|
15
|
+
def filter_data(data)
|
17
16
|
search = @controller.request.query_parameters[@controller.search_query_param]
|
18
17
|
|
19
18
|
if search.present?
|
@@ -41,4 +40,7 @@ class RESTFramework::Filters::ModelSearchFilter < RESTFramework::Filters::BaseFi
|
|
41
40
|
end
|
42
41
|
|
43
42
|
# Alias for convenience.
|
44
|
-
RESTFramework::
|
43
|
+
RESTFramework::SearchFilter = RESTFramework::Filters::SearchFilter
|
44
|
+
|
45
|
+
# TODO: Compatibility; remove in 1.0.
|
46
|
+
RESTFramework::ModelSearchFilter = RESTFramework::Filters::SearchFilter
|
@@ -3,7 +3,7 @@ end
|
|
3
3
|
|
4
4
|
require_relative "filters/base_filter"
|
5
5
|
|
6
|
-
require_relative "filters/
|
7
|
-
require_relative "filters/
|
8
|
-
require_relative "filters/model_search_filter"
|
6
|
+
require_relative "filters/ordering_filter"
|
7
|
+
require_relative "filters/query_filter"
|
9
8
|
require_relative "filters/ransack_filter"
|
9
|
+
require_relative "filters/search_filter"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# This module provides the common functionality for any controller mixins, a `root` action, and
|
2
|
-
# the ability to route arbitrary actions with `extra_actions`. This is also where `
|
3
|
-
#
|
2
|
+
# the ability to route arbitrary actions with `extra_actions`. This is also where `render_api` is
|
3
|
+
# implemented.
|
4
4
|
module RESTFramework::Mixins::BaseControllerMixin
|
5
5
|
RRF_BASE_CONFIG = {
|
6
6
|
extra_actions: nil,
|
@@ -11,9 +11,6 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
11
11
|
title: nil,
|
12
12
|
description: nil,
|
13
13
|
inflect_acronyms: ["ID", "IDs", "REST", "API", "APIs"].freeze,
|
14
|
-
}
|
15
|
-
RRF_BASE_INSTANCE_CONFIG = {
|
16
|
-
filter_backends: nil,
|
17
14
|
|
18
15
|
# Options related to serialization.
|
19
16
|
rescue_unknown_format_with: :json,
|
@@ -21,6 +18,11 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
21
18
|
serialize_to_json: true,
|
22
19
|
serialize_to_xml: true,
|
23
20
|
|
21
|
+
# Custom integrations (reduces serializer performance due to method calls).
|
22
|
+
enable_action_text: false,
|
23
|
+
enable_active_storage: false,
|
24
|
+
}
|
25
|
+
RRF_BASE_INSTANCE_CONFIG = {
|
24
26
|
# Options related to pagination.
|
25
27
|
paginator_class: nil,
|
26
28
|
page_size: 20,
|
@@ -35,12 +37,12 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
35
37
|
|
36
38
|
# Default action for API root.
|
37
39
|
def root
|
38
|
-
|
40
|
+
render_api({message: "This is the API root."})
|
39
41
|
end
|
40
42
|
|
41
43
|
module ClassMethods
|
42
|
-
#
|
43
|
-
#
|
44
|
+
# By default, this is the name of the controller class, titleized and with any custom inflection
|
45
|
+
# acronyms applied.
|
44
46
|
def get_title
|
45
47
|
return self.title || RESTFramework::Utils.inflect(
|
46
48
|
self.name.demodulize.chomp("Controller").titleize(keep_id_suffix: true),
|
@@ -49,7 +51,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
49
51
|
end
|
50
52
|
|
51
53
|
# Get a label from a field/column name, titleized and inflected.
|
52
|
-
def
|
54
|
+
def label_for(s)
|
53
55
|
return RESTFramework::Utils.inflect(
|
54
56
|
s.to_s.titleize(keep_id_suffix: true),
|
55
57
|
self.inflect_acronyms,
|
@@ -57,7 +59,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
57
59
|
end
|
58
60
|
|
59
61
|
# Collect actions (including extra actions) metadata for this controller.
|
60
|
-
def
|
62
|
+
def actions_metadata
|
61
63
|
actions = {}
|
62
64
|
|
63
65
|
# Start with builtin actions.
|
@@ -67,7 +69,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
67
69
|
next unless self.method_defined?(action)
|
68
70
|
|
69
71
|
actions[action] = {
|
70
|
-
path: "", methods: methods, type: :builtin, metadata: {label: self.
|
72
|
+
path: "", methods: methods, type: :builtin, metadata: {label: self.label_for(action)}
|
71
73
|
}
|
72
74
|
end
|
73
75
|
|
@@ -76,7 +78,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
76
78
|
next unless self.method_defined?(action)
|
77
79
|
|
78
80
|
actions[action] = {
|
79
|
-
path: "", methods: methods, type: :builtin, metadata: {label: self.
|
81
|
+
path: "", methods: methods, type: :builtin, metadata: {label: self.label_for(action)}
|
80
82
|
}
|
81
83
|
end
|
82
84
|
|
@@ -89,7 +91,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
89
91
|
end
|
90
92
|
|
91
93
|
# Collect member actions (including extra member actions) metadata for this controller.
|
92
|
-
def
|
94
|
+
def member_actions_metadata
|
93
95
|
actions = {}
|
94
96
|
|
95
97
|
# Start with builtin actions.
|
@@ -97,7 +99,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
97
99
|
next unless self.method_defined?(action)
|
98
100
|
|
99
101
|
actions[action] = {
|
100
|
-
path: "", methods: methods, type: :builtin, metadata: {label: self.
|
102
|
+
path: "", methods: methods, type: :builtin, metadata: {label: self.label_for(action)}
|
101
103
|
}
|
102
104
|
end
|
103
105
|
|
@@ -109,8 +111,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
109
111
|
return actions
|
110
112
|
end
|
111
113
|
|
112
|
-
|
113
|
-
def get_options_metadata
|
114
|
+
def options_metadata
|
114
115
|
return {
|
115
116
|
title: self.get_title,
|
116
117
|
description: self.description,
|
@@ -119,8 +120,8 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
119
120
|
self.serialize_to_json ? "application/json" : nil,
|
120
121
|
self.serialize_to_xml ? "application/xml" : nil,
|
121
122
|
].compact,
|
122
|
-
actions: self.
|
123
|
-
member_actions: self.
|
123
|
+
actions: self.actions_metadata,
|
124
|
+
member_actions: self.member_actions_metadata,
|
124
125
|
}.compact
|
125
126
|
end
|
126
127
|
|
@@ -203,47 +204,24 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
203
204
|
end
|
204
205
|
end
|
205
206
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
# Support dynamically resolving serializer given a symbol or string.
|
211
|
-
serializer_class = serializer_class.to_s if serializer_class.is_a?(Symbol)
|
212
|
-
if serializer_class.is_a?(String)
|
213
|
-
serializer_class = self.class.const_get(serializer_class)
|
214
|
-
end
|
215
|
-
|
216
|
-
# Wrap it with an adapter if it's an active_model_serializer.
|
217
|
-
if defined?(ActiveModel::Serializer) && (serializer_class < ActiveModel::Serializer)
|
218
|
-
serializer_class = RESTFramework::ActiveModelSerializerAdapterFactory.for(serializer_class)
|
207
|
+
def serializer_class
|
208
|
+
# TODO: Compatibility; remove in 1.0.
|
209
|
+
if klass = self.try(:get_serializer_class)
|
210
|
+
return klass
|
219
211
|
end
|
220
212
|
|
221
|
-
return serializer_class
|
213
|
+
return self.class.serializer_class
|
222
214
|
end
|
223
215
|
|
224
216
|
# Serialize the given data using the `serializer_class`.
|
225
217
|
def serialize(data, **kwargs)
|
226
|
-
return self.
|
218
|
+
return RESTFramework::Utils.wrap_ams(self.serializer_class).new(
|
219
|
+
data, controller: self, **kwargs
|
220
|
+
).serialize
|
227
221
|
end
|
228
222
|
|
229
|
-
|
230
|
-
|
231
|
-
return self.filter_backends || []
|
232
|
-
end
|
233
|
-
|
234
|
-
# Filter an arbitrary data set over all configured filter backends.
|
235
|
-
def get_filtered_data(data)
|
236
|
-
# Apply each filter sequentially.
|
237
|
-
self.get_filter_backends.each do |filter_class|
|
238
|
-
filter = filter_class.new(controller: self)
|
239
|
-
data = filter.get_filtered_data(data)
|
240
|
-
end
|
241
|
-
|
242
|
-
return data
|
243
|
-
end
|
244
|
-
|
245
|
-
def get_options_metadata
|
246
|
-
return self.class.get_options_metadata
|
223
|
+
def options_metadata
|
224
|
+
return self.class.options_metadata
|
247
225
|
end
|
248
226
|
|
249
227
|
def rrf_error_handler(e)
|
@@ -295,25 +273,25 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
295
273
|
begin
|
296
274
|
respond_to do |format|
|
297
275
|
if payload == ""
|
298
|
-
format.json { head(kwargs[:status] || :no_content) } if self.serialize_to_json
|
299
|
-
format.xml { head(kwargs[:status] || :no_content) } if self.serialize_to_xml
|
276
|
+
format.json { head(kwargs[:status] || :no_content) } if self.class.serialize_to_json
|
277
|
+
format.xml { head(kwargs[:status] || :no_content) } if self.class.serialize_to_xml
|
300
278
|
else
|
301
279
|
format.json {
|
302
280
|
render(json: payload, **kwargs.merge(json_kwargs))
|
303
|
-
} if self.serialize_to_json
|
281
|
+
} if self.class.serialize_to_json
|
304
282
|
format.xml {
|
305
283
|
render(xml: payload, **kwargs.merge(xml_kwargs))
|
306
|
-
} if self.serialize_to_xml
|
284
|
+
} if self.class.serialize_to_xml
|
307
285
|
# TODO: possibly support more formats here if supported?
|
308
286
|
end
|
309
287
|
format.html {
|
310
288
|
@payload = payload
|
311
289
|
if payload == ""
|
312
|
-
@json_payload = "" if self.serialize_to_json
|
313
|
-
@xml_payload = "" if self.serialize_to_xml
|
290
|
+
@json_payload = "" if self.class.serialize_to_json
|
291
|
+
@xml_payload = "" if self.class.serialize_to_xml
|
314
292
|
else
|
315
|
-
@json_payload = payload.to_json if self.serialize_to_json
|
316
|
-
@xml_payload = payload.to_xml if self.serialize_to_xml
|
293
|
+
@json_payload = payload.to_json if self.class.serialize_to_json
|
294
|
+
@xml_payload = payload.to_xml if self.class.serialize_to_xml
|
317
295
|
end
|
318
296
|
@title ||= self.class.get_title
|
319
297
|
@description ||= self.class.description
|
@@ -329,7 +307,7 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
329
307
|
}
|
330
308
|
end
|
331
309
|
rescue ActionController::UnknownFormat
|
332
|
-
if !already_rescued_unknown_format && rescue_format = self.rescue_unknown_format_with
|
310
|
+
if !already_rescued_unknown_format && rescue_format = self.class.rescue_unknown_format_with
|
333
311
|
request.format = rescue_format
|
334
312
|
already_rescued_unknown_format = true
|
335
313
|
retry
|
@@ -339,9 +317,11 @@ module RESTFramework::Mixins::BaseControllerMixin
|
|
339
317
|
end
|
340
318
|
end
|
341
319
|
|
342
|
-
#
|
320
|
+
# TODO: Might make this the default render method in the future.
|
321
|
+
alias_method :render_api, :api_response
|
322
|
+
|
343
323
|
def options
|
344
|
-
return api_response(self.
|
324
|
+
return api_response(self.options_metadata)
|
345
325
|
end
|
346
326
|
end
|
347
327
|
|
@@ -6,7 +6,7 @@ module RESTFramework::Mixins::BulkCreateModelMixin
|
|
6
6
|
# While bulk update/destroy are obvious because they create new router endpoints, bulk create
|
7
7
|
# overloads the existing collection `POST` endpoint, so we add a special key to the options
|
8
8
|
# metadata to indicate bulk create is supported.
|
9
|
-
def
|
9
|
+
def options_metadata
|
10
10
|
return super.merge({bulk_create: true})
|
11
11
|
end
|
12
12
|
|