habarico 7.2.0.pre.beta.5
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/Rakefile +15 -0
- data/VERSION +1 -0
- data/app/controllers/habarico/action_schema_controller.rb +41 -0
- data/app/controllers/habarico/application_controller.rb +15 -0
- data/app/controllers/habarico/contrib/preferences/user_preferences_controller.rb +105 -0
- data/app/controllers/habarico/global_schema_controller.rb +11 -0
- data/app/controllers/habarico/local_schema_controller.rb +35 -0
- data/app/helpers/habarico/application_helper.rb +4 -0
- data/app/models/habarico/application_record.rb +5 -0
- data/app/views/layouts/habarico/application.html.erb +17 -0
- data/config/locales/cs.yml +39 -0
- data/config/locales/de.yml +39 -0
- data/config/locales/en.yml +39 -0
- data/config/locales/es.yml +39 -0
- data/config/locales/fr.yml +39 -0
- data/config/locales/it.yml +39 -0
- data/config/locales/ja.yml +39 -0
- data/config/locales/nl.yml +39 -0
- data/config/locales/pl.yml +39 -0
- data/config/locales/pt.yml +39 -0
- data/config/locales/ro.yml +39 -0
- data/config/locales/sk.yml +39 -0
- data/config/locales/uk.yml +39 -0
- data/config/routes.rb +1 -0
- data/lib/generators/habarico/preferences/install_generator.rb +27 -0
- data/lib/generators/habarico/preferences/templates/create_habarico_user_preferences.rb.tt +15 -0
- data/lib/habarico/active_model/references.rb +68 -0
- data/lib/habarico/authenticatable.rb +31 -0
- data/lib/habarico/config.rb +71 -0
- data/lib/habarico/context.rb +126 -0
- data/lib/habarico/contrib/preferences/base_preferences.rb +40 -0
- data/lib/habarico/contrib/preferences/config.rb +31 -0
- data/lib/habarico/contrib/preferences/errors.rb +79 -0
- data/lib/habarico/contrib/preferences/locales/cs.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/de.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/en.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/es.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/fr.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/it.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/ja.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/nl.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/pl.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/pt.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/ro.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/sk.yml +8 -0
- data/lib/habarico/contrib/preferences/locales/uk.yml +8 -0
- data/lib/habarico/contrib/preferences/registry.rb +293 -0
- data/lib/habarico/contrib/preferences/routes.rb +17 -0
- data/lib/habarico/contrib/preferences/schemas.rb +129 -0
- data/lib/habarico/contrib/preferences/serializer.rb +19 -0
- data/lib/habarico/contrib/preferences/user_preference.rb +16 -0
- data/lib/habarico/contrib/preferences.rb +32 -0
- data/lib/habarico/controller.rb +584 -0
- data/lib/habarico/definition/action_reference.rb +18 -0
- data/lib/habarico/definition/actions/action.rb +14 -0
- data/lib/habarico/definition/actions/create_action.rb +18 -0
- data/lib/habarico/definition/actions/delete_action.rb +20 -0
- data/lib/habarico/definition/actions/edit_action.rb +18 -0
- data/lib/habarico/definition/actions/link_action.rb +11 -0
- data/lib/habarico/definition/actions/resource_action.rb +12 -0
- data/lib/habarico/definition/actions/save_action.rb +14 -0
- data/lib/habarico/definition/actions/show_action.rb +18 -0
- data/lib/habarico/definition/actions/upload_images_action.rb +11 -0
- data/lib/habarico/definition/actions/view_action.rb +11 -0
- data/lib/habarico/definition/boxes/array_box.rb +10 -0
- data/lib/habarico/definition/boxes/box.rb +32 -0
- data/lib/habarico/definition/boxes/gallery_box.rb +10 -0
- data/lib/habarico/definition/boxes/grid_box.rb +10 -0
- data/lib/habarico/definition/boxes/grid_item_box.rb +10 -0
- data/lib/habarico/definition/boxes/list_box.rb +10 -0
- data/lib/habarico/definition/boxes/object_box.rb +10 -0
- data/lib/habarico/definition/boxes/tab_box.rb +10 -0
- data/lib/habarico/definition/boxes/tab_item_box.rb +10 -0
- data/lib/habarico/definition/boxes/wizard_box.rb +10 -0
- data/lib/habarico/definition/concerns/definition_model.rb +38 -0
- data/lib/habarico/definition/definition.rb +206 -0
- data/lib/habarico/definition/field_reference.rb +9 -0
- data/lib/habarico/definition/filter_reference.rb +7 -0
- data/lib/habarico/definition/menu/link_menu_item.rb +12 -0
- data/lib/habarico/definition/menu/menu_item.rb +9 -0
- data/lib/habarico/definition/menu/text_menu_item.rb +12 -0
- data/lib/habarico/definition/menu/view_menu_item.rb +13 -0
- data/lib/habarico/definition/response.rb +13 -0
- data/lib/habarico/definition/views/action_view.rb +11 -0
- data/lib/habarico/definition/views/create_view.rb +15 -0
- data/lib/habarico/definition/views/edit_view.rb +15 -0
- data/lib/habarico/definition/views/list_view.rb +17 -0
- data/lib/habarico/definition/views/show_view.rb +15 -0
- data/lib/habarico/definition/views/view.rb +10 -0
- data/lib/habarico/engine.rb +6 -0
- data/lib/habarico/file_validations.rb +119 -0
- data/lib/habarico/generators/abstract_generator.rb +21 -0
- data/lib/habarico/generators/action/action_schema_generator.rb +111 -0
- data/lib/habarico/generators/action/confirm_dialog_properties_generator.rb +30 -0
- data/lib/habarico/generators/action/global_resource_action_schema_generator.rb +46 -0
- data/lib/habarico/generators/action/link_action_parameters_generator.rb +31 -0
- data/lib/habarico/generators/action/resource_action_endpoint_parameters_generator.rb +70 -0
- data/lib/habarico/generators/action/resource_action_parameters_generator.rb +40 -0
- data/lib/habarico/generators/action/resource_action_schema_generator.rb +51 -0
- data/lib/habarico/generators/action/save_action_parameters_generator.rb +26 -0
- data/lib/habarico/generators/action/upload_images_action_parameters_generator.rb +41 -0
- data/lib/habarico/generators/action/view_action_endpoint_parameters_generator.rb +37 -0
- data/lib/habarico/generators/action/view_action_parameters_generator.rb +56 -0
- data/lib/habarico/generators/endpoint/dynamic_render_options_generator.rb +26 -0
- data/lib/habarico/generators/endpoint/endpoints_schema_generator.rb +61 -0
- data/lib/habarico/generators/endpoint/enum_type_generator.rb +45 -0
- data/lib/habarico/generators/endpoint/field_options_generator.rb +179 -0
- data/lib/habarico/generators/endpoint/field_schema_generator.rb +381 -0
- data/lib/habarico/generators/endpoint/fields_schema_generator.rb +35 -0
- data/lib/habarico/generators/endpoint/filter_schema_generator.rb +201 -0
- data/lib/habarico/generators/endpoint/filters_schema_generator.rb +25 -0
- data/lib/habarico/generators/endpoint/method_schema_generator.rb +34 -0
- data/lib/habarico/generators/endpoint/number_field_options_generator.rb +45 -0
- data/lib/habarico/generators/endpoint/phone_field_options_generator.rb +29 -0
- data/lib/habarico/generators/endpoint/relation_field_options_generator.rb +90 -0
- data/lib/habarico/generators/endpoint/search_filter_schema_generator.rb +44 -0
- data/lib/habarico/generators/menu/menu_group_schema_generator.rb +63 -0
- data/lib/habarico/generators/menu/menu_link_parameters_generator.rb +31 -0
- data/lib/habarico/generators/menu/menu_schema_generator.rb +105 -0
- data/lib/habarico/generators/menu/menu_view_endpoint_parameters_generator.rb +44 -0
- data/lib/habarico/generators/menu/menu_view_parameters_generator.rb +50 -0
- data/lib/habarico/generators/resource/action_resource_schema_generator.rb +84 -0
- data/lib/habarico/generators/resource/global_resource_schema_generator.rb +70 -0
- data/lib/habarico/generators/resource/local_resource_schema_generator.rb +111 -0
- data/lib/habarico/generators/resource/object_resource_schema_generator.rb +49 -0
- data/lib/habarico/generators/resource/resource_schema_generator.rb +31 -0
- data/lib/habarico/generators/view/action_descriptor_generator.rb +88 -0
- data/lib/habarico/generators/view/array_box_parameters_generator.rb +34 -0
- data/lib/habarico/generators/view/box_options_generator.rb +23 -0
- data/lib/habarico/generators/view/box_schema_generator.rb +194 -0
- data/lib/habarico/generators/view/detail_view_parameters_generator.rb +57 -0
- data/lib/habarico/generators/view/field_descriptor_generator.rb +107 -0
- data/lib/habarico/generators/view/filter_descriptor_generator.rb +75 -0
- data/lib/habarico/generators/view/gallery_box_parameters_generator.rb +71 -0
- data/lib/habarico/generators/view/global_view_schema_generator.rb +59 -0
- data/lib/habarico/generators/view/grid_item_box_parameters_generator.rb +44 -0
- data/lib/habarico/generators/view/list_box_parameters_generator.rb +124 -0
- data/lib/habarico/generators/view/list_view_parameters_generator.rb +120 -0
- data/lib/habarico/generators/view/object_box_parameters_generator.rb +26 -0
- data/lib/habarico/generators/view/sort_descriptor_generator.rb +30 -0
- data/lib/habarico/generators/view/tab_box_parameters_generator.rb +26 -0
- data/lib/habarico/generators/view/view_endpoint_parameters_generator.rb +71 -0
- data/lib/habarico/generators/view/view_schema_generator.rb +87 -0
- data/lib/habarico/generators/view/wizard_box_parameters_generator.rb +32 -0
- data/lib/habarico/helpers/action_helper.rb +9 -0
- data/lib/habarico/helpers/attributes_helper.rb +67 -0
- data/lib/habarico/helpers/box_helper.rb +19 -0
- data/lib/habarico/helpers/callable_helper.rb +15 -0
- data/lib/habarico/helpers/filter_params_helper.rb +49 -0
- data/lib/habarico/helpers/order_by_helper.rb +56 -0
- data/lib/habarico/helpers/reflection_helper.rb +61 -0
- data/lib/habarico/helpers/sorting_helper.rb +136 -0
- data/lib/habarico/schema/action/action_schema.rb +46 -0
- data/lib/habarico/schema/action/confirm_dialog_properties.rb +13 -0
- data/lib/habarico/schema/action/global_resource_action_schema.rb +29 -0
- data/lib/habarico/schema/action/link_action_parameters.rb +15 -0
- data/lib/habarico/schema/action/resource_action_endpoint_parameters.rb +24 -0
- data/lib/habarico/schema/action/resource_action_parameters.rb +17 -0
- data/lib/habarico/schema/action/resource_action_schema.rb +14 -0
- data/lib/habarico/schema/action/save_action_parameters.rb +10 -0
- data/lib/habarico/schema/action/upload_images_action_parameters.rb +23 -0
- data/lib/habarico/schema/action/view_action_endpoint_parameters.rb +16 -0
- data/lib/habarico/schema/action/view_action_parameters.rb +27 -0
- data/lib/habarico/schema/endpoint/ai_field_options.rb +10 -0
- data/lib/habarico/schema/endpoint/box_options.rb +10 -0
- data/lib/habarico/schema/endpoint/choices_field_options.rb +10 -0
- data/lib/habarico/schema/endpoint/dynamic_render_options.rb +14 -0
- data/lib/habarico/schema/endpoint/endpoints_schema.rb +22 -0
- data/lib/habarico/schema/endpoint/enum_type.rb +21 -0
- data/lib/habarico/schema/endpoint/field_options.rb +56 -0
- data/lib/habarico/schema/endpoint/field_schema.rb +65 -0
- data/lib/habarico/schema/endpoint/filter_schema.rb +44 -0
- data/lib/habarico/schema/endpoint/image_field_options.rb +13 -0
- data/lib/habarico/schema/endpoint/key_value.rb +13 -0
- data/lib/habarico/schema/endpoint/method_schema.rb +13 -0
- data/lib/habarico/schema/endpoint/number_field_options.rb +24 -0
- data/lib/habarico/schema/endpoint/phone_field_options.rb +13 -0
- data/lib/habarico/schema/endpoint/relation_field_options.rb +39 -0
- data/lib/habarico/schema/extensible_enum.rb +20 -0
- data/lib/habarico/schema/menu/action_view_choice.rb +13 -0
- data/lib/habarico/schema/menu/menu_group_schema.rb +26 -0
- data/lib/habarico/schema/menu/menu_schema.rb +36 -0
- data/lib/habarico/schema/resource/global_resource_schema.rb +14 -0
- data/lib/habarico/schema/resource/local_resource_schema.rb +14 -0
- data/lib/habarico/schema/resource/object_resource_schema.rb +8 -0
- data/lib/habarico/schema/resource/resource_schema.rb +16 -0
- data/lib/habarico/schema/types.rb +102 -0
- data/lib/habarico/schema/view/action_descriptor.rb +30 -0
- data/lib/habarico/schema/view/array_box_parameters.rb +13 -0
- data/lib/habarico/schema/view/box_schema.rb +67 -0
- data/lib/habarico/schema/view/detail_view_parameters.rb +16 -0
- data/lib/habarico/schema/view/field_descriptor.rb +50 -0
- data/lib/habarico/schema/view/field_wrapper_properties.rb +13 -0
- data/lib/habarico/schema/view/filter_descriptor.rb +24 -0
- data/lib/habarico/schema/view/gallery_box_parameters.rb +31 -0
- data/lib/habarico/schema/view/global_view_schema.rb +25 -0
- data/lib/habarico/schema/view/grid_item_box_parameters.rb +13 -0
- data/lib/habarico/schema/view/list_box_parameters.rb +44 -0
- data/lib/habarico/schema/view/list_view_parameters.rb +31 -0
- data/lib/habarico/schema/view/object_box_parameters.rb +10 -0
- data/lib/habarico/schema/view/sort_descriptor.rb +15 -0
- data/lib/habarico/schema/view/sortable_parameters.rb +13 -0
- data/lib/habarico/schema/view/tab_box_parameters.rb +10 -0
- data/lib/habarico/schema/view/view_action_descriptor_parameters.rb +21 -0
- data/lib/habarico/schema/view/view_endpoint_parameters.rb +22 -0
- data/lib/habarico/schema/view/view_schema.rb +33 -0
- data/lib/habarico/schema/view/wizard_box_parameters.rb +13 -0
- data/lib/habarico/search/field_resolver.rb +138 -0
- data/lib/habarico/search/search_matcher.rb +152 -0
- data/lib/habarico/serializers/meta_serializer.rb +42 -0
- data/lib/habarico/serializers/record_serializer.rb +33 -0
- data/lib/habarico/serializers/serializer.rb +61 -0
- data/lib/habarico/services/resource_service.rb +69 -0
- data/lib/habarico/services/routing_service.rb +160 -0
- data/lib/habarico/services/schema_service.rb +72 -0
- data/lib/habarico/version.rb +5 -0
- data/lib/habarico.rb +262 -0
- metadata +324 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module Habarico
|
|
2
|
+
module Contrib
|
|
3
|
+
module Preferences
|
|
4
|
+
# Base class for all preference errors.
|
|
5
|
+
class PreferenceError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when a key is neither registered exactly nor matched by a pattern.
|
|
8
|
+
class PreferenceNotRegistered < PreferenceError
|
|
9
|
+
attr_reader :key
|
|
10
|
+
|
|
11
|
+
def initialize(key)
|
|
12
|
+
@key = key
|
|
13
|
+
super(%(Preference key "#{key}" is not registered.))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Raised when the same exact key is registered twice.
|
|
18
|
+
class PreferenceAlreadyRegistered < PreferenceError
|
|
19
|
+
attr_reader :key
|
|
20
|
+
|
|
21
|
+
def initialize(key)
|
|
22
|
+
@key = key
|
|
23
|
+
super(%(Preference key "#{key}" is already registered.))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Raised when a key does not match the allowed key format.
|
|
28
|
+
class InvalidPreferenceKey < PreferenceError
|
|
29
|
+
attr_reader :key
|
|
30
|
+
|
|
31
|
+
def initialize(key)
|
|
32
|
+
@key = key
|
|
33
|
+
super(
|
|
34
|
+
%(Preference key "#{key}" is invalid. Keys must match ) +
|
|
35
|
+
%("^[a-zA-Z][a-zA-Z0-9_./-]*$" (ASCII letters, digits, underscore, dot, ) +
|
|
36
|
+
%(slash, hyphen; must start with a letter).)
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Base for the size and count guards. Both surface as 422, like a schema mismatch.
|
|
42
|
+
class PreferenceLimitExceeded < PreferenceError; end
|
|
43
|
+
|
|
44
|
+
# Raised when a value serializes to more bytes than the key's limit allows.
|
|
45
|
+
class PreferenceValueTooLarge < PreferenceLimitExceeded
|
|
46
|
+
attr_reader :key, :limit, :actual
|
|
47
|
+
|
|
48
|
+
def initialize(key, limit, actual)
|
|
49
|
+
@key = key
|
|
50
|
+
@limit = limit
|
|
51
|
+
@actual = actual
|
|
52
|
+
super(%(Value for preference "#{key}" is #{actual} bytes, over the #{limit} byte limit.))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Raised when storing a new key would take a user past `max_keys_per_user`.
|
|
57
|
+
class TooManyPreferences < PreferenceLimitExceeded
|
|
58
|
+
attr_reader :key, :limit
|
|
59
|
+
|
|
60
|
+
def initialize(key, limit)
|
|
61
|
+
@key = key
|
|
62
|
+
@limit = limit
|
|
63
|
+
super(%(Cannot store preference "#{key}": the limit of #{limit} preferences per user is already reached.))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Raised when a value fails JSON Schema validation for its key.
|
|
68
|
+
class PreferenceValidationError < PreferenceError
|
|
69
|
+
attr_reader :key, :validation_message
|
|
70
|
+
|
|
71
|
+
def initialize(key, message)
|
|
72
|
+
@key = key
|
|
73
|
+
@validation_message = message
|
|
74
|
+
super(%(Value for preference "#{key}" failed validation: #{message}))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
de:
|
|
2
|
+
habarico:
|
|
3
|
+
contrib:
|
|
4
|
+
preferences:
|
|
5
|
+
not_found: "Einstellung nicht gefunden."
|
|
6
|
+
invalid_value: "Ungültiger Wert für die Einstellung."
|
|
7
|
+
value_too_large: "Der Wert der Einstellung ist zu groß."
|
|
8
|
+
too_many_preferences: "Für diesen Benutzer sind zu viele Einstellungen gespeichert."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
es:
|
|
2
|
+
habarico:
|
|
3
|
+
contrib:
|
|
4
|
+
preferences:
|
|
5
|
+
not_found: "Preferencia no encontrada."
|
|
6
|
+
invalid_value: "Valor no válido para la preferencia."
|
|
7
|
+
value_too_large: "El valor de la preferencia es demasiado grande."
|
|
8
|
+
too_many_preferences: "Hay demasiadas preferencias almacenadas para este usuario."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
fr:
|
|
2
|
+
habarico:
|
|
3
|
+
contrib:
|
|
4
|
+
preferences:
|
|
5
|
+
not_found: "Préférence introuvable."
|
|
6
|
+
invalid_value: "Valeur non valide pour la préférence."
|
|
7
|
+
value_too_large: "La valeur de la préférence est trop volumineuse."
|
|
8
|
+
too_many_preferences: "Trop de préférences sont enregistrées pour cet utilisateur."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
it:
|
|
2
|
+
habarico:
|
|
3
|
+
contrib:
|
|
4
|
+
preferences:
|
|
5
|
+
not_found: "Preferenza non trovata."
|
|
6
|
+
invalid_value: "Valore non valido per la preferenza."
|
|
7
|
+
value_too_large: "Il valore della preferenza è troppo grande."
|
|
8
|
+
too_many_preferences: "Sono memorizzate troppe preferenze per questo utente."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
nl:
|
|
2
|
+
habarico:
|
|
3
|
+
contrib:
|
|
4
|
+
preferences:
|
|
5
|
+
not_found: "Voorkeur niet gevonden."
|
|
6
|
+
invalid_value: "Ongeldige waarde voor voorkeur."
|
|
7
|
+
value_too_large: "De waarde van de voorkeur is te groot."
|
|
8
|
+
too_many_preferences: "Er zijn te veel voorkeuren opgeslagen voor deze gebruiker."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pl:
|
|
2
|
+
habarico:
|
|
3
|
+
contrib:
|
|
4
|
+
preferences:
|
|
5
|
+
not_found: "Nie znaleziono preferencji."
|
|
6
|
+
invalid_value: "Nieprawidłowa wartość preferencji."
|
|
7
|
+
value_too_large: "Wartość preferencji jest zbyt duża."
|
|
8
|
+
too_many_preferences: "Dla tego użytkownika zapisano zbyt wiele preferencji."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pt:
|
|
2
|
+
habarico:
|
|
3
|
+
contrib:
|
|
4
|
+
preferences:
|
|
5
|
+
not_found: "Preferência não encontrada."
|
|
6
|
+
invalid_value: "Valor inválido para a preferência."
|
|
7
|
+
value_too_large: "O valor da preferência é demasiado grande."
|
|
8
|
+
too_many_preferences: "Estão armazenadas demasiadas preferências para este utilizador."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ro:
|
|
2
|
+
habarico:
|
|
3
|
+
contrib:
|
|
4
|
+
preferences:
|
|
5
|
+
not_found: "Preferința nu a fost găsită."
|
|
6
|
+
invalid_value: "Valoare invalidă pentru preferință."
|
|
7
|
+
value_too_large: "Valoarea preferinței este prea mare."
|
|
8
|
+
too_many_preferences: "Sunt stocate prea multe preferințe pentru acest utilizator."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
sk:
|
|
2
|
+
habarico:
|
|
3
|
+
contrib:
|
|
4
|
+
preferences:
|
|
5
|
+
not_found: "Predvoľba sa nenašla."
|
|
6
|
+
invalid_value: "Neplatná hodnota predvoľby."
|
|
7
|
+
value_too_large: "Hodnota predvoľby je príliš veľká."
|
|
8
|
+
too_many_preferences: "Pre tohto používateľa je uložených príliš veľa predvolieb."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
uk:
|
|
2
|
+
habarico:
|
|
3
|
+
contrib:
|
|
4
|
+
preferences:
|
|
5
|
+
not_found: "Налаштування не знайдено."
|
|
6
|
+
invalid_value: "Недійсне значення налаштування."
|
|
7
|
+
value_too_large: "Значення налаштування завелике."
|
|
8
|
+
too_many_preferences: "Для цього користувача збережено забагато налаштувань."
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "json_schemer"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "habarico/contrib/preferences/errors"
|
|
5
|
+
|
|
6
|
+
module Habarico
|
|
7
|
+
module Contrib
|
|
8
|
+
module Preferences
|
|
9
|
+
# Regex a preference key must match. Mirrors the Python KEY_PATTERN.
|
|
10
|
+
KEY_PATTERN = %r{\A[a-zA-Z][a-zA-Z0-9_./-]*\z}
|
|
11
|
+
|
|
12
|
+
# Sentinel meaning "no default registered". Distinct from nil, which is a valid
|
|
13
|
+
# stored/default value.
|
|
14
|
+
EMPTY = Object.new
|
|
15
|
+
def EMPTY.inspect = "EMPTY"
|
|
16
|
+
|
|
17
|
+
# Value object returned by get_value/get_values/set_value.
|
|
18
|
+
ResolvedPreference = Struct.new(:key, :value, :updated_at, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
# One registered entry: its JSON Schema, its (possibly EMPTY/callable) default, a compiled
|
|
21
|
+
# json_schemer validator and its already-resolved size limit (nil meaning unlimited).
|
|
22
|
+
#
|
|
23
|
+
# `max_bytes` needs no sentinel, unlike `default`: nil is not a usable size, so it simply
|
|
24
|
+
# means "not given at registration" and defers to `max_value_bytes`. It is resolved once, at
|
|
25
|
+
# registration, exactly like `defaults` - nothing reads the configuration per write.
|
|
26
|
+
Entry = Struct.new(:schema, :default, :validator, :max_bytes, keyword_init: true)
|
|
27
|
+
|
|
28
|
+
class PreferenceRegistry
|
|
29
|
+
def initialize
|
|
30
|
+
@entries = {} # key (String) => Entry
|
|
31
|
+
@pattern_entries = [] # [[Regexp, Entry], ...]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Test/reload hook.
|
|
35
|
+
def _clear
|
|
36
|
+
@entries.clear
|
|
37
|
+
@pattern_entries.clear
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def register(key, schema:, default: EMPTY, max_bytes: nil)
|
|
41
|
+
raise InvalidPreferenceKey, key unless key.is_a?(String) && key.match?(KEY_PATTERN)
|
|
42
|
+
raise PreferenceAlreadyRegistered, key if @entries.key?(key)
|
|
43
|
+
|
|
44
|
+
@entries[key] = build_entry(key, schema, default, max_bytes)
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def register_pattern(pattern, schema:, max_bytes: nil)
|
|
49
|
+
# A pattern entry never carries a default - there is no single key to attach one to - so
|
|
50
|
+
# `build_entry` returns on its EMPTY branch without validating anything. Same entry, one
|
|
51
|
+
# place: the two registration paths cannot drift when a field is added to `Entry`.
|
|
52
|
+
compiled = pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern)
|
|
53
|
+
@pattern_entries << [compiled, build_entry(compiled.source, schema, EMPTY, max_bytes)]
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def registered?(key)
|
|
58
|
+
return true if @entries.key?(key)
|
|
59
|
+
|
|
60
|
+
@pattern_entries.any? { |pattern, _| full_match?(pattern, key) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def get_schema(key)
|
|
64
|
+
entry_for(key).schema
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_default(key)
|
|
68
|
+
default = resolve_default(entry_for(key))
|
|
69
|
+
default.equal?(EMPTY) ? nil : default
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Size limit in bytes for a key, or nil when it is unlimited.
|
|
73
|
+
def max_bytes_for(key)
|
|
74
|
+
entry_for(key).max_bytes
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate(key, value)
|
|
78
|
+
entry = entry_for(key)
|
|
79
|
+
ensure_size_allowed!(key, entry, value)
|
|
80
|
+
errors = entry.validator.validate(value).to_a
|
|
81
|
+
raise PreferenceValidationError.new(key, errors.first&.dig("error").to_s) unless errors.empty?
|
|
82
|
+
|
|
83
|
+
value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def get_value(user_id, key)
|
|
87
|
+
entry = entry_for(key)
|
|
88
|
+
record = UserPreference.find_by(user_id: user_id, key: key)
|
|
89
|
+
resolve(key, entry, record)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def get_values(user_id)
|
|
93
|
+
resolved = {}
|
|
94
|
+
|
|
95
|
+
UserPreference.where(user_id: user_id).find_each do |record|
|
|
96
|
+
entry = begin
|
|
97
|
+
entry_for(record.key)
|
|
98
|
+
rescue PreferenceNotRegistered
|
|
99
|
+
record.destroy
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
item = resolve(record.key, entry, record)
|
|
103
|
+
resolved[record.key] = item if item
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@entries.each do |key, entry|
|
|
107
|
+
next if resolved.key?(key)
|
|
108
|
+
|
|
109
|
+
item = resolve(key, entry, nil)
|
|
110
|
+
resolved[key] = item if item
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
resolved.keys.sort.map { |key| resolved[key] }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def set_value(user_id, key, value)
|
|
117
|
+
validate(key, value)
|
|
118
|
+
ensure_key_budget!(user_id, key)
|
|
119
|
+
record = UserPreference.find_or_initialize_by(user_id: user_id, key: key)
|
|
120
|
+
record.id ||= SecureRandom.uuid
|
|
121
|
+
record.value = value
|
|
122
|
+
record.updated_at = Time.current
|
|
123
|
+
record.save!
|
|
124
|
+
ResolvedPreference.new(key: key, value: record.value, updated_at: record.updated_at)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def delete_value(user_id, key)
|
|
128
|
+
entry_for(key) # raises PreferenceNotRegistered for unknown keys
|
|
129
|
+
UserPreference.where(user_id: user_id, key: key).delete_all
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def delete_values(user_id)
|
|
134
|
+
UserPreference.where(user_id: user_id).delete_all
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# Ahead of the schema: the size guard is the cheap check and the one that has to hold for
|
|
141
|
+
# free-form branches (a saved query's `filter`) the schema does not bound at all.
|
|
142
|
+
def ensure_size_allowed!(key, entry, value)
|
|
143
|
+
limit = entry.max_bytes
|
|
144
|
+
return if limit.nil?
|
|
145
|
+
|
|
146
|
+
size = JSON.generate(value).bytesize
|
|
147
|
+
raise PreferenceValueTooLarge.new(key, limit, size) if size > limit
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Configured limit for the key - or pattern - a preference is being registered under. An
|
|
151
|
+
# explicit `max_bytes:` wins; otherwise `max_value_bytes` decides, by the very string the
|
|
152
|
+
# preference is registered under, falling back to its "default" entry; a nil there means
|
|
153
|
+
# unlimited. A pattern may be configured as a Regexp or as its source string. Read once, at
|
|
154
|
+
# registration, exactly like `defaults`: changing the config afterwards does not move an
|
|
155
|
+
# already-registered limit.
|
|
156
|
+
def resolve_max_bytes(registered_as, max_bytes)
|
|
157
|
+
return max_bytes unless max_bytes.nil?
|
|
158
|
+
|
|
159
|
+
limits = Preferences.max_value_bytes || {}
|
|
160
|
+
limits.each do |configured, bytes|
|
|
161
|
+
name = configured.is_a?(Regexp) ? configured.source : configured.to_s
|
|
162
|
+
return bytes if name == registered_as
|
|
163
|
+
end
|
|
164
|
+
limits["default"]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Refuses a NEW key once the user holds the maximum number of preferences. Overwriting a
|
|
168
|
+
# key they already have is always allowed, so nobody already past the limit is locked out
|
|
169
|
+
# of shrinking or fixing what they stored.
|
|
170
|
+
def ensure_key_budget!(user_id, key)
|
|
171
|
+
limit = Preferences.max_keys_per_user
|
|
172
|
+
return if limit.nil?
|
|
173
|
+
|
|
174
|
+
every_key_of_user = UserPreference.where(user_id: user_id)
|
|
175
|
+
# Overwriting a key the user already has adds no row, so it never spends budget - and
|
|
176
|
+
# without this guard a user sitting exactly at the limit could not rewrite anything.
|
|
177
|
+
return if every_key_of_user.where(key: key).exists?
|
|
178
|
+
raise TooManyPreferences.new(key, limit) if every_key_of_user.count >= limit
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# json_schemer patterns must match the WHOLE key, like Python's fullmatch.
|
|
182
|
+
def full_match?(pattern, key)
|
|
183
|
+
match = pattern.match(key)
|
|
184
|
+
!match.nil? && match[0] == key
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def entry_for(key)
|
|
188
|
+
return @entries[key] if @entries.key?(key)
|
|
189
|
+
|
|
190
|
+
@pattern_entries.each { |pattern, entry| return entry if full_match?(pattern, key) }
|
|
191
|
+
raise PreferenceNotRegistered, key
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def build_entry(key, schema, default, max_bytes = nil)
|
|
195
|
+
validator = JSONSchemer.schema(schema)
|
|
196
|
+
entry = Entry.new(schema: schema, default: default, validator: validator,
|
|
197
|
+
max_bytes: resolve_max_bytes(key, max_bytes))
|
|
198
|
+
return entry if default.equal?(EMPTY) || default.respond_to?(:call)
|
|
199
|
+
|
|
200
|
+
errors = validator.validate(default).to_a
|
|
201
|
+
raise PreferenceValidationError.new(key, errors.first&.dig("error").to_s) unless errors.empty?
|
|
202
|
+
|
|
203
|
+
entry
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Resolves a (possibly callable) default. A callable that returns EMPTY or an
|
|
207
|
+
# invalid value degrades to EMPTY, mirroring Python `_resolve_default`.
|
|
208
|
+
def resolve_default(entry)
|
|
209
|
+
default = entry.default
|
|
210
|
+
return default unless default.respond_to?(:call)
|
|
211
|
+
|
|
212
|
+
resolved = default.call
|
|
213
|
+
return EMPTY if resolved.equal?(EMPTY)
|
|
214
|
+
return EMPTY unless entry.validator.valid?(resolved)
|
|
215
|
+
|
|
216
|
+
resolved
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Mirrors Python `_resolve`: a stored value that fails validation is deleted and the
|
|
220
|
+
# default is returned instead; when there is neither a valid stored value nor a default,
|
|
221
|
+
# returns nil (single-GET => 404, list => omit).
|
|
222
|
+
def resolve(key, entry, record)
|
|
223
|
+
if record
|
|
224
|
+
if entry.validator.valid?(record.value)
|
|
225
|
+
return ResolvedPreference.new(key: key, value: record.value, updated_at: record.updated_at)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
record.destroy
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
default = resolve_default(entry)
|
|
232
|
+
return nil if default.equal?(EMPTY)
|
|
233
|
+
|
|
234
|
+
ResolvedPreference.new(key: key, value: default, updated_at: nil)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# ---- module-level singleton API (mirrors Python module functions) ----
|
|
239
|
+
|
|
240
|
+
def self.registry
|
|
241
|
+
@registry ||= PreferenceRegistry.new
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def self.register_preference(key, schema:, default: EMPTY, max_bytes: nil)
|
|
245
|
+
registry.register(key, schema: schema, default: default, max_bytes: max_bytes)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def self.register_preference_pattern(pattern, schema:, max_bytes: nil)
|
|
249
|
+
registry.register_pattern(pattern, schema: schema, max_bytes: max_bytes)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def self.max_bytes_for(key)
|
|
253
|
+
registry.max_bytes_for(key)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def self.registered?(key)
|
|
257
|
+
registry.registered?(key)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def self.get_schema(key)
|
|
261
|
+
registry.get_schema(key)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def self.get_default(key)
|
|
265
|
+
registry.get_default(key)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def self.validate(key, value)
|
|
269
|
+
registry.validate(key, value)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def self.get_value(user_id, key)
|
|
273
|
+
registry.get_value(user_id, key)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def self.get_values(user_id)
|
|
277
|
+
registry.get_values(user_id)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def self.set_value(user_id, key, value)
|
|
281
|
+
registry.set_value(user_id, key, value)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def self.delete_value(user_id, key)
|
|
285
|
+
registry.delete_value(user_id, key)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def self.delete_values(user_id)
|
|
289
|
+
registry.delete_values(user_id)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Registers the user-preferences routes through the RoutingService extension hook, so the
|
|
2
|
+
# host's config/routes.rb (a single `RoutingService.instance.draw_routes`) needs no change.
|
|
3
|
+
# The blocks are drawn at the admin-site root (beside schema/search) - matching the Python
|
|
4
|
+
# AdminSite, where user-preferences lives next to schema/search. RoutingService draws them
|
|
5
|
+
# inside a path-only `scope` (never a `namespace`/`scope module:`), so the controller path
|
|
6
|
+
# must NOT start with a slash: Rails only strips a leading slash when a module scope is
|
|
7
|
+
# active, and raises "is not a supported controller name" otherwise.
|
|
8
|
+
# Keys may contain dots and slashes, so the `:key` segment is a glob with format off.
|
|
9
|
+
Habarico::RoutingService.register_route_block do
|
|
10
|
+
controller_path = "habarico/contrib/preferences/user_preferences"
|
|
11
|
+
|
|
12
|
+
get "user-preferences", to: "#{controller_path}#index"
|
|
13
|
+
delete "user-preferences", to: "#{controller_path}#destroy_all"
|
|
14
|
+
get "user-preferences/*key", to: "#{controller_path}#show", format: false
|
|
15
|
+
put "user-preferences/*key", to: "#{controller_path}#update", format: false
|
|
16
|
+
delete "user-preferences/*key", to: "#{controller_path}#destroy", format: false
|
|
17
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
module Habarico
|
|
2
|
+
module Contrib
|
|
3
|
+
module Preferences
|
|
4
|
+
# Built-in preference value schemas, mirroring the pydantic models in the Python
|
|
5
|
+
# habarico/contrib/preferences/schemas.py. Plain JSON Schema (Draft 2020-12) hashes.
|
|
6
|
+
module Schemas
|
|
7
|
+
THEME = { "enum" => %w[light dark] }.freeze
|
|
8
|
+
LOCALE = { "type" => "string" }.freeze
|
|
9
|
+
APP_PAGE_WIDTH = { "enum" => %w[full centered] }.freeze
|
|
10
|
+
CONTRAST_MODE = { "type" => "boolean" }.freeze
|
|
11
|
+
STRIPED_ROWS = { "type" => "boolean" }.freeze
|
|
12
|
+
APP_DENSITY = { "enum" => %w[compact standard comfortable] }.freeze
|
|
13
|
+
|
|
14
|
+
COLUMNS = { "type" => "array", "items" => { "type" => "string" } }.freeze
|
|
15
|
+
|
|
16
|
+
COLUMN_WIDTHS = {
|
|
17
|
+
"type" => "object",
|
|
18
|
+
"additionalProperties" => { "type" => "number", "minimum" => 0 },
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
SORT = {
|
|
22
|
+
"type" => "object",
|
|
23
|
+
"required" => %w[field order],
|
|
24
|
+
"properties" => {
|
|
25
|
+
"field" => { "type" => "string" },
|
|
26
|
+
"order" => { "enum" => %w[ASC DESC] },
|
|
27
|
+
},
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
SAVED_QUERIES = {
|
|
31
|
+
"type" => "array",
|
|
32
|
+
"items" => {
|
|
33
|
+
"type" => "object",
|
|
34
|
+
"required" => %w[label],
|
|
35
|
+
"properties" => {
|
|
36
|
+
"label" => { "type" => "string" },
|
|
37
|
+
"value" => {
|
|
38
|
+
"type" => "object",
|
|
39
|
+
"properties" => {
|
|
40
|
+
"filter" => { "type" => "object" },
|
|
41
|
+
"displayedFilters" => { "type" => "object", "additionalProperties" => { "type" => "boolean" } },
|
|
42
|
+
"sort" => { "anyOf" => [SORT, { "type" => "null" }] },
|
|
43
|
+
"perPage" => { "type" => %w[integer null], "minimum" => 1 },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
WIDGET_LAYOUT = {
|
|
51
|
+
"type" => "object",
|
|
52
|
+
"required" => %w[x y w h],
|
|
53
|
+
"properties" => {
|
|
54
|
+
"x" => { "type" => "integer", "minimum" => 0 },
|
|
55
|
+
"y" => { "type" => "integer", "minimum" => 0 },
|
|
56
|
+
"w" => { "type" => "integer", "minimum" => 1 },
|
|
57
|
+
"h" => { "type" => "integer", "minimum" => 1 },
|
|
58
|
+
},
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
GROUP_BY_BUCKET = {
|
|
62
|
+
"oneOf" => [
|
|
63
|
+
{
|
|
64
|
+
"type" => "object",
|
|
65
|
+
"required" => %w[type granularity],
|
|
66
|
+
"properties" => {
|
|
67
|
+
"type" => { "const" => "trunc" },
|
|
68
|
+
"granularity" => { "enum" => %w[second minute hour day week month quarter year] },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"type" => "object",
|
|
73
|
+
"required" => %w[type size],
|
|
74
|
+
"properties" => {
|
|
75
|
+
"type" => { "const" => "width" },
|
|
76
|
+
"size" => { "type" => "number" },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
82
|
+
GROUP_BY_ITEM = {
|
|
83
|
+
"type" => "object",
|
|
84
|
+
"required" => %w[field],
|
|
85
|
+
"properties" => {
|
|
86
|
+
"field" => { "type" => "string" },
|
|
87
|
+
"bucket" => { "anyOf" => [GROUP_BY_BUCKET, { "type" => "null" }] },
|
|
88
|
+
},
|
|
89
|
+
}.freeze
|
|
90
|
+
|
|
91
|
+
CHART_OPTIONS = {
|
|
92
|
+
"type" => "object",
|
|
93
|
+
"properties" => {
|
|
94
|
+
"orientation" => { "enum" => %w[vertical horizontal] },
|
|
95
|
+
"variant" => { "enum" => %w[pie doughnut] },
|
|
96
|
+
},
|
|
97
|
+
}.freeze
|
|
98
|
+
|
|
99
|
+
WIDGET_CONFIG = {
|
|
100
|
+
"type" => "object",
|
|
101
|
+
"required" => %w[id type layout],
|
|
102
|
+
"properties" => {
|
|
103
|
+
"id" => { "type" => "string" },
|
|
104
|
+
"type" => { "type" => "string" },
|
|
105
|
+
"resource" => { "type" => %w[string null] },
|
|
106
|
+
"label" => { "type" => %w[string null] },
|
|
107
|
+
"layout" => WIDGET_LAYOUT,
|
|
108
|
+
"filters" => { "type" => %w[object null], "additionalProperties" => { "type" => "string" } },
|
|
109
|
+
"sort" => { "anyOf" => [SORT, { "type" => "null" }] },
|
|
110
|
+
"perPage" => { "type" => %w[integer null], "minimum" => 1 },
|
|
111
|
+
"columns" => { "type" => %w[array null], "items" => { "type" => "string" } },
|
|
112
|
+
"field" => { "type" => %w[string null] },
|
|
113
|
+
"aggregateFunction" => { "type" => %w[string null], "enum" => [nil, "count", "sum", "avg", "min", "max"] },
|
|
114
|
+
"color" => { "type" => %w[string null], "enum" => [nil, "primary", "secondary", "success", "error", "warning", "info", "inherit"] },
|
|
115
|
+
"groupBy" => { "type" => %w[array null], "items" => GROUP_BY_ITEM },
|
|
116
|
+
"chartOptions" => { "anyOf" => [CHART_OPTIONS, { "type" => "null" }] },
|
|
117
|
+
},
|
|
118
|
+
}.freeze
|
|
119
|
+
|
|
120
|
+
DASHBOARD = {
|
|
121
|
+
"type" => "object",
|
|
122
|
+
"properties" => {
|
|
123
|
+
"widgets" => { "type" => "array", "items" => WIDGET_CONFIG },
|
|
124
|
+
},
|
|
125
|
+
}.freeze
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Habarico
|
|
2
|
+
module Contrib
|
|
3
|
+
module Preferences
|
|
4
|
+
# Serializes a ResolvedPreference into the API item shape { key, value, updated_at }.
|
|
5
|
+
# Mirrors the Python UserPreferenceItemSerializer.
|
|
6
|
+
module Serializer
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def item(resolved)
|
|
10
|
+
{
|
|
11
|
+
"key" => resolved.key,
|
|
12
|
+
"value" => resolved.value,
|
|
13
|
+
"updated_at" => resolved.updated_at&.iso8601,
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|