netzke-basepack-zh 0.7.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +1 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.rdoc +445 -0
- data/LICENSE +1 -0
- data/README.md +94 -0
- data/Rakefile +44 -0
- data/TODO.rdoc +8 -0
- data/config/ci/before-travis.sh +28 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/javascripts/basepack.js +139 -0
- data/javascripts/xdatetime.js +196 -0
- data/lib/generators/netzke/basepack_generator.rb +10 -0
- data/lib/generators/netzke/templates/assets/ts-checkbox.gif +0 -0
- data/lib/generators/netzke/templates/create_netzke_field_lists.rb +18 -0
- data/lib/netzke-basepack.rb +26 -0
- data/lib/netzke/active_record.rb +20 -0
- data/lib/netzke/active_record/attributes.rb +256 -0
- data/lib/netzke/active_record/combobox_options.rb +16 -0
- data/lib/netzke/active_record/relation_extensions.rb +37 -0
- data/lib/netzke/basepack.rb +45 -0
- data/lib/netzke/basepack/accordion_panel.rb +39 -0
- data/lib/netzke/basepack/action_column.rb +68 -0
- data/lib/netzke/basepack/action_column/javascripts/action_column.js +61 -0
- data/lib/netzke/basepack/auth_app.rb +159 -0
- data/lib/netzke/basepack/basic_app.rb +7 -0
- data/lib/netzke/basepack/border_layout_panel.rb +53 -0
- data/lib/netzke/basepack/border_layout_panel/javascripts/border_layout_panel.js +40 -0
- data/lib/netzke/basepack/data_accessor.rb +53 -0
- data/lib/netzke/basepack/data_adapters/abstract_adapter.rb +164 -0
- data/lib/netzke/basepack/data_adapters/active_record_adapter.rb +279 -0
- data/lib/netzke/basepack/data_adapters/data_mapper_adapter.rb +264 -0
- data/lib/netzke/basepack/data_adapters/sequel_adapter.rb +260 -0
- data/lib/netzke/basepack/form_panel.rb +144 -0
- data/lib/netzke/basepack/form_panel/fields.rb +208 -0
- data/lib/netzke/basepack/form_panel/javascripts/comma_list_cbg.js +51 -0
- data/lib/netzke/basepack/form_panel/javascripts/form_panel.js +225 -0
- data/lib/netzke/basepack/form_panel/javascripts/misc.js +4 -0
- data/lib/netzke/basepack/form_panel/javascripts/n_radio_group.js +43 -0
- data/lib/netzke/basepack/form_panel/javascripts/readonly_mode.js +35 -0
- data/lib/netzke/basepack/form_panel/services.rb +142 -0
- data/lib/netzke/basepack/form_panel/stylesheets/readonly_mode.css +14 -0
- data/lib/netzke/basepack/grid_panel.rb +440 -0
- data/lib/netzke/basepack/grid_panel/columns.rb +394 -0
- data/lib/netzke/basepack/grid_panel/javascripts/advanced_search.js +27 -0
- data/lib/netzke/basepack/grid_panel/javascripts/check_column_fix.js +6 -0
- data/lib/netzke/basepack/grid_panel/javascripts/edit_in_form.js +51 -0
- data/lib/netzke/basepack/grid_panel/javascripts/event_handling.js +179 -0
- data/lib/netzke/basepack/grid_panel/javascripts/grid_panel.js +438 -0
- data/lib/netzke/basepack/grid_panel/javascripts/misc.js +4 -0
- data/lib/netzke/basepack/grid_panel/javascripts/rows-dd.js +281 -0
- data/lib/netzke/basepack/grid_panel/record_form_window.rb +41 -0
- data/lib/netzke/basepack/grid_panel/services.rb +235 -0
- data/lib/netzke/basepack/paging_form_panel.rb +72 -0
- data/lib/netzke/basepack/paging_form_panel/javascripts/paging_form_panel.js +76 -0
- data/lib/netzke/basepack/panel.rb +11 -0
- data/lib/netzke/basepack/query_builder.rb +107 -0
- data/lib/netzke/basepack/query_builder/javascripts/query_builder.js +153 -0
- data/lib/netzke/basepack/search_panel.rb +79 -0
- data/lib/netzke/basepack/search_panel/javascripts/condition_field.js +160 -0
- data/lib/netzke/basepack/search_panel/javascripts/search_panel.js +65 -0
- data/lib/netzke/basepack/search_window.rb +66 -0
- data/lib/netzke/basepack/simple_app.rb +104 -0
- data/lib/netzke/basepack/simple_app/javascripts/simple_app.js +64 -0
- data/lib/netzke/basepack/simple_app/javascripts/statusbar_ext.js +8 -0
- data/lib/netzke/basepack/tab_panel.rb +21 -0
- data/lib/netzke/basepack/tab_panel/javascripts/tab_panel.js +11 -0
- data/lib/netzke/basepack/version.rb +11 -0
- data/lib/netzke/basepack/window.rb +29 -0
- data/lib/netzke/basepack/window/javascripts/window.js +20 -0
- data/lib/netzke/basepack/wrap_lazy_loaded.rb +28 -0
- data/lib/netzke/basepack/wrapper.rb +28 -0
- data/lib/netzke/data_mapper.rb +18 -0
- data/lib/netzke/data_mapper/attributes.rb +273 -0
- data/lib/netzke/data_mapper/combobox_options.rb +11 -0
- data/lib/netzke/data_mapper/relation_extensions.rb +38 -0
- data/lib/netzke/sequel.rb +18 -0
- data/lib/netzke/sequel/attributes.rb +274 -0
- data/lib/netzke/sequel/combobox_options.rb +10 -0
- data/lib/netzke/sequel/relation_extensions.rb +40 -0
- data/lib/tasks/netzke_basepack_tasks.rake +4 -0
- data/locales/de.yml +79 -0
- data/locales/en.yml +79 -0
- data/netzke-basepack.gemspec +306 -0
- data/stylesheets/basepack.css +72 -0
- data/stylesheets/datetimefield.css +54 -0
- data/test/basepack_test_app/.gitignore +6 -0
- data/test/basepack_test_app/.rvmrc +1 -0
- data/test/basepack_test_app/Gemfile +59 -0
- data/test/basepack_test_app/Gemfile.lock +196 -0
- data/test/basepack_test_app/Guardfile +46 -0
- data/test/basepack_test_app/README +1 -0
- data/test/basepack_test_app/Rakefile +7 -0
- data/test/basepack_test_app/app/components/author_form.rb +32 -0
- data/test/basepack_test_app/app/components/author_grid.rb +3 -0
- data/test/basepack_test_app/app/components/book_form.rb +38 -0
- data/test/basepack_test_app/app/components/book_form_with_custom_fields.rb +21 -0
- data/test/basepack_test_app/app/components/book_form_with_defaults.rb +8 -0
- data/test/basepack_test_app/app/components/book_form_with_nested_attributes.rb +18 -0
- data/test/basepack_test_app/app/components/book_grid.rb +12 -0
- data/test/basepack_test_app/app/components/book_grid_filtering.rb +10 -0
- data/test/basepack_test_app/app/components/book_grid_loader.rb +24 -0
- data/test/basepack_test_app/app/components/book_grid_with_column_actions.rb +15 -0
- data/test/basepack_test_app/app/components/book_grid_with_custom_columns.rb +27 -0
- data/test/basepack_test_app/app/components/book_grid_with_default_values.rb +9 -0
- data/test/basepack_test_app/app/components/book_grid_with_extra_feedback.rb +8 -0
- data/test/basepack_test_app/app/components/book_grid_with_extra_filters.rb +14 -0
- data/test/basepack_test_app/app/components/book_grid_with_nested_attributes.rb +13 -0
- data/test/basepack_test_app/app/components/book_grid_with_overridden_columns.rb +15 -0
- data/test/basepack_test_app/app/components/book_grid_with_paging.rb +10 -0
- data/test/basepack_test_app/app/components/book_grid_with_persistence.rb +8 -0
- data/test/basepack_test_app/app/components/book_grid_with_scoped_authors.rb +8 -0
- data/test/basepack_test_app/app/components/book_grid_with_virtual_attributes.rb +21 -0
- data/test/basepack_test_app/app/components/book_paging_form_panel.rb +22 -0
- data/test/basepack_test_app/app/components/book_query_builder.rb +8 -0
- data/test/basepack_test_app/app/components/book_search_panel.rb +5 -0
- data/test/basepack_test_app/app/components/book_search_panel/javascripts/i18n_de.js +6 -0
- data/test/basepack_test_app/app/components/book_with_custom_primary_key_grid.rb +10 -0
- data/test/basepack_test_app/app/components/books_bound_to_author.rb +10 -0
- data/test/basepack_test_app/app/components/double_book_grid.rb +18 -0
- data/test/basepack_test_app/app/components/extras/book_presentation.rb +27 -0
- data/test/basepack_test_app/app/components/form_without_model.rb +21 -0
- data/test/basepack_test_app/app/components/generic_user_form.rb +12 -0
- data/test/basepack_test_app/app/components/lockable_book_form.rb +17 -0
- data/test/basepack_test_app/app/components/lockable_user_form.rb +7 -0
- data/test/basepack_test_app/app/components/paging_form_with_search.rb +40 -0
- data/test/basepack_test_app/app/components/simple_accordion.rb +11 -0
- data/test/basepack_test_app/app/components/simple_panel.rb +17 -0
- data/test/basepack_test_app/app/components/simple_tab_panel.rb +11 -0
- data/test/basepack_test_app/app/components/simple_window.rb +10 -0
- data/test/basepack_test_app/app/components/simple_wrapper.rb +7 -0
- data/test/basepack_test_app/app/components/some_accordion_panel.rb +22 -0
- data/test/basepack_test_app/app/components/some_auth_app.rb +32 -0
- data/test/basepack_test_app/app/components/some_border_layout.rb +28 -0
- data/test/basepack_test_app/app/components/some_simple_app.rb +35 -0
- data/test/basepack_test_app/app/components/some_tab_panel.rb +20 -0
- data/test/basepack_test_app/app/components/user_form.rb +25 -0
- data/test/basepack_test_app/app/components/user_form_with_default_fields.rb +8 -0
- data/test/basepack_test_app/app/components/user_grid.rb +8 -0
- data/test/basepack_test_app/app/components/user_grid_with_customized_form_fields.rb +18 -0
- data/test/basepack_test_app/app/components/window_component_loader.rb +27 -0
- data/test/basepack_test_app/app/controllers/application_controller.rb +9 -0
- data/test/basepack_test_app/app/controllers/components_controller.rb +10 -0
- data/test/basepack_test_app/app/controllers/welcome_controller.rb +9 -0
- data/test/basepack_test_app/app/helpers/application_helper.rb +8 -0
- data/test/basepack_test_app/app/helpers/embedded_components_helper.rb +2 -0
- data/test/basepack_test_app/app/models/address.rb +29 -0
- data/test/basepack_test_app/app/models/author.rb +38 -0
- data/test/basepack_test_app/app/models/book.rb +49 -0
- data/test/basepack_test_app/app/models/book_with_custom_primary_key.rb +26 -0
- data/test/basepack_test_app/app/models/role.rb +24 -0
- data/test/basepack_test_app/app/models/user.rb +29 -0
- data/test/basepack_test_app/app/presenters/forms/generic_user.rb +6 -0
- data/test/basepack_test_app/app/views/components/loadable_window.html.erb +9 -0
- data/test/basepack_test_app/app/views/components/simple_panel.html.erb +1 -0
- data/test/basepack_test_app/app/views/layouts/application.html.erb +12 -0
- data/test/basepack_test_app/app/views/layouts/components.html.erb +13 -0
- data/test/basepack_test_app/app/views/layouts/nested.html.erb +5 -0
- data/test/basepack_test_app/app/views/welcome/index.html.erb +10 -0
- data/test/basepack_test_app/config.ru +4 -0
- data/test/basepack_test_app/config/application.rb +57 -0
- data/test/basepack_test_app/config/boot.rb +13 -0
- data/test/basepack_test_app/config/cucumber.yml +8 -0
- data/test/basepack_test_app/config/database.yml.sample +41 -0
- data/test/basepack_test_app/config/database.yml.travis +15 -0
- data/test/basepack_test_app/config/environment.rb +6 -0
- data/test/basepack_test_app/config/environments/development.rb +22 -0
- data/test/basepack_test_app/config/environments/production.rb +49 -0
- data/test/basepack_test_app/config/environments/test.rb +35 -0
- data/test/basepack_test_app/config/initializers/backtrace_silencers.rb +7 -0
- data/test/basepack_test_app/config/initializers/data_mapper_logging.rb +3 -0
- data/test/basepack_test_app/config/initializers/inflections.rb +10 -0
- data/test/basepack_test_app/config/initializers/mime_types.rb +5 -0
- data/test/basepack_test_app/config/initializers/netzke.rb +9 -0
- data/test/basepack_test_app/config/initializers/secret_token.rb +7 -0
- data/test/basepack_test_app/config/initializers/sequel.rb +26 -0
- data/test/basepack_test_app/config/initializers/session_store.rb +8 -0
- data/test/basepack_test_app/config/locales/de.yml +35 -0
- data/test/basepack_test_app/config/locales/es.yml +96 -0
- data/test/basepack_test_app/config/routes.rb +68 -0
- data/test/basepack_test_app/db/development_structure.sql +88 -0
- data/test/basepack_test_app/db/migrate/20100914104207_create_users.rb +15 -0
- data/test/basepack_test_app/db/migrate/20100914104236_create_roles.rb +13 -0
- data/test/basepack_test_app/db/migrate/20101026185816_create_authors.rb +14 -0
- data/test/basepack_test_app/db/migrate/20101026190021_create_books.rb +19 -0
- data/test/basepack_test_app/db/migrate/20110101143818_create_addresses.rb +17 -0
- data/test/basepack_test_app/db/migrate/20110213213050_create_netzke_component_states.rb +20 -0
- data/test/basepack_test_app/db/migrate/20110701070052_create_book_with_custom_primary_keys.rb +15 -0
- data/test/basepack_test_app/db/migrate/20110901114016_add_last_read_at_to_books.rb +9 -0
- data/test/basepack_test_app/db/migrate/20110909071740_add_published_on_to_books.rb +5 -0
- data/test/basepack_test_app/db/schema.rb +81 -0
- data/test/basepack_test_app/db/seeds.rb +44 -0
- data/test/basepack_test_app/features/accordion_panel.feature +12 -0
- data/test/basepack_test_app/features/components_in_view.feature +11 -0
- data/test/basepack_test_app/features/form_panel.feature +142 -0
- data/test/basepack_test_app/features/grid_panel.feature +277 -0
- data/test/basepack_test_app/features/grid_panel_filters.feature +73 -0
- data/test/basepack_test_app/features/grid_panel_with_custom_primary_key.feature +15 -0
- data/test/basepack_test_app/features/grid_sorting.feature +47 -0
- data/test/basepack_test_app/features/i18n.feature +18 -0
- data/test/basepack_test_app/features/nested_attributes.feature +26 -0
- data/test/basepack_test_app/features/paging_form_panel.feature +43 -0
- data/test/basepack_test_app/features/search_in_grid.feature +49 -0
- data/test/basepack_test_app/features/simple_app.feature +15 -0
- data/test/basepack_test_app/features/simple_panel.feature +11 -0
- data/test/basepack_test_app/features/step_definitions/accordion_steps.rb +5 -0
- data/test/basepack_test_app/features/step_definitions/ext_steps.rb +16 -0
- data/test/basepack_test_app/features/step_definitions/form_panel_steps.rb +46 -0
- data/test/basepack_test_app/features/step_definitions/generic_steps.rb +44 -0
- data/test/basepack_test_app/features/step_definitions/grid_panel_steps.rb +186 -0
- data/test/basepack_test_app/features/step_definitions/pickle_steps.rb +100 -0
- data/test/basepack_test_app/features/step_definitions/web_steps.rb +219 -0
- data/test/basepack_test_app/features/support/env.rb +81 -0
- data/test/basepack_test_app/features/support/paths.rb +65 -0
- data/test/basepack_test_app/features/support/pickle.rb +24 -0
- data/test/basepack_test_app/features/support/selectors.rb +39 -0
- data/test/basepack_test_app/features/tab_panel.feature +12 -0
- data/test/basepack_test_app/features/validations_in_grid.feature +13 -0
- data/test/basepack_test_app/features/virtual_attributes.feature +16 -0
- data/test/basepack_test_app/features/window.feature +11 -0
- data/test/basepack_test_app/lib/tasks/.gitkeep +0 -0
- data/test/basepack_test_app/lib/tasks/cucumber.rake +71 -0
- data/test/basepack_test_app/lib/tasks/travis.rake +7 -0
- data/test/basepack_test_app/public/404.html +26 -0
- data/test/basepack_test_app/public/422.html +26 -0
- data/test/basepack_test_app/public/500.html +26 -0
- data/test/basepack_test_app/public/favicon.ico +0 -0
- data/test/basepack_test_app/public/images/header-deco.gif +0 -0
- data/test/basepack_test_app/public/images/rails.png +0 -0
- data/test/basepack_test_app/public/javascripts/application.js +2 -0
- data/test/basepack_test_app/public/javascripts/controls.js +965 -0
- data/test/basepack_test_app/public/javascripts/dragdrop.js +974 -0
- data/test/basepack_test_app/public/javascripts/effects.js +1123 -0
- data/test/basepack_test_app/public/javascripts/prototype.js +6001 -0
- data/test/basepack_test_app/public/javascripts/rails.js +175 -0
- data/test/basepack_test_app/public/robots.txt +5 -0
- data/test/basepack_test_app/public/stylesheets/.gitkeep +0 -0
- data/test/basepack_test_app/script/cucumber +10 -0
- data/test/basepack_test_app/script/rails +6 -0
- data/test/basepack_test_app/spec/components/form_panel_spec.rb +53 -0
- data/test/basepack_test_app/spec/components/grid_panel_spec.rb +10 -0
- data/test/basepack_test_app/spec/data_adapter/adapter_spec.rb +68 -0
- data/test/basepack_test_app/spec/data_adapter/attributes_spec.rb +56 -0
- data/test/basepack_test_app/spec/data_adapter/relation_extensions_spec.rb +125 -0
- data/test/basepack_test_app/spec/factories.rb +28 -0
- data/test/basepack_test_app/spec/spec_helper.rb +39 -0
- data/test/basepack_test_app/test/performance/browsing_test.rb +9 -0
- data/test/basepack_test_app/test/test_helper.rb +13 -0
- data/test/basepack_test_app/vendor/plugins/.gitkeep +0 -0
- data/test/console_with_fixtures.rb +4 -0
- data/test/fixtures/books.yml +11 -0
- data/test/fixtures/categories.yml +7 -0
- data/test/fixtures/cities.yml +21 -0
- data/test/fixtures/continents.yml +7 -0
- data/test/fixtures/countries.yml +9 -0
- data/test/fixtures/genres.yml +9 -0
- data/test/fixtures/roles.yml +8 -0
- data/test/fixtures/users.yml +11 -0
- data/test/schema.rb +10 -0
- data/test/test_helper.rb +21 -0
- data/test/unit/accordion_panel_test.rb +20 -0
- data/test/unit/active_record_basepack_test.rb +54 -0
- data/test/unit/fields_configuration_test.rb +18 -0
- data/test/unit/grid_panel_test.rb +52 -0
- data/test/unit/netzke_basepack_test.rb +4 -0
- data/test/unit/tab_panel_test.rb +21 -0
- data/uninstall.rb +1 -0
- metadata +332 -0
@@ -0,0 +1,260 @@
|
|
1
|
+
module Netzke::Basepack::DataAdapters
|
2
|
+
class SequelAdapter < AbstractAdapter
|
3
|
+
def self.for_class?(model_class)
|
4
|
+
model_class <= Sequel::Model
|
5
|
+
end
|
6
|
+
|
7
|
+
def get_records(params, columns=[])
|
8
|
+
get_dataset(params, columns).all
|
9
|
+
end
|
10
|
+
|
11
|
+
def count_records(params, columns=[])
|
12
|
+
# dont pass columns, JOINs will be done as necessary for filters
|
13
|
+
get_dataset(params, [], true).count
|
14
|
+
end
|
15
|
+
|
16
|
+
def map_type type
|
17
|
+
type
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_assoc_property_type assoc_name, prop_name
|
21
|
+
db_schema=class_for(assoc_name.to_sym).db_schema
|
22
|
+
# return nil if prop_name not present in db schema (virtual column)
|
23
|
+
db_schema[prop_name.to_sym] ? db_schema[prop_name.to_sym][:type] : nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# like get_assoc_property_type but for non-association columns
|
27
|
+
def get_property_type column
|
28
|
+
column[:type]
|
29
|
+
end
|
30
|
+
|
31
|
+
def column_virtual? c
|
32
|
+
assoc, method = c[:name].split '__'
|
33
|
+
if method
|
34
|
+
!class_for(assoc.to_sym).columns.include? method.to_sym
|
35
|
+
else
|
36
|
+
!@model_class.columns.include? assoc.to_sym
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns options for comboboxes in grids/forms
|
41
|
+
def combobox_options_for_column(column, method_options = {})
|
42
|
+
query = method_options[:query]
|
43
|
+
|
44
|
+
# First, check if we have options for this column defined in persistent storage
|
45
|
+
options = column[:combobox_options] && column[:combobox_options].split("\n")
|
46
|
+
if options
|
47
|
+
query ? options.select{ |o| o.index(/^#{query}/) }.map{ |el| [el] } : options
|
48
|
+
else
|
49
|
+
assoc_name, assoc_method = column[:name].split '__'
|
50
|
+
|
51
|
+
if assoc_name
|
52
|
+
# Options for an asssociation attribute
|
53
|
+
dataset = class_for(assoc_name)
|
54
|
+
|
55
|
+
dataset = dataset.extend_with(method_options[:scope]) if method_options[:scope]
|
56
|
+
|
57
|
+
if class_for(assoc_name).column_names.include?(assoc_method)
|
58
|
+
# apply query
|
59
|
+
dataset = dataset.where(assoc_method.to_sym.like("%#{query}%")) if query.present?
|
60
|
+
dataset.all.map{ |r| [r.id, r.send(assoc_method)] }
|
61
|
+
else
|
62
|
+
dataset.all.map{ |r| [r.id, r.send(assoc_method)] }.select{ |id,value| value =~ /^#{query}/ }
|
63
|
+
end
|
64
|
+
else
|
65
|
+
# Options for a non-association attribute
|
66
|
+
res=@model_class.netzke_combo_options_for(column[:name], method_options)
|
67
|
+
|
68
|
+
# ensure it is an array-in-array, as Ext will fail otherwise
|
69
|
+
raise RuntimeError, "netzke_combo_options_for should return an Array" unless res.kind_of? Array
|
70
|
+
return [[]] if res.empty?
|
71
|
+
|
72
|
+
unless res.first.kind_of? Array
|
73
|
+
res=res.map do |v|
|
74
|
+
[v]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
return res
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def foreign_key_for assoc_name
|
83
|
+
@model_class.association_reflection(assoc_name.to_sym)[:key].to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the model class for an association
|
87
|
+
def class_for assoc_name
|
88
|
+
@model_class.association_reflection(assoc_name.to_sym)[:class_name].constantize
|
89
|
+
end
|
90
|
+
|
91
|
+
def destroy(ids)
|
92
|
+
@model_class.where(:id => ids).destroy
|
93
|
+
end
|
94
|
+
|
95
|
+
def find_record(id)
|
96
|
+
@model_class[id]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Build a hash of foreign keys and the associated model
|
100
|
+
def hash_fk_model
|
101
|
+
@model_class.all_association_reflections.inject({}) do |res, assoc|
|
102
|
+
res[assoc[:key]] = assoc[:class_name].constantize.model_name.underscore.to_sym
|
103
|
+
res
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# TODO: is this possible with Sequel?
|
108
|
+
def move_records(params)
|
109
|
+
end
|
110
|
+
|
111
|
+
# give the data adapter the opportunity the set special options for
|
112
|
+
# saving
|
113
|
+
def save_record(record)
|
114
|
+
# don't raise an error on saving. basepack will evaluate record.errors
|
115
|
+
# to get validation errors
|
116
|
+
record.raise_on_save_failure = false
|
117
|
+
record.save
|
118
|
+
end
|
119
|
+
|
120
|
+
# give the data adapter the opporunity to process error messages
|
121
|
+
# must return an raay of the form ["Title can't be blank", "Foo can't be blank"]
|
122
|
+
def errors_array(record)
|
123
|
+
record.errors.to_a.inject([]) do |errors, error|
|
124
|
+
field, message = error
|
125
|
+
errors << "#{record.class.human_attribute_name(field)} #{message.join ', '}"
|
126
|
+
errors
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Needed for seed and tests
|
131
|
+
def last
|
132
|
+
@model_class.last
|
133
|
+
end
|
134
|
+
|
135
|
+
# Needed for seed and tests
|
136
|
+
def destroy_all
|
137
|
+
@model_class.destroy
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
def get_dataset params, columns, for_count=false
|
142
|
+
dataset = @model_class
|
143
|
+
|
144
|
+
graphed=[]
|
145
|
+
|
146
|
+
# Parses and applies grid column filters
|
147
|
+
#
|
148
|
+
# Example column grid data:
|
149
|
+
#
|
150
|
+
# {"0" => {
|
151
|
+
# "data" => {
|
152
|
+
# "type" => "numeric",
|
153
|
+
# "comparison" => "gt",
|
154
|
+
# "value" => 10 },
|
155
|
+
# "field" => "id"
|
156
|
+
# },
|
157
|
+
# "1" => {
|
158
|
+
# "data" => {
|
159
|
+
# "type" => "string",
|
160
|
+
# "value" => "pizza"
|
161
|
+
# },
|
162
|
+
# "field" => "food_name"
|
163
|
+
# }}
|
164
|
+
#
|
165
|
+
|
166
|
+
if params[:filter]
|
167
|
+
# these are still JSON-encoded due to the migration to Ext.direct
|
168
|
+
column_filter=JSON.parse(params[:filter])
|
169
|
+
|
170
|
+
column_filter.each do |v|
|
171
|
+
field = v["field"]
|
172
|
+
assoc, method = field.split('__')
|
173
|
+
if method
|
174
|
+
# when filtering on association's columns, we need to graph for LEFT OUTER JOIN
|
175
|
+
dataset = dataset.eager_graph assoc.to_sym unless graphed.include? assoc.to_sym
|
176
|
+
graphed << assoc.to_sym
|
177
|
+
end
|
178
|
+
|
179
|
+
value = v["value"]
|
180
|
+
type = v["type"]
|
181
|
+
op = v["comparison"]
|
182
|
+
|
183
|
+
if type == "string"
|
184
|
+
# strings are always LIKEd (case-insensitive)
|
185
|
+
dataset = dataset.filter field.to_sym.ilike("%#{value}%")
|
186
|
+
else
|
187
|
+
if type == "date"
|
188
|
+
# convert value to the DB date
|
189
|
+
value.match /(\d\d)\/(\d\d)\/(\d\d\d\d)/
|
190
|
+
value = "#{$3}-#{$1}-#{$2}"
|
191
|
+
end
|
192
|
+
# if it's NOT an association column, we need to qualify column name with model's table_name
|
193
|
+
qualified_column_name = method ? field.to_sym : field.to_sym.qualify(@model_class.table_name)
|
194
|
+
case op
|
195
|
+
when 'lt'
|
196
|
+
dataset = dataset.filter ":column < '#{value}'", :column => qualified_column_name
|
197
|
+
when 'gt'
|
198
|
+
dataset = dataset.filter ":column > '#{value}'", :column => qualified_column_name
|
199
|
+
else
|
200
|
+
dataset = dataset.filter qualified_column_name => value
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
# skip sorting, eager joining and paging if dataset is used for count
|
206
|
+
unless for_count
|
207
|
+
if params[:sort] && sort_params = params[:sort]
|
208
|
+
sort_params.each do |sort_param|
|
209
|
+
assoc, method = sort_param["property"].split("__")
|
210
|
+
dir = sort_param["direction"].downcase
|
211
|
+
|
212
|
+
# if a sorting scope is set, call the scope with the given direction
|
213
|
+
column = columns.detect { |c| c[:name] == sort_param["property"] }
|
214
|
+
if column.try(:'has_key?', :sorting_scope)
|
215
|
+
dataset = dataset.send(column[:sorting_scope].to_sym, dir.to_sym)
|
216
|
+
else
|
217
|
+
if method # sorting on associations column
|
218
|
+
# graph the association for LEFT OUTER JOIN
|
219
|
+
dataset = dataset.eager_graph(assoc.to_sym) unless graphed.include? assoc.to_sym
|
220
|
+
graphed << assoc.to_sym
|
221
|
+
end
|
222
|
+
# coincidentally, netzkes convention of specifying association's attributes
|
223
|
+
# i.e. "author__name" on Book matches sequel's convention
|
224
|
+
# so we can just pass symbolized property here
|
225
|
+
dataset = dataset.order(sort_param["property"].to_sym.send(dir))
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# eager load the associations indicated by columns,
|
231
|
+
# but only if we didn't eager_graph them before (for ordering/filtering)
|
232
|
+
# because this saves a ID IN query
|
233
|
+
columns.each do |column|
|
234
|
+
if column[:name].index('__')
|
235
|
+
assoc, _ = column[:name].split('__')
|
236
|
+
dataset = dataset.eager(assoc.to_sym) unless graphed.include? assoc.to_sym
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# apply paging
|
241
|
+
if params[:limit]
|
242
|
+
if params[:start]
|
243
|
+
dataset = dataset.limit params[:limit], params[:start]
|
244
|
+
else
|
245
|
+
dataset = dataset.limit params[:limit]
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# apply scope
|
251
|
+
# need to symbolize_keys, because when the request is made from client-side (as opposed
|
252
|
+
# to server-side on inital render), the scope's keys are given as string {"author_id" => 1}
|
253
|
+
# If we give Sequel a filter like this, it will (correctly) do WHERE 'author_id' = 1 - note the quotes
|
254
|
+
# making the database match the string author_id to 1 and to the column.
|
255
|
+
dataset = dataset.extend_with(params[:scope].symbolize_keys) if params[:scope]
|
256
|
+
dataset
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require "netzke/basepack/form_panel/fields"
|
2
|
+
require "netzke/basepack/form_panel/services"
|
3
|
+
# require "netzke/plugins/configuration_tool"
|
4
|
+
|
5
|
+
module Netzke
|
6
|
+
module Basepack
|
7
|
+
# Ext.form.Panel-based component
|
8
|
+
#
|
9
|
+
# == Netzke-specific config options
|
10
|
+
#
|
11
|
+
# * +model+ - name of the ActiveRecord model that provides data to this GridPanel.
|
12
|
+
# * +record+ - record to be displayd in the form. Takes precedence over +:record_id+
|
13
|
+
# * +record_id+ - id of the record to be displayd in the form. Also see +:record+
|
14
|
+
# * +items+ - the layout of the fields as an array. See "Layout configuration".
|
15
|
+
# * +mode+ - render mode, accepted options:
|
16
|
+
# * +lockable+ - makes the form panel load initially in "display mode", then lets "unlock" it, change the values, and "lock" it again, while updating the values on the server
|
17
|
+
# * +updateMask+ - +Ext.LoadMask+ config options for the mask shown while the form is submitting its values
|
18
|
+
#
|
19
|
+
# === Layout configuration
|
20
|
+
#
|
21
|
+
# The layout of the form is configured by supplying the +item+ config option, same way it would be configured in Ext (thus allowing for complex form layouts). FormPanel will expand fields by looking at their names (unless +no_binding+ set to +true+ is specified for a specific field).
|
22
|
+
#
|
23
|
+
# == Endpoints
|
24
|
+
# FormPanel implements the following endpoints:
|
25
|
+
#
|
26
|
+
# * +netzke_load+ - loads a record with a given id from the server, e.g.:
|
27
|
+
#
|
28
|
+
# someFormPanel.netzkeLoad({id: 100});
|
29
|
+
#
|
30
|
+
# * +netzke_submit+ - gets called when the form gets submitted (e.g. by pressing the Apply button, or by calling onApply)
|
31
|
+
# * +get_combobox_options+ - gets called when a 'remote' combobox field gets expanded
|
32
|
+
class FormPanel < Netzke::Base
|
33
|
+
|
34
|
+
js_base_class "Ext.form.Panel"
|
35
|
+
|
36
|
+
# Class-level configuration
|
37
|
+
class_attribute :config_tool_available
|
38
|
+
self.config_tool_available = true
|
39
|
+
|
40
|
+
include self::Services
|
41
|
+
include self::Fields
|
42
|
+
include Netzke::Basepack::DataAccessor
|
43
|
+
|
44
|
+
delegates_to_dsl :model, :record_id
|
45
|
+
|
46
|
+
action :apply do
|
47
|
+
{
|
48
|
+
:text => I18n.t('netzke.basepack.form_panel.actions.apply'),
|
49
|
+
:tooltip => I18n.t('netzke.basepack.form_panel.actions.apply_tooltip'),
|
50
|
+
:icon => :tick
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
action :edit do
|
55
|
+
{
|
56
|
+
:text => I18n.t('netzke.basepack.form_panel.actions.edit'),
|
57
|
+
:tooltip => I18n.t('netzke.basepack.form_panel.actions.edit_tooltip'),
|
58
|
+
:icon => :pencil
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
action :cancel do
|
63
|
+
{
|
64
|
+
:text => I18n.t('netzke.basepack.form_panel.actions.cancel'),
|
65
|
+
:tooltip => I18n.t('netzke.basepack.form_panel.actions.cancel_tooltip'),
|
66
|
+
:icon => :cancel
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def configuration
|
71
|
+
super.tap do |sup|
|
72
|
+
configure_locked(sup)
|
73
|
+
configure_bbar(sup)
|
74
|
+
|
75
|
+
sup[:record_id] = sup[:record] = nil if sup[:multi_edit] # never set record_id in multi-edit mode
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def configure_locked(c)
|
80
|
+
c[:locked] = c[:locked].nil? ? (c[:mode] == :lockable) : c[:locked]
|
81
|
+
end
|
82
|
+
|
83
|
+
def configure_bbar(c)
|
84
|
+
c[:bbar] = [:apply.action] if c[:bbar].nil? && !c[:read_only]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Extra JavaScripts and stylesheets
|
88
|
+
js_mixin :form_panel
|
89
|
+
js_include :comma_list_cbg
|
90
|
+
js_include :n_radio_group, :readonly_mode
|
91
|
+
css_include :readonly_mode
|
92
|
+
|
93
|
+
# WIP
|
94
|
+
# js_include Netzke::Core.ext_path.join("examples/ux/fileuploadfield/FileUploadField.js")
|
95
|
+
# css_include Netzke::Core.ext_path.join("examples/ux/fileuploadfield/css/fileuploadfield.css")
|
96
|
+
|
97
|
+
# WIP: Needed for FileUploadField
|
98
|
+
# js_include :misc
|
99
|
+
|
100
|
+
def js_config
|
101
|
+
super.tap do |res|
|
102
|
+
res[:pri] = data_class && data_class.primary_key.to_s
|
103
|
+
res[:record] = js_record_data if record
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# A hash of record data including the meta field
|
108
|
+
def js_record_data
|
109
|
+
record.netzke_hash(fields).merge(:_meta => meta_field).literalize_keys
|
110
|
+
end
|
111
|
+
|
112
|
+
def record
|
113
|
+
@record ||= config[:record] || config[:record_id] && data_class && data_adapter.find_record(config[:record_id])
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def self.server_side_config_options
|
119
|
+
super + [:record, :scope]
|
120
|
+
end
|
121
|
+
|
122
|
+
def meta_field
|
123
|
+
{}.tap do |res|
|
124
|
+
assoc_values = get_association_values
|
125
|
+
res[:association_values] = assoc_values.literalize_keys if record && !assoc_values.empty?
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def get_association_values
|
130
|
+
fields_that_need_associated_values = fields.select{ |k,v| k.to_s.index("__") && !fields[k][:nested_attribute] }
|
131
|
+
# Take care of Ruby 1.8.7
|
132
|
+
if fields_that_need_associated_values.is_a?(Array)
|
133
|
+
fields_that_need_associated_values = fields_that_need_associated_values.inject({}){|r,(k,v)| r.merge(k => v)}
|
134
|
+
end
|
135
|
+
|
136
|
+
fields_that_need_associated_values.each_pair.inject({}) do |r,(k,v)|
|
137
|
+
r.merge(k => record.value_for_attribute(fields_that_need_associated_values[k], true))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# include ::Netzke::Plugins::ConfigurationTool if config_tool_available # it will load ConfigurationPanel into a modal window
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
module Netzke
|
2
|
+
module Basepack
|
3
|
+
class FormPanel < Netzke::Base
|
4
|
+
# Because FormPanel allows for arbitrary layout of fields, we need to have all fields configured in one place (the +fields+ method), and then have references to those fields from +items+.
|
5
|
+
module Fields
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
# Items with normalized fields (i.e. containing all the necessary attributes needed by Ext.form.FormPanel to render a field)
|
9
|
+
def items
|
10
|
+
@form_panel_items ||= begin
|
11
|
+
res = normalize_fields(super || data_class && data_class.netzke_attributes || []) # netzke_attributes as default items
|
12
|
+
# if primary key isn't there, insert it as first
|
13
|
+
if data_class && !res.detect{ |f| f[:name] == data_class.primary_key.to_s}
|
14
|
+
primary_key_item = normalize_field(data_class.primary_key.to_sym)
|
15
|
+
@fields_from_config[data_class.primary_key.to_sym] = primary_key_item
|
16
|
+
res.insert(0, primary_key_item)
|
17
|
+
end
|
18
|
+
res
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Hash of fully configured fields, that are referenced in the items. E.g.:
|
23
|
+
# {
|
24
|
+
# :role__name => {:xtype => 'netzkeremotecombo', :disabled => true, :value => "admin"},
|
25
|
+
# :created_at => {:xtype => 'datetime', :disabled => true, :value => "2010-10-10 10:10"}
|
26
|
+
# }
|
27
|
+
def fields
|
28
|
+
@fields ||= begin
|
29
|
+
if static_layout?
|
30
|
+
# extract incomplete field configs from +config+
|
31
|
+
flds = fields_from_config
|
32
|
+
# and merged them with fields from the model
|
33
|
+
deep_merge_existing_fields(flds, fields_from_model) if data_class
|
34
|
+
else
|
35
|
+
# extract flds configs from the model
|
36
|
+
flds = fields_from_model
|
37
|
+
end
|
38
|
+
flds
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# The array of fields as specified on the model level (using +netzke_attribute+ and alike)
|
43
|
+
def fields_array_from_model
|
44
|
+
data_class && data_class.netzke_attributes
|
45
|
+
end
|
46
|
+
|
47
|
+
# Hash of fields as specified on the model level
|
48
|
+
def fields_from_model
|
49
|
+
@fields_from_model ||= fields_array_from_model && fields_array_from_model.inject({}){ |hsh, f| hsh.merge(f[:name].to_sym => f) }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Hash of normalized field configs extracted from :items, e.g.:
|
53
|
+
#
|
54
|
+
# {:role__name => {:xtype => "netzkeremotecombo"}, :password => {:xtype => "passwordfield"}}
|
55
|
+
def fields_from_config
|
56
|
+
items if @fields_from_config.nil? # by calling +items+ we initiate building of @fields_from_config
|
57
|
+
@fields_from_config ||= {}
|
58
|
+
end
|
59
|
+
|
60
|
+
module ClassMethods
|
61
|
+
# Columns to be displayed by the FieldConfigurator, "meta-columns". Each corresponds to a configuration
|
62
|
+
# option for each field in the form.
|
63
|
+
def meta_columns
|
64
|
+
[
|
65
|
+
{:name => "included", :attr_type => :boolean, :width => 40, :header => "Incl", :default_value => true},
|
66
|
+
{:name => "name", :attr_type => :string, :editor => :netzkeremotecombo, :width => 200},
|
67
|
+
{:name => "label", :attr_type => :string, :header => "Label"},
|
68
|
+
{:name => "default_value", :attr_type => :string}
|
69
|
+
]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def load_persistent_fields
|
75
|
+
# NetzkeFieldList.read_list(global_id) if persistent_config_enabled?
|
76
|
+
end
|
77
|
+
|
78
|
+
def load_model_level_attrs
|
79
|
+
# NetzkeModelAttrList.read_list(data_class.name) if persistent_config_enabled? && data_class
|
80
|
+
end
|
81
|
+
|
82
|
+
# This is where we expand our basic field config with all the defaults
|
83
|
+
def normalize_field(field)
|
84
|
+
# field can only be a string, a symbol, or a hash
|
85
|
+
if field.is_a?(Hash)
|
86
|
+
field = field.dup # we don't want to modify original hash
|
87
|
+
return field if field[:no_binding] # stop here if no normalization is needed
|
88
|
+
field[:name] = field[:name].to_s if field[:name] # all names should be strings
|
89
|
+
else
|
90
|
+
field = {:name => field.to_s}
|
91
|
+
end
|
92
|
+
|
93
|
+
field_from_model = fields_from_model && fields_from_model[field[:name].to_sym]
|
94
|
+
|
95
|
+
field_from_model && field.merge!(field_from_model)
|
96
|
+
|
97
|
+
detect_association_with_method(field) # xtype for an association field
|
98
|
+
set_default_field_label(field)
|
99
|
+
set_default_field_xtype(field) if field[:xtype].nil?
|
100
|
+
set_default_read_only(field)
|
101
|
+
|
102
|
+
# temporal datetime setup, while we don't have real datetime field
|
103
|
+
if field[:attr_type] == :date
|
104
|
+
field[:format] ||= "Y-m-d"
|
105
|
+
end
|
106
|
+
|
107
|
+
# provide our special combobox with our id
|
108
|
+
field[:parent_id] = self.global_id if field[:xtype] == :netzkeremotecombo
|
109
|
+
|
110
|
+
field[:hidden] = field[:hide_label] = true if field[:hidden].nil? && primary_key_attr?(field)
|
111
|
+
|
112
|
+
# checkbox setup
|
113
|
+
field[:checked] = field[:value] if field[:attr_type] == :boolean
|
114
|
+
field[:input_value] = true if field[:attr_type] == :boolean
|
115
|
+
|
116
|
+
field
|
117
|
+
end
|
118
|
+
|
119
|
+
# Sets the proper xtype of an asociation field
|
120
|
+
def detect_association_with_method(c)
|
121
|
+
if c[:name].index('__')
|
122
|
+
assoc_name, method = c[:name].split('__').map(&:to_sym)
|
123
|
+
assoc_method_type = data_adapter.get_assoc_property_type(assoc_name, method)
|
124
|
+
if c[:nested_attribute]
|
125
|
+
c[:xtype] ||= xtype_for_attr_type(assoc_method_type)
|
126
|
+
else
|
127
|
+
c[:xtype] ||= assoc_method_type == :boolean ? xtype_for_attr_type(assoc_method_type) : xtype_for_association
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# RECURSIVELY extracts fields configuration from :items
|
133
|
+
def normalize_fields(items)
|
134
|
+
@fields_from_config ||= {}
|
135
|
+
items.map do |item|
|
136
|
+
# at this moment, item is a hash or a symbol
|
137
|
+
if is_field_config?(item)
|
138
|
+
item = normalize_field(item)
|
139
|
+
@fields_from_config[item[:name].to_sym] = item
|
140
|
+
item #.reject{ |k,v| k == :name } # do we really need to remove the :name key?
|
141
|
+
elsif item.is_a?(Hash)
|
142
|
+
item = item.dup # we don't want to modify original hash
|
143
|
+
item[:items].is_a?(Array) ? item.merge(:items => normalize_fields(item[:items])) : item
|
144
|
+
else
|
145
|
+
item
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def is_field_config?(item)
|
151
|
+
item.is_a?(String) || item.is_a?(Symbol) || item[:name] # && !is_component_config?(item)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Deeply merges only those key/values at the top level that are already there
|
155
|
+
def deep_merge_existing_fields(dest, src)
|
156
|
+
dest.each_pair do |k,v|
|
157
|
+
v.deep_merge!(src[k] || {})
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def set_default_field_label(c)
|
162
|
+
# multiple spaces (in case of association attrs) get replaced with one
|
163
|
+
c[:field_label] ||= data_class ? data_class.human_attribute_name(c[:name]) : c[:name].humanize
|
164
|
+
c[:field_label].gsub!(/\s+/, " ")
|
165
|
+
end
|
166
|
+
|
167
|
+
def set_default_field_xtype(field)
|
168
|
+
field[:xtype] = xtype_for_attr_type(field[:attr_type]) unless xtype_for_attr_type(field[:attr_type]).nil?
|
169
|
+
end
|
170
|
+
|
171
|
+
def set_default_read_only(field)
|
172
|
+
enabled_if = !data_class || data_class.column_names.include?(field[:name])
|
173
|
+
enabled_if ||= data_class.instance_methods.map(&:to_s).include?("#{field[:name]}=")
|
174
|
+
enabled_if ||= record && record.respond_to?("#{field[:name]}=")
|
175
|
+
enabled_if ||= association_attr?(field[:name])
|
176
|
+
|
177
|
+
field[:read_only] = !enabled_if if field[:read_only].nil?
|
178
|
+
end
|
179
|
+
|
180
|
+
def attr_type_to_xtype_map
|
181
|
+
{
|
182
|
+
:integer => :numberfield,
|
183
|
+
:boolean => config[:multi_edit] ? :tricheckbox : :checkboxfield,
|
184
|
+
:date => :datefield,
|
185
|
+
:datetime => :xdatetime,
|
186
|
+
:text => :textarea,
|
187
|
+
:json => :jsonfield,
|
188
|
+
:string => :textfield
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
def xtype_for_attr_type(type)
|
193
|
+
attr_type_to_xtype_map[type] || :textfield
|
194
|
+
end
|
195
|
+
|
196
|
+
def xtype_for_association
|
197
|
+
:netzkeremotecombo
|
198
|
+
end
|
199
|
+
|
200
|
+
# Are we provided with a static field layout?
|
201
|
+
def static_layout?
|
202
|
+
!!config[:items]
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|