├── test
├── dummy
│ ├── log
│ │ └── .keep
│ ├── app
│ │ ├── mailers
│ │ │ └── .keep
│ │ ├── models
│ │ │ ├── .keep
│ │ │ └── concerns
│ │ │ │ └── .keep
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ ├── javascripts
│ │ │ │ └── application.js
│ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ ├── controllers
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ └── application_controller.rb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ └── views
│ │ │ └── layouts
│ │ │ └── application.html.erb
│ ├── lib
│ │ └── assets
│ │ │ └── .keep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── config
│ │ ├── routes.rb
│ │ ├── initializers
│ │ │ ├── cookies_serializer.rb
│ │ │ ├── session_store.rb
│ │ │ ├── mime_types.rb
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── assets.rb
│ │ │ ├── wrap_parameters.rb
│ │ │ └── inflections.rb
│ │ ├── environment.rb
│ │ ├── boot.rb
│ │ ├── database.yml
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── secrets.yml
│ │ ├── application.rb
│ │ └── environments
│ │ │ ├── development.rb
│ │ │ ├── test.rb
│ │ │ └── production.rb
│ ├── bin
│ │ ├── rake
│ │ ├── bundle
│ │ ├── rails
│ │ └── setup
│ ├── config.ru
│ ├── Rakefile
│ └── README.rdoc
├── administa_test.rb
├── integration
│ └── navigation_test.rb
├── controllers
│ └── administa
│ │ └── test_controller_test.rb
└── test_helper.rb
├── app
├── assets
│ ├── images
│ │ └── administa
│ │ │ └── .keep
│ ├── javascripts
│ │ └── administa
│ │ │ ├── build
│ │ │ ├── 448c34a56d699c29117adc64c43affeb.woff2
│ │ │ ├── 7799dece2c79854f63f09e7dfa528b88.jpg
│ │ │ ├── e18bbf611f2a2e43afc071aa2f4e1512.ttf
│ │ │ ├── f4769f9bdb7466be65088239c12046d1.eot
│ │ │ └── fa2772327f55d8198301fdb8bcfc8158.woff
│ │ │ └── application.js
│ └── stylesheets
│ │ └── administa
│ │ └── application.css
├── helpers
│ └── administa
│ │ └── application_helper.rb
├── controllers
│ └── administa
│ │ ├── application_controller.rb
│ │ ├── main_controller.rb
│ │ ├── actions
│ │ ├── new.rb
│ │ ├── edit.rb
│ │ ├── destroy.rb
│ │ ├── show.rb
│ │ ├── create.rb
│ │ ├── update.rb
│ │ └── index.rb
│ │ ├── controller.rb
│ │ ├── generic_controller.rb
│ │ ├── error_handlers.rb
│ │ └── base.rb
└── views
│ ├── administa
│ └── main
│ │ └── index.html.erb
│ ├── application
│ └── index.html.erb
│ └── layouts
│ └── administa
│ └── application.html.erb
├── lib
├── administa
│ ├── version.rb
│ ├── engine.rb
│ ├── model.rb
│ ├── model
│ │ ├── finder.rb
│ │ ├── json.rb
│ │ ├── relation.rb
│ │ ├── attributes.rb
│ │ └── options.rb
│ ├── config.rb
│ └── config
│ │ ├── auth.rb
│ │ ├── dynamic_controller.rb
│ │ └── menu.rb
├── tasks
│ └── administa_tasks.rake
├── generators
│ └── administa
│ │ ├── USAGE
│ │ ├── templates
│ │ └── controller.rb
│ │ └── administa_generator.rb
└── administa.rb
├── administa-demo.png
├── js
├── AppDispatcher.js
├── components
│ ├── list
│ │ ├── Header.react.js
│ │ ├── Item.react.js
│ │ ├── SearchBox.react.js
│ │ ├── Pagination.react.js
│ │ ├── List.react.js
│ │ └── Selection.react.js
│ ├── Footer.react.js
│ ├── Main.react.js
│ ├── ContentHeader.react.js
│ ├── form
│ │ ├── HasMany.react.js
│ │ ├── HasOne.react.js
│ │ ├── Through.react.js
│ │ ├── BelongsTo.react.js
│ │ ├── InputMixin.js
│ │ ├── CollectionMixin.js
│ │ ├── Input.react.js
│ │ ├── Form.react.js
│ │ └── Association.react.js
│ ├── detail
│ │ ├── Property.react.js
│ │ └── Detail.react.js
│ ├── Dialog.react.js
│ ├── Dialogs.react.js
│ ├── Header.react.js
│ ├── Resource.react.js
│ ├── Menu.react.js
│ ├── PropertyMixin.js
│ └── LinkMixin.js
├── actions
│ ├── MenuActions.js
│ ├── UserActions.js
│ ├── DialogActions.js
│ └── ResourceActions.js
├── Utils.js
├── Constants.js
├── plugin
│ └── ForceReplaceSourceMapppingURLPlugin.js
├── stores
│ ├── UserStore.js
│ ├── MenuStore.js
│ ├── AppStore.js
│ ├── DialogStore.js
│ └── ResourceStore.js
├── Loader.js
└── app.js
├── config
└── locales
│ ├── administa.ja.yml
│ └── administa.en.yml
├── .gitignore
├── webpack.config.release.js
├── bin
└── rails
├── Gemfile
├── .eslintrc
├── README.md
├── administa.gemspec
├── Rakefile
├── MIT-LICENSE
├── package.json
├── webpack.config.js
├── Gemfile.lock
└── css
└── app.css
/test/dummy/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/administa/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/administa/version.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | VERSION = "0.0.3"
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/administa-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuroyoro/administa/HEAD/administa-demo.png
--------------------------------------------------------------------------------
/js/AppDispatcher.js:
--------------------------------------------------------------------------------
1 | let Dispatcher = Flux.Dispatcher;
2 |
3 | export default new Dispatcher;
4 |
--------------------------------------------------------------------------------
/app/helpers/administa/application_helper.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | module ApplicationHelper
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 |
3 | mount Administa::Engine => "/administa"
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/lib/tasks/administa_tasks.rake:
--------------------------------------------------------------------------------
1 | # desc "Explaining what the task does"
2 | # task :administa do
3 | # # Task goes here
4 | # end
5 |
--------------------------------------------------------------------------------
/app/controllers/administa/application_controller.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | class ApplicationController < ActionController::Base
3 |
4 |
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/dummy/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/test/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../../config/application', __FILE__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/test/administa_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class AdministaTest < ActiveSupport::TestCase
4 | test "truth" do
5 | assert_kind_of Module, Administa
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Rails.application
5 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.action_dispatch.cookies_serializer = :json
4 |
--------------------------------------------------------------------------------
/lib/generators/administa/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Explain the generator
3 |
4 | Example:
5 | rails generate administa Thing
6 |
7 | This will create:
8 | what/will/it/create
9 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session'
4 |
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/app/assets/javascripts/administa/build/448c34a56d699c29117adc64c43affeb.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuroyoro/administa/HEAD/app/assets/javascripts/administa/build/448c34a56d699c29117adc64c43affeb.woff2
--------------------------------------------------------------------------------
/app/assets/javascripts/administa/build/7799dece2c79854f63f09e7dfa528b88.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuroyoro/administa/HEAD/app/assets/javascripts/administa/build/7799dece2c79854f63f09e7dfa528b88.jpg
--------------------------------------------------------------------------------
/app/assets/javascripts/administa/build/e18bbf611f2a2e43afc071aa2f4e1512.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuroyoro/administa/HEAD/app/assets/javascripts/administa/build/e18bbf611f2a2e43afc071aa2f4e1512.ttf
--------------------------------------------------------------------------------
/app/assets/javascripts/administa/build/f4769f9bdb7466be65088239c12046d1.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuroyoro/administa/HEAD/app/assets/javascripts/administa/build/f4769f9bdb7466be65088239c12046d1.eot
--------------------------------------------------------------------------------
/app/assets/javascripts/administa/build/fa2772327f55d8198301fdb8bcfc8158.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuroyoro/administa/HEAD/app/assets/javascripts/administa/build/fa2772327f55d8198301fdb8bcfc8158.woff
--------------------------------------------------------------------------------
/config/locales/administa.ja.yml:
--------------------------------------------------------------------------------
1 | ja:
2 | administa:
3 | flash:
4 | created: "%{name}(id: %{id}) が登録されました"
5 | updated: "%{name}(id: %{id}) が更新されました"
6 | deleted: "%{name}(id: %{id}) が削除されました"
7 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/app/controllers/administa/main_controller.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | class MainController < Administa::ApplicationController
3 | include ::Administa::Base
4 |
5 | def index
6 |
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/integration/navigation_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class NavigationTest < ActionDispatch::IntegrationTest
4 | fixtures :all
5 |
6 | # test "the truth" do
7 | # assert true
8 | # end
9 | end
10 |
11 |
--------------------------------------------------------------------------------
/lib/generators/administa/templates/controller.rb:
--------------------------------------------------------------------------------
1 | class <%= "#{@options[:namespace].camelize}::#{class_name.pluralize}Controller" %> < <%= @options[:base_class] %>
2 | include Administa::Controller
3 |
4 | administa model: <%= class_name %>
5 | end
6 |
--------------------------------------------------------------------------------
/test/controllers/administa/test_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | module Administa
4 | class TestControllerTest < ActionController::TestCase
5 | # test "the truth" do
6 | # assert true
7 | # end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/config/locales/administa.en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | administa:
3 | flash:
4 | created: "%{name}(id: %{id}) was successfully created"
5 | updated: "%{name}(id: %{id}) was successfully updated"
6 | deleted: "%{name}(id: %{id}) was successfully deleted"
7 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
3 |
4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
6 |
--------------------------------------------------------------------------------
/test/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/js/components/list/Header.react.js:
--------------------------------------------------------------------------------
1 | var PT = React.PropTypes
2 |
3 | export default React.createClass({
4 | displayName: 'list/Header',
5 |
6 | propTypes: {
7 | label: PT.string
8 | },
9 |
10 | render() {
11 | return
{ this.props.label } ;
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/js/actions/MenuActions.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../AppDispatcher';
2 | import Constants from '../Constants';
3 |
4 | export default {
5 |
6 | initialize(data) {
7 | AppDispatcher.dispatch({
8 | type: Constants.MENU_INITIALIZED,
9 | data: data
10 | });
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/js/actions/UserActions.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../AppDispatcher';
2 | import Constants from '../Constants';
3 |
4 | export default {
5 |
6 | initialize(data) {
7 | AppDispatcher.dispatch({
8 | type: Constants.USER_INITIALIZED,
9 | data: data
10 | });
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/administa/engine.rb:
--------------------------------------------------------------------------------
1 |
2 | module Administa
3 | class Engine < ::Rails::Engine
4 |
5 | isolate_namespace Administa
6 |
7 | config.after_initialize do
8 | if Rails.env.production? && (not Rails.groups.include?("assets"))
9 | Administa.config.initialize!
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle/
2 | log/*.log
3 | pkg/
4 | test/dummy/db/*.sqlite3
5 | test/dummy/db/*.sqlite3-journal
6 | test/dummy/log/*.log
7 | test/dummy/tmp/
8 | test/dummy/.sass-cache
9 | bower_components/
10 | node_modules/
11 | Gemfile.lock
12 | vendor/
13 | .ruby-version
14 | test/lib/
15 |
16 | app/assets/javascripts/administa/build/*.js.map
17 |
--------------------------------------------------------------------------------
/js/components/Footer.react.js:
--------------------------------------------------------------------------------
1 |
2 | export default React.createClass({
3 | render() {
4 | return(
5 |
10 | );
11 | },
12 | })
13 |
--------------------------------------------------------------------------------
/js/components/Main.react.js:
--------------------------------------------------------------------------------
1 | import Header from 'components/Header.react';
2 | import Menu from 'components/Menu.react';
3 | import Footer from 'components/Footer.react';
4 |
5 | export default React.createClass({
6 | displayName: 'Main',
7 | render() {
8 | return (
9 |
10 |
11 | );
12 | }
13 |
14 | })
15 |
16 |
--------------------------------------------------------------------------------
/app/views/administa/main/index.html.erb:
--------------------------------------------------------------------------------
1 |
6 |
7 |
14 |
--------------------------------------------------------------------------------
/app/views/application/index.html.erb:
--------------------------------------------------------------------------------
1 |
6 |
7 |
14 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/webpack.config.release.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var config = require("./webpack.config.js");
3 |
4 | config.plugins.push(
5 | new webpack.optimize.UglifyJsPlugin({
6 | sourceMap: false,
7 | mangle: {
8 | except: ['$super', '$', 'jQuery', 'React', 'exports', 'require']
9 | },
10 | compress: {
11 | warnings: false,
12 | drop_console: true
13 | }
14 | })
15 | );
16 |
17 | module.exports = config;
18 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
3 |
4 | ENGINE_ROOT = File.expand_path('../..', __FILE__)
5 | ENGINE_PATH = File.expand_path('../../lib/administa/engine', __FILE__)
6 |
7 | # Set up gems listed in the Gemfile.
8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
10 |
11 | require 'rails/all'
12 | require 'rails/engine/commands'
13 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
11 | # Rails.application.config.assets.precompile += %w( search.js )
12 |
--------------------------------------------------------------------------------
/js/actions/DialogActions.js:
--------------------------------------------------------------------------------
1 |
2 | import AppDispatcher from '../AppDispatcher';
3 | import Constants from '../Constants';
4 |
5 | export default {
6 |
7 | open(name, component, props) {
8 | AppDispatcher.dispatch({
9 | type: Constants.DIALOG_OPENED,
10 | data: {
11 | name: name,
12 | component: component,
13 | props: props
14 | }
15 | });
16 | },
17 |
18 | close(name) {
19 | AppDispatcher.dispatch({
20 | type: Constants.DIALOG_CLOSED,
21 | data: {
22 | name: name,
23 | }
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/controllers/administa/actions/new.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | module Actions
3 | module New
4 |
5 | def new
6 |
7 | @result = index_result
8 |
9 | resource = model.klass.new
10 |
11 | @result[:id] = nil
12 | @result[:resource] = model.as_json(resource, action: :create)
13 |
14 | respond_to do |format|
15 | format.html { render :index }
16 | format.json { render json: to_json(@result) }
17 | end
18 | rescue => e
19 | handle_exception(e)
20 | end
21 |
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/js/components/ContentHeader.react.js:
--------------------------------------------------------------------------------
1 | export default React.createClass({
2 | displayName: 'ContentHeader',
3 |
4 | render() {
5 | return (
6 |
7 |
8 |
9 | Page Header
10 | Optional description
11 |
12 |
13 | Level
14 | Here
15 |
16 |
17 | );
18 | }
19 | });
20 |
21 |
22 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/app/controllers/administa/actions/edit.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | module Actions
3 | module Edit
4 |
5 | def edit
6 |
7 | @result = index_result
8 | resource = model.find(params[:id], action: :edit)
9 |
10 | @result[:id] = resource.id
11 | @result[:resource] = model.as_json(resource, action: :edit)
12 |
13 | respond_to do |format|
14 | format.html { render :index }
15 | format.json { render json: to_json(@result) }
16 | end
17 | rescue => e
18 | handle_exception(e)
19 | end
20 |
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/dummy/README.rdoc:
--------------------------------------------------------------------------------
1 | == README
2 |
3 | This README would normally document whatever steps are necessary to get the
4 | application up and running.
5 |
6 | Things you may want to cover:
7 |
8 | * Ruby version
9 |
10 | * System dependencies
11 |
12 | * Configuration
13 |
14 | * Database creation
15 |
16 | * Database initialization
17 |
18 | * How to run the test suite
19 |
20 | * Services (job queues, cache servers, search engines, etc.)
21 |
22 | * Deployment instructions
23 |
24 | * ...
25 |
26 |
27 | Please feel free to use a different markup language if you do not plan to run
28 | rake doc:app .
29 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Declare your gem's dependencies in administa.gemspec.
4 | # Bundler will treat runtime dependencies like base dependencies, and
5 | # development dependencies will be added by default to the :development group.
6 | gemspec
7 |
8 | # Declare any dependencies that are still in development here instead of in
9 | # your gemspec. These might include edge Rails or gems from your path or
10 | # Git. Remember to move these dependencies to your gemspec before releasing
11 | # your gem to rubygems.org.
12 |
13 | # To use a debugger
14 | # gem 'byebug', group: [:development, :test]
15 |
16 |
--------------------------------------------------------------------------------
/test/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require_tree .
14 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "indent": [ 1, 2 ],
4 | "quotes": [ 1, "double" ],
5 | "linebreak-style": [ 2, "unix" ],
6 | "semi": [ 2, "always" ],
7 | "comma-dangle": [2, "always-multiline"],
8 | "no-console": [1],
9 |
10 | },
11 | "env": {
12 | "es6": true,
13 | "node": true,
14 | "browser" : true
15 | },
16 | "extends": "eslint:recommended",
17 | "ecmaFeatures": {
18 | "jsx": true,
19 | "modules": true,
20 | "experimentalObjectRestSpread": true
21 | },
22 | "globals" :{
23 | "React": true,
24 | "jQuery": true,
25 | },
26 | "plugins": [
27 | "react"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/app/controllers/administa/actions/destroy.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | module Actions
3 | module Destroy
4 |
5 | def destroy
6 |
7 | resource = model.find(params[:id], action: :edit)
8 |
9 | unless resource.destroy
10 | handle_validate_errors(resource.errors)
11 | end
12 |
13 | @result = index_result
14 | @result["flash"] = I18n.t("administa.flash.deleted", name: model.label, id: resource.id )
15 |
16 | respond_to do |format|
17 | format.html { render :index }
18 | format.json { render json: to_json(@result) }
19 | end
20 | rescue => e
21 | handle_exception(e)
22 | end
23 |
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Administa
2 |
3 | The administration console framework/generator for Ruby on Rails.
4 | Supported rails versions are Rails3.2+ and Rails4.
5 |
6 | Administa is under heavy development, no tests, no documents ( ꒪⌓꒪)
7 |
8 | ## Demo
9 |
10 | [Demo Application](https://administa-demo.herokuapp.com/)
11 |
12 | [Demo Application Source Code](https://github.com/yuroyoro/administa_demo)
13 |
14 | 
15 |
16 | ## Installation
17 |
18 | Add this line to your application's Gemfile:
19 |
20 | gem 'administa'
21 |
22 | And then execute:
23 |
24 | $ bundle
25 |
26 | Or install it yourself as:
27 |
28 | $ gem install administa
29 |
--------------------------------------------------------------------------------
/lib/administa.rb:
--------------------------------------------------------------------------------
1 | require "administa/engine"
2 | require "administa/config"
3 | require "administa/model"
4 |
5 | #
6 | # TODO
7 | # - animation入れる
8 | # - test書きたい
9 | # - ESLInt
10 | #
11 | # Done;
12 | # - 認証
13 | # - has_many対応
14 | # - has_many through対応
15 | # - validation
16 | # - 子associationをnewした後、再度ダイアログ開くと、選択されているassociationの情報が消える
17 | # - メッセージ国際化対応
18 | # - メニュー
19 | # - メニュー階層化
20 | # - file upload
21 | # - enum
22 | # - boolean
23 | # - genratorつける
24 | # - permalink
25 | # - datetime picker
26 | # - 削除
27 | #
28 | module Administa
29 |
30 | def self.config(&block)
31 | @config ||= Administa::Config.new
32 | if block_given?
33 | yield @config
34 | end
35 |
36 | @config
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/test/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/js/Utils.js:
--------------------------------------------------------------------------------
1 | export default {
2 | present(obj) {
3 | return obj && Object.keys(obj).length > 0;
4 | },
5 |
6 | empty(obj) {
7 | return !obj || Object.keys(obj).length == 0;
8 | },
9 |
10 | isPrimitive(obj) {
11 | return !( (obj instanceof File ) || (obj instanceof Array) || (obj instanceof Object));
12 | },
13 |
14 | reportError (message, error, stack) {
15 | // e instanceof ErrorEvent
16 | jQuery("#resultLoading").hide();
17 |
18 | var html = `javascript error : ${message} `;
19 | var stack = stack || (error && error.stack);
20 | if( stack ) {
21 | html += `
${stack} `
22 | }
23 | html += "
"
24 |
25 | jQuery("body").prepend(html);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/administa/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/app/controllers/administa/actions/show.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | module Actions
3 | module Show
4 |
5 | def show
6 |
7 | @result = show_result(params[:id])
8 |
9 | respond_to do |format|
10 | format.html { render :index }
11 | format.json { render json: to_json(@result) }
12 | end
13 | rescue => e
14 | handle_exception(e)
15 | end
16 |
17 | protected
18 | def show_result(id)
19 | result = request.xhr? ? default_result : index_result
20 |
21 | resource = model.find(id, action: :show)
22 |
23 | result[:id] = resource.id
24 | result[:resource] = model.as_json(resource, action: :show)
25 | result
26 | end
27 |
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/js/Constants.js:
--------------------------------------------------------------------------------
1 |
2 | var keyMirror = function(obj) {
3 | var ret = {};
4 | var key;
5 | if (!(obj instanceof Object && !Array.isArray(obj))) {
6 | throw new Error('keyMirror(...): Argument must be an object.');
7 | }
8 | for (key in obj) {
9 | if (obj.hasOwnProperty(key)) {
10 | ret[key] = key;
11 | }
12 | }
13 | return ret;
14 | };
15 |
16 | export default keyMirror({
17 | INITIALIZE: null,
18 | APP_TRANSITION: null,
19 | RESOURCE_BUILD: null,
20 | RESOURCE_FETCH: null,
21 | RESOURCE_LIST: null,
22 | RESOURCE_UPDATED: null,
23 | RESOURCE_INVALID: null,
24 | RESOURCE_DELETED: null,
25 | DIALOG_OPENED: null,
26 | DIALOG_CLOSED: null,
27 | MENU_INITIALIZED: null,
28 | USER_INITIALIZED: null,
29 |
30 | });
31 |
--------------------------------------------------------------------------------
/app/assets/javascripts/administa/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | // Load packed libraries js file that compiled by webpack
14 | //= require ./build/vendors
15 | //
16 | // Incudes bundled js that compiled by webpack
17 | //= require ./build/app.bundle
18 |
--------------------------------------------------------------------------------
/app/controllers/administa/controller.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | module Controller
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 | include ::Administa::Base
7 | include ::Administa::Actions::Index
8 | include ::Administa::Actions::Show
9 | include ::Administa::Actions::New
10 | include ::Administa::Actions::Create
11 | include ::Administa::Actions::Edit
12 | include ::Administa::Actions::Update
13 | include ::Administa::Actions::Destroy
14 | include ::Administa::ErrorHandlers
15 |
16 | respond_to :html, :json
17 |
18 | class_attribute :model, :instance_write => false
19 | end
20 |
21 | module ClassMethods
22 |
23 | def administa(model:, **options)
24 | self.model = Administa::Model.new(self, model, options)
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/js/plugin/ForceReplaceSourceMapppingURLPlugin.js:
--------------------------------------------------------------------------------
1 | function ForceReplaceSourceMapppingURLPlugin(patterns) {
2 | this.patterns = patterns;
3 | }
4 |
5 | module.exports = ForceReplaceSourceMapppingURLPlugin;
6 | ForceReplaceSourceMapppingURLPlugin.prototype.apply = function(compiler) {
7 | var patterns = this.patterns;
8 | compiler.plugin("compilation", function(compilation) {
9 | compilation.plugin("after-optimize-assets", function(assets) {
10 | patterns.forEach(function(pattern) {
11 | asset = assets[pattern.asset];
12 | if(asset) {
13 | var children = asset.children;
14 | var comment = children.pop();
15 | children.push(
16 | comment.replace(/sourceMappingURL=(.+)/, 'sourceMappingURL=' + pattern.sourceMappingURL)
17 | );
18 | }
19 | });
20 | });
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/administa.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 |
3 | # Maintain your gem's version:
4 | require "administa/version"
5 |
6 | # Describe your gem and declare its dependencies:
7 | Gem::Specification.new do |s|
8 | s.name = "administa"
9 | s.version = Administa::VERSION
10 | s.authors = ["Tomohito Ozaki"]
11 | s.email = ["ozaki@yuroyoro.com"]
12 | s.homepage = "https://github.com/yuroyoro/administa"
13 | s.summary = "The administration console framework/generator"
14 | s.description = "The administration console framework/generator"
15 | s.license = "MIT"
16 |
17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
18 | s.test_files = Dir["test/**/*"]
19 |
20 | s.add_dependency "rails", '>= 3.2', '< 5.0'
21 |
22 | s.add_development_dependency "sqlite3"
23 | end
24 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | begin
2 | require 'bundler/setup'
3 | rescue LoadError
4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5 | end
6 |
7 | require 'rdoc/task'
8 |
9 | RDoc::Task.new(:rdoc) do |rdoc|
10 | rdoc.rdoc_dir = 'rdoc'
11 | rdoc.title = 'Administa'
12 | rdoc.options << '--line-numbers'
13 | rdoc.rdoc_files.include('README.rdoc')
14 | rdoc.rdoc_files.include('lib/**/*.rb')
15 | end
16 |
17 | APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18 | load 'rails/tasks/engine.rake'
19 |
20 |
21 | load 'rails/tasks/statistics.rake'
22 |
23 |
24 |
25 | Bundler::GemHelper.install_tasks
26 |
27 | require 'rake/testtask'
28 |
29 | Rake::TestTask.new(:test) do |t|
30 | t.libs << 'lib'
31 | t.libs << 'test'
32 | t.pattern = 'test/**/*_test.rb'
33 | t.verbose = false
34 | end
35 |
36 |
37 | task default: :test
38 |
--------------------------------------------------------------------------------
/js/components/form/HasMany.react.js:
--------------------------------------------------------------------------------
1 | import InputMixin from './InputMixin';
2 | import CollectionMixin from './CollectionMixin';
3 | import PropertyMixin from 'components/PropertyMixin';
4 |
5 | export default React.createClass({
6 | displayName: 'form/HasMany',
7 |
8 | mixins: [PropertyMixin, InputMixin, CollectionMixin],
9 |
10 | getFormValue() {
11 | var keys = Object.keys(this.refs);
12 | var result = {};
13 | var targets = [];
14 | var name = this.props.column.association.name;
15 |
16 | for (var i = 0, len = keys.length; i < len; i++) {
17 | var key = keys[i];
18 | var property = this.refs[key];
19 | if(property.isDirty()) {
20 | var value = property.getFormValue();
21 | targets.push(value[name]);
22 | }
23 | }
24 |
25 | result[name] = targets;
26 | return result;
27 | },
28 |
29 | })
30 |
--------------------------------------------------------------------------------
/app/controllers/administa/generic_controller.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | class GenericController < ActionController::Base
3 | include Administa::Controller
4 |
5 | if Rails.version < "4.0"
6 | before_filter :reject_except_json
7 | else
8 | before_action :reject_except_json
9 | end
10 |
11 | def model
12 | return @model if @model
13 |
14 | name = params[:model].try(:camelize).try(:singularize)
15 | if @model = Administa.config.models[name]
16 | return @model
17 | end
18 |
19 | model = name.try(:safe_constantize)
20 | @model = Administa::Model.new(self, model, {})
21 | @model.setup_options!
22 | @model
23 | end
24 |
25 | def reject_except_json
26 | unless request.format == :json
27 | render :nothing => true, :status => 406
28 | end
29 | end
30 |
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/administa/model.rb:
--------------------------------------------------------------------------------
1 | require "administa/model/options"
2 | require "administa/model/finder"
3 | require "administa/model/relation"
4 | require "administa/model/json"
5 | require "administa/model/attributes"
6 |
7 | module Administa
8 | class Model
9 | include Administa::Model::Options
10 | include Administa::Model::Finder
11 | include Administa::Model::Json
12 | include Administa::Model::Attributes
13 |
14 | attr_accessor :name, :controller, :klass, :options, :given_options
15 | def initialize(controller, klass, options = {})
16 | self.name = klass.name.underscore
17 | self.controller = controller
18 | self.klass = klass
19 | self.given_options = options
20 |
21 | Administa.config.add_model(self)
22 | end
23 |
24 | delegate :table_name, :column_names, :columns_hash, :arel_table, :to => :klass
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/administa/model/finder.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | class Model
3 | module Finder
4 | def all
5 | rel = (Rails.version < "4.0") ? klass.scoped : klass.all
6 | Administa::Model::Relation.new(self, rel)
7 | end
8 |
9 | def select(options = {})
10 | options = extract_options_for_query(options)
11 | all.select(options)
12 | end
13 |
14 | def find(id, options = {})
15 | options = extract_options_for_query(options)
16 |
17 | klass.includes(options[:includes]).find(id)
18 | end
19 |
20 | private
21 | def extract_options_for_query(options = {})
22 | options = options.dup
23 | action = options.delete(:action)
24 | options[:includes] = options[:includes] || action.try{|act| self.includes(act) } || {}
25 |
26 | options
27 | end
28 |
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Configure Rails Environment
2 | ENV["RAILS_ENV"] = "test"
3 |
4 | require File.expand_path("../../test/dummy/config/environment.rb", __FILE__)
5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../test/dummy/db/migrate", __FILE__)]
6 | ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__)
7 | require "rails/test_help"
8 |
9 | # Filter out Minitest backtrace while allowing backtrace from other libraries
10 | # to be shown.
11 | Minitest.backtrace_filter = Minitest::BacktraceFilter.new
12 |
13 | # Load support files
14 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
15 |
16 | # Load fixtures from the engine
17 | if ActiveSupport::TestCase.respond_to?(:fixture_path=)
18 | ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
19 | ActiveSupport::TestCase.fixtures :all
20 | end
21 |
--------------------------------------------------------------------------------
/app/controllers/administa/actions/create.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | module Actions
3 | module Create
4 |
5 | def create
6 |
7 | resource = model.klass.new
8 |
9 | # TODO: strong parameters
10 | params.permit! if params.respond_to? :permit!
11 |
12 | attrs = params["resource"]
13 |
14 | model.assign(resource, attrs)
15 | unless resource.save!
16 | handle_validate_errors(resource.errors)
17 | return
18 | end
19 |
20 | @result = show_result(resource.id)
21 | @result["flash"] = I18n.t("administa.flash.created", name: model.label, id: resource.id )
22 |
23 | respond_to do |format|
24 | format.html { render :index }
25 | format.json { render json: to_json(@result) }
26 | end
27 | rescue => e
28 | handle_exception(e)
29 | end
30 |
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/views/layouts/administa/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Administa
5 |
6 | <%= stylesheet_link_tag "administa/application", media: "all" %>
7 | <%= javascript_include_tag "administa/application" %>
8 | <%= csrf_meta_tags %>
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | <%= yield %>
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/controllers/administa/actions/update.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | module Actions
3 | module Update
4 |
5 | def update
6 |
7 | resource = model.find(params[:id], action: :edit)
8 |
9 | # TODO: strong parameters
10 | params.permit! if params.respond_to? :permit!
11 |
12 | attrs = params["resource"]
13 |
14 | model.assign(resource, attrs)
15 | unless resource.save!
16 | handle_validate_errors(resource.errors)
17 | return
18 | end
19 |
20 | @result = show_result(params[:id])
21 | @result["flash"] = I18n.t("administa.flash.updated", name: model.label, id: resource.id )
22 |
23 | respond_to do |format|
24 | format.html { render :index }
25 | format.json { render json: to_json(@result) }
26 | end
27 | rescue => e
28 | handle_exception(e)
29 | end
30 |
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/js/components/detail/Property.react.js:
--------------------------------------------------------------------------------
1 | import PropertyMixin from 'components/PropertyMixin';
2 |
3 | export default React.createClass({
4 | displayName: 'detail/Property',
5 |
6 | mixins: [PropertyMixin],
7 |
8 | propTypes: {
9 | column: React.PropTypes.object.isRequired,
10 | resource: React.PropTypes.object.isRequired,
11 | settings: React.PropTypes.object.isRequired,
12 |
13 | },
14 |
15 | render() {
16 | var column = this.props.column;
17 | var name = this.toProperyName(column);
18 |
19 | var resource = this.props.resource;
20 |
21 | var value = this.toLabel(column, resource,
22 | { search_columns: this.props.settings.search_columns, wrap_tag: true }
23 | );
24 |
25 | return(
26 |
27 |
{ name }
28 |
{ value }
29 |
30 | );
31 | },
32 | })
33 |
--------------------------------------------------------------------------------
/test/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 |
4 | # path to your application root.
5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
6 |
7 | Dir.chdir APP_ROOT do
8 | # This script is a starting point to setup your application.
9 | # Add necessary setup steps to this file:
10 |
11 | puts "== Installing dependencies =="
12 | system "gem install bundler --conservative"
13 | system "bundle check || bundle install"
14 |
15 | # puts "\n== Copying sample files =="
16 | # unless File.exist?("config/database.yml")
17 | # system "cp config/database.yml.sample config/database.yml"
18 | # end
19 |
20 | puts "\n== Preparing database =="
21 | system "bin/rake db:setup"
22 |
23 | puts "\n== Removing old logs and tempfiles =="
24 | system "rm -f log/*"
25 | system "rm -rf tmp/cache"
26 |
27 | puts "\n== Restarting application server =="
28 | system "touch tmp/restart.txt"
29 | end
30 |
--------------------------------------------------------------------------------
/js/stores/UserStore.js:
--------------------------------------------------------------------------------
1 | import Events from 'events'
2 | import Constants from '../Constants';
3 | import AppDispatcher from '../AppDispatcher';
4 | import assign from 'object-assign';
5 |
6 | var _user ={};
7 | var UserStore = assign({}, Events.EventEmitter.prototype, {
8 |
9 | emitEvent() {
10 | this.emit();
11 | },
12 |
13 | addEventListener(callback) {
14 | this.on("user:change", callback);
15 | },
16 |
17 | removeEventListener(callback) {
18 | this.removeListener("user:change", callback);
19 | },
20 |
21 | setState(user) {
22 | _user = user;
23 | },
24 |
25 | getState() {
26 | return _user;
27 | },
28 | });
29 |
30 | AppDispatcher.register((action) => {
31 | switch(action.type) {
32 | case Constants.USER_INITIALIZED:
33 | var data = action.data;
34 | UserStore.setState(data);
35 | UserStore.emitEvent();
36 |
37 | break;
38 |
39 | default: // no op
40 | }
41 | });
42 | export default UserStore;
43 |
--------------------------------------------------------------------------------
/js/stores/MenuStore.js:
--------------------------------------------------------------------------------
1 | import Events from 'events'
2 | import Constants from '../Constants';
3 | import AppDispatcher from '../AppDispatcher';
4 | import assign from 'object-assign';
5 |
6 | var _menus = [];
7 | var MenuStore = assign({}, Events.EventEmitter.prototype, {
8 |
9 | emitEvent() {
10 | this.emit();
11 | },
12 |
13 | addEventListener(callback) {
14 | this.on("menu:change", callback);
15 | },
16 |
17 | removeEventListener(callback) {
18 | this.removeListener("menu:change", callback);
19 | },
20 |
21 | setState(menus) {
22 | _menus = menus;
23 | },
24 |
25 | getState() {
26 | return _menus;
27 | },
28 | });
29 |
30 | AppDispatcher.register((action) => {
31 | switch(action.type) {
32 | case Constants.MENU_INITIALIZED:
33 | var data = action.data;
34 | MenuStore.setState(data);
35 | MenuStore.emitEvent();
36 |
37 | break;
38 |
39 | default: // no op
40 | }
41 | });
42 | export default MenuStore;
43 |
--------------------------------------------------------------------------------
/test/dummy/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 83ba22679f6fce98d247dd6cf874b5fd25929533a455911124a8998503baf660d43c48d7ae66bcb62076de4aafc198df98ed6998290ea3d9cc57ad650d7b366d
15 |
16 | test:
17 | secret_key_base: 120d8b6712def827e6293da2abc8bb3c87c1a3687e4f5f294f1b25c940bdf72a72014c829e0884d0c6b358e5a143b9b0b06df6dbb19f0fac5935b7c36a5af729
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015 Tomohito Ozaki
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/app/controllers/administa/error_handlers.rb:
--------------------------------------------------------------------------------
1 |
2 | module Administa
3 | module ErrorHandlers
4 |
5 | def handle_exception(exception, status = 400)
6 | case exception
7 | when ActiveRecord::RecordInvalid
8 | handle_validate_errors(exception.record.errors)
9 | return
10 | when ActiveRecord::RecordNotFound
11 | render :nothing => true, :status => 404
12 | return
13 | when ActiveRecord::PreparedStatementInvalid
14 | render :nothing => true, :status => 400
15 | return
16 | else
17 | raise exception
18 | return
19 | end
20 | end
21 |
22 | def handle_validate_errors(*errors)
23 | errors = errors.as_json
24 | errors = errors.first if errors.is_a? Array
25 | errors[self.model..name.underscore] = I18n.t(:invalid, :scope => "errors.messages") if errors.blank?
26 |
27 | respond_to do |format|
28 | format.json { render :json => {:errors => errors}.as_json, :status => 422 }
29 | format.any { render :nothing => true, :status => 406 }
30 | end
31 |
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | Bundler.require(*Rails.groups)
6 | require "administa"
7 |
8 | module Dummy
9 | class Application < Rails::Application
10 | # Settings in config/environments/* take precedence over those specified here.
11 | # Application configuration should go into files in config/initializers
12 | # -- all .rb files in that directory are automatically loaded.
13 |
14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
16 | # config.time_zone = 'Central Time (US & Canada)'
17 |
18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
20 | # config.i18n.default_locale = :de
21 |
22 | # Do not swallow errors in after_commit/after_rollback callbacks.
23 | config.active_record.raise_in_transactional_callbacks = true
24 | end
25 | end
26 |
27 |
--------------------------------------------------------------------------------
/js/components/form/HasOne.react.js:
--------------------------------------------------------------------------------
1 | import InputMixin from './InputMixin';
2 | import PropertyMixin from 'components/PropertyMixin';
3 |
4 | export default React.createClass({
5 | displayName: 'form/HasOne',
6 |
7 | mixins: [PropertyMixin, InputMixin],
8 |
9 | getFormValue() {
10 | return this.refs.association.getFormValue();
11 | },
12 |
13 | getResourceValue() {
14 | return this.refs.association.getResourceValue();
15 | },
16 |
17 | isDirty() {
18 | return this.refs.association.isDirty()
19 | },
20 |
21 | hasError() {
22 | return this.props.invalid;
23 | },
24 |
25 | render() {
26 | var column = this.props.column;
27 | var name = column.name;
28 | var label = this.toProperyName(column);
29 | var association = this.props.column.association;
30 | var target = this.props.resource[association.name];
31 |
32 | return(
33 |
34 | { label }
35 |
36 | { this.createAssociation(target, {ref: 'association'}) }
37 | { this.errorsBlock(label) }
38 |
39 | );
40 | },
41 | })
42 |
--------------------------------------------------------------------------------
/js/stores/AppStore.js:
--------------------------------------------------------------------------------
1 | import Events from 'events'
2 | import Constants from '../Constants';
3 | import AppDispatcher from '../AppDispatcher';
4 | import assign from 'object-assign';
5 |
6 | var _app={};
7 | var AppStore = assign({}, Events.EventEmitter.prototype, {
8 |
9 | emitEvent() {
10 | this.emit("app:change");
11 | },
12 |
13 | addEventListener(callback) {
14 | this.on("app:change", callback);
15 | },
16 |
17 | removeEventListener(callback) {
18 | this.removeListener("app:change", callback);
19 | },
20 |
21 | transitionTo(route, params, query) {
22 | _app.transitionTo = {
23 | route: route,
24 | params: params,
25 | query: query,
26 | }
27 | },
28 |
29 | setState(app) {
30 | _app = app;
31 | },
32 |
33 | getState() {
34 | return _app;
35 | },
36 | });
37 |
38 | AppDispatcher.register((action) => {
39 | switch(action.type) {
40 | case Constants.APP_TRANSITION:
41 | AppStore.transitionTo(action.route, action.params, action.query);
42 | AppStore.emitEvent();
43 |
44 | break;
45 |
46 | default: // no op
47 | }
48 | });
49 | export default AppStore;
50 |
--------------------------------------------------------------------------------
/app/controllers/administa/actions/index.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | module Actions
3 | module Index
4 |
5 | def index
6 | @result = index_result
7 |
8 | respond_to do |format|
9 | format.html
10 | format.json { render json: to_json(@result) }
11 | end
12 | rescue => e
13 | handle_exception(e)
14 | end
15 |
16 | protected
17 | def default_result
18 | {
19 | name: model.name.pluralize,
20 | settings: model.settings,
21 | csrf_token: form_authenticity_token,
22 | }
23 |
24 | end
25 |
26 | def index_result
27 | page = params[:page].try(&:to_i)
28 | limit = params[:limit].try(&:to_i)
29 | order = params[:order]
30 | q = params[:q]
31 |
32 | resources = model.
33 | select(action: :index).
34 | filter_by_keywords(q).
35 | order(order).
36 | paginate(page: page, limit:limit)
37 |
38 | result = default_result
39 | result[:resources] = model.as_json(resources.to_a, action: :index)
40 | result[:pagination] = resources.pagination_metadata.merge(:q => q)
41 |
42 | result
43 | end
44 |
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/js/components/Dialog.react.js:
--------------------------------------------------------------------------------
1 | import DialogActions from 'actions/DialogActions';
2 |
3 | export default React.createClass({
4 | displayName: 'Dialog',
5 |
6 | propTypes: {
7 | name: React.PropTypes.string,
8 | opened: React.PropTypes.bool,
9 | component: React.PropTypes.func,
10 | componentProps: React.PropTypes.object,
11 | onclose: React.PropTypes.func,
12 | index: React.PropTypes.number,
13 | },
14 |
15 | close() {
16 | if(this.props.onclose) {
17 | this.props.onclose();
18 | }
19 | DialogActions.close(this.props.name);
20 | },
21 |
22 | render() {
23 | if( this.props.opened && this.props.component) {
24 | var InnerComponent = this.props.component;
25 |
26 | var index = this.props.index;
27 | var zindex = 1000 + ((index + 1) * 10);
28 | var style = {
29 | top: (index * 20) + "px",
30 | left: (index * 20) + "px",
31 | };
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | } else {
43 | return
;
44 | }
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "administa",
3 | "version": "1.0.0",
4 | "description": "= Administa",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "engine-strict": true,
10 | "engines": {
11 | "node": ">=4.6.0"
12 | },
13 | "scripts": {
14 | "watch": "webpack --progress --profile --colors -d -w",
15 | "release": "webpack --progress --profile --colors --optimize-occurence-order --optimize-dedupe --config webpack.config.release.js"
16 | },
17 | "author": "",
18 | "license": "ISC",
19 | "devDependencies": {
20 | "babel-core": "^5.8.34",
21 | "babel-loader": "^5.4.0",
22 | "bower": "^1.4.1",
23 | "css-loader": "^0.23.0",
24 | "expose-loader": "^0.7.1",
25 | "file-loader": "^0.8.1",
26 | "less": "^2.5.0",
27 | "less-loader": "^2.2.0",
28 | "style-loader": "^0.13.0",
29 | "url-loader": "^0.5.5",
30 | "webpack": "^1.7.3"
31 | },
32 | "dependencies": {
33 | "extract-text-webpack-plugin": "^0.9.1",
34 | "jquery-datetimepicker": "^2.4.0",
35 | "moment": "^2.10.6",
36 | "react": "~0.13.1",
37 | "jquery": "~2.1.3",
38 | "lodash": "~3.6.0",
39 | "admin-lte": "2.3.2",
40 | "bootstrap": "~3.3.4",
41 | "react-router": "~0.13.2",
42 | "flux": "~2.0.2",
43 | "object-assign": "~2.0.0",
44 | "jqnotifybar": "https://github.com/yuroyoro/jQuery-Notify-bar",
45 | "left-pad": "~1.1.3"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/js/components/Dialogs.react.js:
--------------------------------------------------------------------------------
1 | import DialogStore from 'stores/DialogStore';
2 | import Dialog from './Dialog.react';
3 |
4 | export default React.createClass({
5 | displayName: 'Dialogs',
6 |
7 | getInitialState: function() {
8 | return DialogStore.getAllState();
9 | },
10 |
11 | componentDidMount() {
12 | DialogStore.addEventListener("*", this._onChange);
13 | },
14 |
15 | componentWillUnmount() {
16 | DialogStore.removeEventListener("*", this._onChange);
17 | },
18 |
19 | _onChange(e) {
20 | var st = DialogStore.getAllState();
21 |
22 | this.setState(st);
23 | // this.setState(ResourceStore.getState(this.props.params.name));
24 | },
25 |
26 | render() {
27 |
28 | var dialogs = Object.keys(this.state).
29 | map((k) => { return this.state[k]; }).
30 | filter((d) => { return d.opened; }).
31 | sort((d1, d2) => { return d1 > d2 ; }).
32 | map((dialog) => {
33 | return ( );
42 | });
43 |
44 | return (
45 |
46 | { dialogs }
47 |
48 | );
49 | }
50 | });
51 |
--------------------------------------------------------------------------------
/js/components/Header.react.js:
--------------------------------------------------------------------------------
1 | import UserStore from 'stores/UserStore';
2 |
3 | export default React.createClass({
4 | displayName: 'Header',
5 |
6 | getInitialState() {
7 | return { user: UserStore.getState() };
8 | },
9 |
10 | componentDidMount() {
11 | UserStore.addEventListener(this._onChange);
12 | },
13 |
14 | componentWillUnmount() {
15 | UserStore.removeEventListener(this._onChange);
16 | },
17 |
18 | _onChange() {
19 | var user = UserStore.getState();
20 | this.setState({ user: user});
21 | },
22 |
23 | render() {
24 | return(
25 |
43 | );
44 | },
45 | })
46 |
--------------------------------------------------------------------------------
/js/components/form/Through.react.js:
--------------------------------------------------------------------------------
1 | import InputMixin from './InputMixin';
2 | import CollectionMixin from './CollectionMixin';
3 | import PropertyMixin from 'components/PropertyMixin';
4 |
5 | export default React.createClass({
6 | displayName: 'form/Through',
7 |
8 | mixins: [PropertyMixin, InputMixin, CollectionMixin],
9 |
10 | getFormValue() {
11 | var keys = Object.keys(this.refs);
12 | var result = {};
13 | var targets = [];
14 | var ids = [];
15 | var name = this.props.column.association.name;
16 | var dirty = false;
17 |
18 | for (var i = 0, len = keys.length; i < len; i++) {
19 | var key = keys[i];
20 | var property = this.refs[key];
21 | var target = property.state.target;
22 | if(target && target.id) {
23 | ids.push(target.id);
24 | }
25 | if(property.isDirty()) {
26 | dirty = true
27 | switch (property.state.reason) {
28 | case 'created':
29 | case 'edited':
30 | var value = property.getFormValue();
31 | targets.push(value[name]);
32 | break;
33 | case 'selected':
34 | break;
35 | case 'cleared':
36 | if( property.state.destroy ) {
37 | targets.push(property.state.destroy);
38 | }
39 | }
40 | }
41 | }
42 |
43 | if( targets.length > 0) {
44 | result[name] = targets;
45 | }
46 | if(dirty) {
47 | result[this.props.column.association.foreign_key] = ids;
48 | }
49 | return result;
50 | },
51 | })
52 |
--------------------------------------------------------------------------------
/js/components/form/BelongsTo.react.js:
--------------------------------------------------------------------------------
1 | import InputMixin from './InputMixin';
2 | import PropertyMixin from 'components/PropertyMixin';
3 |
4 | export default React.createClass({
5 | displayName: 'form/BelongsTo',
6 |
7 | mixins: [PropertyMixin, InputMixin],
8 |
9 | getFormValue() {
10 | var value = this.getResourceValue();
11 | var association = this.props.column.association;
12 | if(!(association.create || association.update)) {
13 | // remove if readonly
14 | delete value[association.name];
15 | }
16 |
17 | return value;
18 | },
19 |
20 | getResourceValue() {
21 | var value = {};
22 | var name = this.props.column.name;
23 | value[name] = null;
24 | var target = this.refs.association.state.target;
25 | if(target){
26 | var association_name = this.props.column.association.name;
27 |
28 | value[association_name + "_id"] = target.id;
29 | value[association_name] = target;
30 | }
31 |
32 | return value;
33 | },
34 |
35 | isDirty() {
36 | return this.refs.association.isDirty()
37 | },
38 |
39 | hasError() {
40 | return this.props.invalid;
41 | },
42 |
43 | render() {
44 | var column = this.props.column;
45 | var name = column.name;
46 | var label = this.toProperyName(column);
47 | var association = this.props.column.association;
48 | var target = this.props.resource[association.name];
49 |
50 | return(
51 |
52 | { label }
53 |
54 | { this.createAssociation(target, {ref: 'association'}) }
55 | { this.errorsBlock(label) }
56 |
57 | );
58 | },
59 | })
60 |
--------------------------------------------------------------------------------
/js/components/list/Item.react.js:
--------------------------------------------------------------------------------
1 | import Router from 'react-router';
2 | import ResourceActions from 'actions/ResourceActions';
3 | import LinkMixin from 'components/LinkMixin';
4 | import PropertyMixin from 'components/PropertyMixin';
5 |
6 | var Link = Router.Link;
7 | var PT = React.PropTypes
8 |
9 | export default React.createClass({
10 | displayName: 'list/Item',
11 |
12 | mixins: [LinkMixin, PropertyMixin, Router.Navigation ],
13 |
14 | propTypes: {
15 | name: PT.string.isRequired,
16 | resource: PT.object.isRequired,
17 | columns: PT.array.isRequired,
18 | search_columns: PT.array.isRequired,
19 | pagination: PT.object.isRequired,
20 | onclick: PT.func,
21 | },
22 |
23 | showLink(resource){
24 |
25 | var attrs = this.linkAttrs(this.props.name, resource.id, this.props.pagination);
26 | attrs.label = 'show';
27 |
28 | return this.linkToShow(attrs);
29 | },
30 |
31 |
32 | render() {
33 | var resource = this.props.resource;
34 | var classes = this.props.selected ? "info" : "";
35 |
36 | var cols = this.props.columns.map((col) => {
37 | var label = this.toLabel( col, resource,
38 | { search_columns: this.props.search_columns, wrap_tag: true, ellipsis: true }
39 | );
40 | var title = this.toLabel( col, resource,
41 | { search_columns: this.props.search_columns, wrap_tag: false, ellipsis: true}
42 | );
43 |
44 | return ({ label } );
45 | });
46 |
47 | return(
48 |
49 | { cols }
50 |
51 | );
52 | },
53 | })
54 |
--------------------------------------------------------------------------------
/lib/administa/config.rb:
--------------------------------------------------------------------------------
1 | require "administa/config/auth"
2 | require "administa/config/menu"
3 | require "administa/config/dynamic_controller"
4 |
5 | module Administa
6 | class Config
7 | include ::Administa::Config::Auth
8 | include ::Administa::Config::Menu
9 | include ::Administa::Config::DynamicController
10 |
11 | attr_reader :models, :controllers, :timezone_obj
12 |
13 | def initialized?
14 | !!@initialized
15 | end
16 |
17 | # Initialization of Administa.config is delayed until
18 | # the first request incomming.
19 | #
20 | # see Administa::Base#_ensure_administa_config_initialized
21 | #
22 | def initialize!
23 | run_menu_def
24 | initialize_models!
25 | @initialized = true
26 | end
27 |
28 | def initialize_models!
29 | @models.to_a.each do |_, m|
30 | m.setup_options!
31 | end
32 | end
33 |
34 | def add_model(model)
35 | @models ||= {}
36 | @models[model.name.to_sym] = model
37 | end
38 |
39 | def namespace(ns = nil)
40 | @namespace = ns if ns
41 | @namespace || :administa
42 | end
43 |
44 | def base_controller(base = nil)
45 | @base_controller = base if base
46 | @base_controller || "ApplicationController"
47 | end
48 |
49 | def actions(*args)
50 | @actions = args if args.present?
51 | @actions || [:create, :edit, :destroy]
52 | end
53 |
54 | def timezone(tz = nil)
55 | @timezone = tz if tz
56 | @timezone ||= "Etc/UTC"
57 | end
58 |
59 | def timezone_offset
60 | timezone_obj = ActiveSupport::TimeZone.new(@timezone)
61 | ActiveSupport::TimeZone.seconds_to_utc_offset(timezone_obj.utc_offset)
62 | end
63 |
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/test/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports and disable caching.
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send.
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger.
20 | config.active_support.deprecation = :log
21 |
22 | # Raise an error on page load if there are pending migrations.
23 | config.active_record.migration_error = :page_load
24 |
25 | # Debug mode disables concatenation and preprocessing of assets.
26 | # This option may cause significant delays in view rendering with a large
27 | # number of complex assets.
28 | config.assets.debug = true
29 |
30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
31 | # yet still be able to expire them through the digest params.
32 | config.assets.digest = true
33 |
34 | # Adds additional error checking when serving assets at runtime.
35 | # Checks for improperly declared sprockets dependencies.
36 | # Raises helpful error messages.
37 | config.assets.raise_runtime_errors = true
38 |
39 | # Raises error for missing translations
40 | # config.action_view.raise_on_missing_translations = true
41 | end
42 |
--------------------------------------------------------------------------------
/test/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/lib/administa/model/json.rb:
--------------------------------------------------------------------------------
1 |
2 | module Administa
3 | class Model
4 | module Json
5 | def as_json(records, options ={})
6 | action = options[:action]
7 | raise ArgumentError, ":action is required" unless action
8 |
9 | inc = (options[:includes] || self.includes(action)).try{|xs| convert_for_json_include(xs) } || {}
10 | methods = options[:method] || self.options[action][:columns].select{|c| c[:accessor] == :method }.map{|c| c[:name]}
11 |
12 | opt = (inc.is_a? Hash) ? inc.merge(methods: methods) : { include: inc, methods: methods }
13 |
14 | # specialize each uploader's serializable_hash method to
15 | # contain 'content_type' in json expression hash
16 | file_columns = self.options[action][:columns].select{|c| c[:type] == :file}
17 | file_columns.each do |col|
18 | Array.wrap(records).each do |r|
19 | uploader = r.send(col[:name])
20 | specialize_serializable_hash(uploader)
21 | end
22 | end
23 |
24 | records.as_json(opt)
25 | end
26 |
27 | def specialize_serializable_hash(uploader)
28 | uploader.instance_eval do
29 | def serializable_hash(options = nil)
30 | json = super(options)
31 | json["content_type"] = self.content_type
32 | json["is_image"] = !!(self.content_type =~ /^image\//)
33 | json
34 | end
35 | end
36 | end
37 |
38 | def convert_for_json_include(includes, wrap_include = true)
39 | res = case includes
40 | when Array
41 | includes.map{|value| convert_for_json_include(value, false) }
42 | when Hash
43 | includes.inject({}){|h,(k,v)| h[k] = convert_for_json_include(v);h }
44 | else
45 | includes
46 | end
47 | wrap_include ? {:include => res} : res
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static file server for tests with Cache-Control for performance.
16 | config.serve_static_files = true
17 | config.static_cache_control = 'public, max-age=3600'
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | config.action_mailer.delivery_method = :test
33 |
34 | # Randomize the order test cases are executed.
35 | config.active_support.test_order = :random
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/js/Loader.js:
--------------------------------------------------------------------------------
1 |
2 | export default {
3 | // http://projects.lukehaas.me/css-loaders/
4 | // http://w3lessons.info/2014/01/26/showing-busy-loading-indicator-during-an-ajax-request-using-jquery/
5 |
6 | setup() {
7 | jQuery(document).ajaxStart(() => {
8 | //show ajax indicator
9 | this.start('Loading...');
10 | }).ajaxStop(() => {
11 | //hide ajax indicator
12 | this.stop();
13 | });
14 | },
15 |
16 | start(text) {
17 | if(jQuery('body').find('#resultLoading').attr('id') != 'resultLoading'){
18 | jQuery('body').
19 | append('');
20 | }
21 |
22 | jQuery('#resultLoading').css({
23 | 'width':'100%',
24 | 'height':'100%',
25 | 'position':'fixed',
26 | 'z-index':'10000000',
27 | 'top':'0',
28 | 'left':'0',
29 | 'right':'0',
30 | 'bottom':'0',
31 | 'margin':'auto'
32 | });
33 |
34 | jQuery('#resultLoading .bg').css({
35 | 'background':'#000000',
36 | 'opacity':'0.7',
37 | 'width':'100%',
38 | 'height':'100%',
39 | 'position':'absolute',
40 | 'top':'0'
41 | });
42 |
43 | jQuery('#resultLoading > div:last').css({
44 | // 'width': '250px',
45 | // 'height':'75px',
46 | // 'text-align': 'center',
47 | 'position': 'fixed',
48 | 'top':'0',
49 | 'left':'0',
50 | 'right':'0',
51 | 'bottom':'0',
52 | 'margin':'auto',
53 | // 'font-size':'16px',
54 | 'z-index':'10',
55 | // 'color':'#ffffff'
56 |
57 | });
58 |
59 | jQuery('#resultLoading .bg').height('100%');
60 | jQuery('#resultLoading').fadeIn(300);
61 | jQuery('body').css('cursor', 'wait');
62 | },
63 |
64 |
65 | stop() {
66 | jQuery('#resultLoading .bg').height('100%');
67 | jQuery('#resultLoading').fadeOut(300);
68 | jQuery('body').css('cursor', 'default');
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/js/components/form/InputMixin.js:
--------------------------------------------------------------------------------
1 | import assign from 'object-assign';
2 | import Association from './Association.react';
3 |
4 | export default {
5 | propTypes: {
6 | column: React.PropTypes.object.isRequired,
7 | resource: React.PropTypes.object.isRequired,
8 | settings: React.PropTypes.object.isRequired,
9 | disabled: React.PropTypes.bool,
10 | invalid: React.PropTypes.bool,
11 | errors: React.PropTypes.string,
12 | },
13 |
14 | errorsBlock(label) {
15 | if(!this.props.errors || this.props.errors.length == 0) return null;
16 |
17 | return this.props.errors.map((e, i) => {
18 | var msg = `${label} ${e}`;
19 | return { msg }
;
20 | });
21 | },
22 |
23 | inputClasses() {
24 | var classes = "form-control input-sm";
25 | classes += " " + this.inputStatusClasses();
26 | return classes;
27 | },
28 |
29 | inputStatusClasses() {
30 | var classes = "";
31 | if(this.isDirty()){
32 | classes += "modified";
33 | }
34 | if(this.hasError()){
35 | classes += "invalid";
36 | }
37 | return classes;
38 | },
39 |
40 | formClasses() {
41 | var classes = "form-group";
42 | if(this.hasError()){
43 | classes += " has-error";
44 | }
45 | return classes;
46 | },
47 |
48 | createAssociation(target, options) {
49 | var column = this.props.column;
50 | var name = column.name;
51 | var association = this.props.column.association;
52 |
53 | var attrs = {
54 | name: name,
55 | column: column,
56 | resource: this.props.resource,
57 | settings: this.props.settings,
58 | buttons: { select: association.select, clear: association.select, create: association.create, edit: association.update },
59 | disabled: this.props.disabled,
60 | target: target,
61 | }
62 |
63 | assign(attrs, options);
64 |
65 | return ;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/controllers/administa/base.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | module Base
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 | layout 'administa/application'
7 |
8 | if Rails.version < "4.0"
9 | before_filter :_authenticate!
10 | before_filter :_ensure_administa_config_initialized
11 | else
12 | before_action :_authenticate!
13 | before_action :_ensure_administa_config_initialized
14 | end
15 | helper_method :user_info, :to_json
16 | end
17 |
18 | private
19 |
20 | def _ensure_administa_config_initialized
21 | unless Administa.config.initialized?
22 | Administa.config.initialize!
23 |
24 | # reload routes for install dynamic controller's routes
25 | Rails.application.routes_reloader.reload!
26 |
27 | redirect_to "#{request.path}?#{request.query_string}"
28 | end
29 | end
30 |
31 | def _authenticate!
32 | user = instance_eval(&Administa.config.authenticate_with)
33 | unless user
34 | path = Administa.config.redirect_path
35 | redirect_to path
36 | end
37 | end
38 |
39 | def _current_user
40 | instance_eval(&Administa.config.current_user_method)
41 | end
42 |
43 | def user_info
44 | user = _current_user
45 | {
46 | name: Administa.config.user_name_proc.call(user),
47 | email: Administa.config.user_email_proc.call(user),
48 | icon: Administa.config.user_icon_image_proc.call(user),
49 | }
50 | end
51 |
52 | def to_json(obj)
53 | # change datetime format : FIXME race condition
54 | use_standard_json_time_format = ActiveSupport::JSON::Encoding.use_standard_json_time_format
55 | ActiveSupport::JSON::Encoding.use_standard_json_time_format = false
56 | begin
57 | obj.to_json
58 | ensure
59 | ActiveSupport::JSON::Encoding.use_standard_json_time_format = !!use_standard_json_time_format
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/administa/config/auth.rb:
--------------------------------------------------------------------------------
1 | require 'digest/md5'
2 |
3 | module Administa
4 | class Config
5 | module Auth
6 |
7 | DEFAULT_CURRENT_USER = Proc.new{
8 | request.env["warden"].try(:user) || respond_to?(:current_user) && current_user
9 | }
10 | DEFAULT_AUTHENTICATION = Proc.new{
11 | request.env['warden'].try(:authenticate!)
12 | }
13 |
14 | DEFAULT_USER_ICON_PROC = Proc.new {|user|
15 | Administa.config.user_email_proc.call(user).try{|email|
16 | hash = Digest::MD5.hexdigest(email)
17 | "https://www.gravatar.com/avatar/#{hash}.png"
18 | }
19 | }
20 | DEFAULT_USER_NAME_PROC = Proc.new{|user|
21 | (user.respond_to?(:name) && user.send(:name)) ||
22 | Administa.config.user_email_proc.call(user) ||
23 | nil
24 | }
25 | DEFAULT_USER_EMAIL_PROC = Proc.new{|user|
26 | (user.respond_to?(:email) && user.send(:email)) ||
27 | (user.respond_to?(:mail_adress) && user.send(:mail_adress)) ||
28 | nil
29 | }
30 |
31 | def current_user_method(&block)
32 | @current_user_proc = block if block
33 | @current_user_proc || DEFAULT_CURRENT_USER
34 | end
35 |
36 | def user_icon_image_proc(&block)
37 | @user_icon_proc = block if block
38 | @user_icon_proc || DEFAULT_USER_ICON_PROC
39 | end
40 |
41 | def user_name_proc(&block)
42 | @user_name_proc = block if block
43 | @user_name_proc || DEFAULT_USER_NAME_PROC
44 | end
45 |
46 | def user_email_proc(&block)
47 | @user_email_proc = block if block
48 | @user_email_proc || DEFAULT_USER_EMAIL_PROC
49 | end
50 |
51 | def authenticate_with(&block)
52 | @authenticate = block if block
53 | @authenticate || DEFAULT_AUTHENTICATION
54 | end
55 |
56 | def redirect_path(path = nil)
57 | @redirect_path = path if path
58 | @redirect_path || "/"
59 | end
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/js/components/list/SearchBox.react.js:
--------------------------------------------------------------------------------
1 | import Router from 'react-router';
2 | import ResourceActions from 'actions/ResourceActions';
3 | import LinkMixin from 'components/LinkMixin';
4 |
5 | var PT = React.PropTypes
6 |
7 | export default React.createClass({
8 | displayName: 'list/SearchBox',
9 |
10 | mixins: [LinkMixin, Router.Navigation],
11 |
12 | propTypes: {
13 | name: PT.string,
14 | pagination: PT.object,
15 | transition: PT.bool,
16 | },
17 |
18 | getInitialState: function() {
19 | return {value: this.props.pagination.q};
20 | },
21 |
22 | handleChange: function(event) {
23 | this.setState({value: event.target.value});
24 | },
25 |
26 | handleKeyPress: function(event) {
27 | if(event.charCode == 13){ // enter key
28 | this.search();
29 | }
30 | },
31 |
32 | search() {
33 | let name = this.props.name;
34 | let id = this.props.id;
35 | let route = 'resource';
36 |
37 | var params = { name: name }
38 | if (id) {
39 | params.id = id;
40 | route = 'show';
41 | }
42 |
43 | var options = {
44 | name: name,
45 | id: id,
46 | page: 1,
47 | limit: this.props.pagination.limit,
48 | order: this.props.pagination.order,
49 | q: this.state.value
50 | }
51 | var query = this.linkToListQuery(options);
52 | ResourceActions.list(name, query).then(() => {
53 | var transition = !(this.props.transition == false);
54 | if( transition ) {
55 | this.promiseTransition(route, params, query);
56 | }
57 | });
58 | },
59 |
60 |
61 | render() {
62 | var value = this.state.value;
63 | return(
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | },
72 | })
73 |
--------------------------------------------------------------------------------
/js/stores/DialogStore.js:
--------------------------------------------------------------------------------
1 | import Events from 'events'
2 | import Constants from '../Constants';
3 | import AppDispatcher from '../AppDispatcher';
4 | import assign from 'object-assign';
5 |
6 | var dialogs = {};
7 | var index = 0;
8 |
9 | class DialogState {
10 | constructor(name, component, index, props) {
11 | this.name = name;
12 | this.component = component;
13 | this.index = index
14 | this.props = props;
15 | this.opened = false;
16 | }
17 |
18 | open() {
19 | this.opened = true;
20 | }
21 |
22 | close() {
23 | this.opened = false;
24 | }
25 | }
26 |
27 | var DialogStore = assign({}, Events.EventEmitter.prototype, {
28 |
29 | eventTag(name) {
30 | return "dialog:" + name;
31 | },
32 |
33 | emitEvent(name) {
34 | this.emit(this.eventTag(name));
35 |
36 | if(name != "*") {
37 | this.emit(this.eventTag("*"));
38 | }
39 | },
40 |
41 | addEventListener(name, callback) {
42 | this.on(this.eventTag(name), callback);
43 | },
44 |
45 | removeEventListener(name, callback) {
46 | this.removeListener(this.eventTag(name), callback);
47 | },
48 |
49 | setState(name, dialog) {
50 | dialogs[name] = dialog;
51 | },
52 |
53 | getState(name) {
54 | return dialogs[name];
55 | },
56 |
57 | getAllState() {
58 | return dialogs;
59 | },
60 | });
61 |
62 | AppDispatcher.register((action) => {
63 | switch(action.type) {
64 | case Constants.DIALOG_OPENED:
65 | var data = action.data;
66 | var name = data.name;
67 |
68 | var dialog = new DialogState(name, data.component, index++, data.props);
69 |
70 | dialog.open();
71 | DialogStore.setState(name, dialog);
72 |
73 | DialogStore.emitEvent(name);
74 | break;
75 |
76 | case Constants.DIALOG_CLOSED:
77 | var data = action.data;
78 | var name = data.name;
79 |
80 | var dialog = DialogStore.getState(name);
81 | if( dialog ) {
82 | dialog.close();
83 | --index;
84 | DialogStore.setState(name, dialog);
85 | DialogStore.emitEvent(name);
86 | }
87 |
88 | break;
89 | default: // no op
90 | }
91 | });
92 | export default DialogStore;
93 |
--------------------------------------------------------------------------------
/js/components/Resource.react.js:
--------------------------------------------------------------------------------
1 | import Router from 'react-router';
2 | import ResourceActions from 'actions/ResourceActions';
3 | import ResourceStore from 'stores/ResourceStore';
4 | import List from './list/List.react';
5 |
6 | var RouteHandler = Router.RouteHandler;
7 |
8 | export default React.createClass({
9 |
10 | displayName: 'Resource',
11 |
12 | getInitialState() {
13 | return ResourceStore.getState(this.props.params.name);
14 | },
15 |
16 | componentDidMount() {
17 | ResourceStore.addEventListener(this.props.params.name, this._onChange);
18 | },
19 |
20 | componentWillUnmount() {
21 | ResourceStore.removeEventListener(this.props.params.name, this._onChange);
22 | },
23 |
24 | _onChange() {
25 | var st = ResourceStore.getState(this.props.params.name);
26 |
27 | this.setState(st);
28 | // this.setState(ResourceStore.getState(this.props.params.name));
29 | },
30 |
31 | contextTypes: {
32 | router: React.PropTypes.func
33 | },
34 |
35 | render() {
36 |
37 | var leftcol = 12;
38 | var rightcol= 12;
39 | var id = this.state.currentId || this.props.params.id;
40 | var name = this.state.name || this.props.params.name;
41 | var resource = this.state.currentResource || {};
42 | var resources = this.state.resources || [];
43 | var settings = this.state.settings || {};
44 | var pagination = this.state.pagination || {};
45 | var errors = this.state.errors || {};
46 | var csrfToken = this.state.csrfToken || "";
47 |
48 | if(resource && Object.keys(resource).length != 0) {
49 | leftcol = 8;
50 | rightcol= 4;
51 | id = Number(id);
52 | }
53 |
54 | var attrs = {
55 | name: name,
56 | id: id,
57 | label: settings.label,
58 | settings: settings,
59 | pagination: pagination,
60 | errors: errors,
61 | csrfToken: csrfToken,
62 | };
63 |
64 | var list_attrs = {
65 | col: leftcol,
66 | resources: resources
67 | }
68 |
69 | return (
70 |
71 |
72 |
73 |
74 | );
75 | }
76 | });
77 |
78 |
79 |
--------------------------------------------------------------------------------
/lib/administa/model/relation.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | class Model
3 | class Relation
4 | include Administa::Model::Finder
5 |
6 | attr_accessor :klass, :relation, :pagination_metadata
7 | def initialize(klass, relation)
8 | self.klass = klass
9 | self.relation = relation
10 | end
11 |
12 | def to_a
13 | relation.to_a
14 | end
15 |
16 | def select(options = {})
17 | rel = relation.includes(options[:includes])
18 |
19 | self.class.new(klass, rel)
20 | end
21 |
22 | def filter_by_keywords(keyword)
23 | return self unless keyword.present?
24 |
25 | words = keyword.split(/\s/).map{ |w| "%#{w.gsub(/%/, '%%')}%" }
26 |
27 | columns = klass.column_names & klass.options[:search_columns].map(&:to_s)
28 |
29 | arel = klass.arel_table
30 | query = columns.
31 | select{|col| klass.column_names.include?(col.to_s) }.
32 | map{|col|
33 | arel[col].matches_all(words)
34 | }.inject{|q1, q2| q1.or(q2) }
35 |
36 | query = query.or(arel[:id].eq(keyword.to_i)) if keyword =~ /^\d+$/
37 |
38 | rel = relation.where(query)
39 | self.class.new(klass, rel)
40 | end
41 |
42 | def order(*args)
43 | o = args.compact.presence || klass.options[:order]
44 |
45 | unless o.to_s.include?(".")
46 | o = "%s.%s" % [ klass.table_name, o]
47 | end
48 |
49 | self.class.new(klass, relation.order(o))
50 | end
51 |
52 | def paginate(page: 1, limit: klass.options[:limit])
53 | page ||= 1
54 | limit ||= klass.options[:limit]
55 |
56 | offset = (page <= 1 ? 0 : limit * (page - 1))
57 |
58 | count = relation.count
59 | rel = relation.limit(limit).offset(offset)
60 |
61 | result = self.class.new(klass, rel)
62 | result.calculate_pagination_metadata(count, page, limit, offset)
63 | result
64 | end
65 |
66 | protected
67 | def calculate_pagination_metadata(count , page, limit, offset)
68 | self.pagination_metadata = {
69 | page: page,
70 | limit: limit,
71 | offset: offset,
72 | count: count,
73 | total_pages: limit.zero? ? 0 : (count.to_f / limit).ceil,
74 | }
75 | end
76 |
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/lib/administa/config/dynamic_controller.rb:
--------------------------------------------------------------------------------
1 |
2 | module Administa
3 | class Config
4 | module DynamicController
5 |
6 | attr_reader :dynamic_controllers
7 |
8 | def generate_controller(name, options = {})
9 | base = Administa.config.base_controller
10 | namespace = Administa.config.namespace.to_s.camelize
11 |
12 | unless ns = namespace.safe_constantize
13 | ns = Module.new
14 | Object.const(namespace, nested_mod)
15 | end
16 |
17 | controller_name = "#{name.pluralize.camelize}Controller"
18 | if controller = "#{namespace}::#{controller_name}".safe_constantize
19 | return controller
20 | end
21 |
22 | base_klass = base.constantize
23 | klass = Class.new(base_klass)
24 | model = name.singularize.camelize.constantize
25 |
26 | ns.const_set(controller_name, klass)
27 | klass.send(:include, ::Administa::Controller)
28 |
29 | model_options = options.merge(model: model)
30 | klass.class_eval do
31 | administa model_options
32 | end
33 |
34 | @dynamic_controllers ||= {}
35 | @dynamic_controllers[name.to_s.singularize.to_sym] = klass
36 |
37 | klass
38 | end
39 |
40 | def add_dynamic_controller_routes
41 | ns = self.namespace.to_sym
42 |
43 | dcs = self.controllers.to_a.map(&:model).map(&:name).map(&:pluralize).map(&:to_sym)
44 |
45 | Rails.application.routes.draw do
46 | namespace ns do
47 | dcs.each do |res|
48 | resources res
49 | end
50 |
51 | root to: 'main#index'
52 | get :main, to: 'main#index'
53 |
54 | get ':model', to: 'generic#index'
55 | get ':model/new', to: 'generic#new'
56 | post ':model/create', to: 'generic#create'
57 | get ':model/:id/edit', to: 'generic#edit'
58 | put ':model/:id', to: 'generic#update'
59 | get ':model/:id', to: 'generic#show'
60 | delete ':model/:id', to: 'generic#destroy'
61 |
62 | end
63 | end
64 | end
65 | end
66 | end
67 | end
68 |
69 | class Rails::Application::RoutesReloader
70 |
71 | def load_paths_with_dynamic_routes
72 | Administa.config.add_dynamic_controller_routes
73 |
74 | load_paths_without_dynamic_routes
75 | end
76 | alias_method_chain :load_paths, :dynamic_routes
77 |
78 | end
79 |
--------------------------------------------------------------------------------
/js/components/list/Pagination.react.js:
--------------------------------------------------------------------------------
1 | import Router from 'react-router';
2 | import ResourceActions from 'actions/ResourceActions';
3 | import LinkMixin from 'components/LinkMixin';
4 |
5 | var Link = Router.Link;
6 | var PT = React.PropTypes
7 |
8 | export default React.createClass({
9 | displayName: 'list/Pagination',
10 |
11 | mixins: [LinkMixin, Router.Navigation],
12 |
13 | propTypes: {
14 | name: PT.string,
15 | id: PT.number,
16 | resource: PT.object,
17 | pagination: PT.object,
18 | transition: PT.bool
19 | },
20 |
21 | paginationLink(label, page) {
22 | let name = this.props.name;
23 | let id = this.props.id;
24 |
25 | var className = '';
26 | if (page == this.props.pagination.page) {
27 | className = 'active';
28 | }
29 |
30 | var linkAttrs = {
31 | name: name,
32 | id: id,
33 | label: label,
34 | page: page,
35 | limit: this.props.pagination.limit,
36 | order: this.props.pagination.order,
37 | q: this.props.pagination.q,
38 | transition: this.props.transition
39 | }
40 |
41 | return(
42 |
43 | { this.linkToList(linkAttrs) }
44 |
45 | );
46 |
47 | },
48 |
49 | render() {
50 | var page = this.props.pagination.page;
51 | var count = this.props.pagination.count;
52 | var total = this.props.pagination.total_pages;
53 | var from = page - 2;
54 | var to = page + 2;
55 |
56 | if (from < 1) from = 1;
57 | if (to > total) to = total;
58 |
59 | if (to - from < 4) {
60 | if(from + 4 <= total) {
61 | to = from + 4;
62 | }
63 | else if(to - 4 > 1) {
64 | from = to - 4;
65 | }
66 | }
67 |
68 | var links = [];
69 | if (from > 1) {
70 | links.push(this.paginationLink('«', 1));
71 | }
72 |
73 | for (var i=from; i <= to; i++) {
74 | links.push(this.paginationLink(i, i));
75 | }
76 |
77 | if (to < total) {
78 | links.push(this.paginationLink('»', total));
79 | }
80 |
81 | links.push( { count + " records"} );
82 | links.push( { total + " pages"} );
83 |
84 | return(
85 |
88 | );
89 | }
90 | });
91 |
--------------------------------------------------------------------------------
/js/components/form/CollectionMixin.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | getState(resource) {
4 | var targets = resource[this.props.column.association.name];
5 | return {
6 | targets: targets,
7 | dirty: false,
8 | };
9 | },
10 | getInitialState() {
11 | return this.getState(this.props.resource);
12 | },
13 |
14 | componentWillReceiveProps(nextProps) {
15 | if (nextProps.resource && nextProps.resource.id != this.props.resource.id) {
16 | this.setState(this.getState(nextProps.resource));
17 | }
18 | },
19 |
20 | getResourceValue() {
21 | var keys = Object.keys(this.refs);
22 | var result = {};
23 | var targets = [];
24 | var name = this.props.column.association.name;
25 |
26 | for (var i = 0, len = keys.length; i < len; i++) {
27 | var key = keys[i];
28 | var property = this.refs[key];
29 | var value = property.getResourceValue();
30 | targets.push(value[name]);
31 | }
32 |
33 | result[name] = targets;
34 | return result;
35 | },
36 |
37 | isDirty() {
38 | if( this.props.dirty ) return true;
39 |
40 | var keys = Object.keys(this.refs);
41 | for (var i = 0, len = keys.length; i < len; i++) {
42 | var key = keys[i];
43 | var property = this.refs[key];
44 | if( property.isDirty() ) {
45 | return true;
46 | }
47 | }
48 | return false;
49 | },
50 |
51 | add() {
52 | var targets = this.state.targets;
53 | targets.push(null);
54 | this.setState({
55 | targets: targets,
56 | dirty: false,
57 | });
58 | },
59 |
60 | hasError() {
61 | return this.props.invalid;
62 | },
63 |
64 | render() {
65 | var column = this.props.column;
66 | var name = column.name;
67 | var label = this.toProperyName(column);
68 | var targets = this.state.targets || [];
69 | if(targets.length == 0) {
70 | targets.push(null); // append blank record if empty
71 | }
72 | var associations = targets.map((tgt, i) => {
73 | var attrs = {
74 | key: `${name}[${i}]`,
75 | ref: i,
76 | };
77 | return this.createAssociation(tgt, attrs);
78 | });
79 |
80 | return(
81 |
82 |
{ label }
83 |
84 | { associations }
85 |
86 | Add
87 |
88 |
89 | { this.errorsBlock(label) }
90 |
91 | );
92 | },
93 | };
94 |
--------------------------------------------------------------------------------
/lib/generators/administa/administa_generator.rb:
--------------------------------------------------------------------------------
1 | class AdministaGenerator < Rails::Generators::NamedBase
2 | source_root File.expand_path('../templates', __FILE__)
3 |
4 | argument :name, type: :string, required: false, default: ""
5 | class_option :init, type: "boolean", default: false
6 | class_option :namespace, type: "string", default: "", aliases: '-n'
7 | class_option :base_class, type: "string", default: "", aliases: '-b'
8 |
9 | def initialize(args, *options)
10 | super
11 |
12 | if !self.options[:init] && self.name.blank?
13 | raise ArgumentError, "No value provided for required arguments 'name'"
14 | end
15 | end
16 |
17 | def initialize_options
18 | @options = options.dup
19 | if @options[:namespace].blank?
20 | @options[:namespace] = Administa.config.namespace.to_s
21 | end
22 | if @options[:base_class].blank?
23 | @options[:base_class] = Administa.config.base_controller.to_s
24 | end
25 | end
26 |
27 | # connfig/initializers/administa.rb
28 | def create_initializers
29 | return if File.exist? "config/initializers/administa.rb"
30 |
31 | # generate
32 | initializer "administa.rb", <<-RUBY_CODE.strip_heredoc
33 | Administa.config do |config|
34 |
35 | # Authentication settings
36 | #
37 | # config.current_user_method { current_user }
38 | # config.user_name_proc {|user| user.try(:name) }
39 | # config.user_email_proc {|user| user.try(:email) }
40 | # config.authenticate_with { warden.authenticate! :scope => :user }
41 |
42 | # Menu settings
43 | #
44 | config.menu do
45 | # label "Authors"
46 | # group Administa::AuthorsController do
47 | # menu Administa::BooksController
48 | # end
49 | #
50 | # menu Administa::OrdersController
51 | end
52 |
53 | end
54 | RUBY_CODE
55 | end
56 |
57 | def append_menu
58 | return if @options[:init]
59 |
60 |
61 | # append menu Administa::FooController
62 | insert_into_file "config/initializers/administa.rb",
63 | " menu #{@options[:namespace].camelize}::#{class_name.pluralize}Controller\n",
64 | after: "config.menu do\n"
65 |
66 | end
67 |
68 | # app/controllers/administa/foo_controller.rb
69 | def generate_controller
70 | return if @options[:init]
71 |
72 | dest = File.join("app", "controllers", @options[:namespace])
73 |
74 | # create dir
75 | empty_directory dest unless File.exist? dest
76 |
77 | # generate controller
78 | template "controller.rb", File.join(dest, "#{file_name.pluralize}_controller.rb")
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | module.exports = {
4 |
5 | entry: {
6 | app:[ './js/app'], // application js file
7 | vendors: ['react', 'jquery', 'flux', 'lodash'] // libraries js file
8 | },
9 |
10 | output: {
11 | filename: '[name].bundle.js',
12 | // We want to save the bundle into assets pipeline
13 | path: './app/assets/javascripts/administa/build',
14 | publicPath: 'administa/assets/build'
15 | },
16 |
17 | resolve: {
18 | modulesDirectories: ['node_modules', 'css', 'js'],
19 | alias: {
20 | adminlte: 'admin-lte/dist/js/app.js',
21 | 'adminlte.css': 'admin-lte/dist/css/AdminLTE.css',
22 | 'adminlte-skins-blue.css': 'admin-lte/dist/css/skins/skin-blue.min.css',
23 | 'adminlte-skins-black.css': 'admin-lte/dist/css/skins/skin-black.min.css',
24 | 'bootstrap.css': 'bootstrap/dist/css/bootstrap.css',
25 | 'jquery-datetimepicker.js': 'jquery-datetimepicker/build/jquery.datetimepicker.full.js',
26 | 'jquery.datetimepicker.css': 'jquery-datetimepicker/jquery.datetimepicker.css',
27 | 'jquery.notifyBar.css' : 'jqnotifybar/css/jquery.notifyBar.css'
28 | }
29 | },
30 |
31 | plugins: [
32 | // take the vendors chunk and create a vendors.js from the "vendors" in entry section.
33 | new webpack.optimize.CommonsChunkPlugin('vendors', 'vendors.js'),
34 |
35 | // export to global
36 | new webpack.ProvidePlugin({
37 | $: 'jquery',
38 | jQuery: "jquery",
39 | _: 'lodash',
40 | React: 'react',
41 | Flux: 'flux',
42 | moment: 'moment'
43 | }),
44 | ],
45 |
46 | // Turns on source maps
47 | devtool: '#inline-source-map',
48 |
49 | module: {
50 | loaders: [
51 | // exports Administa object to global
52 | { test: require.resolve('./js/app'), loader: 'expose?Administa' },
53 | { test: require.resolve('./node_modules/jquery/dist/jquery'), loader: "expose?jQuery"},
54 |
55 | // babel-loader : the transpiler es6 to es5
56 | { test: /\.js$/, loader: 'babel-loader', exclude: [/node_modules/], },
57 |
58 | // css-loader
59 | { test: /\.css$/, loaders: ['style-loader', 'css-loader?sourceMap!'] },
60 |
61 | { test: /\.png$/, loader: "url-loader?limit=100000&mimetype=image/png" },
62 | { test: /\.jpg$/, loader: "file-loader" },
63 | { test: /\.gif$/, loader: "file-loader" },
64 | { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=application/font-woff" },
65 | { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=application/font-woff" },
66 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=application/octet-stream" },
67 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file" },
68 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=image/svg+xml" }
69 | ],
70 | noParse: [/\.min\.js/]
71 | }
72 | };
73 |
--------------------------------------------------------------------------------
/js/components/detail/Detail.react.js:
--------------------------------------------------------------------------------
1 | import Router from 'react-router';
2 | import LinkMixin from 'components/LinkMixin';
3 | import Property from './Property.react';
4 |
5 | export default React.createClass({
6 | displayName: 'detali/Detail',
7 |
8 | mixins: [LinkMixin, Router.Navigation],
9 |
10 | propTypes: {
11 | name: React.PropTypes.string,
12 | id: React.PropTypes.number,
13 | col: React.PropTypes.number,
14 | resource: React.PropTypes.object.isRequired,
15 | pagination: React.PropTypes.object,
16 | settings: React.PropTypes.object
17 | },
18 |
19 | shouldComponentUpdate(nextProps, nextState) {
20 | return !!nextProps.resource;
21 | },
22 |
23 | editLink(resource){
24 | if( this.props.settings.actions.indexOf("edit") < 0 ) {
25 | return null;
26 | }
27 |
28 | let name = this.props.name;
29 | let id = resource.id;
30 |
31 | var linkAttrs = {
32 | name: name,
33 | id: id,
34 | label: 'edit',
35 | page: this.props.pagination.page,
36 | limit: this.props.pagination.limit,
37 | order: this.props.pagination.order,
38 | q: this.props.pagination.q
39 | }
40 |
41 | return this.linkToEdit(linkAttrs);
42 | },
43 |
44 | deleteLink(resource){
45 | if( this.props.settings.actions.indexOf("destroy") < 0 ) {
46 | return null;
47 | }
48 | let name = this.props.name;
49 | let id = resource.id;
50 |
51 | var linkAttrs = {
52 | name: name,
53 | id: id,
54 | label: 'delete',
55 | page: this.props.pagination.page,
56 | limit: this.props.pagination.limit,
57 | order: this.props.pagination.order,
58 | q: this.props.pagination.q,
59 | csrfToken: this.props.csrfToken,
60 | confirm: `Are you sure to delete ${this.props.label}(id:${this.props.id})`,
61 | }
62 |
63 | return this.linkToDestroy(linkAttrs);
64 | },
65 |
66 | properties() {
67 | return this.props.settings.show.columns.map((col) => {
68 | return ;
69 | });
70 | },
71 |
72 | render() {
73 | var resource = this.props.resource;
74 | var classes = "resource-detail" ;
75 | classes += " col-md-" + this.props.col;
76 |
77 | if (!resource.id) {
78 | return
79 | }
80 |
81 | var properties = this.properties();
82 |
83 | return(
84 |
85 |
86 |
87 |
Detail : {this.props.label}(id:{this.props.id})
88 |
89 |
90 |
91 | { this.deleteLink(resource) }
92 | { this.editLink(resource) }
93 |
94 |
95 |
96 | { properties }
97 |
98 |
99 |
100 | );
101 | },
102 | })
103 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | administa (0.0.1)
5 | rails (~> 4.2.1.rc3)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | actionmailer (4.2.1)
11 | actionpack (= 4.2.1)
12 | actionview (= 4.2.1)
13 | activejob (= 4.2.1)
14 | mail (~> 2.5, >= 2.5.4)
15 | rails-dom-testing (~> 1.0, >= 1.0.5)
16 | actionpack (4.2.1)
17 | actionview (= 4.2.1)
18 | activesupport (= 4.2.1)
19 | rack (~> 1.6)
20 | rack-test (~> 0.6.2)
21 | rails-dom-testing (~> 1.0, >= 1.0.5)
22 | rails-html-sanitizer (~> 1.0, >= 1.0.1)
23 | actionview (4.2.1)
24 | activesupport (= 4.2.1)
25 | builder (~> 3.1)
26 | erubis (~> 2.7.0)
27 | rails-dom-testing (~> 1.0, >= 1.0.5)
28 | rails-html-sanitizer (~> 1.0, >= 1.0.1)
29 | activejob (4.2.1)
30 | activesupport (= 4.2.1)
31 | globalid (>= 0.3.0)
32 | activemodel (4.2.1)
33 | activesupport (= 4.2.1)
34 | builder (~> 3.1)
35 | activerecord (4.2.1)
36 | activemodel (= 4.2.1)
37 | activesupport (= 4.2.1)
38 | arel (~> 6.0)
39 | activesupport (4.2.1)
40 | i18n (~> 0.7)
41 | json (~> 1.7, >= 1.7.7)
42 | minitest (~> 5.1)
43 | thread_safe (~> 0.3, >= 0.3.4)
44 | tzinfo (~> 1.1)
45 | arel (6.0.0)
46 | builder (3.2.2)
47 | erubis (2.7.0)
48 | globalid (0.3.3)
49 | activesupport (>= 4.1.0)
50 | hike (1.2.3)
51 | i18n (0.7.0)
52 | json (1.8.2)
53 | loofah (2.0.1)
54 | nokogiri (>= 1.5.9)
55 | mail (2.6.3)
56 | mime-types (>= 1.16, < 3)
57 | mime-types (2.4.3)
58 | mini_portile (0.6.2)
59 | minitest (5.5.1)
60 | multi_json (1.11.0)
61 | nokogiri (1.6.6.2)
62 | mini_portile (~> 0.6.0)
63 | rack (1.6.0)
64 | rack-test (0.6.3)
65 | rack (>= 1.0)
66 | rails (4.2.1)
67 | actionmailer (= 4.2.1)
68 | actionpack (= 4.2.1)
69 | actionview (= 4.2.1)
70 | activejob (= 4.2.1)
71 | activemodel (= 4.2.1)
72 | activerecord (= 4.2.1)
73 | activesupport (= 4.2.1)
74 | bundler (>= 1.3.0, < 2.0)
75 | railties (= 4.2.1)
76 | sprockets-rails
77 | rails-deprecated_sanitizer (1.0.3)
78 | activesupport (>= 4.2.0.alpha)
79 | rails-dom-testing (1.0.6)
80 | activesupport (>= 4.2.0.beta, < 5.0)
81 | nokogiri (~> 1.6.0)
82 | rails-deprecated_sanitizer (>= 1.0.1)
83 | rails-html-sanitizer (1.0.2)
84 | loofah (~> 2.0)
85 | railties (4.2.1)
86 | actionpack (= 4.2.1)
87 | activesupport (= 4.2.1)
88 | rake (>= 0.8.7)
89 | thor (>= 0.18.1, < 2.0)
90 | rake (10.4.2)
91 | sprockets (2.12.3)
92 | hike (~> 1.2)
93 | multi_json (~> 1.0)
94 | rack (~> 1.0)
95 | tilt (~> 1.1, != 1.3.0)
96 | sprockets-rails (2.2.4)
97 | actionpack (>= 3.0)
98 | activesupport (>= 3.0)
99 | sprockets (>= 2.8, < 4.0)
100 | sqlite3 (1.3.10)
101 | thor (0.19.1)
102 | thread_safe (0.3.5)
103 | tilt (1.4.1)
104 | tzinfo (1.2.2)
105 | thread_safe (~> 0.1)
106 |
107 | PLATFORMS
108 | ruby
109 |
110 | DEPENDENCIES
111 | administa!
112 | sqlite3
113 |
--------------------------------------------------------------------------------
/js/components/Menu.react.js:
--------------------------------------------------------------------------------
1 | import MenuStore from 'stores/MenuStore';
2 |
3 | export default React.createClass({
4 | displayName: 'Menu',
5 |
6 | getInitialState() {
7 | return { menus: MenuStore.getState() };
8 | },
9 |
10 | componentDidMount() {
11 | MenuStore.addEventListener(this._onChange);
12 | },
13 |
14 | componentWillUnmount() {
15 | MenuStore.removeEventListener(this._onChange);
16 | },
17 |
18 | _onChange() {
19 | var menus = MenuStore.getState();
20 | this.setState({ menus: menus });
21 | },
22 |
23 | label(m, key) {
24 | return { m.label } ;
25 | },
26 |
27 | group(m, key) {
28 | var l = m.label;
29 | var title = null;
30 |
31 | switch(l.type){
32 | case "label":
33 | title = (
34 |
35 | { l.label }
36 | <
37 | /a>
38 | );
39 | break;
40 | case "menu":
41 | var f = (e) => {
42 | e.preventDefault();
43 | e.stopPropagation();
44 | window.location.href = l.path;
45 | return false;
46 | };
47 | var iconstyle = "fa fa-circle-o";
48 | if(l.selected) iconstyle += " text-aqua";
49 |
50 | title = (
51 |
52 |
53 | { l.label }
54 |
55 |
56 | );
57 | break;
58 | }
59 |
60 | var children = m.menus.map((c, i) => {
61 | return this.generate(c, `${key}_${i}`);
62 | });
63 |
64 | var listyle = "";
65 | var ulstyle = "treeview-menu";
66 | if(m.opened){
67 | listyle = "active";
68 | ulstyle += " menu-open";
69 | }
70 | if(!m.visible) {
71 | listyle += " hide";
72 | }
73 |
74 | return (
75 |
76 | { title }
77 |
80 |
81 | );
82 | },
83 |
84 | menu(m, key) {
85 | var className="";
86 | var iconstyle = "fa fa-circle-o";
87 |
88 | if(m.selected) {
89 | className = "active";
90 | iconstyle += " text-aqua";
91 | }
92 | if(!m.visible) {
93 | className += " hide";
94 | }
95 |
96 | return (
97 |
98 |
99 |
100 | { m.label }
101 |
102 |
103 | );
104 | },
105 |
106 | generate(m, key) {
107 | switch(m.type) {
108 | case "label":
109 | return this.label(m, key);
110 | case "group":
111 | return this.group(m, key);
112 | case "menu":
113 | return this.menu(m, key);
114 | default:
115 | //noop
116 | }
117 | },
118 |
119 | menus() {
120 | return this.state.menus.map((m, i) => {
121 | return this.generate(m, i);
122 | });
123 | },
124 |
125 | render() {
126 | return (
127 |
128 |
129 |
130 | { this.menus() }
131 |
132 |
133 |
134 | );
135 | },
136 | })
137 |
--------------------------------------------------------------------------------
/lib/administa/model/attributes.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | class Model
3 | module Attributes
4 |
5 | def assign(record, attrs)
6 | attrs = transform_attributes(attrs, klass)
7 | record.assign_attributes(attrs.except(:id, :updated_at, :created_at))
8 | end
9 |
10 | def transform_attributes(attr, klass = nil)
11 | role = self.options[:attr_accessible_role] || :default
12 | authorizer = (klass.respond_to? :active_authorizer) ? klass.try(:active_authorizer).try(:[], role) : nil
13 | white_list = (authorizer ? authorizer.to_a : klass.column_names).map(&:to_sym)
14 | white_list.unshift(:id)
15 |
16 | res = attr.slice(*white_list)
17 |
18 | nested_attribute_keys = klass.nested_attributes_options.keys
19 |
20 | # reject association that is not specified by accepts_nested_attributes_for
21 | unallowed_associations = (association_names(klass) - nested_attribute_keys)
22 | res.except!(*unallowed_associations)
23 |
24 | remains = attr.except(*white_list)
25 |
26 | # accept if parameter name is "_attributes"
27 | nested_attribute_names = klass.nested_attributes_options.keys
28 | attributes_keys = nested_attribute_names.map{|n| "#{n}_attributes"}
29 | remains.slice(*attributes_keys).each do |name, nested_attr|
30 | res[name] = nested_attr
31 | end
32 | remains.except!(*attributes_keys)
33 |
34 | # if parameter name is "",
35 | # cordinate paramters by nested association recursively
36 | remains.slice(*nested_attribute_keys).each do |name, nested_attr|
37 | next unless nested_attr.present?
38 | nested_klass = klass_of(name) || klass_of(attr[name + '_type'])
39 | if nested_klass
40 | res["#{name}_attributes"] = case nested_attr
41 | when Array then nested_attr.map{|na| transform_attributes(na, nested_klass) }
42 | when Hash then transform_attributes(nested_attr, nested_klass)
43 | else nested_attr
44 | end
45 | end
46 | end
47 | remains.except!(*nested_attribute_keys)
48 |
49 | # if repsond_to? returuns true, set the paramster to record
50 | # accept "_destroy" for destory nested associations
51 | acceptable_names = ["id", "_destroy"]
52 | remains.except!(*association_names(klass))
53 |
54 | # reject common specific attributes(i.e create_at, updated_at)
55 | ignore_attrs = [:created_at, :updated_at].map(&:to_s)
56 |
57 | remains.except!(ignore_attrs)
58 | remains.reject!{|k,v| authorizer.deny?(k) && acceptable_names.include?(k) == false } if authorizer
59 |
60 | methods = klass.instance_methods.map(&:to_s)
61 | remains.each do |k,v|
62 | res[k] = v if methods.include?("#{k}=") || k == "_destroy"
63 | end
64 |
65 | res
66 | end
67 |
68 | def klass_of(name, klass = self.klass)
69 | name.to_s.singularize.camelize.constantize
70 | rescue NameError
71 | association = klass.reflect_on_all_associations.find{|a| a.plural_name == name.to_s && a.options.key?(:class_name) }
72 |
73 | association && association.options[:class_name].to_s.constantize
74 | end
75 |
76 | def association_names(klass = self.klass)
77 | klass.reflect_on_all_associations.map{|a| a.collection? ? a.plural_name.to_sym : a.name }
78 | end
79 | end
80 | end
81 | end
82 |
83 |
84 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like
20 | # NGINX, varnish or squid.
21 | # config.action_dispatch.rack_cache = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
26 |
27 | # Compress JavaScripts and CSS.
28 | config.assets.js_compressor = :uglifier
29 | # config.assets.css_compressor = :sass
30 |
31 | # Do not fallback to assets pipeline if a precompiled asset is missed.
32 | config.assets.compile = false
33 |
34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
35 | # yet still be able to expire them through the digest params.
36 | config.assets.digest = true
37 |
38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
39 |
40 | # Specifies the header that your server uses for sending files.
41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
43 |
44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
45 | # config.force_ssl = true
46 |
47 | # Use the lowest log level to ensure availability of diagnostic information
48 | # when problems arise.
49 | config.log_level = :debug
50 |
51 | # Prepend all log lines with the following tags.
52 | # config.log_tags = [ :subdomain, :uuid ]
53 |
54 | # Use a different logger for distributed setups.
55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
56 |
57 | # Use a different cache store in production.
58 | # config.cache_store = :mem_cache_store
59 |
60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
61 | # config.action_controller.asset_host = 'http://assets.example.com'
62 |
63 | # Ignore bad email addresses and do not raise email delivery errors.
64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
65 | # config.action_mailer.raise_delivery_errors = false
66 |
67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
68 | # the I18n.default_locale when a translation cannot be found).
69 | config.i18n.fallbacks = true
70 |
71 | # Send deprecation notices to registered listeners.
72 | config.active_support.deprecation = :notify
73 |
74 | # Use default logging formatter so that PID and timestamp are not suppressed.
75 | config.log_formatter = ::Logger::Formatter.new
76 |
77 | # Do not dump schema after migrations.
78 | config.active_record.dump_schema_after_migration = false
79 | end
80 |
--------------------------------------------------------------------------------
/js/components/PropertyMixin.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | toProperyName(col) {
4 | var name = col.label;
5 | if(col.association) {
6 | name = col.association.label;
7 | }
8 | return name;
9 | },
10 |
11 | toProperyKey(col) {
12 | var name = col.name;
13 | if(col.association) {
14 | name = col.association.name;
15 | }
16 | return name;
17 | },
18 |
19 | toLabel(col, resource, options = {}) {
20 | var name = this.toProperyKey(col);
21 | var val = resource[name];
22 | var search_columns = options.search_columns;
23 | var wrap_tag = options.wrap_tag || false ;
24 | var ellipsis = options.ellipsis || false ;
25 |
26 | switch( col.type ) {
27 | case "file" :
28 | var imgtag = null;
29 | if( val.url ) {
30 | if ( wrap_tag && val.is_image) {
31 | val = { val.url } ;
32 | } else {
33 | val = val.url
34 | }
35 | }
36 | break;
37 | case "boolean" :
38 | if(!val) {
39 | if( wrap_tag ) {
40 | val = off ;
41 | } else {
42 | val = 'on';
43 | }
44 | } else {
45 | if( wrap_tag ) {
46 | val = on ;
47 | } else {
48 | val = 'on';
49 | }
50 | }
51 | break;
52 | case "datetime":
53 | // convert value with browser timezone by moment.js
54 | var format = 'YYYY/MM/DD HH:mm:ss Z';
55 | val = moment(val, format).format(format);
56 | }
57 |
58 | if(col.association) {
59 | name = col.association.name;
60 | var nested = val;
61 |
62 | if( !nested ) {
63 | return "";
64 | }
65 | if(col.association.type == 'has_many' || col.association.type == 'through') {
66 | val = [];
67 | var to = nested.length;
68 | if (ellipsis && to > 3) to = 3;
69 | for(var i = 0; i < to; i++) {
70 | if( !nested[i] ) continue;
71 | var s = this.extractLabel(col.association.label, nested[i], search_columns);
72 | if( !wrap_tag ) {
73 | val.push(s);
74 | } else {
75 | s = this.wrapPermlink(s, nested[i], col.association);
76 | val.push({s}
);
77 | }
78 | }
79 |
80 | if( ellipsis && nested.length > to ) {
81 | if( !wrap_tag ) {
82 | val.push("...")
83 | } else {
84 | val.push(...
);
85 | }
86 | }
87 |
88 | if( !wrap_tag ) val = val.join(", ")
89 |
90 | return val;
91 | } else {
92 | val = this.extractLabel(name, nested, search_columns);
93 | }
94 |
95 | if( !wrap_tag ) return val;
96 |
97 | // permlink
98 | return this.wrapPermlink(val, nested, col.association);
99 |
100 | }
101 | return val;
102 | },
103 |
104 | wrapPermlink(val, obj, association) {
105 | // permlink
106 | if( !(obj && obj.id && association.controller_path)) {
107 | return val;
108 | }
109 |
110 | var href = `/${ association.controller_path }/${ obj.id }`;
111 | return { val }
112 | },
113 |
114 | extractLabel(name, obj, search_columns) {
115 | var label = name;
116 | var id = obj.id || 'new'
117 | for(var i = 0; i < search_columns.length; i++) {
118 | var v = obj[search_columns[i]];
119 | if(v) {
120 | label = v;
121 | break;
122 | }
123 | }
124 | return label + "(" + id + ")"
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/js/components/list/List.react.js:
--------------------------------------------------------------------------------
1 | import Router from 'react-router';
2 |
3 | import ResourceActions from 'actions/ResourceActions';
4 | import ResourceItem from './Item.react';
5 | import Pagination from './Pagination.react';
6 | import SearchBox from './SearchBox.react';
7 | import LinkMixin from 'components/LinkMixin';
8 | import PropertyMixin from 'components/PropertyMixin';
9 |
10 | var PT = React.PropTypes
11 |
12 | export default React.createClass({
13 | displayName: 'list/List',
14 |
15 | mixins: [LinkMixin, PropertyMixin, Router.Navigation ],
16 |
17 | propTypes: {
18 | name: PT.string,
19 | id: PT.number,
20 | col: PT.number,
21 | resources: PT.arrayOf(PT.object),
22 | settings: PT.object.isRequired,
23 | pagination: PT.object,
24 | },
25 |
26 | clickItem(resource) {
27 | let name = this.props.name;
28 | let id = resource.id;
29 | let pagination = this.props.pagination;
30 |
31 | let attrs = this.linkAttrs(name, id, pagination);
32 | let query = this.linkToListQuery(attrs);
33 |
34 | ResourceActions.fetch(name, id, query).then(() => {
35 | this.transitionToShow(name, id, pagination);
36 | });
37 | },
38 |
39 |
40 | newLink(){
41 | if( this.props.settings.actions.indexOf("create") < 0 ) {
42 | return null;
43 | }
44 | let name = this.props.name;
45 |
46 | var linkAttrs = {
47 | name: name,
48 | label: 'create',
49 | page: this.props.pagination.page,
50 | limit: this.props.pagination.limit,
51 | order: this.props.pagination.order,
52 | q: this.props.pagination.q
53 | }
54 |
55 | return this.linkToNew(linkAttrs);
56 | },
57 |
58 | render() {
59 | let Item = ResourceItem;
60 |
61 | var classes = "resource-list" ;
62 | classes += " col-md-" + this.props.col;
63 |
64 | var index_settings = this.props.settings.index;
65 | var headers = index_settings.columns.map((col) => {
66 | var name = this.toProperyName(col);
67 | return ({ name } );
68 | });
69 |
70 | var items = this.props.resources.map((resource) => {
71 | var selected = resource.id == this.props.id;
72 | var attrs = {
73 | name: this.props.name,
74 | resource: resource,
75 | key: resource.id,
76 | selected: selected,
77 | columns: index_settings.columns,
78 | search_columns: this.props.settings.search_columns,
79 | pagination: this.props.pagination,
80 | onclick: () => { this.clickItem(resource) },
81 | };
82 |
83 | return ( );
84 | });
85 |
86 | return(
87 |
88 |
89 |
90 |
91 |
{this.props.settings.label}
92 |
93 |
94 |
95 |
96 | { this.newLink() }
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | { headers }
108 |
109 |
110 |
111 |
112 | { items }
113 |
114 |
115 |
116 |
117 |
118 |
121 |
122 |
123 | );
124 | },
125 | })
126 |
--------------------------------------------------------------------------------
/lib/administa/config/menu.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | class Config
3 | module Menu
4 |
5 | def menus(controller)
6 |
7 | f = ->(m) {
8 | case m[:type]
9 | when :menu
10 | lf = m.delete(:label_f)
11 | m[:label] = lf.call
12 | sf = m.delete(:selected_f)
13 | m[:selected] = sf.call(controller)
14 | when :group
15 | m[:label] = f.call(m[:label].dup)
16 | m[:menus] = m[:menus].map{|sm| f.call(sm.dup) }
17 | m[:opened] = m[:label][:selected] || m[:menus].any?{|sm| sm[:opend] || sm[:selected] }
18 | when :label
19 | lf = m.delete(:label_f)
20 | m[:label] = lf.call
21 | end
22 |
23 | m
24 | }
25 |
26 | @menu_tree.menus.map do |m|
27 | f.call(m.dup)
28 | end
29 | end
30 |
31 | def menu(&block)
32 | @menu_def = block
33 | end
34 |
35 | def run_menu_def
36 | @menu_tree = Tree.new(&@menu_def)
37 | @menu_tree.run
38 |
39 | @controllers = @menu_tree.controllers
40 | end
41 |
42 | class Tree
43 | attr_accessor :menus, :controllers
44 |
45 | def initialize(&block)
46 | @menus = []
47 | @controllers = []
48 | @block = block
49 | end
50 |
51 | def run(current_controller = nil)
52 | return unless @block
53 | @current_controller = current_controller
54 | self.instance_eval(&@block)
55 | self
56 | end
57 |
58 | def t(s, options = {})
59 | if s.is_a? Symbol
60 | s = I18n.t(s, scope: options[:scope], default: s)
61 | end
62 | return s
63 | end
64 |
65 | def label_def(s, options = {})
66 | {
67 | type: :label,
68 | label_f: -> { t(s) },
69 | visible: (options[:visible].nil? ? true : options[:visible]),
70 | }
71 | end
72 |
73 | def label(s, options = {})
74 | @menus.push(label_def(s, options))
75 | end
76 |
77 | def label_group(title, options = {}, &block)
78 | t = label_def(title, options)
79 | add_group(t, options, &block)
80 | end
81 |
82 | def group(title, options = {}, &block)
83 | c = case title
84 | when String, Symbol then Administa.config.generate_controller(title.to_s, options)
85 | else title
86 | end
87 |
88 | t = controller(c, options)
89 | add_group(t, options, &block)
90 | end
91 |
92 | def add_group(title, options = {}, &block)
93 | m = Tree.new(&block)
94 | m.run
95 |
96 | @controllers += m.controllers.to_a
97 | children = m.menus
98 | opened= children.any?{|h| h[:selected] || h[:opened]}
99 |
100 | @menus.push({
101 | type: :group,
102 | label: title,
103 | menus: children,
104 | opened: opened,
105 | visible: (options[:visible].nil? ? true : options[:visible]),
106 | })
107 | end
108 |
109 | def menu(controller, options = {})
110 | c = case controller
111 | when String, Symbol then Administa.config.generate_controller(controller.to_s, options)
112 | else controller
113 | end
114 |
115 | @menus.push(controller(c, options))
116 | end
117 |
118 | private
119 | def controller(c, options= {})
120 | selected_f = ->(current){ current.try(:controller_path) == c.controller_path }
121 | label_f = -> { c.model.label }
122 | @controllers.push(c)
123 | {
124 | type: :menu,
125 | path: "/#{c.controller_path}",
126 | label_f: label_f,
127 | selected_f: selected_f,
128 | visible: (options[:visible].nil? ? true : options[:visible]),
129 | }
130 | end
131 | end
132 | end
133 | end
134 | end
135 |
--------------------------------------------------------------------------------
/js/components/list/Selection.react.js:
--------------------------------------------------------------------------------
1 | import ResourceStore from 'stores/ResourceStore';
2 | import ResourceItem from './Item.react';
3 | import Pagination from './Pagination.react';
4 | import SearchBox from './SearchBox.react';
5 | import PropertyMixin from 'components/PropertyMixin';
6 |
7 | var PT = React.PropTypes
8 |
9 | export default React.createClass({
10 | displayName: 'list/Selection',
11 |
12 | mixins: [PropertyMixin],
13 |
14 | propTypes: {
15 | name: PT.string,
16 | title: PT.string,
17 | onselect: PT.func,
18 | },
19 |
20 | getInitialState() {
21 | return ResourceStore.getState(this.props.name);
22 | },
23 |
24 | componentDidMount() {
25 | ResourceStore.addEventListener(this.props.name, this._onChange);
26 | },
27 |
28 | componentWillUnmount() {
29 | ResourceStore.removeEventListener(this.props.name, this._onChange);
30 | },
31 |
32 | _onChange() {
33 | var st = ResourceStore.getState(this.props.name);
34 |
35 | this.setState(st);
36 | },
37 |
38 | render() {
39 | let Item = ResourceItem;
40 |
41 | var classes = "dialog-body resource-list" ;
42 |
43 | var settings = this.state.settings;
44 | var index_settings = this.state.settings.index;
45 |
46 | var columns = [];
47 | var push_column = (col) => {
48 | var found = false;
49 | for(var i = 0; i < columns.length; i++) {
50 | found = columns[i].name == col.name ||
51 | (columns[i].association && col.association &&
52 | columns[i].association.name == col.association.name);
53 | if(found) break;
54 | }
55 | if(!found){
56 | columns.push(col);
57 | }
58 | }
59 |
60 | var searchcols = settings.search_columns;
61 | var cols = index_settings.columns;
62 |
63 | for(var i = 0; i < cols.length; i++) {
64 | var col = cols[i];
65 | for(var j = 0; j < searchcols.length; j++) {
66 | if( col.name == 'id' || col.name == searchcols[j]) {
67 | push_column(col);
68 | }
69 | if( col.association && col.association.name == searchcols[j] ) {
70 | push_column(col);
71 | }
72 | }
73 | }
74 | if( columns.length == 1 ) {
75 | for(var i = 0; i < cols.length; i++) {
76 | var col = cols[i];
77 | if( col.name != 'id' && col.name != 'created_at' && col.name != 'updated_at') {
78 | push_column(col);
79 | }
80 | }
81 | }
82 |
83 | var col_span = columns.length * 2;
84 | classes += " col-md-" + col_span ;
85 |
86 | var headers = columns.map((col) => {
87 | var name = this.toProperyName(col);
88 | return ({ name } );
89 | });
90 |
91 | var items = this.state.resources.map((resource) => {
92 | var attrs = {
93 | name: this.props.name,
94 | resource: resource,
95 | key: resource.id,
96 | columns: columns,
97 | search_columns: settings.search_columns,
98 | pagination: this.state.pagination,
99 | onclick: ()=> { this.props.onselect(resource) },
100 | };
101 |
102 | return ( );
103 | });
104 |
105 | var title = this.props.title || this.props.name;
106 |
107 | return(
108 |
109 |
110 |
111 |
112 |
113 |
{ title }
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | { headers }
127 |
128 |
129 |
130 |
131 | { items }
132 |
133 |
134 |
135 |
136 |
137 |
140 |
141 |
142 | );
143 | },
144 | })
145 |
--------------------------------------------------------------------------------
/js/app.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import Router from 'react-router';
4 | import Bootstrap from 'bootstrap';
5 | import AdminLTE from 'adminlte';
6 |
7 | // react-router
8 | var Route = Router.Route;
9 | var NotFoundRoute = Router.NotFoundRoute;
10 | var DefaultRoute = Router.DefaultRoute;
11 | var Link = Router.Link;
12 | var Redirect = Router.Redirect;
13 | var RouteHandler = Router.RouteHandler;
14 |
15 | // require stylesheets
16 | require("bootstrap.css");
17 | require("adminlte.css");
18 | // require("adminlte-skins-blue.css");
19 | require("adminlte-skins-black.css");
20 | require("app.css");
21 |
22 | //setup datetimepicker with moment.js
23 | require("jquery.datetimepicker.css");
24 | require("jquery-datetimepicker.js");
25 |
26 | // setup jquery notify bar
27 | require("jqnotifybar");
28 | require("jquery.notifyBar.css")
29 |
30 | Date.parseDate = function( input, format ){
31 | return moment(input,format).toDate();
32 | };
33 | Date.prototype.dateFormat = function( format ){
34 | return moment(this).format(format);
35 | };
36 |
37 | import AppStore from 'stores/AppStore';
38 | import ResourceActions from 'actions/ResourceActions';
39 | import MenuActions from 'actions/MenuActions';
40 | import UserActions from 'actions/UserActions';
41 |
42 | import Header from 'components/Header.react';
43 | import Menu from 'components/Menu.react';
44 | import Main from 'components/Main.react';
45 | import Footer from 'components/Footer.react';
46 | import Loader from 'Loader';
47 |
48 | import Resource from 'components/Resource.react';
49 | import ResourceDetail from 'components/detail/Detail.react';
50 | import ResourceForm from 'components/form/Form.react';
51 |
52 | import Dialogs from 'components/Dialogs.react';
53 | import Utils from 'Utils';
54 |
55 | // expose React to global (workarround)
56 | global.React = React;
57 |
58 |
59 | // Display js error
60 | window.addEventListener('error', function(e){
61 |
62 | var stack = e.stack || (e.error && e.error.stack);
63 | Utils.reportError(e.message, e.error, stack);
64 | });
65 |
66 | var App = React.createClass({
67 | displayName: 'App',
68 |
69 | mixins: [Router.Navigation],
70 |
71 | getInitialState() {
72 | return AppStore.getState();
73 | },
74 |
75 | componentDidMount() {
76 | AppStore.addEventListener(this._onChange);
77 | },
78 |
79 | componentWillUnmount() {
80 | AppStore.removeEventListener(this._onChange);
81 | },
82 |
83 | componentWillUpdate(nextProps, nextState) {
84 |
85 | if (nextState.transitionTo ) {
86 | var route = nextState.transitionTo.route;
87 | var params = nextState.transitionTo.params;
88 | var query = nextState.transitionTo.query;
89 |
90 | this.transitionTo(route, params, query);
91 | }
92 | },
93 |
94 | _onChange() {
95 | var st = AppStore.getState();
96 | this.setState(st);
97 | },
98 |
99 | render() {
100 | return (
101 |
102 |
103 |
104 |
105 |
106 |
111 |
112 |
113 |
114 |
115 |
116 | );
117 | }
118 | });
119 |
120 | var routes = (
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | );
133 |
134 | Loader.setup();
135 |
136 | export default {
137 | dataElement() {
138 | return document.getElementById('initial-data');
139 | },
140 |
141 | extractFromDataElement(name) {
142 | return JSON.parse(this.dataElement().getAttribute(name));
143 | },
144 |
145 | initialData() {
146 | return this.extractFromDataElement('data-json');
147 | },
148 |
149 | user() {
150 | return this.extractFromDataElement('user-json');
151 | },
152 |
153 | menus() {
154 | return this.extractFromDataElement('menu-json');
155 | },
156 |
157 | initializeAdminLTE() {
158 | //Activate the layout maker
159 | $.AdminLTE.layout.fix()
160 | //Enable sidebar tree view controls
161 | $.AdminLTE.tree('.sidebar');
162 | },
163 |
164 | render(data) {
165 |
166 | ResourceActions.initialize(data);
167 | UserActions.initialize(this.user());
168 | MenuActions.initialize(this.menus());
169 |
170 | Router.run(routes, Router.HistoryLocation, (Handler, state) => {
171 | let params = state.params;
172 | React.render(, document.body);
173 | });
174 |
175 | this.initializeAdminLTE();
176 | }
177 |
178 | }
179 |
--------------------------------------------------------------------------------
/js/stores/ResourceStore.js:
--------------------------------------------------------------------------------
1 | import Events from 'events'
2 | import Constants from '../Constants';
3 | import AppDispatcher from '../AppDispatcher';
4 | import assign from 'object-assign';
5 | import Utils from 'Utils';
6 |
7 | var states = {};
8 |
9 | class ResourceState {
10 | constructor(name, data = {}) {
11 | this.name = name || '';
12 | this.currentId = data.id;
13 | this.currentResource = data.resource;
14 | this.settings = data.settings || {};
15 | this.resources = data.resources || [];
16 | this.pagination = data.pagination || {};
17 | this.errors = data.errors || {};
18 | this.csrfToken = data.csrf_token || "";
19 | this.flash = data.flash || "";
20 | }
21 |
22 | update(other) {
23 | var setUnlessEmpty = function(attr, self) {
24 | if( other.hasOwnProperty(attr) == false ) {
25 | return;
26 | }
27 | var v = other[attr];
28 | if(v && Utils.isPrimitive(v)) {
29 | self[attr] = v;
30 | return;
31 | }
32 | if(v && Object.keys(v).length > 0) {
33 | self[attr] = v;
34 | }
35 | };
36 |
37 | if (other.currentId) this.currentId = other.currentId;
38 |
39 | setUnlessEmpty("currentResource", this);
40 | setUnlessEmpty("resources", this);
41 | setUnlessEmpty("settings", this);
42 | setUnlessEmpty("pagination", this);
43 | setUnlessEmpty("errors", this);
44 | setUnlessEmpty("csrfToken", this);
45 | setUnlessEmpty("flash", this);
46 | return this;
47 | }
48 |
49 | clone() {
50 | return new ResourceState( this.name, {
51 | id: this.currentId,
52 | resource: this.currentResource,
53 | settings: this.settings,
54 | resources: this.resources,
55 | pagination: this.pagination,
56 | csrf_token: this.csrfToken,
57 | flash: this.flash,
58 | });
59 | }
60 | }
61 |
62 | var ResourceStore = assign({}, Events.EventEmitter.prototype, {
63 |
64 | eventTag(name) {
65 | return "resource:" + name;
66 | },
67 |
68 | emitEvent(name) {
69 | this.emit(this.eventTag(name));
70 |
71 | if(name != "*") {
72 | this.emit(this.eventTag("*"));
73 | }
74 | },
75 |
76 | addEventListener(name, callback) {
77 | this.on(this.eventTag(name), callback);
78 | },
79 |
80 | removeEventListener(name, callback) {
81 | this.removeListener(this.eventTag(name), callback);
82 | },
83 |
84 | updateState(name, data) {
85 | var old = this.getState(name);
86 | var state = new ResourceState(name, data);
87 |
88 | if(old) {
89 | state = old.clone().update(state);
90 | }
91 |
92 | this.setState(name, state);
93 | return state;
94 | },
95 |
96 | setState(name, state) {
97 | states[name] = state;
98 | },
99 |
100 | getState(name) {
101 | return states[name];
102 | },
103 |
104 | getAllState() {
105 | return states;
106 | },
107 |
108 | });
109 |
110 | AppDispatcher.register((action) => {
111 | switch(action.type) {
112 | case Constants.INITIALIZE:
113 | var data = action.data;
114 | var name = data.name;
115 |
116 | var state = new ResourceState(name, data);
117 |
118 | ResourceStore.setState(name, state);
119 | ResourceStore.emitEvent(name);
120 |
121 | break;
122 | case Constants.RESOURCE_FETCH:
123 |
124 | var data = action.data;
125 | var name = action.name;
126 |
127 | ResourceStore.updateState(name, data);
128 | ResourceStore.emitEvent(name);
129 |
130 | break;
131 | case Constants.RESOURCE_LIST:
132 | var data = action.data;
133 | var name = action.name;
134 |
135 | ResourceStore.updateState(name, data);
136 | ResourceStore.emitEvent(name);
137 |
138 | break;
139 | case Constants.RESOURCE_BUILD:
140 |
141 | var data = action.data;
142 | var name = action.name;
143 |
144 | var state = ResourceStore.updateState(name, data);
145 | state.currentId = null; // clear currentid
146 | ResourceStore.emitEvent(name);
147 |
148 | break;
149 | case Constants.RESOURCE_CREATED:
150 |
151 | var data = action.data;
152 | var name = action.name;
153 |
154 | ResourceStore.updateState(name, data);
155 | ResourceStore.emitEvent(name);
156 |
157 | break;
158 | case Constants.RESOURCE_UPDATED:
159 |
160 | var data = action.data;
161 | var name = action.name;
162 |
163 | ResourceStore.updateState(name, data);
164 | ResourceStore.emitEvent(name);
165 |
166 | break;
167 | case Constants.RESOURCE_INVALID:
168 |
169 | var data = action.data;
170 | var name = action.name;
171 |
172 | ResourceStore.updateState(name, data);
173 | ResourceStore.emitEvent(name);
174 |
175 | case Constants.RESOURCE_DELETED:
176 |
177 | var data = action.data;
178 | var name = action.name;
179 |
180 | var state = ResourceStore.updateState(name, data);
181 | state.currentId = null; // clear currentid
182 | state.currentResource = null;
183 | ResourceStore.emitEvent(name);
184 |
185 | default: // no op
186 | }
187 | });
188 | export default ResourceStore;
189 |
--------------------------------------------------------------------------------
/js/components/LinkMixin.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../AppDispatcher';
2 | import Constants from '../Constants';
3 | import Router from 'react-router';
4 | import ResourceActions from 'actions/ResourceActions';
5 |
6 | var Link = Router.Link;
7 |
8 | export default {
9 |
10 | promiseTransition(route, params, query) {
11 | AppDispatcher.dispatch({
12 | type: Constants.APP_TRANSITION,
13 | route: route,
14 | params: params,
15 | query: query,
16 | });
17 | },
18 |
19 | transitionToShow(name, id, pagination) {
20 | var options = {
21 | name: name,
22 | id: id,
23 | page: pagination.page,
24 | limit: pagination.limit,
25 | order: pagination.order,
26 | q: pagination.q
27 | };
28 |
29 | var params = { name: name, id: id };
30 | var query = this.linkToListQuery(options);
31 |
32 | this.promiseTransition('show', params, query);
33 |
34 | },
35 |
36 | linkToNew(options = {}) {
37 | let name = options.name;
38 | let label = options.label;
39 |
40 | var params = { name: name };
41 | var query = this.linkToListQuery(options);
42 |
43 | var f = (event) => {
44 | event.preventDefault();
45 |
46 | ResourceActions.build(name, query).then(() => {
47 | this.promiseTransition('new', params, query);
48 | });
49 | };
50 |
51 | var classes = options.classes || "btn btn-primary btn-xs btn-flat"
52 | var href = this.makeHref('new', params, query);
53 |
54 | var link = { label } ;
55 | return link;
56 | },
57 |
58 | linkToEdit(options = {}) {
59 | let name = options.name;
60 | let id = options.id;
61 | let label = options.label;
62 |
63 | var params = { name: name, id: id };
64 | var query = this.linkToListQuery(options);
65 |
66 | var f = (event) => {
67 | event.preventDefault();
68 | ResourceActions.fetch(name, id, query).then(() => {
69 | this.promiseTransition('edit', params, query);
70 | });
71 | };
72 |
73 | var classes = options.classes || "btn btn-primary btn-xs btn-flat"
74 | var href = this.makeHref('edit', params, query);
75 |
76 | var link = { label } ;
77 | return link;
78 | },
79 |
80 | linkToDestroy(options = {}) {
81 | let name = options.name;
82 | let id = options.id;
83 | let label = options.label;
84 |
85 | var params = { name: name, id: id };
86 | var query = this.linkToListQuery(options);
87 |
88 | var f = (event) => {
89 | event.preventDefault();
90 |
91 | if( options.confirm ){
92 | if( !window.confirm(options.confirm)) {
93 | return ;
94 | }
95 | }
96 | ResourceActions.destroy(name, id, query, options).then((data) => {
97 |
98 | var flash = data.flash;
99 |
100 | if( flash ) {
101 | $.notifyBar({ cssClass: "success", html: flash, });
102 | }
103 | });
104 | return true;
105 | };
106 |
107 | var classes = options.classes || "btn btn-danger btn-xs btn-flat"
108 | var href = this.makeHref('show', params, query);
109 |
110 | var link = { label } ;
111 | return link;
112 | },
113 |
114 | linkToShow(options = {}) {
115 | let name = options.name;
116 | let id = options.id;
117 | let label = options.label;
118 |
119 | var params = { name: name, id: id };
120 | var query = this.linkToListQuery(options);
121 |
122 | var f = (event) => {
123 | event.preventDefault();
124 |
125 | ResourceActions.fetch(name, id, query).then(() => {
126 | this.promiseTransition('show', params, query);
127 | });
128 | };
129 |
130 | var classes = options.classes || "btn btn-primary btn-xs btn-flat"
131 | var href = this.makeHref('show', params, query);
132 |
133 | var link = { label } ;
134 | return link;
135 | },
136 |
137 | linkToList(options = {}) {
138 | let route = 'resource';
139 | let name = options.name;
140 | let id = options.id;
141 | let label = options.label;
142 |
143 | var params = { name: name }
144 | if (id) {
145 | params.id = id;
146 | route = 'show';
147 | }
148 | var query = this.linkToListQuery(options);
149 | var transition = !(options.transition == false);
150 |
151 | var f = (event) => {
152 | event.preventDefault();
153 |
154 | ResourceActions.list(options.name, query).then(() => {
155 | if(transition) {
156 | this.promiseTransition(route, params, query);
157 | }
158 | });
159 | };
160 |
161 | var classes = options.classes ;
162 | var href = this.makeHref(route, params, query);
163 |
164 | var link = { label } ;
165 | return link;
166 | },
167 |
168 | linkAttrs(name, id, pagination){
169 | return {
170 | name: name,
171 | id: id,
172 | page: pagination.page,
173 | limit: pagination.limit,
174 | order: pagination.order,
175 | q: pagination.q
176 | }
177 | },
178 |
179 | linkToListQuery(options = {}) {
180 | return {
181 | page: options.page,
182 | limit: options.limit,
183 | order: options.order,
184 | q: options.q
185 | }
186 | },
187 |
188 | }
189 |
--------------------------------------------------------------------------------
/css/app.css:
--------------------------------------------------------------------------------
1 | .main {
2 | height: 100vh;
3 | max-height: 100vh;
4 | overflow-y: auto;
5 | }
6 |
7 | .content-wrapper{
8 | }
9 |
10 | /* header */
11 | .skin-black .main-header>.logo {
12 | background-color: #0C0C0C;
13 | color: #C7C7C7;
14 | border-right: 1px solid #303030;
15 | }
16 |
17 | .skin-black .main-header>.navbar {
18 | background-color: #0C0C0C;
19 | border: 1px solid transparent;
20 | }
21 |
22 |
23 | .skin-black .main-header>.navbar>.sidebar-toggle {
24 | color: #C7C7C7;
25 | border-right: none;
26 | }
27 |
28 |
29 | .skin-black .main-header>.navbar .navbar-custom-menu .navbar-nav>li>a, .skin-black .main-header>.navbar .navbar-right>li>a {
30 | border-left: none;
31 | border-right-width: 0;
32 | }
33 |
34 | .skin-black .main-header>.navbar .nav>li>a {
35 | color: #C7C7C7;
36 | }
37 |
38 | /* sidebar */
39 | .skin-black .wrapper, .skin-black .main-sidebar, .skin-black .left-side {
40 | background-color: #303030;
41 | }
42 |
43 | .skin-black .sidebar-menu>li.header {
44 | color: #CDCDCD;
45 | background: #454545;
46 | }
47 |
48 | .skin-black .main-header>.logo:hover {
49 | background: #454545;
50 | color: #C7C7C7;
51 | }
52 |
53 | .skin-black .main-header>.navbar .nav>li>a:hover, .skin-black .main-header>.navbar .nav>li>a:active, .skin-black .main-header>.navbar .nav>li>a:focus, .skin-black .main-header>.navbar .nav .open>a, .skin-black .main-header>.navbar .nav .open>a:hover, .skin-black .main-header>.navbar .nav .open>a:focus {
54 | background: #454545;
55 | color: #C7C7C7;
56 | }
57 |
58 | .skin-black .sidebar-menu>li:hover>a, .skin-black .sidebar-menu>li.active>a {
59 | color: #fff;
60 | background: #1E2022;
61 | border-left-color: #fff;
62 | }
63 |
64 | .skin-black .sidebar-menu>li>.treeview-menu {
65 | margin: 0 0px;
66 | padding-left: 10px;
67 | background: #373B3D;
68 | }
69 |
70 | .skin-black .treeview-menu>li>a {
71 | color: #A5ADB0;
72 | }
73 |
74 |
75 |
76 |
77 | .skin-black .sidebar-form input[type="text"], .skin-black .sidebar-form .btn {
78 | background-color: #373B3D;
79 | }
80 |
81 | .skin-black .sidebar-form {
82 | border: 1px solid #474C4F;
83 | }
84 |
85 |
86 | /* form */
87 | .form-group .modified {
88 | border-color: royalblue;
89 | color: royalblue;
90 | }
91 | .form-group .invalid{
92 | border-color: tomato;
93 | color: tomato;
94 | }
95 |
96 | .form-group blockquote {
97 | font-size: 14px;
98 | padding: 0px 20px;
99 | }
100 |
101 | .form-group .input-group {
102 | margin-bottom: 4px;
103 | }
104 |
105 | .form-control[disabled]{
106 | background-color: #FAFAFA;
107 | }
108 |
109 | .input-group-btn .btn-default {
110 | background-color: #EEEEEE;
111 | border-color: #d2d6de;
112 | }
113 |
114 | .box-header .box-tools .btn {
115 | margin-left: 4px;
116 |
117 | }
118 | /* resoource-list */
119 | .resource-list .box-body {
120 | scroll: auto;
121 | }
122 | .resource-list .box-tools a,
123 | .resource-list .box-tools button{
124 | margin-right : .75em;
125 | height: 30px;
126 | padding: 5px 10px;
127 | font-size: 12px;
128 | line-height: 1.5;
129 | }
130 |
131 | .resource-list table tr{
132 | cursor: pointer ;
133 | }
134 |
135 | .resource-list table th, td{
136 | white-space: nowrap;
137 | overflow: hidden;
138 | text-overflow: ellipsis;
139 | max-width: 150px;
140 | }
141 |
142 | .resource-list table td img{
143 | max-width: 150px;
144 | }
145 |
146 | input.search-box-text {
147 | min-width: 80px;
148 | }
149 |
150 | /* pagination */
151 | .pagination li {
152 | cursor: pointer;
153 | }
154 |
155 | /* dialog */
156 |
157 | .dialog-base {
158 | width : 100%;
159 | height : 100%;
160 | position : fixed;
161 | z-index : 1000;
162 | top : 0;
163 | left : 0;
164 | right : 0;
165 | bottom : 0;
166 | margin : auto;
167 | }
168 |
169 | .dialog-base .dialog-bg {
170 | background : #000000;
171 | opacity : 0.7;
172 | width : 100%;
173 | height : 100%;
174 | position : absolute;
175 | top : 0;
176 | }
177 |
178 | .dialog-base .dialog-body {
179 | position : fixed;
180 | top : 50px;
181 | left : 0;
182 | right : 0;
183 | bottom : 0;
184 | margin : auto;
185 | z-index : 10;
186 | min-width: 400px;
187 | max-width: 600px;
188 | overflow-y: auto;
189 | }
190 |
191 | /* loader */
192 | .loader,
193 | .loader:before,
194 | .loader:after {
195 | background: #FFF;
196 | -webkit-animation: load1 1s infinite ease-in-out;
197 | animation: load1 1s infinite ease-in-out;
198 | width: 1em;
199 | height: 4em;
200 | }
201 | .loader:before,
202 | .loader:after {
203 | position: absolute;
204 | top: 0;
205 | content: '';
206 | }
207 | .loader:before {
208 | left: -1.5em;
209 | -webkit-animation-delay: -0.32s;
210 | animation-delay: -0.32s;
211 | }
212 | .loader {
213 | text-indent: -9999em;
214 | margin: 8em auto;
215 | position: relative;
216 | font-size: 11px;
217 | -webkit-transform: translateZ(0);
218 | -ms-transform: translateZ(0);
219 | transform: translateZ(0);
220 | -webkit-animation-delay: -0.16s;
221 | animation-delay: -0.16s;
222 | }
223 | .loader:after {
224 | left: 1.5em;
225 | }
226 | @-webkit-keyframes load1 {
227 | 0%,
228 | 80%,
229 | 100% {
230 | box-shadow: 0 0 #FFF;
231 | height: 4em;
232 | }
233 | 40% {
234 | box-shadow: 0 -2em #ffffff;
235 | height: 5em;
236 | }
237 | }
238 | @keyframes load1 {
239 | 0%,
240 | 80%,
241 | 100% {
242 | box-shadow: 0 0 #FFF;
243 | height: 4em;
244 | }
245 | 40% {
246 | box-shadow: 0 -2em #ffffff;
247 | height: 5em;
248 | }
249 | }
250 |
251 |
252 |
253 | div.wild-error-notification {
254 | position: absolute;
255 | background-color: rgba(51, 51, 51, 0.72);
256 | width: 100%;
257 | z-index: 9999;
258 | color: #EEE;
259 | padding: 20px;
260 | }
261 | div.wild-error-notification h3 {
262 | color: orange;
263 | }
264 |
265 |
266 |
--------------------------------------------------------------------------------
/js/components/form/Input.react.js:
--------------------------------------------------------------------------------
1 | import InputMixin from './InputMixin';
2 | import PropertyMixin from 'components/PropertyMixin';
3 |
4 |
5 | export default React.createClass({
6 | displayName: 'form/Input',
7 |
8 | mixins: [PropertyMixin, InputMixin],
9 |
10 | getState(resource) {
11 | return {
12 | value: resource[this.props.column.name],
13 | dirty: false,
14 | };
15 | },
16 |
17 | getInitialState() {
18 | var value = this.props.resource[this.props.column.name];
19 | var dirty = false;
20 |
21 | if( !this.props.resource.id ) { // cordinate itinial values for creation
22 | if ( this.props.column.default && !value) {
23 | value = this.props.column.default;
24 | dirty = true;
25 | }
26 |
27 | if ( this.props.column.type == 'enum' && value ) {
28 | // this filed is considered as 'edited' if enum type has default value on creation
29 | dirty = true;
30 | }
31 | }
32 |
33 | return {
34 | value: value,
35 | dirty: dirty,
36 | };
37 | },
38 |
39 | componentDidMount() {
40 | if ( this.props.column.type == "datetime") {
41 | var options = {
42 | lang: this.props.settings.locale,
43 | format: 'Y/m/d H:i',
44 | defaultDate:new Date()
45 | };
46 | var str_min_sec = "00:00";
47 | var str_timezone = "";
48 | var the_value = this.props.resource[this.props.column.name];
49 | if(the_value) {
50 | var arr = /\d+\/\d+\/\d+ \d+:(\d+):(\d+)( [+-]\d+)?/.exec(the_value);
51 | if(arr) {
52 | str_min_sec = "" + arr[1] + ":" + arr[2];
53 | str_timezone = arr[3] || "";
54 | }
55 | }
56 | options['onClose'] = (current_time, input, event) => {
57 | var determined_value = jQuery(input).val();
58 | var matched = /^\d+\/\d+\/\d+ \d+:/.exec(determined_value);
59 | if(matched) {
60 | determined_value = matched[0] + str_min_sec + str_timezone;
61 | }
62 | this.handleChange(event, determined_value);
63 | }
64 |
65 | // in moment.js, default timezone offset is detected by Date.prototype.getTimezoneOffset
66 | jQuery("input[name='" + this.props.column.name + "'].datetimepicker").datetimepicker(options);
67 | }
68 | },
69 |
70 | componentWillReceiveProps(nextProps) {
71 | if (nextProps.resource && nextProps.resource.id != this.props.resource.id) {
72 | this.setState(this.getState(nextProps.resource));
73 | }
74 | },
75 |
76 | handleChange(event, value) {
77 | var initial = this.props.resource[this.props.column.name];
78 | // set null value that represents "blank"
79 | var value = event.target.value || value || null;
80 | if ( this.props.column.type == 'file') {
81 | value = jQuery(event.target).prop('files')[0];
82 | }
83 | if ( this.props.column.type == 'boolean') {
84 | value = event.target.checked;
85 | }
86 | this.setState({
87 | value: value,
88 | dirty: (value != initial),
89 | });
90 | },
91 |
92 | getFormValue() {
93 | return this.getResourceValue();
94 | },
95 |
96 | getResourceValue() {
97 | var value = {};
98 | value[this.props.column.name] = this.state.value;
99 | return value;
100 | },
101 |
102 | isDirty() {
103 | return this.state.dirty;
104 | },
105 |
106 | hasError() {
107 | return this.props.invalid;
108 | },
109 |
110 | inputField() {
111 | var name = this.props.column.name;
112 | var value = this.state.value;
113 |
114 | // TODO: refactor
115 | switch( this.props.column.type ) {
116 | case "file" :
117 | var imgtag = null;
118 | if( value.url ) {
119 | imgtag = ;
120 | }
121 |
122 | return (
123 |
124 | { imgtag }
125 |
126 |
127 | );
128 | case "boolean" :
129 | var text = 'on';
130 | if(!this.state.value) text = 'off';
131 |
132 | return { text }
133 | case "enum":
134 | var options = this.props.column.enums.map((e) => {
135 | var val = e;
136 | var label = e;
137 | if(Array.isArray(e)){
138 | label = e[0];
139 | val = e[1];
140 | }
141 | return { label } ;
142 | });
143 |
144 | if ( this.props.column.nullable ) {
145 | options.unshift( );
146 | }
147 |
148 | return
149 | { options }
150 |
151 | break;
152 | case "text":
153 | return
154 | case "integer":
155 | return
156 | case "datetime":
157 | return
158 |
159 | default:
160 | return
161 | }
162 |
163 | },
164 |
165 | render() {
166 | var name = this.props.column.name;
167 | var column = this.props.column;
168 | var label = this.toProperyName(column);
169 |
170 | return(
171 |
172 | { label }
173 | { this.inputField() }
174 | { this.errorsBlock(label) }
175 |
176 | );
177 | },
178 | })
179 |
--------------------------------------------------------------------------------
/js/components/form/Form.react.js:
--------------------------------------------------------------------------------
1 | import Router from 'react-router';
2 | import assign from 'object-assign';
3 | import ResourceActions from 'actions/ResourceActions';
4 | import ResourceStore from 'stores/ResourceStore';
5 | import Property from 'components/detail/Property.react';
6 | import Input from './Input.react';
7 | import BelongsTo from './BelongsTo.react';
8 | import HasOne from './HasOne.react';
9 | import HasMany from './HasMany.react';
10 | import Through from './Through.react';
11 | import LinkMixin from 'components/LinkMixin';
12 | import Utils from 'Utils';
13 |
14 | export default React.createClass({
15 | displayName: 'form/Form',
16 |
17 | mixins: [LinkMixin, Router.Navigation],
18 |
19 | propTypes: {
20 | name: React.PropTypes.string,
21 | id: React.PropTypes.number,
22 | col: React.PropTypes.number,
23 | resource: React.PropTypes.object.isRequired,
24 | settings: React.PropTypes.object,
25 | errors: React.PropTypes.object,
26 | onsubmit: React.PropTypes.func,
27 | classes: React.PropTypes.array,
28 | dirty: React.PropTypes.bool,
29 | csrfToken: React.PropTypes.string,
30 | },
31 |
32 | columns() {
33 | var cols = null;
34 | if(this.props.id){
35 | cols = this.props.settings.edit.columns;
36 | } else {
37 | cols = this.props.settings.create.columns;
38 | }
39 |
40 | return cols;
41 | },
42 |
43 | properties() {
44 | var cols = this.columns();
45 |
46 | var resource = this.props.resource;
47 | var errors = this.props.errors;
48 | var errorsPresent = Utils.present(errors);
49 |
50 | return cols.map((col) => {
51 |
52 | var attrs = {
53 | column: col,
54 | resource: this.props.resource,
55 | settings: this.props.settings,
56 | key: col.name,
57 | };
58 |
59 | if( errorsPresent ) {
60 | var msg = errors[col.name];
61 | if(!msg && col.association){
62 | msg = errors[col.association.name];
63 | if( msg instanceof Array ){
64 | msg = msg[0];
65 | }
66 | }
67 |
68 | if( msg ) {
69 | attrs.invalid = true;
70 | attrs.errors = msg;
71 | }
72 | }
73 |
74 | var TheComponent = Property;
75 |
76 | if( col.readonly ){
77 | return
78 | }
79 |
80 | // editable
81 | attrs.ref = col.name;
82 |
83 | TheComponent = Input;
84 | if( col.association ) {
85 | var atype = col.association.type;
86 | switch(atype) {
87 | case 'belongs_to':
88 | TheComponent = BelongsTo;
89 | break;
90 | case 'has_one':
91 | TheComponent = HasOne;
92 | break;
93 | case 'has_many':
94 | TheComponent = HasMany;
95 | break;
96 | case 'through':
97 | TheComponent = Through;
98 | break;
99 | }
100 | }
101 |
102 | return
103 | });
104 | },
105 |
106 | hasFileField () {
107 | return this.columns().some((c) => { return c.type == "file" });
108 | },
109 |
110 | getFormData(){
111 | var data = {};
112 | var keys = Object.keys(this.refs);
113 |
114 | for (var i = 0, len = keys.length; i < len; i++) {
115 | var key = keys[i];
116 | var property = this.refs[key];
117 |
118 | if( property.isDirty() ) {
119 | var value = property.getFormValue();
120 | assign(data, value);
121 | }
122 | }
123 |
124 | if(this.props.resource.id) {
125 | data.id = this.props.resource.id
126 | }
127 |
128 | return data;
129 | },
130 |
131 | getResourceData(){
132 | var resource = assign({}, this.props.resource);
133 | var keys = Object.keys(this.refs);
134 |
135 | for (var i = 0, len = keys.length; i < len; i++) {
136 | var key = keys[i];
137 | var property = this.refs[key];
138 |
139 | if( property.isDirty() ) {
140 | var value = property.getResourceValue();
141 | assign(resource, value);
142 | }
143 | }
144 |
145 | return resource;
146 | },
147 |
148 | isDirty() {
149 | if( this.props.dirty ) return true;
150 |
151 | var keys = Object.keys(this.refs);
152 | for (var i = 0, len = keys.length; i < len; i++) {
153 | var key = keys[i];
154 | var property = this.refs[key];
155 | if( property.isDirty() ) {
156 | return true
157 | }
158 | }
159 | return false;
160 | },
161 |
162 | handleSave() {
163 | var dirty = this.isDirty();
164 |
165 | if( !dirty ) console.log("formdata isn't modified"); // TODO show message
166 |
167 | var data = this.getFormData();
168 | var resource = this.getResourceData();
169 | var f = this.props.onsubmit;
170 |
171 | if( !f ) {
172 | if(this.props.id) {
173 | f = this.update;
174 | } else {
175 | f = this.create;
176 | }
177 | }
178 |
179 | f(resource, data, dirty);
180 | },
181 |
182 | create(resource, data, dirty) {
183 | if( !dirty ) return;
184 |
185 | let name = this.props.name;
186 | let pagination = this.props.pagination;
187 |
188 | ResourceActions.create(name, { resource: data, csrfToken: this.props.csrfToken }).then(() => {
189 | var state = ResourceStore.getState(name);
190 | var id = state.currentId;
191 | var flash = state.flash;
192 |
193 | this.transitionToShow(name, id, pagination);
194 |
195 | if( flash ) {
196 | $.notifyBar({ cssClass: "success", html: flash, });
197 | }
198 |
199 | });
200 | },
201 |
202 | update(resource, data, dirty) {
203 | if( !dirty ) return;
204 |
205 | let name = this.props.name;
206 | let id = this.props.id;
207 | let pagination = this.props.pagination;
208 |
209 | ResourceActions.update(name, id, { resource: data, csrfToken: this.props.csrfToken }).then(() => {
210 | var state = ResourceStore.getState(name);
211 | var flash = state.flash;
212 |
213 | this.transitionToShow(name, id, pagination);
214 |
215 | if( flash ) {
216 | $.notifyBar({ cssClass: "success", html: flash, });
217 | }
218 | });
219 | },
220 |
221 | render() {
222 | var resource = this.props.resource;
223 | var classes = ["resource-form"] ;
224 | if(this.props.col) {
225 | classes.push("col-md-" + this.props.col);
226 | }
227 | if(this.props.classes) {
228 | classes = classes.concat(this.props.classes);
229 | }
230 | classes = classes.join(" ");
231 |
232 | if (!resource) {
233 | return
234 | }
235 |
236 | var title = `New: ${this.props.label}`;
237 | if (this.props.id) {
238 | title = `Edit: ${this.props.settings.label}(id:${this.props.id})`;
239 | }
240 |
241 | var properties = this.properties();
242 | var onsubmit = this.props.onsubmit || this.handleSave
243 |
244 | return(
245 |
246 |
247 |
248 |
{ title }
249 |
250 |
251 |
252 | save
253 |
254 |
255 |
256 |
259 |
260 |
261 |
262 | );
263 | },
264 | })
265 |
--------------------------------------------------------------------------------
/js/components/form/Association.react.js:
--------------------------------------------------------------------------------
1 | import assign from 'object-assign';
2 | import ResourceActions from 'actions/ResourceActions';
3 | import ResourceStore from 'stores/ResourceStore';
4 |
5 | import DialogActions from 'actions/DialogActions';
6 | import PropertyMixin from 'components/PropertyMixin';
7 |
8 | import Selection from 'components/list/Selection.react';
9 |
10 | export default React.createClass({
11 | displayName: 'form/Association',
12 |
13 | mixins: [PropertyMixin],
14 |
15 | propTypes: {
16 | name: React.PropTypes.string.isRequired,
17 | column: React.PropTypes.object.isRequired,
18 | resource: React.PropTypes.object.isRequired,
19 | settings: React.PropTypes.object.isRequired,
20 | target: React.PropTypes.object,
21 | buttons: React.PropTypes.object,
22 | invalid: React.PropTypes.bool,
23 | },
24 |
25 | getState(target) {
26 | return {
27 | target: target,
28 | dirty: false,
29 | }
30 | },
31 |
32 | getInitialState() {
33 | return this.getState(this.props.target);
34 | },
35 |
36 | componentWillReceiveProps(nextProps) {
37 | if (nextProps.target != this.props.target) {
38 | this.setState(this.getState(nextProps.target));
39 | }
40 | },
41 |
42 | getFormValue() {
43 | var value = {};
44 | if( this.state.formdata ) {
45 | value[this.props.column.association.name] = this.state.formdata;
46 | }
47 |
48 | return value;
49 | },
50 |
51 | getResourceValue() {
52 | var value = {};
53 | value[this.props.column.association.name] = this.state.target;
54 | return value;
55 | },
56 |
57 | isDirty() {
58 | return this.state.dirty;
59 | },
60 |
61 | associationName() {
62 | return this.props.column.association.pluralized;
63 | },
64 |
65 | associationLabel() {
66 | return this.props.column.association.label;
67 | },
68 |
69 | dialogName(prefix) {
70 | return prefix + ":" + this.associationName();
71 | },
72 |
73 | clear() {
74 | if(this.state.target && this.state.target.id) {
75 | this.setState({
76 | target: null,
77 | destroy: { id: this.state.target.id, _destroy: true },
78 | dirty: true,
79 | reason: "cleared",
80 | });
81 | }
82 | },
83 |
84 | onSelect(selected) {
85 | this.setState({ target: selected, dirty: true, reason: "selected"});
86 |
87 | DialogActions.close(this.dialogName("selection"));
88 | },
89 |
90 | openSelection() {
91 | var title = this.associationLabel();
92 | var name = this.props.column.association.path;
93 |
94 | ResourceActions.list(name).then(() => {
95 | DialogActions.open(this.dialogName("selection"), Selection, { name: name, title: title, onselect: this.onSelect });
96 | });
97 | },
98 |
99 | settingsForForm(settings, action) {
100 | var modified= assign({}, settings);
101 | var columns = modified[action].columns;
102 | var atype = this.props.column.association.type;
103 | if(atype == 'has_one' || atype == 'has_many'){
104 | var foreign_key = this.props.column.association.foreign_key;
105 | columns = columns.map((col) => {
106 | var res = col;
107 | if(col.name == foreign_key){
108 | col = assign({}, col);
109 | col.readonly = true;
110 | };
111 | return col;
112 | });
113 | }
114 |
115 | modified[action].columns = columns;
116 | return modified;
117 | },
118 |
119 | propsForForm(action, target, settings) {
120 | var name = this.associationName();
121 | var modified_settings = this.settingsForForm(settings, action);
122 |
123 | var modified_target = assign({}, target);
124 | var atype = this.props.column.association.type;
125 |
126 | if(atype == 'has_one' || atype == 'has_many'){
127 | var foreign_key = this.props.column.association.foreign_key;
128 | var foreign_property = foreign_key.replace(/_id$/, ""); // TODO: fix
129 | modified_target[foreign_key] = this.props.resource;
130 | modified_target[foreign_property] = this.props.resource;
131 | }
132 | var dirty = this.state.dirty;
133 | var props = {
134 | name: name,
135 | id: target.id,
136 | resource: modified_target,
137 | settings: modified_settings,
138 | onsubmit: this.onSubmit,
139 | classes: ['dialog-body'],
140 | dirty: dirty
141 | }
142 |
143 | return props;
144 |
145 | },
146 |
147 | openDialog(action, fetchmethod) {
148 | var name = this.props.column.association.path;
149 | var Form = require('./Form.react');
150 |
151 | if(this.state.dirty && this.state.formsettings) {
152 | var props = this.propsForForm(action, this.state.target, this.state.formsettings);
153 | DialogActions.open(this.dialogName(action), Form, props);
154 | } else {
155 | fetchmethod().then(() => {
156 | var state = ResourceStore.getState(name);
157 | var props = this.propsForForm(action, state.currentResource, state.settings);
158 | this.state.formsettings = state.settings;
159 | DialogActions.open(this.dialogName(action), Form, props);
160 | });
161 | }
162 | },
163 |
164 | openEdit() {
165 | var name = this.props.column.association.path;
166 | var id = this.state.target.id;
167 | if(!id) {
168 | return false;
169 | }
170 | this.openDialog("edit", () => {
171 | return ResourceActions.fetch(name, id);
172 | });
173 | },
174 |
175 | openCreate() {
176 | var name = this.props.column.association.path;
177 | this.openDialog("create", () => {
178 | return ResourceActions.build(name);
179 | });
180 | },
181 |
182 | onSubmit(resource, data, dirty) {
183 |
184 | if( dirty ) {
185 | var reason = "created";
186 | if( resource.id ) {
187 | reason = "edited"
188 | }
189 | this.setState({ target: resource, dirty: true, formdata: data, reason: reason});
190 | }
191 |
192 | DialogActions.close(this.dialogName("create"));
193 | DialogActions.close(this.dialogName("edit"));
194 | },
195 |
196 | buttons() {
197 | var default_buttons = {
198 | select: true,
199 | clear: true,
200 | create: true,
201 | edit: true,
202 | }
203 | var buttons = this.props.buttons || default_buttons;
204 | var result = [];
205 |
206 | if(this.props.disabled) {
207 | return result;
208 | }
209 |
210 | if(buttons.clear) {
211 | result.push(
212 |
213 |
214 |
215 | );
216 | }
217 | if(buttons.select) {
218 | result.push(
219 |
220 |
221 |
222 | );
223 | }
224 |
225 | if(buttons.create) {
226 | result.push(
227 |
228 |
229 |
230 | );
231 | }
232 |
233 | if(buttons.edit) {
234 | result.push(
235 |
236 |
237 |
238 | );
239 | }
240 |
241 | return result;
242 | },
243 |
244 | render() {
245 | var displayText = '';
246 | if(this.state.target) {
247 | displayText = this.extractLabel(this.associationLabel(), this.state.target, this.props.settings.search_columns);
248 | }
249 |
250 | var classes = "form-control input-sm";
251 | if(this.isDirty()){
252 | classes += " modified";
253 | }
254 |
255 | if(this.props.invalid) {
256 | classes += " invalid";
257 | }
258 |
259 | return(
260 |
261 |
262 |
263 | { this.buttons() }
264 |
265 |
266 | );
267 | },
268 | })
269 |
--------------------------------------------------------------------------------
/js/actions/ResourceActions.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../AppDispatcher';
2 | import Constants from '../Constants';
3 | import Utils from 'Utils';
4 |
5 | export default {
6 |
7 | initialize(data) {
8 | AppDispatcher.dispatch({
9 | type: Constants.INITIALIZE,
10 | data: data
11 | });
12 | },
13 |
14 | fetch(name, id, query = {}){
15 | let url = '/administa/' + name + '/' + id;
16 |
17 | return $.ajax({
18 | url: url,
19 | dataType: 'json',
20 | data: query
21 | })
22 | .done((data) => {
23 | AppDispatcher.dispatch({
24 | type: Constants.RESOURCE_FETCH,
25 | name: name,
26 | data: data
27 | });
28 | })
29 | .fail(function(xhr, status, err) {
30 | console.error(url, status, err.toString());
31 | var message = `${this.type} ${this.url} ${err}`;
32 | var error = new Error(message);
33 | Utils.reportError(message, error, error.stack);
34 | }).promise();
35 |
36 | },
37 |
38 | list(name, query = {}) {
39 | let url = '/administa/' + name;
40 |
41 | return $.ajax({
42 | url: url,
43 | dataType: 'json',
44 | data: query
45 | })
46 | .done((data) => {
47 | AppDispatcher.dispatch({
48 | type: Constants.RESOURCE_LIST,
49 | name: name,
50 | data: data
51 | });
52 | })
53 | .fail(function(xhr, status, err) {
54 | console.error(url, status, err.toString());
55 | var message = `${this.type} ${this.url} ${err}`;
56 | var error = new Error(message);
57 | Utils.reportError(message, error, error.stack);
58 | }).promise();
59 | },
60 |
61 | build(name, data = {}){
62 | let url = '/administa/' + name + "/new";
63 |
64 | return $.ajax({
65 | url: url,
66 | dataType: 'json',
67 | data: data
68 | })
69 | .done((data) => {
70 | AppDispatcher.dispatch({
71 | type: Constants.RESOURCE_BUILD,
72 | name: name,
73 | data: data
74 | });
75 | })
76 | .fail(function(xhr, status, err) {
77 | console.error(url, status, err.toString());
78 | var message = `${this.type} ${this.url} ${err}`;
79 | var error = new Error(message);
80 | Utils.reportError(message, error, error.stack);
81 | }).promise();
82 | },
83 |
84 | create(name, data = {}) {
85 | let url = '/administa/' + name ;
86 |
87 | return this.sendRequiest("POST", url, data).done((data) => {
88 | AppDispatcher.dispatch({
89 | type: Constants.RESOURCE_CREATED,
90 | name: name,
91 | data: data
92 | });
93 | })
94 | .fail(function(xhr, status, err) {
95 | if( xhr.status == 422) {
96 | var res = xhr.responseJSON;
97 | AppDispatcher.dispatch({
98 | type: Constants.RESOURCE_INVALID,
99 | name: name,
100 | data: res
101 | });
102 | }
103 |
104 | console.error(url, status, err.toString());
105 | var message = `${this.type} ${this.url} ${err}`;
106 | var error = new Error(message);
107 | Utils.reportError(message, error, error.stack);
108 | }).promise();
109 | },
110 |
111 | update(name, id, data = {}) {
112 | let url = '/administa/' + name + "/" + id ;
113 |
114 | return this.sendRequiest("PUT", url, data).done((data) => {
115 | AppDispatcher.dispatch({
116 | type: Constants.RESOURCE_UPDATED,
117 | name: name,
118 | data: data
119 | });
120 | })
121 | .fail(function(xhr, status, err) {
122 | if( xhr.status == 422) {
123 | var res = xhr.responseJSON;
124 | AppDispatcher.dispatch({
125 | type: Constants.RESOURCE_INVALID,
126 | name: name,
127 | data: res
128 | });
129 | }
130 | console.error(url, status, err.toString());
131 | var message = `${this.type} ${this.url} ${err}`;
132 | var error = new Error(message);
133 | Utils.reportError(message, error, error.stack);
134 | }).promise();
135 | },
136 |
137 | destroy(name, id, query, options = {}) {
138 | let url = '/administa/' + name + "/" + id ;
139 |
140 | var csrfToken = options.csrfToken;
141 | var ajaxparams = {
142 | url: url,
143 | type: "DELETE",
144 | dataType: 'json',
145 | data: query,
146 | };
147 |
148 | if( csrfToken ) {
149 | ajaxparams.headers = {
150 | 'X-CSRF-Token': csrfToken
151 | }
152 | }
153 |
154 | return $.ajax(ajaxparams).done((data) => {
155 | AppDispatcher.dispatch({
156 | type: Constants.RESOURCE_DELETED,
157 | name: name,
158 | data: data
159 | });
160 | })
161 | .fail(function(xhr, status, err) {
162 | if( xhr.status == 422) {
163 | var res = xhr.responseJSON;
164 | AppDispatcher.dispatch({
165 | type: Constants.RESOURCE_INVALID,
166 | name: name,
167 | data: res
168 | });
169 | }
170 | console.error(url, status, err.toString());
171 | var message = `${this.type} ${this.url} ${err}`;
172 | var error = new Error(message);
173 | Utils.reportError(message, error, error.stack);
174 | }).promise();
175 | },
176 |
177 | sendRequiest(method, url, data) {
178 | var csrfToken = data.csrfToken;
179 | delete data.csrfToken;
180 |
181 | var reqdata = this.requestData(data);
182 | var ajaxparams = {
183 | url: url,
184 | type: method,
185 | dataType: 'json',
186 | };
187 |
188 | if( csrfToken ) {
189 | ajaxparams.headers = {
190 | 'X-CSRF-Token': csrfToken
191 | }
192 | }
193 |
194 | if( reqdata instanceof FormData ){
195 | ajaxparams.data = reqdata;
196 | ajaxparams.processData = false;
197 | ajaxparams.contentType = false;
198 | } else {
199 | ajaxparams.data = JSON.stringify(reqdata);
200 | ajaxparams.contentType = 'application/json';
201 | }
202 |
203 | return $.ajax(ajaxparams);
204 | },
205 |
206 | requestData(data) {
207 | var res = this.separateJsonAndFiles(data);
208 | var json = res.json;
209 | var files = res.files;
210 |
211 | if( Utils.empty(files) ) {
212 | return json;
213 | }
214 |
215 | var formdata = new FormData();
216 | formdata = this.constructFormData(formdata, json);
217 | formdata = this.constructFormData(formdata, files);
218 |
219 | return formdata;
220 | },
221 |
222 | constructFormData(formdata, data, prefix) {
223 |
224 |
225 | var keys = Object.keys(data);
226 | for (var i = 0, len = keys.length; i < len; i++) {
227 | var key = keys[i];
228 | var v = data[key];
229 |
230 | var name = key;
231 | if( prefix ) {
232 | name = `${prefix}[${key}]`;
233 | }
234 |
235 | if( v instanceof File ) {
236 | formdata.append(name, v, v.name);
237 | continue;
238 | }
239 |
240 | if( v instanceof Array ) {
241 | v.forEach((child) => {
242 | if( Utils.isPrimitive(child) ) {
243 | formdata.append(name + "[]", child);
244 | } else {
245 | this.constructFormData(formdata, child, name + "[]");
246 | }
247 | });
248 | continue;
249 | }
250 |
251 | if( v instanceof Object) {
252 | this.constructFormData(formdata, v, name);
253 | continue;
254 | }
255 | formdata.append(name, v);
256 | }
257 |
258 | return formdata;
259 | },
260 |
261 | separateJsonAndFiles(data) {
262 | var json = {};
263 | var files = {};
264 |
265 | if( Utils.isPrimitive(data) ) {
266 | return { json: data, files: files };
267 | }
268 |
269 | var keys = Object.keys(data);
270 | for (var i = 0, len = keys.length; i < len; i++) {
271 | var name = keys[i];
272 | var v = data[name];
273 |
274 | if( v instanceof File ) {
275 | files[name] = v;
276 | continue;
277 | }
278 |
279 | if( v instanceof Array ) {
280 | var childJson = [];
281 | var childFiles= [];
282 | if( v.length == 0 ) {
283 | json[name] = v;
284 | continue;
285 | }
286 |
287 | v.forEach((child) => {
288 | if( Utils.isPrimitive(child) ) {
289 | childJson.push(child);
290 | } else {
291 | var res = this.separateJsonAndFiles(child);
292 | if( Utils.present(res.json) ) {
293 | childJson.push(res.json);
294 | }
295 | if( Utils.present(res.files) ) {
296 | childFiles.push(res.files);
297 | }
298 | }
299 | });
300 |
301 | if( Utils.present(childJson) ) {
302 | json[name] = childJson;
303 | }
304 | if( Utils.present(childFiles) ) {
305 | files[name] = childFiles;
306 | }
307 | continue;
308 | }
309 |
310 | if( v instanceof Object) {
311 | var res = this.separateJsonAndFiles(v);
312 | if( Utils.present(res.json) ) {
313 | json[name] = res.json;
314 | }
315 | if( Utils.present(res.files) ) {
316 | files[name] = res.files;
317 | }
318 | continue;
319 | }
320 |
321 | json[name] = v;
322 | }
323 |
324 | return { json: json, files: files };
325 | }
326 | }
327 |
328 |
329 |
--------------------------------------------------------------------------------
/lib/administa/model/options.rb:
--------------------------------------------------------------------------------
1 | module Administa
2 | class ConfigurationError < StandardError; end
3 | class Model
4 | module Options
5 |
6 | def settings
7 | translate(options).merge(
8 | locale: I18n.locale,
9 | timezone: Administa.config.timezone,
10 | timezone_offset: Administa.config.timezone_offset,
11 | )
12 | end
13 |
14 | def setup_options!(klass = self.klass, options = self.given_options)
15 |
16 | options = default_settings(klass).deep_merge(options)
17 | self.options = options
18 |
19 | default_cols = default_colums(klass)
20 | global_cols = Array.wrap(options[:columns])
21 | global_append_cols = Array.wrap(options[:append])
22 | global_except_cols = Array.wrap(options[:except])
23 |
24 | # TODO Refactoring
25 | [:index, :show, :create, :edit].each do |action|
26 | options[action] ||= {}
27 | options[action][:columns] = parse_actions_columns_option(action, options, global_cols, global_append_cols, global_except_cols, default_cols)
28 | end
29 |
30 | [:create, :edit].flat_map{|act| options[act][:columns] }.uniq.each do |col|
31 | if col[:name].in?([:id, :created_at, :updated_at])
32 | col[:readonly] = true
33 | end
34 |
35 | check_configuration(col)
36 | end
37 |
38 | @columns_meta = nil
39 | @associations_meta = nil
40 | options
41 | end
42 |
43 | def parse_actions_columns_option(action, options, global_cols, global_append_cols, global_except_cols, default_cols)
44 | actions_option = options.try(:[], action) || {}
45 |
46 | cols = Array.wrap(actions_option[:columns]).presence ||
47 | global_cols.presence ||
48 | default_cols[action][:columns]
49 |
50 | append = (Array.wrap(actions_option[:append]).presence ||
51 | global_append_cols).to_a
52 |
53 | except = (Array.wrap(actions_option[:except]) +
54 | global_except_cols.to_a).map(&:to_sym)
55 |
56 | result = (cols + append ).reject{|col|
57 | name = (col.is_a? Hash) ? col[:name] : col.to_sym
58 | except.include? name
59 | }.map{|col|
60 | parse_column_option(col, action)
61 | }
62 |
63 | compact_columns(result)
64 | end
65 |
66 | def compact_columns(columns)
67 | result = columns.inject([]){|arr, col|
68 | other = arr.find{|c| c[:name] == col[:name] }
69 | if other
70 | other.merge!(col)
71 | else
72 | arr.push(col.dup)
73 | end
74 |
75 | arr
76 | }
77 |
78 | [:created_at, :updated_at].each do |attr|
79 | idx = result.find_index{|col| col[:name] == attr}
80 | if idx && idx >= 0 && c = result.delete_at(idx)
81 | result.push(c)
82 | end
83 | end
84 | result
85 | end
86 |
87 | def parse_column_option(col, action)
88 | name = case col
89 | when String, Symbol then col.to_sym
90 | when Hash then col[:name].try(:to_sym)
91 | end
92 |
93 | unless name
94 | raise ConfigurationError, "given no column name: #{col.inspect} in #{klass} : #{action}"
95 | end
96 |
97 | c = columns_meta(name) || associations_meta(name) || accessor_meta(name)
98 | if col.is_a? Hash
99 | c = (c || {}).merge(col)
100 | c[:accessor] ||= :method
101 |
102 | raise ConfigurationError, "column type is required: #{col.inspect} in #{klass} : #{action}" unless c[:type]
103 | end
104 |
105 | unless c
106 | raise ConfigurationError, "Unknown column : #{col} in #{klass} : #{action}"
107 | end
108 | c
109 | end
110 |
111 | def default_settings(klass)
112 | {
113 | limit: 20,
114 | order: :id,
115 | search_columns: [:name, :title],
116 | attr_accessible_role: :default,
117 | actions: Administa.config.actions,
118 | }
119 | end
120 |
121 | def default_colums(klass)
122 | columns = klass.column_names.map(&:to_sym)
123 | nesteds = klass.nested_attributes_options.keys
124 |
125 | create_columns = (columns - %w(id created_at updated_at).map(&:to_sym) + nesteds).reject{|c| readonly?(c)}
126 | edit_columns = (columns - %w(created_at updated_at).map(&:to_sym) + nesteds).reject{|c| readonly?(c)}
127 |
128 | {
129 | index: {
130 | columns: columns,
131 | },
132 | show: {
133 | columns: columns,
134 | },
135 | create: {
136 | columns: create_columns,
137 | },
138 | edit: {
139 | columns: edit_columns,
140 | }
141 | }
142 | end
143 |
144 | def columns_meta(name)
145 | return @columns_meta[name.to_sym] if @columns_meta && @columns_meta[name.to_sym]
146 |
147 | col = klass.columns_hash[name.to_s]
148 | return unless col
149 |
150 | assocs = klass.reflect_on_all_associations
151 | uploaders = (klass.respond_to?(:uploaders) && klass.uploaders) || {}
152 | enums = enumerized_attributes(klass).try(:[], col.name.to_sym)
153 |
154 | type = col.type
155 | type = :file if uploaders[col.name.to_sym] # carrierwave?
156 |
157 | default = col.default
158 | if enums
159 | type = :enum
160 | default = enums.first if default.nil? && !col.null
161 | end
162 |
163 | meta = {
164 | name: col.name.to_sym,
165 | type: type,
166 | readonly: readonly?(col.name),
167 | accessor: :column,
168 | nullable: col.null,
169 | default: default,
170 | }
171 | meta[:enums] = enums if enums
172 |
173 | assocs.
174 | find{|a| a.macro == :belongs_to && a.foreign_key == col.name}.
175 | try{|a|
176 | # assoc_klass = Administa.config.klasss[a.name.to_s.singularize.to_sym]
177 | meta[:association] = create_association_meta(a)
178 | }
179 |
180 | @columns_meta ||= {}
181 | @columns_meta[name.to_sym] = meta
182 |
183 | meta
184 | end
185 |
186 | def associations_meta(name)
187 | return @associations_meta[name.to_sym] if @associations_meta && @associations_meta[name.to_sym]
188 |
189 | a = klass.reflect_on_association(name)
190 | return nil unless a
191 |
192 | meta = create_association_meta(a)
193 | editable = [:select, :create, :update, :destroy].any?{|action| meta[action]}
194 |
195 | res = {
196 | name: (a.macro == :belongs_to) ? a.foreign_key.to_sym : a.name.to_sym,
197 | type: a.macro,
198 | readonly: (not editable),
199 | accessor: :association,
200 | association: meta,
201 | }
202 |
203 | @associations_meta ||= {}
204 | @associations_meta[name.to_sym] = res
205 | res
206 | end
207 |
208 | def create_association_meta(assoc)
209 | nested_options = klass.nested_attributes_options
210 | nested = nested_options[assoc.name]
211 | type = assoc.macro
212 | foreign_key = assoc.foreign_key
213 |
214 | attributes_name = "#{assoc.name}_attributes"
215 | editable = (not readonly?(attributes_name))
216 |
217 | if type == :has_many && assoc.options[:through]
218 | type = :through
219 | end
220 |
221 | selectable = false
222 | case type
223 | when :belongs_to
224 | selectable = (not readonly?(assoc.foreign_key))
225 | when :has_many
226 | foreign_key = "#{assoc.name.to_s.singularize}_ids"
227 | selectable = (not readonly?(foreign_key))
228 | when :through
229 | foreign_key = "#{assoc.name.to_s.singularize}_ids"
230 | selectable = (not readonly?(foreign_key))
231 | end
232 |
233 | assoc_model_name = assoc.class_name.to_s.underscore
234 | res = {
235 | name: assoc.name,
236 | type: type,
237 | foreign_key: foreign_key,
238 | pluralized: assoc.name.to_s.pluralize,
239 | path: assoc_model_name.pluralize,
240 | select: selectable,
241 | create: (editable && nested.present? && !nested[:update_only]),
242 | update: (editable && nested.present?),
243 | destroy: (nested.present? && !!nested[:allow_destroy]),
244 | }
245 |
246 | if model = Administa.config.models[assoc_model_name.to_sym]
247 | res[:controller_path] = model.controller.controller_path
248 | end
249 |
250 | res
251 | end
252 |
253 | def accessor_meta(name)
254 | return @accessor_meta[name.to_sym] if @accessor_meta && @accessor_meta[name.to_sym]
255 |
256 | reader = klass.instance_method(name) rescue nil
257 | writer = klass.instance_method("#{name}=") rescue nil
258 |
259 | return nil unless reader
260 |
261 | meta = {
262 | name: name.to_sym,
263 | type: :string,
264 | readonly: writer.nil?,
265 | accessor: :accessor,
266 | }
267 |
268 | meta
269 | end
270 |
271 | def includes(action)
272 | from_options = (options[:includes] || options[action].try(:[], :includes)).to_a
273 |
274 | tables = options[action].
275 | try(:[], :columns).
276 | to_a.
277 | map{|meta| meta[:association].try(:[], :name) }
278 |
279 | (tables + from_options).compact
280 | end
281 |
282 | def associations
283 | reflections = klass.reflections
284 | options[:associations].inject({}){|h, (macro, names)|
285 | h[macro] = names.map{|name| reflections[name] }
286 | h
287 | }
288 | end
289 |
290 | def readonly?(column)
291 | role = self.options[:attr_accessible_role] || :default
292 | authorizer = (klass.respond_to? :active_authorizer) ? klass.try(:active_authorizer).try(:[], role) : nil
293 | return false unless authorizer
294 |
295 | authorizer.deny?(column)
296 |
297 | end
298 |
299 | def check_configuration(col)
300 | name = col[:name]
301 | association = col[:association]
302 |
303 | if col[:readonly]
304 | unless association
305 | Rails.logger.debug "[Administa] warning: #{name} isn't specified in 'attr_accessible' of #{klass}. You should add #{name} if you want to edit" unless name.to_sym.in? [:id, :created_at, :updated_at]
306 | return
307 | end
308 |
309 | association_name = association[:name]
310 | attributes_name = "#{association_name.to_s.singularize}_attributes"
311 | readonly = readonly?(attributes_name)
312 |
313 | if readonly
314 | Rails.logger.debug "[Administa] warning: #{attributes_name} isn't specified in 'attr_accessible' of #{klass}. You should add #{attributes_name} if you want to edit #{name} association "
315 | return
316 | end
317 |
318 | nested_options = klass.nested_attributes_options
319 | nested = nested_options[association_name]
320 |
321 | unless nested
322 | Rails.logger.debug "[Administa] warning: #{association_name} isn't specified in 'accepts_nested_attributes_for' of #{klass}. You should add #{attributes_name} if you want to edit #{name} association "
323 | end
324 | end
325 | end
326 |
327 | def translate(options)
328 | scope = "activerecord.attributes.#{self.name}"
329 | [:index, :show, :create, :edit].each do |action|
330 | columns = options.try(:[], action).try(:[], :columns)
331 | columns.each do |col|
332 | i18n_scope = col[:i18n_scope] || scope
333 | col[:label] = I18n.t(col[:name], scope: i18n_scope, default: col[:name].to_s)
334 | if col[:association]
335 | col[:association][:label] = I18n.t(col[:association][:label], scope: scope, default: col[:association][:name].to_s)
336 | end
337 | end
338 | end
339 |
340 | options[:label]= self.label
341 | options
342 | end
343 |
344 | def label
345 | I18n.t(self.name, scope: 'activerecord.models', default: self.name.to_s)
346 | end
347 |
348 | def enumerized_attributes(klass)
349 | return nil unless klass.respond_to? :enumerized_attributes
350 | klass.enumerized_attributes.
351 | try(:attributes).
352 | inject({}){|h, (k, attr)|
353 | h[k.to_sym] = attr.values.try(:map, &:to_sym); h
354 | }
355 | end
356 |
357 | end
358 | end
359 | end
360 |
--------------------------------------------------------------------------------