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