Implements targets and campaigns for conference

This commit is contained in:
Chrisbr 2014-06-26 09:08:26 +02:00
parent 678fe38a25
commit 857ac43e74
40 changed files with 1086 additions and 44 deletions

View file

@ -22,6 +22,7 @@
//= require d3
//= require osem
//= require dashboard
//= require ahoy
$(document).ready(function() {
$('a[disabled=disabled]').click(function(event){

View file

@ -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,

View file

@ -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;
}

View file

@ -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

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

10
app/models/ahoy/event.rb Normal file
View file

@ -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

77
app/models/campaign.rb Normal file
View file

@ -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

View file

@ -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

79
app/models/target.rb Normal file
View file

@ -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

4
app/models/visit.rb Normal file
View file

@ -0,0 +1,4 @@
class Visit < ActiveRecord::Base
has_many :ahoy_events, class_name: "Ahoy::Event"
belongs_to :user
end

View file

@ -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' }

View file

@ -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}

View file

@ -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'

View file

@ -0,0 +1,2 @@
= semantic_form_for(@campaign, url: admin_conference_campaigns_path(@conference.short_title)) do |f|
= render partial: 'form', locals: {f: f}

View file

@ -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!

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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' }

View file

@ -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