admin_data 1.1.12 → 1.1.13

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 (46) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile.lock +1 -1
  3. data/README.md +1 -1
  4. data/app/controllers/admin_data/analytics_controller.rb +38 -0
  5. data/app/controllers/admin_data/public_controller.rb +2 -0
  6. data/app/controllers/admin_data/search_controller.rb +10 -5
  7. data/app/helpers/admin_data/application_helper.rb +21 -0
  8. data/app/views/admin_data/analytics/_bar_chart.html.erb +39 -0
  9. data/app/views/admin_data/analytics/index.html.erb +41 -0
  10. data/app/views/admin_data/search/search/_advance_search_form.html.erb +1 -7
  11. data/app/views/admin_data/search/search/_listing.html.erb +4 -2
  12. data/app/views/admin_data/search/search/_search_form.html.erb +0 -1
  13. data/app/views/admin_data/shared/_secondary_navigation.html.erb +5 -0
  14. data/app/views/layouts/admin_data.html.erb +4 -1
  15. data/config/routes.rb +8 -5
  16. data/lib/admin_data/analytics.rb +176 -0
  17. data/lib/admin_data/exceptions.rb +5 -0
  18. data/lib/admin_data/search.rb +139 -137
  19. data/lib/admin_data/version.rb +1 -1
  20. data/lib/admin_data.rb +3 -0
  21. data/lib/public/images/sort_by_asc.jpg +0 -0
  22. data/lib/public/images/sort_by_desc.jpg +0 -0
  23. data/lib/public/images/sort_by_nothing.jpg +0 -0
  24. data/lib/public/javascripts/advance_search/sortby.js +14 -0
  25. data/lib/public/javascripts/analytics/report.js +7 -0
  26. data/lib/public/stylesheets/base.css +25 -1
  27. data/test/rails_root/Gemfile +15 -12
  28. data/test/rails_root/Gemfile.lock +8 -2
  29. data/test/rails_root/README.md +16 -8
  30. data/test/rails_root/app/models/car.rb +3 -0
  31. data/test/rails_root/config/application.rb +0 -27
  32. data/test/rails_root/config/cucumber.yml +0 -2
  33. data/test/rails_root/config/database.yml.mysql +22 -0
  34. data/test/rails_root/config/database.yml.pg +15 -0
  35. data/test/rails_root/config/{database.yml → database.yml.sqlite3} +1 -1
  36. data/test/rails_root/db/development.sqlite3 +0 -0
  37. data/test/rails_root/db/migrate/{20091030202259_create_users.rb → 20091030202259_create_tables.rb} +7 -1
  38. data/test/rails_root/db/schema.rb +6 -0
  39. data/test/rails_root/features/quick_search.feature +0 -11
  40. data/test/rails_root/lib/tasks/dbs.rake +30 -0
  41. data/test/rails_root/lib/tasks/sample_cars.rake +18 -0
  42. data/test/rails_root/test/unit/car_test.rb +100 -0
  43. metadata +30 -13
  44. data/app/views/admin_data/search/search/_sortby.html.erb +0 -19
  45. data/test/rails_root/features/advance_search/sort.feature +0 -16
  46. data/test/rails_root/test/performance/browsing_test.rb +0 -9
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ test/rails_root/db/*.sqlite3
15
15
  test/rails_root/.rvmrc
16
16
  test/rails_root/.bundle
17
17
  test/rails_root/.bundle/*
18
+ test/rails_root/config/database.yml
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- admin_data (1.1.12)
4
+ admin_data (1.1.13)
5
5
  will_paginate (= 3.0.pre2)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  #Test#
12
12
 
13
- <tt>cd _test/rails_root</tt> and read the instructions mentioned at README.md there.
13
+ <tt>cd test/rails_root</tt> and read the instructions mentioned at README.md there.
14
14
 
15
15
 
16
16
 
@@ -0,0 +1,38 @@
1
+ module AdminData
2
+ class AnalyticsController < ApplicationController
3
+
4
+ before_filter :get_class_from_params
5
+ before_filter :set_ivars
6
+
7
+ rescue_from AdminData::NoCreatedAtColumnException, :with => :render_no_created_at
8
+
9
+ def render_no_created_at
10
+ render :text => "Model #{@klass} does not have created_at column"
11
+ end
12
+
13
+ def daily
14
+ @chart_title = "#{@klass.name} records created in the last 30 days"
15
+
16
+ a = AdminData::Analytics.daily_report(@klass, Time.now)
17
+ @chart_data_s = a.map {|e| e.last }.join(', ')
18
+ @chart_data_x_axis = a.map {|e| e.first}.join(', ')
19
+ render :action => 'index'
20
+ end
21
+
22
+ def monthly
23
+ @chart_title = "#{@klass.name} rercords created last year"
24
+ a = AdminData::Analytics.monthly_report(@klass, Time.now)
25
+ @chart_data_s = a.map {|e| e.last }.join(', ')
26
+ @chart_data_x_axis = a.map {|e| e.first}.join(', ')
27
+ render :action => 'index'
28
+ end
29
+
30
+ def set_ivars
31
+ @chart_width = 950
32
+ @chart_height = 400
33
+ @chart_h_axis_title = ''
34
+ @chart_legend_name = 'Created'
35
+ end
36
+
37
+ end
38
+ end
@@ -15,6 +15,8 @@ module AdminData
15
15
  content_type = "text/javascript"
16
16
  when /\.png$/i
17
17
  content_type = "image/png"
18
+ when /\.jpg$/i
19
+ content_type = "image/jpg"
18
20
  else
19
21
  render :nothing => true, :status => 404 and return
20
22
  end
@@ -1,5 +1,3 @@
1
- require File.join(AdminData::LIBPATH, 'admin_data', 'search')
2
-
3
1
  class SearchAction
4
2
  attr_accessor :relation, :success_message
5
3
 
@@ -27,12 +25,13 @@ module AdminData
27
25
 
28
26
  layout 'search'
29
27
 
30
- include Search
28
+ include AdminData::Search
31
29
 
32
30
  before_filter :get_class_from_params
33
31
  before_filter :ensure_valid_children_klass, :only => [:quick_search]
34
32
  before_filter :ensure_is_authorized_for_update_opration, :only => [:advance_search]
35
33
  before_filter :set_column_type_info, :only => [:advance_search]
34
+ before_filter :handle_sorting
36
35
 
37
36
 
38
37
  def quick_search
@@ -48,7 +47,7 @@ module AdminData
48
47
  @records = has_many_proxy.send(:paginate, h)
49
48
  else
50
49
  params[:query] = params[:query].strip unless params[:query].blank?
51
- cond = build_quick_search_conditions(@klass, params[:query])
50
+ cond = build_quick_search_condition(@klass, params[:query])
52
51
  h = { :page => params[:page], :per_page => per_page, :order => order, :conditions => cond }
53
52
  @records = @klass.unscoped.paginate(h)
54
53
  end
@@ -58,7 +57,7 @@ module AdminData
58
57
 
59
58
  def advance_search
60
59
  @page_title = "Advance search #{@klass.name.underscore}"
61
- hash = build_advance_search_conditions(@klass, params[:adv_search])
60
+ hash = build_advance_search_condition(@klass, params[:adv_search])
62
61
  relation = hash[:cond]
63
62
  errors = hash[:errors]
64
63
  order = default_order
@@ -134,5 +133,11 @@ module AdminData
134
133
  @column_type_info = "{#{column_type_info}}"
135
134
  end
136
135
 
136
+ def handle_sorting
137
+ sort_order = params[:sortby] || 'id desc'
138
+ @sort_by_column_name, @sort_order = sort_order.split
139
+ @sort_css = @sort_order == 'asc' ? 'sort_by_asc' : 'sort_by_desc'
140
+ end
141
+
137
142
  end
138
143
  end
@@ -1,6 +1,27 @@
1
1
  module AdminData
2
2
  module ApplicationHelper
3
3
 
4
+ def get_sort_order(column)
5
+ if column == @sort_by_column_name && @sort_order == 'desc'
6
+ "#{column} asc"
7
+ else
8
+ "#{column} desc"
9
+ end
10
+ end
11
+
12
+ def get_sort_title_with_url(column, klass)
13
+ order = get_sort_order(column)
14
+ link_to column_title(klass, column), admin_data_search_path(:klass => klass, :query => params[:query], :sortby => order)
15
+ end
16
+
17
+ def get_sort_class(column)
18
+ sort_class = 'sortable'
19
+ if column == @sort_by_column_name
20
+ sort_class << ' ' + @sort_css
21
+ end
22
+ sort_class
23
+ end
24
+
4
25
  def parent_layout(layout)
5
26
  @_content_for[:layout] = self.output_buffer
6
27
  self.output_buffer = render(:file => "layouts/#{layout}")
@@ -0,0 +1,39 @@
1
+
2
+ <%= javascript_include_tag "https://www.google.com/jsapi" %>
3
+ <script>
4
+ google.load('visualization', '1', {'packages':['corechart']});
5
+ google.setOnLoadCallback(drawChart);
6
+
7
+ function drawChart() {
8
+
9
+ var data = new google.visualization.DataTable();
10
+ var raw_data = [['<%= chart_legend_name %>', <%= chart_data_s %>]];
11
+
12
+ var years = [<%= chart_data_x_axis %>];
13
+
14
+ data.addColumn('string', 'Year');
15
+ for (var i = 0; i < raw_data.length; ++i) {
16
+ data.addColumn('number', raw_data[i][0]);
17
+ }
18
+
19
+ data.addRows(years.length);
20
+
21
+ for (var j = 0; j < years.length; ++j) {
22
+ data.setValue(j, 0, years[j].toString());
23
+ }
24
+
25
+ for (var i = 0; i < raw_data.length; ++i) {
26
+ for (var j = 1; j < raw_data[i].length; ++j) {
27
+ data.setValue(j-1, i+1, raw_data[i][j]);
28
+ }
29
+ }
30
+
31
+ // Create and draw the visualization.
32
+ var chart = new google.visualization.ColumnChart(document.getElementById('chart_div'));
33
+ chart.draw(data,
34
+ { title: '<%= chart_title %>',
35
+ width: <%= chart_width %>,
36
+ height: <%= chart_height %>,
37
+ hAxis: {title: '<%= chart_h_axis_title %>'}});
38
+ }
39
+ </script>
@@ -0,0 +1,41 @@
1
+ <%= render 'bar_chart', :chart_title => @chart_title,
2
+ :chart_h_axis_title => @chart_h_axis_title,
3
+ :chart_legend_name => @chart_legend_name,
4
+ :chart_data_s => @chart_data_s,
5
+ :chart_data_x_axis => @chart_data_x_axis,
6
+ :chart_width => @chart_width,
7
+ :chart_height => @chart_height %>
8
+
9
+ <div id="main">
10
+
11
+ <%= breadcrum do %>
12
+ <%= link_to @klass, admin_data_search_path(:klass => @klass.name) %>
13
+ <% end %>
14
+
15
+ <%= render 'admin_data/shared/flash_message', :model => @model %>
16
+
17
+ <div class="block" id="block-tables">
18
+ <div class="secondary-navigation">
19
+ <%= render 'admin_data/shared/secondary_navigation', :klass => @klass %>
20
+ <div class="clear"></div>
21
+ </div>
22
+ <div class="content rounded">
23
+ <div>
24
+ <h2 class="title" style='float:left;'>Analytics</h2>
25
+
26
+ <div style='float:left;margin-top:18px;margin-left:20px;'>
27
+ <%= radio_button_tag :report_type, admin_data_daily_analytics_path(:klass => @klass), params[:action] == 'daily' %> Daily
28
+ &nbsp;
29
+ <%= radio_button_tag :report_type, admin_data_monthly_analytics_path(:klass => @klass), params[:action] == 'monthly' %> Monthly
30
+ </div>
31
+ <div style='clear:both;'></div>
32
+ </div>
33
+
34
+
35
+ <div class="inner">
36
+ <div id="chart_div"></div>
37
+
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
@@ -6,15 +6,9 @@
6
6
  <div id='advance_search' class='search_box'>
7
7
  <table id='advance_search_table' class='advtable'>
8
8
  </table>
9
+ <input type="hidden" id="advance_search_sortby" name="sortby" value="<%=params[:sortby]%>" />
9
10
 
10
11
  <div class='sortby_umbrella'>
11
- <div class='group'>
12
- <div class='sortby_text'>Sort by</div>
13
- <select name='sortby' id='sortby'>
14
- <%= AdminData::Util.build_sort_options(klass,params[:sortby]).html_safe %>
15
- </select>
16
- <br />
17
- </div>
18
12
  <div class='clear'></div>
19
13
  <input type="submit" value="Search" class='submit_search' />
20
14
  </div>
@@ -10,9 +10,11 @@
10
10
  <table cellspacing="3" cellpadding="3" id="view_table" class='table colorize'>
11
11
 
12
12
  <thead>
13
- <tr>
13
+ <tr class='thead'>
14
14
  <% columns_order(klass).each do |column| %>
15
- <th> <%= column_title(klass, column) %> </th>
15
+ <th class='<%=get_sort_class(column)%>' data-sortby='<%=get_sort_order(column)%>'>
16
+ <%= get_sort_title_with_url(column, klass) %>
17
+ </th>
16
18
  <% end %>
17
19
  </tr>
18
20
  </thead>
@@ -4,7 +4,6 @@
4
4
  <div id='quick_search'>
5
5
  <label class='label keyword_label'>Search keyword</label>
6
6
  <input type="text" id="quick_search_input" name="query" value="<%=params[:query]%>" />
7
- <%= render :partial => 'admin_data/search/search/sortby', :locals => {:klass => klass} %>
8
7
  <div class='clear'></div>
9
8
  <%= submit_tag 'Search', :name => nil, :disable_with => 'searching ...', :class => 'submit_search' %>
10
9
  </div>
@@ -1,6 +1,7 @@
1
1
  <%
2
2
  quick_search_tab_active = params[:action] == 'quick_search' ? 'active' : ''
3
3
  advance_search_tab_active = params[:action] == 'advance_search' ? 'active' : ''
4
+ analytics_tab_active = params[:controller] == 'admin_data/analytics' ? 'active' : ''
4
5
  table_structure_tab_active = params[:controller] == 'admin_data/table_structure' ? 'active' : ''
5
6
  add_new_record_tab_active = %w(new create).include?(params[:action]) ? 'active' : ''
6
7
  %>
@@ -14,6 +15,10 @@
14
15
  <%= link_to 'Advance Search', admin_data_advance_search_path(:klass => klass.name.underscore) %>
15
16
  </li>
16
17
 
18
+ <li class="<%=analytics_tab_active%>">
19
+ <%= link_to 'Analytics', admin_data_daily_analytics_path(:klass => klass.name.underscore) %>
20
+ </li>
21
+
17
22
  <li class="<%=table_structure_tab_active%>">
18
23
  <%= link_to 'Table Structure', admin_data_table_structure_path(:klass => klass.name.underscore) %>
19
24
  </li>
@@ -29,8 +29,11 @@
29
29
  'advance_search/act_on_result',
30
30
  'advance_search/build_first_row',
31
31
  'advance_search/event_bindings',
32
- 'advance_search/trigger_submit_on_domready'
32
+ 'advance_search/trigger_submit_on_domready',
33
+ 'advance_search/sortby'
33
34
  ) %>
35
+ <% elsif params[:controller] == 'admin_data/analytics' %>
36
+ <%= javascript_include_tag('analytics/report') %>
34
37
  <% end %>
35
38
 
36
39
  <%= csrf_meta_tag %>
data/config/routes.rb CHANGED
@@ -19,15 +19,18 @@ Rails.application.routes.draw do
19
19
  match '/jstest', :to => :jstest, :as => :jstest
20
20
  end
21
21
 
22
- match '/table_structure/:klass' => "table_structure#index", :as => :table_structure
22
+ match '/table_structure/:klass' => "table_structure#index", :as => :table_structure
23
23
 
24
- match '/quick_search/:klass' => "search#quick_search", :as => :search
24
+ match '/quick_search/:klass' => "search#quick_search", :as => :search
25
25
 
26
- match '/advance_search/:klass' => "search#advance_search", :as => :advance_search
26
+ match '/advance_search/:klass' => "search#advance_search", :as => :advance_search
27
27
 
28
- match '/feed/:klasss' => "feed#index", :defaults => { :format =>'rss' }, :as => :feed
28
+ match '/analytics/daily/:klass' => "analytics#daily", :as => :daily_analytics
29
+ match '/analytics/monthly/:klass' => "analytics#monthly", :as => :monthly_analytics
29
30
 
30
- match '/public/*file' => "public#serve", :as => :public
31
+ match '/feed/:klasss' => "feed#index", :defaults => { :format =>'rss' }, :as => :feed
32
+
33
+ match '/public/*file' => "public#serve", :as => :public
31
34
 
32
35
  root :to => "home#index"
33
36
  end
@@ -0,0 +1,176 @@
1
+ require "active_support/all"
2
+
3
+ module AdminData
4
+ module Analytics
5
+
6
+ # a utility class to handle date interpolation for different databases
7
+ class Dater
8
+ attr_accessor :adapter, :type
9
+
10
+ def initialize(adapter, type = 'daily')
11
+ @adapter = adapter
12
+ @type = type
13
+ end
14
+
15
+ def date_select_key
16
+ "date_data"
17
+ end
18
+
19
+ def group_by_key
20
+ if adapter =~ /postgresql/i
21
+ self.type == 'monthly' ? "date_part('year', created_at), date_part('month', created_at)" : "date_data"
22
+ elsif adapter =~ /mysql/i
23
+ self.type == 'monthly' ? "YEAR(created_at), MONTH(created_at)" : "date_data"
24
+ else
25
+ self.type == 'monthly' ? "strftime('%Y', created_at), strftime('%m', created_at)" : "date_data"
26
+ end
27
+ end
28
+
29
+ def count_select_key
30
+ "count_data"
31
+ end
32
+
33
+ def count_function
34
+ "count(*) as count_data"
35
+ end
36
+
37
+ def date_select_function
38
+ self.type == 'monthly' ? date_select_function_monthly : date_select_function_daily
39
+ end
40
+
41
+ private
42
+
43
+ def date_select_function_monthly
44
+ if adapter =~ /mysql/i
45
+ "MONTH(created_at) as date_data"
46
+ elsif adapter =~ /postgresql/i
47
+ "date_part('month', created_at) as date_data"
48
+ else
49
+ "strftime('%m', created_at) as date_data"
50
+ end
51
+ end
52
+
53
+ def date_select_function_daily
54
+ if adapter =~ /mysql/i
55
+ "date_format(created_at, '%Y-%m-%d') as date_data"
56
+ else
57
+ "date(created_at) as date_data"
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ def self.monthly_report(klass, end_date)
64
+ begin_date = end_date.ago(1.year)
65
+ raise "begin_date should not be after end_date" if begin_date > end_date
66
+ raise AdminData::NoCreatedAtColumnException unless klass.columns.find {|r| r.name == 'created_at'}
67
+
68
+ begin_date = begin_date.beginning_of_day
69
+ end_date = end_date.end_of_day
70
+
71
+ dater = Dater.new(ActiveRecord::Base.connection.adapter_name, 'monthly')
72
+
73
+ query = klass.unscoped
74
+ query = query.where(["created_at >= ?", begin_date])
75
+ query = query.where(["created_at <= ?", end_date])
76
+ query = query.group(dater.group_by_key)
77
+ query = query.select(dater.date_select_function)
78
+ query = query.select(dater.count_function)
79
+ debug "sql: " + query.to_sql
80
+ result = query.all
81
+ debug "sql result: #{result.inspect}"
82
+
83
+ extract_data_from_result_set_monthly(result, dater, begin_date, end_date)
84
+ end
85
+
86
+ def self.extract_data_from_result_set_monthly(result, dater, begin_date, end_date)
87
+ debug "extracting from result set now"
88
+ result_hash = {}
89
+ result.each do |record|
90
+ result_hash.merge!(record[dater.date_select_key].to_i => record[dater.count_select_key])
91
+ end
92
+
93
+ debug "result_hash.inspect is #{result_hash.inspect}"
94
+
95
+ current_month = begin_date.strftime('%m').to_i
96
+ months_order = (1..current_month).to_a.reverse + (current_month..12).to_a.reverse
97
+ months_order.uniq!.reverse!
98
+
99
+ debug "months order is #{months_order.inspect}"
100
+
101
+ final_hash = ActiveSupport::OrderedHash.new
102
+ months_order.each do |month|
103
+ month = month.to_i #sqlite3 has months as 03 instead of 3
104
+ m = Time.now.change(:month => month)
105
+ m = m.ago(1.year) if month > current_month
106
+
107
+ key = m.strftime('%b-%Y')
108
+ key = "'#{key}'"
109
+ if dater.adapter =~ /postgresql/i
110
+ value = result_hash[month]
111
+ elsif dater.adapter =~ /mysql/i
112
+ value = result_hash[month]
113
+ else
114
+ value = result_hash[month]
115
+ end
116
+ value = value.to_i
117
+ debug "month: #{month} key: #{key} value: #{value}"
118
+ final_hash.merge!(key => value)
119
+ end
120
+
121
+ final_hash.to_a.tap {|e| debug e.inspect }
122
+ end
123
+
124
+ def self.daily_report(klass, end_date)
125
+ begin_date = end_date.ago(1.month)
126
+ raise "begin_date should not be after end_date" if begin_date > end_date
127
+ raise AdminData::NoCreatedAtColumnException unless klass.columns.find {|r| r.name == 'created_at'}
128
+
129
+ begin_date = begin_date.beginning_of_day
130
+ end_date = end_date.end_of_day
131
+
132
+ dater = Dater.new(ActiveRecord::Base.connection.adapter_name)
133
+
134
+ query = klass.unscoped
135
+ query = query.where(["created_at >= ?", begin_date])
136
+ query = query.where(["created_at <= ?", end_date])
137
+ query = query.group(dater.group_by_key)
138
+ query = query.select(dater.date_select_function)
139
+ query = query.select(dater.count_function)
140
+ debug "sql: " + query.to_sql
141
+ result = query.all
142
+ debug "sql result: #{result.inspect}"
143
+
144
+ extract_data_from_result_set_daily(result, dater, begin_date, end_date).tap {|e| debug "formatted output: "+e.inspect}
145
+ end
146
+
147
+ def self.extract_data_from_result_set_daily(result, dater, begin_date, end_date)
148
+ count = result.map {|r| r[dater.count_select_key] }
149
+ dates = result.map {|r| r[dater.date_select_key] }
150
+
151
+ debug "count is: #{count.inspect}"
152
+ debug "dates is: #{dates.inspect}"
153
+
154
+ final_output= []
155
+
156
+ while(begin_date) do
157
+ s = begin_date.strftime('%Y-%m-%d')
158
+ final_count = if index = dates.index(s)
159
+ count[index].to_i
160
+ else
161
+ 0
162
+ end
163
+ final_output << ["'#{s}'", final_count]
164
+ begin_date = begin_date.tomorrow
165
+ break if begin_date > end_date
166
+ end
167
+ final_output
168
+ end
169
+
170
+ def self.debug(msg)
171
+ puts msg
172
+ end
173
+
174
+ end
175
+
176
+ end
@@ -0,0 +1,5 @@
1
+ module AdminData
2
+
3
+ class NoCreatedAtColumnException < Exception; end
4
+
5
+ end