diff --git a/app/assets/javascripts/dashboard.js b/app/assets/javascripts/dashboard.js
index e18b682f..a81eec29 100644
--- a/app/assets/javascripts/dashboard.js
+++ b/app/assets/javascripts/dashboard.js
@@ -11,7 +11,14 @@ $(function() {
"width":$(el).parent().width(),
"height":$(el).parent().outerHeight()
});
- redraw(animate, $(this));
+ });
+
+ $(".line_chart").each(function(){
+ draw_line_chart(animate, $(this));
+ });
+
+ $(".doughnut_chart").each(function(){
+ draw_doughnut_chart(animate, $(this));
});
var m = 0;
@@ -21,13 +28,37 @@ $(function() {
}, 30);
}
- function redraw(animation, $this){
- var options = {};
+ function draw_doughnut_chart(animation, $this){
+ var options = get_animation({}, animation);
+ var tmp = $this.data('chart');
+
+ if(jQuery.isEmptyObject(tmp)){
+ // Append error message if there is no data
+ $this.parent().append("
No data!
");
+ // Remove canvas
+ $this.remove();
+ }else{
+ var data = [];
+ for (var key in tmp) {
+ data.push(tmp[key]);
+ }
+
+ var ctx = $this.get(0).getContext("2d");
+ new Chart(ctx).Doughnut(data, options);
+ }
+ }
+
+ function get_animation(options, animation){
if (!animation){
options.animation = false;
} else {
options.animation = true;
}
+ return options;
+ }
+
+ function draw_line_chart(animation, $this){
+ var options = get_animation({}, animation);
var chart_data = create_dataset($this);
var weeks = $this.parent().data('weeks');
@@ -36,8 +67,7 @@ $(function() {
datasets : chart_data
}
- var canvas = $this[0];
- var ctx = canvas.getContext("2d");
+ var ctx = $this.get(0).getContext("2d");
new Chart(ctx).Line(data, options);
}
@@ -74,7 +104,7 @@ $(function() {
$('.conferenceCheckboxes input').change(function(){
var chart = $(this).parent().data('chart');
var $canvas = $('#' + chart + 'Chart');
- redraw(false, $canvas);
+ draw_line_chart(false, $canvas);
});
$(window).on('resize', function(){ size(false); });
diff --git a/app/controllers/admin/conference_controller.rb b/app/controllers/admin/conference_controller.rb
index 13a3fc36..50eff4e3 100644
--- a/app/controllers/admin/conference_controller.rb
+++ b/app/controllers/admin/conference_controller.rb
@@ -2,6 +2,12 @@ class Admin::ConferenceController < ApplicationController
before_filter :verify_organizer
def index
+ # Redirect to new form if there is no conference
+ if Conference.count == 0
+ redirect_to new_admin_conference_path
+ return
+ end
+
@total_user = User.count
@new_user = User.where('created_at > ?', current_user.last_sign_in_at).count
@@ -44,11 +50,8 @@ class Admin::ConferenceController < ApplicationController
@registrations = normalize_array_length(@registrations, @registration_weeks)
@registration_weeks = @registration_weeks > 0 ? (1..@registration_weeks).to_a : 1
- # Redirect to new form if there is no conference
- if Conference.count == 0
- redirect_to new_admin_conference_path
- return
- end
+ @event_distribution = Conference.event_distribution
+ @user_distribution = Conference.user_distribution
end
def new
@@ -85,6 +88,8 @@ class Admin::ConferenceController < ApplicationController
@conference = Conference.find_by(short_title: params[:id])
@conference_progress = @conference.get_status
@top_submitter = @conference.get_top_submitter
+ @event_distribution = @conference.event_distribution
+
respond_to do |format|
format.html
format.json { render json: @conference.to_json }
diff --git a/app/models/conference.rb b/app/models/conference.rb
index b49a901e..8ba0e73e 100644
--- a/app/models/conference.rb
+++ b/app/models/conference.rb
@@ -278,8 +278,88 @@ class Conference < ActiveRecord::Base
Conference.calculate_person_submission_hash(submitter, counter)
end
+ ##
+ # Returns a hash with event state => {value: count of event states, color: color}.
+ # The result is calculated over all conferences.
+ #
+ # ====Returns
+ # * +hash+ -> hash
+ def self.event_distribution
+ calculate_event_distribution_hash(Event.group(:state).count)
+ end
+
+ ##
+ # Returns a hash with event state => {value: count of event states, color: color}
+ #
+ # ====Returns
+ # * +hash+ -> hash
+ def event_distribution
+ Conference.calculate_event_distribution_hash(events.group(:state).count)
+ end
+
+ ##
+ # Returns a hash with user distribution => {value: count of user state, color: color}
+ # active: signed in during the last 3 months
+ # unconfirmed: registered but not confirmed
+ # dead: not signed in during the last year
+ #
+ # ====Returns
+ # * +hash+ -> hash
+ def self.user_distribution
+ active_user = User.where('last_sign_in_at > ?', Date.today - 3.months).count
+ unconfirmed_user = User.where('confirmed_at IS NULL').count
+ dead_user = User.where('last_sign_in_at < ?', Date.today - 1.year).count
+
+ calculate_user_distribution_hash(active_user, unconfirmed_user, dead_user)
+ end
+
private
+ ##
+ # Helper method for calculating hash with corresponding colors of user distribution states.
+ #
+ # ====Returns
+ # * +hash+ -> hash
+ def self.calculate_user_distribution_hash(active_user, unconfirmed_user, dead_user)
+ result = {}
+ if active_user > 0
+ result['Active'] = {
+ 'color' => 'green',
+ 'value' => active_user
+ }
+ end
+ if unconfirmed_user > 0
+ result['Unconfirmed'] = {
+ 'color' => 'red',
+ 'value' => unconfirmed_user
+ }
+ end
+ if dead_user > 0
+ result['Dead'] = {
+ 'color' => 'black',
+ 'value' => dead_user
+ }
+ end
+ result
+ end
+
+ ##
+ # Helper method. Calculates hash with corresponding colors of event state distribution.
+ #
+ # ====Returns
+ # * +hash+ -> hash
+ def self.calculate_event_distribution_hash(states)
+ result = {}
+ states.each do |key, value|
+ result[key.capitalize] =
+ {
+ 'value' => value,
+ 'color' => Event.get_state_color(key)
+ }
+ end
+ result
+ end
+
##
# Returns a hash with person => submissions ordered by submissions for all conferences
#
diff --git a/app/models/event.rb b/app/models/event.rb
index 1b985c36..47c611fc 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -154,6 +154,26 @@ class Event < ActiveRecord::Base
created_at.strftime('%W').to_i
end
+ def self.get_state_color(state)
+ # default azure
+ result = '#00FFFF'
+ case state
+ when 'new' # blue
+ result = '#0000FF'
+ when 'withdrawn' # orange
+ result = '#FF8000'
+ when 'confirmed' # green
+ result = '#00FF00'
+ when 'unconfirmed' # yellow
+ result = '#FFFF00'
+ when 'rejected' # red
+ result = '#FF0000'
+ when 'canceled' # grey
+ result = '#848484'
+ end
+ result
+ end
+
private
def abstract_limit
diff --git a/app/views/admin/conference/_event_distribution.html.haml b/app/views/admin/conference/_event_distribution.html.haml
new file mode 100644
index 00000000..d88f5934
--- /dev/null
+++ b/app/views/admin/conference/_event_distribution.html.haml
@@ -0,0 +1,8 @@
+.well
+ .text-center
+ %h4 Event distribution
+ %canvas.doughnut_chart{"data-chart"=>@event_distribution.to_json}
+ .row
+ - if @event_distribution
+ - @event_distribution.each do |key, value|
+ %span{"style"=>"border-bottom: 3px solid #{value['color']}"} #{key}: #{value['value']}
\ No newline at end of file
diff --git a/app/views/admin/conference/_registrations.html.haml b/app/views/admin/conference/_registrations.html.haml
index c08017b4..6f112864 100644
--- a/app/views/admin/conference/_registrations.html.haml
+++ b/app/views/admin/conference/_registrations.html.haml
@@ -4,7 +4,7 @@
%h4 Conference registrations over time
.row
.registrationsChart{"data-chart"=>"#{registrations.to_json}", "data-conferences"=>"#{conferences.to_json}", "data-weeks"=>"#{registration_weeks.to_json}"}
- %canvas#registrationsChart{"data-name"=>"registrations"}
+ %canvas.line_chart#registrationsChart{"data-name"=>"registrations"}
.row
.text-center
weeks
diff --git a/app/views/admin/conference/_submissions.html.haml b/app/views/admin/conference/_submissions.html.haml
index 3593d0a9..8d7673fa 100644
--- a/app/views/admin/conference/_submissions.html.haml
+++ b/app/views/admin/conference/_submissions.html.haml
@@ -4,7 +4,7 @@
%h4 Event submissions over time
.row
.submissionsChart{"data-chart"=>"#{submissions.to_json}", "data-conferences"=>"#{conferences.to_json}", "data-weeks"=>"#{cfp_weeks.to_json}"}
- %canvas#submissionsChart{"data-name"=>"submissions"}
+ %canvas.line_chart#submissionsChart{"data-name"=>"submissions"}
.row
.text-center
weeks
diff --git a/app/views/admin/conference/_user_distribution.html.haml b/app/views/admin/conference/_user_distribution.html.haml
new file mode 100644
index 00000000..272e9d04
--- /dev/null
+++ b/app/views/admin/conference/_user_distribution.html.haml
@@ -0,0 +1,8 @@
+.well
+ .text-center
+ %h4 User distribution
+ %canvas.doughnut_chart{"data-chart"=>user_distribution.to_json}
+ .row
+ - if user_distribution
+ - user_distribution.each do |key, value|
+ %span{"style"=>"border-bottom: 3px solid #{value['color']}"} #{key}: #{value['value']}
\ No newline at end of file
diff --git a/app/views/admin/conference/index.html.haml b/app/views/admin/conference/index.html.haml
index da58db4b..7374fa4f 100644
--- a/app/views/admin/conference/index.html.haml
+++ b/app/views/admin/conference/index.html.haml
@@ -6,11 +6,13 @@
= render partial: 'submissions', locals: { conferences: @conferences, submissions: @submissions,
cfp_weeks: @cfp_weeks }
.col-md-4
+ = render partial: 'event_distribution', locals: { event_distribution: @event_distribution }
.row
.col-md-8
= render partial: 'registrations', locals: { conferences: @conferences, registrations: @registrations,
registration_weeks: @registration_weeks }
.col-md-4
+ = render partial: 'user_distribution', locals: { user_distribution: @user_distribution }
.col-md-8
.row
%ul.nav.nav-tabs#recentTable
diff --git a/app/views/admin/conference/show.html.haml b/app/views/admin/conference/show.html.haml
index be43e9f8..7aa12860 100644
--- a/app/views/admin/conference/show.html.haml
+++ b/app/views/admin/conference/show.html.haml
@@ -6,4 +6,6 @@
.col-md-4
= render partial: 'todo_list', locals: { conference_progress: @conference_progress }
.col-md-4
- = render partial: 'top_submitter', locals: {top_submitter: @top_submitter}
\ No newline at end of file
+ = render partial: 'top_submitter', locals: {top_submitter: @top_submitter}
+ .col-md-4
+ = render partial: 'event_distribution', locals: { event_distribution: @event_distribution }
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 74460169..a4061760 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -34,4 +34,10 @@ Osem::Application.configure do
# Print deprecation notices to the stderr
config.active_support.deprecation = :stderr
+
+ # Set the detault url for action mailer
+ config.action_mailer.default_url_options = { host: 'localhost:3000' }
+
+ # Do not perform deliveries on test
+ config.action_mailer.perform_deliveries = false
end
diff --git a/spec/models/conference_spec.rb b/spec/models/conference_spec.rb
index 425b8919..ae404f3c 100644
--- a/spec/models/conference_spec.rb
+++ b/spec/models/conference_spec.rb
@@ -6,6 +6,175 @@ describe Conference do
let(:subject) { create(:conference) }
+ describe '#event_distribution' do
+
+ before(:each) do
+ @conference = create(
+ :conference,
+ email_settings: create(:email_settings))
+ @conference.email_settings = create(:email_settings)
+
+ @options = {}
+ @options[:send_mail] = 'false'
+
+ create(:event, conference: @conference)
+
+ withdrawn = create(:event, conference: @conference)
+ withdrawn.withdraw!
+
+ unconfirmed = create(:event, conference: @conference)
+ unconfirmed.accept!(@options)
+
+ rejected = create(:event, conference: @conference)
+ rejected.reject!(@options)
+
+ confirmed = create(:event, conference: @conference)
+ confirmed.accept!(@options)
+ confirmed.confirm!
+
+ canceled = create(:event, conference: @conference)
+ canceled.accept!(@options)
+ canceled.cancel!
+
+ @result = {}
+ @result['New'] = { 'value' => 1, 'color' => '#0000FF' }
+ @result['Withdrawn'] = { 'value' => 1, 'color' => '#FF8000' }
+ @result['Unconfirmed'] = { 'value' => 1, 'color' => '#FFFF00' }
+ @result['Rejected'] = { 'value' => 1, 'color' => '#FF0000' }
+ @result['Confirmed'] = { 'value' => 1, 'color' => '#00FF00' }
+ @result['Canceled'] = { 'value' => 1, 'color' => '#848484' }
+ end
+
+ it '#event_distribution does calculate correct values with events' do
+ expect(@conference.event_distribution).to eq(@result)
+ end
+
+ it '#event_distribution does calculate correct values with no events' do
+ @conference.events.clear
+ expect(@conference.event_distribution).to eq({})
+ end
+
+ it 'event_distribution does calculate correct values with just a new event' do
+ conference = create(:conference)
+ create(:event, conference: conference)
+ result = { 'New' => { 'value' => 1, 'color' => '#0000FF' } }
+ expect(conference.event_distribution).to eq(result)
+ end
+
+ it 'event_distribution does calculate correct values with just an withdrawn event' do
+ conference = create(:conference)
+ event = create(:event, conference: conference)
+ event.withdraw!
+ result = { 'Withdrawn' => { 'value' => 1, 'color' => '#FF8000' } }
+ expect(conference.event_distribution).to eq(result)
+ end
+
+ it 'event_distribution does calculate correct values with just an unconfirmed event' do
+ conference = create(:conference)
+ event = create(:event, conference: conference)
+ event.accept!(@options)
+ result = { 'Unconfirmed' => { 'value' => 1, 'color' => '#FFFF00' } }
+ expect(conference.event_distribution).to eq(result)
+ end
+
+ it 'event_distribution does calculate correct values with just an rejected event' do
+ conference = create(:conference)
+ event = create(:event, conference: conference)
+ event.reject!(@options)
+ result = { 'Rejected' => { 'value' => 1, 'color' => '#FF0000' } }
+ expect(conference.event_distribution).to eq(result)
+ end
+
+ it 'event_distribution does calculate correct values with just an confirmed event' do
+ conference = create(:conference)
+ conference.email_settings = create(:email_settings)
+ event = create(:event, conference: conference)
+ event.accept!(@options)
+ event.confirm!
+ result = { 'Confirmed' => { 'value' => 1, 'color' => '#00FF00' } }
+ expect(conference.event_distribution).to eq(result)
+ end
+
+ it 'event_distribution does calculate correct values with just an canceled event' do
+ conference = create(:conference)
+ event = create(:event, conference: conference)
+ event.accept!(@options)
+ event.cancel!
+ result = { 'Canceled' => { 'value' => 1, 'color' => '#848484' } }
+ expect(conference.event_distribution).to eq(result)
+ end
+
+ it 'self#event_distribution does calculate correct values' do
+ expect(Conference.event_distribution).to eq(@result)
+ end
+
+ it 'self#event_distribution does calculate correct values with no events' do
+ @conference.events.clear
+ expect(Conference.event_distribution).to eq({})
+ end
+
+ it 'self#event_distribution does calculate correct values with just a new event' do
+ @conference.events.clear
+ create(:event, conference: @conference)
+ result = { 'New' => { 'value' => 1, 'color' => '#0000FF' } }
+ expect(Conference.event_distribution).to eq(result)
+ end
+
+ it 'self#event_distribution does calculate correct values
+ with just a new events from different conferences' do
+ create(:event, conference: @conference)
+ @result['New'] = { 'value' => 2, 'color' => '#0000FF' }
+ expect(Conference.event_distribution).to eq(@result)
+ end
+ end
+
+ describe 'self#event_distribution' do
+ # It is necessary to use bang version of let to build roles before user
+ let!(:organizer_role) { create(:organizer_role) }
+ let!(:participant_role) { create(:participant_role) }
+ let!(:admin_role) { create(:admin_role) }
+
+ it 'self#event_distribution calculates correct values with user' do
+ create(:user, last_sign_in_at: Time.now - 3.months) # active
+ create(:user, confirmed_at: nil) # unconfirmed
+ create(:user, last_sign_in_at: Time.now - 1.year - 1.day) # dead
+ result = {}
+ result['Active'] = { 'color' => 'green', 'value' => 1 }
+ result['Unconfirmed'] = { 'color' => 'red', 'value' => 1 }
+ result['Dead'] = { 'color' => 'black', 'value' => 1 }
+
+ expect(Conference.user_distribution).to eq(result)
+ end
+
+ it 'self#event_distribution calculates correct with only active user' do
+ create(:user, last_sign_in_at: Time.now - 3.months) # active
+ result = {}
+ result['Active'] = { 'color' => 'green', 'value' => 1 }
+
+ expect(Conference.user_distribution).to eq(result)
+ end
+
+ it 'self#event_distribution calculates correct values with only unconfirmed user' do
+ create(:user, confirmed_at: nil) # unconfirmed
+ result = {}
+ result['Unconfirmed'] = { 'color' => 'red', 'value' => 1 }
+
+ expect(Conference.user_distribution).to eq(result)
+ end
+
+ it 'self#event_distribution calculates correct values with only dead user' do
+ create(:user, last_sign_in_at: Time.now - 1.year - 1.day) # dead
+ result = {}
+ result['Dead'] = { 'color' => 'black', 'value' => 1 }
+
+ expect(Conference.user_distribution).to eq(result)
+ end
+
+ it 'self#event_distribution calculates correct values without user' do
+ expect(Conference.user_distribution).to eq({})
+ end
+ end
+
describe '#get_status' do
before(:each) do