1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-19 13:54:25 +01:00

Merge branch 'xls' into dev

Conflicts:
	CHANGELOG.md
	app/helpers/application_helper.rb
	config/locales/en.yml
	config/locales/fr.yml
This commit is contained in:
Sylvain 2016-07-25 16:56:22 +02:00
commit 74154cf1f3
29 changed files with 710 additions and 110 deletions

View File

@ -20,6 +20,7 @@
- Fix a bug: event category disappear when editing the event
- [TODO DEPLOY] `rake fablab:es_add_event_filters`
- [TODO DEPLOY] `rake db:migrate`
- [TODO DEPLOY] `bundle install`
## v2.3.0 2016 June 28

View File

@ -144,3 +144,8 @@ gem 'openlab_ruby'
gem 'api-pagination'
gem 'has_secure_token'
gem 'apipie-rails'
# XLS files generation
gem 'rubyzip', '~> 1.1.0'
gem 'axlsx', '2.1.0.pre'
gem 'axlsx_rails'

View File

@ -54,6 +54,13 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
axlsx (2.1.0.pre)
htmlentities (~> 4.3.1)
nokogiri (>= 1.4.1)
rubyzip (~> 1.1.7)
axlsx_rails (0.4.0)
axlsx (>= 2.0.1)
rails (>= 3.1)
bcrypt (3.1.10)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
@ -174,6 +181,7 @@ GEM
highline (1.7.1)
hike (1.2.3)
hitimes (1.2.2)
htmlentities (4.3.4)
http (0.6.4)
http_parser.rb (~> 0.6.0)
http-cookie (1.0.2)
@ -325,6 +333,7 @@ GEM
netrc (~> 0.7)
rolify (4.0.0)
ruby-progressbar (1.7.5)
rubyzip (1.1.7)
rufus-scheduler (3.0.9)
tzinfo
rvm-capistrano (1.5.6)
@ -440,6 +449,8 @@ DEPENDENCIES
api-pagination
apipie-rails
awesome_print
axlsx (= 2.1.0.pre)
axlsx_rails
bootstrap-sass
byebug
capistrano
@ -488,6 +499,7 @@ DEPENDENCIES
recurrence
responders (~> 2.0)
rolify
rubyzip (~> 1.1.0)
rvm-capistrano
sass-rails (= 5.0.1)
sdoc (~> 0.4.0)

View File

@ -531,6 +531,11 @@ See https://angular-ui.github.io/bootstrap/#uibdateparser-s-format-codes for a l
**BEWARE**: years format with less than 4 digits will result in problems because the system won't be able to distinct dates with the same less significant digits, eg. 50 could mean 1950 or 2050.
EXCEL_DATE_FORMAT
Date format for dates shown in exported Excel files (eg. statistics)
See https://support.microsoft.com/en-us/kb/264372 for a list a available formats.
<a name="i18n-apply"></a>
#### Applying changes

View File

@ -142,3 +142,7 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
}]).constant('angularMomentConfig', {
timezone: Fablab.timezone
});
angular.isUndefinedOrNull = function(val) {
return angular.isUndefined(val) || val === null
};

View File

@ -1,7 +1,9 @@
'use strict'
Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", "Statistics", "es", "Member", '_t', 'membersPromise', 'statisticsPromise'
, ($scope, $state, $rootScope, Statistics, es, Member, _t, membersPromise, statisticsPromise) ->
Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", '$uibModal', "Export", "es", "Member", '_t', 'membersPromise', 'statisticsPromise'
, ($scope, $state, $rootScope, $uibModal, Export, es, Member, _t, membersPromise, statisticsPromise) ->
@ -56,6 +58,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
## default: results are not sorted
$scope.sorting =
ca: 'none'
date: 'desc'
## active tab will be set here
$scope.selectedIndex = null
@ -148,6 +151,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
$scope.customFilter.value = null
$scope.customFilter.exclude = false
$scope.sorting.ca = 'none'
$scope.sorting.date = 'desc'
buildCustomFiltersList()
refreshStats()
@ -271,6 +275,33 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
##
# Open a modal dialog asking the user for details about exporting the statistics tables to an excel file
##
$scope.exportToExcel = ->
options =
templateUrl: '<%= asset_path "admin/statistics/export.html" %>'
size: 'sm'
controller: 'ExportStatisticsController'
resolve:
dates: ->
start: $scope.datePickerStart.selected
end: $scope.datePickerEnd.selected
query: ->
custom = buildCustomFilterQuery()
buildElasticDataQuery($scope.type.active.key, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
index: ->
key: $scope.selectedIndex.es_type_key
type: ->
key: $scope.type.active.key
$uibModal.open options
.result['finally'](null).then (info)->
console.log(info)
### PRIVATE SCOPE ###
##
@ -308,12 +339,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
$scope.sumStat = 0
$scope.totalHits = null
$scope.searchDate = new Date()
custom = null
if $scope.customFilter.criterion and $scope.customFilter.criterion.key and $scope.customFilter.value
custom = {}
custom.key = $scope.customFilter.criterion.key
custom.value = $scope.customFilter.value
custom.exclude = $scope.customFilter.exclude
custom = buildCustomFilterQuery()
queryElasticStats $scope.selectedIndex.es_type_key, $scope.type.active.key, custom, (res, err)->
if (err)
console.error("[statisticsController::refreshStats] Unable to refresh due to "+err)
@ -485,8 +511,138 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
##
# Build and return an object according to the custom filter set by the user, used to request elasticsearch
# @return {Object|null}
##
buildCustomFilterQuery = ->
custom = null
if !angular.isUndefinedOrNull($scope.customFilter.criterion) and
!angular.isUndefinedOrNull($scope.customFilter.criterion.key) and
!angular.isUndefinedOrNull($scope.customFilter.value)
custom = {}
custom.key = $scope.customFilter.criterion.key
custom.value = $scope.customFilter.value
custom.exclude = $scope.customFilter.exclude
custom
# init the controller (call at the end !)
initialize()
]
Application.Controllers.controller 'ExportStatisticsController', [ '$scope', '$uibModalInstance', 'dates', 'query', 'index', 'type', 'CSRF'
, ($scope, $uibModalInstance, dates, query, index, type, CSRF) ->
## Retrieve Anti-CSRF tokens from cookies
CSRF.setMetaTags()
## Bindings for date range
$scope.dates = dates
## Body of the query to export
$scope.query = JSON.stringify(query)
## API URL where the form will be posted
$scope.actionUrl = '/stats/'+index.key+'/export'
## Key of the current search' statistic type
$scope.typeKey = type.key
## Form action on the above URL
$scope.method = "post"
## Anti-CSRF token to inject into the download form
$scope.csrfToken = angular.element('meta[name="csrf-token"]')[0].content
## Binding of the export type (global / current)
$scope.export =
type: 'current'
## datePicker parameters for interval beginning
$scope.exportStart =
format: Fablab.uibDateFormat
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().subtract(1, 'day').toDate()
options:
startingDay: Fablab.weekStartingDay
## datePicker parameters for interval ending
$scope.exportEnd =
format: Fablab.uibDateFormat
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().subtract(1, 'day').toDate()
options:
startingDay: Fablab.weekStartingDay
##
# Callback to open the datepicker (interval start)
# @param $event {Object} jQuery event object
##
$scope.toggleStartDatePicker = ($event) ->
$scope.exportStart.opened = !$scope.exportStart.opened
##
# Callback to open the datepicker (interval end)
# @param $event {Object} jQuery event object
##
$scope.toggleEndDatePicker = ($event) ->
$scope.exportEnd.opened = !$scope.exportEnd.opened
##
# Callback when exchanging the export type between 'global' and 'current view'
# Adjust the query and the requesting url according to this type.
##
$scope.setRequest = ->
if $scope.export.type == 'global'
$scope.actionUrl = '/stats/global/export'
$scope.query = JSON.stringify(
"query":
"bool":
"must": [
{
"range":
"date":
"gte": moment($scope.dates.start).format()
"lte": moment($scope.dates.end).format()
}
]
)
else
$scope.actionUrl = '/stats/'+index.key+'/export'
$scope.query = JSON.stringify(query)
##
# Callback to close the modal, telling the caller what is exported
##
$scope.exportData = ->
info =
type: $scope.export.type
if info.type == 'global'
info.dates = $scope.dates
$uibModalInstance.close(info)
##
# Callback to cancel the export and close the modal
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]

View File

@ -0,0 +1,6 @@
'use strict'
Application.Services.factory 'Export', ["$http", ($http)->
stats: (scope, query) ->
$http.post('/stats/'+scope+'/export', query)
]

View File

@ -31,13 +31,13 @@
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.members_new" translate>{{ 'add_a_new_member' }}</button>
<div class="pull-right">
<a class="btn btn-default" ng-href="api/members/export_members.xls" target="_blank">
<a class="btn btn-default" ng-href="api/members/export_members.xlsx" target="_blank">
<i class="fa fa-file-excel-o"></i> {{ 'members' | translate }}
</a>
<a class="btn btn-default" ng-href="api/members/export_subscriptions.xls" target="_blank" ng-if="!fablabWithoutPlans">
<a class="btn btn-default" ng-href="api/members/export_subscriptions.xlsx" target="_blank" ng-if="!fablabWithoutPlans">
<i class="fa fa-file-excel-o"></i> {{ 'subscriptions' | translate }}
</a>
<a class="btn btn-default" ng-href="api/members/export_reservations.xls" target="_blank">
<a class="btn btn-default" ng-href="api/members/export_reservations.xlsx" target="_blank">
<i class="fa fa-file-excel-o"></i> {{ 'reservations' | translate }}
</a>
</div>

View File

@ -0,0 +1,76 @@
<div class="modal-header">
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
<h1 translate>{{ 'export_statistics_to_excel' }}</h1>
</div>
<div class="modal-body">
<form>
<div class="radio">
<label><input type="radio" name="scope" ng-model="export.type" value="global" ng-change="setRequest()">{{ 'export_all_statistics' | translate }}</label>
</div>
<div ng-show="export.type == 'global'">
<ul class="list-unstyled">
<li class="row">
<span class="col-md-4" translate>{{ 'start' }}</span>
<div class="input-group black col-md-7 m-r" id="date_pick_start">
<input type="text"
class="form-control"
uib-datepicker-popup="{{exportEnd.format}}"
ng-model="dates.start"
name="startDate"
is-open="exportStart.opened"
min-date="exportStart.minDate"
max-date="exportStart.maxDate"
datepicker-options="exportStart.options"
show-button-bar="false"
placeholder="{{ 'start' | translate }}"
ng-click="toggleStartDatePicker($event)"
ng-change="setRequest()"
required="required"/>
<span class="input-group-btn">
<button type="button" class="btn btn-default btn-search-datepicker" ng-click="toggleStartDatePicker($event)">
<i class="glyphicon glyphicon-calendar"></i>
</button>
</span>
</div>
</li>
<li class="row">
<span class="col-md-4" translate>{{ 'end' }}</span>
<div class="input-group black col-md-7 m-r" id="date_pick_end">
<input type="text"
class="form-control"
uib-datepicker-popup="{{exportEnd.format}}"
ng-model="dates.end"
name="endDate"
is-open="exportEnd.opened"
min-date="exportEnd.minDate"
max-date="exportEnd.maxDate"
datepicker-options="datePickerEnd.options"
show-button-bar="false"
placeholder="{{ 'end' | translate }}"
ng-click="toggleEndDatePicker($event)"
ng-change="setRequest()"
required="required"/>
<span class="input-group-btn">
<button type="button" class="btn btn-default btn-search-datepicker" ng-click="toggleEndDatePicker($event)">
<i class="glyphicon glyphicon-calendar"></i>
</button>
</span>
</div>
</li>
</ul>
</div>
<div class="radio">
<label><input type="radio" name="scope" ng-model="export.type" value="current" ng-change="setRequest()">{{ 'export_the_current_search_results' | translate }}</label>
</div>
</form>
</div>
<div class="modal-footer">
<form role=form" ng-submit="exportData()" name="exportForm" method="post" action="{{ actionUrl }}" class="inline">
<input name="authenticity_token" type="hidden" ng-value="csrfToken"/>
<input name="_method" type="hidden" ng-value="method"/>
<input name="type_key" type="hidden" ng-value="typeKey"/>
<input name="body" type="hidden" ng-value="query"/>
<input type="submit" class="btn btn-info" value="{{ 'export' | translate }}" formtarget="_blank"/>
</form>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>

View File

@ -12,6 +12,7 @@
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper">
<a class="btn btn-lg btn-default rounded m-t-sm text-sm" ng-click="exportToExcel()"><i class="fa fa-file-excel-o"></i></a>
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm upper text-sm" ui-sref="app.admin.stats_graphs" role="button"><i class="fa fa-line-chart"></i> {{ 'evolution' | translate }}</a>
</section>
</div>
@ -287,4 +288,3 @@
</div>
</section>

View File

@ -91,30 +91,24 @@ class API::MembersController < API::ApiController
# export subscriptions
def export_subscriptions
authorize :export
@datas = Subscription.includes(:plan, :user).all
respond_to do |format|
format.html
format.xls
end
@subscriptions = Subscription.all.includes(:plan, :user => [:profile])
render xlsx: 'export_subscriptions.xlsx', filename: "export_subscriptions.xlsx"
end
# export reservations
def export_reservations
authorize :export
@datas = Reservation.includes(:user, :slots).all
respond_to do |format|
format.html
format.xls
end
@reservations = Reservation.all.includes(:slots, :reservable, :user => [:profile])
render xlsx: 'export_reservations.xlsx', filename: "export_reservations.xlsx"
end
def export_members
authorize :export
@datas = User.with_role(:member).includes(:group, :subscriptions, :profile)
respond_to do |format|
format.html
format.xls
end
@members = User.with_role(:member).includes(:group, :trainings, :tags, :invoices, :projects, :subscriptions => [:plan], :profile => [:address])
render xlsx: 'export_members.xlsx', filename: "export_members.xlsx"
end
def merge

View File

@ -14,9 +14,56 @@ class API::StatisticsController < API::ApiController
results = Stats::#{path.classify}.search(query, request.query_parameters.symbolize_keys).response
render json: results
end
def export_#{path}
authorize :statistic, :export_#{path}?
query = MultiJson.load(params[:body])
type_key = params[:type_key]
@results = Elasticsearch::Model.client.search({index: 'stats', type: '#{path}', scroll: '30s', body: query})
scroll_id = @results['_scroll_id']
while @results['hits']['hits'].size != @results['hits']['total']
scroll_res = Elasticsearch::Model.client.scroll(scroll: '30s', scroll_id: scroll_id)
@results['hits']['hits'].concat(scroll_res['hits']['hits'])
scroll_id = scroll_res['_scroll_id']
end
ids = @results['hits']['hits'].map { |u| u['_source']['userId'] }
@users = User.includes(:profile).where(:id => ids)
@index = StatisticIndex.find_by(es_type_key: '#{path}')
@type = StatisticType.find_by(key: type_key, statistic_index_id: @index.id)
@subtypes = @type.statistic_sub_types
@fields = @index.statistic_fields
render xlsx: 'export_current.xlsx', filename: "#{path}.xlsx"
end
}
end
def export_global
authorize :statistic, :export_global?
# query all stats with range arguments
query = MultiJson.load(params[:body])
@results = Elasticsearch::Model.client.search({index: 'stats', scroll: '30s', body: query})
scroll_id = @results['_scroll_id']
while @results['hits']['hits'].size != @results['hits']['total']
scroll_res = Elasticsearch::Model.client.scroll(scroll: '30s', scroll_id: scroll_id)
@results['hits']['hits'].concat(scroll_res['hits']['hits'])
scroll_id = scroll_res['_scroll_id']
end
ids = @results['hits']['hits'].map { |u| u['_source']['userId'] }
@users = User.includes(:profile).where(:id => ids)
@indices = StatisticIndex.all.includes(:statistic_fields, :statistic_types => [:statistic_sub_types])
render xlsx: 'export_global.xlsx', filename: "statistics.xlsx"
end
def scroll
authorize :statistic, :scroll?

View File

@ -56,6 +56,23 @@ module ApplicationHelper
amount / 100.00
end
##
# Retrieve an item in the given array of items
# by default, the "id" is expected to match the given parameter but
# this can be overridden by passing a third parameter to specify the
# property to match
##
def get_item(array, id, key = nil)
array.each do |i|
if key.nil?
return i if i.id == id
else
return i if i[key] == id
end
end
nil
end
private
## inspired by gems/actionview-4.2.5/lib/action_view/helpers/translation_helper.rb

View File

@ -1,5 +1,6 @@
class StatisticPolicy < ApplicationPolicy
%w(index account event machine project subscription training user scroll).each do |action|
%w(index account event machine project subscription training user scroll export_subscription export_machine
export_training export_event export_account export_project export_global).each do |action|
define_method "#{action}?" do
user.is_admin?
end

View File

@ -1,30 +0,0 @@
<table border="1">
<tr>
<th><%=t('export_members.id')%></th>
<th><%=t('export_members.surname')%></th>
<th><%=t('export_members.first_name')%></th>
<th><%=t('export_members.email')%></th>
<th><%=t('export_members.gender')%></th>
<th><%=t('export_members.age')%></th>
<th><%=t('export_members.phone')%></th>
<th><%=t('export_members.group')%></th>
<th><%=t('export_members.subscription')%></th>
<th><%=t('export_members.subscription_end_date')%></th>
<th><%=t('export_members.validated_trainings')%></th>
</tr>
<% @datas.each do |data| %>
<tr>
<td><%= data.id %></td>
<td><%= data.profile.last_name %></td>
<td><%= data.profile.first_name %></td>
<td><%= data.email %></td>
<td><%= data.profile.gender ? t('export_members.man') : t('export_members.woman') %></td>
<td><%= data.profile.age %></td>
<td><%= data.profile.phone %></td>
<td><%= data.group.name %></td>
<td><%= (data.subscription and data.subscription.expired_at > Time.now) ? data.subscription.plan.name : t('export_members.without_subscriptions') %></td>
<td><%= (data.subscription and data.subscription.expired_at > Time.now) ? data.subscription.expired_at : '' %></td>
<td><%= raw data.trainings.map(&:name).join('&#10;') %></td>
</tr>
<% end %>
</table>

View File

@ -0,0 +1,53 @@
wb = xlsx_package.workbook
header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF'
date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format
wb.add_worksheet(name: t('export_members.members')) do |sheet|
## data table
# heading labels
columns = [t('export_members.id'), t('export_members.surname'), t('export_members.first_name'), t('export_members.email'),
t('export_members.gender'), t('export_members.age'), t('export_members.address'), t('export_members.phone'),
t('export_members.website'), t('export_members.job'), t('export_members.interests'),
t('export_members.cad_software_mastered'), t('export_members.group'), t('export_members.subscription'),
t('export_members.subscription_end_date'), t('export_members.validated_trainings'), t('export_members.tags'),
t('export_members.number_of_invoices'), t('export_members.projects'), t('export_members.facebook'),
t('export_members.twitter'), t('export_members.echo_sciences')]
sheet.add_row columns, :style => header
# data rows
@members.each do |member|
data = [
member.id, member.profile.last_name, member.profile.first_name,
member.email, member.profile.gender ? t('export_members.man') : t('export_members.woman'), member.profile.age,
member.profile.address ? member.profile.address.address : '', member.profile.phone, member.profile.website,
member.profile.job, member.profile.interest, member.profile.software_mastered, member.group.name,
(member.subscription and member.subscription.expired_at > Time.now) ? member.subscription.plan.name : t('export_members.without_subscriptions'),
(member.subscription and member.subscription.expired_at > Time.now) ? member.subscription.expired_at.to_date : nil,
member.trainings.map(&:name).join("\n"), member.tags.map(&:name).join("\n"), member.invoices.size,
member.projects.map(&:name).join("\n"), member.profile.facebook || '', member.profile.twitter || '',
member.profile.echosciences || ''
]
styles = [nil, nil, nil,
nil, nil, nil,
nil, nil, nil,
nil, nil, nil, nil,
nil,
date,
nil, nil, nil,
nil, nil, nil,
nil]
types = [:integer, :string, :string,
:string, :string, :integer,
:string, :string, :string,
:string, :string, :string, :string,
:string,
:date,
:string, :string, :integer,
:string, :string, :string,
:string]
sheet.add_row data, :style => styles, :types => types
end
end

View File

@ -1,24 +0,0 @@
<table border="1">
<tr>
<th><%=t('export_reservations.customer_id')%></th>
<th><%=t('export_reservations.customer')%></th>
<th><%=t('export_reservations.email')%></th>
<th><%=t('export_reservations.reservation_date')%></th>
<th><%=t('export_reservations.reservation_type')%></th>
<th><%=t('export_reservations.reservation_object')%></th>
<th><%=t('export_reservations.slots_number_hours_tickets')%></th>
</tr>
<% @datas.each do |d| %>
<tr>
<td><%= d.user.id %></td>
<td><%= d.user.profile.full_name %></td>
<td><%= d.user.email %></td>
<td><%= d.created_at %></td>
<td><%= d.reservable_type %></td>
<td><%= d.reservable.name if !d.reservable.nil? %></td>
<td><%= d.slots.count %></td>
<td><%= (d.stp_invoice_id.nil?)? t('export_reservations.local_payment') : t('export_reservations.online_payment') %></td>
</tr>
<% end %>
</table>

View File

@ -0,0 +1,32 @@
wb = xlsx_package.workbook
header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF'
date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format
wb.add_worksheet(name: t('export_reservations.reservations')) do |sheet|
## data table
# heading labels
columns = [t('export_reservations.customer_id'), t('export_reservations.customer'), t('export_reservations.email'),
t('export_reservations.reservation_date'), t('export_reservations.reservation_type'), t('export_reservations.reservation_object'),
t('export_reservations.slots_number_hours_tickets'), t('export_reservations.payment_method')]
sheet.add_row columns, :style => header
# data rows
@reservations.each do |resrv|
data = [
resrv.user.id,
resrv.user.profile.full_name,
resrv.user.email,
resrv.created_at.to_date,
resrv.reservable_type,
(resrv.reservable.nil? ? '' : resrv.reservable.name),
resrv.slots.count,
(resrv.stp_invoice_id.nil?)? t('export_reservations.local_payment') : t('export_reservations.online_payment')
]
styles = [nil, nil, nil, date, nil, nil, nil, nil]
types = [:integer, :string, :string, :date, :string, :string, :integer, :string]
sheet.add_row data, :style => styles, :types => types
end
end

View File

@ -1,26 +0,0 @@
<table border="1">
<tr>
<th><%=t('export_subscriptions.id')%></th>
<th><%=t('export_subscriptions.customer')%></th>
<th><%=t('export_subscriptions.email')%></th>
<th><%=t('export_subscriptions.subscription')%></th>
<th><%=t('export_subscriptions.period')%></th>
<th><%=t('export_subscriptions.start_date')%></th>
<th><%=t('export_subscriptions.expiration_date')%></th>
<th><%=t('export_subscriptions.amount')%></th>
<th><%=t('export_subscriptions.payment_method')%></th>
</tr>
<% @datas.each do |data| %>
<tr>
<td><%= data.user.id %></td>
<td><%= data.user.profile.full_name %></td>
<td><%= data.user.email %></td>
<td><%= data.plan.human_readable_name(group: true) %></td>
<td><%= t("duration.#{data.plan.interval}", count: data.plan.interval_count) %></td>
<td><%= data.created_at %></td>
<td><%= data.expired_at %></td>
<td><%= number_to_currency(data.plan.amount / 100) %></td>
<td><%= (data.stp_subscription_id.nil?)? t('export_subscriptions.local_payment') : t('export_subscriptions.online_payment') %></td>
</tr>
<% end %>
</table>

View File

@ -0,0 +1,33 @@
wb = xlsx_package.workbook
header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF'
date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format
wb.add_worksheet(name: t('export_subscriptions.subscriptions')) do |sheet|
## data table
# heading labels
columns = [t('export_subscriptions.id'), t('export_subscriptions.customer'), t('export_subscriptions.email'),
t('export_subscriptions.subscription'), t('export_subscriptions.period'), t('export_subscriptions.start_date'),
t('export_subscriptions.expiration_date'), t('export_subscriptions.amount'), t('export_subscriptions.payment_method')]
sheet.add_row columns, :style => header
# data rows
@subscriptions.each do |sub|
data = [
sub.user.id,
sub.user.profile.full_name,
sub.user.email,
sub.plan.human_readable_name(group: true),
t("duration.#{sub.plan.interval}", count: sub.plan.interval_count),
sub.created_at.to_date,
sub.expired_at.to_date,
number_to_currency(sub.plan.amount / 100),
(sub.stp_subscription_id.nil?)? t('export_subscriptions.local_payment') : t('export_subscriptions.online_payment')
]
styles = [nil, nil, nil, nil, nil, date, date, nil, nil]
types = [:integer, :string, :string, :string, :string, :date, :date, :string, :string]
sheet.add_row data, :style => styles, :types => types
end
end

View File

@ -0,0 +1,79 @@
wb = xlsx_package.workbook
bold = wb.styles.add_style :b => true
header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF'
date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format
wb.add_worksheet(name: @index.label) do |sheet|
## heading stats for the current page
sheet.add_row [t('export.entries'), @results['hits']['total']], :style => [bold, nil], :types => [:string, :integer]
if @index.ca
sheet.add_row [t('export.revenue'), @results['aggregations']['total_ca']['value']], :style => [bold, nil], :types => [:string, :float]
end
sheet.add_row [t('export.average_age'), @results['aggregations']['average_age']['value']], :style => [bold, nil], :types => [:string, :float]
unless @type.simple
sheet.add_row ["#{t('export.total')} #{@type.label}", @results['aggregations']['total_stat']['value']], :style => [bold, nil], :types => [:string, :integer]
end
sheet.add_row []
## data table
# heading labels
columns = [t('export.date'), t('export.user'), t('export.email'), t('export.phone'), t('export.gender'), t('export.age'), t('export.type')]
columns.push @type.label unless @type.simple
@fields.each do |f|
columns.push f.label
end
columns.push t('export.revenue') if @index.ca
sheet.add_row columns, :style => header
# data rows
@results['hits']['hits'].each do |hit|
user = get_item(@users, hit['_source']['userId'])
subtype = get_item(@subtypes, hit['_source']['subType'], 'key')
data = [
Date::strptime(hit['_source']['date'],'%Y-%m-%d'),
(user ? user.profile.full_name : "ID #{hit['_source']['userId']}"),
(user ? user.email : ''),
(user ? user.profile.phone : ''),
t("export.#{hit['_source']['gender']}"),
hit['_source']['age'],
subtype.nil? ? "" : subtype.label
]
styles = [date, nil, nil, nil, nil, nil, nil]
types = [:date, :string, :string, :string, :string, :integer, :string]
unless @type.simple
data.push hit['_source']['stat']
styles.push nil
types.push :string
end
@fields.each do |f|
field_data = hit['_source'][f.key]
case f.data_type
when 'date'
data.push Date::strptime(field_data, '%Y-%m-%d')
styles.push date
types.push :date
when 'list'
data.push field_data.map{|e| e['name'] }.join(', ')
styles.push nil
types.push :string
when 'number'
data.push field_data
styles.push nil
types.push :float
else
data.push field_data
styles.push nil
types.push :string
end
end
if @index.ca
data.push hit['_source']['ca']
styles.push nil
types.push :float
end
sheet.add_row data, :style => styles, :types => types
end
end

View File

@ -0,0 +1,84 @@
wb = xlsx_package.workbook
header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF'
date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format
@indices.each do |index|
if index.table
index.statistic_types.each do |type|
# see https://msdn.microsoft.com/fr-fr/library/c6bdca6y(v=vs.90).aspx for unauthorized character list
sheet_name = "#{index.label} - #{type.label}".gsub(/[*|\\:"<>?\/]/,'').truncate(31)
wb.add_worksheet(name: sheet_name) do |sheet|
## data table
# heading labels
columns = [t('export.date'), t('export.user'), t('export.email'), t('export.phone'), t('export.gender'), t('export.age'), t('export.type')]
columns.push type.label unless type.simple
index.statistic_fields.each do |f|
columns.push f.label
end
columns.push t('export.revenue') if index.ca
sheet.add_row columns, :style => header
# data rows
@results['hits']['hits'].each do |hit|
# check that the current result is for the given index and type
if hit['_type'] == index.es_type_key and hit['_source']['type'] == type.key
# get matching objects
user = get_item(@users, hit['_source']['userId'])
subtype = get_item(type.statistic_sub_types, hit['_source']['subType'], 'key')
# start to fill data and associated styles and data-types
data = [
Date::strptime(hit['_source']['date'],'%Y-%m-%d'),
(user ? user.profile.full_name : "ID #{hit['_source']['userId']}"),
(user ? user.email : ''),
(user ? user.profile.phone : ''),
t("export.#{hit['_source']['gender']}"),
hit['_source']['age'],
subtype.nil? ? "" : subtype.label
]
styles = [date, nil, nil, nil, nil, nil, nil]
types = [:date, :string, :string, :string, :string, :integer, :string]
# do not proceed with the 'stat' field if the type is declared as 'simple'
unless type.simple
data.push hit['_source']['stat']
styles.push nil
types.push :string
end
# proceed additional fields
index.statistic_fields.each do |f|
field_data = hit['_source'][f.key]
case f.data_type
when 'date'
data.push Date::strptime(field_data, '%Y-%m-%d')
styles.push date
types.push :date
when 'list'
data.push field_data.map{|e| e['name'] }.join(', ')
styles.push nil
types.push :string
when 'number'
data.push field_data
styles.push nil
types.push :float
else
data.push field_data
styles.push nil
types.push :string
end
end
# proceed teh 'ca' field if requested
if index.ca
data.push hit['_source']['ca']
styles.push nil
types.push :float
end
# finally, add the data row to the workbook's sheet
sheet.add_row data, :style => styles, :types => types
end
end
end
end
end
end

View File

@ -47,6 +47,7 @@ TIME_ZONE: 'Paris'
WEEK_STARTING_DAY: 'monday'
D3_DATE_FORMAT: '%d/%m/%y'
UIB_DATE_FORMAT: 'dd/MM/yyyy'
EXCEL_DATE_FORMAT: 'dd/mm/yyyy'
OPENLAB_APP_SECRET:
OPENLAB_APP_ID:

View File

@ -405,7 +405,10 @@ en:
unknown: "Unknown"
user_id: "User ID"
display_more_results: "Display more results"
export_statistics_to_excel: "Export statistics to Excel"
export_all_statistics: "Export all statistics"
export_the_current_search_results: "Export the current search results"
export: "Export"
stats_graphs:
# statistics graphs

View File

@ -405,7 +405,10 @@ fr:
unknown: "Inconnu"
user_id: "ID Utilisateur"
display_more_results: "Afficher plus de résultats"
export_statistics_to_excel: "Exporter les statistiques vers Excel"
export_all_statistics: "Exporter toutes les statistiques"
export_the_current_search_results: "Exporter les résultats de la recherche courante"
export: "Exporter"
stats_graphs:
# graphiques de statistiques

View File

@ -116,23 +116,36 @@ en:
export_members:
# members list export to EXCEL format
members: "Members"
id: "ID"
surname: "Surname"
first_name: "First name"
email: "E-mail"
gender: "Gender"
age: "Age"
address: "Address"
phone: "Phone"
website: "Website"
job: "Job"
interests: "Interests"
cad_software_mastered: "CAD Softwares mastered"
group: "Group"
subscription: "Subscription"
subscription_end_date: "Subscription end date"
validated_trainings: "Validated trainings"
tags: "Tags"
number_of_invoices: "Number of invoices"
projects: "Projects"
facebook: "Facebook"
twitter: "Twitter"
echo_sciences: "Echosciences"
man: "Man"
woman: "Woman"
without_subscriptions: "Without subscriptions"
export_reservations:
# machines/trainings/events reservations list to EXCEL format
reservations: "Reservations"
customer_id: "Customer ID"
customer: "Customer"
email: "E-mail"
@ -146,6 +159,7 @@ en:
export_subscriptions:
# subscriptions list export to EXCEL format
subscriptions: "Subscriptions"
id: "ID"
customer: "Customer"
email: "E-mail"
@ -263,3 +277,20 @@ en:
revenue: "Revenue"
account_creation: "Account creation"
project_publication: "Project publication"
export:
# statistics exports to the excel file format
entries: "Entries"
revenue: "Revenue"
average_age: "Average Age"
total: "Total"
date: "Date"
user: "User"
email: "Email"
phone: "Phone"
gender: "Gender"
age: "Age"
type: "Type"
revenue: "Revenue"
male: "Man"
female: "Woman"

View File

@ -116,23 +116,36 @@ fr:
export_members:
# export de la liste des members au format EXCEL
members: "Membres"
id: "ID"
surname: "Nom"
first_name: "Prénom"
email: "Courriel"
gender: "Genre"
age: "Âge"
address: "Adresse"
phone: "Tel."
website: "Site web"
job: "Profession"
interests: "Centres d'intérêts"
cad_software_mastered: "Logiciels de conception maîtrisés"
group: "Groupe"
subscription: "Abonnement"
subscription_end_date: "Date de fin de l'abonnement"
validated_trainings: "Formations validées"
tags: "Étiquettes"
number_of_invoices: "Nombre de factures"
projects: "Projets"
facebook: "Facebook"
twitter: "Twitter"
echo_sciences: "Echosciences"
man: "Homme"
woman: "Femme"
without_subscriptions: "Sans Abonnement"
export_reservations:
# export de la liste des réservations machines/formations/évènements au format EXCEL
reservations: "Réservations"
customer_id: "ID client"
customer: "Client"
email: "Courriel"
@ -146,6 +159,7 @@ fr:
export_subscriptions:
# export de la liste des abonnements au format EXCEL
subscriptions: "Abonnements"
id: "ID"
customer: "Client"
email: "Courriel"
@ -263,3 +277,20 @@ fr:
revenue: "Chiffre d'affaires"
account_creation: "Création de compte"
project_publication: "Publication de projet"
export:
# export des statistiques au format excel
entries: "Entrées"
revenue: "Chiffre d'affaires"
average_age: "Âge moyen"
total: "Total"
date: "Date"
user: "Utilisateur"
email: "Courriel"
phone: "Téléphone"
gender: "Genre"
age: "Âge"
type: "Type"
revenue: "Chiffre d'affaires"
male: "Homme"
female: "Femme"

View File

@ -137,7 +137,9 @@ Rails.application.routes.draw do
%w(account event machine project subscription training user).each do |path|
post "/stats/#{path}/_search", to: "api/statistics##{path}"
post "/stats/#{path}/export", to: "api/statistics#export_#{path}"
end
post '/stats/global/export', to: "api/statistics#export_global"
post '_search/scroll', to: "api/statistics#scroll"
match '/project_collaborator/:valid_token', to: 'api/projects#collaborator_valid', via: :get

View File

@ -21,6 +21,7 @@ development:
week_starting_day: <%= ENV["WEEK_STARTING_DAY"] %>
d3_date_format: <%= ENV["D3_DATE_FORMAT"].dump %> # .dump is needed as the value may start by a '%', see https://github.com/tenderlove/psych/issues/75
uib_date_format: <%= ENV["UIB_DATE_FORMAT"] %>
excel_date_format: <%= ENV["EXCEL_DATE_FORMAT"]%>
rails_locale: <%= ENV["RAILS_LOCALE"] %>
moment_locale: <%= ENV["MOMENT_LOCALE"] %>
summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %>
@ -46,6 +47,7 @@ test:
week_starting_day: monday
d3_date_format: '%d/%m/%y'
uib_date_format: dd/MM/yyyy
excel_date_format: dd/mm/yyyy
rails_locale: en
moment_locale: en
summernote_locale: en-US
@ -78,6 +80,7 @@ staging:
week_starting_day: <%= ENV["WEEK_STARTING_DAY"] %>
d3_date_format: <%= ENV["D3_DATE_FORMAT"].dump %>
uib_date_format: <%= ENV["UIB_DATE_FORMAT"] %>
excel_date_format: <%= ENV["EXCEL_DATE_FORMAT"]%>
rails_locale: <%= ENV["RAILS_LOCALE"] %>
moment_locale: <%= ENV["MOMENT_LOCALE"] %>
summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %>
@ -112,6 +115,7 @@ production:
week_starting_day: <%= ENV["WEEK_STARTING_DAY"] %>
d3_date_format: <%= ENV["D3_DATE_FORMAT"].dump %>
uib_date_format: <%= ENV["UIB_DATE_FORMAT"] %>
excel_date_format: <%= ENV["EXCEL_DATE_FORMAT"]%>
rails_locale: <%= ENV["RAILS_LOCALE"] %>
moment_locale: <%= ENV["MOMENT_LOCALE"] %>
summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %>