simple_admin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. data/.gitignore +6 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +12 -0
  4. data/README.rdoc +318 -0
  5. data/Rakefile +67 -0
  6. data/TODO.rdoc +4 -0
  7. data/app/assets/images/simple_admin/active_admin/admin_notes_icon.png +0 -0
  8. data/app/assets/images/simple_admin/active_admin/loading.gif +0 -0
  9. data/app/assets/images/simple_admin/active_admin/nested_menu_arrow.gif +0 -0
  10. data/app/assets/images/simple_admin/active_admin/nested_menu_arrow_dark.gif +0 -0
  11. data/app/assets/images/simple_admin/active_admin/orderable.png +0 -0
  12. data/app/assets/javascripts/simple_admin/active_admin.js +434 -0
  13. data/app/assets/stylesheets/simple_admin/active_admin.css +1445 -0
  14. data/app/controllers/simple_admin/admin_controller.rb +117 -0
  15. data/app/helpers/simple_admin/admin_helper.rb +11 -0
  16. data/app/helpers/simple_admin/display_helper.rb +38 -0
  17. data/app/helpers/simple_admin/filter_helper.rb +147 -0
  18. data/app/helpers/simple_admin/header_helper.rb +60 -0
  19. data/app/helpers/simple_admin/path_helper.rb +12 -0
  20. data/app/helpers/simple_admin/sidebar_helper.rb +9 -0
  21. data/app/helpers/simple_admin/table_helper.rb +39 -0
  22. data/app/helpers/simple_admin/title_helper.rb +35 -0
  23. data/app/views/layouts/simple_admin.html.erb +41 -0
  24. data/app/views/simple_admin/admin/_form.html.erb +16 -0
  25. data/app/views/simple_admin/admin/edit.html.erb +7 -0
  26. data/app/views/simple_admin/admin/index.csv.erb +19 -0
  27. data/app/views/simple_admin/admin/index.html.erb +82 -0
  28. data/app/views/simple_admin/admin/new.html.erb +7 -0
  29. data/app/views/simple_admin/admin/show.html.erb +22 -0
  30. data/config/routes.rb +8 -0
  31. data/lib/rails/generators/simple_admin/simple_admin_generator.rb +33 -0
  32. data/lib/rails/generators/simple_admin/templates/initializer.rb +76 -0
  33. data/lib/simple_admin.rb +100 -0
  34. data/lib/simple_admin/attributes.rb +80 -0
  35. data/lib/simple_admin/breadcrumbs.rb +24 -0
  36. data/lib/simple_admin/builder.rb +35 -0
  37. data/lib/simple_admin/engine.rb +8 -0
  38. data/lib/simple_admin/filters.rb +5 -0
  39. data/lib/simple_admin/interface.rb +55 -0
  40. data/lib/simple_admin/section.rb +30 -0
  41. data/lib/simple_admin/version.rb +3 -0
  42. data/rails/init.rb +2 -0
  43. data/simple_admin.gemspec +34 -0
  44. data/spec/acceptance/admin_thing_spec.rb +13 -0
  45. data/spec/controllers/simple_admin/admin_controller_spec.rb +95 -0
  46. data/spec/factories.rb +14 -0
  47. data/spec/simple_admin/attributes_spec.rb +106 -0
  48. data/spec/simple_admin/breadcrumbs_spec.rb +18 -0
  49. data/spec/simple_admin/builder_spec.rb +57 -0
  50. data/spec/simple_admin/engine_spec.rb +9 -0
  51. data/spec/simple_admin/filters_spec.rb +16 -0
  52. data/spec/simple_admin/interface_spec.rb +98 -0
  53. data/spec/simple_admin/section_spec.rb +63 -0
  54. data/spec/simple_admin/simple_admin_spec.rb +68 -0
  55. data/spec/spec_helper.rb +32 -0
  56. metadata +285 -0
@@ -0,0 +1,117 @@
1
+ require 'kaminari'
2
+ require 'meta_search'
3
+
4
+ module SimpleAdmin
5
+ class AdminController < ::ApplicationController
6
+ before_filter :require_user
7
+ before_filter :lookup_interface
8
+ before_filter :lookup_resource, :only => [:show, :edit, :update, :destroy]
9
+ before_filter :handle_before
10
+
11
+ unloadable
12
+
13
+ respond_to :csv, :json, :xml, :html
14
+
15
+ helper SimpleAdmin::AdminHelper
16
+
17
+ layout 'simple_admin'
18
+
19
+ def index
20
+ @collection = @interface.constant
21
+ @collection = @collection.order("#{@interface.constant.table_name}.#{$1} #{$2}") if params[:order] && params[:order] =~ /^([\w\_\.]+)_(desc|asc)$/
22
+ @collection = @collection.metasearch(clean_search_params(params))
23
+ @collection = @collection.page(params[:page]).per(@per_page || SimpleAdmin.default_per_page) if params[:format].blank? || params[:format] == 'html'
24
+ respond_with(@collection)
25
+ end
26
+
27
+ def show
28
+ @resource = @interface.constant.find(params[:id])
29
+ respond_with(@resource)
30
+ end
31
+
32
+ def new
33
+ @resource = @interface.constant.new
34
+ respond_with(@resource)
35
+ end
36
+
37
+ def edit
38
+ respond_with(@resource)
39
+ end
40
+
41
+ def create
42
+ @resource = @interface.constant.new(params[@interface.member.to_sym])
43
+ # respond_with will fail without explicit urls
44
+ respond_to do |format|
45
+ if @resource.save
46
+ format.html { redirect_to send("simple_admin_#{@interface.member}_path", @resource), :notice => "#{@interface.member.titleize} was successfully created." }
47
+ format.json { render :json => @resource, :status => :created, :location => send("simple_admin_#{@interface.member}_path", @resource) }
48
+ format.xml { render :xml => @resource, :status => :created, :location => send("simple_admin_#{@interface.member}_path", @resource) }
49
+ else
50
+ format.html { render :action => "new" }
51
+ format.json { render :json => @resource.errors, :status => :unprocessable_entity }
52
+ format.xml { render :xml => @resource.errors, :status => :unprocessable_entity }
53
+ end
54
+ end
55
+ end
56
+
57
+ def update
58
+ # respond_with will fail without explicit urls
59
+ respond_to do |format|
60
+ if @resource.update_attributes(params[@interface.member.to_sym])
61
+ format.html { redirect_to send("simple_admin_#{@interface.member}_path", @resource), :notice => "#{@interface.member.titleize} was successfully updated." }
62
+ format.json { head :ok }
63
+ format.xml { head :ok }
64
+ else
65
+ format.html { render :action => "edit" }
66
+ format.json { render :json => @resource.errors, :status => :unprocessable_entity }
67
+ format.xml { render :xml => @resource.errors, :status => :unprocessable_entity }
68
+ end
69
+ end
70
+ end
71
+
72
+ def destroy
73
+ @resource.destroy
74
+ # respond_with will fail without explicit urls
75
+ respond_to do |format|
76
+ format.html { redirect_to send("simple_admin_#{@interface.collection}_path") }
77
+ format.json { head :ok }
78
+ format.xml { head :ok }
79
+ end
80
+ end
81
+
82
+ protected
83
+
84
+ def require_user
85
+ send(SimpleAdmin.require_user_method) if SimpleAdmin.require_user_method
86
+ end
87
+
88
+ def lookup_interface
89
+ SimpleAdmin.registered.each do |interface|
90
+ @interface = interface if interface.collection == params[:interface]
91
+ end
92
+ # This should not be reached, routing should catch errors before this point
93
+ raise UnknownAdminInterface.new("Could not find the interface for simple admin") unless @interface
94
+ end
95
+
96
+ def lookup_resource
97
+ @resource = @interface.constant.find(params[:id])
98
+ end
99
+
100
+ def handle_before
101
+ @interface.before.each do |before|
102
+ next unless before[:actions].include?(params[:action].to_sym)
103
+ instance_eval(&before[:data])
104
+ end
105
+ end
106
+
107
+ def clean_search_params(search_params)
108
+ return {} unless search_params.is_a?(Hash)
109
+ search_params = search_params.dup
110
+ search_params.delete_if do |key, value|
111
+ value == "" ||
112
+ ["utf8", "scope", "commit", "action", "order", "interface", "controller", "format"].include?(key)
113
+ end
114
+ search_params
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,11 @@
1
+ module SimpleAdmin
2
+ module AdminHelper
3
+ include SimpleAdmin::TitleHelper
4
+ include SimpleAdmin::HeaderHelper
5
+ include SimpleAdmin::TableHelper
6
+ include SimpleAdmin::DisplayHelper
7
+ include SimpleAdmin::FilterHelper
8
+ include SimpleAdmin::SidebarHelper
9
+ include SimpleAdmin::PathHelper
10
+ end
11
+ end
@@ -0,0 +1,38 @@
1
+ module SimpleAdmin
2
+ module DisplayHelper
3
+
4
+ # Return a pretty string for any object
5
+ # Date Time are formatted via #localize with :format => :long
6
+ # ActiveRecord objects are formatted via #auto_link
7
+ # We attempt to #display_name of any other objects
8
+ def pretty_format(object)
9
+ case object
10
+ when String
11
+ object
12
+ when Date, Time
13
+ localize(object, :format => :long)
14
+ else
15
+ (object.respond_to?(:display_name) && object.send(:display_name)) ||
16
+ (object.respond_to?(:full_name) && object.send(:full_name)) ||
17
+ (object.respond_to?(:name) && object.send(:name)) ||
18
+ (object.respond_to?(:username) && object.send(:username)) ||
19
+ (object.respond_to?(:login) && object.send(:login)) ||
20
+ (object.respond_to?(:title) && object.send(:title)) ||
21
+ (object.respond_to?(:email) && object.send(:email)) ||
22
+ (object.respond_to?(:to_s) && object.send(:to_s)) ||
23
+ "#{object}"
24
+ end
25
+ end
26
+
27
+ def data_for(col)
28
+ value = if col.data
29
+ col.data.call(@resource, col)
30
+ elsif col.attribute.to_s =~ /^([\w]+)_id$/ && @resource.respond_to?($1.to_sym)
31
+ pretty_format(@resource.send($1))
32
+ else
33
+ pretty_format(@resource.send(col.attribute))
34
+ end
35
+ value ||= content_tag(:span, 'Empty', :class => 'empty')
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,147 @@
1
+ module SimpleAdmin
2
+ module FilterHelper
3
+ def filter_for(method, klass, options={})
4
+ options ||= {}
5
+ options[:as] ||= default_filter_type(klass, method)
6
+ return "" unless options[:as]
7
+ field_type = options.delete(:as)
8
+ content_tag :div, :class => "filter_form_field filter_#{field_type}" do
9
+ send("filter_#{field_type}_input", klass, method, options)
10
+ end
11
+ end
12
+
13
+ def filter_string_input(klass, method, options = {})
14
+ field_name = "#{method}_contains"
15
+
16
+ [ label(field_name, "Search #{method.to_s.titlecase}"),
17
+ text_field_tag(field_name, params[field_name] || '')
18
+ ].join("\n").html_safe
19
+ end
20
+
21
+ def filter_date_range_input(klass, method, options = {})
22
+ gt_field_name = "#{method}_gte"
23
+ lt_field_name = "#{method}_lte"
24
+
25
+ [ label(gt_field_name, method.to_s.titlecase),
26
+ filter_date_text_field(klass, gt_field_name),
27
+ " - ",
28
+ filter_date_text_field(klass, lt_field_name)
29
+ ].join("\n").html_safe
30
+ end
31
+
32
+ def filter_date_text_field(klass, method)
33
+ current_value = params[method] || ''
34
+ text_field_tag(method, current_value.respond_to?(:strftime) ? current_value.strftime("%Y-%m-%d") : current_value, :size => 12, :class => "datepicker", :max => 10)
35
+ end
36
+
37
+ def filter_numeric_input(klass, method, options = {})
38
+ filters = numeric_filters_for_method(method, options.delete(:filters) || default_numeric_filters)
39
+ current_filter = current_numeric_scope(klass, filters)
40
+ filter_select = select_tag '', options_for_select(filters, current_filter), :onchange => "document.getElementById('#{method}_numeric').name = '' + this.value + '';"
41
+ filter_input = text_field_tag(current_filter, params[current_filter] || '', :size => 10, :id => "#{method}_numeric")
42
+
43
+ [ label_tag(method), filter_select, " ", filter_input].join("\n").html_safe
44
+ end
45
+
46
+ def numeric_filters_for_method(method, filters)
47
+ filters.collect{|scope| [scope[0], [method,scope[1]].join("_") ] }
48
+ end
49
+
50
+ # Returns the scope for which we are currently searching. If no search is available
51
+ # it returns the first scope
52
+ def current_numeric_scope(klass, filters)
53
+ filters[1..-1].inject(filters.first){|a,b| params[b[1].to_sym] ? b : a }[1]
54
+ end
55
+
56
+ def default_numeric_filters
57
+ [['Equal To', 'eq'], ['Greater Than', 'gt'], ['Less Than', 'lt']]
58
+ end
59
+
60
+ def filter_select_input(klass, method, options = {})
61
+ association_name = method.to_s.gsub(/_id$/, '').to_sym
62
+ input_name = if reflection = reflection_for(klass, association_name)
63
+ if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
64
+ "#{association_name.to_s.singularize}_ids"
65
+ else
66
+ reflection.options[:foreign_key] || "#{association_name}_id"
67
+ end
68
+ else
69
+ association_name
70
+ end
71
+ input_name = (input_name + "_eq").to_sym
72
+ collection = find_collection_for_column(klass, association_name, options)
73
+ [ label(input_name, method.to_s.titlecase),
74
+ select_tag(input_name, options_for_select(collection, params[input_name]), :include_blank => options[:include_blank] || 'Any')
75
+ ].join("\n").html_safe
76
+ end
77
+
78
+ def find_collection_for_column(klass, column, options) #:nodoc:
79
+ collection = if options[:collection]
80
+ options.delete(:collection)
81
+ elsif reflection = reflection_for(klass, column)
82
+ options[:find_options] ||= {}
83
+ if conditions = reflection.options[:conditions]
84
+ options[:find_options][:conditions] = reflection.klass.merge_conditions(conditions, options[:find_options][:conditions])
85
+ end
86
+ reflection.klass.find(:all, options[:find_options])
87
+ else
88
+ boolean_collection(klass, column, options)
89
+ end
90
+ collection = collection.to_a if collection.is_a?(Hash)
91
+ collection.map { |o| [pretty_format(o), o.id] }
92
+ end
93
+
94
+ def boolean_collection(klass, column, options)
95
+ [['Yes', true], ['No', false]]
96
+ end
97
+
98
+ def filter_check_boxes_input(klass, method, options = {})
99
+ input_name = (generate_association_input_name(method).to_s + "_in").to_sym
100
+ collection = find_collection_for_column(method, options)
101
+ selected_values = klass.send(input_name) || []
102
+ checkboxes = template.content_tag :div, :class => "check_boxes_wrapper" do
103
+ collection.map do |c|
104
+ label = c.is_a?(Array) ? c.first : c
105
+ value = c.is_a?(Array) ? c.last : c
106
+ "<label><input type=\"checkbox\" name=\"#{input_name}[]\" value=\"#{value}\" #{selected_values.include?(value) ? "checked" : ""}/> #{label}</label>"
107
+ end.join("\n").html_safe
108
+ end
109
+
110
+ [ label(input_name, method.to_s.titlecase),
111
+ checkboxes
112
+ ].join("\n").html_safe
113
+ end
114
+
115
+ # Returns the default filter type for a given attribute
116
+ def default_filter_type(klass, method)
117
+ if column = column_for(klass, method)
118
+ case column.type
119
+ when :date, :datetime
120
+ return :date_range
121
+ when :string, :text
122
+ return :string
123
+ when :integer
124
+ return :select if reflection_for(klass, method.to_s.gsub('_id','').to_sym)
125
+ return :numeric
126
+ when :float, :decimal
127
+ return :numeric
128
+ end
129
+ end
130
+
131
+ if reflection = reflection_for(klass, method)
132
+ return :select if reflection.macro == :belongs_to && !reflection.options[:polymorphic]
133
+ end
134
+ end
135
+
136
+ # Returns the column for an attribute on the object being searched
137
+ # if it exists. Otherwise returns nil
138
+ def column_for(klass, method)
139
+ klass.columns_hash[method.to_s] if klass.respond_to?(:columns_hash)
140
+ end
141
+
142
+ # Returns the association reflection for the method if it exists
143
+ def reflection_for(klass, method)
144
+ klass.reflect_on_association(method) if klass.respond_to?(:reflect_on_association)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,60 @@
1
+ module SimpleAdmin
2
+ module HeaderHelper
3
+ def tabs
4
+ content_tag :ul, :id => 'tabs' do
5
+ SimpleAdmin.registered.collect do |interface|
6
+ content_tag :li, :id => interface.collection, :class => "#{'current' if @interface == interface}" do
7
+ link_to interface.collection.titlecase, send("simple_admin_#{interface.collection}_path".to_sym)
8
+ end
9
+ end.join.html_safe
10
+ end
11
+ end
12
+
13
+ def utility_nav
14
+ content_tag :p, :id => 'utility_nav' do
15
+ if SimpleAdmin.current_user_method && send(SimpleAdmin.current_user_method)
16
+ content = "".html_safe
17
+ content << content_tag(:span, send(SimpleAdmin.current_user_name_method), :class => "current_user") if SimpleAdmin.current_user_name_method
18
+ content << link_to("Logout", Rails.application.routes.url_helpers.logout_path) if Rails.application.routes.url_helpers.respond_to?(:logout_path)
19
+ content
20
+ end
21
+ end
22
+ end
23
+
24
+ def breadcrumbs
25
+ content_tag :span, :class => 'breadcrumb' do
26
+ SimpleAdmin::Breadcrumbs.parse(request.fullpath, params[:action]).collect do |crumb|
27
+ link_to(crumb.first, crumb.last) +
28
+ content_tag(:span, ' / ', :class => 'breadcrumb_sep')
29
+ end.join.html_safe
30
+ end
31
+ end
32
+
33
+ def action_items
34
+ content_tag :div, :class => "action_items" do
35
+ content = ""
36
+ # If we are currently showing, then check for edit and destroy action items
37
+ if params[:action].to_sym == :show
38
+ if controller.action_methods.include?('edit')
39
+ content << link_to("Edit #{@interface.member.titlecase}",
40
+ send("edit_simple_admin_#{@interface.member}_path", @object))
41
+ end
42
+ content << "&nbsp;"
43
+ if controller.action_methods.include?("destroy")
44
+ content << link_to("Delete #{@interface.member.titlecase}",
45
+ send("simple_admin_#{@interface.member}_path", @object),
46
+ :method => :delete, :confirm => "Are you sure you want to delete this?")
47
+ end
48
+ end
49
+ # If we are not showing an item or creating a new one, then check for new action items
50
+ unless [:new, :show].include?(params[:action].to_sym)
51
+ if controller.action_methods.include?('new')
52
+ content << link_to("New #{@interface.member.titlecase}",
53
+ send("new_simple_admin_#{@interface.member}_path"))
54
+ end
55
+ end
56
+ content.html_safe
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,12 @@
1
+ module SimpleAdmin
2
+ module PathHelper
3
+ def resource_path(res)
4
+ if res.new_record?
5
+ send("simple_admin_#{@interface.collection}_path")
6
+ else
7
+ send("simple_admin_#{@interface.member}_path", res)
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,9 @@
1
+ module SimpleAdmin
2
+ module SidebarHelper
3
+ def sidebars
4
+ @interface.sidebars_for(:index).each do |sidebar|
5
+ instance_exec(sidebar, &sidebar[:data])
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,39 @@
1
+ module SimpleAdmin
2
+ module TableHelper
3
+ def sortable_header_classes_for(col)
4
+ sort = current_sort
5
+ classes = []
6
+ classes << "sortable" if col.sortable
7
+ classes << "sorted-#{sort[1]}" if sort[0] == col.sort_key
8
+ classes.join(' ')
9
+ end
10
+
11
+ # Returns an array for the current sort order
12
+ # current_sort[0] #=> sort_key
13
+ # current_sort[1] #=> asc | desc
14
+ def current_sort
15
+ if params[:order] && params[:order] =~ /^([\w\_\.]+)_(desc|asc)$/
16
+ [$1,$2]
17
+ else
18
+ []
19
+ end
20
+ end
21
+
22
+ # Returns the order to use for a given sort key
23
+ #
24
+ # Default is to use 'desc'. If the current sort key is
25
+ # 'desc' it will return 'asc'
26
+ def order_for_sort_key(sort_key)
27
+ current_key, current_order = current_sort
28
+ return 'desc' unless current_key == sort_key
29
+ current_order == 'desc' ? 'asc' : 'desc'
30
+ end
31
+
32
+ def resource_actions(object)
33
+ links = link_to "View", send("simple_admin_#{@interface.member}_path", object), :class => "member_link view_link"
34
+ links += link_to "Edit", send("edit_simple_admin_#{@interface.member}_path", object), :class => "member_link edit_link"
35
+ links += link_to "Delete", send("simple_admin_#{@interface.member}_path", object), :method => :delete, :confirm => "Are you sure you want to delete this?", :class => "member_link delete_link"
36
+ links
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ module SimpleAdmin
2
+ module TitleHelper
3
+ def title
4
+ "#{page_title} | #{site_title}"
5
+ end
6
+
7
+ def site_title
8
+ SimpleAdmin::site_title
9
+ end
10
+
11
+ def page_title
12
+ options = @interface.options_for(params[:action].to_sym)
13
+ case options[:title]
14
+ when Proc
15
+ options[:title].call(@resource)
16
+ when Symbol
17
+ if @resource
18
+ @resource.send(optons[:title])
19
+ else
20
+ options[:title].to_s
21
+ end
22
+ when String
23
+ options[:title]
24
+ else
25
+ if @resource && @resource.new_record?
26
+ "New #{@interface.member.titleize}"
27
+ elsif @resource
28
+ "#{@interface.member.titleize}#{@resource.to_param.match(/\d+/) ? ' #' : ': '}#{@resource.to_param}"
29
+ else
30
+ @interface.collection.titlecase
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end