diff --git a/.autotest b/.autotest new file mode 100644 index 0000000000..4acb0c6a6f --- /dev/null +++ b/.autotest @@ -0,0 +1,27 @@ +class Autotest + ## + # Convert a path in a string, s, into a class name, changing + # underscores to CamelCase, etc. + + def path_to_classname(s) + sep = File::SEPARATOR + f = s.sub(/^test#{sep}/, '').sub(/\.rb$/, '').split(sep) + f = f.map { |path| path.split(/_|(\d+)/).map { |seg| seg.capitalize }.join } + f = f.map { |path| path =~ /Test$/ ? path : "#{path}Test" } + f.join('::') + end +end + +Autotest.add_hook :initialize do |at| + # doesn't seem to work + # at.clear_mappings + + at.add_mapping(/^lib\/.*\.rb$/) do |filename, _| + possible = File.basename(filename, 'rb').gsub '_', '_?' + files_matching %r%^test/.*#{possible}_test\.rb$% + end + + at.add_mapping(/^test.*\/.*_test\.rb$/) do |filename, _| + filename + end +end \ No newline at end of file diff --git a/.document b/.document new file mode 100644 index 0000000000..69db7194cb --- /dev/null +++ b/.document @@ -0,0 +1,5 @@ +lib/**/*.rb +bin/* +- +features/**/*.feature +MIT-LICENSE.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..fde8b5163b --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# rcov generated +coverage + +# rdoc generated +rdoc + +# yard generated +doc +.yardoc + +# bundler +.bundle + +# jeweler generated +pkg + +# sass generated +.sass-cache + +# logs +log/** + +# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: +# +# * Create a file at ~/.gitignore +# * Include files you want ignored +# * Run: git config --global core.excludesfile ~/.gitignore +# +# After doing this, these files will be ignored in all your git projects, +# saving you from having to 'pollute' every project you touch with them +# +# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) +# +# For MacOS: +# +#.DS_Store +# +# For TextMate +#*.tmproj +#tmtags +# +# For emacs: +#*~ +#\#* +#.\#* +# +# For vim: +*.swp +# +# For kdevelop: +*.kdev4 +.project +config/initializers/localeapp.rb diff --git a/CHANGELOG b/CHANGELOG index 635c051fef..2816afd12e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,232 @@ += 3.3.3 +- Allow to override select options in active_scaffold_search_select +- Load effects from jQuery UI when using jquery-rails 3 gem +- Fix searching on nested scaffolds (broken on 3.3.2) +- Fix searching when includes is set to nil +- Send parent_controller for render_field requests on subforms for persisted records, as new records were already sending it +- Avoid loading some JS when jquery-ui is not available +- Load CSS from jquery ui rails gem + += 3.3.2 +- Fix subforms inside subforms +- Fix draggable for jquery-rails 3 gem +- Support to change attributes in list column headings with column_heading_attributes helper +- Keep url for embedded in data-refresh attr of active-scaffold-component div +- Remove duplicated results when searching with outer_joins in plural associations + += 3.3.1 +- Set adapter colspan (nested lists and forms) using javascript, fixed issue when active_scaffold_config_list is used +- Fix bug saving default values when get method is overrided on model to return default value +- Fix close action_link without adapter +- Fix update calculations after destroy a row +- Fix count and calculate searching with count_includes +- Fix action_after_create +- Cache associations options (for :select form_ui on associations), it can be disabled with config.cache_association_options = false +- Clean some code so we can stop changing @record in partials and using @record in helpers +- Support jquery-rails 3 gem and jquery-ui-rails gem +- Clean some invalid HTML +- Add store_user_settings option to enable storing sort, page and search params into session (enabled by default for backwards compatibility) +- Fix each_record_in_scope when conditions_for_collection needs some includes +- Add method to override label in search fields (field_search) +- Add method to override attributes for column headings on list +- Support security_method and ignore_method in column's link +- render :super should work for every render call (with partial and locals or with only one hash) + += 3.3.0 +- Unify field overrides and list_ui method signatures +- Improve performance removing some partials and adding some caching for helper overrides and UIs, and caching url generation for lists (caching url is optional) +- Drop support for rails 3.1 +- Add HTML5 form fields +- Add :chosen form_ui, and :chosen and :multi_chosen search_ui +- Fix nested with has_one associations, and belongs_to associations with :list action +- Support optgroup in :select and :chosen form_ui +- Support render_form_field with multiple rows (rows with associated-record class) +- Support for updating columns, row or table after updating a column with inplace_edit +- Keep order for records in plural subforms, so new records are saved in same order as form order +- Support add_subgroup in horizontal subforms +- Improve support in helpers for using :select form_ui with polymorphic associations (needs to use a select to choose the class) +- Allow to create in nested has_many :through associations, when source association is a belongs_to +- Fix calculations using field_search with has_many includes +- Add support to ActiveScaffold.create_record_row to insert after or before of an element +- Add support for dynamic action group +- Improve parsing of datetimes +- Add support for lists filtered by id (one-item lists) +- Fix update_columns with sending whole big forms +- Improve update_columns on subforms +- Support to override ActiveScaffold.remove so effects can be added on page elements deletion +- Add support for conversion methods in controller, so form_uis can define how to convert params to values +- Avoid sorting by contraint columns +- Cosmetic fixes and improvements +- Fix multipart persistent update form +- Support jquery 1.9 (jquery-rails 2.2.0 gem) +- Reduce usage of session +- Allow to include header on list refreshing +- Add search_joins to columns, so it's possible to do left joins without loading associations only for searching +- Fix set focus for inplace edit clone and ajax +- Allow to override label for list column headings with a new helper method column_heading_label +- Keep focus on last field when render_field is called +- Add empty create and update methods to tableless +- Allow multiple subform levels + += 3.2.20 +- Some fixes for ruby 1.9 +- Some fixes for rails 3.1 +- Fix detection of empty for has_and_belongs_to_many on subforms + += 3.2.19 +- Avoid crashing when between is chosen and from or to is not filled + += 3.2.18 +- Fix add existing record with record_select +- Disable link for polymorphic associations in 3.2.x, it doesn't work + += 3.2.17 +- fix constraints for columns with multiple columns in search_sql +- remove unauthorized collection links +- copy parameters and html_options on cloning action link +- fix missing argument in datepicker conditions +- check authorization in model for plural associations nested links +- Allow to set action_group in nested.add_link +- fix current page when is inside outer window +- Support record select with polymorphic associations +- Fix column count in inline adapter for forms and views +- Add support for class prefix on human condition helpers + += 3.2.16 +- Fix use of column.css_class in form when is a proc, it wasn't skipped when form was a columns group +- Fix constraints and colspan in self-referential associations +- Fix rendering parent row after creating or updating in a nested form (singular associations) +- Translate include_blank and prompt in form_ui :select +- Fix mark for nested scaffolds +- Fix STI creation +- Fix null search for numeric columns +- Fix recordselect inplace_edit with plural associations in ruby 1.9 + += 3.2.15 +- Prepare to unify field overrides and list_ui method signatures +- Add :optional to update.persistent +- Add missing triggering of as:element_updated in new subform rows +- Fix conditions for :select in field_search +- Fix datetime field search +- Fix authorization checking in member action links + += 3.2.14 +- Fix default sorting, it was broken in 3.2.13 + += 3.2.13 +- Fix destroy action, was broken in 3.2.12 +- Remove default :method sorting for associations, it wasn't useful and can be slow +- Rescue from ActiveScaffold::ActionNotAllowed and ActiveScaffold::RecordNotAllowed with 401 response, it can be overrided with deny_access method in ApplicationController. +- Allow to set arrays in search_sql so one column can search in multiple db columns, resulting sql chunks will be OR'ed +- Hide text input on searching for null or not null +- Add some classes to forms to improve CSS customization +- Fix nested links for STI controllers with common configuration in a base controller +- Fix adding routes twice in active_scaffold generator +- Restore old behavior for action_link's security_method, ignore_method already hides the link +- Add data-cancel-refresh to action links, so nested scaffold's behaviour can be applied to other action links and when adapter is closed row will be refreshed. +- fix support for tableless models in rails >= 3.2.5 + += 3.2.12 +- improve support for tableless models, add support for count +- fix div id for nested scaffolds +- add config.timestamped_messages and config.highlight_messages +- Improve performance using class to check authorization for plural associations which are not loaded, and avoiding to load associations if it's not needed +- Fix using a scope in main form, it wasn't working for associations (for create multiple from active_scaffold_batch) +- Fix searches with no results, it was loading first page without conditions +- Allow to change highlight options with ActiveScaffold.js_config +- allow to override column count calculation for colspan +- Remove conditions_from_params from nested links +- Fix nested forms for singular associations +- enable to get constraints in helpers, so can be used in options_for_association_conditions, for example +- Allow to edit has_one :through association in nested forms +- Fix cancan bridge broken in a previous release +- Fix mark action + += 3.2.11 +- improve support for tableless models and active_scaffold_batch +- fix count includes which were broken in 3.2.9 +- remove duplicated conditions in nested scaffolds, added by conditions_from_params + += 3.2.10 +- fix nested scaffolds with update action disabled +- initial work for tableless support (index with limited options) +- fix scrolling on closing nested scaffolds +- fix calculations which were broken in 3.2.9 + += 3.2.9 +- remove duplicated conditions with constraints +- fix constraints for polymorphic associations + += 3.2.8 +- add deprecation for update_column, update_columns should be used instead +- fix constraints with hide_nested_column disabled in list and embedded scaffolds which are nested too +- fix setting a hash as includes, cannot be concat in finder +- add option to scroll on close only if element is not visible in viewport (default now) +- fix create and edit singular associations without render_component +- messages go across all table +- display readonly associations in forms, it was ready to display them but it was skipping them +- Some fixes for inplace editors (cloning form overrides, ajax and radiobuttons). Add handlers for empty columns +- add wrap_tag to list so cells content can be wrapped in a tag for better styling +- fix date picker parsing for datetime fields when jquery is used +- add as:element_updated js event when replace or replace_html is called +- rescue database exceptions so you get error messages for it insted of error 500, for example in case you forgot to check uniqueness for a unique index +- add class attribute for displayed unauthorized columns in forms, so they get same styling + += 3.2.7 +- restore missing update.persistent feature +- fix create.persistent +- add new record in first scaffold, not in all nested scaffolds +- fix deleting all in habtm with select_ui + += 3.2.6 +- fix ordering with DISTINCT call +- allow to override some human condition strings in i18n, and translate select values for non-association columns + += 3.2.5 +- add outer window for pagination links +- workaround rails 3.2.3 bug in TimeWithZone#to_time +- fix date picker format for datetime fields +- fix bitfields bridge when some actions are not used +- use core config label when is set and STI is not used + += 3.2.4 +- don't break on custom SQL sorting (e.g. sorting with functions) +- fix cancel inplace edit with jquery +- fix nested scaffolds inside nested and embedded scaffolds +- fix collapsed subgroups +- fix inplace edit for checkboxes columns with true as default value +- fix calendar date select bridge + += 3.0.6 .. 3.2.3 +- many changes + += 3.0.5 + +- switch from explicit requires to autoloading +- get nested action links up and running + += 3.0.4 + +- Fix a typo that made 3.0.3 unusable. + += 3.0.3 + +- Fixing 'require' ordering +- Code for nested and subgrouped action links +- bugfixes for better html generation + += 3.0.2 + +- Two bug fixes and a deprecation silencing. + += 3.0.1 + +- Fixing the requiring of files. + += 3.0.0 + +- Packaging up vhochstein's fork for Rails 3.0 as a gem. + = 1.2RC1 == FEATURES @@ -149,4 +378,4 @@ * fixes for edge rails compatibility * small improvements for localization accessibility * minor string renaming (will affect localization tables, though) -* closed a few XSS holes \ No newline at end of file +* closed a few XSS holes diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..24478d943d --- /dev/null +++ b/Gemfile @@ -0,0 +1,28 @@ +source "http://rubygems.org" +# Add dependencies required to use your gem here. +# Example: +# gem "activesupport", ">= 2.3.5" + +# Add dependencies to develop your gem here. +# Include everything needed to run rake, tests, features, etc. +group :development, :test do + gem "rake" + gem "rdoc" + gem "bundler", ">= 1.0.0" + gem "localeapp" + gem "rack" +end + +group :test do + gem "shoulda", ">= 0" + gem "simplecov", ">= 0" + gem "mocha" + gem "rails", "~> 3.2.6" + platforms :jruby do + gem 'activerecord-jdbcsqlite3-adapter' + end + + platforms :ruby do + gem "sqlite3" + end +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..713e793a44 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,114 @@ +GEM + remote: http://rubygems.org/ + specs: + actionmailer (3.2.13) + actionpack (= 3.2.13) + mail (~> 2.5.3) + actionpack (3.2.13) + activemodel (= 3.2.13) + activesupport (= 3.2.13) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.4) + rack (~> 1.4.5) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.2.1) + activemodel (3.2.13) + activesupport (= 3.2.13) + builder (~> 3.0.0) + activerecord (3.2.13) + activemodel (= 3.2.13) + activesupport (= 3.2.13) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activeresource (3.2.13) + activemodel (= 3.2.13) + activesupport (= 3.2.13) + activesupport (3.2.13) + i18n (= 0.6.1) + multi_json (~> 1.0) + arel (3.0.2) + builder (3.0.4) + erubis (2.7.0) + gli (2.5.6) + hike (1.2.3) + i18n (0.6.1) + journey (1.0.4) + json (1.8.0) + localeapp (0.6.9) + gli + i18n + json + rest-client + ya2yaml + mail (2.5.4) + mime-types (~> 1.16) + treetop (~> 1.4.8) + metaclass (0.0.1) + mime-types (1.23) + mocha (0.13.3) + metaclass (~> 0.0.1) + multi_json (1.7.7) + polyglot (0.3.3) + rack (1.4.5) + rack-cache (1.2) + rack (>= 0.4) + rack-ssl (1.3.3) + rack + rack-test (0.6.2) + rack (>= 1.0) + rails (3.2.13) + actionmailer (= 3.2.13) + actionpack (= 3.2.13) + activerecord (= 3.2.13) + activeresource (= 3.2.13) + activesupport (= 3.2.13) + bundler (~> 1.0) + railties (= 3.2.13) + railties (3.2.13) + actionpack (= 3.2.13) + activesupport (= 3.2.13) + rack-ssl (~> 1.3.2) + rake (>= 0.8.7) + rdoc (~> 3.4) + thor (>= 0.14.6, < 2.0) + rake (10.1.0) + rdoc (3.12.2) + json (~> 1.4) + rest-client (1.6.7) + mime-types (>= 1.16) + shoulda (2.11.3) + simplecov (0.7.1) + multi_json (~> 1.0) + simplecov-html (~> 0.7.1) + simplecov-html (0.7.1) + sprockets (2.2.2) + hike (~> 1.2) + multi_json (~> 1.0) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + sqlite3 (1.3.7) + thor (0.18.1) + tilt (1.4.1) + treetop (1.4.14) + polyglot + polyglot (>= 0.3.1) + tzinfo (0.3.37) + ya2yaml (0.31) + +PLATFORMS + ruby + +DEPENDENCIES + activerecord-jdbcsqlite3-adapter + bundler (>= 1.0.0) + localeapp + mocha + rack + rails (~> 3.2.6) + rake + rdoc + shoulda + simplecov + sqlite3 diff --git a/README b/README deleted file mode 100644 index 5a7ad8f42e..0000000000 --- a/README +++ /dev/null @@ -1,31 +0,0 @@ -********************************************************************************** -** For all documentation see the project website: http://www.ActiveScaffold.com ** -********************************************************************************** - -ActiveScaffold plugin by Scott Rutherford (scott@caronsoftware.com), Richard White (rrwhite@gmail.com), Lance Ivy (lance@cainlevy.net), Ed Moss, and Tim Harper - -Uses DhtmlHistory by Brad Neuberg (bkn3@columbia.edu) -http://codinginparadise.org - -Uses Querystring by Adam Vandenberg -http://adamv.com/dev/javascript/querystring - -Uses Paginator by Bruce Williams -http://paginator.rubyforge.org/ - -Supports RecordSelect by Lance Ivy -http://code.google.com/p/recordselect/ - -== Version Information - -Please note the following list of Active Scaffold branches and Rails versions. Master will not work with Rails < 2.2 - -Rails master (edge): Active Scaffold master -Rails 2.2.*: Active Scaffold rails-2.2 -Rails 2.1.*: Active Scaffold rails-2.1 -Rails < 2.1: Active Scaffold 1-1-stable (no guarantees) - -Since Rails 2.3, render_component plugin is needed for nested and embbeded scaffolds. It works with rails-2.3 branch from ewildgoose repository: -script/plugin install git://github.com/ewildgoose/render_component.git -r rails-2.3 - -Released under the MIT license (included) diff --git a/README.md b/README.md new file mode 100644 index 0000000000..628f8b2b8a --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +Overview +======== +ActiveScaffold provides a quick and powerful user interfaces for CRUD (create, read, update, delete) operations for Rails applications. It offers additonal features including searching, pagination & layout control. + +Branch Details +-------------- +rails-3.2 branch on Github supports Rails 3.1 & 3.2, and is the current source of the 3.2.x line of gems. The master branch (3.3.x) has dropped support for Rails 3.1 + +Quick Start +----------- +To get started with a new Rails project + +Added to Gemfile + + gem 'active_scaffold' + +Run the following commands + + bundle install + bundle exec rake db:create + rails g active_scaffold User name:string + bundle exec rake db:migrate + +Add the following line to app/assets/javascripts/application.js + + //= require active_scaffold + +Add the following line to /app/assets/stylesheets/application.css + + *= require active_scaffold + +Run the app and visit localhost:3000/teams + +Configuration +------------- +See Wiki for instructions on customising ActiveScaffold and to find the full API details. + +Compatability Issues +-------------------- +jQuery 1.9 deprecates some methods that rails-3.2 branch still uses (NB: jQuery 1.9 is supported in 3.3.x, the master branch). You'll therefore need to ensure you use jQuery 1.8. You can do this by fixing version in your Gemfile: + + gem 'jquery-rails', '2.1.4' + +active_scaffold_batch plugin gem (versions 3.2.x) require 3.3.x (master branch). Therefore if you wish to try using active_scaffold_batch with this branch, you'll need to fork the project and edit the runtime dependency in the gemspec file (use at your own discretion) + +Credits +------- +ActiveScaffold grew out of a project named Ajaxscaffold dating back to 2006. It has had numerous contributors including: + +ActiveScaffold Gem/Plugin by Scott Rutherford (scott@caronsoftware.com), Richard White (rrwhite@gmail.com), Lance Ivy (lance@cainlevy.net), Ed Moss, Tim Harper and Sergio Cambra (sergio@entrecables.com) + +Uses DhtmlHistory by Brad Neuberg (bkn3@columbia.edu) +http://codinginparadise.org + +Uses Querystring by Adam Vandenberg +http://adamv.com/dev/javascript/querystring + +Uses Paginator by Bruce Williams +http://paginator.rubyforge.org/ + +Supports RecordSelect by Lance Ivy +http://code.google.com/p/recordselect/ + + +License +======= +Released under the MIT license (included) diff --git a/Rakefile b/Rakefile index 785a135289..4f4b8f34dc 100644 --- a/Rakefile +++ b/Rakefile @@ -1,15 +1,20 @@ require 'rake' +require 'bundler' +begin + Bundler.setup(:default, :development) +rescue Bundler::BundlerError => e + $stderr.puts e.message + $stderr.puts "Run `bundle install` to install missing gems" + exit e.status_code +end +Bundler::GemHelper.install_tasks require 'rake/testtask' -require 'rake/packagetask' -require 'rake/rdoctask' +require 'rdoc/task' require 'find' -desc 'Default: run unit tests.' -task :default => :test - desc 'Test ActiveScaffold.' Rake::TestTask.new(:test) do |t| - t.libs << 'lib' + t.libs << 'lib' << 'test' t.pattern = 'test/**/*_test.rb' t.verbose = true end @@ -17,8 +22,8 @@ end desc 'Generate documentation for ActiveScaffold.' Rake::RDocTask.new(:rdoc) do |rdoc| rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'ActiveScaffold' + rdoc.title = 'ActiveScaffold #{ActiveScaffold::Version::STRING}' rdoc.options << '--line-numbers' << '--inline-source' - rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('README*') rdoc.rdoc_files.include('lib/**/*.rb') end diff --git a/active_scaffold.gemspec b/active_scaffold.gemspec new file mode 100644 index 0000000000..631010ce5a --- /dev/null +++ b/active_scaffold.gemspec @@ -0,0 +1,30 @@ +# -*- encoding: utf-8 -*- +$LOAD_PATH.unshift File.expand_path('../lib', __FILE__) +require 'active_scaffold/version' + +Gem::Specification.new do |s| + s.name = %q{active_scaffold} + s.version = ActiveScaffold::Version::STRING + s.platform = Gem::Platform::RUBY + s.email = %q{activescaffold@googlegroups.com} + s.authors = ["Many, see README"] + s.homepage = %q{http://activescaffold.com} + s.summary = %q{Rails 3.1 Version of activescaffold supporting prototype and jquery} + s.description = %q{Save time and headaches, and create a more easily maintainable set of pages, with ActiveScaffold. ActiveScaffold handles all your CRUD (create, read, update, delete) user interface needs, leaving you more time to focus on more challenging (and interesting!) problems.} + s.require_paths = ["lib"] + s.files = `git ls-files {app,config,lib,public,shoulda_macros,vendor}`.split("\n") + %w[MIT-LICENSE CHANGELOG README.md] + s.extra_rdoc_files = [ + "README.md" + ] + s.licenses = ["MIT"] + s.test_files = `git ls-files test`.split("\n") + + s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= + + s.add_development_dependency(%q, [">= 0"]) + s.add_development_dependency(%q, ["~> 1.0.0"]) + s.add_development_dependency(%q, [">= 0"]) + #s.add_runtime_dependency(%q, [">= 0"]) + s.add_runtime_dependency(%q, "~> 3.2.0") +end + diff --git a/app/assets/images/active_scaffold/add.png b/app/assets/images/active_scaffold/add.png new file mode 100644 index 0000000000..2dd22829a9 Binary files /dev/null and b/app/assets/images/active_scaffold/add.png differ diff --git a/app/assets/images/active_scaffold/arrow_down.png b/app/assets/images/active_scaffold/arrow_down.png new file mode 100644 index 0000000000..641047c277 Binary files /dev/null and b/app/assets/images/active_scaffold/arrow_down.png differ diff --git a/app/assets/images/active_scaffold/arrow_up.png b/app/assets/images/active_scaffold/arrow_up.png new file mode 100644 index 0000000000..acf046aa00 Binary files /dev/null and b/app/assets/images/active_scaffold/arrow_up.png differ diff --git a/app/assets/images/active_scaffold/close.png b/app/assets/images/active_scaffold/close.png new file mode 100644 index 0000000000..caf633a8c9 Binary files /dev/null and b/app/assets/images/active_scaffold/close.png differ diff --git a/app/assets/images/active_scaffold/close_touch.png b/app/assets/images/active_scaffold/close_touch.png new file mode 100644 index 0000000000..73b2ad63d3 Binary files /dev/null and b/app/assets/images/active_scaffold/close_touch.png differ diff --git a/app/assets/images/active_scaffold/config.png b/app/assets/images/active_scaffold/config.png new file mode 100755 index 0000000000..cfc2702acd Binary files /dev/null and b/app/assets/images/active_scaffold/config.png differ diff --git a/frontends/default/images/cross.png b/app/assets/images/active_scaffold/cross.png similarity index 100% rename from frontends/default/images/cross.png rename to app/assets/images/active_scaffold/cross.png diff --git a/app/assets/images/active_scaffold/gears.png b/app/assets/images/active_scaffold/gears.png new file mode 100644 index 0000000000..9857c1c524 Binary files /dev/null and b/app/assets/images/active_scaffold/gears.png differ diff --git a/frontends/default/images/indicator-small.gif b/app/assets/images/active_scaffold/indicator-small.gif similarity index 100% rename from frontends/default/images/indicator-small.gif rename to app/assets/images/active_scaffold/indicator-small.gif diff --git a/frontends/default/images/indicator.gif b/app/assets/images/active_scaffold/indicator.gif similarity index 100% rename from frontends/default/images/indicator.gif rename to app/assets/images/active_scaffold/indicator.gif diff --git a/frontends/default/images/magnifier.png b/app/assets/images/active_scaffold/magnifier.png similarity index 100% rename from frontends/default/images/magnifier.png rename to app/assets/images/active_scaffold/magnifier.png diff --git a/app/assets/javascripts/active_scaffold.js.erb b/app/assets/javascripts/active_scaffold.js.erb new file mode 100644 index 0000000000..bcad558e4c --- /dev/null +++ b/app/assets/javascripts/active_scaffold.js.erb @@ -0,0 +1,32 @@ +<% case ActiveScaffold.js_framework %> +<% when :jquery %> +<% require_asset "getprototypeof" %> +<% if Jquery::Rails.const_defined? 'JQUERY_UI_VERSION' %> +<% require_asset "jquery-ui" %> +<% require_asset "jquery-ui-timepicker-addon" %> +<% require_asset "jquery/date_picker_bridge" %> +<% require_asset "jquery/draggable_lists" %> +<% elsif Jquery.const_defined? 'Ui' %> +<% require_asset "jquery.ui.core" %> +<% require_asset "jquery.ui.effect" %> +<% require_asset "jquery.ui.sortable" %> +<% require_asset "jquery.ui.draggable" %> +<% require_asset "jquery.ui.droppable" %> +<% require_asset "jquery.ui.datepicker" %> +<% require_asset "jquery-ui-timepicker-addon" %> +<% require_asset "jquery/date_picker_bridge" %> +<% require_asset "jquery/draggable_lists" %> +<% end %> +<% require_asset "jquery/active_scaffold" %> +<% require_asset "jquery/jquery.editinplace" %> +<% when :prototype %> +<% require_asset "effects" %> +<% require_asset "controls" %> +<% require_asset "prototype/active_scaffold" %> +<% require_asset "prototype/dhtml_history" %> +<% require_asset "prototype/form_enhancements" %> +<% require_asset "prototype/rico_corner" %> +<% end %> +ActiveScaffold.config = <%= ActiveScaffold.js_config.to_json %>; +<% ActiveScaffold.javascripts.each {|js| require_asset js} %> +<% ActiveScaffold::Bridges.all_javascripts.each {|js| require_asset js} %> diff --git a/app/assets/javascripts/jquery/active_scaffold.js b/app/assets/javascripts/jquery/active_scaffold.js new file mode 100644 index 0000000000..904be770fe --- /dev/null +++ b/app/assets/javascripts/jquery/active_scaffold.js @@ -0,0 +1,1162 @@ +jQuery(document).ready(function($) { + if (/1\.[2-7]\..*/.test(jQuery().jquery)) { + var error = 'ActiveScaffold requires jquery 1.8.0 or greater, please use jquery-rails 2.1.x gem or greater'; + if (typeof console != 'undefined') console.error(error); + else alert(error); + } + + jQuery(document).on('focus', ':input', function() { ActiveScaffold.last_focus = this; }); + jQuery(document).click(function(event) { + jQuery('.action_group.dyn ul').remove(); + }); + jQuery(document).on('ajax:complete', '.action_group.dyn ul a', function(event) { + var action_link = ActiveScaffold.find_action_link(event.target); + if (action_link.loading_indicator) action_link.loading_indicator.css('visibility','hidden'); + jQuery(event.target).closest('.action_group.dyn ul').remove(); + }); + jQuery(document).on('ajax:beforeSend', 'form.as_form', function(event) { + var as_form = jQuery(this).closest("form"); + if (as_form.data('loading') == true) { + ActiveScaffold.disable_form(as_form); + } + return true; + }); + + jQuery(document).on('ajax:complete', 'form.as_form', function(event) { + var as_form = jQuery(this).closest("form"); + if (as_form.data('loading') == true) { + ActiveScaffold.enable_form(as_form); + } + }); + jQuery(document).on('ajax:error', 'form.as_form', function(event, xhr, status, error) { + var as_div = jQuery(this).closest("div.active-scaffold"); + if (as_div.length) { + ActiveScaffold.report_500_response(as_div, xhr); + } + }); + jQuery(document).on('submit', 'form.as_form:not([data-remote])', function(event) { + var as_form = jQuery(this).closest("form"); + if (as_form.data('loading') == true) { + setTimeout("ActiveScaffold.disable_form('" + as_form.attr('id') + "')", 10); + } + return true; + }); + jQuery(document).on('ajax:before', 'a.as_action', function(event) { + var action_link = ActiveScaffold.ActionLink.get(jQuery(this)); + if (action_link) { + if (action_link.is_disabled()) { + return false; + } else { + if (action_link.loading_indicator) action_link.loading_indicator.css('visibility','visible'); + action_link.disable(); + } + } + return true; + }); + jQuery(document).on('ajax:success', 'a.as_action', function(event, response) { + var action_link = ActiveScaffold.ActionLink.get(jQuery(this)); + if (action_link) { + if (action_link.position) { + action_link.insert(response); + if (action_link.hide_target) action_link.target.hide(); + } else { + action_link.enable(); + } + jQuery(this).trigger('as:action_success', action_link); + } + return true; + }); + jQuery(document).on('ajax:complete', 'a.as_action', function(event) { + var action_link = ActiveScaffold.ActionLink.get(jQuery(this)); + if (action_link) { + if (action_link.loading_indicator) action_link.loading_indicator.css('visibility','hidden'); + } + return true; + }); + jQuery(document).on('ajax:error', 'a.as_action', function(event, xhr, status, error) { + var action_link = ActiveScaffold.ActionLink.get(jQuery(this)); + if (action_link) { + ActiveScaffold.report_500_response(action_link.scaffold_id(), xhr); + action_link.enable(); + } + return true; + }); + jQuery(document).on('ajax:before', 'a.as_cancel', function(event) { + var as_cancel = jQuery(this); + var action_link = ActiveScaffold.find_action_link(as_cancel); + + if (action_link) { + var cancel_url = as_cancel.attr('href'); + var refresh_data = action_link.tag.data('cancel-refresh') || as_cancel.data('refresh'); + if (!refresh_data || !cancel_url) { + action_link.close(); + return false; + } + } + return true; + }); + jQuery(document).on('ajax:success', 'a.as_cancel', function(event, response) { + var action_link = ActiveScaffold.find_action_link(jQuery(this)); + + if (action_link) { + if (action_link.position) { + action_link.close(); + } else { + response.evalResponse(); + } + } + return true; + }); + jQuery(document).on('ajax:error', 'a.as_cancel', function(event, xhr, status, error) { + var action_link = ActiveScaffold.find_action_link(jQuery(this)); + if (action_link) { + ActiveScaffold.report_500_response(action_link.scaffold_id(), xhr); + } + return true; + }); + jQuery(document).on('ajax:before', 'a.as_sort', function(event) { + var as_sort = jQuery(this); + var history_controller_id = as_sort.data('page-history'); + if (history_controller_id) addActiveScaffoldPageToHistory(as_sort.attr('href'), history_controller_id); + as_sort.closest('th').addClass('loading'); + return true; + }); + jQuery(document).on('ajax:error', 'a.as_sort', function(event, xhr, status, error) { + var as_scaffold = jQuery(this).closest('.active-scaffold'); + ActiveScaffold.report_500_response(as_scaffold, xhr); + return true; + }); + jQuery(document).on('mouseenter mouseleave', 'td.in_place_editor_field', function(event) { + var td = jQuery(this), span = td.find('span.in_place_editor_field'); + if (event.type == 'mouseenter') { + if (td.hasClass('empty') || typeof(span.data('editInPlace')) === 'undefined') td.find('span').addClass("hover"); + } + if (event.type == 'mouseleave') { + if (td.hasClass('empty') || typeof(span.data('editInPlace')) === 'undefined') td.find('span').removeClass("hover"); + } + return true; + }); + jQuery(document).on('click', 'td.in_place_editor_field, th.as_marked-column_heading', function(event) { + var span = jQuery(this).find('span.in_place_editor_field'); + span.data('addEmptyOnCancel', jQuery(this).hasClass('empty')); + jQuery(this).removeClass('empty'); + if (span.data('editInPlace')) span.trigger('click.editInPlace'); + else ActiveScaffold.in_place_editor_field_clicked(span); + }); + jQuery(document).on('ajax:before', 'a.as_paginate',function(event) { + var as_paginate = jQuery(this); + var history_controller_id = as_paginate.data('page-history'); + if (history_controller_id) addActiveScaffoldPageToHistory(as_paginate.attr('href'), history_controller_id); + as_paginate.prevAll('img.loading-indicator').css('visibility','visible'); + return true; + }); + jQuery(document).on('ajax:error', 'a.as_paginate', function(event, xhr, status, error) { + var as_scaffold = jQuery(this).closest('.active-scaffold'); + ActiveScaffold.report_500_response(as_scaffold, xhr); + return true; + }); + jQuery(document).on('ajax:complete', 'a.as_paginate', function(event) { + jQuery(this).prevAll('img.loading-indicator').css('visibility','hidden'); + return true; + }); + jQuery(document).on('ajax:before', 'a.as_add_existing, a.as_replace_existing', function(event) { + var prev = jQuery(this).prev(); + if (!prev.is(':input')) prev = prev.find(':input'); + var id = prev.val(); + if (id) { + if (!jQuery(this).data('href')) jQuery(this).data('href', jQuery(this).attr('href')); + jQuery(this).attr('href', jQuery(this).data('href').replace('--ID--', id)); + return true; + } else return false; + }); + jQuery(document).on('change', 'input.update_form:not(.recordselect), textarea.update_form, select.update_form', function(event) { + var element = jQuery(this); + var value = element.is("input:checkbox:not(:checked)") ? null : element.val(); + ActiveScaffold.update_column(element, element.data('update_url'), element.data('update_send_form'), element.attr('id'), value); + return true; + }); + jQuery(document).on('recordselect:change', 'input.recordselect.update_form', function(event, id, label) { + var element = jQuery(this); + ActiveScaffold.update_column(element, element.data('update_url'), element.data('update_send_form'), element.attr('id'), id); + return true; + }); + + jQuery(document).on('change', 'select.as_search_range_option', function(event) { + var element = jQuery(this); + ActiveScaffold[element.val() == 'BETWEEN' ? 'show' : 'hide'](element.closest('dd').find('.as_search_range_between')); + ActiveScaffold[(element.val() == 'null' || element.val() == 'not_null') ? 'hide' : 'show'](element.attr('id').replace(/_opt/, '_numeric')); + return true; + }); + + jQuery(document).on('change', 'select.as_search_date_time_option', function(event) { + var element = jQuery(this); + ActiveScaffold[!(element.val() == 'PAST' || element.val() == 'FUTURE' || element.val() == 'RANGE') ? 'show' : 'hide'](element.attr('id').replace(/_opt/, '_numeric')); + ActiveScaffold[(element.val() == 'PAST' || element.val() == 'FUTURE') ? 'show' : 'hide'](element.attr('id').replace(/_opt/, '_trend')); + ActiveScaffold[(element.val() == 'RANGE') ? 'show' : 'hide'](element.attr('id').replace(/_opt/, '_range')); + return true; + }); + + jQuery(document).on('change', 'select.as_update_date_operator', function(event) { + ActiveScaffold[jQuery(this).val() == 'REPLACE' ? 'show' : 'hide'](jQuery(this).next()); + ActiveScaffold[jQuery(this).val() == 'REPLACE' ? 'hide' : 'show'](jQuery(this).next().next()); + return true; + }); + + jQuery(document).on('click', '.active-scaffold .sub-form a.destroy', function(event) { + event.preventDefault(); + ActiveScaffold.delete_subform_record($(this).data('delete-id')); + }); + + jQuery(document).on("click", '.hover_click', function(event) { + var element = jQuery(this); + var ul_element = element.children('ul').first(); + if (ul_element.is(':visible')) { + element.find('ul').hide(); + } else { + ul_element.show(); + } + return false; + }); + jQuery(document).on('click', '.hover_click a.as_action', function(event) { + var element = jQuery(this).closest('.hover_click'); + if (element) { + element.find('ul').hide(); + } + return true; + }); + + jQuery(document).on('click', '.message a.close', function(e) { + ActiveScaffold.hide(jQuery(this).closest('.message')); + e.preventDefault(); + }); + +}); + +/* Simple Inheritance + http://ejohn.org/blog/simple-javascript-inheritance/ +*/ +(function(){ + var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; + + // The base Class implementation (does nothing) + this.Class = function(){}; + + // Create a new Class that inherits from this class + Class.extend = function(prop) { + var _super = this.prototype; + + // Instantiate a base class (but only create the instance, + // don't run the init constructor) + initializing = true; + var prototype = new this(); + initializing = false; + + // Copy the properties over onto the new prototype + for (var name in prop) { + // Check if we're overwriting an existing function + prototype[name] = typeof prop[name] == "function" && + typeof _super[name] == "function" && fnTest.test(prop[name]) ? + (function(name, fn){ + return function() { + var tmp = this._super; + + // Add a new ._super() method that is the same method + // but on the super-class + this._super = _super[name]; + + // The method only need to be bound temporarily, so we + // remove it when we're done executing + var ret = fn.apply(this, arguments); + this._super = tmp; + + return ret; + }; + })(name, prop[name]) : + prop[name]; + } + + // The dummy class constructor + function Class() { + // All construction is actually done in the init method + if ( !initializing && this.init ) + this.init.apply(this, arguments); + } + + // Populate our constructed prototype object + Class.prototype = prototype; + + // Enforce the constructor to be what we expect + Class.constructor = Class; + + // And make this class extendable + Class.extend = arguments.callee; + + return Class; + }; +})(); + +/* + $ delayed observer + (c) 2007 - Maxime Haineault (max@centdessin.com) + + Special thanks to Stephen Goguen & Tane Piper. + + Slight modifications by Elliot Winkler +*/ + +if (typeof(jQuery.fn.delayedObserver) === 'undefined') { + (function($) { + var delayedObserverStack = []; + var observed; + + function delayedObserverCallback(stackPos) { + observed = delayedObserverStack[stackPos]; + if (observed.timer) return; + + observed.timer = setTimeout(function(){ + observed.timer = null; + observed.callback(observed.obj.val(), observed.obj); + }, observed.delay * 1000); + + observed.oldVal = observed.obj.val(); + } + + // going by + // + // I think these codes only work when using keyup or keydown + function isNonPrintableKey(event) { + var code = event.keyCode; + return ( + event.metaKey || + (code >= 9 && code <= 16) || (code >= 27 && code <= 40) || (code >= 91 && code <= 93) || (code >= 112 && code <= 145) + ); + } + + jQuery.fn.extend({ + delayedObserver:function(delay, callback){ + $this = jQuery(this); + + delayedObserverStack.push({ + obj: $this, timer: null, delay: delay, + oldVal: $this.val(), callback: callback + }); + + stackPos = delayedObserverStack.length-1; + + $this.keyup(function(event) { + if (isNonPrintableKey(event)) return; + observed = delayedObserverStack[stackPos]; + if (observed.obj.val() == observed.obj.oldVal) return; + else delayedObserverCallback(stackPos); + }); + } + }); + })(jQuery); +}; + + +/* + * Simple utility methods + */ + +var ActiveScaffold = { + last_focus: null, + records_for: function(tbody_id) { + if (typeof(tbody_id) == 'string') tbody_id = '#' + tbody_id; + return jQuery(tbody_id).children('.record'); + }, + stripe: function(tbody_id) { + var even = false; + var rows = this.records_for(tbody_id); + + rows.each(function (index, row_node) { + row = jQuery(row_node); + if (row_node.tagName != 'SCRIPT' + && !row.hasClass("create") + && !row.hasClass("update") + && !row.hasClass("inline-adapter") + && !row.hasClass("active-scaffold-calculations")) { + + if (even) row.addClass("even-record"); + else row.removeClass("even-record"); + + even = !even; + } + }); + }, + hide_empty_message: function(tbody) { + if (this.records_for(tbody).length != 0) { + jQuery(tbody).parent().find('tbody.messages p.empty-message').hide(); + } + }, + reload_if_empty: function(tbody, url) { + if (this.records_for(tbody).length == 0) { + this.reload(url); + } + }, + reload: function(url) { + jQuery.getScript(url); + }, + removeSortClasses: function(scaffold) { + if (typeof(scaffold) == 'string') scaffold = '#' + scaffold; + scaffold = jQuery(scaffold) + scaffold.find('td.sorted').each(function(element) { + element.removeClass("sorted"); + }); + scaffold.find('th.sorted').each(function(element) { + element.removeClass("sorted"); + element.removeClass("asc"); + element.removeClass("desc"); + }); + }, + decrement_record_count: function(scaffold) { + // decrement the last record count, firsts record count are in nested lists + if (typeof(scaffold) == 'string') scaffold = '#' + scaffold; + scaffold = jQuery(scaffold); + count = scaffold.find('span.active-scaffold-records').last(); + if (count) count.html(parseInt(count.html(), 10) - 1); + }, + increment_record_count: function(scaffold) { + // increment the last record count, firsts record count are in nested lists + if (typeof(scaffold) == 'string') scaffold = '#' + scaffold; + scaffold = jQuery(scaffold); + count = scaffold.find('span.active-scaffold-records').last(); + if (count) count.html(parseInt(count.html(), 10) + 1); + }, + update_row: function(row, html) { + var even_row = false; + var replaced = null; + if (typeof(row) == 'string') row = '#' + row; + row = jQuery(row); + if (row.hasClass('even-record')) even_row = true; + + replaced = this.replace(row, html); + if (even_row === true) replaced.addClass('even-record'); + ActiveScaffold.highlight(replaced); + }, + + replace: function(element, html) { + if (typeof(element) == 'string') element = '#' + element; + element = jQuery(element); + var new_element = typeof(html) == 'string' ? jQuery.parseHTML(html.trim(), true) : html; + new_element = jQuery(new_element); + element.replaceWith(new_element); + new_element.trigger('as:element_updated'); + return new_element; + }, + + replace_html: function(element, html) { + if (typeof(element) == 'string') element = '#' + element; + element = jQuery(element); + element.html(html); + element.trigger('as:element_updated'); + return element; + }, + + remove: function(element, callback) { + if (typeof(element) == 'string') element = '#' + element; + jQuery(element).remove(); + if (callback) callback(); + }, + + update_inplace_edit: function(element, value, empty) { + if (typeof(element) == 'string') element = '#' + element; + this.replace_html(jQuery(element), value); + jQuery(element).closest('td')[empty ? 'addClass' : 'removeClass']('empty'); + }, + + hide: function(element) { + if (typeof(element) == 'string') element = '#' + element; + jQuery(element).hide(); + }, + + show: function(element) { + if (typeof(element) == 'string') element = '#' + element; + jQuery(element).show(); + }, + + reset_form: function(element) { + if (typeof(element) == 'string') element = '#' + element; + jQuery(element).get(0).reset(); + }, + + disable_form: function(as_form, skip_loading_indicator) { + if (typeof(as_form) == 'string') as_form = '#' + as_form; + as_form = jQuery(as_form) + var loading_indicator = jQuery('#' + as_form.attr('id').replace(/-form$/, '-loading-indicator')); + if (!skip_loading_indicator && loading_indicator) loading_indicator.css('visibility','visible'); + jQuery('input[type=submit]', as_form).attr('disabled', 'disabled'); + // data-remote-disabled attr instead of set data because is used to in selector later + jQuery("input:enabled,select:enabled,textarea:enabled", as_form).attr('disabled', 'disabled').attr('data-remove-disabled', true); + }, + + enable_form: function(as_form, skip_loading_indicator) { + if (typeof(as_form) == 'string') as_form = '#' + as_form; + as_form = jQuery(as_form) + var loading_indicator = jQuery('#' + as_form.attr('id').replace(/-form$/, '-loading-indicator')); + if (!skip_loading_indicator && loading_indicator) loading_indicator.css('visibility','hidden'); + jQuery('input[type=submit]', as_form).removeAttr('disabled'); + jQuery("input[data-remove-disabled],select[data-remove-disabled],textarea[data-remove-disabled]", as_form).removeAttr('disabled data-remove-disabled'); + }, + + focus_first_element_of_form: function(form_element) { + if (typeof(form_element) == 'string') form_element = '#' + form_element; + jQuery(form_element + ":first *:input[type!=hidden]:first").focus(); + }, + + create_record_row: function(active_scaffold_id, html, options) { + if (typeof(active_scaffold_id) == 'string') active_scaffold_id = '#' + active_scaffold_id; + tbody = jQuery(active_scaffold_id).find('tbody.records').first(); + + if (options.insert_at == 'top') { + tbody.prepend(html); + var new_row = tbody.children('tr.record:first-child'); + } else if (options.insert_at == 'bottom') { + var rows = tbody.children('tr.record, tr.inline-adapter'); + var new_row = null; + if (rows.length > 0) { + new_row = rows.last().after(html).next(); + } else { + new_row = tbody.append(html).children().last(); + } + } else if (typeof options.insert_at == 'object') { + var insert_method, get_method, row, id; + if (options.insert_at.after) { + insert_method = 'after'; + get_method = 'next'; + } else { + insert_method = 'before'; + get_method = 'prev'; + } + if (id = options.insert_at[insert_method]) row = tbody.children('#' + id); + if (row && row.length) { + new_row = row[insert_method](html)[get_method](); + } + } + this.stripe(tbody); + this.hide_empty_message(tbody); + this.increment_record_count(tbody.closest('div.active-scaffold')); + this.highlight(new_row); + }, + + create_record_row_from_url: function(active_scaffold_id, url, options) { + jQuery.get(url, function(row) { + ActiveScaffold.create_record_row(action_link.scaffold(), row, options); + action_link.close(); + }); + }, + + delete_record_row: function(row, page_reload_url) { + if (typeof(row) == 'string') row = '#' + row; + row = jQuery(row); + var tbody = row.closest('tbody.records'); + + row.find('a.disabled').each(function() {; + var action_link = ActiveScaffold.ActionLink.get(this); + if (action_link) action_link.close(); + }); + + ActiveScaffold.remove(row, function() { + ActiveScaffold.stripe(tbody); + ActiveScaffold.decrement_record_count(tbody.closest('div.active-scaffold')); + ActiveScaffold.reload_if_empty(tbody, page_reload_url); + }); + }, + + delete_subform_record: function(record) { + if (typeof(record) == 'string') record = '#' + record; + record = jQuery(record).closest('.sub-form-record'); + this.remove(record); + }, + + report_500_response: function(active_scaffold_id) { + var server_error = jQuery(active_scaffold_id).find('td.messages-container p.server-error').first(); + if (server_error.is(':visible')) { + ActiveScaffold.highlight(server_error); + } else { + server_error.show(); + } + ActiveScaffold.scroll_to(server_error, ActiveScaffold.config.scroll_on_close == 'checkInViewport'); + }, + + find_action_link: function(element) { + if (typeof(element) == 'string') element = '#' + element; + element = jQuery(element); + return ActiveScaffold.ActionLink.get(element.is('.actions a') ? element : element.closest('.as_adapter')); + }, + + display_dynamic_action_group: function(link, html) { + if (typeof(link) == 'string') link = jQuery('#' + link); + link.next('ul').remove(); + link.closest('td').addClass('action_group dyn'); + link.after(html); + }, + + scroll_to: function(element, checkInViewport) { + if (typeof checkInViewport == 'undefined') checkInViewport = true; + if (typeof(element) == 'string') element = '#' + element; + var form_offset = jQuery(element).offset().top; + if (checkInViewport) { + var docViewTop = jQuery(window).scrollTop(), + docViewBottom = docViewTop + jQuery(window).height(); + // If it's in viewport , don't scroll; + if (form_offset + jQuery(element).height() <= docViewBottom && form_offset >= docViewTop) return; + } + + jQuery(document).scrollTop(form_offset); + }, + + process_checkbox_inplace_edit: function(checkbox, options) { + var checked = checkbox.is(':checked'), td = checkbox.closest('td'); + if (checked === true) options['params'] += '&value=1'; + jQuery.ajax({ + url: options.url, + type: "POST", + data: options['params'], + dataType: options.ajax_data_type, + beforeSend: function(request, settings) { + if (options.beforeSend) options.beforeSend.call(checkbox, request, settings); + checkbox.attr('disabled', 'disabled'); + td.closest('tr').find('td.actions .loading-indicator').css('visibility','visible'); + }, + complete: function(request){ + checkbox.removeAttr('disabled'); + }, + success: function(request){ + td.closest('tr').find('td.actions .loading-indicator').css('visibility','hidden'); + } + }); + }, + + read_inplace_edit_heading_attributes: function(column_heading, options) { + if (column_heading.data('ie-cancel-text')) options.cancel_button = '"; + if (column_heading.data('ie-loading-text')) options.loading_text = column_heading.data('ie-loading-text'); + if (column_heading.data('ie-saving-text')) options.saving_text = column_heading.data('ie-saving-text'); + if (column_heading.data('ie-save-text')) options.save_button = '"; + if (column_heading.data('ie-rows')) options.textarea_rows = column_heading.data('ie-rows'); + if (column_heading.data('ie-cols')) options.textarea_cols = column_heading.data('ie-cols'); + if (column_heading.data('ie-size')) options.text_size = column_heading.data('ie-size'); + }, + + create_inplace_editor: function(span, options) { + span.removeClass('hover'); + span.editInPlace(options); + span.trigger('click.editInPlace'); + }, + + highlight: function(element) { + if (typeof(element) == 'string') element = jQuery('#' + element); + if (typeof(element.effect) == 'function') { + element.effect("highlight", jQuery.extend({}, ActiveScaffold.config.highlight), 3000); + } + }, + + create_visibility_toggle: function(element, options) { + if (typeof(element) == 'string') element = '#' + element; + var toggable = jQuery(element); + var toggler = toggable.prev(); + var initial_label = (options.default_visible === true) ? options.hide_label : options.show_label; + + toggler.append(' (' + initial_label + ')'); + toggler.children('a').click(function(e) { + e.preventDefault(); + toggable.toggle(); + jQuery(this).html((toggable.is(':hidden')) ? options.show_label : options.hide_label); + return false; + }); + }, + + create_associated_record_form: function(element, content, options) { + if (typeof(element) == 'string') element = '#' + element; + var element = jQuery(element); + content = jQuery(content); + if (options.singular == false) { + if (!(options.id && jQuery('#' + options.id).size() > 0)) { + var tfoot = element.children('tfoot'); + if (tfoot.length) tfoot.before(content); + else element.append(content); + content.trigger('as:element_created'); + } + } else { + var current = jQuery('#' + element.attr('id') + ' .sub-form-record') + if (current[0]) { + this.replace(current[0], content); + } else { + element.prepend(content); + content.trigger('as:element_created'); + } + } + }, + + render_form_field: function(source, content, options) { + if (typeof(source) == 'string') source = '#' + source; + var source = jQuery(source); + var element = source.closest('.sub-form-record'); + if (element.length == 0) { + element = source.closest('form > ol.form'); + } + element = element.find('.' + options.field_class).first(); + + if (element) { + if (options.is_subform == false) { + this.replace(element.closest('dl'), content); + } else { + this.replace_html(element, content); + } + } + }, + + record_select_onselect: function(edit_associated_url, active_scaffold_id, id){ + jQuery.ajax({ + url: edit_associated_url.split('--ID--').join(id), + error: function(xhr, textStatus, errorThrown){ + ActiveScaffold.report_500_response(active_scaffold_id, xhr) + } + }); + }, + + // element is tbody id + mark_records: function(element, options) { + if (typeof(element) == 'string') element = '#' + element; + var element = jQuery(element); + if (options.include_checkboxes) { + var mark_checkboxes = jQuery('#' + element.attr('id') + ' > tr.record td.as_marked-column input[type="checkbox"]'); + mark_checkboxes.prop('checked', !!options.checked); + mark_checkboxes.val('' + !options.checked); + } + if(options.include_mark_all) { + var mark_all_checkbox = element.prevAll('thead').find('th.as_marked-column_heading span input[type="checkbox"]'); + mark_all_checkbox.prop('checked', !!options.checked); + mark_all_checkbox.val('' + !options.checked); + } + }, + + in_place_editor_field_clicked: function(span) { + // test editor is open + if (typeof(span.data('editInPlace')) === 'undefined') { + var options = {show_buttons: true, + hover_class: 'hover', + element_id: 'editor_id', + ajax_data_type: "script", + delegate: { + willCloseEditInPlace: function(span, options) { + if (span.data('addEmptyOnCancel')) span.closest('td').addClass('empty'); + span.closest('tr').find('td.actions .loading-indicator').css('visibility','visible'); + }, + didCloseEditInPlace: function(span, options) { + span.closest('tr').find('td.actions .loading-indicator').css('visibility','hidden'); + } + }, + update_value: 'value'}, + csrf_param = jQuery('meta[name=csrf-param]').first(), + csrf_token = jQuery('meta[name=csrf-token]').first(), + my_parent = span.parent(), + column_heading = null; + + if(!(my_parent.is('td') || my_parent.is('th'))){ + my_parent = span.parents('td').eq(0); + } + + if (my_parent.is('td')) { + var column_no = my_parent.prevAll('td').length; + column_heading = my_parent.closest('.active-scaffold').find('th:eq(' + column_no + ')'); + } else if (my_parent.is('th')) { + column_heading = my_parent; + } + + var render_url = column_heading.data('ie-render-url'), + mode = column_heading.data('ie-mode'), + record_id = span.data('ie-id') || ''; + + ActiveScaffold.read_inplace_edit_heading_attributes(column_heading, options); + + if (span.data('ie-url')) { + options.url = span.data('ie-url').replace(/__id__/, record_id); + } else { + options.url = column_heading.data('ie-url').replace(/__id__/, record_id); + } + + if (csrf_param) options['params'] = csrf_param.attr('content') + '=' + csrf_token.attr('content'); + + if (span.closest('div.active-scaffold').data('eid')) { + if (options['params'].length > 0) { + options['params'] += "&"; + } + options['params'] += ("eid=" + span.closest('div.active-scaffold').data('eid')); + } + + if (mode === 'clone') { + options.clone_id_suffix = record_id; + options.clone_selector = '#' + column_heading.attr('id') + ' .as_inplace_pattern'; + options.field_type = 'clone'; + } + + if (render_url) { + var plural = false; + if (column_heading.data('ie-plural')) plural = true; + options.field_type = 'remote'; + options.editor_url = render_url.replace(/__id__/, record_id) + if (!options.delegate) options.delegate = {} + options.delegate.didOpenEditInPlace = function(dom) { dom.trigger('as:element_updated'); } + } + var actions, forms; + options.beforeSend = function(xhr, settings) { + switch (span.data('ie-update')) { + case 'update_row': + actions = span.closest('tr').find('.actions a:not(.disabled)').addClass('disabled'); + break; + case 'update_table': + var table = span.closest('.as_content'); + actions = table.find('.actions a:not(.disabled)').addClass('disabled'); + forms = table.find('.as_form'); + ActiveScaffold.disable_form(forms); + break; + } + } + options.error = options.success = function() { + if (actions) actions.removeClass('disabled'); + if (forms) ActiveScaffold.enable_form(forms); + } + if (mode === 'inline_checkbox') { + ActiveScaffold.process_checkbox_inplace_edit(span.find('input:checkbox'), options); + } else { + ActiveScaffold.create_inplace_editor(span, options); + } + } + }, + + update_column: function(element, url, send_form, source_id, val) { + if (!element) element = jQuery('#' + source_id); + var as_form = element.closest('form.as_form'); + var params = null; + + if (send_form) { + var selector, base = as_form; + if (send_form == 'row') base = element.closest('.association-record, form'); + if (selector = element.data('update_send_form_selector')) + params = base.find(selector).serialize(); + else if (send_form != as_form) params = base.find(':input').serialize(); + else base.serialize(); + params += '&_method=&' + jQuery.param({"source_id": source_id}); + } else { + params = {value: val}; + params.source_id = source_id; + } + + jQuery.ajax({ + url: url, + data: params, + type: 'post', + beforeSend: function(event) { + element.nextAll('img.loading-indicator').css('visibility','visible'); + ActiveScaffold.disable_form(as_form); + }, + complete: function(event) { + element.nextAll('img.loading-indicator').css('visibility','hidden'); + ActiveScaffold.enable_form(as_form); + if (ActiveScaffold.last_focus) $(ActiveScaffold.last_focus).focus().select(); + }, + error: function (xhr, status, error) { + var as_div = element.closest("div.active-scaffold"); + if (as_div) { + ActiveScaffold.report_500_response(as_div, xhr); + } + } + }); + }, + + draggable_lists: function(element) { + jQuery('ul#' + element).draggable_lists(); + } +} + +/* + * DHTML history tie-in + */ +function addActiveScaffoldPageToHistory(url, active_scaffold_id) { + if (typeof dhtmlHistory == 'undefined') return; // it may not be loaded + + var array = url.split('?'); + var qs = new Querystring(array[1]); + var sort = qs.get('sort') + var dir = qs.get('sort_direction') + var page = qs.get('page') + if (sort || dir || page) dhtmlHistory.add(active_scaffold_id+":"+page+":"+sort+":"+dir, url); +} + +/* + * URL modification support. Incomplete functionality. + */ +String.prototype.append_params = function(params) { + var url = this; + if (url.indexOf('?') == -1) url += '?'; + else if (url.lastIndexOf('&') != url.length) url += '&'; + + for(var key in params) { + if (key) url += (key + '=' + params[key] + '&'); + } + + // the loop leaves a comma dangling at the end of string, chop it off + url = url.substring(0, url.length-1); + return url; +}; + + +/** + * A set of links. As a set, they can be controlled such that only one is "open" at a time, etc. + */ +ActiveScaffold.Actions = new Object(); +ActiveScaffold.Actions.Abstract = Class.extend({ + init: function(links, target, loading_indicator, options) { + this.target = jQuery(target); + this.loading_indicator = jQuery(loading_indicator); + this.options = options; + var _this = this; + this.links = jQuery.map(links, function(link) { + var my_link = _this.instantiate_link(link); + return my_link; + }); + }, + + instantiate_link: function(link) { + throw 'unimplemented' + } +}); + +/** + * A DataStructures::ActionLink, represented in JavaScript. + * Concerned with AJAX-enabling a link and adapting the result for insertion into the table. + */ +ActiveScaffold.ActionLink = { + get: function(element) { + if (typeof(element) == 'string') element = '#' + element; + var element = jQuery(element); + if (element.length > 0) { + element.data(); // $ 1.4.2 workaround + if (typeof(element.data('action_link')) === 'undefined' && !element.hasClass('as_adapter')) { + var parent = element.closest('.actions'); + if (parent.length === 0 || parent.is('td')) { + // maybe an column action_link + parent = element.closest('tr.record'); + } + if (parent.is('tr')) { + // record action + var target = parent.find('a.as_action'); + var loading_indicator = parent.find('td.actions .loading-indicator'); + if (!loading_indicator.length) loading_indicator = element.parent().find('.loading-indicator'); + new ActiveScaffold.Actions.Record(target, parent, loading_indicator); + } else if (parent && parent.is('div')) { + //table action + new ActiveScaffold.Actions.Table(parent.find('a.as_action'), parent.closest('div.active-scaffold').find('tbody.before-header').first(), parent.find('.loading-indicator').first()); + } + element = jQuery(element); + } + return element.data('action_link'); + } else { + return null; + } + } +}; +ActiveScaffold.ActionLink.Abstract = Class.extend({ + init: function(a, target, loading_indicator) { + this.tag = jQuery(a); + this.url = this.tag.attr('href'); + this.method = this.tag.data('method') || 'get'; + this.target = target; + this.loading_indicator = loading_indicator; + this.hide_target = false; + this.position = this.tag.data('position'); + this.action = this.tag.data('action'); + + this.tag.data('action_link', this); + return this; + }, + + open: function(event) { + this.tag.click(); + }, + + insert: function(content) { + throw 'unimplemented' + }, + + close: function() { + if (this.adapter) { + var link = this; + ActiveScaffold.remove(this.adapter, function() { + link.enable(); + if (link.hide_target) link.target.show(); + if (ActiveScaffold.config.scroll_on_close) ActiveScaffold.scroll_to(link.target.attr('id'), ActiveScaffold.config.scroll_on_close == 'checkInViewport'); + }); + } + }, + + reload: function() { + this.close(); + this.open(); + }, + + get_new_adapter_id: function() { + var id = 'adapter_'; + var i = 0; + while (jQuery(id + i)) i++; + return id + i; + }, + + enable: function() { + return this.tag.removeClass('disabled'); + }, + + disable: function() { + return this.tag.addClass('disabled'); + }, + + is_disabled: function() { + return this.tag.hasClass('disabled'); + }, + + scaffold_id: function() { + return '#' + this.tag.closest('div.active-scaffold').attr('id'); + }, + + scaffold: function() { + return this.tag.closest('div.active-scaffold'); + }, + + update_flash_messages: function(messages) { + message_node = jQuery(this.scaffold_id().replace(/-active-scaffold/, '-messages')); + if (message_node) message_node.html(messages); + }, + set_adapter: function(element) { + this.adapter = element; + this.adapter.addClass('as_adapter'); + this.adapter.data('action_link', this); + if (this.refresh_url) jQuery('.as_cancel', this.adapter).attr('href', this.refresh_url); + }, + keep_open: function() { + return this.tag.data('keep-open'); + } +}); + +/** + * Concrete classes for record actions + */ +ActiveScaffold.Actions.Record = ActiveScaffold.Actions.Abstract.extend({ + instantiate_link: function(link) { + var l = new ActiveScaffold.ActionLink.Record(link, this.target, this.loading_indicator); + var refresh = this.target.data('refresh'); + if (refresh) l.refresh_url = refresh; + + if (l.position) { + l.url = l.url.append_params({adapter: '_list_inline_adapter'}); + l.tag.attr('href', l.url); + } + l.set = this; + return l; + } +}); + +ActiveScaffold.ActionLink.Record = ActiveScaffold.ActionLink.Abstract.extend({ + close_previous_adapter: function() { + var _this = this; + jQuery.each(this.set.links, function(index, item) { + if (item.url != _this.url && item.is_disabled() && !item.keep_open() && item.adapter) { + ActiveScaffold.remove(item.adapter, function () { item.enable(); }); + } + }); + }, + + insert: function(content) { + this.close_previous_adapter(); + + if (this.position == 'replace') { + this.position = 'after'; + this.hide_target = true; + } + + var colspan = this.target.children().length; + if (content && this.position) { + content = jQuery(content); + content.find('.inline-adapter-cell:first').attr('colspan', colspan); + } + if (this.position == 'after') { + this.target.after(content); + this.set_adapter(this.target.next()); + } + else if (this.position == 'before') { + this.target.before(content); + this.set_adapter(this.target.prev()); + } + else { + return false; + } + ActiveScaffold.highlight(this.adapter.find('td')); + }, + + close: function(refreshed_content_or_reload) { + this._super(); + if (refreshed_content_or_reload) { + if (typeof refreshed_content_or_reload == 'string') { + ActiveScaffold.update_row(this.target, refreshed_content_or_reload); + } else if (this.refresh_url) { + var target = this.target; + jQuery.get(this.refresh_url, function(e, status, response) { + ActiveScaffold.update_row(target, response.responseText); + }); + } + } + }, + + enable: function() { + var _this = this; + jQuery.each(this.set.links, function(index, item) { + if (item.url != _this.url) return; + item.tag.removeClass('disabled'); + }); + }, + + disable: function() { + var _this = this; + jQuery.each(this.set.links, function(index, item) { + if (item.url != _this.url) return; + item.tag.addClass('disabled'); + }); + }, + + set_opened: function() { + if (this.position == 'after') { + this.set_adapter(this.target.next()); + } + else if (this.position == 'before') { + this.set_adapter(this.target.prev()); + } + this.disable(); + } +}); + +/** + * Concrete classes for table actions + */ +ActiveScaffold.Actions.Table = ActiveScaffold.Actions.Abstract.extend({ + instantiate_link: function(link) { + var l = new ActiveScaffold.ActionLink.Table(link, this.target, this.loading_indicator); + if (l.position) { + l.url = l.url.append_params({adapter: '_list_inline_adapter'}); + l.tag.attr('href', l.url); + } + return l; + } +}); + +ActiveScaffold.ActionLink.Table = ActiveScaffold.ActionLink.Abstract.extend({ + insert: function(content) { + if (this.position == 'top') { + this.target.prepend(content); + this.set_adapter(this.target.children().first()); + } + else { + throw 'Unknown position "' + this.position + '"' + } + ActiveScaffold.highlight(this.adapter.find('td').first().children()); + }, +}); diff --git a/app/assets/javascripts/jquery/active_scaffold_chosen.js b/app/assets/javascripts/jquery/active_scaffold_chosen.js new file mode 100644 index 0000000000..a06d466dcb --- /dev/null +++ b/app/assets/javascripts/jquery/active_scaffold_chosen.js @@ -0,0 +1,11 @@ +jQuery(document).ready(function() { + jQuery(document).on('as:element_updated', function(event) { + jQuery('select.chosen', event.target).chosen(); + }); + jQuery(document).on('as:action_success', 'a.as_action', function(event, action_link) { + if (action_link.adapter) { + jQuery('select.chosen', action_link.adapter).chosen(); + } + }); + jQuery('select.chosen').chosen(); +}); diff --git a/app/assets/javascripts/jquery/date_picker_bridge.js.erb b/app/assets/javascripts/jquery/date_picker_bridge.js.erb new file mode 100644 index 0000000000..d7364b5c3d --- /dev/null +++ b/app/assets/javascripts/jquery/date_picker_bridge.js.erb @@ -0,0 +1,44 @@ +<%# encoding: utf-8 %> +<%= ActiveScaffold::Bridges[:date_picker].localization %> +jQuery(function($) { + Object.getPrototypeOf($.datepicker)._attachDatepicker_without_inlineSettings = Object.getPrototypeOf($.datepicker)._attachDatepicker; + $.extend(Object.getPrototypeOf($.datepicker), { + _attachDatepicker: function(target, settings) { + var inlineSettings = {}, $target = $(target); + for (var attrName in this._defaults) { + if(this._defaults.hasOwnProperty(attrName)){ + var attrValue = $target.data(attrName.toLowerCase()); + if (attrValue) { + try { + inlineSettings[attrName] = eval(attrValue); + } catch (err) { + inlineSettings[attrName] = attrValue; + } + } + } + } + this._attachDatepicker_without_inlineSettings(target, $.extend({}, settings || {}, inlineSettings)); + } + }); +}); +jQuery(document).on("focus", "input.date_picker", function(){ + var date_picker = jQuery(this); + if (typeof(date_picker.datepicker) == 'function') { + if (!date_picker.hasClass('hasDatepicker')) { + date_picker.datepicker(); + date_picker.trigger('focus'); + } + } + return true; +}); + +jQuery(document).on("focus", "input.datetime_picker", function(){ + var date_picker = jQuery(this); + if (typeof(date_picker.datetimepicker) == 'function') { + if (!date_picker.hasClass('hasDatepicker')) { + date_picker.datetimepicker(); + date_picker.trigger('focus'); + } + } + return true; +}); diff --git a/app/assets/javascripts/jquery/draggable_lists.js b/app/assets/javascripts/jquery/draggable_lists.js new file mode 100644 index 0000000000..92aa42ef2d --- /dev/null +++ b/app/assets/javascripts/jquery/draggable_lists.js @@ -0,0 +1,26 @@ +jQuery.fn.draggable_lists = function() { + this.addClass('draggable-list'); + var list_selected = jQuery(this.get(0).cloneNode(false)).addClass('selected'); + list_selected.attr('id', list_selected.attr('id') + '_selected').insertAfter(this); + this.find('input:checkbox').each(function(index, item) { + var li = jQuery(item).closest('li').addClass('draggable-item'); + li.children('label').removeAttr('for'); + if (jQuery(item).is(':checked')) li.appendTo(list_selected); + li.draggable({appendTo: 'body', helper: 'clone'}); + }); + jQuery([this, list_selected]).droppable({ + hoverClass: 'hover', + accept: function(draggable) { + var parent_id = draggable.parent().attr('id'), id = jQuery(this).attr('id'), + requested_id = jQuery(this).hasClass('selected') ? id.replace('_selected', '') : id + '_selected'; + return parent_id == requested_id; + }, + drop: function(event, ui) { + jQuery(this).append(ui.draggable); + var input = jQuery('input:checkbox', ui.draggable); + input.prop('checked', jQuery(this).hasClass('selected')); + ui.draggable.css({left: '0px', top: '0px'}); + } + }); + return this; +}; diff --git a/app/assets/javascripts/jquery/jquery.editinplace.js b/app/assets/javascripts/jquery/jquery.editinplace.js new file mode 100644 index 0000000000..bdaf3ecdba --- /dev/null +++ b/app/assets/javascripts/jquery/jquery.editinplace.js @@ -0,0 +1,751 @@ +/* + +A jQuery edit in place plugin + +Version 2.2.0 + +Authors: + Dave Hauenstein + Martin Häcker + +Project home: + http://code.google.com/p/jquery-in-place-editor/ + +Patches with tests welcomed! For guidance see the tests . To submit, attach them to the bug tracker. + +License: +This source file is subject to the BSD license bundled with this package. +Available online: {@link http://www.opensource.org/licenses/bsd-license.php} +If you did not receive a copy of the license, and are unable to obtain it, +learn to use a search engine. + +Rev: 161 +*/ + +(function($){ + +$.fn.editInPlace = function(options) { + + var settings = $.extend({}, $.fn.editInPlace.defaults, options); + assertMandatorySettingsArePresent(settings); + preloadImage(settings.saving_image); + + return this.each(function() { + var dom = $(this); + // This won't work with live queries as there is no specific element to attach this + // one way to deal with this could be to store a reference to self and then compare that in click? + if (dom.data('editInPlace')) + return; // already an editor here + dom.data('editInPlace', true); + + new InlineEditor(settings, dom).init(); + }); +}; + +/// Switch these through the dictionary argument to $(aSelector).editInPlace(overideOptions) +/// Required Options: Either url or callback, so the editor knows what to do with the edited values. +$.fn.editInPlace.defaults = { + url: "", // string: POST URL to send edited content + ajax_data_type: "html", // string: dataType (html|script) for ajax call to save updated value + bg_over: "#ffc", // string: background color of hover of unactivated editor + bg_out: "transparent", // string: background color on restore from hover + hover_class: "", // string: class added to root element during hover. Will override bg_over and bg_out + show_buttons: false, // boolean: will show the buttons: cancel or save; will automatically cancel out the onBlur functionality + save_button: '', // string: image button tag to use as “Save” button + cancel_button: '', // string: image button tag to use as “Cancel” button + params: "", // string: example: first_name=dave&last_name=hauenstein extra paramters sent via the post request to the server + field_type: "text", // string: "text", "textarea", or "select", or "remote", or "clone"; The type of form field that will appear on instantiation + default_text: "(Click here to add text)", // string: text to show up if the element that has this functionality is empty + use_html: false, // boolean, set to true if the editor should use jQuery.fn.html() to extract the value to show from the dom node (keep in mind that IE will uppercase all tags, so use with caution) + textarea_rows: 10, // integer: set rows attribute of textarea, if field_type is set to textarea. Use CSS if possible though + textarea_cols: 25, // integer: set cols attribute of textarea, if field_type is set to textarea. Use CSS if possible though + select_text: "Choose new value", // string: default text to show up in select box + select_options: "", // string or array: Used if field_type is set to 'select'. Can be comma delimited list of options 'textandValue,text:value', Array of options ['textAndValue', 'text:value'] or array of arrays ['textAndValue', ['text', 'value']]. The last form is especially usefull if your labels or values contain colons) + text_size: null, // integer: set cols attribute of text input, if field_type is set to text. Use CSS if possible though + editor_url: null, // for field_type: remote url to get html_code for edit_control + loading_text: 'Loading...', // shown if inplace editor is loaded from server + // Specifying callback_skip_dom_reset will disable all saving_* options + saving_text: undefined, // string: text to be used when server is saving information. Example "Saving..." + saving_image: "", // string: uses saving text specify an image location instead of text while server is saving + saving_animation_color: 'transparent', // hex color string, will be the color the pulsing animation during the save pulses to. Note: Only works if jquery-ui is loaded + clone_selector: null, // if field_type clone a selector to clone editor from + clone_id_suffix: null, // if field_type clone a suffix to create unique ids + + value_required: false, // boolean: if set to true, the element will not be saved unless a value is entered + element_id: "element_id", // string: name of parameter holding the id or the editable + update_value: "update_value", // string: name of parameter holding the updated/edited value + original_value: 'original_value', // string: name of parameter holding the updated/edited value + original_html: "original_html", // string: name of parameter holding original_html value of the editable /* DEPRECATED in 2.2.0 */ use original_value instead. + save_if_nothing_changed: false, // boolean: submit to function or server even if the user did not change anything + on_blur: "save", // string: "save" or null; what to do on blur; will be overridden if show_buttons is true + cancel: "", // string: if not empty, a jquery selector for elements that will not cause the editor to open even though they are clicked. E.g. if you have extra buttons inside editable fields + + // All callbacks will have this set to the DOM node of the editor that triggered the callback + + callback: null, // function: function to be called when editing is complete; cancels ajax submission to the url param. Prototype: function(idOfEditor, enteredText, orinalHTMLContent, settingsParams, callbacks). The function needs to return the value that should be shown in the dom. Returning undefined means cancel and will restore the dom and trigger an error. callbacks is a dictionary with two functions didStartSaving and didEndSaving() that you can use to tell the inline editor that it should start and stop any saving animations it has configured. /* DEPRECATED in 2.1.0 */ Parameter idOfEditor, use $(this).attr('id') instead + callback_skip_dom_reset: false, // boolean: set this to true if the callback should handle replacing the editor with the new value to show + beforeSend: null, // function: this function gets called before sending new value to server. Prototype: function(request, requestSettings) + success: null, // function: this function gets called if server responds with a success. Prototype: function(newEditorContentString) + error: null, // function: this function gets called if server responds with an error. Prototype: function(request) + error_sink: function(idOfEditor, errorString) { alert(errorString); }, // function: gets id of the editor and the error. Make sure the editor has an id, or it will just be undefined. If set to null, no error will be reported. /* DEPRECATED in 2.1.0 */ Parameter idOfEditor, use $(this).attr('id') instead + preinit: null, // function: this function gets called after a click on an editable element but before the editor opens. If you return false, the inline editor will not open. Prototype: function(currentDomNode). DEPRECATED in 2.2.0 use delegate shouldOpenEditInPlace call instead + postclose: null, // function: this function gets called after the inline editor has closed and all values are updated. Prototype: function(currentDomNode). DEPRECATED in 2.2.0 use delegate didCloseEditInPlace call instead + delegate: null // object: if it has methods with the name of the callbacks documented below in delegateExample these will be called. This means that you just need to impelment the callbacks you are interested in. +}; + +// Lifecycle events that the delegate can implement +// this will always be fixed to the delegate +var delegateExample = { + // called while opening the editor. + // return false to prevent editor from opening + shouldOpenEditInPlace: function(aDOMNode, aSettingsDict, triggeringEvent) {}, + // return content to show in inplace editor + willOpenEditInPlace: function(aDOMNode, aSettingsDict) {}, + didOpenEditInPlace: function(aDOMNode, aSettingsDict) {}, + + // called while closing the editor + // return false to prevent the editor from closing + shouldCloseEditInPlace: function(aDOMNode, aSettingsDict, triggeringEvent) {}, + // return value will be shown during saving + willCloseEditInPlace: function(aDOMNode, aSettingsDict) {}, + didCloseEditInPlace: function(aDOMNode, aSettingsDict) {}, + + missingCommaErrorPreventer:'' +}; + + +function InlineEditor(settings, dom) { + this.settings = settings; + this.dom = dom; + this.originalValue = null; + this.didInsertDefaultText = false; + this.shouldDelayReinit = false; +}; + +$.extend(InlineEditor.prototype, { + + init: function() { + this.setDefaultTextIfNeccessary(); + this.connectOpeningEvents(); + }, + + reinit: function() { + if (this.shouldDelayReinit) + return; + + this.triggerCallback(this.settings.postclose, /* DEPRECATED in 2.1.0 */ this.dom); + this.triggerDelegateCall('didCloseEditInPlace'); + + this.markEditorAsInactive(); + this.connectOpeningEvents(); + }, + + setDefaultTextIfNeccessary: function() { + if('' !== this.dom.html()) + return; + + this.dom.html(this.settings.default_text); + this.didInsertDefaultText = true; + }, + + connectOpeningEvents: function() { + var that = this; + this.dom + .bind('mouseenter.editInPlace', function(){ that.addHoverEffect(); }) + .bind('mouseleave.editInPlace', function(){ that.removeHoverEffect(); }) + .bind('click.editInPlace', function(anEvent){ that.openEditor(anEvent); }); + }, + + disconnectOpeningEvents: function() { + // prevent re-opening the editor when it is already open + this.dom.unbind('.editInPlace'); + }, + + addHoverEffect: function() { + if (this.settings.hover_class) + this.dom.addClass(this.settings.hover_class); + else + this.dom.css("background-color", this.settings.bg_over); + }, + + removeHoverEffect: function() { + if (this.settings.hover_class) + this.dom.removeClass(this.settings.hover_class); + else + this.dom.css("background-color", this.settings.bg_out); + }, + + openEditor: function(anEvent) { + if ( ! this.shouldOpenEditor(anEvent)) + return; + + this.disconnectOpeningEvents(); + this.removeHoverEffect(); + this.removeInsertedDefaultTextIfNeccessary(); + this.saveOriginalValue(); + this.markEditorAsActive(); + this.replaceContentWithEditor(); + this.setInitialValue(); + this.workAroundMissingBlurBug(); + this.connectClosingEventsToEditor(); + this.triggerDelegateCall('didOpenEditInPlace'); + }, + + shouldOpenEditor: function(anEvent) { + if (this.isClickedObjectCancelled(anEvent.target)) + return false; + + if (false === this.triggerCallback(this.settings.preinit, /* DEPRECATED in 2.1.0 */ this.dom)) + return false; + + if (false === this.triggerDelegateCall('shouldOpenEditInPlace', true, anEvent)) + return false; + + return true; + }, + + removeInsertedDefaultTextIfNeccessary: function() { + if ( ! this.didInsertDefaultText + || this.dom.html() !== this.settings.default_text) + return; + + this.dom.html(''); + this.didInsertDefaultText = false; + }, + + isClickedObjectCancelled: function(eventTarget) { + if ( ! this.settings.cancel) + return false; + + var eventTargetAndParents = $(eventTarget).parents().andSelf(); + var elementsMatchingCancelSelector = eventTargetAndParents.filter(this.settings.cancel); + return 0 !== elementsMatchingCancelSelector.length; + }, + + saveOriginalValue: function() { + if (this.settings.use_html) + this.originalValue = this.dom.html(); + else + this.originalValue = trim(this.dom.text()); + }, + + restoreOriginalValue: function() { + this.setClosedEditorContent(this.originalValue); + }, + + setClosedEditorContent: function(aValue) { + if (this.settings.use_html) + this.dom.html(aValue); + else + this.dom.text(aValue); + }, + + workAroundMissingBlurBug: function() { + // Strangely, all browser will forget to send a blur event to an input element + // when another one is created and selected programmatically. (at least under some circumstances). + // This means that if another inline editor is opened, existing inline editors will _not_ close + // if they are configured to submit when blurred. + + // Using parents() instead document as base to workaround the fact that in the unittests + // the editor is not a child of window.document but of a document fragment + var ourInput = this.dom.find(':input'); + this.dom.parents(':last').find('.editInPlace-active :input').not(ourInput).blur(); + }, + + replaceContentWithEditor: function() { + var buttons_html = (this.settings.show_buttons) ? this.settings.save_button + ' ' + this.settings.cancel_button : ''; + var editorElement = this.createEditorElement(); // needs to happen before anything is replaced + /* insert the new in place form after the element they click, then empty out the original element */ + this.dom.html('
') + .find('form') + .append(editorElement) + .append(buttons_html); + }, + + createEditorElement: function() { + if (-1 === $.inArray(this.settings.field_type, ['text', 'textarea', 'select', 'remote', 'clone'])) + throw "Unknown field_type , supported are 'text', 'textarea', 'select' and 'remote'"; + + var editor = null; + if ("select" === this.settings.field_type) + editor = this.createSelectEditor(); + else if ("text" === this.settings.field_type) + editor = $(''); + else if ("textarea" === this.settings.field_type) + editor = $('' + + '' + + ''; + document.write(safariHTML); + this.safariStack = document.getElementById(stackID); + this.safariLength = document.getElementById(lengthID); + if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) { + this.safariHistoryStartPoint = history.length; + this.safariLength.value = this.safariHistoryStartPoint; + } else { + this.safariHistoryStartPoint = this.safariLength.value; + } + }, + + /*TODO: make this public again?*/ + /*Private: Get browser's current hash location; for Safari, read value from a hidden form field*/ + getCurrentLocation: function() { + var r = (this.isSafari + ? this.getSafariState() + : this.getCurrentHash() + ); + return r; + }, + + /*TODO: make this public again?*/ + /*Private: Manually parse the current url for a hash; tip of the hat to YUI*/ + getCurrentHash: function() { + var r = window.location.href; + var i = r.indexOf("#"); + return (i >= 0 + ? r.substr(i+1) + : "" + ); + }, + + /*Private: Safari method to read the history stack from a hidden form field*/ + getSafariStack: function() { + var r = this.safariStack.value; + return historyStorage.fromJSON(r); + }, + /*Private: Safari method to read from the history stack*/ + getSafariState: function() { + var stack = this.getSafariStack(); + var state = stack[history.length - this.safariHistoryStartPoint - 1]; + return state; + }, + /*Private: Safari method to write the history stack to a hidden form field*/ + putSafariState: function(newLocation) { + var stack = this.getSafariStack(); + stack[history.length - this.safariHistoryStartPoint] = newLocation; + this.safariStack.value = historyStorage.toJSON(stack); + }, + + /*Private: Notify the listener of new history changes.*/ + fireHistoryEvent: function(newHash) { + var decodedHash = decodeURI(newHash) + /*extract the value from our history storage for this hash*/ + var historyData = historyStorage.get(decodedHash); + this.changeTitle(historyData); + /*call our listener*/ + this.listener.call(null, decodedHash, historyData); + }, + + /*Private: See if the browser has changed location. This is the primary history mechanism for Firefox. For IE, we use this to + handle an important edge case: if a user manually types in a new hash value into their IE location bar and press enter, we want to + to intercept this and notify any history listener.*/ + checkLocation: function() { + + /*Ignore any location changes that we made ourselves for browsers other than IE*/ + if (!this.isIE && this.ignoreLocationChange) { + this.ignoreLocationChange = false; + return; + } + + /*If we are dealing with IE and we are in the middle of making a location change from an iframe, ignore it*/ + if (!this.isIE && this.ieAtomicLocationChange) { + return; + } + + /*Get hash location*/ + var hash = this.getCurrentLocation(); + + /*Do nothing if there's been no change*/ + if (hash == this.currentLocation) { + return; + } + + /*In IE, users manually entering locations into the browser; we do this by comparing the browser's location against the + iframe's location; if they differ, we are dealing with a manual event and need to place it inside our history, otherwise + we can return*/ + this.ieAtomicLocationChange = true; + + if (this.isIE && this.getIframeHash() != hash) { + this.iframe.src = this.blankURL + hash; + } + else if (this.isIE) { + /*the iframe is unchanged*/ + return; + } + + /*Save this new location*/ + this.currentLocation = hash; + + this.ieAtomicLocationChange = false; + + /*Notify listeners of the change*/ + this.fireHistoryEvent(hash); + }, + + /*Private: Get the current location of IE's hidden iframe.*/ + getIframeHash: function() { + var doc = this.iframe.contentWindow.document; + var hash = String(doc.location.search); + if (hash.length == 1 && hash.charAt(0) == "?") { + hash = ""; + } + else if (hash.length >= 2 && hash.charAt(0) == "?") { + hash = hash.substring(1); + } + return hash; + }, + + /*Private: Remove any leading hash that might be on a location.*/ + removeHash: function(hashValue) { + var r; + if (hashValue === null || hashValue === undefined) { + r = null; + } + else if (hashValue === "") { + r = ""; + } + else if (hashValue.length == 1 && hashValue.charAt(0) == "#") { + r = ""; + } + else if (hashValue.length > 1 && hashValue.charAt(0) == "#") { + r = hashValue.substring(1); + } + else { + r = hashValue; + } + return r; + }, + + /*Private: For IE, tell when the hidden iframe has finished loading.*/ + iframeLoaded: function(newLocation) { + /*ignore any location changes that we made ourselves*/ + if (this.ignoreLocationChange) { + this.ignoreLocationChange = false; + return; + } + + /*Get the new location*/ + var hash = String(newLocation.search); + if (hash.length == 1 && hash.charAt(0) == "?") { + hash = ""; + } + else if (hash.length >= 2 && hash.charAt(0) == "?") { + hash = hash.substring(1); + } + /*Keep the browser location bar in sync with the iframe hash*/ + window.location.hash = hash; + + /*Notify listeners of the change*/ + this.fireHistoryEvent(hash); + } + + +}; + +/* + historyStorage: An object that uses a hidden form to store history state across page loads. The mechanism for doing so relies on + the fact that browsers save the text in form data for the life of the browser session, which means the text is still there when + the user navigates back to the page. This object can be used independently of the dhtmlHistory object for caching of Ajax + session information. + + dependencies: + * json2007.js (included in a separate file) or alternate JSON methods passed in through an options bundle. +*/ +window.historyStorage = { + + /*Public: Set up our historyStorage object for use by dhtmlHistory or other objects*/ + setup: function(options) { + + /* + options - object to store initialization parameters - passed in from dhtmlHistory or directly into historyStorage + options.debugMode - boolean that causes hidden form fields to be shown for development purposes. + options.toJSON - function to override default JSON stringifier + options.fromJSON - function to override default JSON parser + */ + + /*process init parameters*/ + if (typeof options !== "undefined") { + if (options.debugMode) { + this.debugMode = options.debugMode; + } + if (options.toJSON) { + this.toJSON = options.toJSON; + } + if (options.fromJSON) { + this.fromJSON = options.fromJSON; + } + } + + /*write a hidden form and textarea into the page; we'll stow our history stack here*/ + var formID = "rshStorageForm"; + var textareaID = "rshStorageField"; + var formStyles = this.debugMode ? historyStorage.showStyles : historyStorage.hideStyles; + var textareaStyles = (historyStorage.debugMode + ? 'width: 800px;height:80px;border:1px solid black;' + : historyStorage.hideStyles + ); + var textareaHTML = '
' + + '' + + '
'; + document.write(textareaHTML); + this.storageField = document.getElementById(textareaID); + if (typeof window.opera !== "undefined") { + this.storageField.focus();/*Opera needs to focus this element before persisting values in it*/ + } + }, + + /*Public*/ + put: function(key, value) { + + var encodedKey = encodeURI(key); + + this.assertValidKey(encodedKey); + /*if we already have a value for this, remove the value before adding the new one*/ + if (this.hasKey(key)) { + this.remove(key); + } + /*store this new key*/ + this.storageHash[encodedKey] = value; + /*save and serialize the hashtable into the form*/ + this.saveHashTable(); + }, + + /*Public*/ + get: function(key) { + + var encodedKey = encodeURI(key); + + this.assertValidKey(encodedKey); + /*make sure the hash table has been loaded from the form*/ + this.loadHashTable(); + var value = this.storageHash[encodedKey]; + if (value === undefined) { + value = null; + } + return value; + }, + + /*Public*/ + remove: function(key) { + + var encodedKey = encodeURI(key); + + this.assertValidKey(encodedKey); + /*make sure the hash table has been loaded from the form*/ + this.loadHashTable(); + /*delete the value*/ + delete this.storageHash[encodedKey]; + /*serialize and save the hash table into the form*/ + this.saveHashTable(); + }, + + /*Public: Clears out all saved data.*/ + reset: function() { + this.storageField.value = ""; + this.storageHash = {}; + }, + + /*Public*/ + hasKey: function(key) { + + var encodedKey = encodeURI(key); + + this.assertValidKey(encodedKey); + /*make sure the hash table has been loaded from the form*/ + this.loadHashTable(); + return (typeof this.storageHash[encodedKey] !== "undefined"); + }, + + /*Public*/ + isValidKey: function(key) { + return (typeof key === "string"); + //TODO - should we ban hash signs and other special characters? + }, + + /*- - - - - - - - - - - -*/ + + /*Private - CSS strings utilized by both objects to hide or show behind-the-scenes DOM elements*/ + showStyles: 'border:0;margin:0;padding:0;', + hideStyles: 'left:-1000px;top:-1000px;width:1px;height:1px;border:0;position:absolute;', + + /*Private - debug mode flag*/ + debugMode: false, + + /*Private: Our hash of key name/values.*/ + storageHash: {}, + + /*Private: If true, we have loaded our hash table out of the storage form.*/ + hashLoaded: false, + + /*Private: DOM reference to our history field*/ + storageField: null, + + /*Private: Assert that a key is valid; throw an exception if it not.*/ + assertValidKey: function(key) { + var isValid = this.isValidKey(key); + if (!isValid && this.debugMode) { + throw new Error("Please provide a valid key for window.historyStorage. Invalid key = " + key + "."); + } + }, + + /*Private: Load the hash table up from the form.*/ + loadHashTable: function() { + if (!this.hashLoaded) { + var serializedHashTable = this.storageField.value; + if (serializedHashTable !== "" && serializedHashTable !== null) { + this.storageHash = this.fromJSON(serializedHashTable); + this.hashLoaded = true; + } + } + }, + /*Private: Save the hash table into the form.*/ + saveHashTable: function() { + this.loadHashTable(); + var serializedHashTable = this.toJSON(this.storageHash); + this.storageField.value = serializedHashTable; + }, + /*Private: Bridges for our JSON implementations - both rely on 2007 JSON.org library - can be overridden by options bundle*/ + toJSON: function(o) { + return o.toJSONString(); + }, + fromJSON: function(s) { + return s.parseJSON(); + } +}; + + +/*******************************************************************/ +/** QueryString Object from http://adamv.com/dev/javascript/querystring */ +/* Client-side access to querystring name=value pairs + Version 1.3 + 28 May 2008 + + License (Simplified BSD): + http://adamv.com/dev/javascript/qslicense.txt +*/ +function Querystring(qs) { // optionally pass a querystring to parse + this.params = {}; + + if (qs == null) qs = location.search.substring(1, location.search.length); + if (qs.length == 0) return; + +// Turn back to +// See: http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4.1 + qs = qs.replace(/\+/g, ' '); + var args = qs.split('&'); // parse out name/value pairs separated via & + +// split out each name=value pair + for (var i = 0; i < args.length; i++) { + var pair = args[i].split('='); + var name = decodeURI(pair[0]); + + var value = (pair.length==2) + ? decodeURI(pair[1]) + : name; + + this.params[name] = value; + } +} + +Querystring.prototype.get = function(key, default_) { + var value = this.params[key]; + return (value != null) ? value : default_; +} + +Querystring.prototype.contains = function(key) { + var value = this.params[key]; + return (value != null); +} + +/*******************************************************************/ +/* Added by Ed Wildgoose - MailASail */ +/* Initialise the library and add our history callback */ +/*******************************************************************/ +window.dhtmlHistory.create({ + toJSON: function(o) { + return Object.toJSON(o); + } + , fromJSON: function(s) { + return s.evalJSON(); + } + + // Enable this to assist with debugging +// , debugMode: true + + // dhtmlHistory has been modified not to need the next line + // But left in for robustness when updating dhtmlHistory + , blankURL: '/blank.html?' +}); + +/** Our callback to receive history + change events. */ +var handleHistoryChange = function(pageId, pageData) { + if (!pageData) return; + var info = pageId.split(':'); + var id = info[0]; + pageData += '&_method=get'; + new Ajax.Request(pageData, {asynchronous:true, evalScripts:true, method: 'get', onLoading:function(request){Element.show(id+'-pagination-loading-indicator');}}); +} + +window.onload = function() { + dhtmlHistory.initialize(handleHistoryChange); +}; + diff --git a/frontends/default/javascripts/form_enhancements.js b/app/assets/javascripts/prototype/form_enhancements.js similarity index 95% rename from frontends/default/javascripts/form_enhancements.js rename to app/assets/javascripts/prototype/form_enhancements.js index cab9997e9c..136c2c0e8f 100644 --- a/frontends/default/javascripts/form_enhancements.js +++ b/app/assets/javascripts/prototype/form_enhancements.js @@ -10,7 +10,12 @@ TextFieldWithExample.prototype = { this.defaultText = defaultText; this.createHiddenInput(); + if (options.focus) this.input.focus(); this.checkAndShowExample(); + if (options.focus) { + this.input.selectionStart = 0; + this.input.selectionEnd = 0; + } Event.observe(this.input, "blur", this.onBlur.bindAsEventListener(this)); Event.observe(this.input, "focus", this.onFocus.bindAsEventListener(this)); @@ -39,9 +44,7 @@ TextFieldWithExample.prototype = { this.checkAndShowExample(); }, onFocus: function(event) { - if (this.exampleShown()) { - this.removeExample(); - } + this.removeExample(); }, onClick: function(event) { this.removeExample(); @@ -57,7 +60,7 @@ TextFieldWithExample.prototype = { Element.addClassName(this.input, this.options.exampleClassName); } }, - removeExample: function() { + removeExample: function() { if (this.exampleShown()) { this.input.value = ''; this.input.name = this.name; diff --git a/frontends/default/javascripts/rico_corner.js b/app/assets/javascripts/prototype/rico_corner.js similarity index 100% rename from frontends/default/javascripts/rico_corner.js rename to app/assets/javascripts/prototype/rico_corner.js diff --git a/app/assets/javascripts/prototype/tiny_mce_bridge.js b/app/assets/javascripts/prototype/tiny_mce_bridge.js new file mode 100644 index 0000000000..ce1f2f9cbc --- /dev/null +++ b/app/assets/javascripts/prototype/tiny_mce_bridge.js @@ -0,0 +1,7 @@ +var action_link_close = ActiveScaffold.ActionLink.Abstract.prototype.close; +ActiveScaffold.ActionLink.Abstract.prototype.close = function() { + this.adapter.select('textarea.mceEditor').each(function(elem) { + tinyMCE.execCommand('mceRemoveControl', false, elem.id); + }); + action_link_close.apply(this); +}; diff --git a/app/assets/stylesheets/active_scaffold-ie.css.scss b/app/assets/stylesheets/active_scaffold-ie.css.scss new file mode 100644 index 0000000000..753b33ab27 --- /dev/null +++ b/app/assets/stylesheets/active_scaffold-ie.css.scss @@ -0,0 +1,54 @@ +/* IE hacks + ==================================== */ + +* html .active-scaffold-header, +.active-scaffold li.form-element, +.active-scaffold li.sub-section { +zoom: 1; +} + +* html .active-scaffold td .messages-container { +border-top: solid 1px #DAFFCD; +} + +* html .active-scaffold-header div.actions a.show_search { +background-image: none; +filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='#{image_path('active_scaffold/magnifier.png')}', sizingMethod='crop'); +} + +* html .active-scaffold .sub-form .association-record a.destroy { +background-image: none; +filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='#{image_path('active_scaffold/cross.png')}', sizingMethod='crop'); +} +* html .active-scaffold-header div.actions a.new, +* html .active-scaffold-header div.actions a.new_existing { +background-image: none; +filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='#{image_path('active_scaffold/add.png')}', sizingMethod='crop'); +} +* html .active-scaffold-header th.asc a, +* html .active-scaffold-header th.asc a:hover { +background-image: none; +filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='#{image_path('active_scaffold/arrow_up.png')}', sizingMethod='crop'); +} +* html .active-scaffold-header th.desc a, +* html .active-scaffold-header th.desc a:hover { +background-image: none; +filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='#{image_path('active_scaffold/arrow_down.png')}', sizingMethod='crop'); +} +* html .active-scaffold-header a.inline-adapter-close { +background-image: none; +filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='#{image_path('active_scaffold/close.png')}', sizingMethod='crop'); +} + +.active-scaffold-header div.actions a.disabled { +filter: alpha(opacity=50); +} + +.active-scaffold .show-view dd, +.active-scaffold li.form-element dd { +float: none; +} + +.active-scaffold li.form-element dt { +padding: 4px 0; +} diff --git a/app/assets/stylesheets/active_scaffold.css.scss b/app/assets/stylesheets/active_scaffold.css.scss new file mode 100644 index 0000000000..a103cd5c1a --- /dev/null +++ b/app/assets/stylesheets/active_scaffold.css.scss @@ -0,0 +1,14 @@ +/* + ActiveScaffold + (c) 2007 Richard White + + ActiveScaffold is freely distributable under the terms of an MIT-style license. + + For details, see the ActiveScaffold web site: http://www.activescaffold.com/ +*/ + +@import 'active_scaffold_layout'; +@import 'active_scaffold_images'; +@import 'active_scaffold_jquery_ui'; +@import 'active_scaffold_extensions'; +@import 'active_scaffold_colors'; diff --git a/app/assets/stylesheets/active_scaffold_colors.css.scss b/app/assets/stylesheets/active_scaffold_colors.css.scss new file mode 100644 index 0000000000..aa127a4df4 --- /dev/null +++ b/app/assets/stylesheets/active_scaffold_colors.css.scss @@ -0,0 +1,402 @@ +/* + ActiveScaffold + (c) 2007 Richard White + + ActiveScaffold is freely distributable under the terms of an MIT-style license. + + For details, see the ActiveScaffold web site: http://www.activescaffold.com/ + +*/ + +$disabled_color: #999 !default; +$actions_disabled_color: #666 !default; +$link_color: #06c !default; +$hover_bg: #ff8 !default; +$header_color: #555 !default; + +$column_header_bg: $header_color !default; +$column_header_link_color: #fff !default; +$column_header_link_hover_bg: #000 !default; +$column_header_link_hover_color: #ff8 !default; +$column_header_color: #eee !default; +$column_header_sorted_bg: #333 !default; + +$column_bg: #E6F2FF !default; +$column_even_bg: #fff !default; +$column_color: #333 !default; +$column_empty_color: #999 !default; +$column_border_color: #C5DBF7 !default; +$column_even_border_color: #ddd !default; +$column_actions_border_color: #ccc !default; + +$column_sorted_bg: #B9DCFF !default; +$column_sorted_border_color: #AFD0F5 !default; +$column_even_sorted_bg: #E6F2FF !default; +$column_even_sorted_border_color: #AFD0F5 !default; + +$calculations_bg: #eee !default; +$calculations_border_color: #005CB8 !default; + +$action_group_color: #0066CC !default; +$action_group_hover_bg: #ff8 !default; +$action_group_border_color: #005CB8 !default; +$action_group_items_bg: #EEE !default; +$action_group_items_border_color: #222 !default; +$action_group_link_color: $column_color !default; + +$nested_bg: #DAFFCD !default; +$nested_border_color: #7FCF00 !default; +$nested_footer_color: #444 !default; +$nested_column_bg: #ECFFE7 !default; +$nested_column_even_bg: #fff !default; +$nested_column_border_color: $column_border_color !default; + +$second_nested_bg: #FFFFBB !default; +$second_nested_border_color: #DDDF37 !default; + +$third_nested_bg: $nested_bg !default; +$third_nested_border_color: $nested_border_color !default; + +$pagination_border_color: #ccc !default; +$msg_color: #333 !default; +$msg_error_bg: #fbb !default; +$msg_error_border_color: #f66 !default; +$msg_warning_bg: #ffb !default; +$msg_warning_border_color: #ff6 !default; +$msg_info_bg: #bbf !default; +$msg_info_border_color: #66f !default; +$msg_filtered_bg: #e8e8e8 !default; +$msg_filtered_color: #666 !default; + +$form_title_color: #1F7F00 !default; +$label_color: $header_color !default; +$description_color: #999 !default; +$placeholder_color: #aaa !default; +$input_border_color: $form_title_color !default; +$input_error_border_color: #f00 !default; +$input_focus_bg: #ffc !default; + +$draggable_list_bg: #FFFF88 !default; +$draggable_list_selected_bg: #7FCF00 !default; +$checkbox_list_bg: #fff !default; + +$subform_color: #999 !default; +$subform_header_color: $header_color !default; +$subform_footer_color: $subform_color !default; + +.active-scaffold a, .active-scaffold a:visited { +color: $link_color; +} + +.active-scaffold a.disabled { +color: $disabled_color; +} + +.active-scaffold a:hover, .active-scaffold div.hover, .active-scaffold td span.hover { +background-color: $hover_bg; +} + +noscript.active-scaffold { +border-color: $msg_error_border_color; +background-color: $msg_error_bg; +color: $column_color; +} + +/* Header + ======================== */ + +.active-scaffold-header h2 { +color: $header_color; +} + +.active-scaffold-header div.actions a.disabled { +color: $actions_disabled_color; +} + +/* Table :: Column Headers + ============================= */ + +.active-scaffold th { +background-color: $column_header_bg; +} + +.active-scaffold th a, +.active-scaffold th p { +background-color: $column_header_bg; +} + +.active-scaffold th a, .active-scaffold th a:visited { +color: $column_header_link_color; +} + +.active-scaffold th p { +color: $column_header_color; +} + +.active-scaffold th a:hover { +background-color: $column_header_link_hover_bg; +color: $column_header_link_hover_color; +} + +.active-scaffold th.sorted { +background-color: $column_header_sorted_bg; +} + +.active-scaffold th.asc a, +.active-scaffold th.asc a:hover, +.active-scaffold th.desc a, +.active-scaffold th.desc a:hover, +.active-scaffold th.loading a, +.active-scaffold th.loading a:hover { + background-color: $column_header_sorted_bg; +} + +/* Table :: Record Rows + ============================= */ + +.active-scaffold tr.record { + background-color: $column_bg; +} +.active-scaffold tr.record td { +color: $column_color; +border-color: $column_border_color; +} + +.active-scaffold tr.even-record { +background-color: $column_even_bg; +} +.active-scaffold tr.even-record td { +border-left-color: $column_even_border_color; +} + +.active-scaffold tr.record td.sorted { +background-color: $column_sorted_bg; +border-bottom-color: $column_sorted_border_color; +} + +.active-scaffold tr.even-record td.sorted { +background-color: $column_even_sorted_bg; +border-bottom-color: $column_even_sorted_border_color; +} + +.active-scaffold tbody.records td.empty { +color: $column_empty_color; +} + +/* Table :: Actions (Edit, Delete) + ============================= */ +.active-scaffold tr.record td.actions { +border-color: $column_actions_border_color; +} + +.active-scaffold tr.record td.actions a.disabled { +color: $actions_disabled_color; +} + +.active-scaffold .actions .action_group div:hover { +background-color: $action_group_hover_bg; +} + +.active-scaffold .actions .action_group { +color: $action_group_color; +} + +.active-scaffold .actions .action_group ul { +border-color: $action_group_border_color; +} + +.active-scaffold .actions .action_group ul li { +background-color: $action_group_items_bg; +border-color: $action_group_items_border_color; +} + +.active-scaffold .actions .action_group ul li a { + color: $action_group_link_color; +} + +/* Table :: Inline Adapter + ============================= */ + +.active-scaffold .view { +background-color: $nested_bg; +border-color: $nested_border_color; +} + +/* Nested + ======================== */ + +.active-scaffold .active-scaffold .active-scaffold-footer { +color: $nested_footer_color; +} + +.active-scaffold .active-scaffold tr.record { +background-color: $nested_column_bg; +border-color: $nested_column_border_color; +} +.active-scaffold .active-scaffold tr.even-record { +background-color: $nested_column_even_bg; +} + +.active-scaffold .active-scaffold td.inline-adapter-cell { +background-color: $second_nested_bg; +border-color: $second_nested_border_color; +} + +.active-scaffold .active-scaffold .active-scaffold td.inline-adapter-cell { +background-color: $third_nested_bg; +border-color: $third_nested_border_color; +} + +/* Footer + ========================== */ + +.active-scaffold-calculations td { +background-color: $calculations_bg; +border-color: $calculations_border_color; +} + +.active-scaffold-footer .next { +border-color: $pagination_border_color; +} + +.active-scaffold-footer .previous { +border-color: $pagination_border_color; +} + +/* Messages + ========================= */ + + +.active-scaffold .empty-message, .active-scaffold .filtered-message { +background-color: $msg_filtered_bg; +color: $msg_filtered_color; +} + +.active-scaffold .message { +color: $msg_color; +} + +.active-scaffold .error-message { +border-color: $msg_error_border_color; +background-color: $msg_error_bg; +} + +.active-scaffold .warning-message { +border-color: $msg_warning_border_color; +background-color: $msg_warning_bg; +} + +.active-scaffold .info-message { +border-color: $msg_info_border_color; +background-color: $msg_info_bg; +} + +/* Error Styling + ========================== */ + +.active-scaffold .errorExplanation { +background-color: $msg_error_bg; +border-color: $msg_error_border_color; +} + +.active-scaffold .errorExplanation h2 { +color: $msg_color; +background-color: $msg_error_border_color; +} + +/* Show + ============================= */ + +.active-scaffold .show-view dt { +color: $label_color; +} + +/* Form + ============================== */ + +.active-scaffold h4, +.active-scaffold h5 { +color: $form_title_color; +} + +/* Form :: Fields + ============================== */ + +.active-scaffold label { +color: $label_color; +} + +.active-scaffold .description { +color: $description_color; +} + +.active-scaffold label.example { +color: $placeholder_color; +} + +.active-scaffold input.text-input, +.active-scaffold select { +border-color: $input_border_color; +} + +.active-scaffold input:invalid, +.active-scaffold textarea:invalid, +.active-scaffold select:invalid, +.active-scaffold .fieldWithErrors input, +.active-scaffold .field_with_errors input, +.active-scaffold .fieldWithErrors textarea, +.active-scaffold .field_with_errors textarea, +.active-scaffold .fieldWithErrors select, +.active-scaffold .field_with_errors select { +border-color: $input_error_border_color; +} + +.active-scaffold input.example { +color: $placeholder_color; +} + +.active-scaffold select:focus, +.active-scaffold input.text-input:focus { +background-color: $input_focus_bg; +} + +.active-scaffold textarea { +border-color: $input_border_color; +} + +.active-scaffold .draggable-list { +background-color: $draggable_list_bg; +} + +.active-scaffold .draggable-list.selected { +background-color: $draggable_list_selected_bg; +} + + +/* Form :: Association Sub-Forms + ============================== */ + +.active-scaffold .sub-form table th { +color: $subform_header_color; +} + +.active-scaffold .sub-form .checkbox-list { +background-color: $checkbox_list_bg; +border-color: $input_border_color; +} + +.active-scaffold .sub-form .checkbox-list label { +display: block; +} + +.active-scaffold .sub-form table td { +color: $subform_color; +} + +.active-scaffold .sub-form .footer { +color: $subform_footer_color; +} + +.as_touch th a, .as_touch th a:visited { +color: $column_header_link_color; +} diff --git a/app/assets/stylesheets/active_scaffold_extensions.css.erb b/app/assets/stylesheets/active_scaffold_extensions.css.erb new file mode 100644 index 0000000000..b42b27de46 --- /dev/null +++ b/app/assets/stylesheets/active_scaffold_extensions.css.erb @@ -0,0 +1,2 @@ +<% ActiveScaffold.stylesheets.each {|css| require_asset css} %> +<% ActiveScaffold::Bridges.all_stylesheets.each {|css| require_asset css} %> diff --git a/app/assets/stylesheets/active_scaffold_images.css.scss b/app/assets/stylesheets/active_scaffold_images.css.scss new file mode 100644 index 0000000000..0ecf5fc96c --- /dev/null +++ b/app/assets/stylesheets/active_scaffold_images.css.scss @@ -0,0 +1,40 @@ +.active-scaffold-header div.actions div.action_group div { + background-image: image-url('active_scaffold/gears.png'); /* default icon for actions or override with css */ +} + +.active-scaffold-header div.actions a.show_config_list { + background-image: image-url('active_scaffold/config.png'); +} + +.active-scaffold-header div.actions a.new, +.active-scaffold-header div.actions a.new_existing { +background-image: image-url('active_scaffold/add.png'); +} + +.active-scaffold-header div.actions a.show_search { +background-image: image-url('active_scaffold/magnifier.png'); +} + +.active-scaffold th.asc a { +background-image: image-url('active_scaffold/arrow_up.png'); +} + +.active-scaffold th.desc a { +background-image: image-url('active_scaffold/arrow_down.png'); +} + +.active-scaffold th.loading a { +background-image: image-url('active_scaffold/indicator-small.gif'); +} + +.active-scaffold a.inline-adapter-close { +background-image: image-url('active_scaffold/close.png'); +} + +.active-scaffold .sub-form .association-record a.destroy { +background-image: image-url('active_scaffold/cross.png'); +} + +.as_touch a.inline-adapter-close { +background-image: image-url('active_scaffold/close_touch.png'); +} diff --git a/app/assets/stylesheets/active_scaffold_jquery_ui.css.erb b/app/assets/stylesheets/active_scaffold_jquery_ui.css.erb new file mode 100644 index 0000000000..0a8fb37515 --- /dev/null +++ b/app/assets/stylesheets/active_scaffold_jquery_ui.css.erb @@ -0,0 +1,7 @@ +<% if Jquery::Rails.const_defined? 'JQUERY_UI_VERSION' %> +<% require_asset "jquery-ui" %> +<% require_asset "jquery-ui-theme" %> +<% elsif Jquery.const_defined? 'Ui' %> +<% require_asset "jquery.ui.datepicker" %> +<% require_asset "jquery-ui-theme" %> +<% end %> diff --git a/app/assets/stylesheets/active_scaffold_layout.css b/app/assets/stylesheets/active_scaffold_layout.css new file mode 100644 index 0000000000..b0b1785276 --- /dev/null +++ b/app/assets/stylesheets/active_scaffold_layout.css @@ -0,0 +1,946 @@ +.active-scaffold form, +.active-scaffold table, +.active-scaffold p, +.active-scaffold div, +.active-scaffold fieldset { +margin: 0; +padding: 0; +} + +.active-scaffold { +margin: 5px 0; +} + +.active-scaffold table { +width: 100%; +border-collapse: collapse; +} +.active-scaffold td, .active-scaffold th { padding: 0; } + +.active-scaffold a, +.active-scaffold a:visited { +text-decoration: none; +} + +.active-scaffold div.actions a img, +.active-scaffold td.actions a img { +border: none; +vertical-align: middle; +} + +.active-scaffold div.actions a.disabled img, +.active-scaffold td.actions a.disabled img { +opacity: 0.5; +} + +.active-scaffold .clear-fix { +clear: both; +} + +noscript.active-scaffold { +border-left: solid 5px; +font-size: 11px; +font-weight: bold; +padding: 5px 20px 5px 5px; +} + +.active-scaffold .mark_record_column { + width: 1px; +} + +/* Header + ======================== */ + +.active-scaffold-header { +position: relative; +} + +.active-scaffold-header h2 { +padding: 2px 0px; +margin: 0; +font: bold 160% arial, sans-serif; +} + +.active-scaffold-header div.actions a, +.active-scaffold-header div.actions { +float: right; +font: bold 14px arial; +letter-spacing: -1px; +text-decoration: none; +padding: 1px 2px; +white-space: nowrap; +margin-left: 5px; +background-position: 1px 50%; +background-repeat: no-repeat; +} + +.active-scaffold-header div.actions a { +padding: 5px 5px; +margin-left: 0px; +} + +.active-scaffold .active-scaffold .active-scaffold-header div.actions > a { +padding: 1px 5px; +} + +.active-scaffold-header div.actions div.action_group { +display: inline; +float: right; +} + +.active-scaffold-header div.actions div.action_group li a, +.active-scaffold-header div.actions div.action_group li div { +float: none; +margin: 0; +} + +.active-scaffold-header div.actions .action_group ul { +line-height: 130%; +top: 19px; +} + +.active-scaffold .active-scaffold .active-scaffold-header div.actions .action_group ul { +top: 14px; +} + +.view .active-scaffold-header div.actions a, +.view .active-scaffold-header div.actions div, +.view .active-scaffold-header div.actions div.action_group { +float: left; +} + +.active-scaffold-header div.actions a.disabled { +opacity: 0.5; +} + +.active-scaffold-header div.actions a.new, +.active-scaffold-header div.actions a.new_existing, +.active-scaffold-header div.actions a.show_search, +.active-scaffold-header div.actions a.show_config_list, +.active-scaffold-header div.actions div.action_group div { +margin:0; +padding: 5px 5px 5px 25px; +background-position: 5px 50%; +background-repeat: no-repeat; +} + +.active-scaffold .active-scaffold .active-scaffold-header div.actions > a.new, +.active-scaffold .active-scaffold .active-scaffold-header div.actions > a.new_existing, +.active-scaffold .active-scaffold .active-scaffold-header div.actions > a.show_search, +.active-scaffold .active-scaffold .active-scaffold-header div.actions > a.show_config_list, +.active-scaffold .active-scaffold .active-scaffold-header div.actions div.action_group > div { +margin:0; +padding: 1px 5px 1px 20px; +background-position: 1px 50%; +background-repeat: no-repeat; +} + +.active-scaffold-header div.actions a.disabled:hover { +background-color: transparent; +cursor: default; +} + +/* Table :: Column Headers + ============================= */ + +.active-scaffold th { +text-align: left; +} + +.active-scaffold th a, +.active-scaffold th p { +font: bold 11px arial, sans-serif; +display: block; +} + +.active-scaffold th a, .active-scaffold th a:visited { +padding: 2px 2px 2px 5px; +} + +.active-scaffold th p { +padding: 2px 5px; +} + +.active-scaffold th.sorted a { +padding-right: 18px; +} + +.active-scaffold th.asc a, +.active-scaffold th.desc a, +.active-scaffold th.loading a { + background: right 50% no-repeat; +} + +.active-scaffold th .mark_heading { +margin-left: 5px; +} + +.active-scaffold th.hidden, .active-scaffold td.hidden { +display: none; +} + +/* Table :: Record Rows + ============================= */ + +.active-scaffold tr.record td { +padding: 5px 4px; +font-family: Verdana, sans-serif; +font-size: 11px; +border: solid 1px; +border-width: 0 0 1px 1px; +} + +.active-scaffold tr.record td.messages-container { +padding: 0px; +} + +.active-scaffold tbody.records td.empty { +text-align: center; +} +.active-scaffold tbody.records td.in_place_editor_field .handle { +display:none; +} +.active-scaffold tbody.records td.in_place_editor_field.empty .handle { +display:inline; +} + +.active-scaffold td.numeric, +.active-scaffold-calculations td { +text-align: right; +} + +/* Table :: Actions (Edit, Delete) + ============================= */ +.active-scaffold tr.record td.actions { +border-right: solid 1px; +padding: 0; +min-width: 1%; +} + +.active-scaffold tr.record td.actions table { +float: right; +width: auto; +margin-right: 5px; +} + +.active-scaffold tr.record td.actions table td { +border: none; +text-align: right; +padding: 0 2px; +} + +.active-scaffold tr.record td.actions a, +.active-scaffold tr.record td.actions div { +font: bold 11px verdana, sans-serif; +letter-spacing: -1px; +padding: 2px; +margin: 0 2px; +line-height: 16px; +white-space: nowrap; +} + +.active-scaffold tr.record td.actions a.disabled { +opacity: 0.5; +} + +.active-scaffold .actions .action_group { +position: relative; +text-align: left; +} + +.active-scaffold .actions .action_group ul { +border: 2px solid; +list-style-type: none; +margin: 0; +padding: 0; +position: absolute; +line-height: 200%; +display: none; +width: 150px; +right: 0px; +z-index: 2; +} +.active-scaffold .actions .action_group.dyn ul { +width: auto; +display: block; +} + +.active-scaffold .actions .action_group ul ul { +display: none; +position: absolute; +top: 0; +right: 150px; +} + +.active-scaffold .actions .action_group ul li { +background: none repeat scroll 0 0; +border-top: 1px dashed; +display: block; +position: relative; +width: auto; +text-align: left; +} + +.active-scaffold .actions .action_group ul li div { + margin: 0; + padding: 5px 5px 5px 25px; + background-position: 5px 50%; + background-repeat: no-repeat; +} + +.active-scaffold .actions .action_group ul li a { + display: block; + margin: 0; + padding: 5px 5px 5px 25px; + background-position: 5px 50%; + background-repeat: no-repeat; +} +.active-scaffold .actions .action_group.dyn ul li a { + padding-left: 5px; +} + +.active-scaffold .actions .action_group ul li.top { +border-top-width: 0px; +} + +.active-scaffold .actions .action_group:hover ul ul, +.active-scaffold .actions .action_group:hover ul ul ul { +display: none; +} + +.active-scaffold .actions .action_group:hover ul, +.active-scaffold .actions .action_group ul li:hover > ul, +.active-scaffold .actions .action_group ul ul li:hover ul { +display: block; +} + +/* Table :: Inline Adapter + ============================= */ + +.active-scaffold .view { +padding: 4px; +border: solid 1px; +} + +.active-scaffold tbody.records td.inline-adapter-cell .view { +border-top: none; +} + +.active-scaffold .before-header td.inline-adapter-cell .view { +border-bottom: none; +} + +.active-scaffold a.inline-adapter-close { +float: right; +text-indent: -4000px; +width: 16px; +height: 17px; +background: 0 0 no-repeat; +} + +/* Nested + ======================== */ + +.active-scaffold .active-scaffold .active-scaffold-header { +margin-right: 25px; +} + +.active-scaffold .active-scaffold .active-scaffold-header h2 { +font-size: 12px; +font-weight: bold; +} + + +.active-scaffold .active-scaffold .active-scaffold-header div.actions { +top: 0px; +right: 0px; +} + +.active-scaffold .active-scaffold .active-scaffold-header div.actions a, +.active-scaffold .active-scaffold .active-scaffold-header div.actions div { +font: bold 11px verdana, sans-serif; +} + +.active-scaffold .active-scaffold .view { +background-color: transparent; +padding: 0px; +border: none; +} + +.active-scaffold .active-scaffold td { +border-bottom: solid 1px; +border-left: solid 1px; +} + +.active-scaffold .active-scaffold td.inline-adapter-cell { +padding: 4px; +border: solid 1px; +border-top: none; +} + +.active-scaffold .active-scaffold .active-scaffold td.inline-adapter-cell { +padding: 4px; +border: solid 1px; +border-top: none; +} + +.active-scaffold .active-scaffold .active-scaffold-footer { +font-size: 11px; +} + +/* Footer + ========================== */ + +.active-scaffold-calculations td { +border-top: 2px solid; +font: bold 12px arial, sans-serif; +} + +.active-scaffold .active-scaffold-footer { +padding: 3px 0px 2px 0px; +border-bottom: none; +font: bold 12px arial, sans-serif; +} +.active-scaffold .active-scaffold-footer > br { +clear: both; +} + +.active-scaffold-footer .active-scaffold-pagination { +float: right; +white-space: nowrap; +margin-right: 5px; +} + +.active-scaffold-footer a { +text-decoration: none; +letter-spacing: 0; +padding: 0 2px; +margin: 0 -2px; +font: bold 12px arial, sans-serif; +} + +.active-scaffold-footer .next { +margin-left: 0; +padding-left: 5px; +border-left: solid 1px; +} + +.active-scaffold-footer .previous { +margin-right: 0; +padding-right: 5px; +border-right: solid 1px; +} + +/* Messages + ========================= */ + +.active-scaffold .messages-container, +.active-scaffold .active-scaffold .messages-container{ +padding: 0; +margin: 0 7px; +border: none; +} + +.active-scaffold .empty-message, .active-scaffold .filtered-message { +padding: 4px; +text-align: center; +position: relative; +} + +.active-scaffold .message { +font-size: 11px; +font-weight: bold; +padding: 5px 20px 5px 5px; +position: relative; +margin: 2px 7px; +line-height: 12px; +} +.active-scaffold .message .timestamp, +.active-scaffold .message .message-content { +display: inline; +} + +.active-scaffold .filtered-message .reset { +position: absolute; +display: inline; +right: 10px; +top: 4px; +padding: 0; +} + +.active-scaffold .message a.close { +position: absolute; +right: 10px; +top: 4px; +padding: 0; +font: bold 11px verdana, sans-serif; +letter-spacing: -1px; +} + +.active-scaffold .messages-container .message { +margin: 0; +} + +.active-scaffold .error-message { +border-left: solid 5px; +} + +.active-scaffold .warning-message { +border-left: solid 5px; +} + +.active-scaffold .info-message { +border-left: solid 5px; +} + +/* Error Styling + ========================== */ + +.active-scaffold .errorExplanation { +border: solid 1px; +} + +.active-scaffold fieldset { +clear: both; +} + +.active-scaffold .errorExplanation h2 { +padding: 2px 5px; +font-size: 11px; +margin: 0; +letter-spacing: 0; +font-family: Verdana; +} + +.active-scaffold .errorExplanation ul { +margin: 0; +padding: 0 2px 4px 25px; +list-style: disc; +} + +.active-scaffold .errorExplanation p { +font-size: 11px; +padding: 2px 5px; +font-family: Verdana; +margin: 0; +} + +.active-scaffold .errorExplanation ul li { +font: bold 11px verdana; +letter-spacing: -1px; +margin: 0; +padding: 0; +background-color: transparent; +} + +/* Loading Indicators + ============================== */ + +.active-scaffold .loading-indicator { +vertical-align: text-bottom; +width: 16px; +margin: 0; +} + +.active-scaffold .active-scaffold-header .loading-indicator { +margin-top: 3px; +} + +/* Show + ============================= */ + +.active-scaffold .show-view dl { +margin-left: 5px; +} +.active-scaffold .show-view dl dl { +margin-left: 0px; +} + +.active-scaffold .show-view dt { +width: 12em; +float: left; +clear: left; +font: normal 11px verdana, sans-serif; +line-height: 16px; +} + +.active-scaffold .show-view dd { +float: left; +font: bold 14px arial; +padding-left: 5px; +margin-bottom: 5px; +} + +/* Form + ============================== */ + +.active-scaffold dl { +margin: 0; +} + +.active-scaffold .submit { +font-weight: bold; +font-size: 14px; +font-family: Arial, sans-serif; +letter-spacing: 0; +margin: 0; +margin-top: 5px; +} + +.active-scaffold form p { +clear: both; +} + +.active-scaffold fieldset { +border: none; +} + +.active-scaffold h4, +.active-scaffold h5 { +padding: 2px; +margin: 0; +text-transform: none; +letter-spacing: -1px; +font: bold 16px arial; +} + +.active-scaffold h5 { +padding: 0; +margin: 5px 0 2px 0; +font-size: 14px; +letter-spacing: 0; +} + +.active-scaffold ol { +clear: both; +float: none; +padding: 2px; +margin-left: 5px; +list-style: none; +} +.active-scaffold ol:after { +content: '.'; +visibility: hidden; +line-height: 0; +height: 0; +display: block; +clear: both; +} + +.active-scaffold p.form-footer { +clear: both; +} + +.active-scaffold a.as_cancel, +.active-scaffold p.form-footer a { +font: bold 14px arial, sans-serif; +letter-spacing: 0; +} + +/* Form :: Fields + ============================== */ + +.active-scaffold li.form-element { +clear: both; +} + +.active-scaffold label { +font: normal 11px verdana, sans-serif; +} + +.active-scaffold li.form-element dt { +float: left; +width: 12em; +padding: 6px 0; +} + +.active-scaffold li.form-element dd { +float: left; +} + +.active-scaffold li.form-element dd p, +.active-scaffold li.form-element dd input[type="checkbox"] { +margin-top: 6px; +} + +.active-scaffold .form dd { +margin: 0; +} + + +.active-scaffold .description { +display: inline-block; +font-size: 10px; +margin-left: 5px; +} + +.active-scaffold .required label { +font-weight: bold; +} + +.active-scaffold label.example { +font-size: 11px; +font-family: arial; +} + +.active-scaffold input.text-input, +.active-scaffold select { +font: bold 16px arial; +letter-spacing: -1px; +border: solid 1px; +} + +.active-scaffold input.text-input { +padding: 2px; +} + +.active-scaffold .fieldWithErrors input, +.active-scaffold .field_with_errors input, +.active-scaffold .fieldWithErrors textarea, +.active-scaffold .field_with_errors textarea, +.active-scaffold .fieldWithErrors select, +.active-scaffold .field_with_errors select { +border: solid 1px; +} + +.active-scaffold select { +padding: 1px; +} + + +.active-scaffold textarea { +font-family: Arial, sans-serif; +font-size: 12px; +padding: 1px; +border: solid 1px; +} + +.active-scaffold .checkbox-list { +padding-left: 0px; +} + +.active-scaffold .checkbox-list li { +padding-right: 5px; +display: inline; +} + +.active-scaffold .checkbox-list li label { +padding: 0 0 0 2px; +} + +.active-scaffold .draggable-list { +float: left; +width: 300px; +margin-right: 15px; +min-height: 30px; +max-height: 100px; +overflow: auto; +} + +.active-scaffold .draggable-list.hover { +opacity: 0.5; +} + + +.active-scaffold .draggable-list li { +display: block; +} + +li.draggable-item { + list-style: none; +} +li.draggable-item input, +.active-scaffold .draggable-list input { +display: none; +} + +/* Form :: Sub-Sections + ============================== */ + +.active-scaffold li.sub-section { +clear: left; +padding: 5px 0; +} + +/* Form :: Association Sub-Forms + ============================== */ + +.active-scaffold .sub-form { +float: left; +clear: left; +padding: 5px 0; +padding-left: 5px; +} + +.active-scaffold .sub-form h5 { +margin-left: -5px; +} + +.active-scaffold .sub-form table, +.active-scaffold .sub-form table td { +width: auto; +background: none; +} + +.active-scaffold .sub-form table th { +font: normal 10px verdana, sans-serif; +padding: 0 5px 0 1px; +background: none; +} + +.active-scaffold .horizontal-sub-form td dt label { +display: none; +} + +.active-scaffold .horizontal-sub-form .associated-record dl { +float: left; +margin-right: 5px; +} + +.active-scaffold .sub-form .checkbox-list { +padding: 0 2px 2px 2px; +border: solid 1px; +} + +.active-scaffold .sub-form .checkbox-list label { +display: block; +} + +.active-scaffold .sub-form table td { +border: none; +background-color: transparent; +padding: 1px; +vertical-align: top; +} + +.active-scaffold .sub-form .actions { +vertical-align: middle; +background-color: transparent; +clear: left; +} + +.active-scaffold .sub-form .association-record a.destroy { +font-weight: bold; +display: block; +height: 16px; +padding: 0; +width: 16px; +text-indent: -4000px; +background: 0 0 no-repeat; +} + +.active-scaffold .sub-form .locked a.destroy { +display: none; +} + +.active-scaffold .sub-form .association-record a { +font: bold 12px arial; +} + +.active-scaffold .sub-form input.text-input, +.active-scaffold .sub-form select { +letter-spacing: 0; +font: bold 12px arial; +} + +.active-scaffold .sub-form .footer-wrapper { +margin-top: 3px; +margin-right: 10px; +} + +.active-scaffold .sub-form .footer { +padding: 3px 5px; +} + +.active-scaffold .sub-form .footer select, +.active-scaffold .sub-form .footer input { +font-weight: bold; +font-size: 12px; +padding: 0; +} + +.active-scaffold a.visibility-toggle { +font-size: 100%; +} + +.active-scaffold-found { + float:left; +} + +.as_touch a.inline-adapter-close { +width: 25px; +height: 27px; +background: 0 0 no-repeat; +} + +.as_touch .as_paginate { +font-size: 20px; +padding: 3px 10px; +} + +.as_touch .active-scaffold-header div.actions a { +padding: 7px 5px; +} + +.as_touch .active-scaffold .active-scaffold-header div.actions a { +padding: 7px 5px; +} + +.as_touch .active-scaffold-header div.actions .action_group ul { +line-height: 130%; +top: 23px; +} + +.as_touch .active-scaffold .active-scaffold-header div.actions .action_group ul { +top: 23px; +} + +.as_touch .active-scaffold-header div.actions a.new, +.as_touch .active-scaffold-header div.actions a.new_existing, +.as_touch .active-scaffold-header div.actions a.show_search, +.as_touch .active-scaffold-header div.actions a.show_config_list, +.as_touch .active-scaffold-header div.actions div.action_group div { +padding: 7px 5px 7px 25px; +} + +.as_touch .active-scaffold .active-scaffold-header div.actions > a.new, +.as_touch .active-scaffold .active-scaffold-header div.actions > a.new_existing, +.as_touch .active-scaffold .active-scaffold-header div.actions > a.show_search, +.as_touch .active-scaffold .active-scaffold-header div.actions > a.show_config_list, +.as_touch .active-scaffold .active-scaffold-header div.actions div.action_group > div { +padding: 7px 5px 7px 25px; +background-position: 5px 50%; +} + +.as_touch .actions .action_group ul li div { +padding: 7px 5px 7px 25px; +} + +.as_touch .actions .action_group ul li a { +padding: 7px 5px 7px 25px; +} + +.as_touch .active-scaffold-header h2 { +padding: 4px 0px; +} + +.as_touch .active-scaffold .active-scaffold-header div.actions a, +.as_touch .active-scaffold .active-scaffold-header div.actions div { + font: bold 14px arial; +} + +.as_touch .active-scaffold .active-scaffold-header div.actions { + right: 15px; +} + +.as_touch tr.record { +line-height: 130%; +} + +.as_touch th a, .as_touch th a:visited { +padding: 5px 2px 5px 5px; +} + +.as_touch tr.record td { +padding: 5px 10px; +} diff --git a/app/assets/stylesheets/blue-theme.css b/app/assets/stylesheets/blue-theme.css new file mode 100644 index 0000000000..2c65c58c2a --- /dev/null +++ b/app/assets/stylesheets/blue-theme.css @@ -0,0 +1,74 @@ +/* + ActiveScaffold Blue Theme + (c) 2007 Richard White + + ActiveScaffold is freely distributable under the terms of an MIT-style license. + + For details, see the ActiveScaffold web site: http://www.activescaffold.com/ + +*/ + +/* Header + ======================== */ + +.blue-theme .active-scaffold-header { +background-color: #005CB8; +} + +.blue-theme .active-scaffold-header h2 { +color: #fff; +padding: 2px 5px 4px 5px; +} + +.blue-theme .active-scaffold-header div.actions a { +color: #fff; +} + +.blue-theme .active-scaffold-header div.actions a.disabled { +color: #fff; +opacity: 0.5; +} + +.blue-theme .active-scaffold-header div.actions a:hover { +background-color: #378CDF; +} + +.blue-theme .active-scaffold .active-scaffold-header, +.blue-theme .active-scaffold .active-scaffold-footer { +background-color: #1F7F00; +background: transparent; +} + +.blue-theme .active-scaffold .active-scaffold-header h2 { +color: #444; +} + +.blue-theme .active-scaffold .active-scaffold-header div.actions a, +.blue-theme .active-scaffold .active-scaffold-header div.actions a:visited { +color: #06c; +} + +.blue-theme .active-scaffold .active-scaffold-header div.actions a:hover { +background-color: #ff8; +} + +/* Footer + ========================== */ + +.blue-theme .active-scaffold-footer { +background-color: #005CB8; +color: #ccc; +} + +.blue-theme .active-scaffold-footer .active-scaffold-records { +margin-left: 5px; +} + +.blue-theme .active-scaffold-footer a, +.blue-theme .active-scaffold-footer a:visited { +color: #fff; +} + +.blue-theme .active-scaffold-footer a:hover { +background-color: #378CDF; +} diff --git a/app/views/active_scaffold_overrides/_add_existing_form.html.erb b/app/views/active_scaffold_overrides/_add_existing_form.html.erb new file mode 100644 index 0000000000..7feb11a6b4 --- /dev/null +++ b/app/views/active_scaffold_overrides/_add_existing_form.html.erb @@ -0,0 +1,30 @@ +<% url_options = params_for(:action => :add_existing) -%> +<% xhr = request.xhr? -%> +<%= +options = {:id => element_form_id(:action => :add_existing), + :class => "as_form create", + :method => :post, + 'data-loading' => true} + options[:remote] = true if xhr + form_tag url_options, options -%> + +

<%= active_scaffold_config.nested.label -%>

+ + <% if xhr -%> +
+ <% else -%> + <%= render :partial => 'form_messages' %> + <% end -%> + + + <%= active_scaffold_add_existing_input(:name => 'associated_id', :url_options => url_options) %> + + + + +<%= javascript_tag("ActiveScaffold.focus_first_element_of_form('#{element_form_id(:action => :add_existing)}');") %> + diff --git a/app/views/active_scaffold_overrides/_base_form.html.erb b/app/views/active_scaffold_overrides/_base_form.html.erb new file mode 100644 index 0000000000..6b7e742615 --- /dev/null +++ b/app/views/active_scaffold_overrides/_base_form.html.erb @@ -0,0 +1,53 @@ +<% scope ||= nil + footer_extension ||= nil + url_options ||= params_for(:action => form_action) + xhr = request.xhr? if xhr.nil? + if active_scaffold_config.actions.include? form_action + multipart ||= active_scaffold_config.send(form_action).multipart? + columns ||= active_scaffold_config.send(form_action).columns + persistent ||= active_scaffold_config.send(form_action).persistent + else + multipart ||= false + columns ||= nil + persistent ||= false + end + method ||= :post + cancel_link = true if cancel_link.nil? + submit_text ||= form_action + body_partial ||= 'form' %> +<%= +options = {:onsubmit => onsubmit, + :id => element_form_id(:action => form_action), + :multipart => multipart, + :class => "as_form #{form_action.to_s}", + :method => method, + 'data-loading' => true} +cancel_options = {:class => 'as_cancel'} + +cancel_options[:remote] = true if xhr #cancel link does nt have to care about multipart forms +if xhr && multipart # file_uploads + form_remote_upload_tag url_options.merge({:iframe => true}), options +else + options[:remote] = true if xhr && !multipart + form_tag url_options, options +end +-%> + +

<%= headline -%>

+ +
+ <%= render :partial => 'form_messages' %> +
+ + <%= render :partial => body_partial, :locals => { :columns => columns, :form_action => form_action, :scope => scope } %> + + + + +<%= javascript_tag("ActiveScaffold.focus_first_element_of_form('#{element_form_id(:action => form_action)}');") %> diff --git a/app/views/active_scaffold_overrides/_create_form.html.erb b/app/views/active_scaffold_overrides/_create_form.html.erb new file mode 100644 index 0000000000..9bfaf10615 --- /dev/null +++ b/app/views/active_scaffold_overrides/_create_form.html.erb @@ -0,0 +1,8 @@ +<% form_action ||= :create + cancel_link = true if cancel_link.nil? %> +<%= render :partial => "base_form", :locals => {:xhr => xhr ||= nil, + :form_action => form_action, + :method => method ||= :post, + :cancel_link => cancel_link, + :headline => headline ||= active_scaffold_config.send(form_action).label(active_scaffold_config.add_sti_create_links? ? @record.class.model_name.human(:count => 1) : nil)} %> + diff --git a/app/views/active_scaffold_overrides/_create_form_on_list.html.erb b/app/views/active_scaffold_overrides/_create_form_on_list.html.erb new file mode 100644 index 0000000000..7a735f0841 --- /dev/null +++ b/app/views/active_scaffold_overrides/_create_form_on_list.html.erb @@ -0,0 +1,6 @@ +<% cancel_link = false if cancel_link.nil? %> +<%= render :partial => "base_form", :locals => {:xhr => xhr ||= nil, + :form_action => form_action ||= :create, + :method => method ||= :post, + :cancel_link => cancel_link, + :headline => headline ||= active_scaffold_config.create.label(active_scaffold_config.add_sti_create_links? ? @record.class.model_name.human(:count => 1) : nil)} %> \ No newline at end of file diff --git a/app/views/active_scaffold_overrides/_field_search.html.erb b/app/views/active_scaffold_overrides/_field_search.html.erb new file mode 100644 index 0000000000..9be68c6c6a --- /dev/null +++ b/app/views/active_scaffold_overrides/_field_search.html.erb @@ -0,0 +1,32 @@ +<% url_options = params_for(:action => :index, :search => nil) -%> +<%= +options = {:id => element_form_id(:action => 'search'), + :class => "as_form search", + :remote => true, + :method => :get, + 'data-loading' => true} +form_tag url_options, options %> +
    + <% visibles, hiddens = visibles_and_hiddens(active_scaffold_config.field_search) %> + <% visibles.each do |column| -%> +
  1. <%= search_attribute(column, @record) %>
  2. + <% end -%> + <% unless hiddens.empty? -%> +
  3. +
    <%= as_(:optional_attributes) %>
    + + <%= link_to_visibility_toggle(sub_section_id(:sub_section => 'further_options'), {:default_visible => false}) %> +
  4. + <% end -%> +
+ + +<%= javascript_tag("ActiveScaffold.focus_first_element_of_form('#{element_form_id(:action => 'search')}');") %> diff --git a/app/views/active_scaffold_overrides/_form.html.erb b/app/views/active_scaffold_overrides/_form.html.erb new file mode 100644 index 0000000000..1db47f4d09 --- /dev/null +++ b/app/views/active_scaffold_overrides/_form.html.erb @@ -0,0 +1,28 @@ +<% + scope ||= nil + subsection_id ||= nil + show_unauthorized_columns = active_scaffold_config.send(form_action).show_unauthorized_columns if active_scaffold_config.actions.include? form_action +%> +
    <%= "style=\"display: none;\"".html_safe if columns.collapsed %>> + <% columns.each :for => @record, :crud_type => (:read if show_unauthorized_columns) do |column| %> + <% column_css_class = column.css_class unless column.css_class.nil? || column.css_class.is_a?(Proc) %> + <% renders_as = column_renders_as(column) %> + <% authorized = show_unauthorized_columns ? @record.authorized_for?(:crud_type => form_action, :column => column.name) : true unless renders_as == :subsection %> + <% if renders_as == :subsection -%> + <% subsection_id = sub_section_id(:sub_section => column.label) %> +
  1. +
    <%= column.label %>
    + <%= render :partial => 'form', :locals => { :columns => column, :subsection_id => subsection_id, :form_action => form_action, :scope => scope } %> + <%= link_to_visibility_toggle(subsection_id, {:default_visible => !column.collapsed}) -%> +
  2. + <% elsif renders_as == :subform and authorized -%> +
  3. + <%= render_column(column, @record, renders_as, scope) %> +
  4. + <% else -%> +
  5. + <%= render_column(column, @record, renders_as, scope, !authorized) %> +
  6. + <% end -%> + <% end -%> +
diff --git a/app/views/active_scaffold_overrides/_form_association.html.erb b/app/views/active_scaffold_overrides/_form_association.html.erb new file mode 100644 index 0000000000..da9e4d1221 --- /dev/null +++ b/app/views/active_scaffold_overrides/_form_association.html.erb @@ -0,0 +1,23 @@ +<% +parent_record ||= @record # save @record, some partial can change @record TODO remove when changing @record is removed +associated = column.singular_association? ? [parent_record.send(column.name)].compact : parent_record.send(column.name).to_a +associated = associated.sort_by {|r| r.new_record? ? 99999999999 : r.id} unless column.association.options.has_key?(:order) +if column.show_blank_record?(associated) + show_blank_record = build_associated(column, parent_record) +end +disable_required_for_new = @disable_required_for_new +@disable_required_for_new = !!show_blank_record unless (column.singular_association? && column.required?) +subform_div_id = "#{sub_form_id(:association => column.name, :id => parent_record.id || generated_id(parent_record) || 99999999999)}-div" +-%> +
<%= column.label -%>
+
> +<%# HACK to be able to delete all associated records %> + <%= hidden_field_tag "#{active_scaffold_input_options(column, scope)[:name]}[0]", '' if column.plural_association? %> + <%= render :partial => subform_partial_for_column(column), :locals => {:column => column, :parent_record => parent_record, :associated => associated, :show_blank_record => show_blank_record, :scope => scope} %> + <%= render :partial => 'form_association_footer', :locals => {:parent_record => parent_record, :column => column, :associated => associated, :scope => scope} -%> +
+<%= link_to_visibility_toggle(subform_div_id, {:default_visible => !column.collapsed}) -%> +<% + @record = parent_record # restore @record, some partials can change it TODO remove when changing @record is removed + @disable_required_for_new = disable_required_for_new +-%> diff --git a/app/views/active_scaffold_overrides/_form_association_footer.html.erb b/app/views/active_scaffold_overrides/_form_association_footer.html.erb new file mode 100644 index 0000000000..0df7b32538 --- /dev/null +++ b/app/views/active_scaffold_overrides/_form_association_footer.html.erb @@ -0,0 +1,50 @@ +<% +begin + remote_controller = active_scaffold_controller_for(column.association.klass) +rescue ActiveScaffold::ControllerNotFound + remote_controller = nil +end +@record = parent_record + +show_add_existing = column_show_add_existing(column) +show_add_new = column_show_add_new(column, associated, @record) + +return unless show_add_new or show_add_existing + +temporary_id = generated_id(parent_record) if parent_record.new_record? +controller_path = active_scaffold_controller_for(parent_record.class).controller_path +edit_associated_url = params_for(:controller => controller_path, :action => 'edit_associated', :child_association => column.name, :associated_id => '--ID--', :scope => scope, :id => parent_record.id, :generated_id => temporary_id, :parent_controller => controller.controller_path) if show_add_existing +add_new_url = params_for(:controller => controller_path, :action => 'edit_associated', :child_association => column.name, :scope => scope, :id => parent_record.id, :generated_id => temporary_id, :parent_controller => controller.controller_path) if show_add_new + +-%> + diff --git a/app/views/active_scaffold_overrides/_form_association_record.html.erb b/app/views/active_scaffold_overrides/_form_association_record.html.erb new file mode 100644 index 0000000000..7de22b9706 --- /dev/null +++ b/app/views/active_scaffold_overrides/_form_association_record.html.erb @@ -0,0 +1,88 @@ +<% + record_column = column + Rails.logger.warn "Relying on @record to render form_asssociation_record partial with no :object is deprecated, called from #{caller(1).detect {|l| l =~ /\.erb:/ }.gsub(/(.*:\d+):.*/, '\1')}" unless local_assigns[:form_association_record] + record = form_association_record ||= @record # TODO remove me, backwards compatibility, no :collection neither object in render + @record = record # TODO remove me, backward compatibility, helpers using @record + readonly = (record.readonly? or not record.authorized_for?(:crud_type => :update)) + crud_type = record.new_record? ? :create : (readonly ? :read : :update) + show_actions = false + locked ||= false + config = active_scaffold_config_for(record.class) + scope = column_scope(record_column, scope, record) + options = active_scaffold_input_options(config.columns[record.class.primary_key], scope) + tr_id = "association-#{options[:id]}" + + if config.subform.layout == :vertical + record_tag ||= :div + row_tag ||= :ol + column_tag ||= :li + error_tag ||= :div + error_inner_tag ||= nil + default_col_class = ['form-element'] + flatten = true unless local_assigns.has_key? :flatten + else + record_tag ||= :tbody + row_tag ||= :tr + column_tag ||= :td + error_tag ||= :tr + error_inner_tag ||= :td + default_col_class = [] + flatten ||= false + end + index ||= form_association_record_counter ||= nil + + columns_length = 0 + columns_groups = [] +-%> +<<%= record_tag %> class="sub-form-record"> +<% unless record.errors.empty? -%> +<%= content_tag error_tag, :class => "association-record-errors", :id => element_messages_id(:action => record.class.name.underscore, :id => "#{parent_record.id}-#{index}") do %> + <% errors = active_scaffold_error_messages_for(:record, :object_name => record.class.model_name.human.downcase) %> + <%= error_inner_tag ? content_tag(error_inner_tag, errors, :colspan => (active_scaffold_config_for(record.class).subform.columns.length + 1 if error_inner_tag == :td)) : errors %> +<% end %> +<% end %> +<%= content_tag row_tag, :id => tr_id, :class => "association-record#{' association-record-new' if record.new_record?}#{' locked' if locked}" do %> +<% config.subform.columns.each :for => record.class, :crud_type => :read, :flatten => flatten do |column| %> +<% + if column.is_a? ActiveScaffold::DataStructures::ActionColumns + columns_groups << column + next + end + + next unless in_subform?(column, parent_record) + columns_length += 1 + show_actions = true + column = column.clone + column.form_ui ||= :select if column.association + + col_class = default_col_class.clone + col_class << 'required' if column.required? + col_class << column.css_class unless column.css_class.nil? || column.css_class.is_a?(Proc) + col_class << 'hidden' if column_renders_as(column) == :hidden +-%> + <%= content_tag column_tag, :class => col_class.join(' ') do %> + <%= active_scaffold_render_subform_column(column, scope, crud_type, readonly, false, record) %> + <% end %> +<% end -%> +<% if show_actions -%> + <%= content_tag column_tag, :class => "actions" do %> + <% if record_column.plural_association? and (record.authorized_for?(:crud_type => :delete) or not [:destroy, :delete_all].include?(record_column.association.options[:dependent])) %> + <%= link_to as_(:remove), '#', :class => 'destroy', :id => "#{options[:id]}-destroy" , :data => {:delete_id => tr_id} unless locked %> + <% end %> + <% unless record.new_record? %> + + <% end -%> + <% end %> +<% end -%> +<% end %> + +<% columns_groups.each do |column| %> +<%= content_tag row_tag, :class => 'associated-record' do %> + <%= content_tag column_tag, :colspan => (columns_length if column_tag == :td) do %> + <% column.each :for => @record.class, :crud_type => :read, :flatten => true do |col| %> + <%= active_scaffold_render_subform_column(col, scope, crud_type, readonly, true, record) %> + <% end %> + <% end %> +<% end %> +<% end %> +> diff --git a/app/views/active_scaffold_overrides/_form_messages.html.erb b/app/views/active_scaffold_overrides/_form_messages.html.erb new file mode 100644 index 0000000000..d0c07ab620 --- /dev/null +++ b/app/views/active_scaffold_overrides/_form_messages.html.erb @@ -0,0 +1,5 @@ +<%= render :partial => 'messages' unless request.xhr? %> + +<% unless @record.nil? %> + <%= active_scaffold_error_messages_for :record, :object_name => @record.class.model_name.human.downcase %> +<% end %> diff --git a/app/views/active_scaffold_overrides/_horizontal_subform.html.erb b/app/views/active_scaffold_overrides/_horizontal_subform.html.erb new file mode 100644 index 0000000000..0b3b514b4c --- /dev/null +++ b/app/views/active_scaffold_overrides/_horizontal_subform.html.erb @@ -0,0 +1,12 @@ + +<% + header_record_class = (show_blank_record && show_blank_record.class) || column.association.klass +-%> + <%= render :partial => 'horizontal_subform_header', :locals => {:parent_record => parent_record, :record_class => header_record_class} %> + + <%= render :partial => 'form_association_record', :collection => associated, :locals => {:scope => scope, :parent_record => parent_record, :column => column} %> + <%= render :partial => 'form_association_record', :object => show_blank_record, :locals => {:scope => scope, :parent_record => parent_record, :column => column, :locked => true, :index => associated.size} if show_blank_record %> + + <%= render :partial => 'horizontal_subform_footer', :locals => {:scope => scope, :parent_record => parent_record, :column => column} %> + +
diff --git a/app/views/active_scaffold_overrides/_horizontal_subform_footer.html.erb b/app/views/active_scaffold_overrides/_horizontal_subform_footer.html.erb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/active_scaffold_overrides/_horizontal_subform_header.html.erb b/app/views/active_scaffold_overrides/_horizontal_subform_header.html.erb new file mode 100644 index 0000000000..f26f675069 --- /dev/null +++ b/app/views/active_scaffold_overrides/_horizontal_subform_header.html.erb @@ -0,0 +1,17 @@ + + + <% + active_scaffold_config_for(record_class).subform.columns.each :for => record_class, :crud_type => :read do |column| + next if column.is_a? ActiveScaffold::DataStructures::ActionColumns + next unless in_subform?(column, parent_record) + hidden = column_renders_as(column) == :hidden + -%> + "> + + <% if column.description.present? -%> + <%= column.description %> + <% end -%> + + <% end -%> + + diff --git a/app/views/active_scaffold_overrides/_human_conditions.html.erb b/app/views/active_scaffold_overrides/_human_conditions.html.erb new file mode 100644 index 0000000000..4bb8ae63db --- /dev/null +++ b/app/views/active_scaffold_overrides/_human_conditions.html.erb @@ -0,0 +1 @@ +<%= columns.collect {|column| active_scaffold_human_condition_for(column)}.compact.to_sentence %> \ No newline at end of file diff --git a/app/views/active_scaffold_overrides/_list.html.erb b/app/views/active_scaffold_overrides/_list.html.erb new file mode 100644 index 0000000000..1fda6b6e08 --- /dev/null +++ b/app/views/active_scaffold_overrides/_list.html.erb @@ -0,0 +1,35 @@ +<% if active_scaffold_config.list.messages_above_header %> + + + + + + +
+ +
+ <%= render :partial => 'messages' %> +
+
+<% end %> + + + + <% columns = list_columns %> + <%= render :partial => 'list_column_headings', :locals => {:columns => columns} %> + + + <%= render :partial => 'list_messages', :locals => {:columns => columns} %> + + <% if !@page.empty? -%> + <%= render :partial => 'list_record', :collection => @page.items, :locals => {:hidden => false, :columns => columns, :action_links => active_scaffold_config.action_links.member, :data_refresh => url_for(params_for(:action => :row, :id => '--ID--', :_method => :get))} %> + <% end -%> + <% if columns.any? {|c| c.calculation?} -%> + <%= render :partial => 'list_calculations', :locals => {:columns => columns} %> + <% end -%> + +
+<%= render :partial => 'list_pagination' %> diff --git a/app/views/active_scaffold_overrides/_list_calculations.html.erb b/app/views/active_scaffold_overrides/_list_calculations.html.erb new file mode 100644 index 0000000000..568e78b3c1 --- /dev/null +++ b/app/views/active_scaffold_overrides/_list_calculations.html.erb @@ -0,0 +1,14 @@ +<% display_class = ( @records.kind_of?(Array) ? @records.first : @records ) + columns ||= list_columns -%> + + <% columns.each do |column| -%> + <% if column.calculation? %> + <%= render_column_calculation(column) %> + <% else %> +   + <% end -%> + <% end -%> + <% unless active_scaffold_config.action_links.empty? -%> +   + <% end -%> + diff --git a/app/views/active_scaffold_overrides/_list_column_headings.html.erb b/app/views/active_scaffold_overrides/_list_column_headings.html.erb new file mode 100644 index 0000000000..e2ad42c2d5 --- /dev/null +++ b/app/views/active_scaffold_overrides/_list_column_headings.html.erb @@ -0,0 +1,12 @@ +<% +sorting = active_scaffold_config.list.user.sorting +sorting_stages = ['reset', 'ASC', 'DESC'] +default_sorting = active_scaffold_config.list.user.default_sorting +default_sorting_stages = ['ASC', 'DESC'] +-%> +<% columns.each do |column| -%> + <% stages = default_sorting.sorts_on?(column) ? default_sorting_stages : sorting_stages -%> + <%= render_column_heading(column, sorting, stages.after(sorting.direction_of(column)) || 'ASC') %> +<% end -%> + + diff --git a/app/views/active_scaffold_overrides/_list_header.html.erb b/app/views/active_scaffold_overrides/_list_header.html.erb new file mode 100644 index 0000000000..a9a0bf614c --- /dev/null +++ b/app/views/active_scaffold_overrides/_list_header.html.erb @@ -0,0 +1,8 @@ +<% action_links = active_scaffold_config.action_links.collection + unless action_links.empty? -%> +
+ <%= display_action_links(action_links, nil, :skip_unauthorized => true, :reverse => nested?) %> + <%= loading_indicator_tag(:action => :table) %> +
+<% end %> +

<%= active_scaffold_config.list.user.label %>

diff --git a/app/views/active_scaffold_overrides/_list_inline_adapter.html.erb b/app/views/active_scaffold_overrides/_list_inline_adapter.html.erb new file mode 100644 index 0000000000..13d6e46a88 --- /dev/null +++ b/app/views/active_scaffold_overrides/_list_inline_adapter.html.erb @@ -0,0 +1,12 @@ +<%# nested_id, allows us to remove a nested scaffold programmatically %> + + +<% if controller.send(:successful?) %> +
<%= "#{nested? ? nested.name : id_from_controller(params[:controller])}-view" %> view"> + <%= link_to(as_(:close), '', :class => 'inline-adapter-close as_cancel', :remote => true, :title => as_(:close)) -%> + <%= payload -%> +
+<% end %> + + +<%= javascript_tag("setTimeout(function() { var action_link = ActiveScaffold.ActionLink.get('#{element_row_id(:action => :nested)}'); if (action_link) { action_link.update_flash_messages('#{escape_javascript(render(:partial => 'messages').strip)}');#{' action_link.close(); ActiveScaffold.scroll_to(action_link.scaffold(), ActiveScaffold.config.scroll_on_close == "checkInViewport");' unless controller.send(:successful?)} } }, 10);") %> diff --git a/app/views/active_scaffold_overrides/_list_messages.html.erb b/app/views/active_scaffold_overrides/_list_messages.html.erb new file mode 100644 index 0000000000..46a411ddb0 --- /dev/null +++ b/app/views/active_scaffold_overrides/_list_messages.html.erb @@ -0,0 +1,28 @@ +<% column_count ||= columns.length + 1 -%> + + + +<% unless active_scaffold_config.list.messages_above_header %> + +
+ <%= render :partial => 'messages' %> +
+<% end %> +
> + <%= @filtered.is_a?(Array) ? render(:partial => 'human_conditions', :locals => {:columns => @filtered}) : as_(active_scaffold_config.list.filtered_message) %> + <% if active_scaffold_config.list.show_search_reset && @filtered -%> +
+ <%= loading_indicator_tag(:action => :record, :id => nil) %> + <%= render_action_link(active_scaffold_config.list.reset_link) %> +
+ <% end -%> +
+

> + <%= as_(active_scaffold_config.list.no_entries_message) %> +

+ + + diff --git a/app/views/active_scaffold_overrides/_list_pagination.html.erb b/app/views/active_scaffold_overrides/_list_pagination.html.erb new file mode 100644 index 0000000000..3f5ce64ac8 --- /dev/null +++ b/app/views/active_scaffold_overrides/_list_pagination.html.erb @@ -0,0 +1,11 @@ +<% if active_scaffold_config.list.pagination -%> + +<% end -%> diff --git a/app/views/active_scaffold_overrides/_list_pagination_links.html.erb b/app/views/active_scaffold_overrides/_list_pagination_links.html.erb new file mode 100644 index 0000000000..d936e97399 --- /dev/null +++ b/app/views/active_scaffold_overrides/_list_pagination_links.html.erb @@ -0,0 +1,9 @@ +<% unless current_page.nil? -%> + <% url_options = params_for(:action => :index) -%> + <% options = {'data-page-history' => controller_id, :remote => true, :method => :get} -%> + + <%= loading_indicator_tag :action => :pagination %> + <%= link_to as_(:previous), url_options.merge(:page => current_page.number - 1), options.merge(:class => "as_paginate previous") if current_page.prev? %> + <%= pagination_ajax_links current_page, url_options, options, active_scaffold_config.list.page_links_inner_window, active_scaffold_config.list.page_links_outer_window %> + <%= link_to as_(:next), url_options.merge(:page => current_page.number + 1), options.merge(:class => "as_paginate next") if current_page.next? %> +<% end -%> diff --git a/app/views/active_scaffold_overrides/_list_record.html.erb b/app/views/active_scaffold_overrides/_list_record.html.erb new file mode 100644 index 0000000000..4adde5cc16 --- /dev/null +++ b/app/views/active_scaffold_overrides/_list_record.html.erb @@ -0,0 +1,29 @@ +<% +record = list_record if list_record # compat with render :partial :collection +columns ||= list_columns +tr_class = cycle("", "even-record") + ' ' + list_row_class(record) +action_links ||= active_scaffold_config.action_links.member +data_refresh ||= url_for(params_for(:action => :row, :id => '--ID--', :_method => :get)) +-%> + + <% columns.each do |column| %> + <% authorized = record.authorized_for?(:crud_type => :read, :column => column.name) -%> + <% column_value = authorized ? get_column_value(record, column) : active_scaffold_config.list.empty_field_text -%> + + <%= content_tag :td, column_attributes(column, record).merge(:class => column_class(column, column_value, record)) do %> + <%= authorized ? render_list_column(column_value, column, record) : column_value %> + <% end %> + <% end -%> + + + + + <%= display_action_links(action_links, record, :level_0_tag => :td, :for => record.persisted? ? record : record.class) %> + +
+ <%= loading_indicator_tag(:action => :record, :id => record.id) %> +
+ + + <%= render_nested_view(action_links, record) unless @nested_auto_open.nil? %> + diff --git a/app/views/active_scaffold_overrides/_list_with_header.html.erb b/app/views/active_scaffold_overrides/_list_with_header.html.erb new file mode 100644 index 0000000000..395e7988af --- /dev/null +++ b/app/views/active_scaffold_overrides/_list_with_header.html.erb @@ -0,0 +1,36 @@ +
> +
+ <%= render :partial => 'list_header' %> +
+ + + <% if active_scaffold_config.list.always_show_search %> + <% old_record, @record = @record, new_model %> + + + + <% @record = old_record %> + <% else %> + + <% end %> + <% if active_scaffold_config.list.always_show_create %> + <% old_record, @record = @record, new_model %> + + + + <% @record = old_record %> + <% end %> + +
+
view"> + <%= render :partial => active_scaffold_config.list.search_partial %> +
+
+
view"> + <%= render :partial => 'create_form_on_list' %> +
+
+
+ <%= render :partial => 'list' %> +
+
diff --git a/app/views/active_scaffold_overrides/_messages.html.erb b/app/views/active_scaffold_overrides/_messages.html.erb new file mode 100644 index 0000000000..aa2a197ada --- /dev/null +++ b/app/views/active_scaffold_overrides/_messages.html.erb @@ -0,0 +1,10 @@ +<% for name in [:info, :warning, :error] %> + <% if flash[name] %> +
"> + <%= display_message flash[name] %> + <% if request.xhr? %> + <%= as_(:close) %> + <% end %> +
+ <% end %> +<% end %> diff --git a/app/views/active_scaffold_overrides/_refresh_list.js.erb b/app/views/active_scaffold_overrides/_refresh_list.js.erb new file mode 100644 index 0000000000..8a20d894f6 --- /dev/null +++ b/app/views/active_scaffold_overrides/_refresh_list.js.erb @@ -0,0 +1,5 @@ +<% if active_scaffold_config.list.refresh_with_header -%> +ActiveScaffold.replace('<%= active_scaffold_id %>', '<%= escape_javascript(render('list_with_header')) %>'); +<% else -%> +ActiveScaffold.replace_html('<%= active_scaffold_content_id %>', '<%= escape_javascript(render('list')) %>'); +<% end -%> diff --git a/app/views/active_scaffold_overrides/_render_field.js.erb b/app/views/active_scaffold_overrides/_render_field.js.erb new file mode 100644 index 0000000000..536f33e072 --- /dev/null +++ b/app/views/active_scaffold_overrides/_render_field.js.erb @@ -0,0 +1,29 @@ +<% + column = if render_field.is_a? ActiveScaffold::DataStructures::Column + render_field + else + active_scaffold_config.columns[render_field.to_sym] + end + return unless @main_columns.include? column.name + @rendered ||= Set.new + return if @rendered.include? column.name + @rendered << column.name + renders_as = column_renders_as(column) + if renders_as == :subform + options = {:is_subform => true, :field_class => "#{column.name}-sub-form"} + else + options = {:is_subform => false, :field_class => "#{column.name}-input"} + end + html = if scope + readonly = (@record.readonly? or not @record.authorized_for?(:crud_type => :update)) + crud_type = @record.new_record? ? :create : (readonly ? :read : :update) + active_scaffold_render_subform_column(column, scope, crud_type, readonly, !active_scaffold_config.subform.columns.names_without_auth_check.include?(column.name), @record) + else + render_column(column, @record, renders_as, scope) + end +-%> + +ActiveScaffold.render_form_field('<%= source_id %>','<%= escape_javascript(html) %>', <%= options.to_json.html_safe %>); +<%if column.update_columns && !column.update_columns.empty?%> + <%= render(:partial => "render_field", :collection => column.update_columns, :locals => {:source_id => source_id, :scope => scope})%> +<%end%> diff --git a/app/views/active_scaffold_overrides/_row.html.erb b/app/views/active_scaffold_overrides/_row.html.erb new file mode 100644 index 0000000000..79475acb57 --- /dev/null +++ b/app/views/active_scaffold_overrides/_row.html.erb @@ -0,0 +1 @@ +<%= render :partial => 'list_record', :locals => {:record => record} %> diff --git a/app/views/active_scaffold_overrides/_search.html.erb b/app/views/active_scaffold_overrides/_search.html.erb new file mode 100644 index 0000000000..67fae54167 --- /dev/null +++ b/app/views/active_scaffold_overrides/_search.html.erb @@ -0,0 +1,34 @@ +<% live_search = active_scaffold_config.search.live? -%> +<% url_options = params_for(:action => :index).delete_if{|k,v| k == 'search'} -%> +<%= +options = {:id => element_form_id(:action => 'search'), + :class => "as_form search", + :remote => true, + :method => :get} +options['data-loading'] = true unless live_search + form_tag url_options, options %> + <%= text_field_tag :search, search_params, :class => 'text-input', :id => search_input_id, :size => 50, :autocomplete => :off %> + <%= submit_tag as_(:search), :class => "submit" %> + <%= link_to as_(:reset), url_for(url_options.merge(:search => '')), :class => 'as_cancel', :remote => true, :data => {:refresh => true} %> + <%= loading_indicator_tag(:action => :search) %> + + + diff --git a/app/views/active_scaffold_overrides/_show.html.erb b/app/views/active_scaffold_overrides/_show.html.erb new file mode 100644 index 0000000000..91e8dc6e25 --- /dev/null +++ b/app/views/active_scaffold_overrides/_show.html.erb @@ -0,0 +1,8 @@ +

<%= active_scaffold_config.show.label(@record.to_label.nil? ? nil : @record.to_label) %>

+ +<%= render :partial => 'show_columns', :locals => {:columns => active_scaffold_config.show.columns} -%> + + diff --git a/app/views/active_scaffold_overrides/_show_columns.html.erb b/app/views/active_scaffold_overrides/_show_columns.html.erb new file mode 100644 index 0000000000..2577ffcedf --- /dev/null +++ b/app/views/active_scaffold_overrides/_show_columns.html.erb @@ -0,0 +1,15 @@ +
+<% columns.each :for => @record do |column| %> +
<%= column.label -%>
+<% if column.is_a? ActiveScaffold::DataStructures::ActionColumns -%> +
+ <%= render :partial => 'show_columns', :locals => {:columns => column} %> +<% else -%> + <% css_class = "#{column.name}-view" + css_class.concat(" #{column.css_class}") unless column.css_class.nil? %> +
+ <%= show_column_value(@record, column) -%>   +<% end -%> +
+<% end -%> +
diff --git a/app/views/active_scaffold_overrides/_update_actions.html.erb b/app/views/active_scaffold_overrides/_update_actions.html.erb new file mode 100644 index 0000000000..def042480c --- /dev/null +++ b/app/views/active_scaffold_overrides/_update_actions.html.erb @@ -0,0 +1,9 @@ +
+
+ <% active_scaffold_config.action_links.member.each do |link| -%> + <% next unless link.action == 'index' -%> + <% next if skip_action_link?(link, record) || !action_link_authorized?(link, record) -%> + <%= render_action_link(link, record, :authorized => true) -%> + <% end -%> +
+
diff --git a/app/views/active_scaffold_overrides/_update_calculations.js.erb b/app/views/active_scaffold_overrides/_update_calculations.js.erb new file mode 100644 index 0000000000..d01ac2dfd4 --- /dev/null +++ b/app/views/active_scaffold_overrides/_update_calculations.js.erb @@ -0,0 +1,5 @@ +<% calculations_id ||= active_scaffold_calculations_id -%> +<% if active_scaffold_config.list.columns.any? {|c| c.calculation?} %> +<% params.delete(:id) %> + ActiveScaffold.replace('<%= calculations_id %>', '<%= escape_javascript(render(:partial => 'list_calculations')) %>'); +<% end %> diff --git a/app/views/active_scaffold_overrides/_update_column.js.erb b/app/views/active_scaffold_overrides/_update_column.js.erb new file mode 100644 index 0000000000..c4cd3ff4c0 --- /dev/null +++ b/app/views/active_scaffold_overrides/_update_column.js.erb @@ -0,0 +1,16 @@ +<% + column = if update_column.is_a? ActiveScaffold::DataStructures::Column + update_column + else + active_scaffold_config.columns[update_column.to_sym] + end + @rendered ||= Set.new + return if @rendered.include? column.name + @rendered << column.name +-%> +<% if @record.authorized_for?(:crud_type => :read, :column => column.name) -%> + ActiveScaffold.replace_html('<%= row_id %> .<%= column.name %>-column','<%= escape_javascript(render_list_column(get_column_value(@record, column), column, @record)) %>'); +<% end -%> +<% if column.update_columns && !column.update_columns.empty? %> + <%= render(:partial => 'update_column', :collection => column.update_columns & active_scaffold_config.list.columns.names, :locals => {:row_id => row_id})%> +<% end %> diff --git a/app/views/active_scaffold_overrides/_update_form.html.erb b/app/views/active_scaffold_overrides/_update_form.html.erb new file mode 100644 index 0000000000..9ec9c320f1 --- /dev/null +++ b/app/views/active_scaffold_overrides/_update_form.html.erb @@ -0,0 +1,6 @@ +<% cancel_link = true if cancel_link.nil? %> +<%= render :partial => "base_form", :locals => {:xhr => xhr ||= nil, + :form_action => form_action ||= :update, + :method => method ||= :put, + :cancel_link => cancel_link, + :headline => headline ||= @record.to_label.nil? ? active_scaffold_config.update.label : as_(:update_model, :model => @record.to_label)} %> diff --git a/app/views/active_scaffold_overrides/_update_messages.js.erb b/app/views/active_scaffold_overrides/_update_messages.js.erb new file mode 100644 index 0000000000..3800667202 --- /dev/null +++ b/app/views/active_scaffold_overrides/_update_messages.js.erb @@ -0,0 +1,2 @@ +<% messages_id ||= active_scaffold_messages_id -%> +ActiveScaffold.replace_html('<%= messages_id %>', '<%= escape_javascript(render(:partial => 'messages')) %>'); diff --git a/app/views/active_scaffold_overrides/_vertical_subform.html.erb b/app/views/active_scaffold_overrides/_vertical_subform.html.erb new file mode 100644 index 0000000000..023fb86ffc --- /dev/null +++ b/app/views/active_scaffold_overrides/_vertical_subform.html.erb @@ -0,0 +1,4 @@ +
+ <%= render :partial => 'form_association_record', :collection => associated, :locals => {:scope => scope, :parent_record => parent_record, :column => column} %> + <%= render :partial => 'form_association_record', :object => show_blank_record, :locals => {:scope => scope, :parent_record => parent_record, :column => column, :locked => true, :index => associated.size} if show_blank_record %> +
diff --git a/app/views/active_scaffold_overrides/action_confirmation.html.erb b/app/views/active_scaffold_overrides/action_confirmation.html.erb new file mode 100644 index 0000000000..dedc2baf53 --- /dev/null +++ b/app/views/active_scaffold_overrides/action_confirmation.html.erb @@ -0,0 +1,13 @@ +
+
+ <%= form_tag params_for(:action => link.action, :id => params[:id]), { :method => link.method } %> +

<%= link.confirm(record.try(:to_label)) -%>

+ + + + +
+
\ No newline at end of file diff --git a/app/views/active_scaffold_overrides/add_existing.js.erb b/app/views/active_scaffold_overrides/add_existing.js.erb new file mode 100644 index 0000000000..ea16f7e505 --- /dev/null +++ b/app/views/active_scaffold_overrides/add_existing.js.erb @@ -0,0 +1,14 @@ +<% new_row = render :partial => 'list_record', :locals => {:record => @record} %> +ActiveScaffold.create_record_row('<%= active_scaffold_id %>', '<%= escape_javascript(new_row) %>', <%= {:insert_at => :top}.to_json.html_safe %>); + +<%= render :partial => 'update_calculations', :formats => [:js] %> + +<% if form_stays_open ||= true %> + <%# why not just re-render the form? that wouldn't utilize a possible do_new override which sets default values.%> + ActiveScaffold.reset_form('<%= element_form_id %>'); + ActiveScaffold.replace_html('<%= element_messages_id(:action => :add_existing) %>', '<%= escape_javascript(render(:partial => 'form_messages')) %>'); + <%# have to delay the focus, because there's no "firstElement" in prototype until at least one element is not disabled%> + ActiveScaffold.focus_first_element_of_form<%= '.defer' if ActiveScaffold.js_framework == :prototype %>('<%= element_form_id %>'); +<% else %> + ActiveScaffold.find_action_link('<%= element_form_id(:action => :new_existing) %>').close(); +<% end %> diff --git a/frontends/default/views/add_existing_form.html.erb b/app/views/active_scaffold_overrides/add_existing_form.html.erb similarity index 51% rename from frontends/default/views/add_existing_form.html.erb rename to app/views/active_scaffold_overrides/add_existing_form.html.erb index 49570161a9..f75446c2d1 100644 --- a/frontends/default/views/add_existing_form.html.erb +++ b/app/views/active_scaffold_overrides/add_existing_form.html.erb @@ -1,5 +1,5 @@
-
view"> +
view"> <%= render :partial => 'add_existing_form' -%>
\ No newline at end of file diff --git a/frontends/default/views/create.html.erb b/app/views/active_scaffold_overrides/create.html.erb similarity index 50% rename from frontends/default/views/create.html.erb rename to app/views/active_scaffold_overrides/create.html.erb index 4647991dcd..1bcb00c78b 100644 --- a/frontends/default/views/create.html.erb +++ b/app/views/active_scaffold_overrides/create.html.erb @@ -1,5 +1,5 @@
-
view"> +
view"> <%= render :partial => 'create_form' -%>
\ No newline at end of file diff --git a/frontends/default/views/delete.html.erb b/app/views/active_scaffold_overrides/delete.html.erb similarity index 63% rename from frontends/default/views/delete.html.erb rename to app/views/active_scaffold_overrides/delete.html.erb index bf85bd7f8a..426c02e9e8 100644 --- a/frontends/default/views/delete.html.erb +++ b/app/views/active_scaffold_overrides/delete.html.erb @@ -1,11 +1,11 @@
<%= form_tag params_for(:action => :destroy, :id => params[:id]), { :method => :delete } %> -

<%= as_(:are_you_sure) -%>

+

<%= as_(:are_you_sure_to_delete, :label => @record.try(:to_label)) -%>

diff --git a/app/views/active_scaffold_overrides/destroy.js.erb b/app/views/active_scaffold_overrides/destroy.js.erb new file mode 100644 index 0000000000..f0c5d0c20c --- /dev/null +++ b/app/views/active_scaffold_overrides/destroy.js.erb @@ -0,0 +1,26 @@ +<% messages_id = active_scaffold_messages_id %> +<% if controller.send(:successful?) %> + <% if render_parent? %> + <% if render_parent_action == :row %> + <%# TODO: That s not working with delete.... %> + <% current_id = controller_id(params[:eid] || params[:parent_sti]) -%> + ActiveScaffold.delete_record_row('<%= element_row_id(:controller_id => current_id, :action => 'list', :id => params[:id]) %>', '<%= url_for(params_for(:action => :index, :id => nil, :page => [active_scaffold_config.list.user.page.to_i - 1, 1].max)) %>'); + <% messages_id = active_scaffold_messages_id(:controller_id => current_id) %> + <%= render :partial => 'update_calculations', :locals => {:calculations_id => active_scaffold_calculations_id(:controller_id => current_id)}, :formats => [:js] %> + <% elsif render_parent_action == :index %> + <% if controller.respond_to?(:render_component_into_view, true) %> + <%= escape_javascript(controller.send(:render_component_into_view, render_parent_options)) %> + <% else %> + ActiveScaffold.reload('<%= url_for render_parent_options %>'); + <% end %> + <% end %> + <% elsif (active_scaffold_config.delete.refresh_list) %> + <%= render :partial => 'refresh_list' %> + <% else %> + ActiveScaffold.delete_record_row('<%= element_row_id(:action => 'list', :id => params[:id]) %>', '<%= url_for(params_for(:action => :index, :id => nil, :page => [active_scaffold_config.list.user.page.to_i - 1, 1].max)) %>'); + <%= render :partial => 'update_calculations', :formats => [:js] %> + <% end %> +<% else %> + <% flash[:error] = active_scaffold_error_messages_for(@record, :object_name => "#{@record.class.model_name.human.downcase}#{@record.new_record? ? '' : ": #{@record.to_label}"}", :header_message => '', :message => "#{@record.class.model_name.human.downcase}#{@record.new_record? ? '' : ": #{@record.to_label}"}", :container_tag => nil, :list_type => :br) if @record.errors.present? %> +<% end %> +<%= render :partial => 'update_messages', :locals => {:messages_id => messages_id} %> diff --git a/app/views/active_scaffold_overrides/edit_associated.js.erb b/app/views/active_scaffold_overrides/edit_associated.js.erb new file mode 100644 index 0000000000..7f2a1cb057 --- /dev/null +++ b/app/views/active_scaffold_overrides/edit_associated.js.erb @@ -0,0 +1,12 @@ +<% +associated_form = render :partial => "form_association_record", :object => @record, :locals => {:scope => @scope, :parent_record => @parent_record, :column => @column, :locked => @record.new_record? && @column.singular_association?} +options = {:singular => false} +if @column.singular_association? + options[:singular] = true +else + unless @record.new_record? + column = active_scaffold_config_for(@record.class).columns[@record.class.primary_key] + options[:id] = active_scaffold_input_options(column, column_scope(@column, @scope, @record))[:id] + end +end %> +ActiveScaffold.create_associated_record_form('<%=sub_form_list_id(:association => @column.name, :id => @parent_record.id || generated_id(@parent_record) || 99999999999)%>','<%=escape_javascript(associated_form)%>', <%= options.to_json.html_safe %>); diff --git a/frontends/default/views/field_search.html.erb b/app/views/active_scaffold_overrides/field_search.html.erb similarity index 100% rename from frontends/default/views/field_search.html.erb rename to app/views/active_scaffold_overrides/field_search.html.erb diff --git a/app/views/active_scaffold_overrides/form_messages.js.erb b/app/views/active_scaffold_overrides/form_messages.js.erb new file mode 100644 index 0000000000..a865405e74 --- /dev/null +++ b/app/views/active_scaffold_overrides/form_messages.js.erb @@ -0,0 +1 @@ +ActiveScaffold.replace_html('<%=element_messages_id%>','<%=escape_javascript(render(:partial => 'form_messages'))%>'); \ No newline at end of file diff --git a/app/views/active_scaffold_overrides/list.html.erb b/app/views/active_scaffold_overrides/list.html.erb new file mode 100644 index 0000000000..8f51a58140 --- /dev/null +++ b/app/views/active_scaffold_overrides/list.html.erb @@ -0,0 +1 @@ +<%= render :partial => 'list_with_header' -%> diff --git a/app/views/active_scaffold_overrides/on_action_update.js.erb b/app/views/active_scaffold_overrides/on_action_update.js.erb new file mode 100644 index 0000000000..effddbb9ae --- /dev/null +++ b/app/views/active_scaffold_overrides/on_action_update.js.erb @@ -0,0 +1,22 @@ +<% if controller.send :successful? %> + <% if @record %> + <%= render :partial => 'update_messages' %> + <% row = escape_javascript(render(:partial => 'list_record', :locals => {:record => @record})) -%> + <% if @action_link.try(:position) %> + ActiveScaffold.find_action_link('<%= element_form_id(:action => action_name) %>').close('<%= row %>'); + <% else %> + ActiveScaffold.update_row('<%= element_row_id(:action => :list, :id => @record.id) %>', '<%= row %>'); + ActiveScaffold.scroll_to('<%= element_row_id(:action => :list, :id => @record.id) %>', true); + <% end %> + <%= render :partial => 'update_calculations', :formats => [:js] %> + <% else %> + <% if @action_link.try(:position) %> + ActiveScaffold.find_action_link('<%= element_row_id(:action => action_name) %>').close(); + <% end %> + <%= render :partial => 'refresh_list' %> + <% end %> +<% else %> + <% flash[:error] = active_scaffold_error_messages_for(@record, :object_name => "#{@record.class.model_name.human.downcase}#{@record.new_record? ? '' : ": #{@record.to_label}"}", :header_message => '', :message => "#{@record.class.model_name.human.downcase}#{@record.new_record? ? '' : ": #{@record.to_label}"}", :container_tag => nil, :list_type => :br) if @record.try(:errors).present? %> + ActiveScaffold.replace_html('<%= active_scaffold_messages_id %>','<%= escape_javascript(render(:partial => 'messages')) %>'); + ActiveScaffold.scroll_to('<%= active_scaffold_messages_id %>', true); +<% end %> diff --git a/app/views/active_scaffold_overrides/on_create.js.erb b/app/views/active_scaffold_overrides/on_create.js.erb new file mode 100644 index 0000000000..cbedefad46 --- /dev/null +++ b/app/views/active_scaffold_overrides/on_create.js.erb @@ -0,0 +1,45 @@ +try { +var action_link; +<% form_selector = "#{element_form_id(:action => :create)}" +insert_at ||= :top -%> +<% if active_scaffold_config.list.always_show_create -%> +<%= render :partial => 'update_messages' %> +<% else -%> +action_link = ActiveScaffold.find_action_link('<%= form_selector %>'); +action_link.update_flash_messages('<%= escape_javascript(render(:partial => 'messages')) %>'); +<% end -%> +<% if controller.send :successful? -%> + <% if render_parent? %> + <% if nested_singular_association? %> + action_link.close(true); + <% elsif params[:parent_sti] && render_parent_action == :row %> + ActiveScaffold.create_record_row_from_url(action_link,'<%= url_for(render_parent_options) %>', <%= {:insert_at => insert_at}.to_json.html_safe %>); + <% else %> + ActiveScaffold.reload('<%= url_for render_parent_options %>'); + <% end %> + <% elsif active_scaffold_config.create.refresh_list %> + <%= render :partial => 'refresh_list' %> + <% elsif params[:parent_controller].nil? %> + <% new_row = render :partial => 'list_record', :locals => {:record => @record} %> + ActiveScaffold.create_record_row(action_link ? action_link.scaffold() : '<%= active_scaffold_id %>', '<%= escape_javascript(new_row) %>', <%= {:insert_at => insert_at}.to_json.html_safe %>); + <%= render :partial => 'update_calculations', :formats => [:js] %> + <% end %> + + <% unless render_parent? %> + <% if active_scaffold_config.list.always_show_create %> + ActiveScaffold.reset_form('<%= form_selector %>'); + <% elsif active_scaffold_config.create.persistent %> + action_link.reload(); + <% else %> + action_link.close(); + <% end %> + <% if (active_scaffold_config.create.action_after_create) %> + var link = ActiveScaffold.find_action_link('<%= action_link_id active_scaffold_config.create.action_after_create, @record.id %>'); + if (link) (function() { link.open() })<%= '.defer' if ActiveScaffold.js_framework == :prototype %>(); + <% end %> + <% end %> +<% else %> + ActiveScaffold.replace('<%= form_selector %>','<%= escape_javascript(render(:partial => 'create_form', :locals => {:xhr => true})) %>'); + ActiveScaffold.scroll_to('<%= form_selector %>', true); +<% end %> +} catch (e) { alert('RJS error:\n\n' + e.toString());} diff --git a/app/views/active_scaffold_overrides/on_mark.js.erb b/app/views/active_scaffold_overrides/on_mark.js.erb new file mode 100644 index 0000000000..f18d5b79b4 --- /dev/null +++ b/app/views/active_scaffold_overrides/on_mark.js.erb @@ -0,0 +1,6 @@ +<% + checked = all_marked? unless local_assigns.has_key? :checked + options = {:checked => checked, :include_mark_all => true, :include_checkboxes => params[:id].nil?} +%> +ActiveScaffold.mark_records('<%= active_scaffold_tbody_id %>',<%= options.to_json.html_safe %>); +<%= render :partial => 'update_messages' %> diff --git a/app/views/active_scaffold_overrides/on_update.js.erb b/app/views/active_scaffold_overrides/on_update.js.erb new file mode 100644 index 0000000000..74d8b8c51d --- /dev/null +++ b/app/views/active_scaffold_overrides/on_update.js.erb @@ -0,0 +1,35 @@ +try { +<% form_selector = "#{element_form_id(:action => :update, :id => @record.try(:id) || params[:id])}" %> +var action_link = ActiveScaffold.find_action_link('<%= form_selector %>'); +action_link.update_flash_messages('<%= escape_javascript(render(:partial => 'messages')) %>'); +<% if controller.send :successful? %> + <% if params[:dont_close] %> + <% row_selector = element_row_id(:action => :list, :id => @record.id) %> + ActiveScaffold.update_row('<%= row_selector %>', '<%= escape_javascript(render(:partial => 'list_record', :locals => {:record => @record})) %>'); + action_link.target = $('#<%= row_selector %>'); + <%= render :partial => 'update_calculations', :formats => [:js] %> + <%= "ActiveScaffold.enable_form('#{form_selector}');" if params[:iframe] == 'true' %> + <% else %> + <% if render_parent? %> + <% if nested_singular_association? || render_parent_action == :row %> + action_link.close(true); + <% else %> + ActiveScaffold.reload('<%= url_for render_parent_options %>'); + <% end %> + <% elsif update_refresh_list? %> + <%= render :partial => 'refresh_list' %> + <% else %> + <% if @record %> + action_link.close('<%= escape_javascript(render(:partial => 'list_record', :locals => {:record => @record})) %>'); + <% else %> + action_link.close(); + ActiveScaffold.delete_record_row('<%= element_row_id(:action => :list, :id => params[:id]) %>'); + <% end %> + <%= render :partial => 'update_calculations', :formats => [:js] %> + <% end %> + <% end %> +<% else %> + ActiveScaffold.replace('<%= form_selector %>', '<%= escape_javascript(render(:partial => 'update_form', :locals => {:xhr => true})) %>'); + ActiveScaffold.scroll_to('<%= form_selector %>', true); +<% end %> +} catch (e) { alert('RJS error:\n\n' + e.toString());} diff --git a/app/views/active_scaffold_overrides/render_field.js.erb b/app/views/active_scaffold_overrides/render_field.js.erb new file mode 100644 index 0000000000..d892da8f8a --- /dev/null +++ b/app/views/active_scaffold_overrides/render_field.js.erb @@ -0,0 +1 @@ +<%= render :partial => "render_field", :collection => @columns, :locals => {:source_id => @source_id, :scope => @scope} %> diff --git a/app/views/active_scaffold_overrides/render_field_inplace.html.erb b/app/views/active_scaffold_overrides/render_field_inplace.html.erb new file mode 100644 index 0000000000..bb068b9096 --- /dev/null +++ b/app/views/active_scaffold_overrides/render_field_inplace.html.erb @@ -0,0 +1,6 @@ +<% + column = active_scaffold_config.columns[@column.name] + options = active_scaffold_input_options(column) + options[:class] = "#{options[:class]} inplace_field" +-%> +<%= active_scaffold_input_for(column, nil, options) %> diff --git a/app/views/active_scaffold_overrides/row.js.erb b/app/views/active_scaffold_overrides/row.js.erb new file mode 100644 index 0000000000..27a37f4e73 --- /dev/null +++ b/app/views/active_scaffold_overrides/row.js.erb @@ -0,0 +1,2 @@ +ActiveScaffold.update_row('<%= element_row_id(:action => :list) %>', '<%= escape_javascript render('list_record', :record => @record) %>'); +<%= render :partial => 'update_calculations', :formats => [:js] %> diff --git a/app/views/active_scaffold_overrides/search.html.erb b/app/views/active_scaffold_overrides/search.html.erb new file mode 100644 index 0000000000..873d0da377 --- /dev/null +++ b/app/views/active_scaffold_overrides/search.html.erb @@ -0,0 +1,5 @@ +
+
view"> + <%= render :partial => 'search' -%> +
+
diff --git a/app/views/active_scaffold_overrides/show.html.erb b/app/views/active_scaffold_overrides/show.html.erb new file mode 100644 index 0000000000..3382a7b722 --- /dev/null +++ b/app/views/active_scaffold_overrides/show.html.erb @@ -0,0 +1,5 @@ +
+
view"> + <%= render :partial => 'show' -%> +
+
\ No newline at end of file diff --git a/frontends/default/views/update.html.erb b/app/views/active_scaffold_overrides/update.html.erb similarity index 50% rename from frontends/default/views/update.html.erb rename to app/views/active_scaffold_overrides/update.html.erb index 0749f23bbf..529fe95e17 100644 --- a/frontends/default/views/update.html.erb +++ b/app/views/active_scaffold_overrides/update.html.erb @@ -1,8 +1,8 @@
-
view"> - <% if active_scaffold_config.update.nested_links and active_scaffold_config.action_links.any? {|link| link.type == :record } -%> - <%= render :partial => 'update_actions', :locals => {:record => @record, :url_options => params_for(:action => :list, :id => @record.id)} %> +
view"> + <% if active_scaffold_config.update.nested_links and active_scaffold_config.action_links.member.empty? -%> + <%= render :partial => 'update_actions', :locals => {:record => @record} %> <% end -%> <%= render :partial => 'update_form' -%>
-
\ No newline at end of file +
diff --git a/app/views/active_scaffold_overrides/update_column.js.erb b/app/views/active_scaffold_overrides/update_column.js.erb new file mode 100644 index 0000000000..802efa1246 --- /dev/null +++ b/app/views/active_scaffold_overrides/update_column.js.erb @@ -0,0 +1,26 @@ +<% @column_span_id ||= element_cell_id(:id => @record.id.to_s, :action => 'update_column', :name => @column.name) -%> +<% unless controller.send :successful? -%> + alert('<%= escape_javascript(@record.errors.full_messages.join("\n")) %>'); + <% @record.reload -%> +<% end -%> +<% if @column.inplace_edit + ipe_update = @column.inplace_edit_update if controller.send :successful? -%> + <% case ipe_update + when :row -%> + ActiveScaffold.update_row('<%= element_row_id(:action => :list) %>', '<%= escape_javascript render('row', :record => @record) %>'); + <% when :table -%> + ActiveScaffold.replace_html('<%= active_scaffold_content_id %>', '<%= escape_javascript(render('list')) %>'); + <% else + formatted_value = get_column_value(@record, @column) -%> + ActiveScaffold.update_inplace_edit('<%= @column_span_id %>','<%= escape_javascript(formatted_value) %>', <%= column_empty?(formatted_value).to_json %>); + <% if ipe_update == :columns && @column.update_columns && !@column.update_columns.empty? + @rendered = Set.new([@column.name]) -%> + <%= render :partial => 'update_column', :collection => @column.update_columns & active_scaffold_config.list.columns.names, :locals => {:row_id => element_row_id(:action => :list, :id => @record.id)} %> + <% end %> + <% end -%> +<% else -%> + ActiveScaffold.replace_html('<%= @column_span_id %>','<%= escape_javascript(get_column_value(@record, @column)) %>'); +<% end -%> +<% if @column.calculation? -%> + ActiveScaffold.replace_html('<%= active_scaffold_calculations_id(:column => @column) %>', '<%= escape_javascript(render_column_calculation(@column)) %>'); +<% end -%> diff --git a/app/views/active_scaffold_overrides/update_row.js.erb b/app/views/active_scaffold_overrides/update_row.js.erb new file mode 100644 index 0000000000..177d86f7b3 --- /dev/null +++ b/app/views/active_scaffold_overrides/update_row.js.erb @@ -0,0 +1 @@ +ActiveScaffold.update_row('<%=element_row_id(:action => 'list', :id => @record.id)%>','<%=escape_javascript(render(:partial => 'list_record', :locals => {:record => @record}))%>'); \ No newline at end of file diff --git a/config/locales/de.yml b/config/locales/de.yml new file mode 100644 index 0000000000..71acd092c5 --- /dev/null +++ b/config/locales/de.yml @@ -0,0 +1,126 @@ +de: + time: + formats: + picker: ! '%d.%m.%Y %H:%M' + active_scaffold: + add: Hinzufügen + add_existing: Existierenden Eintrag hinzufügen + add_existing_model: Existierende %{model} hinzufügen + apply: Apply + are_you_sure_to_delete: Sind Sie sicher? + cancel: Abbrechen + click_to_edit: Zum Editieren anklicken + click_to_reset: Reset + close: Schließen + config_list: Konfigurieren + config_list_model: Konfiguriere Spalten für %{model} + create: Anlegen + create_model: Lege %{model} an + create_another: Weitere anlegen + created_model: ! '%{model} angelegt' + create_new: Neu anlegen + customize: Anpassen + delete: Löschen + deleted_model: ! '%{model} gelöscht' + delimiter: Trennzeichen + download: Download + edit: Bearbeiten + export: Exportieren + nested_for_model: ! '%{nested_model} für %{parent_model}' + nested_of_model: ! '%{nested_model} von %{parent_model}' + 'false': 'False' + filtered: (Gefiltert) + found: Gefunden + hide: Verstecken + inplace_edit_handle: -- + live_search: Live-Suche + loading: Lade… + mark_all_records: Mark all + next: Vor + no_entries: Keine Einträge + no_options: Keine Optionen + omit_header: Lasse Header weg + options: Optionen + pdf: PDF + previous: Zurück + print: Drucken + records_marked: + one: 1 marked %{model} + other: ! '%{count} marked %{model}' + refresh: Neu laden + remove: Entfernen + remove_file: Entferne oder Ersetze Datei + replace_existing: Existierenden ersetzen + replace_with_new: Mit Neuer ersetzen + revisions_for_model: Revisionen für %{model} + reset: Zurücksetzen + saving: Speichern… + search: Suche + search_terms: Suchbegriffe + _select_: ! '- Auswählen -' + show: Anzeigen + show_model: Zeige %{model} an + _to_: ! ' zu ' + 'true': 'True' + update: Speichern + update_model: Editiere %{model} + updated_model: ! '%{model} aktualisiert' + =: = + ! '>=': ! '>=' + <=: <= + ! '>': ! '>' + <: < + ! '!=': ! '!=' + between: Zwischen + contains: Enthält + begins_with: Beginnt + ends_with: Endet + today: Heute + yesterday: Gestern + tomorrow: Morgen + this_week: Diese Woche + prev_week: Letzte Woche + next_week: Nächste Woche + this_month: Diesen Monat + prev_month: Letzten Monat + next_month: Nächsten Monat + this_year: Dieses Jahr + prev_year: Letztes Jahr + next_year: Nächstes Jahr + past: Letzten + future: Nächsten + range: Zeitraum + seconds: Sekunden + minutes: Minuten + hours: Stunden + days: Tage + weeks: Wochen + months: Monate + years: Jahre + optional_attributes: Weitere + :null: 'Null' + not_null: Nicht Null + date_picker_options: + weekHeader: Wo + firstDay: 1 + isRTL: false + showMonthAfterYear: false + datetime_picker_options: + timeText: Uhrzeit + currentText: Jetzt + closeText: Schließen + human_conditions: + boolean: ! '%{column} = %{value}' + association: ! '%{column} = %{value}' + errors: + template: + header: + one: ! 'Konnte %{model} nicht speichern: ein Fehler.' + other: ! 'Konnte %{model} nicht speichern: %{count} Fehler.' + body: ! 'Bitte überprüfen Sie die folgenden Felder:' + cant_destroy_record: ! '%{record} kann nicht gelöscht werden' + internal_error: Fehler bei der Verarbeitung (code 500, Interner Fehler) + version_inconsistency: Inkonsistente Versionen - dieser Eintrag wurde verändert nachdem Sie mit der Bearbeitung begonnen haben. + record_not_saved: Eintrag kann nicht gespeichert werden. Ursache unbekannt. + no_authorization_for_action: Keine Berechtigung für Aktion %{action} + 'null': 'Null' diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000000..c16735654e --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,126 @@ +en: + time: + formats: + picker: ! '%a, %d %b %Y %H:%M:%S' + active_scaffold: + add: Add + add_existing: Add Existing + add_existing_model: Add Existing %{model} + apply: Apply + are_you_sure_to_delete: Are you sure you want to delete %{label}? + cancel: Cancel + click_to_edit: Click to edit + click_to_reset: Click to reset + close: Close + config_list: Configure + config_list_model: Configure Columns for %{model} + create: Create + create_model: Create %{model} + create_another: Create Another %{model} + created_model: Created %{model} + create_new: Create New + customize: Customize + delete: Delete + deleted_model: Deleted %{model} + delimiter: Delimiter + download: Download + edit: Edit + export: Export + nested_for_model: ! '%{nested_model} for %{parent_model}' + nested_of_model: ! '%{nested_model} of %{parent_model}' + 'false': 'False' + filtered: (Filtered) + found: Found + hide: Hide + inplace_edit_handle: -- + live_search: Live Search + loading: Loading… + mark_all_records: Mark all + next: Next + no_entries: No Entries + no_options: no options + omit_header: Omit Header + options: Options + pdf: PDF + previous: Previous + print: Print + records_marked: + one: 1 marked %{model} + other: ! '%{count} marked %{model}' + refresh: Refresh + remove: Remove + remove_file: Remove or Replace file + replace_existing: Replace Existing + replace_with_new: Replace With New + revisions_for_model: Revisions for %{model} + reset: Reset + saving: Saving… + search: Search + search_terms: Search Terms + _select_: ! '- select -' + show: Show + show_model: Show %{model} + _to_: ! ' to ' + 'true': 'True' + update: Update + update_model: Update %{model} + updated_model: Updated %{model} + =: = + ! '>=': ! '>=' + <=: <= + ! '>': ! '>' + <: < + ! '!=': ! '!=' + between: Between + contains: Contains + begins_with: Begins with + ends_with: Ends with + today: Today + yesterday: Yesterday + tomorrow: Tomorrow + this_week: This Week + prev_week: Last Week + next_week: Next Week + this_month: This Month + prev_month: Last Month + next_month: Next Month + this_year: This Year + prev_year: Last Year + next_year: Next Year + past: Past + future: Future + range: Range + seconds: Seconds + minutes: Minutes + hours: Hours + days: Days + weeks: Weeks + months: Months + years: Years + optional_attributes: Further Options + :null: 'Null' + not_null: Not Null + date_picker_options: + weekHeader: Wk + firstDay: 0 + isRTL: false + showMonthAfterYear: false + datetime_picker_options: + closeText: Close + currentText: Now + timeText: Hour + human_conditions: + boolean: ! '%{column} = %{value}' + association: ! '%{column} = %{value}' + errors: + template: + header: + one: 1 error prohibited this %{model} from being saved. + other: ! '%{count} errors prohibited this %{model} from being saved' + body: ! 'There were problems with the following fields:' + cant_destroy_record: ! '%{record} can''t be destroyed' + internal_error: Request Failed (code 500, Internal Error) + version_inconsistency: Version inconsistency - this record has been modified since you started editing it. + record_not_saved: Failed to save record cause of an unknown error + no_authorization_for_action: No Authorization for action %{action} + 'null': 'Null' diff --git a/config/locales/es.yml b/config/locales/es.yml new file mode 100644 index 0000000000..a46d3dcaff --- /dev/null +++ b/config/locales/es.yml @@ -0,0 +1,128 @@ +es: + time: + formats: + picker: ! '%a, %d %b %Y %H:%M:%S' + active_scaffold: + add: Añadir + add_existing: Añadir Existente + add_existing_model: Añadir %{model} Existente + apply: Aplicar + are_you_sure_to_delete: ¿Estás seguro de que quieres borrar %{label}? + cancel: Cancelar + click_to_edit: Pulsa para editar + click_to_reset: Pulsa para restaurar + close: Cerrar + config_list: Configurar + config_list_model: Configurar columnas de %{model} + create: Crear + create_model: Crear %{model} + create_another: Crear Otro %{model} + created_model: ! '%{model} creado' + create_new: Crear Nuevo + customize: Personalizar + delete: Borrar + deleted_model: ! '%{model} borrado' + delimiter: Delimitador + download: Descargar + edit: Editar + export: Exportar + 'false': 'No' + filtered: (Filtrado) + found: + one: encontrado + other: encontrados + hide: Ocultar + inplace_edit_handle: -- + live_search: Buscar en Vivo + loading: Cargando… + mark_all_records: Seleccionar todos + nested_for_model: ! '%{nested_model} de %{parent_model}' + nested_of_model: ! '%{nested_model} de %{parent_model}' + next: Siguiente + no_entries: Sin entradas + no_options: sin opciones + omit_header: Omitir Cabecera + options: Opciones + pdf: PDF + previous: Anterior + print: Imprimir + records_marked: + one: 1 %{model} seleccionado + other: ! '%{count} %{model} seleccionados' + refresh: Recargar + remove: Eliminar + remove_file: Eliminar o Reemplazar archivo + replace_existing: Reemplazar existente + replace_with_new: Reemplazar con Nuevo + revisions_for_model: Revisiones de %{model} + reset: Restaurar + saving: Guardando… + search: Buscar + search_terms: Términos a buscar + _select_: ! '- seleccionar -' + show: Ver + show_model: Ver %{model} + _to_: ! ' a ' + 'true': Sí + update: Actualizar + update_model: Actualizar %{model} + updated_model: ! '%{model} actualizado' + =: Igual + ! '>=': Mayor o igual + <=: Menor o igual + ! '>': Mayor + <: Menor + ! '!=': Distinto + between: Entre + contains: Contiene + begins_with: Empieza con + ends_with: Termina con + today: Hoy + yesterday: Ayer + tomorrow: Mañana + this_week: Esta semana + prev_week: Semana pasada + next_week: Próxima semana + this_month: Este mes + prev_month: Mes pasado + next_month: Próximo mes + this_year: Este año + prev_year: Año pasado + next_year: Próximo año + past: Pasado + future: Futuro + range: Rango + seconds: Segundos + minutes: Minutos + hours: Horas + days: Días + weeks: Semanas + months: Meses + years: Años + optional_attributes: Más opciones + :null: Nulo + not_null: No Nulo + date_picker_options: + weekHeader: Sm + firstDay: 1 + isRTL: false + showMonthAfterYear: false + datetime_picker_options: + timeText: Hora + currentText: Ahora + closeText: Cerrar + human_conditions: + boolean: ! '%{column} = %{value}' + association: ! '%{column} = %{value}' + errors: + template: + header: + one: No se pudo guardar %{model} debido a un error. + other: No se pudo guardar %{model} debido a %{count} errores. + body: ! 'Hubo problemas con los siguientes campos:' + cant_destroy_record: No se pudo borrar %{record} + internal_error: Petición fallida (código 500, error interno) + version_inconsistency: Inconsistencia de versiones - este registro se ha modificado después de que empezó a editarlo. + record_not_saved: Fallo guardando el registro debido a un error desconocido + no_authorization_for_action: No dispone de autorización para la acción %{action} + 'null': Nulo diff --git a/config/locales/fr.yml b/config/locales/fr.yml new file mode 100644 index 0000000000..5995e3fbcd --- /dev/null +++ b/config/locales/fr.yml @@ -0,0 +1,126 @@ +fr: + time: + formats: + picker: ! '%a, %d %b %Y %H:%M:%S' + active_scaffold: + add: Ajouter + add_existing: Ajouter un(e) existant(e) + add_existing_model: Ajouter un(e) %{model} existant(e) + apply: Appliquer + are_you_sure_to_delete: Êtes vous sûr de vouloir supprimer %{label} ? + cancel: Annuler + click_to_edit: Cliquer pour éditer + click_to_reset: Cliquer pour ré-initialiser + close: Fermer + config_list: Configurer + config_list_model: Configurer les colonnes pour %{model} + create: Créer + create_model: Créer %{model} + create_another: Créer un autre %{model} + created_model: ! '%{model} créé' + create_new: Créer un nouveau + customize: Personnaliser + delete: Supprimer + deleted_model: Suppression de %{model} + delimiter: Délimiteur + download: Télécharger + edit: Éditer + export: Exporter + nested_for_model: ! '%{nested_model} pour %{parent_model}' + nested_of_model: ! '%{nested_model} de %{parent_model}' + 'false': Faux + filtered: (Filtré) + found: Trouvé + hide: Cacher + inplace_edit_handle: -- + live_search: Recherche en temps réel + loading: Chargement… + mark_all_records: Marquer tous + next: Suivant + no_entries: Pas d'entrée + no_options: pas d'option + omit_header: Omettre les en-têtes + options: Options + pdf: PDF + previous: Précédent + print: Imprimer + records_marked: + one: 1 %{model} marqué + other: ! '%{count} %{model} marqués' + refresh: Rafraîchir + remove: Supprimer + remove_file: Supprimer et remplacer le fichier + replace_existing: Remplacer existant(e) + replace_with_new: Remplacer avec le nouveau + revisions_for_model: Révision pour %{model} + reset: Annuler + saving: Sauvegarder… + search: Rechercher + search_terms: Recherche de termes + _select_: ! '- sélectionner -' + show: Montrer + show_model: Montrer %{model} + _to_: ! ' à ' + 'true': Vrai + update: Mettre à jour + update_model: Mettre à jour le(/la) %{model} + updated_model: Mis à jour de %{model} + =: = + ! '>=': ! '>=' + <=: <= + ! '>': ! '>' + <: < + ! '!=': ! '!=' + between: Entre + contains: Contient + begins_with: Commençant par + ends_with: Se terminant par + today: Aujourd'hui + yesterday: Hier + tomorrow: Demain + this_week: Cette Semaine + prev_week: Semaine dernière + next_week: Semaine prochaine + this_month: Ce Mois + prev_month: Mois dernier + next_month: Mois prochain + this_year: Cette Année + prev_year: Année dernière + next_year: Année prochaine + past: Passé + future: Futur + range: Intervale + seconds: Secondes + minutes: Minutes + hours: Heures + days: Jours + weeks: Semaines + months: Mois + years: Années + optional_attributes: Options additionnelles + :null: Nulle + not_null: Non Nulle + date_picker_options: + weekHeader: Sm + firstDay: 1 + isRTL: false + showMonthAfterYear: false + datetime_picker_options: + timeText: Heure + currentText: Maintenant + closeText: Fermer + human_conditions: + boolean: ! '%{column} = %{value}' + association: ! '%{column} = %{value}' + errors: + template: + header: + one: 1 erreur interdit ce(tte) %{model} d'être sauvegardé. + other: ! '%{count} erreurs interdisent ce(tte) %{model} d''être sauvegardé' + body: ! 'Il y avait des problèmes avec les champs suivants :' + cant_destroy_record: ! '%{record} ne peut être supprimé' + internal_error: Erreur de la requête (code 500, Erreur interne) + version_inconsistency: Version incomplète - Cet enregistrement a été modifié depuis que vous avez commencé à l'éditer. + record_not_saved: Impossible d'enregistrer l'enregistrement à cause d'une erreur inconnue + no_authorization_for_action: Aucune autorisation pour l'action %{action} + 'null': Nulle diff --git a/config/locales/hu.yml b/config/locales/hu.yml new file mode 100644 index 0000000000..20e424078a --- /dev/null +++ b/config/locales/hu.yml @@ -0,0 +1,126 @@ +hu: + time: + formats: + picker: ! '%a, %d %b %Y %H:%M:%S' + active_scaffold: + add: Hozzáadás + add_existing: Meglevő hozzáadása + add_existing_model: Meglevő %{model} hozzáadása + apply: Apply + are_you_sure_to_delete: + cancel: Mégse + click_to_edit: Kattints a szerkesztéshez + click_to_reset: Kattints az alapállapothoz + close: Bezárás + config_list: Configure + config_list_model: Configure Columns for %{model} + create: Létrehozás + create_model: ! '%{model} létrehozása' + create_another: + created_model: ! '%{model} létrehozva' + create_new: Új létrehozása + customize: Testreszabás + delete: Törlés + deleted_model: ! '%{model} törölve' + delimiter: Elválasztó + download: Letöltés + edit: Szerkesztés + export: Exportálás + nested_for_model: ! '%{nested_model} / %{parent_model}' + nested_of_model: ! '%{nested_model} of %{parent_model}' + 'false': 'False' + filtered: (Szűrt) + found: Találat + hide: Elrejtés + inplace_edit_handle: -- + live_search: Élő keresés + loading: Betöltés… + mark_all_records: Mark all + next: Következő + no_entries: Nincs elem + no_options: nincsenek opciók + omit_header: Fejléc mellőzése + options: Opciók + pdf: PDF + previous: Előző + print: Nyomtatás + records_marked: + one: 1 marked %{model} + other: ! '%{count} marked %{model}' + refresh: Frissítés + remove: Törlés + remove_file: Fájl törlése, vagy cseréje + replace_existing: Replace existing + replace_with_new: Csere újjal + revisions_for_model: ! '%{model} revíziói' + reset: Alapállapot + saving: Mentés… + search: Keresés + search_terms: Keresési kifejezések + _select_: ! '- válassz -' + show: Mutatás + show_model: ! '%{model} mutatása' + _to_: ! ' – ' + 'true': 'True' + update: Modosítás + update_model: ! '%{model} modosítása' + updated_model: ! '%{model} módosítva' + =: = + ! '>=': ! '>=' + <=: <= + ! '>': ! '>' + <: < + ! '!=': ! '!=' + between: Között + contains: Contains + begins_with: Begins with + ends_with: Ends with + today: Today + yesterday: Yesterday + tomorrow: Tommorrow + this_week: This Week + prev_week: Last Week + next_week: Next Week + this_month: This Month + prev_month: Last Month + next_month: Next Month + this_year: This Year + prev_year: Last Year + next_year: Next Year + past: Past + future: Future + range: Range + seconds: Seconds + minutes: Minutes + hours: Hours + days: Days + weeks: Weeks + months: Months + years: Years + optional_attributes: Further Options + :null: 'Null' + not_null: Not Null + date_picker_options: + weekHeader: Wk + firstDay: 0 + isRTL: false + showMonthAfterYear: false + datetime_picker_options: + closeText: + currentText: + timeText: + human_conditions: + boolean: ! '%{column} = %{value}' + association: ! '%{column} = %{value}' + errors: + template: + header: + one: 1 error prohibited this %{model} from being saved. + other: ! '%{count} errors prohibited this %{model} from being saved' + body: ! 'There were problems with the following fields:' + cant_destroy_record: ! 'nem törölhető: %{record}' + internal_error: A lekérés sikertelen (code 500, Internal Error) + version_inconsistency: Verzió ütközés - ezt a rekordot módosították mióta elkezdted szerkeszteni. + no_authorization_for_action: + 'null': 'Null' + record_not_saved: diff --git a/config/locales/ja.yml b/config/locales/ja.yml new file mode 100644 index 0000000000..126074fc4c --- /dev/null +++ b/config/locales/ja.yml @@ -0,0 +1,126 @@ +ja: + time: + formats: + picker: ! '%a, %d %b %Y %H:%M:%S' + active_scaffold: + add: 追加 + add_existing: 既存のものを追加 + add_existing_model: 既存の%{model}を追加 + apply: Apply + are_you_sure_to_delete: + cancel: キャンセル + click_to_edit: クリックして編集 + click_to_reset: Click to reset + close: 閉じる + config_list: Configure + config_list_model: Configure Columns for %{model} + create: 作成 + create_model: ! '%{model}を作成' + create_another: + created_model: ! '%{model}を作成しました' + create_new: 新規作成 + customize: カスタマイズ + delete: 削除 + deleted_model: ! '%{model}を削除しました' + delimiter: Delimiter + download: ダウンロード + edit: 編集 + export: Export + nested_for_model: ! '%{parent_model}の%{nested_model}' + nested_of_model: ! '%{nested_model} of %{parent_model}' + 'false': 'False' + filtered: (フィルタ中) + found: 個ありました + hide: 隠す + inplace_edit_handle: -- + live_search: その場で検索 + loading: 読み込み中… + mark_all_records: Mark all + next: 次 + no_entries: 見つかりませんでした + no_options: オプション無し + omit_header: Omit Header + options: オプション + pdf: PDF + previous: 前 + print: 印刷 + records_marked: + one: 1 marked %{model} + other: ! '%{count} marked %{model}' + refresh: Refresh + remove: 削除 + remove_file: ファイルを削除または置換 + replace_existing: Replace existing + replace_with_new: 新しいもので置換 + revisions_for_model: Revisions for %{model} + reset: リセット + saving: 保存中… + search: 検索 + search_terms: 検索単語 + _select_: ! '- 選択してください -' + show: 表示 + show_model: ! '%{model}を表示' + _to_: ! ' to ' + 'true': 'True' + update: 更新 + update_model: ! '%{model}を更新' + updated_model: ! '%{model}を更新しました' + =: = + ! '>=': ! '>=' + <=: <= + ! '>': ! '>' + <: < + ! '!=': ! '!=' + between: Between + contains: Contains + begins_with: Begins with + ends_with: Ends with + today: Today + yesterday: Yesterday + tomorrow: Tommorrow + this_week: This Week + prev_week: Last Week + next_week: Next Week + this_month: This Month + prev_month: Last Month + next_month: Next Month + this_year: This Year + prev_year: Last Year + next_year: Next Year + past: Past + future: Future + range: Range + seconds: Seconds + minutes: Minutes + hours: Hours + days: Days + weeks: Weeks + months: Months + years: Years + optional_attributes: Further Options + :null: 'Null' + not_null: Not Null + date_picker_options: + weekHeader: Wk + firstDay: 0 + isRTL: false + showMonthAfterYear: false + datetime_picker_options: + closeText: + currentText: + timeText: + human_conditions: + boolean: ! '%{column} = %{value}' + association: ! '%{column} = %{value}' + errors: + template: + header: + one: 1 error prohibited this %{model} from being saved. + other: ! '%{count} errors prohibited this %{model} from being saved' + body: ! 'There were problems with the following fields:' + cant_destroy_record: ! '%{record}を削除で来ません' + internal_error: ! 'リクエストが失敗しました(コード500: 内部エラー)' + version_inconsistency: バージョンが一致しません - あなたが編集している間にこのレコードが変更されました。 + no_authorization_for_action: + 'null': 'Null' + record_not_saved: diff --git a/config/locales/ru.yml b/config/locales/ru.yml new file mode 100644 index 0000000000..0891bb6dc8 --- /dev/null +++ b/config/locales/ru.yml @@ -0,0 +1,134 @@ +ru: + time: + formats: + picker: ! '%a, %d %b %Y %H:%M:%S' + active_scaffold: + add: Добавить запись + add_existing: Добавить существующую запись + add_existing_model: ! '%{model}: добавить существующую запись' + apply: Применить + are_you_sure_to_delete: Удалить %{label}? + cancel: Отмена + click_to_edit: Нажмите для редактирования + click_to_reset: Нажмите для сброса + close: Закрыть + config_list: Настройки списка + config_list_model: ! '%{model}: настройки списка' + create: Создать + create_model: ! '%{model}: создать запись' + create_another: ! '%{model}: Создать другую запись' + created_model: ! '%{model}: запись создана' + create_new: Создать новую запись + customize: Настроить + delete: Удалить + deleted_model: ! '%{model}: запись удалена' + delimiter: Разделитель + download: Загрузить + edit: Изменить + export: Экспорт + nested_for_model: ! '%{parent_model} / %{nested_model}' + nested_of_model: ! '%{nested_model} @ %{parent_model}' + 'false': Нет + filtered: (Найденное) + found: + one: запись + few: записи + many: записей + other: записи + hide: Скрыть + inplace_edit_handle: -- + live_search: Поиск + loading: Загрузка… + mark_all_records: Отметить все + next: Следующее + no_entries: Нет записей + no_options: Нет вариантов + omit_header: Пропустить заголовок + options: Настройки + pdf: PDF + previous: Предыдущее + print: Печать + records_marked: + one: Отмечена 1 запись + few: Отмечено %{count} записи + many: Отмечено %{count} записей + other: Отмечено %{count} записи + refresh: Обновить + remove: Удалить + remove_file: Удалить или заменить файл + replace_existing: Заменить существующим + replace_with_new: Заменить новым + revisions_for_model: ! '%{model}: редакции' + reset: Сброс + saving: Сохранение… + search: Поиск + search_terms: Ключевые слова + _select_: ! '- выбрать -' + show: Показать + show_model: ! '%{model}: показать запись' + _to_: ! ' to ' + 'true': Да + update: Обновить запись + update_model: ! '%{model}: обновить запись' + updated_model: ! '%{model}: запись обновлена' + =: = + ! '>=': ! '>=' + <=: <= + ! '>': ! '>' + <: < + ! '!=': ! '!=' + between: В интервале + contains: Содержит + begins_with: Начинается с + ends_with: Оканчивается на + today: Сегодня + yesterday: Вчера + tomorrow: Завтра + this_week: На этой неделе + prev_week: На прошлой неделе + next_week: На следующей неделе + this_month: В этом месяце + prev_month: В прошлом месяце + next_month: В следующем месяце + this_year: В этом году + prev_year: В прошлом году + next_year: В следующем году + past: Прошедшие + future: Будущие + range: Интервал + seconds: секунд + minutes: минут + hours: часов + days: дней + weeks: недель + months: месяцев + years: лет + optional_attributes: Дополнительные настройки + :null: Пусто + not_null: Не пусто + date_picker_options: + weekHeader: Нед. + firstDay: 1 + isRTL: false + showMonthAfterYear: false + datetime_picker_options: + closeText: + currentText: + timeText: + human_conditions: + boolean: ! '%{column} = %{value}' + association: ! '%{column} = %{value}' + errors: + template: + header: + one: ! '%{model}: сохранение не удалось из-за %{count} ошибки' + few: ! '%{model}: сохранение не удалось из-за %{count} ошибок' + many: ! '%{model}: сохранение не удалось из-за %{count} ошибок' + other: ! '%{model}: сохранение не удалось из-за %{count} ошибки' + body: ! 'Проблемы возникли со следующими полями:' + cant_destroy_record: Запись %{record} не может быть удалена + internal_error: 500 Внутренняя ошибка сервера + version_inconsistency: Эта запись была обновлена с того момента, как вы начали ее редактировать + record_not_saved: Запись не может быть сохранена из-за неизвестной ошибки + no_authorization_for_action: Нет прав на выполнение действия "%{action}" + 'null': Пусто diff --git a/frontends/default/views/_add_existing_form.html.erb b/frontends/default/views/_add_existing_form.html.erb deleted file mode 100644 index 73687e897d..0000000000 --- a/frontends/default/views/_add_existing_form.html.erb +++ /dev/null @@ -1,40 +0,0 @@ -<% url_options = params_for(:action => :add_existing) -%> -<% if request.xhr? -%> -<%= form_remote_tag :url => url_options, - :after => "$('#{loading_indicator_id(:action => :add_existing, :id => params[:id])}').style.visibility = 'visible'; Form.disable('#{element_form_id(:action => :add_existing)}');", - :complete => "$('#{loading_indicator_id(:action => :add_existing, :id => params[:id])}').style.visibility = 'hidden'; Form.enable('#{element_form_id(:action => :add_existing)}');", - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :html => { :href => url_for(url_options), - :id => element_form_id(:action => :add_existing), - :class => 'create' } %> -<% else -%> -<%= form_tag url_options, - :id => element_form_id(:action => :add_existing), - :class => 'create' %> -<% end -%> - -

<%= active_scaffold_config.nested.label -%>

- - <% if request.xhr? -%> -
- <% else -%> - <%= render :partial => 'form_messages' %> - <% end -%> - - - <%# select_options = options_for_select(options_for_association(nested_association)) unless column.through_association? -%> - <% select_options ||= options_for_select(active_scaffold_config.model.find(:all).collect {|c| [h(c.to_label), c.id]}) -%> - <% unless select_options.empty? -%> - <%= select_tag 'associated_id', '' + select_options %> - <% end -%> - - - - - diff --git a/frontends/default/views/_create_form.html.erb b/frontends/default/views/_create_form.html.erb deleted file mode 100644 index 781f35721d..0000000000 --- a/frontends/default/views/_create_form.html.erb +++ /dev/null @@ -1,49 +0,0 @@ -<% url_options = params_for(:action => :create) -%> -<%= -if request.xhr? - if active_scaffold_config.create.multipart? # file_uploads - form_remote_upload_tag url_options.merge({:iframe => true}), - :onsubmit => onsubmit, - :id => element_form_id(:action => :create), - :loading => "$('#{loading_indicator_id(:action => :create, :id => params[:id])}').style.visibility = 'visible';", - :class => 'create' - else - form_remote_tag :url => url_options, - :after => "$('#{loading_indicator_id(:action => :create, :id => params[:id])}').style.visibility = 'visible'; Form.disable('#{element_form_id(:action => :create)}');", - :complete => "$('#{loading_indicator_id(:action => :create, :id => params[:id])}').style.visibility = 'hidden'; Form.enable('#{element_form_id(:action => :create)}');", - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :html => { - :href => url_for(url_options), - :onsubmit => onsubmit, - :id => element_form_id(:action => :create), - :class => 'create' - } - end -else - form_tag url_options, - :onsubmit => onsubmit, - :id => element_form_id(:action => :create), - :multipart => active_scaffold_config.create.multipart?, - :class => 'create' -end -%> - -

<%= active_scaffold_config.create.label -%>

- - <% if request.xhr? -%> -
<%= error_messages_for :record, :object_name => @record.class.human_name.downcase %>
- <% else -%> - <%= render :partial => 'form_messages' %> - <% end -%> - - <%= render :partial => 'form', :locals => { :columns => active_scaffold_config.create.columns } %> - - - - - diff --git a/frontends/default/views/_create_form_on_list.html.erb b/frontends/default/views/_create_form_on_list.html.erb deleted file mode 100644 index 2d15a9d6d3..0000000000 --- a/frontends/default/views/_create_form_on_list.html.erb +++ /dev/null @@ -1,38 +0,0 @@ -<% url_options = params_for(:action => :create) -%> -<%= -if active_scaffold_config.create.multipart? # file_uploads - form_remote_upload_tag url_options.merge({:iframe => true}), - :onsubmit => onsubmit, - :id => element_form_id(:action => :create), - :class => 'create' -else - form_remote_tag :url => url_options, - :after => "$('#{loading_indicator_id(:action => :create, :id => params[:id])}').style.visibility = 'visible'; Form.disable('#{element_form_id(:action => :create)}');", - :complete => "$('#{loading_indicator_id(:action => :create, :id => params[:id])}').style.visibility = 'hidden'; Form.enable('#{element_form_id(:action => :create)}');", - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :html => { - :href => url_for(url_options), - :onsubmit => onsubmit, - :id => element_form_id(:action => :create), - :class => 'create' - } -end -%> - -

<%= active_scaffold_config.create.label -%>

- - <% if request.xhr? -%> -
<%= error_messages_for :record, :object_name => @record.class.human_name.downcase %>
- <% else -%> - <%= render :partial => 'form_messages' %> - <% end -%> - - <%= render :partial => 'form', :locals => { :columns => active_scaffold_config.create.columns } %> - - - - \ No newline at end of file diff --git a/frontends/default/views/_field_search.html.erb b/frontends/default/views/_field_search.html.erb deleted file mode 100644 index ea93a57b54..0000000000 --- a/frontends/default/views/_field_search.html.erb +++ /dev/null @@ -1,45 +0,0 @@ -<% -# We have to remove search form params before the url_for method call, otherwise it throughs it on -search_params = params[:search] -params.merge!(:search => nil) -href = url_for(params_for(:action => :update_table, :escape => false).delete_if{|k,v| k == 'search'}) --%> -<%= form_remote_tag :url => href, - :method => :get, - :before => "addActiveScaffoldPageToHistory('#{href}', '#{params[:controller]}')", - :after => "$('#{loading_indicator_id(:action => :search, :id => params[:id])}').style.visibility = 'visible'; Form.disable('#{search_form_id}');", - :complete => "$('#{loading_indicator_id(:action => :search, :id => params[:id])}').style.visibility = 'hidden'; Form.enable('#{search_form_id}');", - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :update => active_scaffold_content_id, - :html => { :href => href, :id => search_form_id, :class => 'search', :method => :get } %> - -
    - <% active_scaffold_config.field_search.columns.each do |column| -%> - <% next unless column.search_sql -%> - <% name = "search[#{column.name}]" %> - <% value = nil %> - <% value = search_params[column.name] if search_params %> -
  1. -
    -
    - -
    -
    - <%= active_scaffold_search_for(column) %> -
    -
    -
  2. - <% end -%> -
- - - - \ No newline at end of file diff --git a/frontends/default/views/_form.html.erb b/frontends/default/views/_form.html.erb deleted file mode 100644 index fee5086dac..0000000000 --- a/frontends/default/views/_form.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -
    > - <% columns.each :for => @record do |column| -%> - <% if is_subsection? column -%> -
  1. -
    <%= column.label %> (<%= link_to_visibility_toggle(:default_visible => !column.collapsed) -%>)
    - <%= render :partial => 'form', :locals => { :columns => column } %> -
  2. - <% elsif is_subform? column and !override_form_field?(column) -%> -
  3. - <%= render :partial => form_partial_for_column(column), :locals => { :column => column } -%> -
  4. - <% else -%> -
  5. - <%= render :partial => form_partial_for_column(column), :locals => { :column => column } -%> -
  6. - <% end -%> - <% end -%> -
diff --git a/frontends/default/views/_form_association.html.erb b/frontends/default/views/_form_association.html.erb deleted file mode 100644 index f073e3ca0a..0000000000 --- a/frontends/default/views/_form_association.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<% -parent_record = @record -associated = column.singular_association? ? [parent_record.send(column.name)].compact : parent_record.send(column.name).to_a -associated = associated.sort_by {|r| r.new_record? ? 99999999999 : r.id} unless column.association.options.has_key?(:order) - -if column.show_blank_record - show_blank_record = (column.plural_association? or (column.singular_association? and associated.empty?)) - show_blank_record = false if column.through_association? - show_blank_record = false unless column.association.klass.authorized_for?(:action => :create) -else - show_blank_record = false -end - -associated << column.association.klass.new if show_blank_record --%> -
<%= column.label -%> (<%= link_to_visibility_toggle(:default_visible => !column.collapsed) -%>)
-
> -<%= render :partial => subform_partial_for_column(column), :locals => {:column => column, :parent_record => parent_record, :associated => associated} %> -
-<% @record = parent_record -%> diff --git a/frontends/default/views/_form_association_footer.html.erb b/frontends/default/views/_form_association_footer.html.erb deleted file mode 100644 index 3140d2feea..0000000000 --- a/frontends/default/views/_form_association_footer.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -<% -begin - remote_controller = active_scaffold_controller_for(column.association.klass) -rescue ActiveScaffold::ControllerNotFound - remote_controller = nil -end - -show_add_existing = (!column.through_association? and options_for_association_count(column.association) > 0) - -show_add_new = !column.through_association? and (column.plural_association? or (column.singular_association? and not associated.empty?)) -show_add_new = false unless @record.class.authorized_for?(:action => :create) - -edit_associated_url = url_for(:action => 'edit_associated', :id => parent_record.id, :association => column.name, :associated_id => '--ID--', :escape => false, :eid => params[:eid], :parent_controller => params[:parent_controller], :parent_id => params[:parent_id]) -add_new_url = url_for(:action => 'edit_associated', :id => parent_record.id, :association => column.name, :escape => false, :eid => params[:eid], :parent_controller => params[:parent_controller], :parent_id => params[:parent_id]); - -return unless show_add_new or show_add_existing - --%> - - - diff --git a/frontends/default/views/_form_attribute.html.erb b/frontends/default/views/_form_attribute.html.erb deleted file mode 100644 index 64264430bc..0000000000 --- a/frontends/default/views/_form_attribute.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<% scope ||= nil %> -
-
- -
-
- <%= active_scaffold_input_for column, scope %> - <% if column.description -%> - <%= column.description %> - <% end -%> -
-
\ No newline at end of file diff --git a/frontends/default/views/_form_hidden_attribute.html.erb b/frontends/default/views/_form_hidden_attribute.html.erb deleted file mode 100644 index 370b589fdc..0000000000 --- a/frontends/default/views/_form_hidden_attribute.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= hidden_field :record, column.name %> diff --git a/frontends/default/views/_form_messages.html.erb b/frontends/default/views/_form_messages.html.erb deleted file mode 100644 index ba7217a63f..0000000000 --- a/frontends/default/views/_form_messages.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<%= render :partial => 'messages' %> - -<% unless @record.nil? %> - <%= error_messages_for :record, :object_name => @record.class.human_name.downcase %> -<% end %> \ No newline at end of file diff --git a/frontends/default/views/_horizontal_subform.html.erb b/frontends/default/views/_horizontal_subform.html.erb deleted file mode 100644 index 507ccbfcae..0000000000 --- a/frontends/default/views/_horizontal_subform.html.erb +++ /dev/null @@ -1,19 +0,0 @@ - - <% @record = column.association.klass.new -%> - <%= render :partial => 'horizontal_subform_header', :locals => {:parent_record => parent_record} %> - - - <% associated.each_index do |index| %> - <% @record = associated[index] -%> - <% if @record.errors.count -%> - - - - <% end %> - <%= render :partial => 'horizontal_subform_record', :locals => {:scope => column_scope(column), :parent_record => parent_record, :column => column, :locked => @record.new_record? && @record == associated.last} %> - <% end -%> - -
- <%= error_messages_for :record, :object_name => @record.class.human_name.downcase %> -
-<%= render :partial => 'form_association_footer', :locals => {:parent_record => parent_record, :column => column, :associated => associated} -%> diff --git a/frontends/default/views/_horizontal_subform_header.html.erb b/frontends/default/views/_horizontal_subform_header.html.erb deleted file mode 100644 index e1760b5067..0000000000 --- a/frontends/default/views/_horizontal_subform_header.html.erb +++ /dev/null @@ -1,10 +0,0 @@ - - - <% - active_scaffold_config_for(@record.class).subform.columns.each :for => @record, :flatten => true do |column| - next unless in_subform?(column, parent_record) and column_renders_as(column) != :hidden - -%> - <%= column.label %> - <% end -%> - - \ No newline at end of file diff --git a/frontends/default/views/_horizontal_subform_record.html.erb b/frontends/default/views/_horizontal_subform_record.html.erb deleted file mode 100644 index ea4eff341b..0000000000 --- a/frontends/default/views/_horizontal_subform_record.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<% record_column = column -%> -<% readonly = (@record.readonly? or not @record.authorized_for?(:action => :update)) -%> -<% action = @record.new_record? ? :create : (readonly ? :read : nil) -%> -<% show_actions = false -%> - -<% active_scaffold_config_for(@record.class).subform.columns.each :for => @record.class, :action => action, :flatten => true do |column| %> -<% - next unless in_subform?(column, parent_record) - show_actions = true - column = column.clone - column.form_ui ||= :select if column.association --%> - - <% unless readonly and not @record.new_record? -%> - <%= render :partial => form_partial_for_column(column), :locals => { :column => column, :scope => scope } -%> - <% else -%> -

<%= get_column_value(@record, column) -%>

- <% end -%> - -<% end -%> -<% if show_actions -%> - - <%= link_to_function as_(:remove), '$(this).up(".association-record").remove()', { :class => "destroy" } if record_column.plural_association? and (@record.authorized_for?(:action => :destroy) or not [:destroy, :delete_all].include?(record_column.association.options[:dependent])) %> - <% unless @record.new_record? %> - " value="<%= @record.id -%>" /> - <% end -%> - -<% end -%> - diff --git a/frontends/default/views/_list.html.erb b/frontends/default/views/_list.html.erb deleted file mode 100644 index 299ea875db..0000000000 --- a/frontends/default/views/_list.html.erb +++ /dev/null @@ -1,37 +0,0 @@ - - - - <%= render :partial => 'list_column_headings' %> - - - - - - - - - <% if !@records.empty? -%> - <%= render :partial => 'list_record', :collection => @page.items, :locals => { :hidden => false, :dont_show_calculations => true } %> - <% end -%> - <% if active_scaffold_config.list.columns.any? {|c| c.calculation?} -%> - <%= render :partial => 'list_calculations' %> - <% end -%> - -
-
- <%= render :partial => 'messages' %> -
-

> - <%= as_(active_scaffold_config.list.filtered_message) %> -

-

> - <%= as_(active_scaffold_config.list.no_entries_message) %> -

-
- diff --git a/frontends/default/views/_list_actions.html.erb b/frontends/default/views/_list_actions.html.erb deleted file mode 100644 index 90d0279135..0000000000 --- a/frontends/default/views/_list_actions.html.erb +++ /dev/null @@ -1,13 +0,0 @@ - - - - <% active_scaffold_config.action_links.each :record do |link| -%> - <% next if controller.respond_to? link.security_method and !controller.send(link.security_method) -%> - - <% end -%> - -
- <%= loading_indicator_tag(:action => :record, :id => record.id) %> - - <%= record.authorized_for?(:action => link.crud_type) ? render_action_link(link, url_options) : "#{link.label}" -%> -
\ No newline at end of file diff --git a/frontends/default/views/_list_calculations.html.erb b/frontends/default/views/_list_calculations.html.erb deleted file mode 100644 index 0f07158f38..0000000000 --- a/frontends/default/views/_list_calculations.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<% display_class = ( @records.kind_of?(Array) ? @records.first : @records ) -%> - - <% active_scaffold_config.list.columns.each do |column| -%> - - <% if column.calculation? -%> - <% - calculation = column_calculation(column) - - override_formatter = "render_#{column.name}_#{column.calculate}" - calculation = self.method(override_formatter).call(calculation) if respond_to? override_formatter - - -%> - <%= calculation.to_s %> - <% else -%> -   - <% end -%> - - <% end -%> - <% unless active_scaffold_config.action_links.empty? -%> -   - <% end -%> - diff --git a/frontends/default/views/_list_column_headings.html.erb b/frontends/default/views/_list_column_headings.html.erb deleted file mode 100644 index aeae7b13fa..0000000000 --- a/frontends/default/views/_list_column_headings.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -<% -sorting = active_scaffold_config.list.user.sorting -sorting_stages = ['reset', 'ASC', 'DESC'] -default_sorting = active_scaffold_config.list.sorting -default_sorting_stages = ['ASC', 'DESC'] --%> -<% active_scaffold_config.list.columns.each do |column| -%> - <% - stages = default_sorting.sorts_on?(column) ? default_sorting_stages : sorting_stages - column_sort_direction = stages.after(sorting.direction_of(column)) || 'ASC' - sort_params = params_for(:action => :update_table, :page => 1, - :sort => column.name, :sort_direction => column_sort_direction) - column_header_id = active_scaffold_column_header_id(column) - -%> - "> - <% if column.sortable? -%> - <% href = url_for(sort_params) -%> - <%= link_to_remote column.label, - { :url => sort_params, - :before => "addActiveScaffoldPageToHistory('#{href}', '#{controller_id}')", - :loading => "Element.addClassName('#{column_header_id}','loading');", - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :update => active_scaffold_content_id, - :method => :get }, - { :href => href, :title => column.description } %> - <% else -%> -

<%= column.label %>

- <% end -%> - -<% end -%> - - diff --git a/frontends/default/views/_list_header.html.erb b/frontends/default/views/_list_header.html.erb deleted file mode 100644 index fc0e823929..0000000000 --- a/frontends/default/views/_list_header.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -<% if active_scaffold_config.action_links.any? { |link| link.type == :table } -%> -
- <% new_params = params_for(:action => :table) %> - <% active_scaffold_config.action_links.each :table do |link| -%> - <% next if controller.respond_to? link.security_method and !controller.send(link.security_method) -%> - <% next if link.action == 'new' && params[:nested].nil? && active_scaffold_config.list.always_show_create %> - <% next if link.action == 'show_search' && active_scaffold_config.list.always_show_search %> - <%= render_action_link(link, new_params) -%> - <% end -%> - - <%= loading_indicator_tag(:action => :table) %> -
-<% end %> -

<%= active_scaffold_config.list.user.label %>

\ No newline at end of file diff --git a/frontends/default/views/_list_inline_adapter.html.erb b/frontends/default/views/_list_inline_adapter.html.erb deleted file mode 100644 index ab76b60de4..0000000000 --- a/frontends/default/views/_list_inline_adapter.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<%= update_page_tag { |page| page.replace_html active_scaffold_messages_id, :partial => 'messages' } %> - - -
<%= "#{params[:associations] ? params[:associations] : params[:controller]}-view" %> view"> - <%= as_(:close) %> - <%= payload -%> -
- - diff --git a/frontends/default/views/_list_pagination_links.html.erb b/frontends/default/views/_list_pagination_links.html.erb deleted file mode 100644 index a41f3acfdc..0000000000 --- a/frontends/default/views/_list_pagination_links.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<% unless current_page.nil? -%> - <% pagination_params = params_for(:action => 'update_table') -%> - <% indicator_params = pagination_params.merge(:action => 'pagination') -%> - <% previous_url = url_for(pagination_params.merge(:page => current_page.number - 1)) -%> - <% next_url = url_for(pagination_params.merge(:page => current_page.number + 1)) -%> - <% current_url = url_for(pagination_params.merge(:page => current_page.number)) -%> - - <%= loading_indicator_tag :action => :pagination %> - <%= link_to_remote(as_(:previous), - { :url => pagination_params.merge(:page => current_page.number - 1), - :after => "$('#{loading_indicator_id(:action => :pagination)}').style.visibility = 'visible';", - :before => "addActiveScaffoldPageToHistory('#{previous_url}', '#{controller_id}');", - :complete => "$('#{loading_indicator_id(:action => :pagination)}').style.visibility = 'hidden';", - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :update => active_scaffold_content_id, - :method => :get }, - { :href => previous_url, - :class => "previous"}) if current_page.prev? %> - <%= pagination_ajax_links current_page, pagination_params %> - <%= link_to_remote(as_(:next), - { :url => pagination_params.merge(:page => current_page.number + 1), - :after => "$('#{loading_indicator_id(:action => :pagination)}').style.visibility = 'visible';", - :before => "addActiveScaffoldPageToHistory('#{next_url}', '#{controller_id}');", - :complete => "$('#{loading_indicator_id(:action => :pagination)}').style.visibility = 'hidden';", - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :update => active_scaffold_content_id, - :method => :get }, - { :href => next_url, - :class => "next"}) if current_page.next? %> -<% end -%> \ No newline at end of file diff --git a/frontends/default/views/_list_record.html.erb b/frontends/default/views/_list_record.html.erb deleted file mode 100644 index 736b4a39bf..0000000000 --- a/frontends/default/views/_list_record.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -<% -record = list_record if list_record # compat with render :partial :collection -dont_show_calculations ||= false -tr_class = cycle("", "even-record") -tr_class += " #{list_row_class(record)}" if respond_to? :list_row_class -url_options = params_for(:action => :list, :id => record.id) --%> - - - <% active_scaffold_config.list.columns.each do |column| %> - <% column_value = get_column_value(record, column) -%> - - - <%= record.authorized_for?(:action => :read, :column => column.name) ? render_list_column(column_value, column, record) : '' %> - - <% end -%> - <% if active_scaffold_config.action_links.any? {|link| link.type == :record } -%> - - <%= render :partial => 'list_actions', :locals => {:record => record, :url_options => url_options} %> - - <% end -%> - - -<% target_id = element_row_id(:action => :list, :id => record.id) -%> - diff --git a/frontends/default/views/_live_search.html.erb b/frontends/default/views/_live_search.html.erb deleted file mode 100644 index 5b70cfae0c..0000000000 --- a/frontends/default/views/_live_search.html.erb +++ /dev/null @@ -1,25 +0,0 @@ -<% href = url_for(params_for(:action => :update_table, :escape => false).delete_if{|k,v| k == 'search'}) -%> -<%= form_remote_tag :url => href, - :method => :get, - :before => "addActiveScaffoldPageToHistory('#{href}', '#{params[:controller]}')", - :after => "$('#{loading_indicator_id(:action => :search, :id => params[:id])}').style.visibility = 'visible';", - :complete => "$('#{loading_indicator_id(:action => :search, :id => params[:id])}').style.visibility = 'hidden';", - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :update => active_scaffold_content_id, - :html => { :href => href, :id => search_form_id, :class => 'search', :method => :get } %> - - <%= as_(:reset) -%> - <%= loading_indicator_tag(:action => :search) %> - - - diff --git a/frontends/default/views/_messages.html.erb b/frontends/default/views/_messages.html.erb deleted file mode 100644 index f21e14ca41..0000000000 --- a/frontends/default/views/_messages.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% for name in [:info, :warning, :error] %> - <% if flash[name] %> -

" > - <%= h flash[name] %> - <% if request.xhr? %> - - <% end %> -

- <% end %> -<% end %> diff --git a/frontends/default/views/_nested.html.erb b/frontends/default/views/_nested.html.erb deleted file mode 100644 index 20a92a87cc..0000000000 --- a/frontends/default/views/_nested.html.erb +++ /dev/null @@ -1,44 +0,0 @@ -

 

-<% - # TODO: shouldn't this logic happen in the controller action instead of the template? - # Actually, maybe we should make render :active_scaffold work in the controller, and not even have a _nested.rhtml? - - # This assumes that the association is included as a column in the active_scaffold_config.columns collection - associated_columns = [] - associated_columns = params[:associations].split(" ") unless params[:associations].nil? - unless associated_columns.empty? - parent_id = params[:id] - associated_columns.each do | column_name | - # find the column and the association - column = active_scaffold_config.columns[column_name] - association = column.association - - # determine what constraints we need - if column.through_association? - @constraints = { - association.source_reflection.reverse => { - association.through_reflection.reverse => parent_id - } - } - else - @constraints = { association.reverse => parent_id } - end - - # generate the customized label - @label = as_(:nested_for_model, :nested_model => active_scaffold_config_for(association.klass).label, :parent_model => format_value(@record.to_label)) - - begin - controller = active_scaffold_controller_for(association.klass) - rescue ActiveScaffold::ControllerNotFound => error - concat "#{error.class} - #{error.message}" - else - concat render(:active_scaffold => controller.controller_path, - :constraints => @constraints, - :conditions => association.options[:conditions], - :label => h(@label), - :params => {:nested => true, :parent_column => column_name, :parent_model => association.active_record} - ) - end - end - end -%> diff --git a/frontends/default/views/_search.html.erb b/frontends/default/views/_search.html.erb deleted file mode 100644 index 01b05c0e89..0000000000 --- a/frontends/default/views/_search.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<% href = url_for(params_for(:action => :update_table, :escape => false).delete_if{|k,v| k == 'search'}) -%> -<%= form_remote_tag :url => href, - :method => :get, - :before => "addActiveScaffoldPageToHistory('#{href}', '#{params[:controller]}')", - :after => "$('#{loading_indicator_id(:action => :search, :id => params[:id])}').style.visibility = 'visible'; Form.disable('#{search_form_id}');", - :complete => "$('#{loading_indicator_id(:action => :search, :id => params[:id])}').style.visibility = 'hidden'; Form.enable('#{search_form_id}');", - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :update => active_scaffold_content_id, - :html => { :href => href, :id => search_form_id, :class => 'search', :method => :get } %> - - <%= submit_tag as_(:search), :class => "submit" %> - <%= as_(:reset) -%> - <%= loading_indicator_tag(:action => :search) %> - - - \ No newline at end of file diff --git a/frontends/default/views/_show.html.erb b/frontends/default/views/_show.html.erb deleted file mode 100644 index 1c8146ad8c..0000000000 --- a/frontends/default/views/_show.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -

<%= active_scaffold_config.show.label -%>

- -<%= render :partial => 'show_columns', :locals => {:columns => active_scaffold_config.show.columns} -%> - - \ No newline at end of file diff --git a/frontends/default/views/_show_columns.html.erb b/frontends/default/views/_show_columns.html.erb deleted file mode 100644 index e5248d4594..0000000000 --- a/frontends/default/views/_show_columns.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -
-<% columns.each :for => @record do |column| %> - <% if column.is_a? ActiveScaffold::DataStructures::ActionColumns -%> -
<%= column.label -%>
-
<%= render :partial => 'show_columns', :locals => {:columns => column} %>
- <% else -%> -
<%= column.label -%>
-
<%= show_column_value(@record, column) -%>  
- <% end -%> -<% end -%> -
diff --git a/frontends/default/views/_update_actions.html.erb b/frontends/default/views/_update_actions.html.erb deleted file mode 100644 index 392f05b003..0000000000 --- a/frontends/default/views/_update_actions.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -
-
- <% active_scaffold_config.action_links.each :record do |link| -%> - <% next unless link.action == 'nested' -%> - <% next if controller.respond_to? link.security_method and !controller.send(link.security_method) -%> - <%= record.authorized_for?(:action => link.crud_type) ? render_action_link(link, url_options) : "#{link.label}" -%> - <% end -%> -
-
diff --git a/frontends/default/views/_update_form.html.erb b/frontends/default/views/_update_form.html.erb deleted file mode 100644 index a0b3b6467b..0000000000 --- a/frontends/default/views/_update_form.html.erb +++ /dev/null @@ -1,53 +0,0 @@ -<% url_options = params_for(:action => :update) -%> -<%= -if request.xhr? - if active_scaffold_config.update.multipart? # file_uploads - form_remote_upload_tag url_options.merge({:iframe => true}), - :onsubmit => onsubmit, - :id => element_form_id(:action => :update), - :loading => "$('#{loading_indicator_id(:action => :update, :id => params[:id])}').style.visibility = 'visible';", - :class => 'update', - :method => :put - else - form_remote_tag :url => url_options, - :after => "$('#{loading_indicator_id(:action => :update, :id => params[:id])}').style.visibility = 'visible'; Form.disable('#{element_form_id(:action => :update)}');", - :complete => "$('#{loading_indicator_id(:action => :update, :id => params[:id])}').style.visibility = 'hidden'; Form.enable('#{element_form_id(:action => :update)}');", - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :html => { - :href => url_for(url_options), - :onsubmit => onsubmit, - :id => element_form_id(:action => :update), - :class => 'update', - :method => :put - } - end -else - form_tag url_options, - :onsubmit => onsubmit, - :id => element_form_id(:action => :update), - :multipart => active_scaffold_config.update.multipart?, - :class => 'update', - :method => :put -end -%> - -

<%= @record.to_label.nil? ? active_scaffold_config.update.label : as_(:update_model, :model => clean_column_value(@record.to_label)) %>

- - <% if request.xhr? -%> -
<%= error_messages_for :record, :object_name => @record.class.human_name.downcase %>
- <% else -%> - <%= render :partial => 'form_messages' %> - <% end -%> - - <%= render :partial => 'form', :locals => { :columns => active_scaffold_config.update.columns } %> - - - - - diff --git a/frontends/default/views/_vertical_subform.html.erb b/frontends/default/views/_vertical_subform.html.erb deleted file mode 100644 index 7aa4a9b029..0000000000 --- a/frontends/default/views/_vertical_subform.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -
- <% associated.each_index do |index| %> - <% @record = associated[index] -%> - <% if @record.errors.count -%> -
- <%= error_messages_for :record, :object_name => @record.class.human_name.downcase %> -
- <% end %> - <%= render :partial => 'vertical_subform_record', :locals => {:scope => column_scope(column), :parent_record => parent_record, :column => column, :locked => @record.new_record? && @record == associated.last} %> - <% end -%> -
-<%= render :partial => 'form_association_footer', :locals => {:parent_record => parent_record, :column => column, :associated => associated} -%> diff --git a/frontends/default/views/_vertical_subform_record.html.erb b/frontends/default/views/_vertical_subform_record.html.erb deleted file mode 100644 index 4fa47b626d..0000000000 --- a/frontends/default/views/_vertical_subform_record.html.erb +++ /dev/null @@ -1,31 +0,0 @@ -<% - record_column = column - readonly = (@record.readonly? or not @record.authorized_for?(:action => :update)) - action = @record.new_record? ? :create : (readonly ? :read : nil) - show_actions = false --%> -
    -<% active_scaffold_config_for(@record.class).subform.columns.each :for => @record, :action => action, :flatten => true do |column| %> -<% - next unless in_subform?(column, parent_record) - show_actions = true - column = column.clone - column.form_ui ||= :select if column.association --%> -
  1. - <% unless readonly -%> - <%= render :partial => form_partial_for_column(column), :locals => { :column => column, :scope => scope } -%> - <% else -%> -

    <%= get_column_value(@record, column) -%>

    - <% end -%> -
  2. -<% end -%> -<% if show_actions -%> -
  3. - <%= link_to_function as_(:remove), '$(this).up(".association-record").remove()', { :class => "destroy" } if record_column.plural_association? and (@record.authorized_for?(:action => :destroy) or not [:destroy, :delete_all].include?(record_column.association.options[:dependent])) %> - <% unless @record.new_record? %> - " value="<%= @record.id -%>" /> - <% end -%> -
  4. -<% end -%> -
diff --git a/frontends/default/views/add_existing.js.rjs b/frontends/default/views/add_existing.js.rjs deleted file mode 100644 index aaee3cf6f0..0000000000 --- a/frontends/default/views/add_existing.js.rjs +++ /dev/null @@ -1,16 +0,0 @@ -page.insert_html :top, active_scaffold_tbody_id, :partial => 'list_record', :locals => {:record => @record} -page << "ActiveScaffold.stripe($('#{active_scaffold_tbody_id}'))" -page << "ActiveScaffold.hide_empty_message('#{active_scaffold_tbody_id}','#{empty_message_id}');" -page << "ActiveScaffold.increment_record_count('#{active_scaffold_id}');" - -if (form_stays_open = true) - # why not just re-render the form? that wouldn't utilize a possible do_new override which sets default values. - page << "$('#{element_form_id}').reset()" - page.replace_html element_messages_id(:action => :add_existing), :partial => 'form_messages' - # have to delay the focus, because there's no "firstElement" in prototype until at least one element is not disabled - page.delay 0.1 do - page << "Form.focusFirstElement('#{element_form_id}');" - end -else - page << "$$('##{element_form_id(:action => :new_existing)} a.cancel').first().link.close();" -end \ No newline at end of file diff --git a/frontends/default/views/destroy.js.rjs b/frontends/default/views/destroy.js.rjs deleted file mode 100644 index 2baa860a98..0000000000 --- a/frontends/default/views/destroy.js.rjs +++ /dev/null @@ -1,10 +0,0 @@ -if controller.send(:successful?) - page << "$('#{action_link_id((respond_to?(:nested_habtm?) and nested_habtm? and active_scaffold_config.nested.shallow_delete) ? 'destroy_existing' : 'destroy', params[:id])}').action_link.close_previous_adapter();" - page.remove element_row_id(:action => 'list', :id => params[:id]) - page << "ActiveScaffold.reload_if_empty('#{active_scaffold_tbody_id}','#{url_for(params_for(:action => 'update_table', :id => nil, :page => [active_scaffold_config.list.user.page.to_i - 1, 1].max, :escape => false))}');" - page << "ActiveScaffold.stripe('#{active_scaffold_tbody_id}');" - page << "ActiveScaffold.decrement_record_count('#{active_scaffold_id}');" - page.replace active_scaffold_calculations_id, :partial => 'list_calculations' if active_scaffold_config.list.columns.any? {|c| c.calculation?} -end - -page.replace_html active_scaffold_messages_id, :partial => 'messages' diff --git a/frontends/default/views/edit_associated.js.rjs b/frontends/default/views/edit_associated.js.rjs deleted file mode 100644 index 33e4a11d1b..0000000000 --- a/frontends/default/views/edit_associated.js.rjs +++ /dev/null @@ -1,14 +0,0 @@ -associated_form = render :partial => "#{subform_partial_for_column(@column)}_record", :locals => {:scope => @scope, :parent_record => @parent_record, :column => @column, :locked => @record.new_record? && @column.singular_association?} - -if @column.singular_association? - page << %| - associated = #{associated_form.to_json}; - if (current = $$('##{sub_form_list_id(:association => @column.name)} .association-record')[0]) { - Element.replace(current, associated) - } else { - new Insertion.Top('#{sub_form_list_id(:association => @column.name)}', associated) - } - | -else - page.insert_html :bottom, sub_form_list_id(:association => @column.name), associated_form -end diff --git a/frontends/default/views/form_messages.js.rjs b/frontends/default/views/form_messages.js.rjs deleted file mode 100644 index 85478db21a..0000000000 --- a/frontends/default/views/form_messages.js.rjs +++ /dev/null @@ -1 +0,0 @@ -page.replace_html element_messages_id, :partial => 'form_messages' \ No newline at end of file diff --git a/frontends/default/views/form_messages_on_create.js.rjs b/frontends/default/views/form_messages_on_create.js.rjs deleted file mode 100644 index 9bab189da1..0000000000 --- a/frontends/default/views/form_messages_on_create.js.rjs +++ /dev/null @@ -1,2 +0,0 @@ -page.replace_html element_messages_id, :partial => 'form_messages' -page << "$('#{loading_indicator_id(:action => :create, :id => params[:id])}').style.visibility = 'hidden';" diff --git a/frontends/default/views/form_messages_on_update.js.rjs b/frontends/default/views/form_messages_on_update.js.rjs deleted file mode 100644 index 93832992fa..0000000000 --- a/frontends/default/views/form_messages_on_update.js.rjs +++ /dev/null @@ -1,2 +0,0 @@ -page.replace_html element_messages_id, :partial => 'form_messages' -page << "$('#{loading_indicator_id(:action => :update, :id => params[:id])}').style.visibility = 'hidden';" diff --git a/frontends/default/views/list.html.erb b/frontends/default/views/list.html.erb deleted file mode 100644 index 18414d11a7..0000000000 --- a/frontends/default/views/list.html.erb +++ /dev/null @@ -1,44 +0,0 @@ -
<%= active_scaffold_config.theme %>-theme"> -
- <%= render :partial => 'list_header' %> -
- - - <% if active_scaffold_config.list.always_show_search %> - - - - <% end %> - <% if params[:nested].nil? && active_scaffold_config.list.always_show_create %> - - - - <% end %> - -
-
view"> - <%= render :partial => active_scaffold_config.list.search_partial %> -
-
-
view"> - <%= render :partial => 'create_form_on_list' %> -
-
-
- <%= render :partial => 'list' %> -
-
- - diff --git a/frontends/default/views/on_create.js.rjs b/frontends/default/views/on_create.js.rjs deleted file mode 100644 index 81f22f2b46..0000000000 --- a/frontends/default/views/on_create.js.rjs +++ /dev/null @@ -1,25 +0,0 @@ -cancel_selector = "##{element_form_id(:action => :create)} a.cancel".to_json - -if controller.send :successful? - if @insert_row - page.insert_html :top, active_scaffold_tbody_id, :partial => 'list_record', :locals => {:record => @record} - page << "ActiveScaffold.stripe($('#{active_scaffold_tbody_id}'))" - page << "ActiveScaffold.hide_empty_message('#{active_scaffold_tbody_id}','#{empty_message_id}');" - page << "ActiveScaffold.increment_record_count('#{active_scaffold_id}');" - end - - if (active_scaffold_config.create.persistent) - page << "$$(#{cancel_selector}).first().link.reload();" - else - page << "$$(#{cancel_selector}).first().link.close#{'_with_refresh' unless @insert_row}();" - end - if (active_scaffold_config.create.edit_after_create) - page << "var link = $('#{action_link_id 'edit', @record.id}');" - page << "if (link) (function() { link.action_link.open() }).defer();" - end -else - page << "l = $$(#{cancel_selector}).first().link;" - page.replace element_form_id(:action => :create), :partial => 'create_form' - page << "l.register_cancel_hooks();" -end -page.replace_html active_scaffold_messages_id, :partial => 'messages' \ No newline at end of file diff --git a/frontends/default/views/on_update.js.rjs b/frontends/default/views/on_update.js.rjs deleted file mode 100644 index fcd31779bc..0000000000 --- a/frontends/default/views/on_update.js.rjs +++ /dev/null @@ -1,11 +0,0 @@ -cancel_selector = "##{element_form_id(:action => :update)} a.cancel".to_json - -if controller.send :successful? - page << "$$(#{cancel_selector}).first().link.close_with_refresh();" - page.replace active_scaffold_calculations_id, :partial => 'list_calculations' if active_scaffold_config.list.columns.any? {|c| c.calculation?} -else - page << "l = $$(#{cancel_selector}).first().link;" - page.replace element_form_id(:action => :update), :partial => 'update_form' - page << "l.register_cancel_hooks();" -end -page.replace_html active_scaffold_messages_id, :partial => 'messages' \ No newline at end of file diff --git a/frontends/default/views/render_field.js.rjs b/frontends/default/views/render_field.js.rjs deleted file mode 100644 index c67d81814b..0000000000 --- a/frontends/default/views/render_field.js.rjs +++ /dev/null @@ -1,2 +0,0 @@ -field_id = active_scaffold_input_options(@update_column, params[:scope])[:id] -page[field_id].up('li').replace_html :partial => form_partial_for_column(@update_column), :locals => { :column => @update_column, :scope => params[:scope] } diff --git a/frontends/default/views/search.html.erb b/frontends/default/views/search.html.erb deleted file mode 100644 index 6ed3901b86..0000000000 --- a/frontends/default/views/search.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -
-
view"> - <%= render :partial => 'search' -%> -
-
\ No newline at end of file diff --git a/frontends/default/views/show.html.erb b/frontends/default/views/show.html.erb deleted file mode 100644 index 0b36a919b7..0000000000 --- a/frontends/default/views/show.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -
-
view"> - <%= render :partial => 'show' -%> -
-
\ No newline at end of file diff --git a/frontends/default/views/update_column.js.rjs b/frontends/default/views/update_column.js.rjs deleted file mode 100644 index ed642b3adf..0000000000 --- a/frontends/default/views/update_column.js.rjs +++ /dev/null @@ -1,12 +0,0 @@ -column_span_id = element_cell_id(:id => @record.id.to_s, :action => 'update_column', :name => params[:column]) -unless controller.send :successful? - page.alert(@record.errors.full_messages(active_scaffold_config).join("\n")) - @record.reload -end -column = active_scaffold_config.columns[params[:column]] -if column.inplace_edit - page.replace_html(column_span_id,format_inplace_edit_column(@record, column)) -else - formatted_value = get_column_value(@record, column) - page.replace_html(column_span_id, formatted_value) -end \ No newline at end of file diff --git a/init.rb b/init.rb index 019d73a582..dfc70336b2 100755 --- a/init.rb +++ b/init.rb @@ -1,18 +1,4 @@ -## -## Initialize the environment -## -unless Rails::VERSION::MAJOR == 2 && Rails::VERSION::MINOR >= 3 - raise "This version of ActiveScaffold requires Rails 2.3 or higher. Please use an earlier version." -end +ACTIVE_SCAFFOLD_PLUGIN = true +require 'active_scaffold' +ActiveSupport.run_load_hooks(:active_scaffold, ActiveScaffold) -require File.dirname(__FILE__) + '/environment' - -## -## Run the install assets script, too, just to make sure -## But at least rescue the action in production -## -begin - require File.dirname(__FILE__) + '/install_assets' -rescue - raise $! unless Rails.env == 'production' -end diff --git a/install.rb b/install.rb deleted file mode 100644 index 6aec94c5b2..0000000000 --- a/install.rb +++ /dev/null @@ -1,39 +0,0 @@ -## -## Install ActiveScaffold assets into /public -## - -require File.dirname(__FILE__) + '/install_assets' - -## -## Install Counter -## -# -# What's going on here? -# We're incrementing a web counter so we can track SVN installs of ActiveScaffold -# -# How? -# We're making a GET request to errcount.com to update a simple counter. No data is transmitted. -# -# Why? -# So we can know how many people are using ActiveScaffold and modulate our level of effort accordingly. -# Despite numerous pleas our Googly overlords still only provide us with download stats for the zip distro. -# -# *Thanks for your understanding* -# - -class ErrCounter # using errcount.com - require "net/http" - - @@ACCOUNT_ID = 341 - @@SITE_DOMAIN = 'installs.activescaffold.com' - - def self.increment - @http = Net::HTTP.new("errcount.com") - resp, data = @http.get2("/ctr/#{@@ACCOUNT_ID}.js", {'Referer' => @@SITE_DOMAIN}) - end -end - -begin - ErrCounter.increment -rescue -end diff --git a/install_assets.rb b/install_assets.rb deleted file mode 100755 index 37b30063d5..0000000000 --- a/install_assets.rb +++ /dev/null @@ -1,36 +0,0 @@ -# Workaround a problem with script/plugin and http-based repos. -# See http://dev.rubyonrails.org/ticket/8189 -Dir.chdir(Dir.getwd.sub(/vendor.*/, '')) do - -## -## Copy over asset files (javascript/css/images) from the plugin directory to public/ -## - -def copy_files(source_path, destination_path, directory) - source, destination = File.join(directory, source_path), File.join(Rails.root, destination_path) - FileUtils.mkdir(destination) unless File.exist?(destination) - FileUtils.cp_r(Dir.glob(source+'/*.*'), destination) -end - -directory = File.dirname(__FILE__) - -copy_files("/public", "/public", directory) - -available_frontends = Dir[File.join(directory, 'frontends', '*')].collect { |d| File.basename d } -[ :stylesheets, :javascripts, :images].each do |asset_type| - path = "/public/#{asset_type}/active_scaffold" - copy_files(path, path, directory) - - File.open(File.join(Rails.root, path, 'DO_NOT_EDIT'), 'w') do |f| - f.puts "Any changes made to files in sub-folders will be lost." - f.puts "See http://activescaffold.com/tutorials/faq#custom-css." - end - - available_frontends.each do |frontend| - source = "/frontends/#{frontend}/#{asset_type}/" - destination = "/public/#{asset_type}/active_scaffold/#{frontend}" - copy_files(source, destination, directory) - end -end - -end \ No newline at end of file diff --git a/lib/active_record_permissions.rb b/lib/active_record_permissions.rb deleted file mode 100644 index fa8e6e767b..0000000000 --- a/lib/active_record_permissions.rb +++ /dev/null @@ -1,128 +0,0 @@ -# This module attempts to create permissions conventions for your ActiveRecord models. It supports english-based -# methods that let you restrict access per-model, per-record, per-column, per-action, and per-user. All at once. -# -# You may define instance methods in the following formats: -# def #{column}_authorized_for_#{action}? -# def #{column}_authorized? -# def authorized_for_#{action}? -# -# Your methods should allow for the following special cases: -# * cron scripts -# * guest users (or nil current_user objects) -module ActiveRecordPermissions - # ActiveRecordPermissions needs to know what method on your ApplicationController will return the current user, - # if available. This defaults to the :current_user method. You may configure this in your environment.rb if you - # have a different setup. - def self.current_user_method=(v); @@current_user_method = v; end - def self.current_user_method; @@current_user_method; end - @@current_user_method = :current_user - - # Whether the default permission is permissive or not - # If set to true, then everything's allowed until configured otherwise - def self.default_permission=(v); @@default_permission = v; end - def self.default_permission; @@default_permission; end - @@default_permission = true - - # This is a module aimed at making the current_user available to ActiveRecord models for permissions. - module ModelUserAccess - module Controller - def self.included(base) - base.prepend_before_filter :assign_current_user_to_models - end - - # We need to give the ActiveRecord classes a handle to the current user. We don't want to just pass the object, - # because the object may change (someone may log in or out). So we give ActiveRecord a proc that ties to the - # current_user_method on this ApplicationController. - def assign_current_user_to_models - ActiveRecord::Base.current_user_proc = proc {send(ActiveRecordPermissions.current_user_method)} - end - end - - module Model - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods - # The proc to call that retrieves the current_user from the ApplicationController. - attr_accessor :current_user_proc - - # Class-level access to the current user - def current_user - ActiveRecord::Base.current_user_proc.call if ActiveRecord::Base.current_user_proc - end - end - - # Instance-level access to the current user - def current_user - self.class.current_user - end - end - end - - module Permissions - def self.included(base) - base.extend ClassMethods - end - - # A generic authorization query. This is what will be called programatically, since - # the actual permission methods can't be guaranteed to exist. And because we want to - # intelligently combine multiple applicable methods. - # - # options[:action] should be a CRUD verb (:create, :read, :update, :destroy) - # options[:column] should be the name of a model attribute - def authorized_for?(options = {}) - raise ArgumentError, "unknown action #{options[:action]}" if options[:action] and ![:create, :read, :update, :destroy].include?(options[:action]) - - # column_authorized_for_action? has priority over other methods, - # you can disable an action and enable that action for a column - # (for example, disable update and enable inplace_edit in a column) - method = column_and_action_security_method(options[:column], options[:action]) - return send(method) if method and respond_to?(method) - - # collect the possibly-related methods that actually exist - methods = [ - column_security_method(options[:column]), - action_security_method(options[:action]), - ].compact.select {|m| respond_to?(m)} - - # if any method returns false, then return false - return false if methods.any? {|m| !send(m)} - - # if any method actually exists then it must've returned true, so return true - return true unless methods.empty? - - # if no method exists, return the default permission - return ActiveRecordPermissions.default_permission - end - - # Because any class-level queries get delegated to the instance level via a new record, - # it's useful to know when the authorization query is meant for a specific record or not. - # But using new_record? is confusing, even though accurate. So this is basically just a wrapper. - def existing_record_check? - !new_record? - end - - module ClassMethods - # Class level just delegates to instance level - def authorized_for?(*args) - @authorized_for_delegatee ||= self.new - @authorized_for_delegatee.authorized_for?(*args) - end - end - - private - - def column_security_method(column) - "#{column}_authorized?" if column - end - - def action_security_method(action) - "authorized_for_#{action}?" if action - end - - def column_and_action_security_method(column, action) - "#{column}_authorized_for_#{action}?" if column and action - end - end -end diff --git a/lib/active_scaffold.rb b/lib/active_scaffold.rb index c7b501dced..e346e24a08 100644 --- a/lib/active_scaffold.rb +++ b/lib/active_scaffold.rb @@ -1,4 +1,58 @@ +unless Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR >= 1 + raise "This version of ActiveScaffold requires Rails 3.1 or higher. Please use an earlier version." +end + +begin + require 'render_component' +rescue LoadError +end + +require 'active_scaffold/active_record_permissions' +require 'active_scaffold/paginator' +require 'active_scaffold/responds_to_parent' + +require 'active_scaffold/version' +require 'active_scaffold/engine' unless defined? ACTIVE_SCAFFOLD_PLUGIN +require 'json' # for js_config + module ActiveScaffold + autoload :AttributeParams, 'active_scaffold/attribute_params' + autoload :Configurable, 'active_scaffold/configurable' + autoload :Constraints, 'active_scaffold/constraints' + autoload :Finder, 'active_scaffold/finder' + autoload :MarkedModel, 'active_scaffold/marked_model' + autoload :Bridges, 'active_scaffold/bridges' + + mattr_accessor :stylesheets + self.stylesheets = [] + mattr_accessor :javascripts + self.javascripts = [] + + def self.autoload_subdir(dir, mod=self, root = File.dirname(__FILE__)) + Dir["#{root}/active_scaffold/#{dir}/*.rb"].each { |file| + basename = File.basename(file, ".rb") + mod.module_eval { + autoload basename.camelcase.to_sym, "active_scaffold/#{dir}/#{basename}" + } + } + end + + module Actions + ActiveScaffold.autoload_subdir('actions', self) + end + + module Config + ActiveScaffold.autoload_subdir('config', self) + end + + module DataStructures + ActiveScaffold.autoload_subdir('data_structures', self) + end + + module Helpers + ActiveScaffold.autoload_subdir('helpers', self) + end + class ControllerNotFound < RuntimeError; end class DependencyFailure < RuntimeError; end class MalformedConstraint < RuntimeError; end @@ -11,13 +65,18 @@ def self.included(base) base.module_eval do # TODO: these should be in actions/core before_filter :handle_user_settings + before_filter :check_input_device end + + base.helper_method :touch_device? + base.helper_method :hover_via_click? + base.helper_method :active_scaffold_constraints end def self.set_defaults(&block) ActiveScaffold::Config::Core.configure &block end - + def active_scaffold_config self.class.active_scaffold_config end @@ -26,57 +85,113 @@ def active_scaffold_config_for(klass) self.class.active_scaffold_config_for(klass) end - def active_scaffold_session_storage - id = params[:eid] || params[:controller] - session_index = "as:#{id}" + def active_scaffold_session_storage_key(id = nil) + id ||= params[:eid] || "#{params[:controller]}#{"_#{nested.parent_id}" if nested?}" + "as:#{id}" + end + + def active_scaffold_session_storage(id = nil) + session_index = active_scaffold_session_storage_key(id) session[session_index] ||= {} session[session_index] end + def clear_storage + session_index = active_scaffold_session_storage_key + session.delete(session_index) unless session[session_index].present? + end + # at some point we need to pass the session and params into config. we'll just take care of that before any particular action occurs by passing those hashes off to the UserSettings class of each action. def handle_user_settings if self.class.uses_active_scaffold? active_scaffold_config.actions.each do |action_name| conf_instance = active_scaffold_config.send(action_name) rescue next next if conf_instance.class::UserSettings == ActiveScaffold::Config::Base::UserSettings # if it hasn't been extended, skip it - active_scaffold_session_storage[action_name] ||= {} - conf_instance.user = conf_instance.class::UserSettings.new(conf_instance, active_scaffold_session_storage[action_name], params) + storage = active_scaffold_config.store_user_settings ? active_scaffold_session_storage : {} + conf_instance.user = conf_instance.class::UserSettings.new(conf_instance, storage, params) end end end + def check_input_device + if request.env["HTTP_USER_AGENT"] && request.env["HTTP_USER_AGENT"][/(iPhone|iPod|iPad)/i] + session[:input_device_type] = 'TOUCH' + session[:hover_supported] = false + else + session[:input_device_type] = 'MOUSE' + session[:hover_supported] = true + end if session[:input_device_type].nil? + end + + def touch_device? + session[:input_device_type] == 'TOUCH' + end + + def hover_via_click? + session[:hover_supported] == false + end + + def self.js_framework=(framework) + @@js_framework = framework + end + + def self.js_framework + @@js_framework ||= if defined? Jquery + :jquery + elsif defined? PrototypeRails + :prototype + end + end + + def self.js_config=(config) + @@js_config = config + end + + def self.js_config + @@js_config ||= {:scroll_on_close => :checkInViewport} + end + + # exclude bridges you do not need + # name of bridge subdir should be used to exclude it + # eg + # ActiveScaffold.exclude_bridges = [:cancan, :ancestry] + # if you are using Activescaffold as a gem add to initializer + # if you are using Activescaffold as a plugin add to active_scaffold_env.rb + def self.exclude_bridges=(bridges) + @@exclude_bridges = bridges + end + + def self.exclude_bridges + @@exclude_bridges ||= [] + end + + def self.root + File.dirname(__FILE__) + "/.." + end + module ClassMethods def active_scaffold(model_id = nil, &block) + extend Prefixes # initialize bridges here - ActiveScaffold::Bridge.run_all + ActiveScaffold::Bridges.run_all # converts Foo::BarController to 'bar' and FooBarsController to 'foo_bar' and AddressController to 'address' model_id = self.to_s.split('::').last.sub(/Controller$/, '').pluralize.singularize.underscore unless model_id # run the configuration @active_scaffold_config = ActiveScaffold::Config::Core.new(model_id) + @active_scaffold_config_block = block + self.links_for_associations + + self.active_scaffold_superclasses_blocks.each {|superblock| self.active_scaffold_config.configure &superblock} + self.active_scaffold_config.sti_children = nil # reset sti_children if set in parent block self.active_scaffold_config.configure &block if block_given? + self.active_scaffold_config._configure_sti unless self.active_scaffold_config.sti_children.nil? self.active_scaffold_config._load_action_columns - self.links_for_associations # defines the attribute read methods on the model, so record.send() doesn't find protected/private methods instead klass = self.active_scaffold_config.model - klass.define_attribute_methods unless klass.generated_methods? - - @active_scaffold_overrides = [] - ActionController::Base.view_paths.each do |dir| - active_scaffold_overrides_dir = File.join(dir,"active_scaffold_overrides") - @active_scaffold_overrides << active_scaffold_overrides_dir if File.exists?(active_scaffold_overrides_dir) - end - @active_scaffold_frontends = [] - if active_scaffold_config.frontend.to_sym != :default - active_scaffold_custom_frontend_path = File.join(Rails.root, 'vendor', 'plugins', ActiveScaffold::Config::Core.plugin_directory, 'frontends', active_scaffold_config.frontend.to_s , 'views') - @active_scaffold_frontends << active_scaffold_custom_frontend_path - end - active_scaffold_default_frontend_path = File.join(Rails.root, 'vendor', 'plugins', ActiveScaffold::Config::Core.plugin_directory, 'frontends', 'default' , 'views') - @active_scaffold_frontends << active_scaffold_default_frontend_path - @active_scaffold_custom_paths = [] - + klass.define_attribute_methods unless klass.attribute_methods_generated? # include the rest of the code into the controller: the action core and the included actions module_eval do include ActiveScaffold::Finder @@ -89,51 +204,123 @@ def active_scaffold(model_id = nil, &block) # sneak the action links from the actions into the main set if link = active_scaffold_config.send(mod).link rescue nil - active_scaffold_config.action_links << link + if link.is_a? Array + link.each {|current| active_scaffold_config.action_links.add_to_group(current, active_scaffold_config.send(mod).action_group)} + elsif link.is_a? ActiveScaffold::DataStructures::ActionLink + active_scaffold_config.action_links.add_to_group(link, active_scaffold_config.send(mod).action_group) + end end end end + self._add_sti_create_links if self.active_scaffold_config.add_sti_create_links? + end + + module Prefixes + def parent_prefixes + @parent_prefixes ||= super << 'active_scaffold_overrides' + end + end + + # To be called after include action modules + def _add_sti_create_links + new_action_link = active_scaffold_config.action_links.collection['new'] + unless new_action_link.nil? || active_scaffold_config.sti_children.empty? + active_scaffold_config.action_links.collection.delete('new') + active_scaffold_config.sti_children.each do |child| + new_sti_link = Marshal.load(Marshal.dump(new_action_link)) # deep clone + new_sti_link.label = as_(:create_model, :model => child.to_s.camelize.constantize.model_name.human) + new_sti_link.parameters = {:parent_sti => controller_path} + new_sti_link.controller = Proc.new { active_scaffold_controller_for(child.to_s.camelize.constantize).controller_path } + active_scaffold_config.action_links.collection.create.add(new_sti_link) + end + end end # Create the automatic column links. Note that this has to happen when configuration is *done*, because otherwise the Nested module could be disabled. Actually, it could still be disabled later, couldn't it? def links_for_associations return unless active_scaffold_config.actions.include? :list and active_scaffold_config.actions.include? :nested - active_scaffold_config.list.columns.each do |column| - next unless column.link.nil? and column.autolink + active_scaffold_config.columns.each do |column| + next unless column.link.nil? and column.autolink? + #lazy load of action_link, cause it was really slowing down app in dev mode + #and might lead to trouble cause of cyclic constantization of controllers + #and might be unnecessary cause it is done before columns are configured + column.set_link(Proc.new {|col| link_for_association(col)}) + end + end + + def active_scaffold_controller_for_column(column, options = {}) + begin + if column.polymorphic_association? + :polymorph + elsif options.include?(:controller) + "#{options[:controller].to_s.camelize}Controller".constantize + else + active_scaffold_controller_for(column.association.klass) + end + rescue ActiveScaffold::ControllerNotFound + nil + end + end + + def link_for_association(column, options = {}) + controller = active_scaffold_controller_for_column(column, options) + + unless controller.nil? + options.reverse_merge! :position => :after, :type => :member, :controller => (controller == :polymorph ? controller : controller.controller_path), :column => column + options[:parameters] ||= {} + options[:parameters].reverse_merge! :association => column.association.name if column.plural_association? # note: we can't create nested scaffolds on :through associations because there's no reverse association. - column.set_link('nested', :parameters => {:associations => column.name.to_sym}) #unless column.through_association? - elsif column.polymorphic_association? - # note: we can't create inline forms on singular polymorphic associations - column.clear_link + + ActiveScaffold::DataStructures::ActionLink.new('index', options.merge(:refresh_on_close => true)) #unless column.through_association? else - model = column.association.klass - begin - controller = active_scaffold_controller_for(model) - rescue ActiveScaffold::ControllerNotFound - next - end - - actions = controller.active_scaffold_config.actions + actions = controller.active_scaffold_config.actions unless controller == :polymorph + actions ||= [:create, :update, :show] column.actions_for_association_links.delete :new unless actions.include? :create column.actions_for_association_links.delete :edit unless actions.include? :update column.actions_for_association_links.delete :show unless actions.include? :show - column.set_link(:none, :controller => controller.controller_path, :crud_type => nil) - end + ActiveScaffold::DataStructures::ActionLink.new(nil, options.merge(:html_options => {:class => column.name})) + end end end + + def link_for_association_as_scope(scope, options = {}) + options.reverse_merge! :label => scope, :position => :after, :type => :member, :controller => controller_path + options[:parameters] ||= {} + options[:parameters].reverse_merge! :named_scope => scope + ActiveScaffold::DataStructures::ActionLink.new('index', options) + end def add_active_scaffold_path(path) - @active_scaffold_paths = nil # Force active_scaffold_paths to rebuild - @active_scaffold_custom_paths << path + as_path = File.join(ActiveScaffold::Config::Core.plugin_directory, 'app', 'views') + index = view_paths.find_index { |p| p.to_s == as_path } + if index + self.view_paths = view_paths[0..index-1] + Array(path) + view_paths[index..-1] + else + append_view_path path + end end - def active_scaffold_paths - @active_scaffold_paths ||= ActionView::PathSet.new(@active_scaffold_overrides + @active_scaffold_custom_paths + @active_scaffold_frontends) unless @active_scaffold_overrides.nil? || @active_scaffold_custom_paths.nil? || @active_scaffold_frontends.nil? + def active_scaffold_config + if @active_scaffold_config.nil? + self.superclass.active_scaffold_config if self.superclass.respond_to? :active_scaffold_config + else + @active_scaffold_config + end end - def active_scaffold_config - @active_scaffold_config || self.superclass.instance_variable_get('@active_scaffold_config') + def active_scaffold_config_block + @active_scaffold_config_block + end + + def active_scaffold_superclasses_blocks + blocks = [] + klass = self.superclass + while klass.respond_to? :active_scaffold_superclasses_blocks + blocks << klass.active_scaffold_config_block + klass = klass.superclass + end + blocks.compact.reverse end def active_scaffold_config_for(klass) @@ -152,23 +339,25 @@ def active_scaffold_config_for(klass) # Searches in the namespace of the current controller for singular and plural versions of the conventional "#{model}Controller" syntax. # You may override this method to customize the search routine. def active_scaffold_controller_for(klass) - namespace = self.to_s.split('::')[0...-1].join('::') + '::' + controller_namespace = self.to_s.split('::')[0...-1].join('::') + '::' error_message = [] - ["#{klass.to_s.underscore.pluralize}", "#{klass.to_s.underscore.pluralize.singularize}"].each do |controller_name| - begin - controller = "#{namespace}#{controller_name.camelize}Controller".constantize - rescue NameError => error - # Only rescue NameError associated with the controller constant not existing - not other compile errors - if error.message["uninitialized constant #{controller}"] - error_message << "#{namespace}#{controller_name.camelize}Controller" - next - else - raise + [controller_namespace, ''].each do |namespace| + ["#{klass.to_s.underscore.pluralize}", "#{klass.to_s.underscore.pluralize.singularize}"].each do |controller_name| + begin + controller = "#{namespace}#{controller_name.camelize}Controller".constantize + rescue NameError => error + # Only rescue NameError associated with the controller constant not existing - not other compile errors + if error.message["uninitialized constant #{controller}"] + error_message << "#{namespace}#{controller_name.camelize}Controller" + next + else + raise + end end + raise ActiveScaffold::ControllerNotFound, "#{controller} missing ActiveScaffold", caller unless controller.uses_active_scaffold? + raise ActiveScaffold::ControllerNotFound, "ActiveScaffold on #{controller} is not for #{klass} model.", caller unless controller.active_scaffold_config.model.to_s == klass.to_s + return controller end - raise ActiveScaffold::ControllerNotFound, "#{controller} missing ActiveScaffold", caller unless controller.uses_active_scaffold? - raise ActiveScaffold::ControllerNotFound, "ActiveScaffold on #{controller} is not for #{klass} model.", caller unless controller.active_scaffold_config.model == klass - return controller end raise ActiveScaffold::ControllerNotFound, "Could not find " + error_message.join(" or "), caller end @@ -178,3 +367,5 @@ def uses_active_scaffold? end end end + +require 'active_scaffold_env' diff --git a/lib/active_scaffold/actions/common_search.rb b/lib/active_scaffold/actions/common_search.rb new file mode 100644 index 0000000000..4f998c3d5f --- /dev/null +++ b/lib/active_scaffold/actions/common_search.rb @@ -0,0 +1,26 @@ +module ActiveScaffold::Actions + module CommonSearch + protected + def store_search_params_into_session + if active_scaffold_config.store_user_settings + active_scaffold_session_storage[:search] = params.delete :search if params[:search] + else + @search_params = params.delete :search + end + end + + def search_params + @search_params || active_scaffold_session_storage[:search] + end + + def search_ignore? + active_scaffold_config.list.always_show_search + end + + # The default security delegates to ActiveRecordPermissions. + # You may override the method to customize. + def search_authorized? + authorized_for?(:crud_type => :read) + end + end +end diff --git a/lib/active_scaffold/actions/core.rb b/lib/active_scaffold/actions/core.rb index 982c8882e6..f5f5dd24ef 100644 --- a/lib/active_scaffold/actions/core.rb +++ b/lib/active_scaffold/actions/core.rb @@ -2,35 +2,88 @@ module ActiveScaffold::Actions module Core def self.included(base) base.class_eval do + prepend_before_filter :register_constraints_with_action_columns, :unless => :nested? after_filter :clear_flashes + after_filter :clear_storage + rescue_from ActiveScaffold::RecordNotAllowed, ActiveScaffold::ActionNotAllowed, :with => :deny_access end + base.helper_method :successful? + base.helper_method :nested? + base.helper_method :calculate_query + base.helper_method :new_model end def render_field - @record = active_scaffold_config.model.new - column = active_scaffold_config.columns[params[:column]] - value = if column.association - params[:value].blank? ? nil : column.association.klass.find(params[:value]) + if request.get? + render_field_for_inplace_editing else - params[:value] + render_field_for_update_columns end - @record.send "#{column.name}=", value - @update_column = active_scaffold_config.columns[column.options[:update_column]] end - + protected + def embedded? + @embedded ||= params.delete(:embedded) + end - def authorized_for?(*args) - active_scaffold_config.model.authorized_for?(*args) + def nested? + false end - def clear_flashes - if request.xhr? - flash.keys.each do |flash_key| - flash[flash_key] = nil + def render_field_for_inplace_editing + @column = active_scaffold_config.columns[params[:update_column]] + @record = find_if_allowed(params[:id], :crud_type => :update, :column => params[:update_column]) + render :action => 'render_field_inplace', :layout => false + end + + def render_field_for_update_columns + @column = active_scaffold_config.columns[params.delete(:column)] + unless @column.nil? + @source_id = params.delete(:source_id) + @columns = @column.update_columns + @scope = params.delete(:scope) + @main_columns = active_scaffold_config.send(@scope ? :subform : (params[:id] ? :update : :create)).columns + + if @column.send_form_on_update_column + if @scope + hash = @scope.gsub('[','').split(']').inject(params[:record]) do |hash, index| + hash[index] + end + id = hash[:id] + else + hash = params[:record] + id = params[:id] + end + @record = id ? find_if_allowed(id, :update) : new_model + @record = update_record_from_params(@record, @main_columns, hash) + else + @record = new_model + value = column_value_from_param_value(@record, @column, params.delete(:value)) + @record.send "#{@column.name}=", value end + + after_render_field(@record, @column) end end + + # override this method if you want to do something after render_field + def after_render_field(record, column); end + + def authorized_for?(options = {}) + active_scaffold_config.model.authorized_for?(options) + end + + def clear_flashes + flash.clear if request.xhr? + end + + def each_marked_record(&block) + active_scaffold_config.model.find(marked_records.to_a).each &block + end + def marked_records + active_scaffold_session_storage[:marked_records] ||= Set.new + end + def default_formats [:html, :js, :json, :xml, :yaml] end @@ -51,7 +104,11 @@ def accepts?(*types) end def response_status - successful? ? 200 : 500 + if successful? + action_name == 'create' ? 201 : 200 + else + 422 + end end # API response object that will be converted to XML/YAML/JSON using to_xxx @@ -59,30 +116,23 @@ def response_object @response_object = successful? ? (@record || @records) : @record.errors end - # Success is the existence of certain variables and the absence of errors (when applicable). - # Success can also be defined. + # Success is the existence of one or more model objects. Most actions + # circumvent this method by setting @success directly. def successful? if @successful.nil? - @records or (@record and @record.errors.count == 0 and @record.no_errors_in_associated?) + true else @successful end end - + def successful=(val) @successful = (val) ? true : false end # Redirect to the main page (override if the ActiveScaffold is used as a component on another controllers page) for Javascript degradation def return_to_main - unless params[:parent_controller].nil? - params[:controller] = params[:parent_controller] - params[:eid] = nil - params[:parent_model] = nil - params[:parent_column] = nil - params[:parent_id] = nil - end - redirect_to params_for(:action => "index") + redirect_to main_path_to_return end # Override this method on your controller to define conditions to be used when querying a recordset (e.g. for List). The return of this method should be any format compatible with the :conditions clause of ActiveRecord::Base's find. @@ -98,37 +148,57 @@ def custom_finder_options {} end - + #Overide this method on your controller to provide model with named scopes + def beginning_of_chain + active_scaffold_config.model + end + # Builds search conditions by search params for column names. This allows urls like "contacts/list?company_id=5". def conditions_from_params - conditions = nil - params.reject {|key, value| [:controller, :action, :id].include?(key.to_sym)}.each do |key, value| - next unless active_scaffold_config.model.column_names.include?(key) - if value.is_a?(Array) - conditions = merge_conditions(conditions, ["#{active_scaffold_config.model.table_name}.#{key.to_s} in (?)", value]) - else - conditions = merge_conditions(conditions, ["#{active_scaffold_config.model.table_name}.#{key.to_s} = ?", value]) + @conditions_from_params ||= begin + conditions = {} + params.except(:controller, :action, :page, :sort, :sort_direction).each do |key, value| + column = active_scaffold_config.model.columns_hash[key.to_s] + key = key.to_sym + next unless column + next if active_scaffold_constraints[key] + next if nested? and nested.param_name == key + conditions[key] = column.type_cast(value) end + conditions + end + end + + def new_model + model = beginning_of_chain + if nested? && nested.association && nested.association.collection? && model.columns_hash[column = model.inheritance_column] + model_name = params.delete(column) # in new action inheritance_column must be in params + model_name ||= params[:record].delete(column) unless params[:record].blank? # in create action must be inside record key + model_name = model_name.camelize if model_name + model_name ||= active_scaffold_config.model.name + build_options = {column.to_sym => model_name} if model_name end - conditions + model.respond_to?(:build) ? model.build(build_options || {}) : model.new end + private def respond_to_action(action) respond_to do |type| - send("#{action}_formats").each do |format| - type.send(format){ send("#{action}_respond_to_#{format}") } + action_formats.each do |format| + type.send(format) do + if respond_to?(method_name = "#{action}_respond_to_#{format}", true) + send(method_name) + end + end end end end - def response_code_for_rescue(exception) - case exception - when ActiveScaffold::RecordNotAllowed - "403 Record Not Allowed" - when ActiveScaffold::ActionNotAllowed - "403 Action Not Allowed" - else - super + def action_formats + @action_formats ||= if respond_to? "#{action_name}_formats", true + send("#{action_name}_formats") + else + (default_formats + active_scaffold_config.formats).uniq end end end diff --git a/lib/active_scaffold/actions/create.rb b/lib/active_scaffold/actions/create.rb index c4bc5b9b90..51a46cefee 100644 --- a/lib/active_scaffold/actions/create.rb +++ b/lib/active_scaffold/actions/create.rb @@ -2,10 +2,6 @@ module ActiveScaffold::Actions module Create def self.included(base) base.before_filter :create_authorized_filter, :only => [:new, :create] - base.prepend_before_filter :constraints_for_nested_create, :only => [:new, :create] - base.verify :method => :post, - :only => :create, - :redirect_to => { :action => :index } end def new @@ -15,11 +11,14 @@ def new def create do_create - @insert_row = params[:parent_controller].nil? respond_to_action(:create) end protected + def response_location + url_for(params_for(:action => "show", :id => @record.id)) if successful? + end + def new_respond_to_html if successful? render(:action => 'create') @@ -34,25 +33,23 @@ def new_respond_to_js def create_respond_to_html if params[:iframe]=='true' # was this an iframe post ? + do_refresh_list if successful? && active_scaffold_config.create.refresh_list && !render_parent? responds_to_parent do - if successful? - render :action => 'on_create.js' - else - render :action => 'form_messages_on_create.js' - end + render :action => 'on_create', :formats => [:js], :layout => false end else if successful? flash[:info] = as_(:created_model, :model => @record.to_label) - if active_scaffold_config.create.edit_after_create - redirect_to params.merge(:action => "edit", :id => @record.id) + if action = active_scaffold_config.create.action_after_create + redirect_to params_for(:action => action, :id => @record.id) + elsif active_scaffold_config.create.persistent + redirect_to params_for(:action => "new") else return_to_main end else - if params[:nested].nil? && active_scaffold_config.actions.include?(:list) && active_scaffold_config.list.always_show_create - do_list - render(:action => 'list') + if active_scaffold_config.actions.include?(:list) && active_scaffold_config.list.always_show_create + list else render(:action => 'create') end @@ -61,56 +58,54 @@ def create_respond_to_html end def create_respond_to_js - render :action => 'on_create' + do_refresh_list if successful? && active_scaffold_config.create.refresh_list && !render_parent? + render :action => 'on_create', :content_type => Mime::JS, :layout => false end def create_respond_to_xml - render :xml => response_object.to_xml, :content_type => Mime::XML, :status => response_status + render :xml => response_object.to_xml(:only => active_scaffold_config.create.columns.names), :content_type => Mime::XML, :status => response_status, :location => response_location end def create_respond_to_json - render :text => response_object.to_json, :content_type => Mime::JSON, :status => response_status + render :text => response_object.to_json(:only => active_scaffold_config.create.columns.names), :content_type => Mime::JSON, :status => response_status, :location => response_location end def create_respond_to_yaml - render :text => response_object.to_yaml, :content_type => Mime::YAML, :status => response_status - end - - def constraints_for_nested_create - if params[:parent_column] && params[:parent_id] - @old_eid = params[:eid] - @remove_eid = true - constraints = {params[:parent_column].to_sym => params[:parent_id]} - params[:eid] = Digest::MD5.hexdigest(params[:parent_controller] + params[:controller].to_s + constraints.to_s) - session["as:#{params[:eid]}"] = {:constraints => constraints} - end + render :text => Hash.from_xml(response_object.to_xml(:only => active_scaffold_config.create.columns.names)).to_yaml, :content_type => Mime::YAML, :status => response_status, :location => response_location end # A simple method to find and prepare an example new record for the form # May be overridden to customize the behavior (add default values, for instance) def do_new - @record = active_scaffold_config.model.new + @record = new_model apply_constraints_to_record(@record) - params[:eid] = @old_eid if @remove_eid + create_association_with_parent(@record) if nested? @record end # A somewhat complex method to actually create a new record. The complexity is from support for subforms and associated records. # If you want to customize this behavior, consider using the +before_create_save+ and +after_create_save+ callbacks. - def do_create + def do_create(options = {}) + attributes = options[:attributes] || params[:record] begin active_scaffold_config.model.transaction do - @record = update_record_from_params(active_scaffold_config.model.new, active_scaffold_config.create.columns, params[:record]) + @record = update_record_from_params(new_model, active_scaffold_config.create.columns, attributes) apply_constraints_to_record(@record, :allow_autosave => true) - params[:eid] = @old_eid if @remove_eid + create_association_with_parent(@record) if nested? before_create_save(@record) - self.successful = [@record.valid?, @record.associated_valid?].all? {|v| v == true} # this syntax avoids a short-circuit - if successful? - @record.save! and @record.save_associated! - after_create_save(@record) - end + self.successful = [@record.valid?, @record.associated_valid?].all? # this syntax avoids a short-circuit + create_save(@record) unless options[:skip_save] end - rescue ActiveRecord::RecordInvalid + rescue ActiveRecord::ActiveRecordError => ex + flash[:error] = ex.message + self.successful = false + end + end + + def create_save(record) + if successful? + record.save! and record.save_associated! + after_create_save(record) end end @@ -122,8 +117,13 @@ def after_create_save(record); end # The default security delegates to ActiveRecordPermissions. # You may override the method to customize. + + def create_ignore? + active_scaffold_config.list.always_show_create + end + def create_authorized? - authorized_for?(:action => :create) + !(nested? && (nested.readonly? || nested.readonly_through_association?)) && authorized_for?(:crud_type => :create) end private def create_authorized_filter diff --git a/lib/active_scaffold/actions/delete.rb b/lib/active_scaffold/actions/delete.rb index 3426b05c4c..5006864209 100644 --- a/lib/active_scaffold/actions/delete.rb +++ b/lib/active_scaffold/actions/delete.rb @@ -1,59 +1,69 @@ module ActiveScaffold::Actions module Delete def self.included(base) - base.before_filter :delete_authorized_filter, :only => [:delete, :destroy] - end - - # this method is for html mode. it provides "the missing action" (http://thelucid.com/articles/2006/07/26/simply-restful-the-missing-action). - # it also gives us delete confirmation for html mode. woo! - def delete - destroy_find_record - render :action => 'delete' + base.before_filter :delete_authorized_filter, :only => [:destroy] end def destroy - return redirect_to(params.merge(:action => :delete)) if request.get? - do_destroy - respond_to_action(:destroy) + params.delete :destroy_action + process_action_link_action(:destroy) do |record| + do_destroy + end end protected def destroy_respond_to_html - flash[:info] = as_(:deleted_model, :model => @record.to_label) + if self.successful? + flash[:info] = as_(:deleted_model, :model => @record.to_label) + else + #error_message_for not available in controller... + #flash[:error] = active_scaffold_error_messages_for(@record, :object_name => "#{@record.class.model_name.human.downcase}#{@record.new_record? ? '' : ": #{@record.to_label}"}", :header_message => '', :message => "#{@record.class.model_name.human.downcase}#{@record.new_record? ? '' : ": #{@record.to_label}"}", :container_tag => nil, :list_type => :br) + end return_to_main end def destroy_respond_to_js - render(:action => 'destroy') + do_refresh_list if successful? && active_scaffold_config.delete.refresh_list && !render_parent? + render(:action => 'destroy', :content_type => Mime::JS, :layout => false) end def destroy_respond_to_xml - render :xml => successful? ? "" : response_object.to_xml, :content_type => Mime::XML, :status => response_status + render :xml => successful? ? "" : response_object.to_xml(:only => active_scaffold_config.list.columns.names), :content_type => Mime::XML, :status => response_status end def destroy_respond_to_json - render :text => successful? ? "" : response_object.to_json, :content_type => Mime::JSON, :status => response_status + render :text => successful? ? "" : response_object.to_json(:only => active_scaffold_config.list.columns.names), :content_type => Mime::JSON, :status => response_status end def destroy_respond_to_yaml - render :text => successful? ? "" : response_object.to_yaml, :content_type => Mime::YAML, :status => response_status + render :text => successful? ? "" : Hash.from_xml(response_object.to_xml(:only => active_scaffold_config.list.columns.names)).to_yaml, :content_type => Mime::YAML, :status => response_status end def destroy_find_record - @record = find_if_allowed(params[:id], :destroy) + @record = find_if_allowed(params[:id], :delete) end # A simple method to handle the actual destroying of a record # May be overridden to customize the behavior def do_destroy - destroy_find_record - self.successful = @record.destroy + @record ||= destroy_find_record + begin + self.successful = @record.destroy + rescue Exception => ex + flash[:warning] = as_(:cant_destroy_record, :record => @record.to_label) + self.successful = false + logger.debug ex.message + logger.debug ex.backtrace.join("\n") + end end # The default security delegates to ActiveRecordPermissions. # You may override the method to customize. - def delete_authorized? - authorized_for?(:action => :destroy) + def delete_authorized?(record = nil) + (!nested? || !nested.readonly?) && (record || self).send(:authorized_for?, :crud_type => :delete) + end + def delete_ignore?(record = nil) + (nested? && nested.readonly?) || !self.send(:authorized_for?, :crud_type => :delete) end private def delete_authorized_filter diff --git a/lib/active_scaffold/actions/field_search.rb b/lib/active_scaffold/actions/field_search.rb index 61f18065f5..af63f31274 100644 --- a/lib/active_scaffold/actions/field_search.rb +++ b/lib/active_scaffold/actions/field_search.rb @@ -1,19 +1,39 @@ module ActiveScaffold::Actions module FieldSearch + include ActiveScaffold::Actions::CommonSearch def self.included(base) base.before_filter :search_authorized_filter, :only => :show_search - base.before_filter :do_search + base.before_filter :store_search_params_into_session, :only => [:index] + base.before_filter :do_search, :only => [:index] + base.helper_method :field_search_params + base.helper_method :search_params end # FieldSearch uses params[:search] and not @record because search conditions do not always pass the Model's validations. # This facilitates for example, textual searches against associations via .search_sql def show_search - params[:search] ||= {} - @record = active_scaffold_config.model.new + @record = new_model respond_to_action(:field_search) end protected + + def store_search_params_into_session + set_field_search_default_params(active_scaffold_config.field_search.default_params) unless active_scaffold_config.field_search.default_params.nil? + super + active_scaffold_session_storage[:search] = nil if search_params.is_a?(String) + end + + def set_field_search_default_params(default_params) + if (params[:search].nil? && search_params.nil?) || (params[:search].is_a?(String) && params[:search].blank?) + params[:search] = default_params.is_a?(Proc) ? self.instance_eval(&default_params) : default_params + end + end + + def field_search_params + search_params || {} + end + def field_search_respond_to_html render(:action => "field_search") end @@ -23,30 +43,30 @@ def field_search_respond_to_js end def do_search - unless params[:search].nil? - like_pattern = active_scaffold_config.field_search.full_text_search? ? '%?%' : '?%' - search_conditions = [] + unless search_params.blank? + filtered_columns = [] + text_search = active_scaffold_config.field_search.text_search columns = active_scaffold_config.field_search.columns - columns.each do |column| - search_conditions << self.class.condition_for_column(column, params[:search][column.name], like_pattern) + search_params.each do |key, value| + next unless columns.include? key + column = active_scaffold_config.columns[key] + search_condition = self.class.condition_for_column(column, value, text_search) + unless search_condition.blank? + self.active_scaffold_outer_joins << column.search_joins unless active_scaffold_config.list.user.count_includes.nil? && column.includes.present? && list_columns.include?(column) + self.active_scaffold_conditions << search_condition + filtered_columns << column + end + end + unless filtered_columns.blank? + @filtered = active_scaffold_config.field_search.human_conditions ? filtered_columns : true end - search_conditions.compact! - self.active_scaffold_conditions = merge_conditions(self.active_scaffold_conditions, *search_conditions) - @filtered = !search_conditions.blank? - - includes_for_search_columns = columns.collect{ |column| column.includes}.flatten.uniq.compact - self.active_scaffold_joins.concat includes_for_search_columns active_scaffold_config.list.user.page = nil end end - # The default security delegates to ActiveRecordPermissions. - # You may override the method to customize. - def search_authorized? - authorized_for?(:action => :read) - end private + def search_authorized_filter link = active_scaffold_config.field_search.link || active_scaffold_config.field_search.class.link raise ActiveScaffold::ActionNotAllowed unless self.send(link.security_method) diff --git a/lib/active_scaffold/actions/list.rb b/lib/active_scaffold/actions/list.rb index cc05f6bab2..719b09558c 100644 --- a/lib/active_scaffold/actions/list.rb +++ b/lib/active_scaffold/actions/list.rb @@ -1,96 +1,220 @@ module ActiveScaffold::Actions module List def self.included(base) - base.before_filter :list_authorized_filter, :only => [:index, :table, :update_table, :row, :list] + base.before_filter :list_authorized_filter, :only => [:index, :row] + base.helper_method :list_columns end def index list end - def table - do_list - render(:action => 'list', :layout => false) - end - - # This is called when changing pages, sorts and search - def update_table - do_list - respond_to_action(:update_table) - end - # get just a single row def row - render :partial => 'list_record', :locals => {:record => find_if_allowed(params[:id], :read)} + get_row + respond_to_action(:row) end def list - do_list - if active_scaffold_config.list.always_show_create - do_new + if %w(index list).include? action_name + do_list + else + do_refresh_list end + @nested_auto_open = active_scaffold_config.list.nested_auto_open respond_to_action(:list) end - + protected def list_respond_to_html - render :action => 'list' + if embedded? + render :action => 'list', :layout => false + else + render :action => 'list' + end end def list_respond_to_js - render :action => 'list', :layout => false + if params[:adapter] || embedded? + render(:partial => 'list_with_header') + else + render :partial => 'refresh_list', :formats => [:js] + end end def list_respond_to_xml - render :xml => response_object.to_xml, :content_type => Mime::XML, :status => response_status + render :xml => response_object.to_xml(:only => list_columns_names), :content_type => Mime::XML, :status => response_status end def list_respond_to_json - render :text => response_object.to_json, :content_type => Mime::JSON, :status => response_status + render :text => response_object.to_json(:only => list_columns_names), :content_type => Mime::JSON, :status => response_status end def list_respond_to_yaml - render :text => response_object.to_yaml, :content_type => Mime::YAML, :status => response_status + render :text => Hash.from_xml(response_object.to_xml(:only => list_columns_names)).to_yaml, :content_type => Mime::YAML, :status => response_status + end + + def row_respond_to_html + render(:partial => 'row', :locals => {:record => @record}) end - def update_table_respond_to_html - return_to_main + + def row_respond_to_js + render :action => 'row', :content_type => Mime::JS, :layout => false + end + + # The actual algorithm to prepare for the list view + def set_includes_for_columns(action = :list) + @cache_associations = true + columns = if respond_to?(:"#{action}_columns") + send(:"#{action}_columns") + else + active_scaffold_config.send(action).columns.collect_visible(:flatten => true) + end + includes_for_list_columns = columns.map{ |c| c.includes }.flatten.uniq.compact + self.active_scaffold_includes.concat includes_for_list_columns end - def update_table_respond_to_js - render(:partial => 'list') + + def get_row + set_includes_for_columns + klass = beginning_of_chain.includes(active_scaffold_includes) + @record = find_if_allowed(params[:id], :read, klass) end + # The actual algorithm to prepare for the list view def do_list - includes_for_list_columns = active_scaffold_config.list.columns.collect{ |c| c.includes }.flatten.uniq.compact - self.active_scaffold_joins.concat includes_for_list_columns + set_includes_for_columns options = { :sorting => active_scaffold_config.list.user.sorting, - :count_includes => active_scaffold_config.list.user.count_includes } + :count_includes => active_scaffold_config.list.user.count_includes } paginate = (params[:format].nil?) ? (accepts? :html, :js) : ['html', 'js'].include?(params[:format]) if paginate options.merge!({ - :per_page => active_scaffold_config.list.user.per_page, - :page => active_scaffold_config.list.user.page - }) + :per_page => active_scaffold_config.list.user.per_page, + :page => active_scaffold_config.list.user.page, + :pagination => active_scaffold_config.list.pagination + }) + end + if active_scaffold_config.list.auto_select_columns + options[:select] = active_scaffold_config.list.columns.map(&:select_columns).compact.flatten + active_scaffold_config.columns[active_scaffold_config.model.primary_key].select_columns end - page = find_page(options); - if page.items.empty? - page = page.pager.first - active_scaffold_config.list.user.page = 1 + page = find_page(options) + total_pages = page.pager.number_of_pages + if !page.pager.infinite? && !total_pages.zero? && page.number > total_pages + page = page.pager.last + active_scaffold_config.list.user.page = page.number end @page, @records = page, page.items end + def do_refresh_list + params.delete(:id) + do_search if respond_to? :do_search, true + do_list + end + + def each_record_in_page + _page = active_scaffold_config.list.user.page + do_search if respond_to? :do_search, true + active_scaffold_config.list.user.page = _page + do_list + @page.items.each {|record| yield record} + end + + def each_record_in_scope + do_search if respond_to? :do_search, true + set_includes_for_columns + append_to_query(beginning_of_chain, finder_options).all.each {|record| yield record} + end + # The default security delegates to ActiveRecordPermissions. # You may override the method to customize. def list_authorized? - authorized_for?(:action => :read) + authorized_for?(:crud_type => :read) end + + # call this method in your action_link action to simplify processing of actions + # eg for member action_link :fire + # process_action_link_action do |record| + # record.update_attributes(:fired => true) + # self.successful = true + # flash[:info] = 'Player fired' + # end + def process_action_link_action(render_action = :action_update, crud_type = nil) + if request.get? + # someone has disabled javascript, we have to show confirmation form first + @record = find_if_allowed(params[:id], :read) if params[:id] && params[:id].to_i > 0 + respond_to_action(:action_confirmation) + else + @action_link = active_scaffold_config.action_links[action_name] + if params[:id] && params[:id].to_i > 0 + crud_type ||= (request.post? || request.put?) ? :update : :delete + set_includes_for_columns + klass = beginning_of_chain.includes(active_scaffold_includes) + @record = find_if_allowed(params[:id], crud_type, klass) + unless @record.nil? + yield @record + else + self.successful = false + flash[:error] = as_(:no_authorization_for_action, :action => action_name) + end + else + yield + end + respond_to_action(render_action) + end + end + + def action_confirmation_respond_to_html(confirm_action = action_name.to_sym) + link = active_scaffold_config.action_links[confirm_action] + render :action => 'action_confirmation', :locals => {:record => @record, :link => link} + end + + def action_update_respond_to_html + redirect_to :action => 'index' + end + + def action_update_respond_to_js + do_refresh_list unless @record.present? + render(:action => 'on_action_update', :content_type => Mime::JS, :layout => false) + end + + def action_update_respond_to_xml + render :xml => successful? ? "" : response_object.to_xml(:only => list_columns_names), :content_type => Mime::XML, :status => response_status + end + + def action_update_respond_to_json + render :text => successful? ? "" : response_object.to_json(:only => list_columns_names), :content_type => Mime::JSON, :status => response_status + end + + def action_update_respond_to_yaml + render :text => successful? ? "" : Hash.from_xml(response_object.to_xml(:only => list_columns_names)).to_yaml, :content_type => Mime::YAML, :status => response_status + end + private def list_authorized_filter raise ActiveScaffold::ActionNotAllowed unless list_authorized? end - def update_table_formats - (default_formats + active_scaffold_config.formats).uniq - end + def list_formats (default_formats + active_scaffold_config.formats + active_scaffold_config.list.formats).uniq end + alias_method :index_formats, :list_formats + + def row_formats + ([:html, :js] + active_scaffold_config.formats + active_scaffold_config.list.formats).uniq + end + + def action_update_formats + (default_formats + active_scaffold_config.formats).uniq + end + + def action_confirmation_formats + (default_formats + active_scaffold_config.formats).uniq + end + + def list_columns + @list_columns ||= active_scaffold_config.list.columns.collect_visible + end + + def list_columns_names + list_columns.collect(&:name) + end end end diff --git a/lib/active_scaffold/actions/live_search.rb b/lib/active_scaffold/actions/live_search.rb deleted file mode 100644 index 5228366baa..0000000000 --- a/lib/active_scaffold/actions/live_search.rb +++ /dev/null @@ -1,57 +0,0 @@ -module ActiveScaffold::Actions - module LiveSearch - def self.included(base) - base.before_filter :search_authorized_filter, :only => :show_search - base.before_filter :do_search - end - - def show_search - respond_to_action(:live_search) - end - - protected - - def live_search_respond_to_html - if successful? - render(:partial => "live_search", :layout => true) - else - return_to_main - end - end - - def live_search_respond_to_js - render(:partial => "live_search") - end - - def do_search - @query = params[:search].to_s.strip rescue '' - - unless @query.empty? - columns = active_scaffold_config.live_search.columns - like_pattern = active_scaffold_config.live_search.full_text_search? ? '%?%' : '?%' - search_conditions = self.class.create_conditions_for_columns(@query.split(' '), columns, like_pattern) - self.active_scaffold_conditions = merge_conditions(self.active_scaffold_conditions, search_conditions) - @filtered = !search_conditions.blank? - - includes_for_search_columns = columns.collect{ |column| column.includes}.flatten.uniq.compact - self.active_scaffold_joins.concat includes_for_search_columns - - active_scaffold_config.list.user.page = nil - end - end - - # The default security delegates to ActiveRecordPermissions. - # You may override the method to customize. - def search_authorized? - authorized_for?(:action => :read) - end - private - def search_authorized_filter - link = active_scaffold_config.live_search.link || active_scaffold_config.live_search.class.link - raise ActiveScaffold::ActionNotAllowed unless self.send(link.security_method) - end - def live_search_formats - (default_formats + active_scaffold_config.formats + active_scaffold_config.live_search.formats).uniq - end - end -end diff --git a/lib/active_scaffold/actions/mark.rb b/lib/active_scaffold/actions/mark.rb new file mode 100644 index 0000000000..f65fadf32f --- /dev/null +++ b/lib/active_scaffold/actions/mark.rb @@ -0,0 +1,89 @@ +module ActiveScaffold::Actions + module Mark + + def self.included(base) + base.before_filter :mark_authorized?, :only => :mark + base.before_filter :assign_marked_records_to_model + base.helper_method :marked_records + end + + def mark + if mark? || mark_all_scope_forced? + do_mark + else + do_demark + end + if marked_records.length > 0 + count = marked_records.length + flash[:info] = as_(:records_marked, :count => count, :model => active_scaffold_config.label(:count => count)) + end + respond_to_action(:mark) + end + protected + + def mark_respond_to_html + do_list + list_respond_to_html + end + + def mark_respond_to_js + if params[:id] + do_search if respond_to? :do_search, true + set_includes_for_columns if active_scaffold_config.actions.include? :list + @page = find_page(:pagination => active_scaffold_config.mark.mark_all_mode != :page) + render :action => 'on_mark', :content_type => Mime::JS, :layout => false + else + render :action => 'on_mark', :content_type => Mime::JS, :layout => false, :locals => {:checked => mark?} + end + end + + # We need to give the ActiveRecord classes a handle to currently marked records. We don't want to just pass the object, + # because the object may change. So we give ActiveRecord a proc that ties to the marked_records_method on this ApplicationController. + def assign_marked_records_to_model + active_scaffold_config.model.marked_records = marked_records + end + + def mark? + @mark ||= [true, 'true', 1, '1', 'T', 't'].include?(params[:value].class == String ? params[:value].downcase : params[:value]) + end + + def mark_all_scope_forced? + params[:mark_target] == 'scope' unless params[:id] + end + + def do_mark + if params[:id] + find_if_allowed(params[:id], :read).as_marked = true + elsif active_scaffold_config.mark.mark_all_mode == :page && !mark_all_scope_forced? + each_record_in_page { |record| record.as_marked = true } + else + each_record_in_scope { |record| record.as_marked = true } + end + end + + def do_demark + if params[:id] + find_if_allowed(params[:id], :read).as_marked = false + elsif active_scaffold_config.mark.mark_all_mode == :page + each_record_in_page { |record| record.as_marked = false } + else + each_record_in_scope { |record| record.as_marked = false } + end + end + + def do_destroy + super + @record.as_marked = false if successful? + end + + # The default security delegates to ActiveRecordPermissions. + # You may override the method to customize. + def mark_authorized? + authorized_for?(:crud_type => :read) + end + + def mark_formats + (default_formats + active_scaffold_config.formats).uniq + end + end +end diff --git a/lib/active_scaffold/actions/nested.rb b/lib/active_scaffold/actions/nested.rb index a9224789df..abb1805c41 100644 --- a/lib/active_scaffold/actions/nested.rb +++ b/lib/active_scaffold/actions/nested.rb @@ -5,73 +5,111 @@ module Nested def self.included(base) super base.module_eval do + prepend_before_filter :set_nested + before_filter :configure_nested include ActiveScaffold::Actions::Nested::ChildMethods if active_scaffold_config.model.reflect_on_all_associations.any? {|a| a.macro == :has_and_belongs_to_many} end base.before_filter :include_habtm_actions - base.helper_method :nested_habtm? - end - - def nested - do_nested - respond_to_action(:nested) + base.helper_method :nested + base.helper_method :nested_parent_record end protected - def nested_respond_to_html - render :partial => 'nested', :layout => true + def nested + @nested end - def nested_respond_to_js - render :partial => 'nested' + + def nested? + !nested.nil? end - # A simple method to find the record we'll be nesting *from* - # May be overridden to customize the behavior - def do_nested - @record = find_if_allowed(params[:id], :read) + + def set_nested + if params[:parent_scaffold] && (params[:association] || params[:named_scope]) + @nested = ActiveScaffold::DataStructures::NestedInfo.get(active_scaffold_config.model, params) + register_constraints_with_action_columns(@nested.constrained_fields) unless @nested.nil? + end end - - def include_habtm_actions - if nested_habtm? - # Production mode is ok with adding a link everytime the scaffold is nested - we ar not ok with that. - active_scaffold_config.action_links.add('new_existing', :label => :add_existing, :type => :table, :security_method => :add_existing_authorized?) unless active_scaffold_config.action_links['new_existing'] - if active_scaffold_config.nested.shallow_delete - active_scaffold_config.action_links.add('destroy_existing', :label => :remove, :type => :record, :confirm => 'are_you_sure', :method => :delete, :position => false, :security_method => :delete_existing_authorized?) unless active_scaffold_config.action_links['destroy_existing'] - active_scaffold_config.action_links.delete("destroy") if active_scaffold_config.action_links['destroy'] + + def configure_nested + if nested? + active_scaffold_config.list.user.label = if nested.belongs_to? + as_(:nested_of_model, :nested_model => active_scaffold_config.model.model_name.human, :parent_model => nested_parent_record.to_label) + else + as_(:nested_for_model, :nested_model => active_scaffold_config.list.label, :parent_model => nested_parent_record.to_label) end - else - # Production mode is caching this link into a non nested scaffold - active_scaffold_config.action_links.delete('new_existing') if active_scaffold_config.action_links['new_existing'] - - if active_scaffold_config.nested.shallow_delete - active_scaffold_config.action_links.delete("destroy_existing") if active_scaffold_config.action_links['destroy_existing'] - active_scaffold_config.action_links.add('destroy', :label => :delete, :type => :record, :confirm => 'are_you_sure', :method => :delete, :position => false, :security_method => :delete_authorized?) unless active_scaffold_config.action_links['destroy'] + if nested.sorted? && !active_scaffold_config.nested.ignore_order_from_association + active_scaffold_config.list.user.nested_default_sorting = {:table_name => active_scaffold_config.model.model_name, :default_sorting => nested.default_sorting} end - end end - - def nested? - !params[:nested].nil? + + def nested_authorized?(record = nil) + true end - def nested_habtm? - begin - a = active_scaffold_config.columns[nested_association] - return a.association.macro == :has_and_belongs_to_many if a and nested? - false - rescue - raise ActiveScaffold::MalformedConstraint, constraint_error(active_scaffold_config.model, nested_association), caller + def include_habtm_actions + if nested? + if nested.habtm? + # Production mode is ok with adding a link everytime the scaffold is nested - we ar not ok with that. + active_scaffold_config.action_links.add('new_existing', :label => :add_existing, :type => :collection, :security_method => :add_existing_authorized?) unless active_scaffold_config.action_links['new_existing'] + if active_scaffold_config.nested.shallow_delete + active_scaffold_config.action_links.add('destroy_existing', :label => :remove, :type => :member, :confirm => :are_you_sure_to_delete, :method => :delete, :position => false, :security_method => :delete_existing_authorized?) unless active_scaffold_config.action_links['destroy_existing'] + if active_scaffold_config.actions.include?(:delete) + active_scaffold_config.action_links.delete("delete") if active_scaffold_config.action_links['delete'] + end + end + else + # Production mode is caching this link into a non nested scaffold + active_scaffold_config.action_links.delete('new_existing') if active_scaffold_config.action_links['new_existing'] + + if active_scaffold_config.nested.shallow_delete + active_scaffold_config.action_links.delete("destroy_existing") if active_scaffold_config.action_links['destroy_existing'] + if active_scaffold_config.actions.include?(:delete) + active_scaffold_config.action_links.add(ActiveScaffold::Config::Delete.link) unless active_scaffold_config.action_links['delete'] + end + end + end end end - - def nested_association - return active_scaffold_constraints.keys.to_s.to_sym if nested? - nil + + def beginning_of_chain + if nested? && nested.association + if nested.association.collection? + nested_parent_record.send(nested.association.name) + elsif nested.association.options[:through] || nested.child_association.nil? # has_one :through doesn't need conditions, and without child_association is not possible to add them + active_scaffold_config.model + elsif nested.child_association.belongs_to? + active_scaffold_config.model.where(nested.child_association.foreign_key => nested_parent_record) + elsif nested.association.belongs_to? + active_scaffold_config.model.joins(nested.child_association.name).where(nested.association.active_record.table_name => {nested.association.active_record.primary_key => nested_parent_record}).readonly(false) + end + elsif nested? && nested.scope + nested_parent_record.send(nested.scope) + else + active_scaffold_config.model + end end - def nested_parent_id - return active_scaffold_constraints.values.to_s if nested? - nil + def nested_parent_record(crud = :read) + @nested_parent_record ||= find_if_allowed(nested.parent_id, crud, nested.parent_model) + end + + def create_association_with_parent(record) + if nested? + if (nested.belongs_to? || nested.has_one? || nested.habtm?) && nested.child_association + parent = nested_parent_record(:read) + case nested.child_association.macro + when :has_one + record.send("#{nested.child_association.name}=", parent) + when :belongs_to + record.send("#{nested.child_association.name}=", parent) + when :has_many, :has_and_belongs_to_many + record.send("#{nested.child_association.name}").send(:<<, parent) + end unless parent.nil? + end + end end + private def nested_formats (default_formats + active_scaffold_config.formats + active_scaffold_config.nested.formats).uniq @@ -84,10 +122,6 @@ module ChildMethods def self.included(base) super - # This .verify method call is clashing with other non .add_existing actions. How do we do this correctly? Can we make it action specific. - # base.verify :method => :post, - # :only => :add_existing, - # :redirect_to => { :action => :index } end def new_existing @@ -127,19 +161,19 @@ def add_existing_respond_to_html end def add_existing_respond_to_js if successful? - render :action => 'add_existing' + render :action => 'add_existing', :content_type => Mime::JS, :layout => false else - render :action => 'form_messages' + render :action => 'form_messages', :content_type => Mime::JS, :layout => false end end def add_existing_respond_to_xml - render :xml => response_object.to_xml, :content_type => Mime::XML, :status => response_status + render :xml => response_object.to_xml(:only => active_scaffold_config.list.columns.names), :content_type => Mime::XML, :status => response_status end def add_existing_respond_to_json - render :text => response_object.to_json, :content_type => Mime::JSON, :status => response_status + render :text => response_object.to_json(:only => active_scaffold_config.list.columns.names), :content_type => Mime::JSON, :status => response_status end def add_existing_respond_to_yaml - render :text => response_object.to_yaml, :content_type => Mime::YAML, :status => response_status + render :text => Hash.from_xml(response_object.to_xml(:only => active_scaffold_config.list.columns.names)).to_yaml, :content_type => Mime::YAML, :status => response_status end def destroy_existing_respond_to_html flash[:info] = as_(:deleted_model, :model => @record.to_label) @@ -147,21 +181,28 @@ def destroy_existing_respond_to_html end def destroy_existing_respond_to_js - render(:action => 'destroy') + render(:action => 'destroy', :content_type => Mime::JS, :layout => false) end def destroy_existing_respond_to_xml - render :xml => successful? ? "" : response_object.to_xml, :content_type => Mime::XML, :status => response_status + render :xml => successful? ? "" : response_object.to_xml(:only => active_scaffold_config.list.columns.names), :content_type => Mime::XML, :status => response_status end def destroy_existing_respond_to_json - render :text => successful? ? "" : response_object.to_json, :content_type => Mime::JSON, :status => response_status + render :text => successful? ? "" : response_object.to_json(:only => active_scaffold_config.list.columns.names), :content_type => Mime::JSON, :status => response_status end def destroy_existing_respond_to_yaml - render :text => successful? ? "" : response_object.to_yaml, :content_type => Mime::YAML, :status => response_status + render :text => successful? ? "" : Hash.from_xml(response_object.to_xml(:only => active_scaffold_config.list.columns.names)).to_yaml, :content_type => Mime::YAML, :status => response_status end + def add_existing_authorized?(record = nil) + true + end + def delete_existing_authorized?(record = nil) + true + end + def after_create_save(record) if params[:association_macro] == :has_and_belongs_to_many params[:associated_id] = record @@ -169,24 +210,22 @@ def after_create_save(record) end end - def nested_action_from_params - return params[:parent_model].constantize, nested_parent_id, params[:parent_column] - end - # The actual "add_existing" algorithm def do_add_existing - parent_model, id, association = nested_action_from_params - parent_record = find_if_allowed(id, :update, parent_model) + parent_record = nested_parent_record(:update) @record = active_scaffold_config.model.find(params[:associated_id]) - parent_record.send(association) << @record - parent_record.save + if parent_record && @record + parent_record.send(nested.association.name) << @record + parent_record.save + else + false + end end def do_destroy_existing if active_scaffold_config.nested.shallow_delete - parent_model, id, association = nested_action_from_params - @record = find_if_allowed(id, :update, parent_model) - collection = @record.send(association) + @record = nested_parent_record(:update) + collection = @record.send(nested.association.name) assoc_record = collection.find(params[:id]) collection.delete(assoc_record) else diff --git a/lib/active_scaffold/actions/search.rb b/lib/active_scaffold/actions/search.rb index 858e244378..a7b6fdc513 100644 --- a/lib/active_scaffold/actions/search.rb +++ b/lib/active_scaffold/actions/search.rb @@ -1,8 +1,11 @@ module ActiveScaffold::Actions module Search + include ActiveScaffold::Actions::CommonSearch def self.included(base) base.before_filter :search_authorized_filter, :only => :show_search - base.before_filter :do_search + base.before_filter :store_search_params_into_session, :only => [:index] + base.before_filter :do_search, :only => [:index] + base.helper_method :search_params end def show_search @@ -17,27 +20,22 @@ def search_respond_to_js render(:partial => "search") end def do_search - @query = params[:search].to_s.strip rescue '' - - unless @query.empty? + query = search_params.to_s.strip rescue '' + unless query.empty? columns = active_scaffold_config.search.columns - like_pattern = active_scaffold_config.search.full_text_search? ? '%?%' : '?%' - search_conditions = self.class.create_conditions_for_columns(@query.split(' '), columns, like_pattern) - self.active_scaffold_conditions = merge_conditions(self.active_scaffold_conditions, search_conditions) + text_search = active_scaffold_config.search.text_search + query = query.split(active_scaffold_config.search.split_terms) if active_scaffold_config.search.split_terms + search_conditions = self.class.create_conditions_for_columns(query, columns, text_search) @filtered = !search_conditions.blank? + self.active_scaffold_conditions.concat search_conditions if @filtered - includes_for_search_columns = columns.collect{ |column| column.includes}.flatten.uniq.compact - self.active_scaffold_joins.concat includes_for_search_columns + outer_joins = columns.collect{ |column| column.search_joins unless column.includes.present? && list_columns.include?(column)} + self.active_scaffold_outer_joins.concat outer_joins.flatten.uniq.compact active_scaffold_config.list.user.page = nil end end - # The default security delegates to ActiveRecordPermissions. - # You may override the method to customize. - def search_authorized? - authorized_for?(:action => :read) - end private def search_authorized_filter link = active_scaffold_config.search.link || active_scaffold_config.search.class.link diff --git a/lib/active_scaffold/actions/show.rb b/lib/active_scaffold/actions/show.rb index 37a41495cb..8f2ca6c3a4 100644 --- a/lib/active_scaffold/actions/show.rb +++ b/lib/active_scaffold/actions/show.rb @@ -5,23 +5,29 @@ def self.included(base) end def show - do_show - successful? - respond_to_action(:show) + # rest destroy falls back to rest show in case of disabled javascript + # just render action_confirmation message for destroy + unless params.delete :destroy_action + do_show + respond_to_action(:show) + else + @record = find_if_allowed(params[:id], :read) if params[:id] && params[:id].to_i > 0 + action_confirmation_respond_to_html(:destroy) + end end protected - + def show_respond_to_json - render :text => response_object.to_json, :content_type => Mime::JSON, :status => response_status + render :text => response_object.to_json(:only => active_scaffold_config.show.columns.names), :content_type => Mime::JSON, :status => response_status end def show_respond_to_yaml - render :text => response_object.to_yaml, :content_type => Mime::YAML, :status => response_status + render :text => Hash.from_xml(response_object.to_xml(:only => active_scaffold_config.show.columns.names)).to_yaml, :content_type => Mime::YAML, :status => response_status end def show_respond_to_xml - render :xml => response_object.to_xml, :content_type => Mime::XML, :status => response_status + render :xml => response_object.to_xml(:only => active_scaffold_config.show.columns.names), :content_type => Mime::XML, :status => response_status end def show_respond_to_js @@ -34,15 +40,20 @@ def show_respond_to_html # A simple method to retrieve and prepare a record for showing. # May be overridden to customize show routine def do_show - @record = find_if_allowed(params[:id], :read) + set_includes_for_columns(:show) if active_scaffold_config.actions.include? :list + klass = beginning_of_chain.includes(active_scaffold_includes) + @record = find_if_allowed(params[:id], :read, klass) end # The default security delegates to ActiveRecordPermissions. # You may override the method to customize. - def show_authorized? - authorized_for?(:action => :read) + def show_authorized?(record = nil) + (record || self).send(:authorized_for?, :crud_type => :read) + end + def show_ignore?(record = nil) + !self.send(:authorized_for?, :crud_type => :read) end - private + private def show_authorized_filter link = active_scaffold_config.show.link || active_scaffold_config.show.class.link raise ActiveScaffold::ActionNotAllowed unless self.send(link.security_method) diff --git a/lib/active_scaffold/actions/subform.rb b/lib/active_scaffold/actions/subform.rb index 12e2808edf..02c39673b1 100644 --- a/lib/active_scaffold/actions/subform.rb +++ b/lib/active_scaffold/actions/subform.rb @@ -1,17 +1,22 @@ module ActiveScaffold::Actions module Subform def edit_associated - @parent_record = params[:id].nil? ? active_scaffold_config.model.new : find_if_allowed(params[:id], :update) - @column = active_scaffold_config.columns[params[:association]] + do_edit_associated + render :action => 'edit_associated', :formats => [:js], :readonly => @column.association.options[:readonly] + end - # NOTE: we don't check whether the user is allowed to update this record, because if not, we'll still let them associate the record. we'll just refuse to do more than associate, is all. - @record = @column.association.klass.find(params[:associated_id]) if params[:associated_id] - @record ||= @column.association.klass.new + protected - @scope = "[#{@column.name}]" - @scope += (@record.new_record?) ? "[#{(Time.now.to_f*1000).to_i.to_s}]" : "[#{@record.id}]" if @column.plural_association? + def do_edit_associated + @parent_record = params[:id].nil? ? new_model : find_if_allowed(params[:id], :update) + generate_temporary_id(@parent_record, params[:generated_id]) if @parent_record.new_record? && params[:generated_id] + @column = active_scaffold_config.columns[params[:child_association]] - render :action => 'edit_associated' + # NOTE: we don't check whether the user is allowed to update this record, because if not, we'll still let them associate the record. we'll just refuse to do more than associate, is all. + @record = @column.association.klass.find(params[:associated_id]) if params[:associated_id] + @record ||= build_associated(@column, @parent_record) + @scope = params[:scope] end + end end diff --git a/lib/active_scaffold/actions/update.rb b/lib/active_scaffold/actions/update.rb index 13cb6e3cd3..71168c23c2 100644 --- a/lib/active_scaffold/actions/update.rb +++ b/lib/active_scaffold/actions/update.rb @@ -2,9 +2,7 @@ module ActiveScaffold::Actions module Update def self.included(base) base.before_filter :update_authorized_filter, :only => [:edit, :update] - base.verify :method => [:post, :put], - :only => :update, - :redirect_to => { :action => :index } + base.helper_method :update_refresh_list? end def edit @@ -20,7 +18,7 @@ def update # for inline (inlist) editing def update_column do_update_column - render :action => 'update_column' + @column_span_id = params.delete(:editor_id) || params.delete(:editorId) end protected @@ -34,14 +32,11 @@ def edit_respond_to_html def edit_respond_to_js render(:partial => 'update_form') end - def update_respond_to_html + def update_respond_to_html if params[:iframe]=='true' # was this an iframe post ? + do_refresh_list if successful? && active_scaffold_config.create.refresh_list && !render_parent? responds_to_parent do - if successful? - render :action => 'on_update.js' - else - render :action => 'form_messages_on_update.js' - end + render :action => 'on_update', :formats => [:js], :layout => false end else # just a regular post if successful? @@ -53,17 +48,30 @@ def update_respond_to_html end end def update_respond_to_js - render :action => 'on_update' + if successful? + if !render_parent? && active_scaffold_config.actions.include?(:list) + if update_refresh_list? + do_refresh_list + else + @updated_record = @record + # get_row so associations are cached like in list action + @record = get_row rescue nil # if record doesn't fullfil current conditions remove it from list + end + end + flash.now[:info] = as_(:updated_model, :model => (@updated_record || @record).to_label) if active_scaffold_config.update.persistent + end + render :action => 'on_update', :content_type => Mime::JS, :layout => false end def update_respond_to_xml - render :xml => response_object.to_xml, :content_type => Mime::XML, :status => response_status + render :xml => response_object.to_xml(:only => active_scaffold_config.update.columns.names), :content_type => Mime::XML, :status => response_status end def update_respond_to_json - render :text => response_object.to_json, :content_type => Mime::JSON, :status => response_status + render :text => response_object.to_json(:only => active_scaffold_config.update.columns.names), :content_type => Mime::JSON, :status => response_status end def update_respond_to_yaml - render :text => response_object.to_yaml, :content_type => Mime::YAML, :status => response_status + render :text => Hash.from_xml(response_object.to_xml(:only => active_scaffold_config.update.columns.names)).to_yaml, :content_type => Mime::YAML, :status => response_status end + # A simple method to find and prepare a record for editing # May be overridden to customize the record (set default values, etc.) def do_edit @@ -74,29 +82,66 @@ def do_edit # If you want to customize this algorithm, consider using the +before_update_save+ callback def do_update do_edit + update_save + end + + def update_save(options = {}) + attributes = options[:attributes] || params[:record] begin active_scaffold_config.model.transaction do - @record = update_record_from_params(@record, active_scaffold_config.update.columns, params[:record]) + @record = update_record_from_params(@record, active_scaffold_config.update.columns, attributes) unless options[:no_record_param_update] before_update_save(@record) - self.successful = [@record.valid?, @record.associated_valid?].all? {|v| v == true} # this syntax avoids a short-circuit + self.successful = [@record.valid?, @record.associated_valid?].all? # this syntax avoids a short-circuit if successful? @record.save! and @record.save_associated! after_update_save(@record) + else + # some associations such as habtm are saved before saved is called on parent object + # we have to revert these changes if validation fails + raise ActiveRecord::Rollback, "don't save habtm associations unless record is valid" end end - rescue ActiveRecord::RecordInvalid rescue ActiveRecord::StaleObjectError - @record.errors.add_to_base as_(:version_inconsistency) - self.successful=false + @record.errors.add(:base, as_(:version_inconsistency)) + self.successful = false + rescue ActiveRecord::RecordNotSaved + @record.errors.add(:base, as_(:record_not_saved)) if @record.errors.empty? + self.successful = false + rescue ActiveRecord::ActiveRecordError => ex + flash[:error] = ex.message + self.successful = false end end def do_update_column - @record = active_scaffold_config.model.find(params[:id]) - if @record.authorized_for?(:action => :update, :column => params[:column]) - params[:value] ||= @record.column_for_attribute(params[:column]).default unless @record.column_for_attribute(params[:column]).null - @record.send("#{params[:column]}=", params[:value]) - @record.save + # delete from params so update :table won't break urls, also they shouldn't be used in sort links too + value = params.delete(:value) + column = params.delete(:column).to_sym + params.delete(:original_html) + params.delete(:original_value) + + @record = find_if_allowed(params[:id], :read) + if @record.authorized_for?(:crud_type => :update, :column => column) + @column = active_scaffold_config.columns[column] + value ||= unless @column.column.nil? || @column.column.null + @column.column.default == true ? false : @column.column.default + end + unless @column.nil? + value = column_value_from_param_value(@record, @column, value) + value = [] if value.nil? && @column.form_ui && @column.plural_association? + end + @record.send("#{@column.name}=", value) + before_update_save(@record) + self.successful = @record.save + if self.successful? && active_scaffold_config.actions.include?(:list) + if @column.inplace_edit_update == :table + params.delete(:id) + do_list + elsif @column.inplace_edit_update + get_row + end + end + after_update_save(@record) end end @@ -106,10 +151,18 @@ def before_update_save(record); end # override this method if you want to do something after the save def after_update_save(record); end + # should we refresh whole list after update operation + def update_refresh_list? + active_scaffold_config.update.refresh_list + end + # The default security delegates to ActiveRecordPermissions. # You may override the method to customize. - def update_authorized? - authorized_for?(:action => :update) + def update_authorized?(record = nil) + (!nested? || !nested.readonly?) && (record || self).authorized_for?(:crud_type => :update) + end + def update_ignore?(record = nil) + !self.authorized_for?(:crud_type => :update) end private def update_authorized_filter diff --git a/lib/active_scaffold/active_record_permissions.rb b/lib/active_scaffold/active_record_permissions.rb new file mode 100644 index 0000000000..c3be43cd83 --- /dev/null +++ b/lib/active_scaffold/active_record_permissions.rb @@ -0,0 +1,152 @@ +# This module attempts to create permissions conventions for your ActiveRecord models. It supports english-based +# methods that let you restrict access per-model, per-record, per-column, per-action, and per-user. All at once. +# +# You may define instance methods in the following formats: +# def #{column}_authorized_for_#{action}? +# def #{column}_authorized? +# def authorized_for_#{action}? +# +# Your methods should allow for the following special cases: +# * cron scripts +# * guest users (or nil current_user objects) +module ActiveRecordPermissions + # ActiveRecordPermissions needs to know what method on your ApplicationController will return the current user, + # if available. This defaults to the :current_user method. You may configure this in your environment.rb if you + # have a different setup. + def self.current_user_method=(v); @@current_user_method = v; end + def self.current_user_method; @@current_user_method; end + @@current_user_method = :current_user + + # Whether the default permission is permissive or not + # If set to true, then everything's allowed until configured otherwise + def self.default_permission=(v); @@default_permission = v; end + def self.default_permission; @@default_permission; end + @@default_permission = true + + # This is a module aimed at making the current_user available to ActiveRecord models for permissions. + module ModelUserAccess + module Controller + def self.included(base) + base.prepend_before_filter :assign_current_user_to_models + end + + # We need to give the ActiveRecord classes a handle to the current user. We don't want to just pass the object, + # because the object may change (someone may log in or out). So we give ActiveRecord a proc that ties to the + # current_user_method on this ApplicationController. + def assign_current_user_to_models + ActiveRecord::Base.current_user_proc = proc {send(ActiveRecordPermissions.current_user_method)} + end + end + + module Model + def self.included(base) + base.extend ClassMethods + base.send :include, ActiveRecordPermissions::Permissions + end + + module ClassMethods + # The proc to call that retrieves the current_user from the ApplicationController. + attr_accessor :current_user_proc + + # Class-level access to the current user + def current_user + ActiveRecord::Base.current_user_proc.call if ActiveRecord::Base.current_user_proc + end + end + + # Instance-level access to the current user + def current_user + self.class.current_user + end + end + end + + module Permissions + def self.included(base) + base.extend SecurityMethods + base.send :include, SecurityMethods + class << base + attr_accessor :class_security_methods + attr_accessor :instance_security_methods + end + end + + # Because any class-level queries get delegated to the instance level via a new record, + # it's useful to know when the authorization query is meant for a specific record or not. + # But using new_record? is confusing, even though accurate. So this is basically just a wrapper. + def existing_record_check? + !new_record? + end + + module SecurityMethods + # A generic authorization query. This is what will be called programatically, since + # the actual permission methods can't be guaranteed to exist. And because we want to + # intelligently combine multiple applicable methods. + # + # options[:crud_type] should be a CRUD verb (:create, :read, :update, :destroy) + # options[:column] should be the name of a model attribute + # options[:action] is the name of a method + def authorized_for?(options = {}) + raise ArgumentError, "unknown crud type #{options[:crud_type]}" if options[:crud_type] and ![:create, :read, :update, :delete].include?(options[:crud_type]) + + # collect other possibly-related methods that actually exist + methods = cached_authorized_for_methods(options) + return ActiveRecordPermissions.default_permission if methods.empty? + return send(methods.first) if methods.one? + + # if any method returns false, then return false + return false if methods.any? {|m| !send(m)} + true + end + + def cached_authorized_for_methods(options) + key = "#{options[:crud_type]}##{options[:column]}##{options[:action]}" + if self.is_a? Class + self.class_security_methods ||= {} + self.class_security_methods[key] ||= authorized_for_methods(options) + else + self.class.instance_security_methods ||= {} + self.class.instance_security_methods[key] ||= authorized_for_methods(options) + end + end + + def authorized_for_methods(options) + # column_authorized_for_crud_type? has the highest priority over other methods, + # you can disable a crud verb and enable that verb for a column + # (for example, disable update and enable inplace_edit in a column) + method = column_and_crud_type_security_method(options[:column], options[:crud_type]) + return [method] if method and respond_to?(method) + + # authorized_for_action? has higher priority than other methods, + # you can disable a crud verb and enable an action with that crud verb + # (for example, disable update and enable an action with update as crud type) + method = action_security_method(options[:action]) + return [method] if method and respond_to?(method) + + # collect other possibly-related methods that actually exist + methods = [ + column_security_method(options[:column]), + crud_type_security_method(options[:crud_type]), + ].compact.select {|m| respond_to?(m)} + end + + private + + def column_security_method(column) + "#{column}_authorized?" if column + end + + def crud_type_security_method(crud_type) + "authorized_for_#{crud_type}?" if crud_type + end + + def action_security_method(action) + "authorized_for_#{action}?" if action + end + + def column_and_crud_type_security_method(column, crud_type) + "#{column}_authorized_for_#{crud_type}?" if column and crud_type + end + end + end +end diff --git a/lib/active_scaffold/attribute_params.rb b/lib/active_scaffold/attribute_params.rb index cd7eaf2e19..526420057d 100644 --- a/lib/active_scaffold/attribute_params.rb +++ b/lib/active_scaffold/attribute_params.rb @@ -29,14 +29,15 @@ module ActiveScaffold # 'location' => '12' # } module AttributeParams + protected # Takes attributes (as from params[:record]) and applies them to the parent_record. Also looks for # association attributes and attempts to instantiate them as associated objects. # # This is a secure way to apply params to a record, because it's based on a loop over the columns # set. The columns set will not yield unauthorized columns, and it will not yield unregistered columns. def update_record_from_params(parent_record, columns, attributes) - action = parent_record.new_record? ? :create : :update - return parent_record unless parent_record.authorized_for?(:action => action) + crud_type = parent_record.new_record? ? :create : :update + return parent_record unless parent_record.authorized_for?(:crud_type => crud_type) multi_parameter_attributes = {} attributes.each do |k, v| @@ -46,140 +47,183 @@ def update_record_from_params(parent_record, columns, attributes) multi_parameter_attributes[column_name] << [k, v] end - columns.each :for => parent_record, :action => action, :flatten => true do |column| + columns.each :for => parent_record, :crud_type => crud_type, :flatten => true do |column| + # Set any passthrough parameters that may be associated with this column (ie, file column "keep" and "temp" attributes) + unless column.params.empty? + column.params.each{|p| parent_record.send("#{p}=", attributes[p]) if attributes.has_key? p} + end + if multi_parameter_attributes.has_key? column.name parent_record.send(:assign_multiparameter_attributes, multi_parameter_attributes[column.name]) elsif attributes.has_key? column.name - value = attributes[column.name] - - # convert the value, possibly by instantiating associated objects - value = if value.is_a?(Hash) - # this is just for backwards compatibility. we should clean this up in 2.0. - if column.form_ui == :select - ids = if column.singular_association? - value[:id] - else - value.values.collect {|hash| hash[:id]} - end - (ids and not ids.empty?) ? column.association.klass.find(ids) : nil - - elsif column.singular_association? - hash = value - record = find_or_create_for_params(hash, column, parent_record.send("#{column.name}")) - if record - record_columns = active_scaffold_config_for(column.association.klass).subform.columns - update_record_from_params(record, record_columns, hash) - record.unsaved = true - end - record - - elsif column.plural_association? - collection = value.collect do |key_value_pair| - hash = key_value_pair[1] - record = find_or_create_for_params(hash, column, parent_record.send("#{column.name}")) - if record - record_columns = active_scaffold_config_for(column.association.klass).subform.columns - update_record_from_params(record, record_columns, hash) - record.unsaved = true - end - record - end - collection.compact - end - else - if column.singular_association? - # it's a single id - column.association.klass.find(value) if value and not value.empty? - elsif column.plural_association? - # it's an array of ids - column.association.klass.find(value) if value and not value.empty? - else - # convert empty strings into nil. this works better with 'null => true' columns (and validations), - # and 'null => false' columns should just convert back to an empty string. - # ... but we can at least check the ConnectionAdapter::Column object to see if nulls are allowed - value = nil if value.is_a? String and value.empty? and !column.column.nil? and column.column.null - value - end - end - - # we avoid assigning a value that already exists because otherwise has_one associations will break (AR bug in has_one_association.rb#replace) - parent_record.send("#{column.name}=", value) unless column.through_association? or parent_record.send(column.name) == value - - # Set any passthrough parameters that may be associated with this column (ie, file column "keep" and "temp" attributes) - unless column.params.empty? - column.params.each{|p| parent_record.send("#{p}=", attributes[p])} - end - - # plural associations may not actually appear in the params if all of the options have been unselected or cleared away. - # NOTE: the "form_ui" check isn't really necessary, except that without it we have problems - # with subforms. the UI cuts out deep associations, which means they're not present in the - # params even though they're in the columns list. the result is that associations were being - # emptied out way too often. BUT ... this means there's still a lingering bug in the default association - # form code: you can't delete the last association in the list. - elsif column.form_ui and column.plural_association? and not column.through_association? - parent_record.send("#{column.name}=", []) + value = column_value_from_param_value(parent_record, column, attributes[column.name]) + parent_record.send("#{column.name}=", value) end end if parent_record.new_record? parent_record.class.reflect_on_all_associations.each do |a| - next unless [:has_one, :has_many].include?(a.macro) and not a.options[:through] - next unless association_proxy = parent_record.send(a.name) + next unless [:has_one, :has_many].include?(a.macro) and not (a.options[:through] || a.options[:finder_sql]) + next unless (association_proxy = parent_record.send(a.name)).present? - raise ActiveScaffold::ReverseAssociationRequired, "In order to support :has_one and :has_many where the parent record is new and the child record(s) validate the presence of the parent, ActiveScaffold requires the reverse association (the belongs_to)." unless a.reverse + raise ActiveScaffold::ReverseAssociationRequired, "Association #{a.name} in class #{parent_record.class.name}: In order to support :has_one and :has_many where the parent record is new and the child record(s) validate the presence of the parent, ActiveScaffold requires the reverse association (the belongs_to)." unless a.reverse association_proxy = [association_proxy] if a.macro == :has_one association_proxy.each { |record| record.send("#{a.reverse}=", parent_record) } end end - + + flash[:warning] = parent_record.errors.to_a.join("\n") if parent_record.errors.present? parent_record end + + def manage_nested_record_from_params(parent_record, column, attributes) + record = find_or_create_for_params(attributes, column, parent_record) + if record + record_columns = active_scaffold_config_for(column.association.klass).subform.columns + record_columns.constraint_columns = [column.association.reverse] + update_record_from_params(record, record_columns, attributes) + record.unsaved = true + end + record + end + + def column_value_from_param_value(parent_record, column, value) + # convert the value, possibly by instantiating associated objects + form_ui = column.form_ui || column.column.try(:type) + if form_ui && self.respond_to?("column_value_for_#{form_ui}_type", true) + self.send("column_value_for_#{form_ui}_type", parent_record, column, value) + elsif value.is_a?(Hash) + column_value_from_param_hash_value(parent_record, column, value) + else + column_value_from_param_simple_value(parent_record, column, value) + end + end + + def datetime_conversion_for_value(column) + if column.column + column.column.type == :date ? :to_date : :to_time + else + :to_time + end + end + + def column_value_for_datetime_type(parent_record, column, value) + new_value = self.class.condition_value_for_datetime(column, value, self.class.datetime_conversion_for_condition(column)) + if new_value.nil? && value.present? + parent_record.errors.add column.name, :invalid + end + new_value + end + + def column_value_from_param_simple_value(parent_record, column, value) + if column.singular_association? + if value.present? + if column.polymorphic_association? + class_name = parent_record.send(column.association.foreign_type) + class_name.constantize.find(value) if class_name + else + # it's a single id + column.association.klass.find(value) + end + end + elsif column.plural_association? + column_plural_assocation_value_from_value(column, Array(value)) + elsif column.number? && column.options[:format] && column.form_ui != :number + column.number_to_native(value) + else + # convert empty strings into nil. this works better with 'null => true' columns (and validations), + # and 'null => false' columns should just convert back to an empty string. + # ... but we can at least check the ConnectionAdapter::Column object to see if nulls are allowed + value = nil if value.is_a? String and value.empty? and !column.column.nil? and column.column.null + value + end + end + + def column_plural_assocation_value_from_value(column, value) + # it's an array of ids + if value and not value.empty? + ids = value.select {|id| id.present?} + ids.empty? ? [] : column.association.klass.find(ids) + end + end + + def column_value_from_param_hash_value(parent_record, column, value) + # this is just for backwards compatibility. we should clean this up in 2.0. + if column.form_ui == :select + ids = if column.singular_association? + value[:id] + else + value.values.collect {|hash| hash[:id]} + end + (ids and not ids.empty?) ? column.association.klass.find(ids) : nil + + elsif column.singular_association? + manage_nested_record_from_params(parent_record, column, value) + elsif column.plural_association? + # HACK to be able to delete all associated records, hash will include "0" => "" + value.sort.collect {|key, value| manage_nested_record_from_params(parent_record, column, value) unless value == ""}.compact + else + value + end + end - # Attempts to create or find an instance of klass (which must be an ActiveRecord object) from the + # Attempts to create or find an instance of klass (which must be an ActiveRecord object) from the # request parameters given. If params[:id] exists it will attempt to find an existing object # otherwise it will build a new one. - def find_or_create_for_params(params, parent_column, current) + def find_or_create_for_params(params, parent_column, parent_record) + current = parent_record.send(parent_column.name) klass = parent_column.association.klass - return nil if parent_column.show_blank_record and attributes_hash_is_empty?(params, klass) + pk = klass.primary_key.to_sym + return nil if parent_column.show_blank_record?(current) and attributes_hash_is_empty?(params, klass) - if params.has_key? :id + if params.has_key? pk # modifying the current object of a singular association - if current and current.is_a? ActiveRecord::Base and current.id.to_s == params[:id] - return current + pk_val = params[pk] + if current and current.is_a? ActiveRecord::Base and current.id.to_s == pk_val + current # modifying one of the current objects in a plural association - elsif current and current.respond_to?(:any?) and current.any? {|o| o.id.to_s == params[:id]} - return current.detect {|o| o.id.to_s == params[:id]} + elsif current and current.respond_to?(:any?) and current.any? {|o| o.id.to_s == pk_val} + current.detect {|o| o.id.to_s == pk_val} # attaching an existing but not-current object else - return klass.find(params[:id]) + klass.find(pk_val) end else - return klass.new if klass.authorized_for?(:action => :create) + build_associated(parent_column, parent_record) if klass.authorized_for?(:crud_type => :create) end end - # Determines whether the given attributes hash is "empty". # This isn't a literal emptiness - it's an attempt to discern whether the user intended it to be empty or not. def attributes_hash_is_empty?(hash, klass) + ignore_column_types = [:boolean] hash.all? do |key,value| # convert any possible multi-parameter attributes like 'created_at(5i)' to simply 'created_at' - column_name = key.to_s.split('(').first + parts = key.to_s.split('(') + #old style date form management... ignore them too + ignore_column_types = [:boolean, :datetime, :date, :time] if parts.length > 1 + column_name = parts.first column = klass.columns_hash[column_name] # booleans and datetimes will always have a value. so we ignore them when checking whether the hash is empty. # this could be a bad idea. but the current situation (excess record entry) seems worse. - next true if column and [:boolean, :datetime, :date, :time].include?(column.type) + next true if column and ignore_column_types.include?(column.type) # defaults are pre-filled on the form. we can't use them to determine if the user intends a new row. - next true if column and value == column.default.to_s + next true if value == column_default_value(column_name, klass, column) if value.is_a?(Hash) attributes_hash_is_empty?(value, klass) + elsif value.is_a?(Array) + value.all?(&:blank?) else value.respond_to?(:empty?) ? value.empty? : false end end end + + def column_default_value(column_name, klass, column) + column.default.to_s if column + end end -end \ No newline at end of file +end diff --git a/lib/active_scaffold/bridges.rb b/lib/active_scaffold/bridges.rb new file mode 100644 index 0000000000..9895d73c81 --- /dev/null +++ b/lib/active_scaffold/bridges.rb @@ -0,0 +1,61 @@ +module ActiveScaffold + module Bridges + ActiveScaffold.autoload_subdir('bridges', self) + module Shared + autoload :DateBridge, 'active_scaffold/bridges/shared/date_bridge' + end + + mattr_accessor :bridges + mattr_accessor :bridges_run + self.bridges = {} + + def self.register(file) + match = file.match(/(active_scaffold\/bridges\/(.*))\.rb\Z/) + self.bridges[match[2].to_sym] = match[1] if match + end + + def self.load(bridge_name) + bridge = self.bridges[bridge_name.to_sym] + if bridge.is_a? String + if ActiveScaffold.exclude_bridges.exclude? bridge_name.to_sym + bridge = bridge.camelize.constantize + self.bridges[bridge_name.to_sym] = bridge + else + self.bridges.delete bridge_name + bridge = nil + end + end + bridge + end + class << self + alias_method :[], :load + end + + def self.run_all + return false if self.bridges_run + self.bridges.keys.each do |bridge_name| + bridge = self[bridge_name] + bridge.run if bridge + end + self.bridges_run = true + end + + def self.all_stylesheets + self.bridges.keys.collect do |bridge_name| + bridge = self[bridge_name] + bridge.stylesheets if bridge and bridge.install? + end.compact.flatten + end + + def self.all_javascripts + self.bridges.keys.collect do |bridge_name| + bridge = self[bridge_name] + bridge.javascripts if bridge and bridge.install? + end.compact.flatten + end + end +end + +(Dir[File.join(File.dirname(__FILE__), "bridges/*.rb")] - [__FILE__]).each{|bridge_require| + ActiveScaffold::Bridges.register bridge_require +} diff --git a/lib/active_scaffold/bridges/ancestry.rb b/lib/active_scaffold/bridges/ancestry.rb new file mode 100644 index 0000000000..965195f576 --- /dev/null +++ b/lib/active_scaffold/bridges/ancestry.rb @@ -0,0 +1,5 @@ +class ActiveScaffold::Bridges::Ancestry < ActiveScaffold::DataStructures::Bridge + def self.install + require File.join(File.dirname(__FILE__), "ancestry/ancestry_bridge.rb") + end +end diff --git a/lib/active_scaffold/bridges/ancestry/ancestry_bridge.rb b/lib/active_scaffold/bridges/ancestry/ancestry_bridge.rb new file mode 100644 index 0000000000..0b321d365c --- /dev/null +++ b/lib/active_scaffold/bridges/ancestry/ancestry_bridge.rb @@ -0,0 +1,39 @@ +ActiveScaffold::Config::Core.class_eval do + def initialize_with_ancestry(model_id) + initialize_without_ancestry(model_id) + + return unless self.model.respond_to? :ancestry_column + + self.columns << :parent_id + self.columns[:parent_id].form_ui = :ancestry + update.columns.exclude :ancestry + create.columns.exclude :ancestry, :parent_id + list.columns.exclude :ancestry, :parent_id + end + + alias_method_chain :initialize, :ancestry +end + +module ActiveScaffold::Bridges + class Ancestry + module FormColumnHelpers + def active_scaffold_input_ancestry(column, options) + select_options = [] + select_control_options = {:selected => @record.parent_id} + select_control_options[:include_blank] = as_(:_select_) if @record.parent_id.nil? + traverse_ancestry = proc do|key, value| + unless key == @record + select_options << ["#{'__' * key.depth}#{key.to_label}", key.id] + value.each(&traverse_ancestry) if value.is_a?(Hash) && !value.empty? + end + end + @record.class.arrange.each(&traverse_ancestry) + select(:record, :ancestry, select_options, select_control_options, options) + end + end + end +end + +ActionView::Base.class_eval do + include ActiveScaffold::Bridges::Ancestry::FormColumnHelpers +end diff --git a/lib/active_scaffold/bridges/bitfields.rb b/lib/active_scaffold/bridges/bitfields.rb new file mode 100644 index 0000000000..51fd4f045c --- /dev/null +++ b/lib/active_scaffold/bridges/bitfields.rb @@ -0,0 +1,6 @@ +class ActiveScaffold::Bridges::Bitfields < ActiveScaffold::DataStructures::Bridge + def self.install + require File.join(File.dirname(__FILE__), "bitfields/bitfields_bridge") + ActiveScaffold::Config::Core.send :include, ActiveScaffold::Bridges::Bitfields::BitfieldsBridge + end +end diff --git a/lib/active_scaffold/bridges/bitfields/bitfields_bridge.rb b/lib/active_scaffold/bridges/bitfields/bitfields_bridge.rb new file mode 100644 index 0000000000..305c378021 --- /dev/null +++ b/lib/active_scaffold/bridges/bitfields/bitfields_bridge.rb @@ -0,0 +1,40 @@ +module ActiveScaffold + module Bridges + class Bitfields + module BitfieldsBridge + def initialize_with_bitfields(model_id) + initialize_without_bitfields(model_id) + return unless self.model.respond_to?(:bitfields) and self.model.bitfields.present? + + self.model.bitfields.each do |column_name, options| + self.columns << options.keys + options.each do |column, value| + self.columns[column].form_ui = :checkbox + self.columns[column].weight = 1000 + value.to_s(2).size + end + end + end + + def _load_action_columns_with_bitfields + self.model.bitfields.each do |column_name, options| + columns = options.keys.sort_by { |column| self.columns[column].weight } + [:create, :update, :show, :subform].each do |action| + if self.actions.include? action + self.send(action).columns.exclude column_name + self.send(action).columns.add_subgroup(column_name) { |group| group.add *columns } + end + end + end if self.model.respond_to?(:bitfields) and self.model.bitfields.present? + + _load_action_columns_without_bitfields + end + + + def self.included(base) + base.alias_method_chain :initialize, :bitfields + base.alias_method_chain :_load_action_columns, :bitfields + end + end + end + end +end diff --git a/lib/active_scaffold/bridges/calendar_date_select.rb b/lib/active_scaffold/bridges/calendar_date_select.rb new file mode 100644 index 0000000000..d7498487e7 --- /dev/null +++ b/lib/active_scaffold/bridges/calendar_date_select.rb @@ -0,0 +1,24 @@ +class ActiveScaffold::Bridges::CalendarDateSelect < ActiveScaffold::DataStructures::Bridge + def self.install + # check to see if the old bridge was installed. If so, warn them + # we can detect this by checking to see if the bridge was installed before calling this code + + if ActiveScaffold::Config::Core.method_defined?(:initialize_with_calendar_date_select) + raise RuntimeError, "We've detected that you have active_scaffold_calendar_date_select_bridge installed. This plugin has been moved to core. Please remove active_scaffold_calendar_date_select_bridge to prevent any conflicts" + end + + require File.join(File.dirname(__FILE__), "calendar_date_select/as_cds_bridge.rb") + end + + def self.install? + super && ActiveScaffold.js_framework == :prototype + end + + def self.stylesheets + calendar_date_select_stylesheets + end + + def self.javascripts + calendar_date_select_javascripts + end +end diff --git a/lib/active_scaffold/bridges/calendar_date_select/as_cds_bridge.rb b/lib/active_scaffold/bridges/calendar_date_select/as_cds_bridge.rb new file mode 100644 index 0000000000..2dd4fdaf86 --- /dev/null +++ b/lib/active_scaffold/bridges/calendar_date_select/as_cds_bridge.rb @@ -0,0 +1,66 @@ +module ActiveScaffold::Config + class Core < Base + + def initialize_with_calendar_date_select(model_id) + initialize_without_calendar_date_select(model_id) + + calendar_date_select_fields = self.model.columns.collect{|c| c.name.to_sym if [:date, :datetime].include?(c.type) }.compact + # check to see if file column was used on the model + return if calendar_date_select_fields.empty? + + # automatically set the forum_ui to a file column + calendar_date_select_fields.each{|field| + self.columns[field].form_ui = :calendar_date_select + } + end + + alias_method_chain :initialize, :calendar_date_select + + end +end + + +module ActiveScaffold + module Bridges + class CalendarDateSelect + # Helpers that assist with the rendering of a Form Column + module FormColumnHelpers + def active_scaffold_input_calendar_date_select(column, options) + options[:class] = "#{options[:class]} text-input".strip + calendar_date_select("record", column.name, options.merge(column.options)) + end + end + + module SearchColumnHelpers + def active_scaffold_search_date_bridge_calendar_control(column, options, current_search, name) + if current_search.is_a? Hash + value = controller.class.condition_value_for_datetime(column, current_search[name], column.column.type == :date ? :to_date : :to_time) + else + value = current_search + end + calendar_date_select("record", column.name, + {:name => "#{options[:name]}[#{name}]", + :value => (value ? l(value) : nil), + :class => 'text-input', + :id => "#{options[:id]}_#{name}", + :time => column_datetime?(column) ? true : false, + :style => (options[:show].nil? || options[:show]) ? nil : "display: none"}) + end + end + end + end +end + +ActionView::Base.class_eval do + include ActiveScaffold::Bridges::CalendarDateSelect::FormColumnHelpers + include ActiveScaffold::Bridges::Shared::DateBridge::SearchColumnHelpers + alias_method :active_scaffold_search_calendar_date_select, :active_scaffold_search_date_bridge + include ActiveScaffold::Bridges::Shared::DateBridge::HumanConditionHelpers + alias_method :active_scaffold_human_condition_calendar_date_select, :active_scaffold_human_condition_date_bridge + include ActiveScaffold::Bridges::CalendarDateSelect::SearchColumnHelpers +end + +ActiveScaffold::Finder::ClassMethods.module_eval do + include ActiveScaffold::Bridges::Shared::DateBridge::Finder::ClassMethods + alias_method :condition_for_calendar_date_select_type, :condition_for_date_bridge_type +end diff --git a/lib/active_scaffold/bridges/cancan.rb b/lib/active_scaffold/bridges/cancan.rb new file mode 100644 index 0000000000..37c7536560 --- /dev/null +++ b/lib/active_scaffold/bridges/cancan.rb @@ -0,0 +1,15 @@ +class ActiveScaffold::Bridges::Cancan < ActiveScaffold::DataStructures::Bridge + def self.install + require File.join(File.dirname(__FILE__), "cancan", "cancan_bridge.rb") + + ActiveScaffold::ClassMethods.send :include, ActiveScaffold::Bridges::Cancan::ClassMethods + ActiveScaffold::Actions::Core.send :include, ActiveScaffold::Bridges::Cancan::Actions::Core + ActiveScaffold::Actions::Nested.send :include, ActiveScaffold::Bridges::Cancan::Actions::Core + ActionController::Base.send :include, ActiveScaffold::Bridges::Cancan::ModelUserAccess::Controller + ::ActiveRecord::Base.send :include, ActiveScaffold::Bridges::Cancan::ModelUserAccess::Model + ::ActiveRecord::Base.send :include, ActiveScaffold::Bridges::Cancan::ActiveRecord + end + def self.install? + Object.const_defined? 'CanCan' + end +end diff --git a/lib/active_scaffold/bridges/cancan/cancan_bridge.rb b/lib/active_scaffold/bridges/cancan/cancan_bridge.rb new file mode 100644 index 0000000000..bcf4ac31e5 --- /dev/null +++ b/lib/active_scaffold/bridges/cancan/cancan_bridge.rb @@ -0,0 +1,126 @@ +# Allow users to easily define aliases for AS actions. +# Ability#as_action_aliases should be called by the user in his ability class +# +# class Ability < CanCan::Ability +# def initialize(user) +# as_action_aliases +# end +# end +# +module CanCan + module Ability + def as_action_aliases + alias_action :list, :row, :show_search, :render_field, :to => :read + alias_action :update_column, :edit_associated, :new_existing, :add_existing, :to => :update + alias_action :delete, :destroy_existing, :to => :destroy + end + end +end + +module ActiveScaffold::Bridges + class Cancan + + # controller level authorization + # As already has callbacks to ensure authorization at controller method via "authorization_method" + # but let's include this too, just in case, no sure how performance is affected tough :TODO benchmark + module ClassMethods + extend ActiveSupport::Concern + included do + alias_method_chain :active_scaffold, :cancan + end + + def active_scaffold_with_cancan(model_id = nil, &block) + active_scaffold_without_cancan(model_id, &block) + authorize_resource( + :class => active_scaffold_config.model, + :instance => :record + ) + end + end + + # beginning of chain integration + module Actions + module Core + extend ActiveSupport::Concern + included do + alias_method_chain :beginning_of_chain, :cancan + end + # :TODO can this be expanded more ? + def beginning_of_chain_with_cancan + beginning_of_chain_without_cancan.accessible_by(current_ability) + end + end + end + + # This is a module aimed at making the current_ability available to ActiveRecord models for permissions. + module ModelUserAccess + module Controller + extend ActiveSupport::Concern + included do + prepend_before_filter :assign_current_ability_to_models + end + + # We need to give the ActiveRecord classes a handle to the current ability. We don't want to just pass the object, + # because the object may change (someone may log in or out). So we give ActiveRecord a proc that ties to the + # current_ability_method on this ApplicationController. + def assign_current_ability_to_models + ::ActiveRecord::Base.current_ability_proc = proc {send(:current_ability)} + end + end + + module Model + extend ActiveSupport::Concern + + module ClassMethods + # The proc to call that retrieves the current_ability from the ApplicationController. + attr_accessor :current_ability_proc + + # Class-level access to the current ability + def current_ability + ::ActiveRecord::Base.current_ability_proc.call if ::ActiveRecord::Base.current_ability_proc + end + end + + # Instance-level access to the current ability + def current_ability; self.class.current_ability end + end + end + + + # plug into AS#authorized_for calls + module ActiveRecord + extend ActiveSupport::Concern + included do + extend SecurityMethods + include SecurityMethods + alias_method_chain :authorized_for?, :cancan + class << self + alias_method_chain :authorized_for?, :cancan + end + end + + module SecurityMethods + class InvalidArgument < StandardError; end + + # is usually called with :crud_type and :column, or :action + # {:crud_type=>:update, :column=>"some_colum_name"} + # {:action=>"edit"} + # to allow access cancan must allow both :crud_type and :action + # if cancan says "no", it delegates to default AS behavior + def authorized_for_with_cancan?(options = {}) + raise InvalidArgument if options[:crud_type].blank? and options[:action].blank? + if current_ability.present? + crud_type_result = options[:crud_type].nil? ? true : current_ability.can?(options[:crud_type], self) + action_result = options[:action].nil? ? true : current_ability.can?(options[:action].to_sym, self) + else + crud_type_result, action_result = false, false + end + default_result = authorized_for_without_cancan?(options) + result = (crud_type_result && action_result) || default_result + return result + end + end + end + + end +end diff --git a/lib/active_scaffold/bridges/carrierwave.rb b/lib/active_scaffold/bridges/carrierwave.rb new file mode 100644 index 0000000000..f6c6c4117e --- /dev/null +++ b/lib/active_scaffold/bridges/carrierwave.rb @@ -0,0 +1,12 @@ +class ActiveScaffold::Bridges::Carrierwave < ActiveScaffold::DataStructures::Bridge + def self.install + require File.join(File.dirname(__FILE__), "carrierwave/form_ui") + require File.join(File.dirname(__FILE__), "carrierwave/list_ui") + require File.join(File.dirname(__FILE__), "carrierwave/carrierwave_bridge_helpers") + require File.join(File.dirname(__FILE__), "carrierwave/carrierwave_bridge") + ActiveScaffold::Config::Core.send :include, ActiveScaffold::Bridges::Carrierwave::CarrierwaveBridge + end + def self.install? + Object.const_defined? 'CarrierWave' + end +end diff --git a/lib/active_scaffold/bridges/carrierwave/carrierwave_bridge.rb b/lib/active_scaffold/bridges/carrierwave/carrierwave_bridge.rb new file mode 100644 index 0000000000..f426f173ca --- /dev/null +++ b/lib/active_scaffold/bridges/carrierwave/carrierwave_bridge.rb @@ -0,0 +1,31 @@ +module ActiveScaffold + module Bridges + class Carrierwave + module CarrierwaveBridge + def initialize_with_carrierwave(model_id) + initialize_without_carrierwave(model_id) + return unless self.model.respond_to?(:uploaders) && self.model.uploaders.present? + + self.update.multipart = true + self.create.multipart = true + + self.model.uploaders.keys.each do |field| + configure_carrierwave_field(field.to_sym) + end + end + + def self.included(base) + base.alias_method_chain :initialize, :carrierwave + end + + private + def configure_carrierwave_field(field) + self.columns << field + self.columns[field].form_ui ||= :carrierwave # :TODO thumbnail + self.columns[field].params.add "#{field}_cache" + self.columns[field].params.add "remove_#{field}" + end + end + end + end +end diff --git a/lib/active_scaffold/bridges/carrierwave/carrierwave_bridge_helpers.rb b/lib/active_scaffold/bridges/carrierwave/carrierwave_bridge_helpers.rb new file mode 100644 index 0000000000..02a4d9e55f --- /dev/null +++ b/lib/active_scaffold/bridges/carrierwave/carrierwave_bridge_helpers.rb @@ -0,0 +1,10 @@ +module ActiveScaffold + module Bridges + class Carrierwave + module CarrierwaveBridgeHelpers + mattr_accessor :thumbnail_style + self.thumbnail_style = :thumbnail + end + end + end +end diff --git a/lib/active_scaffold/bridges/carrierwave/form_ui.rb b/lib/active_scaffold/bridges/carrierwave/form_ui.rb new file mode 100644 index 0000000000..6cb36f9a4b --- /dev/null +++ b/lib/active_scaffold/bridges/carrierwave/form_ui.rb @@ -0,0 +1,45 @@ +module ActiveScaffold + module Helpers + module FormColumnHelpers + def active_scaffold_input_carrierwave(column, options) + options = active_scaffold_input_text_options(options.merge(column.options)) + carrierwave = @record.send("#{column.name}") + if !carrierwave.file.blank? + + remove_field_options = { + :name => options[:name].gsub(/\[#{column.name}\]$/, "[remove_#{column.name}]"), + :id => 'remove_' + options[:id], + :value => false + } + + cache_field_options = { + :name => options[:name].gsub(/\[#{column.name}\]$/, "[#{column.name}_cache]"), + :id => options[:id] + '_cache' + } + + if ActiveScaffold.js_framework == :jquery + js_remove_file_code = "$(this).prev('input#remove_#{options[:id]}').val('true'); $(this).parent().hide().next().show(); return false;"; + js_dont_remove_file_code = "$(this).parents('div.carrierwave_controls').find('input#remove_#{options[:id]}').val('false'); return false;"; + else + js_remove_file_code = "$(this).previous('input#remove_#{options[:id]}').value='true'; $(this).up().hide().next().show(); return false;"; + js_dont_remove_file_code = "$(this).up('div.carrierwave_controls').down('input#remove_#{options[:id]}').value='false'; return false;"; + end + + input = file_field(:record, column.name, options.merge(:onchange => js_dont_remove_file_code)) + content_tag( :div, + content_tag(:div, ( + get_column_value(@record, column) + " | " + + hidden_field(:record, "#{column.name}_cache", cache_field_options) + + hidden_field(:record, "remove_#{column.name}", remove_field_options) + + content_tag(:a, as_(:remove_file), {:href => '#', :onclick => js_remove_file_code}) + ).html_safe + ) + content_tag(:div, input, :style => "display: none"), + :class => 'carrierwave_controls' + ) + else + file_field(:record, column.name, options) + end + end + end + end +end diff --git a/lib/active_scaffold/bridges/carrierwave/list_ui.rb b/lib/active_scaffold/bridges/carrierwave/list_ui.rb new file mode 100644 index 0000000000..ba799242df --- /dev/null +++ b/lib/active_scaffold/bridges/carrierwave/list_ui.rb @@ -0,0 +1,17 @@ +module ActiveScaffold + module Helpers + module ListColumnHelpers + def active_scaffold_column_carrierwave(record, column) + carrierwave = record.send("#{column.name}") + return nil unless !carrierwave.file.blank? + thumbnail_style = ActiveScaffold::Bridges::Carrierwave::CarrierwaveBridgeHelpers.thumbnail_style + content = if carrierwave.versions.keys.include?(thumbnail_style) + image_tag(carrierwave.url(thumbnail_style), :border => 0).html_safe + else + record.send(record.send(:_mounter, column.name).send(:serialization_column)) + end + link_to(content, carrierwave.url, :target => '_blank') + end + end + end +end diff --git a/lib/active_scaffold/bridges/chosen.rb b/lib/active_scaffold/bridges/chosen.rb new file mode 100644 index 0000000000..59705d7cc7 --- /dev/null +++ b/lib/active_scaffold/bridges/chosen.rb @@ -0,0 +1,14 @@ +class ActiveScaffold::Bridges::Chosen < ActiveScaffold::DataStructures::Bridge + def self.install + require File.join(File.dirname(__FILE__), "chosen/helpers.rb") + end + def self.install? + super && [:jquery, :prototype].include?(ActiveScaffold.js_framework) + end + def self.stylesheets + 'chosen' + end + def self.javascripts + ["chosen-#{ActiveScaffold.js_framework}", "#{ActiveScaffold.js_framework}/active_scaffold_chosen"] + end +end diff --git a/lib/active_scaffold/bridges/chosen/helpers.rb b/lib/active_scaffold/bridges/chosen/helpers.rb new file mode 100644 index 0000000000..13f862cef0 --- /dev/null +++ b/lib/active_scaffold/bridges/chosen/helpers.rb @@ -0,0 +1,48 @@ +class ActiveScaffold::Bridges::Chosen + module Helpers + def self.included(base) + base.class_eval do + include FormColumnHelpers + include SearchColumnHelpers + end + end + + module FormColumnHelpers + # requires RecordSelect plugin to be installed and configured. + def active_scaffold_input_chosen(column, html_options) + html_options[:class] << ' chosen' + if column.plural_association? + associated_options, select_options = active_scaffold_plural_association_options(column) + options = {:selected => associated_options.collect {|a| a[1]}, :include_blank => as_(:_select_)} + + html_options.update(:multiple => true).update(column.options[:html_options] || {}) + options.update(column.options) + html_options[:name] = "#{html_options[:name]}[]" if html_options[:multiple] == true && !html_options[:name].to_s.ends_with?("[]") + + if optgroup = options.delete(:optgroup) + select(:record, column.name, active_scaffold_grouped_options(column, select_options, optgroup), options, html_options) + else + collection_select(:record, column.name, select_options, :id, :to_label, options, html_options) + end + else + active_scaffold_input_select(column, html_options) + end + end + end + + module SearchColumnHelpers + def active_scaffold_search_chosen(column, options) + options[:class] << ' chosen' + active_scaffold_search_select(column, options) + end + + def active_scaffold_search_multi_chosen(column, options) + options[:class] << ' chosen' + options[:multiple] = true + active_scaffold_search_select(column, options) + end + end + end +end + +ActionView::Base.class_eval { include ActiveScaffold::Bridges::Chosen::Helpers } diff --git a/lib/active_scaffold/bridges/country_helper.rb b/lib/active_scaffold/bridges/country_helper.rb new file mode 100644 index 0000000000..0263910cc3 --- /dev/null +++ b/lib/active_scaffold/bridges/country_helper.rb @@ -0,0 +1,9 @@ +class ActiveScaffold::Bridges::CountryHelper < ActiveScaffold::DataStructures::Bridge + def self.install + require File.join(File.dirname(__FILE__), "country_helper/country_helper_bridge.rb") + end + + def self.install? + true + end +end diff --git a/lib/active_scaffold/helpers/country_helpers.rb b/lib/active_scaffold/bridges/country_helper/country_helper_bridge.rb similarity index 89% rename from lib/active_scaffold/helpers/country_helpers.rb rename to lib/active_scaffold/bridges/country_helper/country_helper_bridge.rb index eaffc7ab65..2c9c38e442 100644 --- a/lib/active_scaffold/helpers/country_helpers.rb +++ b/lib/active_scaffold/bridges/country_helper/country_helper_bridge.rb @@ -1,5 +1,5 @@ -module ActiveScaffold - module Helpers +module ActiveScaffold::Bridges + class CountryHelper module CountryHelpers # Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags. def country_select(object, method, priority_countries = nil, options = {}, html_options = {}) @@ -16,14 +16,13 @@ def usa_state_select(object, method, priority_states = nil, options = {}, html_o # # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. def country_options_for_select(selected = nil, priority_countries = nil) - country_options = "" - if priority_countries - country_options += options_for_select(priority_countries, selected) - country_options += "\n" + country_options = options_for_select(priority_countries.collect {|country| [I18n.t("countries.#{country}", :default => country.to_s.titleize), country.to_s]} + [['-------------', '']], :selected => selected, :disabled => '') + else + country_options = "" end - return country_options + options_for_select(COUNTRIES.collect {|country| [I18n.t("countries.#{country}", :default => country.to_s.titleize), country]}, selected) + return country_options + options_for_select(COUNTRIES.collect {|country| [I18n.t("countries.#{country}", :default => country.to_s.titleize), country.to_s]}, :selected => selected) end # Returns a string of option tags for the states in the United States. Supply a state name as +selected to @@ -31,16 +30,16 @@ def country_options_for_select(selected = nil, priority_countries = nil) # in case you want to highligh a local area # NOTE: Only the option tags are returned from this method, wrap it in a or a collection of checkboxes to (dis)associate records - attr_writer :form_ui + def form_ui=(value) + self.list_method = nil if @list_ui.nil? && value != @form_ui + @form_ui = value + end def form_ui @form_ui end - attr_writer :list_ui + def list_ui=(value) + self.list_method = nil if value != @list_ui + @list_ui = value + end + def list_ui @list_ui || @form_ui end attr_writer :search_ui def search_ui - @search_ui || @form_ui - end - - # DEPRECATED - alias :ui_type :form_ui - def ui_type=(val) - ::ActiveSupport::Deprecation.warn("config.columns[:#{name}].ui_type will disappear in version 2.0. Please use config.columns[:#{name}].form_ui instead.", caller) - self.form_ui = val + @search_ui || @form_ui || (@association && !polymorphic_association? ? :select : nil) end # a place to store dev's column specific options attr_accessor :options def options - @options || {} + @options ||= {} end - # associate an action_link with this column - attr_reader :link - - # set an action_link to nested list or inline form in this column - attr_reader :autolink - - # this should not only delete any existing link but also prevent column links from being automatically added by later routines - def clear_link - @link = nil - @autolink = false + def link + @link = @link.call(self) if @link.is_a? Proc + @link end + # associate an action_link with this column def set_link(action, options = {}) - if action.is_a? ActiveScaffold::DataStructures::ActionLink + if action.is_a?(ActiveScaffold::DataStructures::ActionLink) || (action.is_a? Proc) @link = action else options[:label] ||= self.label options[:position] ||= :after unless options.has_key?(:position) - options[:type] ||= :record + options[:type] ||= :member @link = ActiveScaffold::DataStructures::ActionLink.new(action, options) end end + # set an action_link to nested list or inline form in this column + def autolink? + @autolink + end + + # this should not only delete any existing link but also prevent column links from being automatically added by later routines + def clear_link + @link = nil + @autolink = false + end + # define a calculation for the column. anything that ActiveRecord::Calculations::ClassMethods#calculate accepts will do. attr_accessor :calculate @@ -135,19 +175,44 @@ def calculation? # a collection of associations to pre-load when finding the records on a page attr_reader :includes def includes=(value) - @includes = value.is_a?(Array) ? value : [value] # automatically convert to an array + @includes = case value + when Array then value + else value ? [value] : value # not convert nil to [nil] + end + end + + # a collection of associations to do left join when this column is included on search + def search_joins + @search_joins || @includes + end + + def search_joins=(value) + @search_joins = case value + when Array then value + else [value] # automatically convert to an array + end end + # a collection of columns to load when eager loading is disabled, if it's nil all columns will be loaded + attr_accessor :select_associated_columns + # describes how to search on a column # search = true default, uses intelligent search sql # search = "CONCAT(a, b)" define your own sql for searching. this should be the "left-side" of a WHERE condition. the operator and value will be supplied by ActiveScaffold. - attr_writer :search_sql + # search = [:a, :b] searches in both fields + def search_sql=(value) + @search_sql = if value + (value == true || value.is_a?(Proc)) ? value : Array(value) + else + value + end + end def search_sql self.initialize_search_sql if @search_sql === true @search_sql end def searchable? - search_sql != false && search_sql != nil + !!search_sql end # to modify the default order of columns @@ -169,12 +234,21 @@ def associated_number? # whether a blank row must be shown in the subform cattr_accessor :show_blank_record @@show_blank_record = true - attr_accessor :show_blank_record + attr_writer :show_blank_record + def show_blank_record?(associated) + if @show_blank_record + return false unless self.association.klass.authorized_for?(:crud_type => :create) and not self.association.options[:readonly] + self.plural_association? or (self.singular_association? and associated.blank?) + end + end # methods for automatic links in singular association columns cattr_accessor :actions_for_association_links @@actions_for_association_links = [:new, :edit, :show] attr_accessor :actions_for_association_links + + cattr_accessor :association_form_ui + @@association_form_ui = nil # ----------------------------------------------------------------- # # the below functionality is intended for internal consumption only # @@ -197,11 +271,25 @@ def through_association? def polymorphic_association? self.association and self.association.options.has_key? :polymorphic and self.association.options[:polymorphic] end + def readonly_association? + if self.association + if self.association.options.has_key? :readonly + self.association.options[:readonly] + else + self.through_association? + end + end + end # an interpreted property. the column is virtual if it isn't from the active record model or any associated models def virtual? column.nil? && association.nil? end + + attr_writer :number + def number? + @number + end # this is so that array.delete and array.include?, etc., will work by column name def ==(other) #:nodoc: @@ -220,40 +308,66 @@ def ==(other) #:nodoc: # instantiation is handled internally through the DataStructures::Columns object def initialize(name, active_record_class) #:nodoc: self.name = name.to_sym + @tableless = active_record_class < ActiveScaffold::Tableless @column = active_record_class.columns_hash[self.name.to_s] @association = active_record_class.reflect_on_association(self.name) @autolink = !@association.nil? @active_record_class = active_record_class @table = active_record_class.table_name - @weight = 0 @associated_limit = self.class.associated_limit @associated_number = self.class.associated_number @show_blank_record = self.class.show_blank_record - @actions_for_association_links = self.class.actions_for_association_links if @association - - # default all the configurable variables - self.css_class = '' - if active_record_class.respond_to? :reflect_on_validations_for - column_names = [name] - column_names << @association.primary_key_name if @association - self.required = column_names.any? do |column_name| - active_record_class.reflect_on_validations_for(column_name.to_sym).any? do |val| - val.macro == :validates_presence_of or (val.macro == :validates_inclusion_of and not val.options[:allow_nil] and not val.options[:allow_blank]) + @send_form_on_update_column = self.class.send_form_on_update_column + @actions_for_association_links = self.class.actions_for_association_links.clone if @association + @select_columns = if @association.nil? && @column + [field] + elsif polymorphic_association? + [field, quoted_field(@active_record_class.connection.quote_column_name(@association.foreign_type))] + elsif @association + if self.association.macro == :belongs_to + [field] + else + columns = [] + if active_record_class.columns_hash[count_column = "#{@association.name}_count"] + columns << quoted_field(@active_record_class.connection.quote_column_name(count_column)) + end + if @association.through_reflection.try(:macro) == :belongs_to + columns << quoted_field(@active_record_class.connection.quote_column_name(@association.through_reflection.foreign_key)) end + columns end - else - self.required = false + end + + self.number = @column.try(:number?) + @options = {:format => :i18n_number} if self.number? + @form_ui = :checkbox if @column and @column.type == :boolean + @form_ui = :textarea if @column and @column.type == :text + @form_ui = :number if @column and self.number? + @allow_add_existing = true + @form_ui = self.class.association_form_ui if @association && self.class.association_form_ui + + # default all the configurable variables + self.css_class = '' + self.required = active_record_class.validators_on(self.name).any? do |val| + !val.options[:if] && !val.options[:unless] && (ActiveModel::Validations::PresenceValidator === val || + (ActiveModel::Validations::InclusionValidator === val && !val.options[:allow_nil] && !val.options[:allow_blank]) + ) end self.sort = true self.search_sql = true + + @weight = estimate_weight - self.includes = (association and not polymorphic_association?) ? [association.name] : [] + if association && !polymorphic_association? + self.includes = [association.name] + self.search_joins = self.includes.clone + end end # just the field (not table.field) def field_name return nil if virtual? - column ? @active_record_class.connection.quote_column_name(column.name) : association.primary_key_name + @field_name ||= column ? @active_record_class.connection.quote_column_name(column.name) : association.foreign_key end def <=>(other_column) @@ -261,42 +375,83 @@ def <=>(other_column) order_weight != 0 ? order_weight : self.name.to_s <=> other_column.name.to_s end + def number_to_native(value) + return value if value.blank? || !value.is_a?(String) + native = '.' # native ruby separator + format = {:separator => '', :delimiter => ''}.merge! I18n.t('number.format', :default => {}) + specific = case self.options[:format] + when :currency + I18n.t('number.currency.format', :default => nil) + when :size + I18n.t('number.human.format', :default => nil) + when :percentage + I18n.t('number.percentage.format', :default => nil) + end + format.merge! specific unless specific.nil? + unless format[:separator].blank? || !value.include?(format[:separator]) && value.include?(native) && (format[:delimiter] != native || value !~ /\.\d{3}$/) + value.gsub(/[^0-9\-#{format[:separator]}]/, '').gsub(format[:separator], native) + else + value + end + end + + # to cache method to get value in list + attr_accessor :list_method + + # cache constraints for numeric columns (get in ActiveScaffold::Helpers::FormColumnHelpers::numerical_constraints_for_column) + attr_accessor :numerical_constraints + + # the table.field name for this column, if applicable + def field + @field ||= quoted_field(field_name) + end + protected + def quoted_field(name) + [@active_record_class.quoted_table_name, name].join('.') + end + def initialize_sort if self.virtual? # we don't automatically enable method sorting for virtual columns because it's slow, and we expect fewer complaints this way. self.sort = false else - if self.singular_association? - self.sort = {:method => "#{self.name}.to_s"} - elsif self.plural_association? - self.sort = {:method => "#{self.name}.join(',')"} - else + if column && !@tableless self.sort = {:sql => self.field} + else + self.sort = false end end end - + def initialize_search_sql - if self.virtual? - self.search_sql = nil - else + self.search_sql = unless self.virtual? if association.nil? - self.search_sql = self.field.to_s - else - # with associations we really don't know what to sort by without developer intervention. we could sort on the primary key ('id'), but that's hardly useful. previously ActiveScaffold would try and search using the same sql as from :sort, but we decided to just punt. - self.search_sql = nil + self.field.to_s unless @tableless + elsif !self.polymorphic_association? + [association.klass.quoted_table_name, association.klass.quoted_primary_key].join('.') unless association.klass < ActiveScaffold::Tableless end end end # the table name from the ActiveRecord class attr_reader :table - - # the table.field name for this column, if applicable - def field - @field ||= [@table, field_name].join('.') + + def estimate_weight + if singular_association? + 400 + elsif plural_association? + 500 + elsif [:created_at, :updated_at].include?(self.name) + 600 + elsif [:name, :label, :title].include?(self.name) + 100 + elsif required? + 200 + else + 300 + end end end end diff --git a/lib/active_scaffold/data_structures/nested_info.rb b/lib/active_scaffold/data_structures/nested_info.rb new file mode 100644 index 0000000000..31e14e10af --- /dev/null +++ b/lib/active_scaffold/data_structures/nested_info.rb @@ -0,0 +1,158 @@ +module ActiveScaffold::DataStructures + class NestedInfo + def self.get(model, params) + nested_info = {} + begin + unless params[:association].nil? + ActiveScaffold::DataStructures::NestedInfoAssociation.new(model, params) + else + ActiveScaffold::DataStructures::NestedInfoScope.new(model, params) + end + rescue ActiveScaffold::ControllerNotFound + nil + end + end + + attr_accessor :association, :child_association, :parent_model, :parent_scaffold, :parent_id, :param_name, :constrained_fields, :scope + + def initialize(model, params) + @parent_scaffold = "#{params[:parent_scaffold].to_s.camelize}Controller".constantize + @parent_model = @parent_scaffold.active_scaffold_config.model + end + + def to_params + {:parent_scaffold => parent_scaffold.controller_path} + end + + def new_instance? + result = @new_instance.nil? + @new_instance = false + result + end + + def habtm? + false + end + + def has_many? + false + end + + def belongs_to? + false + end + + def has_one? + false + end + + def singular_association? + belongs_to? || has_one? + end + + def plural_association? + has_many? || habtm? + end + + def readonly_through_association? + false + end + + def through_association? + false + end + + def readonly? + false + end + + def sorted? + false + end + end + + class NestedInfoAssociation < NestedInfo + def initialize(model, params) + super + @association = parent_model.reflect_on_association(params[:association].to_sym) + @param_name = @association.active_record.name.foreign_key.to_sym + @parent_id = params[@param_name] + iterate_model_associations(model) + end + + def name + self.association.name + end + + def has_many? + association.macro == :has_many + end + + def habtm? + association.macro == :has_and_belongs_to_many + end + + def belongs_to? + association.belongs_to? + end + + def has_one? + association.macro == :has_one + end + + # A through association with has_one or has_many as source association + # create cannot be called in such through association + def readonly_through_association? + association.options[:through] && association.source_reflection.macro != :belongs_to + end + + def through_association? + association.options[:through] + end + + def readonly? + association.options[:readonly] + end + + def sorted? + association.options.has_key? :order + end + + def default_sorting + association.options[:order] + end + + def to_params + super.merge(:association => @association.name, :assoc_id => parent_id) + end + + protected + + def iterate_model_associations(model) + @constrained_fields = [] + constrained_fields << association.foreign_key.to_sym unless association.belongs_to? + if reverse = association.reverse(model) + @child_association = model.reflect_on_association(reverse) + constrained_fields << @child_association.name unless @child_association == association + end + end + end + + class NestedInfoScope < NestedInfo + def initialize(model, params) + super + @scope = params[:named_scope].to_sym + @param_name = parent_model.name.foreign_key.to_sym + @parent_id = params[@param_name] + @constrained_fields = [] + end + + def to_params + super.merge(:named_scope => @scope) + end + + def name + self.scope + end + end +end diff --git a/lib/active_scaffold/data_structures/set.rb b/lib/active_scaffold/data_structures/set.rb index b25cdb24e8..29cbb43b1d 100644 --- a/lib/active_scaffold/data_structures/set.rb +++ b/lib/active_scaffold/data_structures/set.rb @@ -3,12 +3,11 @@ class Set include Enumerable include ActiveScaffold::Configurable - attr_writer :label - def label - as_(@label) + def initialize(*args) + set_values(*args) end - def initialize(*args) + def set_values(*args) @set = [] self.add *args end @@ -59,4 +58,4 @@ def empty? end end -end \ No newline at end of file +end diff --git a/lib/active_scaffold/data_structures/sorting.rb b/lib/active_scaffold/data_structures/sorting.rb index e5af042f55..e71d2e41fe 100644 --- a/lib/active_scaffold/data_structures/sorting.rb +++ b/lib/active_scaffold/data_structures/sorting.rb @@ -3,11 +3,33 @@ module ActiveScaffold::DataStructures class Sorting include Enumerable + attr_accessor :constraint_columns + def initialize(columns) @columns = columns @clauses = [] + @constraint_columns = [] + end + + def set_default_sorting(model) + model_scope = model.send(:build_default_scope) + order_clause = model_scope.arel.order_clauses.join(",") if model_scope + + # If an ORDER BY clause is found set default sorting according to it, else + # fallback to setting primary key ordering + if order_clause + set_sorting_from_order_clause(order_clause, model.table_name) + @default_sorting = true + else + set(model.primary_key, 'ASC') if model.column_names.include?(model.primary_key) + end end + def set_nested_sorting(table_name, order_clause) + clear + set_sorting_from_order_clause(order_clause, table_name) + end + # add a clause to the sorting, assuming the column is sortable def add(column_name, direction = nil) direction ||= 'ASC' @@ -15,7 +37,7 @@ def add(column_name, direction = nil) column = get_column(column_name) raise ArgumentError, "Could not find column #{column_name}" if column.nil? raise ArgumentError, "Sorting direction unknown" unless [:ASC, :DESC].include? direction.to_sym - @clauses << [column, direction] if column.sortable? + @clauses << [column, direction.untaint] if column.sortable? raise ArgumentError, "Can't mix :method- and :sql-based sorting" if mixed_sorting? end @@ -32,6 +54,7 @@ def set(*args) # clears the sorting def clear + @default_sorting = false @clauses = [] end @@ -65,6 +88,23 @@ def first @clauses.first end + # builds an order-by clause + def clause + return nil if sorts_by_method? || default_sorting? + + # unless the sorting is by method, create the sql string + order = [] + each do |sort_column, sort_direction| + next if constraint_columns.include? sort_column.name + sql = sort_column.sort[:sql] + next if sql.nil? or sql.empty? + + order << Array(sql).map {|column| "#{column} #{sort_direction}"}.join(', ') + end + + order unless order.empty? + end + protected # retrieves the sorting clause for the given column @@ -85,5 +125,48 @@ def get_column(name_or_column) def mixed_sorting? sorts_by_method? and sorts_by_sql? end + + def default_sorting? + @default_sorting + end + + def set_sorting_from_order_clause(order_clause, model_table_name = nil) + clear + order_clause.to_s.split(',').each do |criterion| + unless criterion.blank? + order_parts = extract_order_parts(criterion) + add(order_parts[:column_name], order_parts[:direction]) unless different_table?(model_table_name, order_parts[:table_name]) + end + end + end + + def extract_order_parts(criterion_parts) + column_name_part, direction_part = criterion_parts.strip.split(' ') + column_name_parts = column_name_part.split('.') + order = {:direction => extract_direction(direction_part), + :column_name => remove_quotes(column_name_parts.last)} + order[:table_name] = remove_quotes(column_name_parts[-2]) if column_name_parts.length >= 2 + order + end + + def different_table?(model_table_name, order_table_name) + !order_table_name.nil? && model_table_name != order_table_name + end + + def remove_quotes(sql_name) + if sql_name.starts_with?('"') || sql_name.starts_with?('`') + sql_name[1, (sql_name.length - 2)] + else + sql_name + end + end + + def extract_direction(direction_part) + if direction_part.to_s.upcase == 'DESC' + 'DESC' + else + 'ASC' + end + end end -end \ No newline at end of file +end diff --git a/lib/active_scaffold/engine.rb b/lib/active_scaffold/engine.rb new file mode 100644 index 0000000000..9910f48a5c --- /dev/null +++ b/lib/active_scaffold/engine.rb @@ -0,0 +1,4 @@ +module ActiveScaffold + class Engine < ::Rails::Engine + end +end diff --git a/lib/extensions/action_controller_rendering.rb b/lib/active_scaffold/extensions/action_controller_rendering.rb similarity index 69% rename from lib/extensions/action_controller_rendering.rb rename to lib/active_scaffold/extensions/action_controller_rendering.rb index eaadda40ad..da5e5e7e42 100644 --- a/lib/extensions/action_controller_rendering.rb +++ b/lib/active_scaffold/extensions/action_controller_rendering.rb @@ -2,19 +2,19 @@ module ActionController #:nodoc: class Base def render_with_active_scaffold(*args, &block) - if self.class.uses_active_scaffold? and params[:adapter] and @rendering_adapter.nil? + if self.class.uses_active_scaffold? and params[:adapter] and @rendering_adapter.nil? and request.xhr? @rendering_adapter = true # recursion control # if we need an adapter, then we render the actual stuff to a string and insert it into the adapter template + opts = args.blank? ? Hash.new : args.first render :partial => params[:adapter][1..-1], - :locals => {:payload => render_to_string(args.first, &block)}, - :use_full_path => true, :layout => false + :locals => {:payload => render_to_string(opts.merge(:layout => false), &block).html_safe}, + :use_full_path => true, :layout => false, :content_type => :html @rendering_adapter = nil # recursion control else render_without_active_scaffold(*args, &block) end end alias_method_chain :render, :active_scaffold - - # Rails 2.x implementation is post-initialization on :active_scaffold method end end + diff --git a/lib/active_scaffold/extensions/action_controller_rescueing.rb b/lib/active_scaffold/extensions/action_controller_rescueing.rb new file mode 100644 index 0000000000..69745f1396 --- /dev/null +++ b/lib/active_scaffold/extensions/action_controller_rescueing.rb @@ -0,0 +1,8 @@ +module ActionController #:nodoc: + class Base + # adding to ActionController::Base so it can overrided in ApplicationController + def deny_access + head :unauthorized + end + end +end diff --git a/lib/active_scaffold/extensions/action_view_rendering.rb b/lib/active_scaffold/extensions/action_view_rendering.rb new file mode 100644 index 0000000000..fc7b2c3c54 --- /dev/null +++ b/lib/active_scaffold/extensions/action_view_rendering.rb @@ -0,0 +1,135 @@ +module ActionView + class LookupContext + attr_accessor :last_template + + def find_template_with_last_template(name, prefixes = [], partial = false, keys = [], options = {}) + self.last_template = find_template_without_last_template(name, prefixes, partial, keys, options) + end + alias_method_chain :find_template, :last_template + end +end + +# wrap the action rendering for ActiveScaffold views +module ActionView::Helpers #:nodoc: + module RenderingHelper + # + # Adds two rendering options. + # + # ==render :super + # + # This syntax skips all template overrides and goes directly to the provided ActiveScaffold templates. + # Useful if you want to wrap an existing template. Just call super! + # + # ==render :active_scaffold => #{controller.to_s}, options = {}+ + # + # Lets you embed an ActiveScaffold by referencing the controller where it's configured. + # + # You may specify options[:constraints] for the embedded scaffold. These constraints have three effects: + # * the scaffold's only displays records matching the constraint + # * all new records created will be assigned the constrained values + # * constrained columns will be hidden (they're pretty boring at this point) + # + # You may also specify options[:conditions] for the embedded scaffold. These only do 1/3 of what + # constraints do (they only limit search results). Any format accepted by ActiveRecord::Base.find is valid. + # + # Defining options[:label] lets you completely customize the list title for the embedded scaffold. + # + def render_with_active_scaffold(*args, &block) + if args.first.is_a? Hash and args.first[:active_scaffold] + require 'digest/md5' + options = args.first + + remote_controller = options[:active_scaffold] + constraints = options[:constraints] + conditions = options[:conditions] + eid = Digest::MD5.hexdigest(params[:controller] + remote_controller.to_s + constraints.to_s + conditions.to_s) + eid_info = session["as:#{eid}"] ||= {} + if constraints + eid_info[:constraints] = constraints + else + eid_info.delete :constraints + end + if conditions + eid_info[:conditions] = conditions + else + eid_info.delete :conditions + end + if options[:label] + eid_info[:list] = {:label => options[:label]} + else + eid_info.delete :list + end + session.delete "as:#{eid}" if eid_info.empty? + options[:params] ||= {} + options[:params].merge! :eid => eid, :embedded => true + + id = "as_#{eid}-embedded" + url_options = {:controller => remote_controller.to_s, :action => 'index'}.merge(options[:params]) + + if controller.respond_to?(:render_component_into_view, true) + controller.send(:render_component_into_view, url_options) + else + url = url_for(url_options) + content_tag(:div, :id => id, :class => 'active-scaffold-component', :data => {:refresh => url}) do + # parse the ActiveRecord model name from the controller path, which + # might be a namespaced controller (e.g., 'admin/admins') + model = remote_controller.to_s.sub(/.*\//, '').singularize + content_tag(:div, :class => 'active-scaffold-header') do + content_tag :h2, link_to(args.first[:label] || active_scaffold_config_for(model).list.label, url, :remote => true) + end << + if ActiveScaffold.js_framework == :prototype + javascript_tag("new Ajax.Updater('#{id}', '#{url}', {method: 'get', evalScripts: true});") + elsif ActiveScaffold.js_framework == :jquery + javascript_tag("jQuery('##{id}').load('#{url}', function() { $(this).trigger('as:element_updated'); });") + end + end + end + + elsif args.first == :super + @_view_paths ||= lookup_context.view_paths.clone + parts = @virtual_path.split('/') + template = parts.pop + prefix = parts.join('/') + + options = args[1] || {} + options[:locals] ||= {} + if view_stack.last + options[:locals] = view_stack.last[:locals].merge!(options[:locals]) if view_stack.last[:locals] + options[:object] ||= view_stack.last[:object] if view_stack.last[:object] + end + options[:template] = template + # if prefix is active_scaffold_overrides we must try to render with this prefix in following paths + if prefix != 'active_scaffold_overrides' + options[:prefixes] = lookup_context.prefixes.drop((lookup_context.prefixes.find_index(prefix) || -1) + 1) + else + options[:prefixes] = ['active_scaffold_overrides'] + last_view_path = File.expand_path(File.dirname(File.dirname(lookup_context.last_template.inspect)), Rails.root) + lookup_context.view_paths = view_paths.drop(view_paths.find_index {|path| path.to_s == last_view_path} + 1) + end + result = render_without_active_scaffold options + lookup_context.view_paths = @_view_paths if @_view_paths + result + else + @_view_paths ||= lookup_context.view_paths.clone + last_template = lookup_context.last_template + if args[0].is_a?(Hash) + current_view = {:locals => args[0][:locals], :object => args[0][:object]} + else # call is render 'partial', locals_hash + current_view = {:locals => args[1]} + end + view_stack << current_view if current_view + lookup_context.view_paths = @_view_paths # reset view_paths in case a view render :super, and then render :partial + result = render_without_active_scaffold(*args, &block) + view_stack.pop if current_view.present? + lookup_context.last_template = last_template + result + end + end + alias_method_chain :render, :active_scaffold + + def view_stack + @_view_stack ||= [] + end + + end +end diff --git a/lib/extensions/array.rb b/lib/active_scaffold/extensions/array.rb similarity index 100% rename from lib/extensions/array.rb rename to lib/active_scaffold/extensions/array.rb diff --git a/lib/active_scaffold/extensions/cache_association.rb b/lib/active_scaffold/extensions/cache_association.rb new file mode 100644 index 0000000000..80568df025 --- /dev/null +++ b/lib/active_scaffold/extensions/cache_association.rb @@ -0,0 +1,16 @@ +module ActiveRecord + class Relation + def target=(records) + @loaded = true + @records = records + @records + end + end +end +module ActiveRecord + module Associations + class CollectionProxy + delegate :target=, :to => :@association + end + end +end diff --git a/lib/active_scaffold/extensions/left_outer_joins.rb b/lib/active_scaffold/extensions/left_outer_joins.rb new file mode 100644 index 0000000000..f066407a9d --- /dev/null +++ b/lib/active_scaffold/extensions/left_outer_joins.rb @@ -0,0 +1,33 @@ +module ActiveScaffold + module OuterJoins + def outer_joins(*assocs) + joins(outer_joins_sql(*assocs)) + end + + private + def outer_joins_sql(*assocs) + assocs.collect do |assoc| + if assoc.is_a? Array + outer_joins_sql(*assoc) + elsif assoc.is_a? Hash + assoc.collect do |key, val| + [left_outer_join_sql(key), klass.reflect_on_association(key).klass.outer_joins_sql(*val)] + end + elsif assoc.is_a? Symbol + left_outer_join_sql(assoc) + elsif assoc + assoc + end + end.flatten.compact + end + + def left_outer_join_sql(association_name) + t = ActiveRecord::Associations::JoinDependency.new(klass, association_name, []).join_associations.first.join_relation(klass).arel + t.joins(t) + end + end +end +ActiveRecord::Relation.send :include, ActiveScaffold::OuterJoins +module ActiveRecord::Querying + delegate :outer_joins, :outer_joins_sql, :to => :scoped +end diff --git a/lib/extensions/localize.rb b/lib/active_scaffold/extensions/localize.rb similarity index 69% rename from lib/extensions/localize.rb rename to lib/active_scaffold/extensions/localize.rb index 671a4633ea..01b6001881 100644 --- a/lib/extensions/localize.rb +++ b/lib/active_scaffold/extensions/localize.rb @@ -1,7 +1,7 @@ class Object def as_(key, options = {}) unless key.blank? - text = I18n.translate "#{key}", {:scope => [:active_scaffold], :default => key.is_a?(String) ? key : key.to_s.titleize}.merge(options) + text = I18n.translate "#{key}", {:scope => [:active_scaffold, *options.delete(:scope)], :default => key.is_a?(String) ? key : key.to_s.titleize}.merge(options) # text = nil if text.include?('translation missing:') end text ||= key diff --git a/lib/extensions/name_option_for_datetime.rb b/lib/active_scaffold/extensions/name_option_for_datetime.rb similarity index 100% rename from lib/extensions/name_option_for_datetime.rb rename to lib/active_scaffold/extensions/name_option_for_datetime.rb diff --git a/lib/active_scaffold/extensions/paginator_extensions.rb b/lib/active_scaffold/extensions/paginator_extensions.rb new file mode 100644 index 0000000000..b6736063f8 --- /dev/null +++ b/lib/active_scaffold/extensions/paginator_extensions.rb @@ -0,0 +1,34 @@ +require 'active_scaffold/paginator' + +class Paginator + + # Total number of pages + def number_of_pages_with_infinite + number_of_pages_without_infinite unless infinite? + end + alias_method_chain :number_of_pages, :infinite + + # Is this an "infinite" paginator + def infinite? + @count.nil? + end + + class Page + # Checks to see if there's a page after this one + def next_with_infinite? + return true if @pager.infinite? + next_without_infinite? + end + alias_method_chain :next?, :infinite + + def empty? + if @pager.infinite? + items.to_a.empty? + else + @pager.count == 0 + end + end + end + +end + diff --git a/lib/active_scaffold/extensions/reverse_associations.rb b/lib/active_scaffold/extensions/reverse_associations.rb new file mode 100644 index 0000000000..ddf1c3ac66 --- /dev/null +++ b/lib/active_scaffold/extensions/reverse_associations.rb @@ -0,0 +1,68 @@ +module ActiveRecord + module Reflection + class AssociationReflection #:nodoc: + def inverse_for?(klass) + inverse_class = inverse_of.try(:active_record) + inverse_class.present? && (inverse_class == klass || klass < inverse_class) + end + + attr_writer :reverse + def reverse(klass = nil) + unless defined? @reverse + @reverse ||= inverse_of.try(:name) + end + @reverse || (autodetect_inverse(klass).try(:name) unless klass.nil?) + end + + def inverse_of_with_autodetect + inverse_of_without_autodetect || autodetect_inverse + end + alias_method_chain :inverse_of, :autodetect + + protected + + def autodetect_inverse(klass = nil) + return nil if klass.nil? && options[:polymorphic] + klass ||= self.klass + reverse_matches = [] + + # stage 1 filter: collect associations that point back to this model and use the same foreign_key + klass.reflect_on_all_associations.each do |assoc| + if self.options[:through] + # only iterate has_many :through associations + next unless assoc.options[:through] + next unless assoc.through_reflection.klass == self.through_reflection.klass + else + # skip over has_many :through associations + next if assoc.options[:through] + next unless assoc.options[:polymorphic] or assoc.class_name == self.active_record.name + + case [assoc.macro, self.macro].find_all{|m| m == :has_and_belongs_to_many}.length + # if both are a habtm, then match them based on the join table + when 2 + next unless assoc.options[:join_table] == self.options[:join_table] + + # if only one is a habtm, they do not match + when 1 + next + + # otherwise, match them based on the foreign_key + when 0 + next unless assoc.foreign_key.to_sym == self.foreign_key.to_sym + end + end + + reverse_matches << assoc + end + + # stage 2 filter: name-based matching (association name vs self.active_record.to_s) + reverse_matches.find_all do |assoc| + self.active_record.to_s.underscore.include? assoc.name.to_s.pluralize.singularize + end if reverse_matches.length > 1 + + reverse_matches.first + end + + end + end +end diff --git a/lib/active_scaffold/extensions/routing_mapper.rb b/lib/active_scaffold/extensions/routing_mapper.rb new file mode 100644 index 0000000000..30f8da6cde --- /dev/null +++ b/lib/active_scaffold/extensions/routing_mapper.rb @@ -0,0 +1,49 @@ +module ActionDispatch + module Routing + ACTIVE_SCAFFOLD_CORE_ROUTING = { + :collection => {:show_search => :get, :render_field => :post, :mark => :post}, + :member => {:row => :get, :update_column => :post, :render_field => [:get, :post], :mark => :post} + } + ACTIVE_SCAFFOLD_ASSOCIATION_ROUTING = { + :collection => {:edit_associated => :get, :new_existing => :get, :add_existing => :post}, + :member => {:edit_associated => :get, :destroy_existing => :delete} + } + class Mapper + module Base + def as_routes(options = {:association => true}) + collection do + ActionDispatch::Routing::ACTIVE_SCAFFOLD_CORE_ROUTING[:collection].each {|name, type| match(name, :via => type)} + end + member do + ActionDispatch::Routing::ACTIVE_SCAFFOLD_CORE_ROUTING[:member].each {|name, type| match(name, :via => type)} + get 'list', :action => :index + end + as_association_routes if options[:association] + end + + def as_association_routes + collection do + ActionDispatch::Routing::ACTIVE_SCAFFOLD_ASSOCIATION_ROUTING[:collection].each {|name, type| send(type, name)} + end + member do + ActionDispatch::Routing::ACTIVE_SCAFFOLD_ASSOCIATION_ROUTING[:member].each {|name, type| send(type, name)} + end + end + + def as_nested_resources(*resources) + options = resources.extract_options! + resources.each do |resource| + resources(resource, options.merge(:parent_scaffold => merge_module_scope(@scope[:module], parent_resource.plural), :association => resource)) { yield if block_given? } + end + end + + def as_scoped_routes(*scopes) + options = scopes.extract_options! + scopes.each do |scope| + resources(scope, options.merge(:parent_scaffold => merge_module_scope(@scope[:module], parent_resource.plural), :named_scope => scope, :controller => parent_resource.plural)) { yield if block_given? } + end + end + end + end + end +end diff --git a/lib/extensions/to_label.rb b/lib/active_scaffold/extensions/to_label.rb similarity index 100% rename from lib/extensions/to_label.rb rename to lib/active_scaffold/extensions/to_label.rb diff --git a/lib/extensions/unsaved_associated.rb b/lib/active_scaffold/extensions/unsaved_associated.rb similarity index 91% rename from lib/extensions/unsaved_associated.rb rename to lib/active_scaffold/extensions/unsaved_associated.rb index 1a9ae4b836..860b5096e6 100644 --- a/lib/extensions/unsaved_associated.rb +++ b/lib/active_scaffold/extensions/unsaved_associated.rb @@ -4,7 +4,7 @@ def associated_valid?(path = []) return true if path.include?(self) # prevent recursion (if associated and parent are new records) path << self # using [].all? syntax to avoid a short-circuit - with_unsaved_associated { |a| [a.valid?, a.associated_valid?(path)].all? {|v| v == true} } + with_unsaved_associated { |a| [a.valid?, a.associated_valid?(path)].all? } end def save_associated @@ -47,10 +47,10 @@ def associations_for_update # returns false if any yield returns false. # returns true otherwise, even when none of the associations have been instantiated. build wrapper methods accordingly. def with_unsaved_associated - associations_for_update.all? do |association| - association_proxy = instance_variable_get("@#{association.name}") - if association_proxy - records = association_proxy + associations_for_update.all? do |assoc| + association_proxy = self.association(assoc.name) + if association_proxy.target.present? + records = association_proxy.target records = [records] unless records.is_a? Array # convert singular associations into collections for ease of use records.select {|r| r.unsaved? and not r.readonly?}.all? {|r| yield r} # must use select instead of find_all, which Rails overrides on association proxies for db access else diff --git a/lib/extensions/unsaved_record.rb b/lib/active_scaffold/extensions/unsaved_record.rb similarity index 100% rename from lib/extensions/unsaved_record.rb rename to lib/active_scaffold/extensions/unsaved_record.rb diff --git a/lib/extensions/usa_state.rb b/lib/active_scaffold/extensions/usa_state.rb similarity index 100% rename from lib/extensions/usa_state.rb rename to lib/active_scaffold/extensions/usa_state.rb diff --git a/lib/active_scaffold/finder.rb b/lib/active_scaffold/finder.rb index 19857bf717..3dabf34443 100644 --- a/lib/active_scaffold/finder.rb +++ b/lib/active_scaffold/finder.rb @@ -1,89 +1,241 @@ module ActiveScaffold module Finder + def self.like_operator + @@like_operator ||= ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE" + end + module ClassMethods # Takes a collection of search terms (the tokens) and creates SQL that # searches all specified ActiveScaffold columns. A row will match if each # token is found in at least one of the columns. - def create_conditions_for_columns(tokens, columns, like_pattern = '%?%') + def create_conditions_for_columns(tokens, columns, text_search = :full) # if there aren't any columns, then just return a nil condition return unless columns.length > 0 + like_pattern = like_pattern(text_search) tokens = [tokens] if tokens.is_a? String where_clauses = [] columns.each do |column| - where_clauses << ((column.column.nil? || column.column.text?) ? "LOWER(#{column.search_sql}) LIKE ?" : "#{column.search_sql} = ?") + Array(column.search_sql).each do |search_sql| + where_clauses << "#{search_sql} #{(column.column.nil? || column.column.text?) ? ActiveScaffold::Finder.like_operator : '='} ?" + end end - phrase = "(#{where_clauses.join(' OR ')})" + phrase = where_clauses.join(' OR ') - sql = ([phrase] * tokens.length).join(' AND ') - tokens = tokens.collect do |value| - columns.collect {|column| (column.column.nil? || column.column.text?) ? like_pattern.sub('?', value.downcase) : column.column.type_cast(value)} - end.flatten - - [sql, *tokens] + tokens.collect do |value| + columns.inject([phrase]) do |condition, column| + Array(column.search_sql).size.times do + condition.push((column.column.nil? || column.column.text?) ? like_pattern.sub('?', value) : column.column.type_cast(value)) + end + condition + end + end end # Generates an SQL condition for the given ActiveScaffold column based on # that column's database type (or form_ui ... for virtual columns?). # TODO: this should reside on the column, not the controller - def condition_for_column(column, value, like_pattern = '%?%') - # we must check false or not blank because we want to search for false but false is blank - return unless column and column.search_sql and not value.blank? - search_ui = column.search_ui || column.column.type + def condition_for_column(column, value, text_search = :full) + like_pattern = like_pattern(text_search) if self.respond_to?("condition_for_#{column.name}_column") - self.send("condition_for_#{column.name}_column", column, value, like_pattern) - elsif self.respond_to?("condition_for_#{search_ui}_type") - self.send("condition_for_#{search_ui}_type", column, value, like_pattern) - else - case search_ui - when :boolean, :checkbox - ["#{column.search_sql} = ?", column.column.type_cast(value)] - when :select - ["#{column.search_sql} = ?", value[:id]] unless value[:id].blank? - when :multi_select - ["#{column.search_sql} in (?)", value.values.collect{|hash| hash[:id]}] + return self.send("condition_for_#{column.name}_column", column, value, like_pattern) + end + return unless column and column.search_sql and not value.blank? + search_ui = column.search_ui || column.column.try(:type) + begin + sql, *values = if search_ui && self.respond_to?("condition_for_#{search_ui}_type") + self.send("condition_for_#{search_ui}_type", column, value, like_pattern) + else + if column.search_sql.instance_of? Proc + column.search_sql.call(value) else - if column.column.nil? || column.column.text? - ["LOWER(#{column.search_sql}) LIKE ?", like_pattern.sub('?', value.downcase)] + case search_ui + when :boolean, :checkbox + ["%{search_sql} = ?", column.column ? column.column.type_cast(value) : value] + when :integer, :decimal, :float + condition_for_numeric(column, value) + when :string, :range + condition_for_range(column, value, like_pattern) + when :date, :time, :datetime, :timestamp + condition_for_datetime(column, value) + when :select, :multi_select, :country, :usa_state, :chosen, :multi_chosen + ["%{search_sql} in (?)", Array(value)] else - ["#{column.search_sql} = ?", column.column.type_cast(value)] + if column.column.nil? || column.column.text? + ["%{search_sql} #{ActiveScaffold::Finder.like_operator} ?", like_pattern.sub('?', value)] + else + ["%{search_sql} = ?", column.column.type_cast(value)] + end end + end end + return nil unless sql + + conditions = [column.search_sql.collect { |search_sql| sql % {:search_sql => search_sql} }.join(' OR ')] + conditions += values*column.search_sql.size if values.present? + conditions + rescue Exception => e + logger.error Time.now.to_s + "#{e.inspect} -- on the ActiveScaffold column :#{column.name}, search_ui = #{search_ui} in #{self.name}" + raise e end end - def condition_for_integer_type(column, value, like_pattern) - if value['from'].blank? or not ActiveScaffold::Finder::NumericComparators.include?(value['opt']) + def condition_for_numeric(column, value) + if !value.is_a?(Hash) + ["%{search_sql} = ?", condition_value_for_numeric(column, value)] + elsif ActiveScaffold::Finder::NullComparators.include?(value[:opt]) + condition_for_null_type(column, value[:opt]) + elsif value[:from].blank? or not ActiveScaffold::Finder::NumericComparators.include?(value[:opt]) nil - elsif value['opt'] == 'BETWEEN' - ["#{column.search_sql} BETWEEN ? AND ?", value['from'].to_f, value['to'].to_f] + elsif value[:opt] == 'BETWEEN' + ["(%{search_sql} BETWEEN ? AND ?)", condition_value_for_numeric(column, value[:from]), condition_value_for_numeric(column, value[:to])] else - ["#{column.search_sql} #{value['opt']} ?", value['from'].to_f] + ["%{search_sql} #{value[:opt]} ?", condition_value_for_numeric(column, value[:from])] + end + end + + def condition_for_range(column, value, like_pattern = nil) + if !value.is_a?(Hash) + if column.column.nil? || column.column.text? + ["%{search_sql} #{ActiveScaffold::Finder.like_operator} ?", like_pattern.sub('?', value)] + else + ["%{search_sql} = ?", column.column.type_cast(value)] + end + elsif ActiveScaffold::Finder::NullComparators.include?(value[:opt]) + condition_for_null_type(column, value[:opt], like_pattern) + elsif value[:from].blank? + nil + elsif ActiveScaffold::Finder::StringComparators.values.include?(value[:opt]) + ["%{search_sql} #{ActiveScaffold::Finder.like_operator} ?", value[:opt].sub('?', value[:from])] + elsif value[:opt] == 'BETWEEN' + ["(%{search_sql} BETWEEN ? AND ?)", value[:from], value[:to]] + elsif ActiveScaffold::Finder::NumericComparators.include?(value[:opt]) + ["%{search_sql} #{value[:opt]} ?", value[:from]] + else + nil + end + end + + def translate_days_and_months(value, format) + keys = { + '%A' => 'date.day_names', + '%a' => 'date.abbr_day_names', + '%B' => 'date.month_names', + '%b' => 'date.abbr_month_names' + } + keys.each do |f, k| + if format.include? f + table = Hash[I18n.t(k).compact.zip(I18n.t(k, :locale => :en).compact)] + value.gsub!(Regexp.union(table.keys)) { |s| table[s] } + end end + value + end + + def condition_value_for_datetime(column, value, conversion = :to_time) + if value.is_a? Hash + Time.zone.local(*[:year, :month, :day, :hour, :minute, :second].collect {|part| value[part].to_i}) rescue nil + elsif value.respond_to?(:strftime) + if conversion == :to_time + # Explicitly get the current zone, because TimeWithZone#to_time in rails 3.2.3 returns UTC. + # https://github.com/rails/rails/pull/2453 + value.to_time.in_time_zone + else + value.send(conversion) + end + elsif conversion == :to_date + Date.strptime(value, I18n.t("date.formats.#{column.options[:format] || :default}")) rescue nil + else + parts = Date._parse(value) + format = I18n.translate "time.formats.#{column.options[:format] || :picker}", :default => '' if ActiveScaffold.js_framework == :jquery + if format.blank? + time_parts = [[:hour, '%H'], [:min, '%M'], [:sec, '%S']].collect {|part, format_part| format_part if parts[part].present?}.compact + format = "#{I18n.t('date.formats.default')} #{time_parts.join(':')} #{'%z' if parts[:offset].present?}" + else + if parts[:hour] + [[:min, '%M'], [:sec, '%S']].each {|part, f| format.gsub!(":#{f}", '') unless parts[part].present?} + else + value += ' 00:00:00' + end + format += ' %z' if parts[:offset].present? && format !~ /%z/i + end + if !parts[:year] && !parts[:month] && !parts[:mday] + value = "#{Date.today.strftime(format.gsub(/%[HI].*/, ''))} #{value}" + end + value = translate_days_and_months(value, format) if I18n.locale != :en + time = DateTime.strptime(value, format) rescue nil + if time + time = Time.zone.local_to_utc(time).in_time_zone unless parts[:offset] + time = time.send(conversion) unless conversion == :to_time + end + time + end unless value.nil? || value.blank? end - alias_method :condition_for_decimal_type, :condition_for_integer_type - alias_method :condition_for_float_type, :condition_for_integer_type - def condition_for_datetime_type(column, value, like_pattern) - conversion = value['from']['hour'].blank? && value['to']['hour'].blank? ? 'to_date' : 'to_time' - from_value, to_value = ['from', 'to'].collect do |field| - Time.zone.local(*['year', 'month', 'day', 'hour', 'minutes', 'seconds'].collect {|part| value[field][part].to_i}) rescue nil + def condition_value_for_numeric(column, value) + return value if value.nil? + value = column.number_to_native(value) if column.options[:format] && column.search_ui != :number + case (column.search_ui || column.column.type) + when :integer then value.to_i rescue value ? 1 : 0 + when :float then value.to_f + when :decimal then ActiveRecord::ConnectionAdapters::Column.value_to_decimal(value) + else + value + end + end + + def datetime_conversion_for_condition(column) + if column.column + column.column.type == :date ? :to_date : :to_time + else + :to_time end + end + + def condition_for_datetime(column, value, like_pattern = nil) + conversion = datetime_conversion_for_condition(column) + from_value = condition_value_for_datetime(column, value[:from], conversion) + to_value = condition_value_for_datetime(column, value[:to], conversion) if from_value.nil? and to_value.nil? nil elsif !from_value - ["#{column.search_sql} <= ?", to_value.send(conversion).to_s(:db)] + ["%{search_sql} <= ?", to_value.to_s(:db)] elsif !to_value - ["#{column.search_sql} >= ?", from_value.send(conversion).to_s(:db)] + ["%{search_sql} >= ?", from_value.to_s(:db)] + else + ["%{search_sql} BETWEEN ? AND ?", from_value.to_s(:db), to_value.to_s(:db)] + end + end + + def condition_for_record_select_type(column, value, like_pattern = nil) + if value.is_a?(Array) + ["%{search_sql} IN (?)", value] else - ["#{column.search_sql} BETWEEN ? AND ?", from_value.send(conversion).to_s(:db), to_value.send(conversion).to_s(:db)] + ["%{search_sql} = ?", value] + end + end + + def condition_for_null_type(column, value, like_pattern = nil) + case value.to_sym + when :null + ["%{search_sql} is null", []] + when :not_null + ["%{search_sql} is not null", []] + else + nil + end + end + + def like_pattern(text_search) + case text_search + when :full then '%?%' + when :start then '?%' + when :end then '%?' + else '?' end end - alias_method :condition_for_date_type, :condition_for_datetime_type - alias_method :condition_for_time_type, :condition_for_datetime_type - alias_method :condition_for_timestamp_type, :condition_for_datetime_type end NumericComparators = [ @@ -95,6 +247,17 @@ def condition_for_datetime_type(column, value, like_pattern) '!=', 'BETWEEN' ] + StringComparators = { + :contains => '%?%', + :begins_with => '?%', + :ends_with => '%?' + } + NullComparators = [ + 'null', + 'not_null' + ] + + def self.included(klass) klass.extend ClassMethods @@ -107,9 +270,9 @@ def active_scaffold_conditions @active_scaffold_conditions ||= [] end - attr_writer :active_scaffold_joins - def active_scaffold_joins - @active_scaffold_joins ||= [] + attr_writer :active_scaffold_includes + def active_scaffold_includes + @active_scaffold_includes ||= [] end attr_writer :active_scaffold_habtm_joins @@ -117,70 +280,120 @@ def active_scaffold_habtm_joins @active_scaffold_habtm_joins ||= [] end + attr_writer :active_scaffold_outer_joins + def active_scaffold_outer_joins + @active_scaffold_outer_joins ||= [] + end + def all_conditions - merge_conditions( + [ active_scaffold_conditions, # from the search modules conditions_for_collection, # from the dev conditions_from_params, # from the parameters (e.g. /users/list?first_name=Fred) conditions_from_constraints, # from any constraints (embedded scaffolds) active_scaffold_session_storage[:conditions] # embedding conditions (weaker constraints) - ) + ].reject(&:blank?) end - - # returns a single record (the given id) but only if it's allowed for the specified action. + + # returns a single record (the given id) but only if it's allowed for the specified security options. + # security options can be a hash for authorized_for? method or a value to check as a :crud_type # accomplishes this by checking model.#{action}_authorized? # TODO: this should reside on the model, not the controller - def find_if_allowed(id, action, klass = nil) - klass ||= active_scaffold_config.model + def find_if_allowed(id, security_options, klass = beginning_of_chain) record = klass.find(id) - raise ActiveScaffold::RecordNotAllowed unless record.authorized_for?(:action => action.to_sym) + security_options = {:crud_type => security_options.to_sym} unless security_options.is_a? Hash + raise ActiveScaffold::RecordNotAllowed, "#{klass} with id = #{id}" unless record.authorized_for? security_options return record end - - # returns a Paginator::Page (not from ActiveRecord::Paginator) for the given parameters - # options may include: + # valid options may include: # * :sorting - a Sorting DataStructure (basically an array of hashes of field => direction, e.g. [{:field1 => 'asc'}, {:field2 => 'desc'}]). please note that multi-column sorting has some limitations: if any column in a multi-field sort uses method-based sorting, it will be ignored. method sorting only works for single-column sorting. # * :per_page # * :page - # TODO: this should reside on the model, not the controller - def find_page(options = {}) - options.assert_valid_keys :sorting, :per_page, :page, :count_includes - - full_includes = (active_scaffold_joins.blank? ? nil : active_scaffold_joins) - options[:per_page] ||= 999999999 - options[:page] ||= 1 - options[:count_includes] ||= full_includes - - klass = active_scaffold_config.model + def finder_options(options = {}) + search_conditions = all_conditions + full_includes = (active_scaffold_includes.blank? ? nil : active_scaffold_includes) # create a general-use options array that's compatible with Rails finders - finder_options = { :order => build_order_clause(options[:sorting]), - :conditions => all_conditions, + finder_options = { :reorder => options[:sorting].try(:clause), + :conditions => search_conditions, :joins => joins_for_finder, - :include => options[:count_includes]} - + :outer_joins => active_scaffold_outer_joins, + :includes => full_includes, + :select => options[:select]} + finder_options.merge! custom_finder_options + finder_options + end + def count_items(find_options = {}, count_includes = nil) + count_includes ||= find_options[:includes] unless find_options[:conditions].blank? + options = find_options.reject{|k,v| [:select, :reorder].include? k} + options[:includes] = count_includes + # NOTE: we must use :include in the count query, because some conditions may reference other tables - count = klass.count(finder_options.reject{|k,v| [:select, :order].include? k}) + count_query = append_to_query(beginning_of_chain, options) + count = count_query.count(:distinct => true) + + # Converts count to an integer if ActiveRecord returned an OrderedHash + # that happens when find_options contains a :group key + count = count.length if count.is_a? ActiveSupport::OrderedHash + count + end + + # returns a Paginator::Page (not from ActiveRecord::Paginator) for the given parameters + # See finder_options for valid options + def find_page(options = {}) + options.assert_valid_keys :sorting, :per_page, :page, :count_includes, :pagination, :select + options[:per_page] ||= 999999999 + options[:page] ||= 1 - finder_options.merge! :include => full_includes + find_options = finder_options(options) + + # NOTE: we must use :include in the count query, because some conditions may reference other tables + if options[:pagination] && options[:pagination] != :infinite + count = count_items(find_options, options[:count_includes]) + end + klass = beginning_of_chain + klass = klass.where(nil).uniq if find_options[:outer_joins].present? # HACK: call where(nil) because calling uniq on associations (nested scaffolds) send SQL to DB # we build the paginator differently for method- and sql-based sorting if options[:sorting] and options[:sorting].sorts_by_method? pager = ::Paginator.new(count, options[:per_page]) do |offset, per_page| - sorted_collection = sort_collection_by_column(klass.find(:all, finder_options), *options[:sorting].first) - sorted_collection.slice(offset, per_page) + sorted_collection = sort_collection_by_column(append_to_query(klass, find_options).all, *options[:sorting].first) + sorted_collection = sorted_collection.slice(offset, per_page) if options[:pagination] + sorted_collection end else pager = ::Paginator.new(count, options[:per_page]) do |offset, per_page| - klass.find(:all, finder_options.merge(:offset => offset, :limit => per_page)) + find_options.merge!(:offset => offset, :limit => per_page) if options[:pagination] + append_to_query(klass, find_options) end end - pager.page(options[:page]) end + def calculate_query + conditions = all_conditions + includes = active_scaffold_config.list.count_includes + includes ||= active_scaffold_includes unless conditions.blank? + primary_key = active_scaffold_config.model.primary_key + subquery = append_to_query(beginning_of_chain, :conditions => conditions, :joins => joins_for_finder, :outer_joins => active_scaffold_outer_joins) + subquery = subquery.select(active_scaffold_config.columns[primary_key].field) + if includes + includes_relation = beginning_of_chain.includes(includes) + subquery = subquery.send(:apply_join_dependency, subquery, includes_relation.send(:construct_join_dependency_for_association_find)) + end + active_scaffold_config.model.where(primary_key => subquery) + end + + def append_to_query(query, options) + options.assert_valid_keys :where, :select, :group, :reorder, :limit, :offset, :joins, :outer_joins, :includes, :lock, :readonly, :from, :conditions + query = apply_conditions(query, *options[:conditions]) if options[:conditions] + options.reject{|k, v| k == :conditions || v.blank?}.inject(query) do |query, (k, v)| + query.send((k.to_sym), v) + end + end + def joins_for_finder case joins_for_collection when String @@ -192,37 +405,21 @@ def joins_for_finder end + active_scaffold_habtm_joins end - # TODO: this should reside on the model, not the controller - def merge_conditions(*conditions) - active_scaffold_config.model.merge_conditions(*conditions) - end - - # accepts a DataStructure::Sorting object and builds an order-by clause - # TODO: this should reside on the model, not the controller - def build_order_clause(sorting) - return nil if sorting.nil? or sorting.sorts_by_method? - - # unless the sorting is by method, create the sql string - order = [] - sorting.each do |clause| - sort_column, sort_direction = clause - sql = sort_column.sort[:sql] - next if sql.nil? or sql.empty? - - order << "#{sql} #{sort_direction}" + def apply_conditions(query, *conditions) + conditions.reject(&:blank?).inject(query) do |query, condition| + if condition.is_a?(Array) && !condition.first.is_a?(String) # multiple conditions + apply_conditions(query, *condition) + else + query.where(condition) + end end - - order = order.join(', ') - order = nil if order.empty? - - order end # TODO: this should reside on the column, not the controller def sort_collection_by_column(collection, column, order) sorter = column.sort[:method] collection = collection.sort_by { |record| - value = (sorter.is_a? Proc) ? record.instance_eval(&sorter) : record.instance_eval(sorter) + value = (sorter.is_a? Proc) ? record.instance_eval(&sorter) : record.instance_eval(sorter.to_s) value = '' if value.nil? value } diff --git a/lib/active_scaffold/helpers/association_helpers.rb b/lib/active_scaffold/helpers/association_helpers.rb index 2048bd1b1e..71504406af 100644 --- a/lib/active_scaffold/helpers/association_helpers.rb +++ b/lib/active_scaffold/helpers/association_helpers.rb @@ -1,17 +1,53 @@ module ActiveScaffold module Helpers module AssociationHelpers + # Cache the optins for select + def cache_association_options(association, conditions, klass, cache = true) + if active_scaffold_config.cache_association_options && cache + @_associations_cache ||= Hash.new { |h,k| h[k] = {} } + key = [association.name, association.active_record.name, klass.name].join('/') + @_associations_cache[key][conditions] ||= yield + else + yield + end + end + # Provides a way to honor the :conditions on an association while searching the association's klass - def association_options_find(association, conditions = nil) - association.klass.find(:all, :conditions => controller.send(:merge_conditions, conditions, association.options[:conditions])) + def association_options_find(association, conditions = nil, klass = nil) + if klass.nil? && association.options[:polymorphic] + class_name = @record.send(association.foreign_type) + if class_name.present? + klass = class_name.constantize + else + return [] + end + cache = !block_given? + else + cache = !block_given? && klass.nil? + klass ||= association.klass + end + + conditions = options_for_association_conditions(association) if conditions.nil? + cache_association_options(association, conditions, klass, cache) do + relation = klass.where(conditions).where(association.options[:conditions]) + relation = relation.includes(association.options[:include]) if association.options[:include] + relation = yield(relation) if block_given? + relation.to_a + end + end + + # Sorts the options for select + def sorted_association_options_find(association, conditions = nil) + association_options_find(association, conditions).sort_by(&:to_label) end def association_options_count(association, conditions = nil) - association.klass.count(:all, :conditions => controller.send(:merge_conditions, conditions, association.options[:conditions])) + association.klass.where(conditions).where(association.options[:conditions]).count end # returns options for the given association as a collection of [id, label] pairs intended for the +options_for_select+ helper. def options_for_association(association, include_all = false) + ActiveSupport::Deprecation.warn "options_for_association should not be used, use association_options_find directly" available_records = association_options_find(association, include_all ? nil : options_for_association_conditions(association)) available_records ||= [] available_records.sort{|a,b| a.to_label <=> b.to_label}.collect { |model| [ model.to_label, model.id ] } @@ -29,7 +65,7 @@ def options_for_association_conditions(association) case association.macro when :has_one, :has_many # Find only orphaned objects - "#{association.primary_key_name} IS NULL" + "#{association.foreign_key} IS NULL" when :belongs_to, :has_and_belongs_to_many # Find all nil diff --git a/lib/active_scaffold/helpers/controller_helpers.rb b/lib/active_scaffold/helpers/controller_helpers.rb index c9e8123306..abc5db09b7 100644 --- a/lib/active_scaffold/helpers/controller_helpers.rb +++ b/lib/active_scaffold/helpers/controller_helpers.rb @@ -2,24 +2,101 @@ module ActiveScaffold module Helpers module ControllerHelpers def self.included(controller) - controller.class_eval { helper_method :params_for } + controller.class_eval { helper_method :params_for, :conditions_from_params, :main_path_to_return, :render_parent?, :render_parent_options, :render_parent_action, :nested_singular_association?, :build_associated, :generate_temporary_id, :generated_id} end include ActiveScaffold::Helpers::IdHelpers + def generate_temporary_id(record = nil, generated_id = nil) + (generated_id || (Time.now.to_f*1000).to_i.to_s).tap do |id| + (@temporary_ids ||= {})[record.class.name] = id if record + end + end + + def generated_id(record) + @temporary_ids[record.class.name] if record && @temporary_ids + end + def params_for(options = {}) # :adapter and :position are one-use rendering arguments. they should not propagate. # :sort, :sort_direction, and :page are arguments that stored in the session. they need not propagate. # and wow. no we don't want to propagate :record. # :commit is a special rails variable for form buttons - blacklist = [:adapter, :position, :sort, :sort_direction, :page, :record, :commit, :_method] + blacklist = [:adapter, :position, :sort, :sort_direction, :page, :record, :commit, :_method, :authenticity_token, :iframe, :associated_id, :dont_close] unless @params_for - @params_for = params.clone.delete_if { |key, value| blacklist.include? key.to_sym if key } - @params_for[:controller] = '/' + @params_for[:controller] unless @params_for[:controller].first(1) == '/' # for namespaced controllers + @params_for = {} + params.except(*blacklist).each {|key, value| @params_for[key.to_sym] = value.duplicable? ? value.clone : value} + @params_for[:controller] = '/' + @params_for[:controller].to_s unless @params_for[:controller].to_s.first(1) == '/' # for namespaced controllers @params_for.delete(:id) if @params_for[:id].nil? end @params_for.merge(options) end + + # Parameters to generate url to the main page (override if the ActiveScaffold is used as a component on another controllers page) + def main_path_to_return + if params[:return_to] + params[:return_to] + else + exclude_parameters = [:utf8, :associated_id] + parameters = {} + if params[:parent_scaffold] && nested? && nested.singular_association? + parameters[:controller] = params[:parent_scaffold] + exclude_parameters.concat [nested.param_name, :association, :parent_scaffold] + #parameters[:eid] = params[:parent_scaffold] # not neeeded anymore? + end + parameters.merge! nested.to_params if nested? + if params[:parent_sti] + parameters[:controller] = params[:parent_sti] + #parameters[:eid] = nil # not neeeded anymore? + end + parameters[:action] = "index" + parameters[:id] = nil + params_for(parameters).except(*exclude_parameters) + end + end + + def nested_singular_association? + nested? && (nested.belongs_to? || nested.has_one?) + end + + def render_parent? + nested_singular_association? || params[:parent_sti] + end + + def render_parent_options + if nested_singular_association? + {:controller => nested.parent_scaffold.controller_path, :action => :row, :id => nested.parent_id} + elsif params[:parent_sti] + options = params_for(:controller => params[:parent_sti], :action => render_parent_action, :parent_sti => nil) + options.merge(:id => @record.id) if render_parent_action == :row + end + end + + def render_parent_action + begin + @parent_action = :row + if params[:parent_sti] + parent_controller = "#{params[:parent_sti].to_s.camelize}Controller".constantize + @parent_action = :index if action_name == 'create' && parent_controller.active_scaffold_config.actions.include?(:create) && parent_controller.active_scaffold_config.create.refresh_list == true + @parent_action = :index if action_name == 'update' && parent_controller.active_scaffold_config.actions.include?(:update) && parent_controller.active_scaffold_config.update.refresh_list == true + @parent_action = :index if action_name == 'destroy' && parent_controller.active_scaffold_config.actions.include?(:delete) && parent_controller.active_scaffold_config.delete.refresh_list == true + end + rescue ActiveScaffold::ControllerNotFound + end if @parent_action.nil? + @parent_action + end + + def build_associated(column, record) + if column.singular_association? + if column.association.options[:through] + record.send(:"build_#{column.association.through_reflection.name}").send(:"build_#{column.name}") + else + record.send(:"build_#{column.name}") + end + else + record.send(column.name).build + end + end end end end diff --git a/lib/active_scaffold/helpers/form_column_helpers.rb b/lib/active_scaffold/helpers/form_column_helpers.rb index dbdc09d420..e1c28da2ae 100644 --- a/lib/active_scaffold/helpers/form_column_helpers.rb +++ b/lib/active_scaffold/helpers/form_column_helpers.rb @@ -4,42 +4,78 @@ module Helpers module FormColumnHelpers # This method decides which input to use for the given column. # It does not do any rendering. It only decides which method is responsible for rendering. - def active_scaffold_input_for(column, scope = nil) - options = active_scaffold_input_options(column, scope) - # first, check if the dev has created an override for this specific field - if override_form_field?(column) - send(override_form_field(column), @record, options[:name]) - # second, check if the dev has specified a valid form_ui for this column - elsif column.form_ui and override_input?(column.form_ui) - send(override_input(column.form_ui), column, options) - # fallback: we get to make the decision - else - if column.association - # if we get here, it's because the column has a form_ui but not one ActiveScaffold knows about. - raise "Unknown form_ui `#{column.form_ui}' for column `#{column.name}'" - elsif column.virtual? - active_scaffold_input_virtual(column, options) - - else # regular model attribute column - # if we (or someone else) have created a custom render option for the column type, use that - if override_input?(column.column.type) - send(override_input(column.column.type), column, options) - # final ultimate fallback: use rails' generic input method - else - # for textual fields we pass different options - text_types = [:text, :string, :integer, :float, :decimal] - options = active_scaffold_input_text_options(options) if text_types.include?(column.column.type) - if column.column.type == :string && options[:maxlength].blank? - options[:maxlength] = column.column.limit - options[:size] ||= ActionView::Helpers::InstanceTag::DEFAULT_FIELD_OPTIONS["size"] + def active_scaffold_input_for(column, scope = nil, options = nil) + options ||= active_scaffold_input_options(column, scope) + options = update_columns_options(column, scope, options) + active_scaffold_render_input(column, options) + end + + def active_scaffold_render_input(column, options) + begin + # first, check if the dev has created an override for this specific field + if (method = override_form_field(column)) + send(method, options[:object] || @record, options) + # second, check if the dev has specified a valid form_ui for this column + elsif column.form_ui and (method = override_input(column.form_ui)) + send(method, column, options) + # fallback: we get to make the decision + else + if column.association + if column.form_ui.nil? + # its an association and nothing is specified, we will assume form_ui :select + active_scaffold_input_select(column, options) + else + # if we get here, it's because the column has a form_ui but not one ActiveScaffold knows about. + raise "Unknown form_ui `#{column.form_ui}' for column `#{column.name}'" + end + elsif column.virtual? + options[:value] = format_number_value((options[:object] || @record).send(column.name), column.options) if column.number? + active_scaffold_input_virtual(column, options) + + else # regular model attribute column + # if we (or someone else) have created a custom render option for the column type, use that + if (method = override_input(column.column.type)) + send(method, column, options) + # final ultimate fallback: use rails' generic input method + else + # for textual fields we pass different options + text_types = [:text, :string, :integer, :float, :decimal, :date, :time, :datetime] + options = active_scaffold_input_text_options(options) if text_types.include?(column.column.type) + if column.column.type == :string && options[:maxlength].blank? + options[:maxlength] = column.column.limit + options[:size] ||= ActionView::Helpers::InstanceTag::DEFAULT_FIELD_OPTIONS["size"] + end + options[:include_blank] = true if column.column.null and [:date, :datetime, :time].include?(column.column.type) + options[:value] = format_number_value((options[:object] || @record).send(column.name), column.options) if column.number? + text_field(:record, column.name, options.merge(column.options)) end - input(:record, column.name, options.merge(column.options)) end end - end.to_s + javascript_for_update_column(column, scope, options) + rescue Exception => e + logger.error Time.now.to_s + "#{e.inspect} -- on the ActiveScaffold column = :#{column.name} in #{controller.class}" + raise e + end + end + + def active_scaffold_render_subform_column(column, scope, crud_type, readonly, add_class = false, record = nil) + Rails.logger.warn "Relying on @record is deprecated, call active_scaffold_render_subform_column with record. Called from #{caller.first.gsub(/(.*:\d+):.*/, '\1')}" if record.nil? # TODO Remove when relying on @record is removed + record ||= @record # TODO Remove when relying on @record is removed + if add_class + col_class = [] + col_class << 'required' if column.required? + col_class << column.css_class unless column.css_class.nil? || column.css_class.is_a?(Proc) + col_class << 'hidden' if column_renders_as(column) == :hidden + col_class << 'checkbox' if column.form_ui == :checkbox + col_class = col_class.join(' ') + end + unless readonly and not record.new_record? or not record.authorized_for?(:crud_type => crud_type, :column => column.name) + render_column(column, record, column_renders_as(column), scope, false, col_class) + else + options = active_scaffold_input_options(column, scope).except(:name) + options[:class] = "#{options[:class]} #{col_class}" if col_class + content_tag :span, get_column_value(record, column), options + end end - - alias form_column active_scaffold_input_for # the standard active scaffold options used for textual inputs def active_scaffold_input_text_options(options = {}) @@ -49,109 +85,207 @@ def active_scaffold_input_text_options(options = {}) end # the standard active scaffold options used for class, name and scope - def active_scaffold_input_options(column, scope = nil) + def active_scaffold_input_options(column, scope = nil, options = {}) name = scope ? "record#{scope}[#{column.name}]" : "record[#{column.name}]" + # Add some HTML5 attributes for in-browser validation and better user experience + if column.required? && (!@disable_required_for_new || scope.nil? || @record.persisted?) + options[:required] = true + end + options[:placeholder] = column.placeholder if column.placeholder.present? + # Fix for keeping unique IDs in subform - id_control = "record_#{column.name}_#{[params[:eid], params[:id]].compact.join '_'}" - id_control += scope.gsub(/(\[|\])/, '_').gsub('__', '_').gsub(/_$/, '') if scope + id_control = "record_#{column.name}_#{[params[:eid], params[:parent_id] || params[:id]].compact.join '_'}" + id_control += scope_id(scope) if scope + + classes = "#{column.name}-input" + classes += ' numeric-input' if column.number? - { :name => name, :class => "#{column.name}-input", :id => id_control} + { :name => name, :class => classes, :id => id_control}.merge(options) end - def javascript_for_update_column(column, scope, options) - if column.options[:update_column] - url_params = {:action => 'render_field', :id => params[:id]} - url_params[:controller] = controller.class.active_scaffold_controller_for(@record.class).controller_path if scope + def update_columns_options(column, scope, options) + record = options[:object] + Rails.logger.warn "Relying on @record is deprecated, call update_columns_options with record. Called from #{caller.first.gsub(/(.*:\d+):.*/, '\1')}" if record.nil? # TODO Remove when relying on @record is removed + record ||= @record # TODO Remove when relying on @record is removed + form_action = if scope + subform_controller = controller.class.active_scaffold_controller_for(record.class) + subform_controller.active_scaffold_config.subform + elsif [:new, :create, :edit, :update, :render_field].include? params[:action].to_sym + active_scaffold_config.send(record.new_record? ? :create : :update) + end + if form_action && column.update_columns && (column.update_columns & form_action.columns.names).present? + url_params = params_for(:action => 'render_field', :column => column.name, :id => record.id) + url_params = url_params.except(:parent_scaffold, :association, nested.param_name) if nested? && scope + url_params[:eid] = params[:eid] if params[:eid] + if scope + url_params[:parent_controller] ||= url_params[:controller] + url_params[:controller] = subform_controller.controller_path + url_params[:scope] = scope + url_params[:parent_id] = params[:parent_id] || params[:id] + end + + options[:class] = "#{options[:class]} update_form".strip + options['data-update_url'] = url_for(url_params) + options['data-update_send_form'] = column.send_form_on_update_column + options['data-update_send_form_selector'] = column.options[:send_form_selector] if column.options[:send_form_selector] + end + options + end - parameters = "column=#{column.name}" - parameters << "&eid=#{params[:eid]}" if params[:eid] - parameters << "&scope=#{scope}" if scope - javascript_tag("$(#{options[:id].to_json}).observe('change', function(event) { new Ajax.Request(#{url_for(url_params).to_json}, {parameters: '#{parameters}&value=' + this.value, method: 'get'}); });") + def field_attributes(column, record) + {} + end + + def render_column(column, record, renders_as, scope = nil, only_value = false, col_class = nil) + if override_form_field_partial?(column) + render :partial => override_form_field_partial(column), :locals => { :column => column, :only_value => only_value, :scope => scope, :col_class => col_class, :record => record } + elsif renders_as == :field || override_form_field?(column) + form_attribute(column, record, scope, only_value, col_class) + elsif renders_as == :subform + render :partial => 'form_association', :locals => { :column => column, :scope => scope, :parent_record => record } + else + form_hidden_attribute(column, record, scope) + end + end + + def form_attribute(column, record, scope = nil, only_value = false, col_class = nil) + column_options = active_scaffold_input_options(column, scope, :object => record) + attributes = field_attributes(column, record) + attributes[:class] = "#{attributes[:class]} #{col_class}" if col_class.present? + field = unless only_value + active_scaffold_input_for column, scope, column_options else - '' + content_tag(:span, get_column_value(record, column), column_options.except(:name, :object)) << + hidden_field(:record, column.association ? column.association.foreign_key : column.name, column_options) + end + + content_tag :dl, attributes do + %|
#{label_tag label_for(column, column_options), column.label}
#{field} +#{loading_indicator_tag(:action => :render_field, :id => params[:id]) if column.update_columns} +#{content_tag :span, column.description, :class => 'description' if column.description.present?} +
|.html_safe end end + def label_for(column, options) + options[:id] unless column.form_ui == :select && column.plural_association? + end + + def form_hidden_attribute(column, record, scope = nil) + %|
+#{hidden_field :record, column.name, active_scaffold_input_options(column, scope).merge(:object => record)} +
|.html_safe + end + ## ## Form input methods ## + + def active_scaffold_grouped_options(column, select_options, optgroup) + group_label = active_scaffold_config_for(column.association.klass).columns[optgroup].try(:association) ? :to_label : :to_s + select_options.group_by(&optgroup.to_sym).collect do |group, options| + [group.send(group_label), options.collect {|r| [r.to_label, r.id]}] + end + end - def active_scaffold_input_singular_association(column, options) + def active_scaffold_translate_select_options(options) + options[:include_blank] = as_(options[:include_blank].to_s) if options[:include_blank].is_a? Symbol + options[:prompt] = as_(options[:prompt].to_s) if options[:prompt].is_a? Symbol + options + end + + def active_scaffold_input_singular_association(column, html_options) associated = @record.send(column.association.name) - select_options = options_for_association(column.association) - select_options.unshift([ associated.to_label, associated.id ]) unless associated.nil? or select_options.find {|label, id| id == associated.id} + select_options = sorted_association_options_find(column.association) + select_options.unshift(associated) unless associated.nil? || select_options.include?(associated) - selected = associated.nil? ? nil : associated.id - method = column.association.macro == :belongs_to ? column.association.primary_key_name : column.name - options[:name] += '[id]' - select(:record, method, select_options.uniq, {:selected => selected, :include_blank => as_(:_select_)}, options.merge(column.options)) - end + method = column.name + options = {:selected => associated.try(:id), :include_blank => as_(:_select_), :object => html_options.delete(:object)} - def active_scaffold_input_plural_association(column, options) - associated_options = @record.send(column.association.name).collect {|r| [r.to_label, r.id]} - select_options = associated_options | options_for_association(column.association) - return content_tag(:span, as_(:no_options), :id => options[:id]) if select_options.empty? - - html = "
    " - - associated_ids = associated_options.collect {|a| a[1]} - select_options.each_with_index do |option, i| - label, id = option - this_name = "#{options[:name]}[#{i}][id]" - this_id = "#{options[:id]}_#{i}_id" - html << "
  • " - html << check_box_tag(this_name, id, associated_ids.include?(id), :id => this_id) - html << "" - html << "
  • " + html_options.update(column.options[:html_options] || {}) + options.update(column.options) + html_options[:name] = "#{html_options[:name]}[]" if html_options[:multiple] == true && !html_options[:name].to_s.ends_with?("[]") + active_scaffold_translate_select_options(options) + + if optgroup = options.delete(:optgroup) + select(:record, method, active_scaffold_grouped_options(column, select_options, optgroup), options, html_options) + else + collection_select(:record, method, select_options, :id, :to_label, options, html_options) end + end - html << '
' - html << javascript_tag("new DraggableLists('#{options[:id]}')") if column.options[:draggable_lists] - html + def active_scaffold_plural_association_options(column) + associated_options = @record.send(column.association.name) + [associated_options, associated_options | sorted_association_options_find(column.association)] end - def active_scaffold_input_select(column, options) - if column.singular_association? - active_scaffold_input_singular_association(column, options) - elsif column.plural_association? - active_scaffold_input_plural_association(column, options) - else - select(:record, column.name, column.options, { :selected => @record.send(column.name) }, options) + def active_scaffold_input_plural_association(column, options) + associated_options, select_options = active_scaffold_plural_association_options(column) + return content_tag(:span, as_(:no_options), :class => options[:class], :id => options[:id]) if select_options.empty? + + active_scaffold_checkbox_list(column, select_options.collect {|r| [r.to_label, r.id]}, associated_options.collect(&:id), options) + end + + def active_scaffold_checkbox_list(column, select_options, associated_ids, options) + html = hidden_field_tag("#{options[:name]}[]", '', :id => nil) + html << content_tag(:ul, :class => "#{options[:class]} checkbox-list", :id => options[:id]) do + content = ''.html_safe + select_options.each_with_index do |option, i| + label, id = option + this_id = "#{options[:id]}_#{i}_id" + content << content_tag(:li) do + check_box_tag("#{options[:name]}[]", id, associated_ids.include?(id), :id => this_id) << + content_tag(:label, h(label), :for => this_id) + end + end + content end + html << javascript_tag("ActiveScaffold.draggable_lists('#{options[:id]}')") if column.options[:draggable_lists] + html end - # only works for singular associations - # requires RecordSelect plugin to be installed and configured. - # ... maybe this should be provided in a bridge? - def active_scaffold_input_record_select(column, options) - unless column.association - raise ArgumentError, "record_select can only work against associations (and #{column.name} is not). A common mistake is to specify the foreign key field (like :user_id), instead of the association (:user)." - end - remote_controller = active_scaffold_controller_for(column.association.klass).controller_path + def active_scaffold_translated_option(column, text, value = nil) + value = text if value.nil? + [(text.is_a?(Symbol) ? column.active_record_class.human_attribute_name(text) : text), value] + end + + def active_scaffold_enum_options(column) + column.options[:options] + end - # if the opposite association is a :belongs_to (in that case association in this class must be has_one or has_many) - # then only show records that have not been associated yet - if [:has_one, :has_many].include?(column.association.macro) - params.merge!({column.association.primary_key_name => ''}) + def active_scaffold_input_enum(column, html_options) + options = { :selected => @record.send(column.name), :object => html_options.delete(:object) } + options_for_select = active_scaffold_enum_options(column).collect do |text, value| + active_scaffold_translated_option(column, text, value) end - - record_select_options = {:controller => remote_controller, :id => options[:id]} - record_select_options.merge!(active_scaffold_input_text_options) - record_select_options.merge!(column.options) + html_options.update(column.options[:html_options] || {}) + options.update(column.options) + active_scaffold_translate_select_options(options) + select(:record, column.name, options_for_select, options, html_options) + end + def active_scaffold_input_select(column, html_options) if column.singular_association? - record_select_field(options[:name], (@record.send(column.name) || column.association.klass.new), record_select_options) + active_scaffold_input_singular_association(column, html_options) elsif column.plural_association? - record_multi_select_field(options[:name], @record.send(column.name), record_select_options) - end + active_scaffold_input_plural_association(column, html_options) + else + active_scaffold_input_enum(column, html_options) + end + end + + def active_scaffold_input_radio(column, html_options) + html_options.update(column.options[:html_options] || {}) + column.options[:options].inject('') do |html, (text, value)| + text, value = active_scaffold_translated_option(column, text, value) + html << content_tag(:label, radio_button(:record, column.name, value, html_options.merge(:id => html_options[:id] + '-' + value.to_s)) + text) + end.html_safe end def active_scaffold_input_checkbox(column, options) - check_box(:record, column.name, options) + check_box(:record, column.name, options.merge(column.options)) end def active_scaffold_input_password(column, options) @@ -160,25 +294,60 @@ def active_scaffold_input_password(column, options) end def active_scaffold_input_textarea(column, options) - text_area(:record, column.name, options.merge(:cols => column.options[:cols], :rows => column.options[:rows])) + text_area(:record, column.name, options.merge(:cols => column.options[:cols], :rows => column.options[:rows], :size => column.options[:size])) end - + def active_scaffold_input_virtual(column, options) options = active_scaffold_input_text_options(options) text_field :record, column.name, options.merge(column.options) end + # Some fields from HTML5 (primarily for using in-browser validation) + # Sadly, many of them lacks browser support + + # A text box, that accepts only valid email address (in-browser validation) + def active_scaffold_input_email(column, options) + options = active_scaffold_input_text_options(options) + email_field :record, column.name, options.merge(column.options) + end + + # A text box, that accepts only valid URI (in-browser validation) + def active_scaffold_input_url(column, options) + options = active_scaffold_input_text_options(options) + url_field :record, column.name, options.merge(column.options) + end + + # A text box, that accepts only valid phone-number (in-browser validation) + def active_scaffold_input_telephone(column, options) + options = active_scaffold_input_text_options(options) + telephone_field :record, column.name, options.merge(column.options) + end + + # A spinbox control for number values (in-browser validation) + def active_scaffold_input_number(column, options) + options = numerical_constraints_for_column(column, options) + options = active_scaffold_input_text_options(options) + number_field :record, column.name, options.merge(column.options) + end + + # A slider control for number values (in-browser validation) + def active_scaffold_input_range(column, options) + options = numerical_constraints_for_column(column, options) + options = active_scaffold_input_text_options(options) + range_field :record, column.name, options.merge(column.options) + end + # # Column.type-based inputs # def active_scaffold_input_boolean(column, options) select_options = [] - select_options << [as_(:_select_), nil] if column.column.null + select_options << [as_(:_select_), nil] if !column.virtual? && column.column.null select_options << [as_(:true), true] select_options << [as_(:false), false] - select_tag(options[:name], options_for_select(select_options, @record.send(column.name))) + select_tag(options[:name], options_for_select(select_options, @record.send(column.name)), options) end def onsubmit @@ -190,8 +359,7 @@ def onsubmit # add functionality for overriding subform partials from association class path def override_subform_partial?(column, subform_partial) - path, partial_name = partial_pieces(override_subform_partial(column, subform_partial)) - template_exists?(File.join(path, "_#{partial_name}")) + template_exists?(override_subform_partial(column, subform_partial), true) end def override_subform_partial(column, subform_partial) @@ -199,44 +367,26 @@ def override_subform_partial(column, subform_partial) end def override_form_field_partial?(column) - path, partial_name = partial_pieces(override_form_field_partial(column)) - template_exists?(File.join(path, "_#{partial_name}")) + template_exists?(override_form_field_partial(column), true) end - # the naming convention for overriding form fields with partials + # the naming convention for overriding form fields with helpers def override_form_field_partial(column) - "#{column.name}_form_column" + path = active_scaffold_controller_for(column.active_record_class).controller_path + File.join(path, "#{clean_column_name(column.name)}_form_column") end - def override_form_field?(column) - respond_to?(override_form_field(column)) - end - - # the naming convention for overriding form fields with helpers def override_form_field(column) - "#{column.name}_form_column" - end - - def override_input?(form_ui) - respond_to?(override_input(form_ui)) + override_helper column, 'form_column' end + alias_method :override_form_field?, :override_form_field # the naming convention for overriding form input types with helpers def override_input(form_ui) - "active_scaffold_input_#{form_ui}" - end - - def form_partial_for_column(column) - if override_form_field_partial?(column) - override_form_field_partial(column) - elsif column_renders_as(column) == :field or override_form_field?(column) - "form_attribute" - elsif column_renders_as(column) == :subform - "form_association" - elsif column_renders_as(column) == :hidden - "form_hidden_attribute" - end + method = "active_scaffold_input_#{form_ui}" + method if respond_to? method end + alias_method :override_input?, :override_input def subform_partial_for_column(column) subform_partial = "#{active_scaffold_config_for(column.association.klass).subform.layout}_subform" @@ -254,29 +404,96 @@ def subform_partial_for_column(column) def column_renders_as(column) if column.is_a? ActiveScaffold::DataStructures::ActionColumns return :subsection - elsif column.active_record_class.locking_column.to_s == column.name.to_s + elsif column.active_record_class.locking_column.to_s == column.name.to_s or column.form_ui == :hidden return :hidden - elsif column.association.nil? or column.form_ui or !active_scaffold_config_for(column.association.klass).actions.include?(:subform) + elsif column.association.nil? or column.form_ui or !active_scaffold_config_for(column.association.klass).actions.include?(:subform) or override_form_field?(column) return :field else return :subform end end - def is_subsection?(column) - column_renders_as(column) == :subsection + def column_scope(column, scope = nil, record = nil) + Rails.logger.warn "Relying on @record is deprecated, call column_scope with record. Called from #{caller.first.gsub(/(.*:\d+):.*/, '\1')}" if record.nil? # TODO Remove when relying on @record is removed + if column.plural_association? + "#{scope}[#{column.name}][#{record.id || generate_temporary_id(record)}]" + else + "#{scope}[#{column.name}]" + end end - def is_subform?(column) - column_renders_as(column) == :subform + def active_scaffold_add_existing_input(options) + if ActiveScaffold.js_framework == :prototype && controller.respond_to?(:record_select_config, true) + remote_controller = active_scaffold_controller_for(record_select_config.model).controller_path + options.merge!(:controller => remote_controller) + options.merge!(active_scaffold_input_text_options) + record_select_field(options[:name], @record, options) + else + select_options = sorted_association_options_find(nested.association) + select_options ||= active_scaffold_config.model.all + select_options = options_from_collection_for_select(select_options, :id, :to_label) + select_tag 'associated_id', ('' + select_options).html_safe unless select_options.empty? + end end - def column_scope(column) - if column.plural_association? - "[#{column.name}][#{@record.id || generate_temporary_id}]" + def active_scaffold_add_existing_label + if controller.respond_to?(:record_select_config, true) + record_select_config.model.model_name.human else - "[#{column.name}]" + active_scaffold_config.model.model_name.human + end + end + + # Try to get numerical constraints from model's validators + def numerical_constraints_for_column(column, options) + if column.numerical_constraints.nil? + numerical_constraints = {} + validators = column.active_record_class.validators.select do |v| + v.is_a? ActiveModel::Validations::NumericalityValidator and v.attributes.include? column.name + end + equal_to = (v = validators.find{ |v| v.options[:equal_to] }) ? v.options[:equal_to] : nil + + # If there is equal_to constraint - use it (unless otherwise specified by user) + if equal_to and not (options[:min] or options[:max]) + numerical_constraints[:min] = numerical_constraints[:max] = equal_to + else # find minimum and maximum from validators + # we can safely modify :min and :max by 1 for :greater_tnan or :less_than value only for integer values + only_integer = column.column.type == :integer if column.column + only_integer ||= !!validators.find{ |v| v.options[:only_integer] } + margin = only_integer ? 1 : 0 + + # Minimum + unless options[:min] + min = validators.map{ |v| v.options[:greater_than_or_equal] }.compact.max + greater_than = validators.map{ |v| v.options[:greater_than] }.compact.max + numerical_constraints[:min] = [min, (greater_than+margin if greater_than)].compact.max + end + + # Maximum + unless options[:max] + max = validators.map{ |v| v.options[:less_than_or_equal] }.compact.min + less_than = validators.map{ |v| v.options[:less_than] }.compact.min + numerical_constraints[:max] = [max, (less_than-margin if less_than)].compact.min + end + + # Set step = 2 for column values restricted to be odd or even (but only if minimum is set) + unless options[:step] + only_odd_valid = validators.any?{ |v| v.options[:odd] } + only_even_valid = validators.any?{ |v| v.options[:even] } unless only_odd_valid + if !only_integer + numerical_constraints[:step] ||= "0.#{'0'*(column.column.scale-1)}1" if column.column && column.column.scale.to_i > 0 + elsif options[:min] and options[:min].respond_to? :even? and (only_odd_valid or only_even_valid) + numerical_constraints[:step] = 2 + numerical_constraints[:min] += 1 if only_odd_valid and not options[:min].odd? + numerical_constraints[:min] += 1 if only_even_valid and not options[:min].even? + end + numerical_constraints[:step] ||= 'any' unless only_integer + end + end + + column.numerical_constraints = numerical_constraints end + return column.numerical_constraints.merge(options) end end end diff --git a/lib/active_scaffold/helpers/human_condition_helpers.rb b/lib/active_scaffold/helpers/human_condition_helpers.rb new file mode 100644 index 0000000000..2e611c7164 --- /dev/null +++ b/lib/active_scaffold/helpers/human_condition_helpers.rb @@ -0,0 +1,64 @@ +module ActiveScaffold + module Helpers + # Helpers that assist with rendering of a human readable search statement + module HumanConditionHelpers + + def active_scaffold_human_condition_for(column) + value = field_search_params[column.name] + search_ui = column.search_ui + search_ui ||= column.column.type if column.column + if override_human_condition_column?(column) + send(override_human_condition_column(column), value, {}) + elsif search_ui and override_human_condition?(column.search_ui) + send(override_human_condition(column.search_ui), column, value) + else + case search_ui + when :integer, :decimal, :float + "#{column.active_record_class.human_attribute_name(column.name)} #{as_(value[:opt].downcase).downcase} #{format_number_value(controller.class.condition_value_for_numeric(column, value[:from]), column.options) if value[:from].present?} #{value[:opt] == 'BETWEEN' ? '- ' + format_number_value(controller.class.condition_value_for_numeric(column, value[:to]), column.options).to_s : ''}" + when :string + opt = ActiveScaffold::Finder::StringComparators.index(value[:opt]) || value[:opt] + "#{column.active_record_class.human_attribute_name(column.name)} #{as_(opt).downcase} '#{value[:from]}' #{opt == 'BETWEEN' ? '- ' + value[:to].to_s : ''}" + when :date, :time, :datetime, :timestamp + conversion = column.column.type == :date ? :to_date : :to_time + from = controller.condition_value_for_datetime(column, value[:from], conversion) + to = controller.condition_value_for_datetime(column, value[:to], conversion) + "#{column.active_record_class.human_attribute_name(column.name)} #{as_(value[:opt])} #{I18n.l(from)} #{value[:opt] == 'BETWEEN' ? '- ' + I18n.l(to) : ''}" + when :select, :multi_select, :record_select + associated = value + associated = [associated].compact unless associated.is_a? Array + if column.association + associated = column.association.klass.where(:id => associated.map(&:to_i)).collect(&:to_label) + elsif column.options[:options] + associated = associated.collect do |value| + text, val = column.options[:options].find {|text, val| (val.nil? ? text : val).to_s == value.to_s} + value = active_scaffold_translated_option(column, text, val).first if text + value + end + end + as_(:association, :scope => :human_conditions, :column => column.active_record_class.human_attribute_name(column.name), :value => associated.join(', ')) + when :boolean, :checkbox + label = column.column.type_cast(value) ? as_(:true) : as_(:false) + as_(:boolean, :scope => :human_conditions, :column => column.active_record_class.human_attribute_name(column.name), :value => label) + when :null + "#{column.active_record_class.human_attribute_name(column.name)} #{as_(value.to_sym)}" + end + end unless value.nil? + end + + # the naming convention for overriding form fields with helpers + def override_human_condition_column(column) + override_helper column, 'human_condition_column' + end + alias_method :override_human_condition_column?, :override_human_condition_column + + def override_human_condition?(search_ui) + respond_to?(override_human_condition(search_ui)) + end + + # the naming convention for overriding human condition search_ui types + def override_human_condition(search_ui) + "active_scaffold_human_condition_#{search_ui}" + end + end + end +end diff --git a/lib/active_scaffold/helpers/id_helpers.rb b/lib/active_scaffold/helpers/id_helpers.rb index d2ac02dded..830ffc4774 100644 --- a/lib/active_scaffold/helpers/id_helpers.rb +++ b/lib/active_scaffold/helpers/id_helpers.rb @@ -2,8 +2,16 @@ module ActiveScaffold module Helpers # A bunch of helper methods to produce the common view ids module IdHelpers - def controller_id - @controller_id ||= (params[:eid] || params[:parent_controller] || params[:controller]).gsub("/", "__") + def id_from_controller(controller) + controller.to_s.gsub("/", "__").html_safe + end + + def controller_id(controller = (params[:eid] || nested_id || params[:parent_controller] || params[:controller])) + controller_id ||= 'as_' + id_from_controller(controller) + end + + def nested_id + "#{nested.parent_scaffold.controller_path}-#{nested.parent_id}-#{params[:controller]}" if nested? end def active_scaffold_id @@ -18,12 +26,12 @@ def active_scaffold_tbody_id "#{controller_id}-tbody" end - def active_scaffold_messages_id - "#{controller_id}-messages" + def active_scaffold_messages_id(options = {}) + "#{options[:controller_id] || controller_id}-messages" end - def active_scaffold_calculations_id - "#{controller_id}-calculations" + def active_scaffold_calculations_id(options = {}) + "#{options[:controller_id] || controller_id}-calculations#{'-' + options[:column].name.to_s if options[:column]}" end def empty_message_id @@ -34,18 +42,10 @@ def before_header_id "#{controller_id}-search-container" end - def search_form_id - "#{controller_id}-search-form" - end - def search_input_id "#{controller_id}-search-input" end - def table_action_id(name) - "#{controller_id}-action-table-#{name}" - end - def action_link_id(link_action,link_id) "#{controller_id}-#{link_action}-#{link_id}-link" end @@ -58,14 +58,14 @@ def active_scaffold_column_header_id(column) def element_row_id(options = {}) options[:action] ||= params[:action] options[:id] ||= params[:id] - options[:id] ||= params[:parent_id] - clean_id "#{controller_id}-#{options[:action]}-#{options[:id]}-row" + options[:id] ||= nested.parent_id if nested? + clean_id "#{options[:controller_id] || controller_id}-#{options[:action]}-#{options[:id]}-row" end def element_cell_id(options = {}) options[:action] ||= params[:action] options[:id] ||= params[:id] - options[:id] ||= params[:parent_id] + options[:id] ||= nested.parent_id if nested? options[:name] ||= params[:name] clean_id "#{controller_id}-#{options[:action]}-#{options[:id]}-#{options[:name]}-cell" end @@ -73,7 +73,7 @@ def element_cell_id(options = {}) def element_form_id(options = {}) options[:action] ||= params[:action] options[:id] ||= params[:id] - options[:id] ||= params[:parent_id] + options[:id] ||= nested.parent_id if nested? clean_id "#{controller_id}-#{options[:action]}-#{options[:id]}-form" end @@ -84,35 +84,41 @@ def association_subform_id(column) def loading_indicator_id(options = {}) options[:action] ||= params[:action] - unless options[:id] - clean_id "#{controller_id}-#{options[:action]}-loading-indicator" - else - clean_id "#{controller_id}-#{options[:action]}-#{options[:id]}-loading-indicator" - end + clean_id "#{controller_id}-#{options[:action]}-#{options[:id]}-loading-indicator" + end + + def sub_section_id(options = {}) + options[:id] ||= params[:id] + options[:id] ||= nested.parent_id if nested? + clean_id "#{controller_id}-#{options[:id]}-#{options[:sub_section]}-subsection" end def sub_form_id(options = {}) options[:id] ||= params[:id] - options[:id] ||= params[:parent_id] + options[:id] ||= nested.parent_id if nested? clean_id "#{controller_id}-#{options[:id]}-#{options[:association]}-subform" end - + def sub_form_list_id(options = {}) options[:id] ||= params[:id] - options[:id] ||= params[:parent_id] + options[:id] ||= nested.parent_id if nested? clean_id "#{controller_id}-#{options[:id]}-#{options[:association]}-subform-list" end def element_messages_id(options = {}) options[:action] ||= params[:action] options[:id] ||= params[:id] - options[:id] ||= params[:parent_id] + options[:id] ||= nested.parent_id if nested? clean_id "#{controller_id}-#{options[:action]}-#{options[:id]}-messages" end def action_iframe_id(options) "#{controller_id}-#{options[:action]}-#{options[:id]}-iframe" end + + def scope_id(scope) + scope.gsub(/(\[|\])/, '_').gsub('__', '_').gsub(/_$/, '') + end private diff --git a/lib/active_scaffold/helpers/list_column_helpers.rb b/lib/active_scaffold/helpers/list_column_helpers.rb index e60eafe055..1da4d4fb37 100644 --- a/lib/active_scaffold/helpers/list_column_helpers.rb +++ b/lib/active_scaffold/helpers/list_column_helpers.rb @@ -4,111 +4,56 @@ module Helpers # Helpers that assist with the rendering of a List Column module ListColumnHelpers def get_column_value(record, column) - # check for an override helper - value = if column_override? column - # we only pass the record as the argument. we previously also passed the formatted_value, - # but mike perham pointed out that prohibited the usage of overrides to improve on the - # performance of our default formatting. see issue #138. - send(column_override(column), record) - # second, check if the dev has specified a valid list_ui for this column - elsif column.list_ui and override_column_ui?(column.list_ui) - send(override_column_ui(column.list_ui), column, record) - - elsif column.inplace_edit and record.authorized_for?(:action => :update, :column => column.name) - active_scaffold_inplace_edit(record, column) - elsif column.column and override_column_ui?(column.column.type) - send(override_column_ui(column.column.type), column, record) - else - value = record.send(column.name) + begin + method = get_column_method(record, column) + value = send(method, record, column) + value = ' '.html_safe if value.nil? or value.blank? # fix for IE 6 + return value + rescue Exception => e + logger.error "#{Time.now.to_s} #{e.inspect} -- on the ActiveScaffold column = :#{column.name} in #{controller.class}" + raise e + end + end + - if column.association.nil? or column_empty?(value) - formatted_value = clean_column_value(format_value(value, column.options)) + def get_column_method(record, column) + # check for an override helper + method = column.list_method + unless method + method = if (method = column_override(column)) + # we only pass the record as the argument. we previously also passed the formatted_value, + # but mike perham pointed out that prohibited the usage of overrides to improve on the + # performance of our default formatting. see issue #138. + method + # second, check if the dev has specified a valid list_ui for this column + elsif column.list_ui and (method = override_column_ui(column.list_ui)) + method + elsif column.column and (method = override_column_ui(column.column.type)) + method else - case column.association.macro - when :has_one, :belongs_to - formatted_value = clean_column_value(format_value(value.to_label)) - - when :has_many, :has_and_belongs_to_many - if column.associated_limit.nil? - firsts = value.collect { |v| v.to_label } - else - firsts = value.first(column.associated_limit + 1).collect { |v| v.to_label } - firsts[column.associated_limit] = '…' if firsts.length > column.associated_limit - end - if column.associated_limit == 0 - formatted_value = value.length if column.associated_number? - else - formatted_value = clean_column_value(format_value(firsts.join(', '))) - formatted_value << " (#{value.length})" if column.associated_number? and column.associated_limit and firsts.length > column.associated_limit - end - formatted_value - end + :format_column_value end - - formatted_value + column.list_method = method end - - value = ' ' if value.nil? or (value.respond_to?(:empty?) and value.empty?) # fix for IE 6 - return value + method end # TODO: move empty_field_text and   logic in here? - # TODO: move active_scaffold_inplace_edit in here? # TODO: we need to distinguish between the automatic links *we* create and the ones that the dev specified. some logic may not apply if the dev specified the link. def render_list_column(text, column, record) - if column.link + if column.link && !skip_action_link?(column.link, record) link = column.link associated = record.send(column.association.name) if column.association - url_options = params_for(:action => nil, :id => record.id, :link => text) - url_options[:parent_controller] = params[:controller] if link.controller and link.controller.to_s != params[:controller] - url_options[:id] = associated.id if associated and link.controller and link.controller.to_s != params[:controller] - - # setup automatic link - if column.autolink # link to nested scaffold or inline form - link = action_link_to_inline_form(column, associated) if link.crud_type.nil? # automatic link to inline form (singular association) - return text if link.crud_type.nil? - if link.crud_type == :create - url_options[:link] = as_(:create_new) - url_options[:parent_id] = record.id - url_options[:parent_column] = column.association.reverse - url_options[:parent_model] = record.class.name # needed for polymorphic associations - url_options.delete :id - end - end - - # check authorization - if column.association - authorized = (associated ? associated : column.association.klass).authorized_for?(:action => link.crud_type) - authorized = authorized and record.authorized_for?(:action => :update, :column => column.name) if link.crud_type == :create - else - authorized = record.authorized_for?(:action => link.crud_type) - end - return "#{text}" unless authorized - - render_action_link(link, url_options) + render_action_link(link, record, :link => text, :authorized => link.action.nil? || column_link_authorized?(link, column, record, associated)) + elsif inplace_edit?(record, column) + active_scaffold_inplace_edit(record, column, {:formatted_column => text}) + elsif active_scaffold_config.list.wrap_tag + content_tag active_scaffold_config.list.wrap_tag, text else text end end - # setup the action link to inline form - def action_link_to_inline_form(column, associated) - link = column.link.clone - if column_empty?(associated) # if association is empty, we only can link to create form - if column.actions_for_association_links.include?(:new) - link.action = 'new' - link.crud_type = :create - end - elsif column.actions_for_association_links.include?(:edit) - link.action = 'edit' - link.crud_type = :update - elsif column.actions_for_association_links.include?(:show) - link.action = 'show' - link.crud_type = :read - end - link - end - # There are two basic ways to clean a column's value: h() and sanitize(). The latter is useful # when the column contains *valid* html data, and you want to just disable any scripting. People # can always use field overrides to clean data one way or the other, but having this override @@ -123,82 +68,270 @@ def clean_column_value(v) ## ## Overrides ## - def active_scaffold_column_text(column, record) - truncate(clean_column_value(record.send(column.name)), :length => column.options[:truncate] || 50) - end - - def active_scaffold_column_checkbox(column, record) - column_value = record.send(column.name) - checked = column_value.class.to_s.include?('Class') ? column_value : column_value == 1 - if column.inplace_edit and record.authorized_for?(:action => :update, :column => column.name) - id_options = {:id => record.id.to_s, :action => 'update_column', :name => column.name.to_s} - tag_options = {:tag => "span", :id => element_cell_id(id_options), :class => "in_place_editor_field"} - script = remote_function(:method => 'POST', :url => {:controller => params_for[:controller], :action => "update_column", :column => column.name, :id => record.id.to_s, :value => !column_value, :eid => params[:eid]}) - content_tag(:span, check_box_tag(tag_options[:id], 1, checked, {:onclick => script}) , tag_options) - else - check_box_tag(nil, 1, checked, :disabled => true) - end + def active_scaffold_column_text(record, column) + clean_column_value(truncate(record.send(column.name), :length => column.options[:truncate] || 50)) end - def column_override(column) - "#{column.name.to_s.gsub('?', '')}_column" # parse out any question marks (see issue 227) + def active_scaffold_column_fulltext(record, column) + clean_column_value(record.send(column.name)) end - def column_override?(column) - respond_to?(column_override(column)) + def active_scaffold_column_marked(record, column) + options = {:id => nil, :object => record} + content_tag(:span, check_box(:record, column.name, options), :class => 'in_place_editor_field', :data => {:ie_id => record.id.to_s}) end - def override_column_ui?(list_ui) - respond_to?(override_column_ui(list_ui)) + def active_scaffold_column_checkbox(record, column) + options = {:disabled => true, :id => nil, :object => record} + options.delete(:disabled) if inplace_edit?(record, column) + check_box(:record, column.name, options) end + def column_override(column) + override_helper column, 'column' + end + alias_method :column_override?, :column_override + # the naming convention for overriding column types with helpers def override_column_ui(list_ui) - "active_scaffold_column_#{list_ui}" + @_column_ui_overrides ||= {} + return @_column_ui_overrides[list_ui] if @_column_ui_overrides.include? list_ui + method = "active_scaffold_column_#{list_ui}" + @_column_ui_overrides[list_ui] = (method if respond_to? method) end + alias_method :override_column_ui?, :override_column_ui ## ## Formatting ## + def format_column_value(record, column, value = nil) + value ||= record.send(column.name) unless record.nil? + if value && column.association # cache association size before calling column_empty? + associated_size = value.size if column.plural_association? and column.associated_number? # get count before cache association + cache_association(value, column, associated_size) if column.plural_association? + end + if column.association.nil? or column_empty?(value) + if column.form_ui == :select && column.options[:options] + text, val = column.options[:options].find {|text, val| (val.nil? ? text : val).to_s == value.to_s} + value = active_scaffold_translated_option(column, text, val).first if text + end + if value.is_a? Numeric + format_number_value(value, column.options) + else + format_value(value, column.options) + end + else + format_association_value(value, column, associated_size) + end + end + + def format_number_value(value, options = {}) + value = case options[:format] + when :size + number_to_human_size(value, options[:i18n_options] || {}) + when :percentage + number_to_percentage(value, options[:i18n_options] || {}) + when :currency + number_to_currency(value, options[:i18n_options] || {}) + when :i18n_number + number_with_delimiter(value, options[:i18n_options] || {}) + else + value + end + clean_column_value(value) + end + + def format_association_value(value, column, size) + format_value case column.association.macro + when :has_one, :belongs_to + if column.polymorphic_association? + "#{value.class.model_name.human}: #{value.to_label}" + else + value.to_label + end + when :has_many, :has_and_belongs_to_many + if column.associated_limit.nil? + firsts = value.collect { |v| v.to_label } + else + firsts = value.first(column.associated_limit) + firsts.collect! { |v| v.to_label } + firsts[column.associated_limit] = '…' if value.size > column.associated_limit + end + if column.associated_limit == 0 + size if column.associated_number? + else + joined_associated = firsts.join(h(active_scaffold_config.list.association_join_text)).html_safe + joined_associated << " (#{size})" if column.associated_number? and column.associated_limit and value.size > column.associated_limit + joined_associated + end + end + end def format_value(column_value, options = {}) - if column_empty?(column_value) + value = if column_empty?(column_value) active_scaffold_config.list.empty_field_text elsif column_value.is_a?(Time) || column_value.is_a?(Date) l(column_value, :format => options[:format] || :default) + elsif [FalseClass, TrueClass].include?(column_value.class) + as_(column_value.to_s.to_sym) else column_value.to_s end + clean_column_value(value) + end + + def cache_association(value, column, size) + # we are not using eager loading, cache firsts records in order not to query the database in a future + unless value.loaded? + # load at least one record, is needed to display '...' + if column.associated_limit.nil? + Rails.logger.warn "ActiveScaffold: Enable eager loading for #{column.name} association to reduce SQL queries" + elsif column.associated_limit > 0 + value.target = value.find(:all, :limit => column.associated_limit + 1, :select => column.select_associated_columns) + elsif @cache_associations + value.target = size.to_i.zero? ? [] : [nil] + end + end end # ========== # = Inline Edit = # ========== - def format_inplace_edit_column(record,column) - value = record.send(column.name) + + def inplace_edit?(record, column) + if column.inplace_edit + editable = controller.send(:update_authorized?, record) if controller.respond_to?(:update_authorized?, true) + editable ||= record.authorized_for?(:crud_type => :update, :column => column.name) + end + end + + def inplace_edit_cloning?(column) + column.inplace_edit != :ajax and (override_form_field?(column) or column.form_ui or (column.column and override_input?(column.column.type))) + end + + def active_scaffold_inplace_edit(record, column, options = {}) + formatted_column = options[:formatted_column] || format_column_value(record, column) + id_options = {:id => record.id.to_s, :action => 'update_column', :name => column.name.to_s} + tag_options = {:id => element_cell_id(id_options), :class => "in_place_editor_field", + :title => as_(:click_to_edit), :data => {:ie_id => record.id.to_s}} + tag_options[:data][:ie_update] = column.inplace_edit if column.inplace_edit != true + + content_tag(:span, as_(:inplace_edit_handle), :class => 'handle') << + content_tag(:span, formatted_column, tag_options) + end + + def inplace_edit_control(column) + if inplace_edit?(active_scaffold_config.model, column) and inplace_edit_cloning?(column) + old_record, @record = @record, new_model # TODO remove when relying on @record is removed + column = column.clone + column.options = column.options.clone + column.form_ui = :select if (column.association && column.form_ui.nil?) + options = active_scaffold_input_options(column).merge(:object => new_model) + options[:class] = "#{options[:class]} inplace_field" + content_tag(:div, active_scaffold_input_for(column, nil, options), :style => "display:none;", :class => inplace_edit_control_css_class).tap do + @record = old_record + end + end + end + + def inplace_edit_control_css_class + "as_inplace_pattern" + end + + def inplace_edit_data(column) + data = {} + data[:ie_url] = url_for(params_for(:action => "update_column", :column => column.name, :id => '__id__')) + data[:ie_cancel_text] = column.options[:cancel_text] || as_(:cancel) + data[:ie_loading_text] = column.options[:loading_text] || as_(:loading) + data[:ie_save_text] = column.options[:save_text] || as_(:update) + data[:ie_saving_text] = column.options[:saving_text] || as_(:saving) + data[:ie_rows] = column.options[:rows] || 5 if column.column.try(:type) == :text + data[:ie_cols] = column.options[:cols] if column.options[:cols] + data[:ie_size] = column.options[:size] if column.options[:size] + if column.list_ui == :checkbox - active_scaffold_column_checkbox(column, record) + data[:ie_mode] = :inline_checkbox + elsif inplace_edit_cloning?(column) + data[:ie_mode] = :clone + elsif column.inplace_edit == :ajax + url = url_for(:controller => params_for[:controller], :action => 'render_field', :id => '__id__', :update_column => column.name) + plural = column.plural_association? && !override_form_field?(column) && [:select, :record_select].include?(column.form_ui) + data[:ie_render_url] = url + data[:ie_mode] = :ajax + data[:ie_plural] = plural + end + data + end + + def all_marked? + if active_scaffold_config.mark.mark_all_mode == :page + all_marked = @page.items.detect { |record| !marked_records.include?(record.id) }.nil? else - clean_column_value(format_value(value)) + all_marked = (marked_records.length >= @page.pager.count.to_i) end end - - def active_scaffold_inplace_edit(record, column) - formatted_column = format_inplace_edit_column(record,column) - id_options = {:id => record.id.to_s, :action => 'update_column', :name => column.name.to_s} - tag_options = {:tag => "span", :id => element_cell_id(id_options), :class => "in_place_editor_field"} - in_place_editor_options = {:url => {:controller => params_for[:controller], :action => "update_column", :column => column.name, :id => record.id.to_s}, - :with => params[:eid] ? "Form.serialize(form) + '&eid=#{params[:eid]}'" : nil, - :click_to_edit_text => as_(:click_to_edit), - :cancel_text => as_(:cancel), - :loading_text => as_(:loading), - :save_text => as_(:update), - :saving_text => as_(:saving), - :options => "{method: 'post'}", - :script => true}.merge(column.options) - content_tag(:span, formatted_column, tag_options) + in_place_editor(tag_options[:id], in_place_editor_options) + + def mark_column_heading + tag_options = { + :id => "#{controller_id}_mark_heading", + :class => "mark_heading in_place_editor_field", + } + content_tag(:span, check_box_tag("#{controller_id}_mark_heading_span_input", '1', all_marked?), tag_options) + end + + def column_heading_attributes(column, sorting, sort_direction) + tag_options = {:id => active_scaffold_column_header_id(column), :class => column_heading_class(column, sorting), :title => strip_tags(column.description)} end + def render_column_heading(column, sorting, sort_direction) + tag_options = column_heading_attributes(column, sorting, sort_direction) + if column.name == :as_marked + tag_options[:data] = { + :ie_mode => :inline_checkbox, + :ie_url => url_for(params_for(:action => 'mark', :id => '__id__')) + } + else + tag_options[:data] = inplace_edit_data(column) if column.inplace_edit + end + content_tag(:th, column_heading_value(column, sorting, sort_direction) + inplace_edit_control(column), tag_options) + end + + + def column_heading_value(column, sorting, sort_direction) + if column.name == :as_marked + mark_column_heading + elsif column.sortable? + options = {:id => nil, :class => "as_sort", + 'data-page-history' => controller_id, + :remote => true, :method => :get} + url_options = params_for(:action => :index, :page => 1, + :sort => column.name, :sort_direction => sort_direction) + unless active_scaffold_config.store_user_settings + url_options.merge!(:search => search_params) if search_params.present? + end + link_to column_heading_label(column), url_options, options + else + content_tag(:p, column_heading_label(column)) + end + end + + def column_heading_label(column) + column.label + end + + def render_nested_view(action_links, record) + rendered = [] + action_links.member.each do |link| + if link.nested_link? && link.column && @nested_auto_open[link.column.name] && @records.length <= @nested_auto_open[link.column.name] && controller.respond_to?(:render_component_into_view, true) + link_url_options = {:adapter => '_list_inline_adapter', :format => :js}.merge(action_link_url_options(link, record)) + link_id = get_action_link_id(link, record) + rendered << (controller.send(:render_component_into_view, link_url_options) + javascript_tag("ActiveScaffold.ActionLink.get('#{link_id}').set_opened();")) + end + end + rendered.join(' ').html_safe + end + end end end + diff --git a/lib/active_scaffold/helpers/pagination_helpers.rb b/lib/active_scaffold/helpers/pagination_helpers.rb index c67b28ea18..b98550c980 100644 --- a/lib/active_scaffold/helpers/pagination_helpers.rb +++ b/lib/active_scaffold/helpers/pagination_helpers.rb @@ -1,36 +1,69 @@ module ActiveScaffold module Helpers module PaginationHelpers - def pagination_ajax_link(page_number, params) - page_link = link_to_remote(page_number, - { :url => params.merge(:page => page_number), - :after => "$('#{loading_indicator_id(:action => :pagination)}').style.visibility = 'visible';", - :complete => "$('#{loading_indicator_id(:action => :pagination)}').style.visibility = 'hidden';", - :update => active_scaffold_content_id, - :failure => "ActiveScaffold.report_500_response('#{active_scaffold_id}')", - :method => :get }, - { :href => url_for(params.merge(:page => page_number)) }) + def pagination_ajax_link(page_number, url_options, options) + link_to page_number, url_options.merge(:page => page_number), options.merge(:class => "as_paginate") end - def pagination_ajax_links(current_page, params) - start_number = current_page.number - 2 - end_number = current_page.number + 2 + def pagination_ajax_links(current_page, url_options, options, inner_window, outer_window) + unless active_scaffold_config.store_user_settings + url_options.merge!(:search => search_params) if search_params.present? + if active_scaffold_config.list.user.user_sorting? + column, direction = active_scaffold_config.list.user.sorting.first + url_options.merge!(:sort => column.name, :sort_direction => direction) + end + end + + start_number = current_page.number - inner_window + end_number = current_page.number + inner_window start_number = 1 if start_number <= 0 - end_number = current_page.pager.last.number if end_number > current_page.pager.last.number + if current_page.pager.infinite? + offsets = [20, 100] + else + end_number = current_page.pager.last.number if end_number > current_page.pager.last.number + end html = [] - html << pagination_ajax_link(1, params) unless current_page.number <= 3 - html << ".." unless current_page.number <= 4 - start_number.upto(end_number) do |num| + if current_page.number == 1 + last_page = 0 + else + last_page = 1 + last_page.upto([last_page + outer_window, current_page.number - 1].min) do |num| + html << pagination_ajax_link(num, url_options, options) + last_page = num + end + end + if current_page.pager.infinite? + offsets.reverse.each do |offset| + page = current_page.number - offset + if page < start_number && page > last_page + html << '..' if page > last_page + 1 + html << pagination_ajax_link(page, url_options, options) + last_page = page + end + end + end + html << ".." if start_number > last_page + 1 + + [start_number, last_page + 1].max.upto(end_number) do |num| if current_page.number == num - html << num + html << content_tag(:span, num.to_s, {:class => "as_paginate current"}) else - html << pagination_ajax_link(num, params) + html << pagination_ajax_link(num, url_options, options) + end + end + + if current_page.pager.infinite? + offsets.each do |offset| + html << '..' << pagination_ajax_link(current_page.number + offset, url_options, options) + end + else + html << ".." unless end_number >= current_page.pager.last.number - outer_window - 1 + [end_number + 1, current_page.pager.last.number - outer_window].max.upto(current_page.pager.last.number) do |num| + html << pagination_ajax_link(num, url_options, options) end end - html << ".." unless current_page.number >= current_page.pager.last.number - 3 - html << pagination_ajax_link(current_page.pager.last.number, params) unless current_page.number >= current_page.pager.last.number - 2 - html.join(' ') + html.join(' ').html_safe end end end diff --git a/lib/active_scaffold/helpers/search_column_helpers.rb b/lib/active_scaffold/helpers/search_column_helpers.rb index 56c342ce59..6edf4f3758 100644 --- a/lib/active_scaffold/helpers/search_column_helpers.rb +++ b/lib/active_scaffold/helpers/search_column_helpers.rb @@ -4,24 +4,24 @@ module Helpers module SearchColumnHelpers # This method decides which input to use for the given column. # It does not do any rendering. It only decides which method is responsible for rendering. - def active_scaffold_search_for(column) - options = active_scaffold_search_options(column) + def active_scaffold_search_for(column, options = nil) + options ||= active_scaffold_search_options(column) # first, check if the dev has created an override for this specific field for search - if override_search_field?(column) - send(override_search_field(column), @record, options[:name]) - - # first, check if the dev has created an override for this specific field - elsif override_form_field?(column) - send(override_form_field(column), @record, options[:name]) + if (method = override_search_field(column)) + send(method, options[:object] || @record, options) # second, check if the dev has specified a valid search_ui for this column, using specific ui for searches - elsif column.search_ui and override_search?(column.search_ui) - send(override_search(column.search_ui), column, options) + elsif column.search_ui and (method = override_search(column.search_ui)) + send(method, column, options) # third, check if the dev has specified a valid search_ui for this column, using generic ui for forms - elsif column.search_ui and override_input?(column.search_ui) - send(override_input(column.search_ui), column, options) + elsif column.search_ui and (method = override_input(column.search_ui)) + send(method, column, options) + + # fourth, check if the dev has created an override for this specific field + elsif (method = override_form_field(column)) + send(method, options[:object] || @record, options) # fallback: we get to make the decision else @@ -30,17 +30,17 @@ def active_scaffold_search_for(column) else # regular model attribute column # if we (or someone else) have created a custom render option for the column type, use that - if override_search?(column.column.type) - send(override_search(column.column.type), column, options) + if (method = override_search(column.column.type)) + send(method, column, options) # if we (or someone else) have created a custom render option for the column type, use that - elsif override_input?(column.column.type) - send(override_input(column.column.type), column, options) + elsif (method = override_input(column.column.type)) + send(method, column, options) # final ultimate fallback: use rails' generic input method else # for textual fields we pass different options text_types = [:text, :string, :integer, :float, :decimal] options = active_scaffold_input_text_options(options) if text_types.include?(column.column.type) - input(:record, column.name, options.merge(column.options)) + text_field(:record, column.name, options.merge(column.options)) end end end @@ -48,52 +48,67 @@ def active_scaffold_search_for(column) # the standard active scaffold options used for class, name and scope def active_scaffold_search_options(column) - { :name => "search[#{column.name}]", :class => "#{column.name}-input", :id => "search_#{column.name}"} + { :name => "search[#{column.name}]", :class => "#{column.name}-input", :id => "search_#{column.name}", :value => field_search_params[column.name] } + end + + def search_attribute(column, record) + column_options = active_scaffold_search_options(column).merge(:object => record) + field = active_scaffold_search_for column, column_options + %|
#{label_tag search_label_for(column, column_options), search_column_label(column, record)}
#{field}
|.html_safe end + def search_label_for(column, options) + options[:id] unless [:range, :integer, :decimal, :float, :string, :date_picker, :datetime_picker, :calendar_date_select].include? column.search_ui + end + ## ## Search input methods ## def active_scaffold_search_multi_select(column, options) - associated_options = @record.send(column.association.name).collect {|r| [r.to_label, r.id]} - select_options = associated_options | options_for_association(column.association, true) - return as_(:no_options) if select_options.empty? - - html = "
    " - - associated_ids = associated_options.collect {|a| a[1]} - select_options.each_with_index do |option, i| - label, id = option - this_name = "#{options[:name]}[#{i}][id]" - this_id = "#{options[:id]}_#{i}_id" - html << "
  • " - html << check_box_tag(this_name, id, associated_ids.include?(id), :id => this_id) - html << "" - html << "
  • " + associated = options.delete :value + associated = [associated].compact unless associated.is_a? Array + associated.collect!(&:to_i) + + if column.association + select_options = sorted_association_options_find(column.association).collect {|r| [r.to_label, r.id]} + else + select_options = column.options[:options].collect do |text, value| + active_scaffold_translated_option(column, text, value) + end end + return as_(:no_options) if select_options.empty? - html << '
' - html << javascript_tag("new DraggableLists('#{options[:id]}')") if column.options[:draggable_lists] - html + active_scaffold_checkbox_list(column, select_options, associated, options) end - def active_scaffold_search_select(column, options) + def active_scaffold_search_select(column, html_options, options = {}) + associated = html_options.delete :value if column.association - associated = @record.send(column.association.name) - associated = associated.first if associated.is_a?(Array) # for columns with plural association - - select_options = options_for_association(column.association, true) - select_options.unshift([ associated.to_label, associated.id ]) unless associated.nil? or select_options.find {|label, id| id == associated.id} + associated = associated.is_a?(Array) ? associated.map(&:to_i) : associated.to_i unless associated.nil? + method = column.association.macro == :belongs_to ? column.association.foreign_key : column.name + select_options = sorted_association_options_find(column.association, false) + else + method = column.name + select_options = column.options[:options].collect do |text, value| + active_scaffold_translated_option(column, text, value) + end + end - selected = associated.nil? ? nil : associated.id - method = column.association.macro == :belongs_to ? column.association.primary_key_name : column.name - options[:name] += '[id]' - select(:record, method, select_options.uniq, {:selected => selected, :include_blank => as_(:_select_)}, options) + options = options.merge(:selected => associated).merge column.options + html_options.merge! column.options[:html_options] || {} + if html_options[:multiple] + html_options[:name] += '[]' + else + options[:include_blank] ||= as_(:_select_) + end + + if optgroup = options.delete(:optgroup) + select(:record, method, active_scaffold_grouped_options(column, select_options, optgroup), options, html_options) + elsif column.association + collection_select(:record, method, select_options, :id, :to_label, options, html_options) else - select(:record, column.name, column.options, { :selected => @record.send(column.name) }, options) + select(:record, method, select_options, options, html_options) end end @@ -109,33 +124,102 @@ def active_scaffold_search_boolean(column, options) select_options << [as_(:true), true] select_options << [as_(:false), false] - select_tag(options[:name], options_for_select(select_options, @record.send(column.name))) + select_tag(options[:name], options_for_select(select_options, column.column.type_cast(field_search_params[column.name])), :id => options[:id]) end # we can't use checkbox ui because it's not possible to decide whether search for this field or not alias_method :active_scaffold_search_checkbox, :active_scaffold_search_boolean + + def active_scaffold_search_null(column, options) + select_options = [] + select_options << [as_(:_select_), nil] + select_options.concat ActiveScaffold::Finder::NullComparators.collect {|comp| [as_(comp), comp]} + select_tag(options[:name], options_for_select(select_options, field_search_params[column.name]), :id => options[:id]) + end + + def field_search_params_range_values(column) + values = field_search_params[column.name] + return nil if values.nil? + return values[:opt], (values[:from].blank? ? nil : values[:from]), (values[:to].blank? ? nil : values[:to]) + + end + + def active_scaffold_search_range_string?(column) + (column.column && column.column.text?) || column.search_ui == :string + end + + def include_null_comparators?(column) + return column.options[:null_comparators] if column.options.has_key? :null_comparators + if column.association + column.association.macro != :belongs_to || active_scaffold_config.columns[column.association.foreign_key].column.try(:null) + else + column.column.try(:null) + end + end + + def active_scaffold_search_range_comparator_options(column) + select_options = ActiveScaffold::Finder::NumericComparators.collect {|comp| [as_(comp.downcase.to_sym), comp]} + if active_scaffold_search_range_string?(column) + select_options.unshift *ActiveScaffold::Finder::StringComparators.collect {|title, comp| [as_(title), comp]} + end + if include_null_comparators? column + select_options += ActiveScaffold::Finder::NullComparators.collect {|comp| [as_(comp), comp]} + end + select_options + end + + def include_null_comparators?(column) + return column.options[:null_comparators] if column.options.has_key? :null_comparators + if column.association + column.association.macro != :belongs_to || active_scaffold_config.columns[column.association.foreign_key].column.try(:null) + else + column.column.try(:null) + end + end + + def active_scaffold_search_range(column, options) + opt_value, from_value, to_value = field_search_params_range_values(column) - def active_scaffold_search_integer(column, options) - html = [] - html << select_tag("#{options[:name]}[opt]", - options_for_select(ActiveScaffold::Finder::NumericComparators.collect {|comp| [as_(comp.downcase.to_sym), comp]}), + select_options = active_scaffold_search_range_comparator_options(column) + if active_scaffold_search_range_string?(column) + text_field_size = 15 + opt_value ||= '%?%' + else + text_field_size = 10 + opt_value ||= '=' + end + + from_value = controller.class.condition_value_for_numeric(column, from_value) + to_value = controller.class.condition_value_for_numeric(column, to_value) + from_value = format_number_value(from_value, column.options) if from_value.is_a?(Numeric) + to_value = format_number_value(to_value, column.options) if to_value.is_a?(Numeric) + html = select_tag("#{options[:name]}[opt]", + options_for_select(select_options, opt_value), :id => "#{options[:id]}_opt", - :onchange => "Element[this.value == 'BETWEEN' ? 'show' : 'hide']('#{options[:id]}_between');") - html << text_field_tag("#{options[:name]}[from]", nil, active_scaffold_input_text_options(:id => options[:id], :size => 10)) - html << content_tag(:span, ' - ' + text_field_tag("#{options[:name]}[to]", nil, - active_scaffold_input_text_options(:id => "#{options[:id]}_to", :size => 10)), - :id => "#{options[:id]}_between", :style => "display:none") - html * ' ' + :class => "as_search_range_option") + html << content_tag("span", :id => "#{options[:id]}_numeric", :style => ActiveScaffold::Finder::NullComparators.include?(opt_value) ? "display: none" : nil) do + text_field_tag("#{options[:name]}[from]", from_value, active_scaffold_input_text_options(:id => options[:id], :size => text_field_size)) << + content_tag(:span, (' - ' + text_field_tag("#{options[:name]}[to]", to_value, + active_scaffold_input_text_options(:id => "#{options[:id]}_to", :size => text_field_size))).html_safe, + :id => "#{options[:id]}_between", :class => "as_search_range_between", :style => (opt_value == 'BETWEEN') ? nil : "display: none") + end + content_tag :span, html, :class => 'search_range' end - alias_method :active_scaffold_search_decimal, :active_scaffold_search_integer - alias_method :active_scaffold_search_float, :active_scaffold_search_integer + alias_method :active_scaffold_search_integer, :active_scaffold_search_range + alias_method :active_scaffold_search_decimal, :active_scaffold_search_range + alias_method :active_scaffold_search_float, :active_scaffold_search_range + alias_method :active_scaffold_search_string, :active_scaffold_search_range + def field_search_datetime_value(value) + DateTime.new(value[:year].to_i, value[:month].to_i, value[:day].to_i, value[:hour].to_i, value[:minute].to_i, value[:second].to_i) unless value.nil? || value[:year].blank? + end + def active_scaffold_search_datetime(column, options) + opt_value, from_value, to_value = field_search_params_range_values(column) options = column.options.merge(options) helper = "select_#{'date' unless options[:discard_date]}#{'time' unless options[:discard_time]}" - html = [] - html << send(helper, nil, {:include_blank => true, :prefix => "#{options[:name]}[from]"}.merge(options)) - html << send(helper, nil, {:include_blank => true, :prefix => "#{options[:name]}[to]"}.merge(options)) - html * ' - ' + + send(helper, field_search_datetime_value(from_value), {:include_blank => true, :prefix => "#{options[:name]}[from]"}.merge(options)) << + ' - '.html_safe << send(helper, field_search_datetime_value(to_value), {:include_blank => true, :prefix => "#{options[:name]}[to]"}.merge(options)) end def active_scaffold_search_date(column, options) @@ -149,23 +233,45 @@ def active_scaffold_search_time(column, options) ## ## Search column override signatures ## - - def override_search_field?(column) - respond_to?(override_search_field(column)) + + def search_column_label(column, record) + column.label end - # the naming convention for overriding form fields with helpers def override_search_field(column) - "#{column.name}_search_column" - end - - def override_search?(search_ui) - respond_to?(override_search(search_ui)) + override_helper column, 'search_column' end # the naming convention for overriding search input types with helpers def override_search(form_ui) - "active_scaffold_search_#{form_ui}" + method = "active_scaffold_search_#{form_ui}" + method if respond_to? method + end + + def visibles_and_hiddens(search_config) + visibles = [] + hiddens = [] + search_config.columns.each do |column| + next unless column.search_sql + if search_config.optional_columns.include?(column.name) && !searched_by?(column) + hiddens << column + else + visibles << column + end + end + return visibles, hiddens + end + + def searched_by?(column) + value = field_search_params[column.name] + case value + when Hash + !value['from'].blank? + when String + !value.blank? + else + false + end end end end diff --git a/lib/active_scaffold/helpers/show_column_helpers.rb b/lib/active_scaffold/helpers/show_column_helpers.rb index a330e63c68..c0a10d8d66 100644 --- a/lib/active_scaffold/helpers/show_column_helpers.rb +++ b/lib/active_scaffold/helpers/show_column_helpers.rb @@ -4,42 +4,35 @@ module Helpers module ShowColumnHelpers def show_column_value(record, column) # check for an override helper - if show_column_override? column + if (method = show_column_override(column)) # we only pass the record as the argument. we previously also passed the formatted_value, # but mike perham pointed out that prohibited the usage of overrides to improve on the # performance of our default formatting. see issue #138. - send(show_column_override(column), record) + send(method, record, column) # second, check if the dev has specified a valid list_ui for this column - elsif column.list_ui and override_show_column_ui?(column.list_ui) - send(override_show_column_ui(column.list_ui), column, record) + elsif column.list_ui and (method = override_show_column_ui(column.list_ui)) + send(method, record, column) else - if column.column and override_show_column_ui?(column.column.type) - send(override_show_column_ui(column.column.type), column, record) + if column.column and (method = override_show_column_ui(column.column.type)) + send(method, record, column) else get_column_value(record, column) end end end - def active_scaffold_show_text(column, record) + def active_scaffold_show_text(record, column) simple_format(clean_column_value(record.send(column.name))) end def show_column_override(column) - "#{column.name.to_s.gsub('?', '')}_show_column" # parse out any question marks (see issue 227) - end - - def show_column_override?(column) - respond_to?(show_column_override(column)) - end - - def override_show_column_ui?(list_ui) - respond_to?(override_show_column_ui(list_ui)) + override_helper column, 'show_column' end # the naming convention for overriding show types with helpers def override_show_column_ui(list_ui) - "active_scaffold_show_#{list_ui}" + method = "active_scaffold_show_#{list_ui}" + method if respond_to? method end end end diff --git a/lib/active_scaffold/helpers/view_helpers.rb b/lib/active_scaffold/helpers/view_helpers.rb index 67ed240aa9..f1ef427904 100755 --- a/lib/active_scaffold/helpers/view_helpers.rb +++ b/lib/active_scaffold/helpers/view_helpers.rb @@ -3,14 +3,15 @@ module Helpers # All extra helpers that should be included in the View. # Also a dumping ground for uncategorized helpers. module ViewHelpers + NESTED_PARAMS = [:eid, :association, :parent_scaffold] include ActiveScaffold::Helpers::IdHelpers include ActiveScaffold::Helpers::AssociationHelpers include ActiveScaffold::Helpers::PaginationHelpers include ActiveScaffold::Helpers::ListColumnHelpers include ActiveScaffold::Helpers::ShowColumnHelpers include ActiveScaffold::Helpers::FormColumnHelpers - include ActiveScaffold::Helpers::CountryHelpers include ActiveScaffold::Helpers::SearchColumnHelpers + include ActiveScaffold::Helpers::HumanConditionHelpers ## ## Delegates @@ -18,23 +19,34 @@ module ViewHelpers # access to the configuration variable def active_scaffold_config - @controller.class.active_scaffold_config + controller.class.active_scaffold_config end def active_scaffold_config_for(*args) - @controller.class.active_scaffold_config_for(*args) + controller.class.active_scaffold_config_for(*args) end def active_scaffold_controller_for(*args) - @controller.class.active_scaffold_controller_for(*args) + controller.class.active_scaffold_controller_for(*args) end ## ## Uncategorized ## - def generate_temporary_id - (Time.now.to_f*1000).to_i.to_s + def controller_path_for_activerecord(klass) + begin + controller = active_scaffold_controller_for(klass) + controller.controller_path + rescue ActiveScaffold::ControllerNotFound + controller = nil + end + end + + # This is the template finder logic, keep it updated with however we find stuff in rails + # currently this very similar to the logic in ActionBase::Base.render for options file + def template_exists?(template_name, partial = false) + lookup_context.exists? template_name, '', partial end # Turns [[label, value]] into