Skip to content

Commit

Permalink
More flexible reporting (#33)
Browse files Browse the repository at this point in the history
* Port over changes made for solidus

* Some naming changes, initializer and configuration

1. Standardize on report_category instead of using type and category alternatively.
2. Add initializer to configure list of reports to be used.
3. Reduce responsibility of report generation service to just generate reports

* Move config to nested subclass. Remove annual promotional cost report.

* Updated readme and gemspec

* Fixing formatting

* Adding section in readme for 'adding new reports'
  • Loading branch information
Nimish Mehta authored and bansalakhil committed May 18, 2017
1 parent cc38332 commit e22efb5
Show file tree
Hide file tree
Showing 65 changed files with 2,008 additions and 1,351 deletions.
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
source 'https://rubygems.org'

gem 'spree', github: 'spree/spree', branch: 'master'
gem 'spree_events_tracker', git: "https://github.com/vinsol/spree_events_tracker.git", branch: 'master'
gem 'spree', '~> 3.2'
gem 'spree_events_tracker', '~> 3.2'

gemspec
49 changes: 39 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@ When it comes to driving an Ecommerce business, knowing the right metrics and ac

This extension provides extensive and targeted reports for the Admin. Which products were viewed the most yesterday, which brand is most popular in a particular geography, which user is a consistent buyer and much more, all the reports a website owner could probably need are a click away!

Dependency
---------
you need to install [spree_events_tracker](https://github.com/vinsol-spree-contrib/spree_events_tracker) gem.


Features
--------
Elaborate reporting from the following categories are available:
* Financial Analysis - Involves reports around sales, payment methods and shipping etc
* Product Analysis - Insights of product purchase, abandoned cart etc
* Promotional analysis - Reports of promotional costs etc are available.
* Search Analysis - Search details reports.
* User Analysis - Includes elaborate user analysis.
* **Financial Analysis -** Involves reports around sales, payment methods and shipping etc
* **Product Analysis -** Insights of product purchase, abandoned cart etc
* **Promotional analysis -** Reports of promotional costs etc are available.
* **Search Analysis -** Search details reports.
* **User Analysis -** Includes elaborate user analysis.

**Other features include :**
* Search and Filter
Expand All @@ -31,13 +29,15 @@ Installation
1. Add spree_admin_insights to your Gemfile:

```ruby
gem 'spree_admin_insights', git: 'https://github.com/vinsol-spree-contrib/spree-admin-insights'
gem 'spree_events_tracker'
gem 'spree_admin_insights'
```

2. Bundle your dependencies and run the installation generator:

```shell
bundle
bundle exec rails g spree_events_tracker:install # Ensure event tracker files are installed
bundle exec rails g spree_admin_insights:install
```

Expand All @@ -49,6 +49,35 @@ Once installed it will automatically starts all data loging and statistical anal

To access these reports goto admin section and click on 'Insights' section in the vertical menu bar.


Adding new reports
-------------------

Create a class that inherits from `Spree::Report` and define a `report_query` method. If the report is to be paginated. it should define also define a method called `paginated` and set it to return `true` alongwith defining `paginated_report_query` and `record_count_query`. the `_query` methods should return objects that respond to `to_sql` which returns sql string for reporting query.

All reports need to define the following constants:
1. `SORTABLE_ATTRIBUTES`: Other attributes based on which reports can be sorted.
2. `DEFAULT_SORTABLE_ATTRIBUTE`: The attribute which is used by default to sort the report results.
3. `HEADERS`: The static header fields for report. Note time based fields are automatically added. Any field not declared here but available in observation will be ignored while displaying the report.
4. `SEARCH_ATTRIBUTES`: A hash containing the attributes and their name on frontend based on which report result can be filtered.

Additionally they need to define two nested classes. `Result` and `Result::Observation`.

`Result` class can inherit from `Spree::Report::Result` if it is a basic report or from `Spree::Report::TimedResult` if the result can be time scaled(i.e. changing reporting period changes the scale of report).

Similarly Observation class needs to inherit either from `Spree::Report::Observation` or `Spree::Report::TimedObservation`. It defines a macro call `observation_fields` which can be passed an array or hash with default values of fields which form one report item. Create a method of same name in Observation class for virtual fields which are not returned by queries. ie. average or for formatting db results.

`TimedResult` has 2 lifecycle methods which can be overriden for customizing the report output.
1. `build_empty_observations`: Generates empty observations which are later filled with datapoints returned by report query.
2. `populate_observations`: Fill the empty observations with data returned via query.

`TimedObservation` defines a describes? method which can be overriden to change where the query results gets copied to.

You can add charts to reports by calling `charts` with a list of classes representing the chart. Each chart implementing class gets the results in it's initializer and need to implement to_h method returning the json representation of chart.

Finally, register the report in initializer `solidus_admin_insights.rb` in its appropriate category or make a new category to make it available in admin dashboard.


Testing
-------

Expand All @@ -64,4 +93,4 @@ Credits

[![vinsol.com: Ruby on Rails, iOS and Android developers](http://vinsol.com/vin_logo.png "Ruby on Rails, iOS and Android developers")](http://vinsol.com)

Copyright (c) 2016 [vinsol.com](http://vinsol.com "Ruby on Rails, iOS and Android developers"), released under the New MIT License
Copyright (c) 2017 [vinsol.com](http://vinsol.com "Ruby on Rails, iOS and Android developers"), released under the New MIT License
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Paginator.prototype.removePagination = function(currentElement) {
sorted_attributes = this.tableSorter.fetchSortedAttribute(),
attribute = sorted_attributes[0],
sortOrder = sorted_attributes[1],
requestUrl = $element.data('url') + '&sort%5Battribute%5D=' + attribute + '&sort%5Btype%5D=' + sortOrder + '&' + $('#filter-search').serialize() + '&no_pagination=true';
requestUrl = $element.data('url') + '&sort%5Battribute%5D=' + attribute + '&sort%5Btype%5D=' + sortOrder + '&' + $('#filter-search').serialize() + '&paginate=false';
$(currentElement).attr('href', requestUrl);
_this.reportLoader.requestUrl = requestUrl;
$element.val('');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ ReportLoader.prototype.bindEvents = function() {
ReportLoader.prototype.resetFilters = function(event) {
event.preventDefault();
var $element = $(event.target),
noPagination = this.removePaginationButton.closest('span').hasClass('hide');
$element.attr('href', this.perPageSelector.data('url') + '&no_pagination=' + noPagination);
$element.data('url', this.perPageSelector.data('url') + '&no_pagination=' + noPagination);
paginated = !this.removePaginationButton.closest('span').hasClass('hide');
$element.attr('href', this.perPageSelector.data('url') + '&paginate=' + paginated);
$element.data('url', this.perPageSelector.data('url') + '&paginate=' + paginated);
this.loadChart($element);
this.searcherObject.clearSearchFields();
};
Expand Down Expand Up @@ -136,7 +136,7 @@ ReportLoader.prototype.fetchChartData = function(url, $selectedOption) {
$(object).removeClass('col-md-3').addClass('col-md-2');
});
}
_this.perPageSelector.data('url', data['request_path'] + '?type=' + data['report_type']);
_this.perPageSelector.data('url', data['request_path'] + '?report_category=' + data['report_category']);
_this.setDownloadLinksPath();
_this.searcherObject.refreshSearcher($selectedOption, data);
_this.paginatorObject.refreshPaginator(data);
Expand Down Expand Up @@ -187,7 +187,7 @@ ReportLoader.prototype.populateInsightsData = function(data) {
ReportLoader.prototype.setDownloadLinksPath = function($selectedOption) {
var _this = this;
$.each(this.downloadLinks, function() {
$(this).attr('href', $(this).data('url') + '?id=' + _this.$selectList.val() + '&no_pagination=true');
$(this).attr('href', $(this).data('url') + '?id=' + _this.$selectList.val() + '&paginate=false');
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ Searcher.prototype.refreshSearcher = function($selectedInsight, data) {
_this.setFormActions(_this.$filterForm, requestPath);

_this.$filterForm.on('submit', function() {
var noPagination = _this.reportLoader.removePaginationButton.closest('span').hasClass('hide');
var paginated = !_this.reportLoader.removePaginationButton.closest('span').hasClass('hide');
_this.addSearchStatus();
$.ajax({
type: "GET",
url: _this.$filterForm.attr('action'),
data: _this.$filterForm.serialize() + "&per_page=" + _this.reportLoader.pageSelector.find(':selected').attr('value') + '&no_pagination=' + noPagination,
data: _this.$filterForm.serialize() + "&per_page=" + _this.reportLoader.pageSelector.find(':selected').attr('value') + '&paginate=' + paginated,
dataType: 'json',
success: function(data) {
_this.clearFormFields();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ TableSorter.prototype.bindEvents = function() {
this.$insightsTableList.on('click', '#admin-insight .sortable-link', function() {
event.preventDefault();
var currentPage = _this.paginatorDiv.find('li.active a').html() - 1,
noPagination = _this.reportLoader.removePaginationButton.closest('span').hasClass('hide'),
requestPath = $(event.target).attr('href') + '&' + $('#filter-search').serialize() + '&page=' + currentPage + "&per_page=" + _this.reportLoader.pageSelector.find(':selected').attr('value') + '&no_pagination=' + noPagination;
paginated = !_this.reportLoader.removePaginationButton.closest('span').hasClass('hide'),
requestPath = $(event.target).attr('href') + '&' + $('#filter-search').serialize() + '&page=' + currentPage + "&per_page=" + _this.reportLoader.pageSelector.find(':selected').attr('value') + '&paginate=' + paginated;
_this.reportLoader.requestUrl = requestPath;

$.ajax({
Expand Down
83 changes: 50 additions & 33 deletions app/controllers/spree/admin/insights_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Spree
module Admin
class InsightsController < Spree::Admin::BaseController
before_action :ensure_report_exists, :set_default_pagination, only: [:show, :download]
before_action :set_reporting_period, only: [:index, :show, :download]
before_action :load_reports, only: [:index, :show]

def index
Expand All @@ -12,46 +13,29 @@ def index
end

def show
@headers, @stats, @total_pages, @search_attributes, @chart_json, @resource = ReportGenerationService.generate_report(
@report_name,
params.merge(@pagination_hash)
)

@report_data_json = {
current_page: params[:page] || 0,
headers: @headers,
report_type: params[:type],
request_path: request.path,
search_attributes: @search_attributes,
stats: @stats,
total_pages: @total_pages,
url: request.url,
searched_fields: params[:search],
per_page: @pagination_hash[:records_per_page],
chart_json: @chart_json,
pagination_required: !@resource.no_pagination?
}
report = ReportGenerationService.generate_report(@report_name, params.merge(@pagination_hash))

@report_data = shared_data.merge(report.to_h)
respond_to do |format|
format.html { render :index }
format.json { render json: @report_data_json }
format.json { render json: @report_data }
end
end

def download
@headers, @stats = ReportGenerationService.generate_report(@report_name, params.merge(@pagination_hash))
@report = ReportGenerationService.generate_report(@report_name, params.merge(@pagination_hash))

respond_to do |format|
format.csv do
send_data ReportGenerationService.download(@headers, @stats),
send_data ReportGenerationService.download(@report),
filename: "#{ @report_name.to_s }.csv"
end
format.xls do
send_data ReportGenerationService.download({ col_sep: "\t" }, @headers, @stats),
send_data ReportGenerationService.download(@report, { col_sep: "\t" }),
filename: "#{ @report_name.to_s }.xls"
end
format.text do
send_data ReportGenerationService.download(@headers, @stats),
send_data ReportGenerationService.download(@report),
filename: "#{ @report_name.to_s }.txt"
end
format.pdf do
Expand All @@ -65,27 +49,60 @@ def download
private
def ensure_report_exists
@report_name = params[:id].to_sym
unless ReportGenerationService::REPORTS[get_reports_type].include? @report_name
unless ReportGenerationService.report_exists?(get_report_category, @report_name)
redirect_to admin_insights_path, alert: Spree.t(:not_found, scope: [:reports])
end
end

def load_reports
@reports = ReportGenerationService::REPORTS[get_reports_type]
@reports = ReportGenerationService.reports_for_category(get_report_category)
end

def shared_data
{
current_page: params[:page] || 0,
report_category: params[:report_category],
request_path: request.path,
url: request.url,
searched_fields: params[:search],
}
end

def get_report_category
params[:report_category] = if params[:report_category]
params[:report_category].to_sym
else
session[:report_category].try(:to_sym) || ReportGenerationService.default_report_category
end
session[:report_category] = params[:report_category]
end

def get_reports_type
params[:type] = if params[:type]
params[:type].to_sym
def set_reporting_period
if params[:search].present?
if params[:search][:start_date] == ""
# When clicking on 'x' to remove the filter
params[:search][:start_date] = nil
else
params[:search][:start_date] = params[:search][:start_date] || session[:search_start_date]
end
if params[:search][:end_date] == ""
params[:search][:end_date] = nil
else
params[:search][:end_date] = params[:search][:end_date].presence || session[:search_end_date]
end
else
session[:report_category].try(:to_sym) || ReportGenerationService::REPORTS.keys.first
params[:search] = {}
params[:search][:start_date] = session[:search_start_date]
params[:search][:end_date] = session[:search_end_date]
end
session[:report_category] = params[:type]
session[:search_start_date] = params[:search][:start_date]
session[:search_end_date] = params[:search][:end_date]
end

def set_default_pagination
@pagination_hash = {}
if params[:no_pagination] != 'true'
@pagination_hash = { paginate: false }
unless params[:paginate] == 'false'
@pagination_hash[:paginate] = true
@pagination_hash[:records_per_page] = params[:per_page].try(:to_i) || Spree::Config[:records_per_page]
@pagination_hash[:offset] = params[:page].to_i * @pagination_hash[:records_per_page]
end
Expand Down
3 changes: 3 additions & 0 deletions app/models/spree/product_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Spree::Product.class_eval do
has_many :page_view_events, -> { viewed.product }, class_name: 'Spree::PageEvent', foreign_key: :target_id
end
3 changes: 3 additions & 0 deletions app/models/spree/promotion_action_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Spree::PromotionAction.class_eval do
has_one :adjustment, -> { promotion }, class_name: 'Spree::Adjustment', foreign_key: :source_id
end
4 changes: 4 additions & 0 deletions app/models/spree/return_authorization_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Spree::ReturnAuthorization.class_eval do
has_many :variants, through: :inventory_units
has_many :products, through: :variants
end
3 changes: 3 additions & 0 deletions app/models/spree/user_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Spree::User.class_eval do
has_many :spree_orders, class_name: 'Spree::Order'
end
58 changes: 0 additions & 58 deletions app/reports/spree/annual_promotional_cost_report.rb

This file was deleted.

Loading

0 comments on commit e22efb5

Please sign in to comment.