From 857ac43e74ae04ab10863d119beb6cd0df4e347f Mon Sep 17 00:00:00 2001 From: Chrisbr Date: Thu, 26 Jun 2014 09:08:26 +0200 Subject: [PATCH] Implements targets and campaigns for conference --- Gemfile | 4 + Gemfile.lock | 19 +++ app/assets/javascripts/application.js | 1 + app/assets/javascripts/osem.js | 42 ++++++ app/assets/stylesheets/osem.css | 4 +- app/controllers/admin/campaigns_controller.rb | 65 ++++++++++ .../admin/conference_controller.rb | 8 ++ app/controllers/admin/targets_controller.rb | 21 +++ .../conference_registration_controller.rb | 2 + app/controllers/proposal_controller.rb | 1 + app/helpers/application_helper.rb | 24 ++++ app/models/ahoy/event.rb | 10 ++ app/models/campaign.rb | 77 +++++++++++ app/models/conference.rb | 42 +++++- app/models/target.rb | 79 ++++++++++++ app/models/visit.rb | 4 + app/views/admin/campaigns/_form.html.haml | 12 ++ app/views/admin/campaigns/edit.html.haml | 2 + app/views/admin/campaigns/index.html.haml | 41 ++++++ app/views/admin/campaigns/new.html.haml | 2 + .../admin/conference/_campaigns.html.haml | 40 ++++++ app/views/admin/conference/_targets.html.haml | 32 +++++ app/views/admin/conference/show.html.haml | 17 ++- .../admin/targets/_target_fields.html.haml | 6 + app/views/admin/targets/index.html.haml | 5 + app/views/layouts/_admin_sidebar.html.haml | 24 +++- config/initializers/ahoy.rb | 3 + config/routes.rb | 4 + db/migrate/20140616132153_create_targets.rb | 12 ++ db/migrate/20140616132456_create_campaigns.rb | 14 ++ db/migrate/20140623100942_create_visits.rb | 51 ++++++++ .../20140623101032_create_ahoy_events.rb | 20 +++ db/schema.rb | 63 ++++++++- spec/factories/campaigns.rb | 8 ++ spec/factories/targets.rb | 9 ++ spec/factories/visits.rb | 6 + spec/features/campaign_spec.rb | 59 +++++++++ spec/models/campaign_spec.rb | 71 ++++++++++ spec/models/conference_spec.rb | 121 +++++++++++++----- spec/models/target_spec.rb | 105 +++++++++++++++ 40 files changed, 1086 insertions(+), 44 deletions(-) create mode 100644 app/controllers/admin/campaigns_controller.rb create mode 100644 app/controllers/admin/targets_controller.rb create mode 100644 app/models/ahoy/event.rb create mode 100644 app/models/campaign.rb create mode 100644 app/models/target.rb create mode 100644 app/models/visit.rb create mode 100644 app/views/admin/campaigns/_form.html.haml create mode 100644 app/views/admin/campaigns/edit.html.haml create mode 100644 app/views/admin/campaigns/index.html.haml create mode 100644 app/views/admin/campaigns/new.html.haml create mode 100644 app/views/admin/conference/_campaigns.html.haml create mode 100644 app/views/admin/conference/_targets.html.haml create mode 100644 app/views/admin/targets/_target_fields.html.haml create mode 100644 app/views/admin/targets/index.html.haml create mode 100644 config/initializers/ahoy.rb create mode 100644 db/migrate/20140616132153_create_targets.rb create mode 100644 db/migrate/20140616132456_create_campaigns.rb create mode 100644 db/migrate/20140623100942_create_visits.rb create mode 100644 db/migrate/20140623101032_create_ahoy_events.rb create mode 100644 spec/factories/campaigns.rb create mode 100644 spec/factories/targets.rb create mode 100644 spec/factories/visits.rb create mode 100644 spec/features/campaign_spec.rb create mode 100644 spec/models/campaign_spec.rb create mode 100644 spec/models/target_spec.rb diff --git a/Gemfile b/Gemfile index 9e69ed44..d709a6ea 100644 --- a/Gemfile +++ b/Gemfile @@ -97,6 +97,10 @@ gem 'rdoc-generator-fivefish' # We use factory_girl for seeds gem 'factory_girl_rails' +# We use ahoy for visitor tracking +gem 'ahoy_matey' +gem 'activeuuid' + # Use guard and spring for testing in development group :development do # rspec Guard rules diff --git a/Gemfile.lock b/Gemfile.lock index db70ef34..d6a462c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,11 +29,22 @@ GEM minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) + activeuuid (0.5.0) + activerecord (>= 3.1) + uuidtools acts_as_commentable_with_threading (1.2.0) activerecord (>= 3.0) activesupport (>= 3.0) awesome_nested_set (>= 2.0) addressable (2.3.6) + ahoy_matey (1.0.0) + addressable + browser (>= 0.4.0) + geocoder + referer-parser + request_store + user_agent_parser + uuidtools arel (5.0.1.20140414130214) ast (2.0.0) awesome_nested_set (3.0.0.rc.5) @@ -48,6 +59,7 @@ GEM bcrypt (3.1.7) bootstrap-sass (3.1.1.1) sass (~> 3.2) + browser (0.6.0) builder (3.2.2) cancan (1.6.10) capybara (2.2.1) @@ -111,6 +123,7 @@ GEM actionpack (>= 3.0) formtastic-bootstrap (3.0.0) formtastic (>= 2.2) + geocoder (1.2.2) gravtastic (3.2.6) guard (2.6.0) formatador (>= 0.2.4) @@ -258,6 +271,8 @@ GEM loggability (~> 0.6) rdoc (~> 4.0) yajl-ruby (~> 1.1) + referer-parser (0.2.1) + request_store (1.0.6) rest-client (1.6.7) mime-types (>= 1.16) rspec (3.0.0.beta2) @@ -343,6 +358,8 @@ GEM uglifier (2.5.0) execjs (>= 0.3.0) json (>= 1.8.0) + user_agent_parser (2.1.5) + uuidtools (2.1.4) warden (1.2.3) rack (>= 1.0) xpath (2.0.0) @@ -354,7 +371,9 @@ PLATFORMS DEPENDENCIES active_model_serializers + activeuuid acts_as_commentable_with_threading + ahoy_matey awesome_nested_set (~> 3.0.0.rc.5) axlsx_rails bootstrap-sass diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 512a0179..35653050 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -22,6 +22,7 @@ //= require d3 //= require osem //= require dashboard +//= require ahoy $(document).ready(function() { $('a[disabled=disabled]').click(function(event){ diff --git a/app/assets/javascripts/osem.js b/app/assets/javascripts/osem.js index 3cbb66e5..2fe1c40d 100644 --- a/app/assets/javascripts/osem.js +++ b/app/assets/javascripts/osem.js @@ -1,4 +1,41 @@ $(function() { + /** + * Opens a prompt with the URL to copy to clipboard. + * Used in the campaign index view. + */ + $('.copyLink').on('click', function(){ + var url = $(this).data('url'); + copyToClipboard(url); + }) + function copyToClipboard(text) { + window.prompt("Copy to clipboard: Ctrl+C, Enter", text); + } + + /** + * Toggles the targets on the conference site with a more / less link. + */ + $('.show_targets').click(function () { + if($(this).text().trim() == 'more'){ + $(this).text("less"); + }else{ + $(this).text("more"); + } + $('#' + $(this).data('name')).toggle(); + }); + + /** + * Appends the datetimepicker to new injected nested target fields. + */ + $('a:contains("Add target")').click(function () { + setTimeout(function () { + $('.target-due-date-datepicker').not('.hasDatepicker').datepicker({ + dateFormat: 'yy/mm/dd', + numberOfMonths: 1 + }); + }, + 5) + }); + $("#event_media_type").change(function () { $(".media-type").hide(); $('#' + $(this).val().toLowerCase() + '-help').show(); @@ -27,6 +64,11 @@ $(function() { } }); + $(".target-due-date-datepicker").datepicker({ + dateFormat: 'yy/mm/dd', + numberOfMonths: 1 + }); + $("#cfp-hard-datepicker").datepicker({ dateFormat: 'yy/mm/dd', numberOfMonths: 2, diff --git a/app/assets/stylesheets/osem.css b/app/assets/stylesheets/osem.css index 5adccac6..089acc74 100644 --- a/app/assets/stylesheets/osem.css +++ b/app/assets/stylesheets/osem.css @@ -27,8 +27,8 @@ body { padding: 15px; } -.nav > li.nav-header > a { - font-size: 12px; +.nav-header-bigger { + font-size: 15px; font-weight: bold; text-transform: uppercase; } diff --git a/app/controllers/admin/campaigns_controller.rb b/app/controllers/admin/campaigns_controller.rb new file mode 100644 index 00000000..68c4521d --- /dev/null +++ b/app/controllers/admin/campaigns_controller.rb @@ -0,0 +1,65 @@ +module Admin + class CampaignsController < ApplicationController + before_filter :verify_organizer + + def index + @conference = Conference.find_by(short_title: params[:conference_id]) + @campaigns = @conference.campaigns + end + + def create + @conference = Conference.find_by(short_title: params[:conference_id]) + @campaign = @conference.campaigns.new(params[:campaign]) + @campaign.conference_id = @conference.id + + if @conference.save + redirect_to(admin_conference_campaigns_path(conference_id: @conference.short_title), + notice: 'Campaign successfully created.') + else + redirect_to(new_admin_conference_campaign_path(conference_id: @conference.short_title), + alert: "Creating of Campaign for #{@conference.short_title} failed." \ + "#{@campaign.errors.full_messages.join('. ')}.") + end + end + + def new + @conference = Conference.find_by(short_title: params[:conference_id]) + @campaign = @conference.campaigns.new + end + + def edit + @conference = Conference.find_by(short_title: params[:conference_id]) + @campaign = Campaign.find(params[:id]) + end + + def update + @conference = Conference.find_by(short_title: params[:conference_id]) + @campaign = Campaign.find(params[:id]) + + if @campaign.update_attributes(params[:campaign]) + redirect_to(admin_conference_campaigns_path( + conference_id: @conference.short_title), + notice: "Campaign '#{@campaign.name}' successfully updated.") + else + redirect_to(edit_admin_conference_campaign_path( + conference_id: @conference.short_title, + id: @campaign.id), + alert: "Update of Campaign for #{@conference.short_title} failed." \ + "#{@campaign.errors.full_messages.join('. ')}.") + end + end + + def destroy + @conference = Conference.find_by(short_title: params[:conference_id]) + @campaign = Campaign.find(params[:id]) + if @campaign.destroy + redirect_to(admin_conference_campaigns_path(conference_id: @conference.short_title), + notice: "Campaign '#{@campaign.name}' successfully deleted.") + else + redirect_to(admin_conference_campaigns_path(conference_id: @conference.short_title), + alert: "Delete of Campaign for #{@conference.short_title} failed." \ + "#{@campaign.errors.full_messages.join('. ')}.") + end + end + end +end diff --git a/app/controllers/admin/conference_controller.rb b/app/controllers/admin/conference_controller.rb index 95eff08d..6093d2d2 100644 --- a/app/controllers/admin/conference_controller.rb +++ b/app/controllers/admin/conference_controller.rb @@ -144,6 +144,14 @@ class Admin::ConferenceController < ApplicationController @top_submitter = @conference.get_top_submitter + # get targets + @registration_targets = @conference.get_targets(Target.units[:registrations]) + @submission_targets = @conference.get_targets(Target.units[:submissions]) + @program_minutes_targets = @conference.get_targets(Target.units[:program_minutes]) + + # get campaigns + @campaigns = @conference.get_campaigns + respond_to do |format| format.html format.json { render json: @conference.to_json } diff --git a/app/controllers/admin/targets_controller.rb b/app/controllers/admin/targets_controller.rb new file mode 100644 index 00000000..94562de3 --- /dev/null +++ b/app/controllers/admin/targets_controller.rb @@ -0,0 +1,21 @@ +module Admin + class TargetsController < ApplicationController + before_filter :verify_organizer + + def index + end + + def update + if @conference.update_attributes(params[:conference]) + redirect_to(admin_conference_targets_path( + conference_id: @conference.short_title), + notice: 'Targets were successfully updated.') + else + redirect_to(admin_conference_targets_path( + conference_id: @conference.short_title), + alert: 'Targets update failed: ' \ + "#{@conference.errors.full_messages.join('. ')}") + end + end + end +end diff --git a/app/controllers/conference_registration_controller.rb b/app/controllers/conference_registration_controller.rb index 266bc0a3..a7710b18 100644 --- a/app/controllers/conference_registration_controller.rb +++ b/app/controllers/conference_registration_controller.rb @@ -75,6 +75,8 @@ class ConferenceRegistrationController < ApplicationController if update_registration redirect_message = "Registration updated." else + # Track ahoy event + ahoy.track 'Registered', title: 'New registration' if conference.email_settings.send_on_registration? Mailbot.registration_mail(conference, current_user.person).deliver end diff --git a/app/controllers/proposal_controller.rb b/app/controllers/proposal_controller.rb index 6c98b95d..095952f0 100644 --- a/app/controllers/proposal_controller.rb +++ b/app/controllers/proposal_controller.rb @@ -137,6 +137,7 @@ class ProposalController < ApplicationController end registration = person.registrations.where(:conference_id => @conference.id).first + ahoy.track 'Event submission', title: 'New submission' if registration.nil? redirect_to(register_conference_path(@conference.short_title), :notice => 'Event was successfully submitted. You probably want to register for the conference now!') else diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index cc90e9ee..58fdf091 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,4 +1,28 @@ module ApplicationHelper + def target_progress_color(progress) + progress = progress.to_i + if progress > 90 + result = 'green' + elsif progress < 90 && progress > 80 + result = 'orange' + else + result = 'red' + end + result + end + + def days_left_color(days_left) + days_left = days_left.to_i + if days_left > 30 + result = 'green' + elsif days_left < 30 && days_left > 10 + result = 'orange' + else + result = 'red' + end + result + end + def bootstrap_class_for(flash_type) logger.debug "flash_type is #{flash_type}" case flash_type diff --git a/app/models/ahoy/event.rb b/app/models/ahoy/event.rb new file mode 100644 index 00000000..7599143a --- /dev/null +++ b/app/models/ahoy/event.rb @@ -0,0 +1,10 @@ +module Ahoy + class Event < ActiveRecord::Base + self.table_name = "ahoy_events" + + belongs_to :visit + belongs_to :user + + serialize :properties, JSON + end +end diff --git a/app/models/campaign.rb b/app/models/campaign.rb new file mode 100644 index 00000000..0228406b --- /dev/null +++ b/app/models/campaign.rb @@ -0,0 +1,77 @@ +class Campaign < ActiveRecord::Base + attr_accessible :name, :utm_source, :utm_medium, :utm_term, + :utm_content, :utm_campaign, :target_ids + + validates :name, :utm_campaign, presence: true + + has_many :targets + belongs_to :conference + + ## + # Returns the utm parameters formatted as url. + # + # ====Returns + # * +String+ -> url parameters e.g. ?utm_source=facebook + def url_parameters + kv = [] + get_parameters.each_pair do |k, v| + kv += ["#{k}=#{v}"] + end + '?' + kv.join('&') unless kv.empty? + end + + ## + # Returns the counted visits generated by this campaign. + # + # ====Returns + # * +Fixnum+ -> visits + def visits_count + Visit.where(get_parameters).where('started_at > ?', created_at).count + end + + ## + # Returns the counted registrations generated by this campaign. + # + # ====Returns + # * +Fixnum+ -> visits + def registrations_count + events_by_name('Registered') + end + + ## + # Returns the counted event submissions generated by this campaign. + # + # ====Returns + # * +Fixnum+ -> visits + def submissions_count + events_by_name('Event submission') + end + + private + + ## + # Helper method for submissions and registrations. + # + # ====Returns + # * +Fixnum+ -> registrations / submissions + def events_by_name(event_name) + parameters = get_parameters + parameters['ahoy_events.name'] = event_name + Visit.joins(:ahoy_events).where(parameters).where('started_at > ?', created_at).count + end + + ## + # Helper method to get the parameters for queries. + # + # ====Returns + # * +Hash+ -> parameter => value + def get_parameters + conditions = {} + conditions[:utm_source] = self[:utm_source] unless self[:utm_source].blank? + conditions[:utm_medium] = self[:utm_medium] unless self[:utm_medium].blank? + conditions[:utm_term] = self[:utm_term] unless self[:utm_term].blank? + conditions[:utm_content] = self[:utm_content] unless self[:utm_content].blank? + conditions[:utm_campaign] = self[:utm_campaign] unless self[:utm_campaign].blank? + conditions + end +end diff --git a/app/models/conference.rb b/app/models/conference.rb index dc71e9cc..5bfdd282 100644 --- a/app/models/conference.rb +++ b/app/models/conference.rb @@ -21,7 +21,8 @@ class Conference < ActiveRecord::Base :include_tickets_in_splash, :include_social_media_in_splash, :include_program_in_splash, :make_conference_public, :photos_attributes, :banner_photo, - :include_banner_in_splash + :include_banner_in_splash, + :targets, :targets_attributes, :campaigns, :campaigns_attributes has_paper_trail @@ -45,6 +46,8 @@ class Conference < ActiveRecord::Base has_many :sponsorship_levels, dependent: :destroy has_many :sponsors, dependent: :destroy has_many :photos, dependent: :destroy + has_many :targets, dependent: :destroy + has_many :campaigns, dependent: :destroy belongs_to :venue accepts_nested_attributes_for :rooms, :reject_if => proc {|r| r["name"].blank?}, :allow_destroy => true @@ -62,6 +65,8 @@ class Conference < ActiveRecord::Base accepts_nested_attributes_for :vdays, :allow_destroy => true accepts_nested_attributes_for :vpositions, :allow_destroy => true accepts_nested_attributes_for :photos, allow_destroy: true + accepts_nested_attributes_for :targets, allow_destroy: true + accepts_nested_attributes_for :campaigns, allow_destroy: true has_attached_file :logo, styles: { thumb: '100x100>', large: '300x300>' } @@ -457,6 +462,35 @@ class Conference < ActiveRecord::Base result end + ## + # A map with all conference targets with progress in percent of a certain unit. + # + # ====Returns + # * +Map+ -> target => progress + def get_targets(target_unit) + conference_target = targets.where('unit = ?', target_unit) + result = {} + conference_target.each do |target| + result[target.to_s] = target.get_progress + end + result + end + + ## + # A map with all conference campaigns associated with targets. + # + # ====Returns + # * +Map+ -> campaign => {actual, target, progress} + def get_campaigns + result = {} + campaigns.each do |campaign| + campaign.targets.each do |target| + result["#{target} from #{campaign.name}"] = target.get_campaign + end + end + result + end + private ## @@ -655,12 +689,12 @@ class Conference < ActiveRecord::Base # # ====Returns # * +hash+ -> person: submissions - def self.calculate_person_submission_hash(submitter, counter) + def self.calculate_person_submission_hash(submitters, counter) result = ActiveSupport::OrderedHash.new counter.each do |key, value| - submitter = submitter.find_by_id(key) + submitter = submitters.where(person_id: key).first if submitter - result[submitter] = value + result[submitter.person] = value end end result diff --git a/app/models/target.rb b/app/models/target.rb new file mode 100644 index 00000000..6a7990a8 --- /dev/null +++ b/app/models/target.rb @@ -0,0 +1,79 @@ +class Target < ActiveRecord::Base + include ActionView::Helpers::TextHelper + + attr_accessible :due_date, :target_count, :unit + + default_scope { order('due_date ASC') } + + def self.units + { + registrations: 'Registration', + submissions: 'Submission', + program_minutes: 'Program minute' + } + end + + validates :due_date, :target_count, :unit, presence: true + validates :target_count, + allow_nil: false, + numericality: { only_integer: true, greater_than: 0 } + validates :unit, allow_nil: false, inclusion: { in: Target.units.values } + + belongs_to :conference + belongs_to :campaign + + ## + # Returns the actual progress of the target in percent. + # + # ====Returns + # * +String+ -> progress in percent + def get_progress + numerator = 0 + if unit == Target.units[:submissions] + numerator = conference.events.where('created_at < ?', due_date).count + elsif unit == Target.units[:registrations] + numerator = conference.registrations.where('created_at < ?', due_date).count + elsif unit == Target.units[:program_minutes] + numerator = conference.current_program_hours + end + (numerator / target_count.to_f * 100).round(0).to_s + end + + ## + # Returns a hash with values of the corresponding campaign. + # + # ====Returns + # * +Hash+ -> target_name, campaign_name, value, unit, created_at, progress, days_left + def get_campaign + numerator = 0 + if unit == Target.units[:submissions] + numerator = campaign.submissions_count + elsif unit == Target.units[:registrations] + numerator = campaign.registrations_count + elsif unit == Target.units[:program_minutes] + numerator = campaign.current_program_hours + end + + progress = (numerator / target_count.to_f * 100).round(0).to_s + result = { + 'target_name' => to_s, + 'campaign_name' => campaign.name, + 'value' => numerator, + 'unit' => unit, + 'created_at' => created_at, + 'progress' => progress, + 'days_left' => days_left, + } + result + end + + def to_s + "#{pluralize(target_count, unit)} by #{due_date}" + end + + private + + def days_left + (due_date - Date.today).to_i + end +end diff --git a/app/models/visit.rb b/app/models/visit.rb new file mode 100644 index 00000000..6bb47fed --- /dev/null +++ b/app/models/visit.rb @@ -0,0 +1,4 @@ +class Visit < ActiveRecord::Base + has_many :ahoy_events, class_name: "Ahoy::Event" + belongs_to :user +end diff --git a/app/views/admin/campaigns/_form.html.haml b/app/views/admin/campaigns/_form.html.haml new file mode 100644 index 00000000..3493849c --- /dev/null +++ b/app/views/admin/campaigns/_form.html.haml @@ -0,0 +1,12 @@ += f.inputs do + = f.input :name + = f.inputs name: 'UTM Parameters' do + = f.input :utm_campaign, label: 'Campaign', hint: 'Groups all of the content form one campaign. E.g. 20percentpromocode' + = f.input :utm_source, label: 'Source', hint: 'Which website is sending you traffic. E.g. Facebook, google+, blog' + = f.input :utm_medium, label: 'Medium', hint: 'The type of marketing medium that the link is featured in. E.g. Facebook wallpost or facebook advertisement' + = f.input :utm_term, label: 'Term', hint: 'Campaign keywords. E.g. marketing+conference+opensource' + = f.input :utm_content, label: 'Content', hint: 'Used to track the different types of content that point to the same URL (A/B Test).' + = f.inputs name: 'Targets' do + = f.input :targets += f.actions do + = f.action :submit, button_html: { class: 'btn btn-primary' } diff --git a/app/views/admin/campaigns/edit.html.haml b/app/views/admin/campaigns/edit.html.haml new file mode 100644 index 00000000..db20bce2 --- /dev/null +++ b/app/views/admin/campaigns/edit.html.haml @@ -0,0 +1,2 @@ += semantic_form_for(@campaign, url: admin_conference_campaign_path(@conference.short_title, @campaign)) do |f| + = render partial: 'form', locals: {f: f} diff --git a/app/views/admin/campaigns/index.html.haml b/app/views/admin/campaigns/index.html.haml new file mode 100644 index 00000000..fd59016f --- /dev/null +++ b/app/views/admin/campaigns/index.html.haml @@ -0,0 +1,41 @@ +%h1 + Campaigns +.row + .col-md-12 + .table-responsive + %table.table + %thead + %tr + %th # + %th Name + %th Visits + %th Registrations + %th Submissions + %th Link + %th Edit + %th Delete + %tbody + - @campaigns.each_with_index do |campaign, index| + %tr + %td + = index + 1 + %td{'id'=> "name_#{index}"} + = campaign.name + %td{'id'=> "visits_#{index}"} + = campaign.visits_count + %td{'id'=> "registrations_#{index}"} + = campaign.registrations_count + %td{'id'=> "submissions_#{ + index}"} + = campaign.submissions_count + %td + %a.copyLink{'href'=> '#', 'data-url'=>root_path + campaign.url_parameters} + Copy link + %td + = link_to 'Edit', + edit_admin_conference_campaign_path(@conference.short_title, campaign.id) + %td + = link_to 'Delete', + admin_conference_campaign_path(@conference.short_title, campaign.id), method: :delete +.row + .col-md-12 + %b= link_to 'New Campaign', new_admin_conference_campaign_path, class: 'btn btn-success' diff --git a/app/views/admin/campaigns/new.html.haml b/app/views/admin/campaigns/new.html.haml new file mode 100644 index 00000000..aba7247d --- /dev/null +++ b/app/views/admin/campaigns/new.html.haml @@ -0,0 +1,2 @@ += semantic_form_for(@campaign, url: admin_conference_campaigns_path(@conference.short_title)) do |f| + = render partial: 'form', locals: {f: f} diff --git a/app/views/admin/conference/_campaigns.html.haml b/app/views/admin/conference/_campaigns.html.haml new file mode 100644 index 00000000..8a3dd9d2 --- /dev/null +++ b/app/views/admin/conference/_campaigns.html.haml @@ -0,0 +1,40 @@ +- if campaigns && !campaigns.empty? + .row + .col-md-12 + %p + Your target "#{campaigns.values[0]['target_name']}" from campaign "#{campaigns.values[0]['campaign_name']}" has generated + %strong + #{pluralize(campaigns.values[0]['value'], campaigns.values[0]['unit'])} + since #{campaigns.values[0]['created_at']}. + %p + That is + %strong{'style'=>"color: #{target_progress_color(campaigns.values[0]['progress'])};"} + #{campaigns.values[0]['progress']} % + of your target, there are + %strong{'style'=>"color: #{days_left_color(campaigns.values[0]['days_left'])};"} + #{pluralize(campaigns.values[0]['days_left'], 'day')} left. + .row + .col-md-12 + %div{'style'=>'display: none;', 'id'=>"#{name}"} + - campaigns.drop(1).each do |(key, value)| + %div + %p + Your target "#{value['target_name']}" from campaign "#{value['campaign_name']}" has generated + %strong + #{pluralize(value['value'], value['unit'])} + since #{value['created_at']}. + %p + That is + %strong{'style'=>"color: #{target_progress_color(value['progress'])};"} + #{value['progress']} % + of your target, there are + %strong{'style'=>"color: #{days_left_color(value['days_left'])};"} + #{pluralize(value['days_left'], 'day')} left. + .row + .col-md-12 + - if campaigns.length > 1 + %a.show_targets{'href'=>'#', 'data-name'=>"#{name}"} + more +- else + %h5.text-warning.text-center + No Campaigns! \ No newline at end of file diff --git a/app/views/admin/conference/_targets.html.haml b/app/views/admin/conference/_targets.html.haml new file mode 100644 index 00000000..682d02ca --- /dev/null +++ b/app/views/admin/conference/_targets.html.haml @@ -0,0 +1,32 @@ +- if targets && !targets.empty? + .row + .col-md-8.col-md-offset-2 + .row + .col-md-10 + %h6.text-muted.pull-left + Target 1 + .row + .col-md-10 + .progress{'title'=>"#{targets.keys[0]}"} + .progress-bar{ 'role'=>'progressbar', 'aria-valuenow'=>"#{targets.values[0]}", 'aria-valuemin'=>'0', + 'aria-valuemax'=>'100', 'style'=>"width: #{targets.values[0]}%;"} + = "#{targets.values[0]} %" + .row + .col-md-8.col-md-offset-2 + %div{'style'=>'display: none;', 'id'=>"#{name}"} + - targets.drop(1).each_with_index do |(key, value), index| + .row + .col-md-10 + %h6.text-muted.pull-left + = "Target #{index + 2}" + .row + .col-md-10 + .progress{'title'=>"#{key}"} + .progress-bar{ 'role'=>'progressbar', 'aria-valuenow'=>"#{value}", 'aria-valuemin'=>'0', + 'aria-valuemax'=>'100', 'style'=>"width: #{value}%;" } + = "#{value} %" + .row + .col-md-2.col-md-offset-2 + - if targets.length > 1 + %a.show_targets{'href'=>'#', 'data-name'=>"#{name}"} + more diff --git a/app/views/admin/conference/show.html.haml b/app/views/admin/conference/show.html.haml index 3d0f68b0..f9e8f8d8 100644 --- a/app/views/admin/conference/show.html.haml +++ b/app/views/admin/conference/show.html.haml @@ -10,6 +10,8 @@ .text %label.text-muted total registrations: #{@total_reg} %label.text-muted new registrations: #{@new_reg} + .row + = render partial: 'targets', locals: { targets: @registration_targets, name: 'registrations' } .col-sm-4 .dashbox.text-center .icon @@ -17,13 +19,17 @@ .text %label.text-muted total submissions: #{@total_submissions} %label.text-muted new submissions: #{@new_submissions} + .row + = render partial: 'targets', locals: { targets: @submission_targets, name: 'submissions' } .col-sm-4 .dashbox.text-center .icon %i.glyphicon.glyphicon-user .text - %label.text-muted programm hours: #{@program_length} h - %label.text-muted new programm hours: #{@new_program_length} h + %label.text-muted programm: #{@program_length} min + %label.text-muted new programm: #{@new_program_length} min + .row + = render partial: 'targets', locals: { targets: @program_minutes_targets, name: 'program_hours' } .row .col-md-12 .row @@ -97,6 +103,13 @@ = render partial: 'recent_submissions', locals: {recent_events: @recent_events} .col-md-4 = render partial: 'top_submitter', locals: {top_submitter: @top_submitter} +.row + .col-md-12 + %h3 + %span + %i.glyphicon.glyphicon-flag + Campaigns + = render partial: 'campaigns', locals: {campaigns: @campaigns, name: 'campaigns'} :javascript $('#recentTable a').click(function (e) { diff --git a/app/views/admin/targets/_target_fields.html.haml b/app/views/admin/targets/_target_fields.html.haml new file mode 100644 index 00000000..b64e44d5 --- /dev/null +++ b/app/views/admin/targets/_target_fields.html.haml @@ -0,0 +1,6 @@ +.nested-fields + = f.inputs do + = f.input :due_date, as: :string, input_html: { class: 'target-due-date-datepicker', readonly: 'readonly' } + = f.input :target_count + = f.input :unit, as: :select, label: 'Unit', class: 'form-control', collection: Target.units.values, include_blank: false + = remove_association_link :target, f \ No newline at end of file diff --git a/app/views/admin/targets/index.html.haml b/app/views/admin/targets/index.html.haml new file mode 100644 index 00000000..001e4042 --- /dev/null +++ b/app/views/admin/targets/index.html.haml @@ -0,0 +1,5 @@ +.row + .col-md-8 + = semantic_form_for(@conference, url: admin_conference_target_path(@conference.short_title, @conference.targets)) do |f| + = dynamic_association :targets, 'Targets', f + = f.action :submit, as: :button, button_html: { class: 'btn btn-primary' } diff --git a/app/views/layouts/_admin_sidebar.html.haml b/app/views/layouts/_admin_sidebar.html.haml index d8a6cc2c..f9eddd03 100644 --- a/app/views/layouts/_admin_sidebar.html.haml +++ b/app/views/layouts/_admin_sidebar.html.haml @@ -20,26 +20,38 @@ %span.glyphicon.glyphicon-plus New Conference %hr - %li{:class=> active_nav_li(admin_conference_registrations_path(@conference.short_title))} - = link_to(admin_conference_registrations_path(@conference.short_title)) do - %span.glyphicon.glyphicon-user - Registrations + %li{:class=> "#{active_nav_li(admin_conference_path(@conference.short_title))} nav-header nav-header-bigger"} + = link_to(admin_conference_path(@conference.short_title)) do + %span.glyphicon.glyphicon-dashboard + Manage %li{:class=> active_nav_li(admin_conference_events_path(@conference.short_title))} = link_to(admin_conference_events_path(@conference.short_title)) do %span.glyphicon.glyphicon-comment Events + %li{:class=> active_nav_li(admin_conference_registrations_path(@conference.short_title))} + = link_to(admin_conference_registrations_path(@conference.short_title)) do + %span.glyphicon.glyphicon-user + Registrations %li{class: active_nav_li(admin_conference_schedule_path(@conference.short_title))} = link_to(admin_conference_schedule_path(@conference.short_title), target: '_blank') do %span.glyphicon.glyphicon-calendar Schedule + %li{class: active_nav_li(admin_conference_campaigns_path(@conference.short_title))} + = link_to(admin_conference_campaigns_path(@conference.short_title)) do + %span.glyphicon.glyphicon-bullhorn + Campaigns %hr - %li{:class=> "#{active_nav_li(edit_admin_conference_path(@conference.short_title))} nav-header"} + %li{:class=> "#{active_nav_li(edit_admin_conference_path(@conference.short_title))} nav-header nav-header-bigger"} = link_to(edit_admin_conference_path(@conference.short_title)) do %span.glyphicon.glyphicon-cog Settings + %li{:class=> "#{active_nav_li(admin_conference_targets_path(@conference.short_title))}"} + = link_to(admin_conference_targets_path(@conference.short_title)) do + %span.glyphicon.glyphicon-flag + Targets %li{:class=> "#{active_nav_li(admin_conference_venue_info_path(@conference.short_title))} myAccordion"} = link_to(admin_conference_venue_info_path(@conference.short_title)) do - %span.glyphicon.glyphicon-calendar + %span.glyphicon.glyphicon-road Venue %span.small.glyphicon.glyphicon-chevron-right %ul.nav.nav-stacked.nav-pills.small.collapse.subNav diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb new file mode 100644 index 00000000..22d326e5 --- /dev/null +++ b/config/initializers/ahoy.rb @@ -0,0 +1,3 @@ +class Ahoy::Store < Ahoy::Stores::ActiveRecordStore + # customize here +end diff --git a/config/routes.rb b/config/routes.rb index 6d5084ff..a907ff77 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,10 @@ Osem::Application.routes.draw do resources :lodgings, only: [:show, :update, :index] + resources :targets, only: [:update, :index] + + resources :campaigns + resources :eventtypes, only: [:show, :index] do collection do patch :update diff --git a/db/migrate/20140616132153_create_targets.rb b/db/migrate/20140616132153_create_targets.rb new file mode 100644 index 00000000..0295816e --- /dev/null +++ b/db/migrate/20140616132153_create_targets.rb @@ -0,0 +1,12 @@ +class CreateTargets < ActiveRecord::Migration + def change + create_table :targets do |t| + t.integer :conference_id + t.integer :campaign_id + t.date :due_date + t.integer :target_count + t.string :unit + t.timestamps + end + end +end diff --git a/db/migrate/20140616132456_create_campaigns.rb b/db/migrate/20140616132456_create_campaigns.rb new file mode 100644 index 00000000..9e696b68 --- /dev/null +++ b/db/migrate/20140616132456_create_campaigns.rb @@ -0,0 +1,14 @@ +class CreateCampaigns < ActiveRecord::Migration + def change + create_table :campaigns do |t| + t.integer :conference_id + t.string :name + t.string :utm_source + t.string :utm_medium + t.string :utm_term + t.string :utm_content + t.string :utm_campaign + t.timestamps + end + end +end diff --git a/db/migrate/20140623100942_create_visits.rb b/db/migrate/20140623100942_create_visits.rb new file mode 100644 index 00000000..ca5e325b --- /dev/null +++ b/db/migrate/20140623100942_create_visits.rb @@ -0,0 +1,51 @@ +class CreateVisits < ActiveRecord::Migration + def change + create_table :visits, id: false do |t| + t.uuid :id, primary_key: true + t.uuid :visitor_id + + # the rest are recommended but optional + # simply remove the columns you don't want + + # standard + t.string :ip + t.text :user_agent + t.text :referrer + t.text :landing_page + + # user + t.integer :user_id + # add t.string :user_type if polymorphic + + # traffic source + t.string :referring_domain + t.string :search_keyword + + # technology + t.string :browser + t.string :os + t.string :device_type + + # location + t.string :country + t.string :region + t.string :city + + # utm parameters + t.string :utm_source + t.string :utm_medium + t.string :utm_term + t.string :utm_content + t.string :utm_campaign + + # native apps + # t.string :platform + # t.string :app_version + # t.string :os_version + + t.timestamp :started_at + end + + add_index :visits, [:user_id] + end +end diff --git a/db/migrate/20140623101032_create_ahoy_events.rb b/db/migrate/20140623101032_create_ahoy_events.rb new file mode 100644 index 00000000..de6a5205 --- /dev/null +++ b/db/migrate/20140623101032_create_ahoy_events.rb @@ -0,0 +1,20 @@ +class CreateAhoyEvents < ActiveRecord::Migration + def change + create_table :ahoy_events, id: false do |t| + t.uuid :id, primary_key: true + t.uuid :visit_id + + # user + t.integer :user_id + # add t.string :user_type if polymorphic + + t.string :name + t.text :properties + t.timestamp :time + end + + add_index :ahoy_events, [:visit_id] + add_index :ahoy_events, [:user_id] + add_index :ahoy_events, [:time] + end +end diff --git a/db/schema.rb b/db/schema.rb index d140e248..563673c2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,20 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140620134535) do +ActiveRecord::Schema.define(version: 20140623101032) do + + create_table "ahoy_events", force: true do |t| + t.uuid "visit_id" + t.integer "user_id" + t.string "name" + t.text "properties" + t.datetime "time" + end + + #add_index "ahoy_events", ["id"], name: "sqlite_autoindex_ahoy_events_1", unique: true + add_index "ahoy_events", ["time"], name: "index_ahoy_events_on_time" + add_index "ahoy_events", ["user_id"], name: "index_ahoy_events_on_user_id" + add_index "ahoy_events", ["visit_id"], name: "index_ahoy_events_on_visit_id" create_table "answers", force: true do |t| t.string "title" @@ -32,6 +45,18 @@ ActiveRecord::Schema.define(version: 20140620134535) do t.boolean "include_cfp_in_splash", default: false end + create_table "campaigns", force: true do |t| + t.integer "conference_id" + t.string "name" + t.string "utm_source" + t.string "utm_medium" + t.string "utm_term" + t.string "utm_content" + t.string "utm_campaign" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "comments", force: true do |t| t.string "title", limit: 50, default: "" t.text "body" @@ -381,6 +406,16 @@ ActiveRecord::Schema.define(version: 20140620134535) do t.datetime "created_at" end + create_table "targets", force: true do |t| + t.integer "conference_id" + t.integer "campaign_id" + t.date "due_date" + t.integer "target_count" + t.string "unit" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "tracks", force: true do |t| t.string "guid", null: false t.integer "conference_id" @@ -457,6 +492,32 @@ ActiveRecord::Schema.define(version: 20140620134535) do add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" + create_table "visits", force: true do |t| + t.uuid "visitor_id" + t.string "ip" + t.text "user_agent" + t.text "referrer" + t.text "landing_page" + t.integer "user_id" + t.string "referring_domain" + t.string "search_keyword" + t.string "browser" + t.string "os" + t.string "device_type" + t.string "country" + t.string "region" + t.string "city" + t.string "utm_source" + t.string "utm_medium" + t.string "utm_term" + t.string "utm_content" + t.string "utm_campaign" + t.datetime "started_at" + end + + #add_index "visits", ["id"], name: "sqlite_autoindex_visits_1", unique: true + add_index "visits", ["user_id"], name: "index_visits_on_user_id" + create_table "votes", force: true do |t| t.integer "person_id" t.integer "event_id" diff --git a/spec/factories/campaigns.rb b/spec/factories/campaigns.rb new file mode 100644 index 00000000..a773d420 --- /dev/null +++ b/spec/factories/campaigns.rb @@ -0,0 +1,8 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :campaign do + name 'Test Campaign' + utm_campaign 'testcampaign' + end +end diff --git a/spec/factories/targets.rb b/spec/factories/targets.rb new file mode 100644 index 00000000..ea9d4e9f --- /dev/null +++ b/spec/factories/targets.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :target do + due_date Date.today + 14 + target_count 100 + unit Target.units[:submissions] + end +end diff --git a/spec/factories/visits.rb b/spec/factories/visits.rb new file mode 100644 index 00000000..3a03b203 --- /dev/null +++ b/spec/factories/visits.rb @@ -0,0 +1,6 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :visit do + end +end diff --git a/spec/features/campaign_spec.rb b/spec/features/campaign_spec.rb new file mode 100644 index 00000000..9c71a759 --- /dev/null +++ b/spec/features/campaign_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +feature Campaign 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) } + + shared_examples 'add and update campaign' do |user| + scenario 'adds and update a campaign', feature: true, js: true do + expected_count = Campaign.count + 1 + conference = create(:conference, short_title: 'osc14') + sign_in create(user) + + visit admin_conference_campaigns_path(conference.short_title) + + click_link 'New Campaign' + + click_button 'Create Campaign' + + expect(flash). + to eq("Creating of Campaign for osc14 failed.Name can't be blank. Utm campaign can't be blank.") + + fill_in 'campaign_name', with: 'Test Campaign' + fill_in 'campaign_utm_campaign', with: 'campaign' + fill_in 'campaign_utm_source', with: 'source' + fill_in 'campaign_utm_medium', with: 'medium' + fill_in 'campaign_utm_term', with: 'term' + fill_in 'campaign_utm_content', with: 'content' + + click_button 'Create Campaign' + + # Validations + expect(flash). + to eq('Campaign successfully created.') + + expect(find('#name_0').text).to eq('Test Campaign') + expect(find('#visits_0').text).to eq('0') + expect(find('#registrations_0').text).to eq('0') + expect(find('#submissions_0').text).to eq('0') + + expect(Campaign.count).to eq(expected_count) + + campaign = Campaign.where('name'=> 'Test Campaign').first + visit edit_admin_conference_campaign_path(conference.short_title, campaign.id) + + fill_in 'campaign_name', with: 'Test Campaign 42' + click_button 'Update Campaign' + expect(flash). + to eq("Campaign 'Test Campaign 42' successfully updated.") + end + end + + describe 'admin' do + it_behaves_like 'add and update campaign', :admin + it_behaves_like 'add and update campaign', :organizer + end +end diff --git a/spec/models/campaign_spec.rb b/spec/models/campaign_spec.rb new file mode 100644 index 00000000..e836d3ba --- /dev/null +++ b/spec/models/campaign_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' +require 'ahoy' + +describe Campaign do + describe 'validations' do + it 'has a valid factory' do + expect(build(:campaign)).to be_valid + end + + it 'is not valid without a name' do + should validate_presence_of(:name) + end + end + + describe '#url_parameters' do + it 'returns the parameters in the correct format' do + campaign = create(:campaign, utm_source: 'google+', utm_medium: 'advertisement', + utm_term: 'opensource', utm_content: 'content', utm_campaign: '20percent') + campaign.conference = create(:conference) + + result = '?utm_source=google+&utm_medium=advertisement&utm_term=opensource&utm_content=content&utm_campaign=20percent' + expect(campaign.url_parameters).to eq(result) + end + + it 'returns only utm_campaign parameter if there are no parameters' do + campaign = create(:campaign) + campaign.conference = create(:conference) + expect(campaign.url_parameters).to eq('?utm_campaign=testcampaign') + end + end + + describe '#visits' do + it 'returns one if there is one visit' do + campaign = create(:campaign, utm_source: 'google+', utm_medium: 'advertisement', + utm_term: 'opensource', utm_content: 'content', utm_campaign: '20percent') + campaign.conference = build(:conference) + + create(:visit, utm_source: 'google+', utm_medium: 'advertisement', + utm_term: 'opensource', utm_content: 'content', utm_campaign: '20percent', started_at: Time.now) + expect(campaign.visits_count).to eq(1) + end + + it 'returns zero if there are no visits' do + campaign = create(:campaign, utm_source: 'google+', utm_medium: 'advertisement', + utm_term: 'opensource', utm_content: 'content', utm_campaign: '20percent') + campaign.conference = create(:conference) + + expect(campaign.visits_count).to eq(0) + end + end + + describe '#registrations' do + it 'returns zero if there are no registration' do + campaign = build(:campaign, utm_source: 'google+', utm_medium: 'advertisement', + utm_term: 'opensource', utm_content: 'content', utm_campaign: '20percent') + campaign.conference = build(:conference) + + expect(campaign.registrations_count).to eq(0) + end + end + + describe '#submissions' do + it 'returns zero if there are no submissions' do + campaign = build(:campaign, utm_source: 'google+', utm_medium: 'advertisement', + utm_term: 'opensource', utm_content: 'content', utm_campaign: '20percent') + campaign.conference = build(:conference) + + expect(campaign.submissions_count).to eq(0) + end + end +end diff --git a/spec/models/conference_spec.rb b/spec/models/conference_spec.rb index f8853054..52ba893d 100644 --- a/spec/models/conference_spec.rb +++ b/spec/models/conference_spec.rb @@ -6,35 +6,98 @@ describe Conference do let(:subject) { create(:conference) } - # describe '#get_top_submitter' 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 'calculates correct hash with top submitters' do - # event = create(:event, conference: subject) - # result = { - # event.submitter => 1 - # } - # expect(subject.get_top_submitter).to eq(result) - # end - # - # it 'returns the submitter ordered by submissions' do - # e1 = create(:event, conference: subject) - # - # e2 = create(:event, conference: subject) - # e3 = create(:event, conference: subject) - # e4 = create(:event, conference: subject) - # - # e3.event_people = [create(:event_person, person: e2.submitter, event_role: 'submitter')] - # e4.event_people = [create(:event_person, person: e2.submitter, event_role: 'submitter')] - # - # expect(subject.get_top_submitter.values).to eq([3, 1]) - # expect(subject.get_top_submitter.keys).to eq([e2.submitter, e1.submitter]) - # end - # - # end + describe '#get_top_submitter' 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 'calculates correct hash with top submitters' do + event = create(:event, conference: subject) + result = { + event.submitter => 1 + } + expect(subject.get_top_submitter).to eq(result) + end + + it 'returns the submitter ordered by submissions' do + e1 = create(:event, conference: subject) + + e2 = create(:event, conference: subject) + e3 = create(:event, conference: subject) + e4 = create(:event, conference: subject) + + e3.event_people = [create(:event_person, person: e2.submitter, event_role: 'submitter')] + e4.event_people = [create(:event_person, person: e2.submitter, event_role: 'submitter')] + + expect(subject.get_top_submitter.values).to eq([3, 1]) + expect(subject.get_top_submitter.keys).to eq([e2.submitter, e1.submitter]) + end + + end + + describe '#get_targets' do + it 'returns 0 if there is no registration' do + target = build(:target, target_count: 10, unit: Target.units[:registrations]) + subject.targets = [target] + result = { + "10 Registrations by #{target.due_date}" => '0' + } + expect(subject.get_targets(Target.units[:registrations])).to eq(result) + end + + it 'returns 10 if there is 1 registration of 10' do + target = build(:target, target_count: 10, unit: Target.units[:registrations]) + subject.targets = [target] + subject.registrations = [create(:registration)] + result = { + "10 Registrations by #{target.due_date}" => '10' + } + expect(subject.get_targets(Target.units[:registrations])).to eq(result) + end + + it 'returns an empty hash if there is no target' do + expect(subject.get_targets(Target.units[:registrations])).to eq({}) + end + + it 'returns 0 if there is no submission' do + target = build(:target, target_count: 10, unit: Target.units[:submissions]) + subject.targets = [target] + result = { + "10 Submissions by #{target.due_date}" => '0' + } + expect(subject.get_targets(Target.units[:submissions])).to eq(result) + end + + it 'returns 10 if there is 1 submissions of 10' do + target = build(:target, target_count: 10, unit: Target.units[:submissions]) + subject.targets = [target] + subject.events = [create(:event)] + result = { + "10 Submissions by #{target.due_date}" => '10' + } + expect(subject.get_targets(Target.units[:submissions])).to eq(result) + end + + it 'returns 0 if there is no program minute' do + target = build(:target, target_count: 300, unit: Target.units[:program_minutes]) + subject.targets = [target] + result = { + "300 Program minutes by #{target.due_date}" => '0' + } + expect(subject.get_targets(Target.units[:program_minutes])).to eq(result) + end + + it 'returns 10 if there is 30 program minutes of 300' do + target = build(:target, target_count: 300, unit: Target.units[:program_minutes]) + subject.targets = [target] + subject.events = [create(:event)] + result = { + "300 Program minutes by #{target.due_date}" => '10' + } + expect(subject.get_targets(Target.units[:program_minutes])).to eq(result) + end + end describe 'program hours' do before(:each) do diff --git a/spec/models/target_spec.rb b/spec/models/target_spec.rb new file mode 100644 index 00000000..e57b5406 --- /dev/null +++ b/spec/models/target_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe Target do + + describe 'validations' do + it 'has a valid factory' do + expect(build(:target)).to be_valid + end + + it 'is not valid without a due date' do + should validate_presence_of(:due_date) + end + + it 'is not valid without a target_count' do + should validate_presence_of(:target_count) + end + + it 'is not valid without a unit' do + should validate_presence_of(:unit) + end + + it 'is valid with a target_count greater than zero' do + should allow_value(10).for(:target_count) + end + + it 'is not valid with a target_count equals zero' do + should_not allow_value(0).for(:target_count) + end + + it 'is not valid with a target_count smaller than zero' do + should_not allow_value(-10).for(:target_count) + end + end + + describe '#get_progress' do + it 'returns zero if there are no registrations' do + conference = build(:conference) + target = build(:target, target_count: 10, unit: Target.units[:registrations]) + conference.targets = [target] + + expect(target.get_progress).to eq('0') + end + + it 'returns 10 if there one registrations of 10' do + conference = create(:conference) + target = create(:target, target_count: 10, unit: Target.units[:registrations]) + registration = create(:registration) + + conference.targets = [target] + conference.registrations = [registration] + + expect(target.get_progress).to eq('10') + end + + it 'returns zero if there are no submissions' do + conference = build(:conference) + target = build(:target, target_count: 10, unit: Target.units[:submissions]) + conference.targets = [target] + + expect(target.get_progress).to eq('0') + end + + it 'returns 10 if there one submissions of 10' do + conference = create(:conference) + target = create(:target, target_count: 10, unit: Target.units[:submissions]) + event = create(:event) + + conference.targets = [target] + conference.events = [event] + + expect(target.get_progress).to eq('10') + end + + it 'returns zero if there are no program minutes' do + conference = build(:conference) + target = build(:target, target_count: 10, unit: Target.units[:program_minutes]) + conference.targets = [target] + + expect(target.get_progress).to eq('0') + end + + it 'returns 10 if there are 30 program minutes of 300' do + conference = create(:conference) + target = create(:target, target_count: 300, unit: Target.units[:program_minutes]) + event = create(:event) + + conference.targets = [target] + conference.events = [event] + + expect(target.get_progress).to eq('10') + end + end + + describe '#to_s' do + it 'returns a string in the correct format' do + conference = build(:conference) + target = build(:target, target_count: 10, unit: Target.units[:registrations]) + conference.targets = [target] + + result = "10 Registrations by #{Date.today + 14}" + + expect(target.to_s).to eq(result) + end + end +end