This commit is contained in:
Shlok Srivastava 2017-05-08 15:53:26 +00:00 committed by GitHub
commit aa974489df
69 changed files with 1371 additions and 377 deletions

View file

@ -23,6 +23,9 @@ gem 'mysql2'
# for observing records
gem 'rails-observers'
# for rating
gem 'ratyrate', github: 'wazery/ratyrate', branch: 'master'
# for tracking data changes
gem 'paper_trail'

View file

@ -1,3 +1,10 @@
GIT
remote: git://github.com/wazery/ratyrate.git
revision: b738c56a4b53260d4083bbbc7895d7d32f3c0264
branch: master
specs:
ratyrate (1.2.2.alpha)
GEM
remote: https://rubygems.org/
remote: https://rails-assets.org/
@ -611,6 +618,7 @@ DEPENDENCIES
rails-i18n (~> 4.0.0)
rails-observers
rails_12factor
ratyrate!
rdoc-generator-fivefish
redcarpet
responders (~> 2.0)

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

View file

@ -47,6 +47,9 @@
//= require unobtrusive_flash_bootstrap
//= require countable
//= require selectize
//= require jquery.raty
//= require ratyrate
//= require osem-rating
$(document).ready(function() {
$('a[disabled=disabled]').click(function(event){

View file

@ -0,0 +1,760 @@
/*!
* jQuery Raty - A Star Rating Plugin
*
* The MIT License
*
* @author : Washington Botelho
* @doc : http://wbotelhos.com/raty
* @version : 2.7.0
*
*/
;
(function($) {
'use strict';
var methods = {
init: function(options) {
return this.each(function() {
this.self = $(this);
methods.destroy.call(this.self);
this.opt = $.extend(true, {}, $.fn.raty.defaults, options);
methods._adjustCallback.call(this);
methods._adjustNumber.call(this);
methods._adjustHints.call(this);
this.opt.score = methods._adjustedScore.call(this, this.opt.score);
if (this.opt.starType !== 'img') {
methods._adjustStarType.call(this);
}
methods._adjustPath.call(this);
methods._createStars.call(this);
if (this.opt.cancel) {
methods._createCancel.call(this);
}
if (this.opt.precision) {
methods._adjustPrecision.call(this);
}
methods._createScore.call(this);
methods._apply.call(this, this.opt.score);
methods._setTitle.call(this, this.opt.score);
methods._target.call(this, this.opt.score);
if (this.opt.readOnly) {
methods._lock.call(this);
} else {
this.style.cursor = 'pointer';
methods._binds.call(this);
}
});
},
_adjustCallback: function() {
var options = ['number', 'readOnly', 'score', 'scoreName', 'target'];
for (var i = 0; i < options.length; i++) {
if (typeof this.opt[options[i]] === 'function') {
this.opt[options[i]] = this.opt[options[i]].call(this);
}
}
},
_adjustedScore: function(score) {
if (!score) {
return score;
}
return methods._between(score, 0, this.opt.number);
},
_adjustHints: function() {
if (!this.opt.hints) {
this.opt.hints = [];
}
if (!this.opt.halfShow && !this.opt.half) {
return;
}
var steps = this.opt.precision ? 10 : 2;
for (var i = 0; i < this.opt.number; i++) {
var group = this.opt.hints[i];
if (Object.prototype.toString.call(group) !== '[object Array]') {
group = [group];
}
this.opt.hints[i] = [];
for (var j = 0; j < steps; j++) {
var
hint = group[j],
last = group[group.length - 1];
if (last === undefined) {
last = null;
}
this.opt.hints[i][j] = hint === undefined ? last : hint;
}
}
},
_adjustNumber: function() {
this.opt.number = methods._between(this.opt.number, 1, this.opt.numberMax);
},
_adjustPath: function() {
this.opt.path = this.opt.path || '';
if (this.opt.path && this.opt.path.charAt(this.opt.path.length - 1) !== '/') {
this.opt.path += '/';
}
},
_adjustPrecision: function() {
this.opt.half = true;
},
_adjustStarType: function() {
var replaces = ['cancelOff', 'cancelOn', 'starHalf', 'starOff', 'starOn'];
this.opt.path = '';
for (var i = 0; i < replaces.length; i++) {
this.opt[replaces[i]] = this.opt[replaces[i]].replace('.', '-');
}
},
_apply: function(score) {
methods._fill.call(this, score);
if (score) {
if (score > 0) {
this.score.val(score);
}
methods._roundStars.call(this, score);
}
},
_between: function(value, min, max) {
return Math.min(Math.max(parseFloat(value), min), max);
},
_binds: function() {
if (this.cancel) {
methods._bindOverCancel.call(this);
methods._bindClickCancel.call(this);
methods._bindOutCancel.call(this);
}
methods._bindOver.call(this);
methods._bindClick.call(this);
methods._bindOut.call(this);
},
_bindClick: function() {
var that = this;
that.stars.on('click.raty', function(evt) {
var
execute = true,
score = (that.opt.half || that.opt.precision) ? that.self.data('score') : (this.alt || $(this).data('alt'));
if (that.opt.click) {
execute = that.opt.click.call(that, +score, evt);
}
if (execute || execute === undefined) {
if (that.opt.half && !that.opt.precision) {
score = methods._roundHalfScore.call(that, score);
}
methods._apply.call(that, score);
}
});
},
_bindClickCancel: function() {
var that = this;
that.cancel.on('click.raty', function(evt) {
that.score.removeAttr('value');
if (that.opt.click) {
that.opt.click.call(that, null, evt);
}
});
},
_bindOut: function() {
var that = this;
that.self.on('mouseleave.raty', function(evt) {
var score = +that.score.val() || undefined;
methods._apply.call(that, score);
methods._target.call(that, score, evt);
methods._resetTitle.call(that);
if (that.opt.mouseout) {
that.opt.mouseout.call(that, score, evt);
}
});
},
_bindOutCancel: function() {
var that = this;
that.cancel.on('mouseleave.raty', function(evt) {
var icon = that.opt.cancelOff;
if (that.opt.starType !== 'img') {
icon = that.opt.cancelClass + ' ' + icon;
}
methods._setIcon.call(that, this, icon);
if (that.opt.mouseout) {
var score = +that.score.val() || undefined;
that.opt.mouseout.call(that, score, evt);
}
});
},
_bindOver: function() {
var that = this,
action = that.opt.half ? 'mousemove.raty' : 'mouseover.raty';
that.stars.on(action, function(evt) {
var score = methods._getScoreByPosition.call(that, evt, this);
methods._fill.call(that, score);
if (that.opt.half) {
methods._roundStars.call(that, score, evt);
methods._setTitle.call(that, score, evt);
that.self.data('score', score);
}
methods._target.call(that, score, evt);
if (that.opt.mouseover) {
that.opt.mouseover.call(that, score, evt);
}
});
},
_bindOverCancel: function() {
var that = this;
that.cancel.on('mouseover.raty', function(evt) {
var
starOff = that.opt.path + that.opt.starOff,
icon = that.opt.cancelOn;
if (that.opt.starType === 'img') {
that.stars.attr('src', starOff);
} else {
icon = that.opt.cancelClass + ' ' + icon;
that.stars.attr('class', starOff);
}
methods._setIcon.call(that, this, icon);
methods._target.call(that, null, evt);
if (that.opt.mouseover) {
that.opt.mouseover.call(that, null);
}
});
},
_buildScoreField: function() {
return $('<input />', { name: this.opt.scoreName, type: 'hidden' }).appendTo(this);
},
_createCancel: function() {
var icon = this.opt.path + this.opt.cancelOff,
cancel = $('<' + this.opt.starType + ' />', { title: this.opt.cancelHint, 'class': this.opt.cancelClass });
if (this.opt.starType === 'img') {
cancel.attr({ src: icon, alt: 'x' });
} else {
// TODO: use $.data
cancel.attr('data-alt', 'x').addClass(icon);
}
if (this.opt.cancelPlace === 'left') {
this.self.prepend('&#160;').prepend(cancel);
} else {
this.self.append('&#160;').append(cancel);
}
this.cancel = cancel;
},
_createScore: function() {
var score = $(this.opt.targetScore);
this.score = score.length ? score : methods._buildScoreField.call(this);
},
_createStars: function() {
for (var i = 1; i <= this.opt.number; i++) {
var
name = methods._nameForIndex.call(this, i),
attrs = { alt: i, src: this.opt.path + this.opt[name] };
if (this.opt.starType !== 'img') {
attrs = { 'data-alt': i, 'class': attrs.src }; // TODO: use $.data.
}
attrs.title = methods._getHint.call(this, i);
$('<' + this.opt.starType + ' />', attrs).appendTo(this);
if (this.opt.space) {
this.self.append(i < this.opt.number ? '&#160;' : '');
}
}
this.stars = this.self.children(this.opt.starType);
},
_error: function(message) {
$(this).text(message);
$.error(message);
},
_fill: function(score) {
var hash = 0;
for (var i = 1; i <= this.stars.length; i++) {
var
icon,
star = this.stars[i - 1],
turnOn = methods._turnOn.call(this, i, score);
if (this.opt.iconRange && this.opt.iconRange.length > hash) {
var irange = this.opt.iconRange[hash];
icon = methods._getRangeIcon.call(this, irange, turnOn);
if (i <= irange.range) {
methods._setIcon.call(this, star, icon);
}
if (i === irange.range) {
hash++;
}
} else {
icon = this.opt[turnOn ? 'starOn' : 'starOff'];
methods._setIcon.call(this, star, icon);
}
}
},
_getFirstDecimal: function(number) {
var
decimal = number.toString().split('.')[1],
result = 0;
if (decimal) {
result = parseInt(decimal.charAt(0), 10);
if (decimal.slice(1, 5) === '9999') {
result++;
}
}
return result;
},
_getRangeIcon: function(irange, turnOn) {
return turnOn ? irange.on || this.opt.starOn : irange.off || this.opt.starOff;
},
_getScoreByPosition: function(evt, icon) {
var score = parseInt(icon.alt || icon.getAttribute('data-alt'), 10);
if (this.opt.half) {
var
size = methods._getWidth.call(this),
percent = parseFloat((evt.pageX - $(icon).offset().left) / size);
score = score - 1 + percent;
}
return score;
},
_getHint: function(score, evt) {
if (score !== 0 && !score) {
return this.opt.noRatedMsg;
}
var
decimal = methods._getFirstDecimal.call(this, score),
integer = Math.ceil(score),
group = this.opt.hints[(integer || 1) - 1],
hint = group,
set = !evt || this.move;
if (this.opt.precision) {
if (set) {
decimal = decimal === 0 ? 9 : decimal - 1;
}
hint = group[decimal];
} else if (this.opt.halfShow || this.opt.half) {
decimal = set && decimal === 0 ? 1 : decimal > 5 ? 1 : 0;
hint = group[decimal];
}
return hint === '' ? '' : hint || score;
},
_getWidth: function() {
var width = this.stars[0].width || parseFloat(this.stars.eq(0).css('font-size'));
if (!width) {
methods._error.call(this, 'Could not get the icon width!');
}
return width;
},
_lock: function() {
var hint = methods._getHint.call(this, this.score.val());
this.style.cursor = '';
this.title = hint;
this.score.prop('readonly', true);
this.stars.prop('title', hint);
if (this.cancel) {
this.cancel.hide();
}
this.self.data('readonly', true);
},
_nameForIndex: function(i) {
return this.opt.score && this.opt.score >= i ? 'starOn' : 'starOff';
},
_resetTitle: function(star) {
for (var i = 0; i < this.opt.number; i++) {
this.stars[i].title = methods._getHint.call(this, i + 1);
}
},
_roundHalfScore: function(score) {
var integer = parseInt(score, 10),
decimal = methods._getFirstDecimal.call(this, score);
if (decimal !== 0) {
decimal = decimal > 5 ? 1 : 0.5;
}
return integer + decimal;
},
_roundStars: function(score, evt) {
var
decimal = (score % 1).toFixed(2),
name ;
if (evt || this.move) {
name = decimal > 0.5 ? 'starOn' : 'starHalf';
} else if (decimal > this.opt.round.down) { // Up: [x.76 .. x.99]
name = 'starOn';
if (this.opt.halfShow && decimal < this.opt.round.up) { // Half: [x.26 .. x.75]
name = 'starHalf';
} else if (decimal < this.opt.round.full) { // Down: [x.00 .. x.5]
name = 'starOff';
}
}
if (name) {
var
icon = this.opt[name],
star = this.stars[Math.ceil(score) - 1];
methods._setIcon.call(this, star, icon);
} // Full down: [x.00 .. x.25]
},
_setIcon: function(star, icon) {
star[this.opt.starType === 'img' ? 'src' : 'className'] = this.opt.path + icon;
},
_setTarget: function(target, score) {
if (score) {
score = this.opt.targetFormat.toString().replace('{score}', score);
}
if (target.is(':input')) {
target.val(score);
} else {
target.html(score);
}
},
_setTitle: function(score, evt) {
if (score) {
var
integer = parseInt(Math.ceil(score), 10),
star = this.stars[integer - 1];
star.title = methods._getHint.call(this, score, evt);
}
},
_target: function(score, evt) {
if (this.opt.target) {
var target = $(this.opt.target);
if (!target.length) {
methods._error.call(this, 'Target selector invalid or missing!');
}
var mouseover = evt && evt.type === 'mouseover';
if (score === undefined) {
score = this.opt.targetText;
} else if (score === null) {
score = mouseover ? this.opt.cancelHint : this.opt.targetText;
} else {
if (this.opt.targetType === 'hint') {
score = methods._getHint.call(this, score, evt);
} else if (this.opt.precision) {
score = parseFloat(score).toFixed(1);
}
var mousemove = evt && evt.type === 'mousemove';
if (!mouseover && !mousemove && !this.opt.targetKeep) {
score = this.opt.targetText;
}
}
methods._setTarget.call(this, target, score);
}
},
_turnOn: function(i, score) {
return this.opt.single ? (i === score) : (i <= score);
},
_unlock: function() {
this.style.cursor = 'pointer';
this.removeAttribute('title');
this.score.removeAttr('readonly');
this.self.data('readonly', false);
for (var i = 0; i < this.opt.number; i++) {
this.stars[i].title = methods._getHint.call(this, i + 1);
}
if (this.cancel) {
this.cancel.css('display', '');
}
},
cancel: function(click) {
return this.each(function() {
var self = $(this);
if (self.data('readonly') !== true) {
methods[click ? 'click' : 'score'].call(self, null);
this.score.removeAttr('value');
}
});
},
click: function(score) {
return this.each(function() {
if ($(this).data('readonly') !== true) {
score = methods._adjustedScore.call(this, score);
methods._apply.call(this, score);
if (this.opt.click) {
this.opt.click.call(this, score, $.Event('click'));
}
methods._target.call(this, score);
}
});
},
destroy: function() {
return this.each(function() {
var self = $(this),
raw = self.data('raw');
if (raw) {
self.off('.raty').empty().css({ cursor: raw.style.cursor }).removeData('readonly');
} else {
self.data('raw', self.clone()[0]);
}
});
},
getScore: function() {
var score = [],
value ;
this.each(function() {
value = this.score.val();
score.push(value ? +value : undefined);
});
return (score.length > 1) ? score : score[0];
},
move: function(score) {
return this.each(function() {
var
integer = parseInt(score, 10),
decimal = methods._getFirstDecimal.call(this, score);
if (integer >= this.opt.number) {
integer = this.opt.number - 1;
decimal = 10;
}
var
width = methods._getWidth.call(this),
steps = width / 10,
star = $(this.stars[integer]),
percent = star.offset().left + steps * decimal,
evt = $.Event('mousemove', { pageX: percent });
this.move = true;
star.trigger(evt);
this.move = false;
});
},
readOnly: function(readonly) {
return this.each(function() {
var self = $(this);
if (self.data('readonly') !== readonly) {
if (readonly) {
self.off('.raty').children('img').off('.raty');
methods._lock.call(this);
} else {
methods._binds.call(this);
methods._unlock.call(this);
}
self.data('readonly', readonly);
}
});
},
reload: function() {
return methods.set.call(this, {});
},
score: function() {
var self = $(this);
return arguments.length ? methods.setScore.apply(self, arguments) : methods.getScore.call(self);
},
set: function(options) {
return this.each(function() {
$(this).raty($.extend({}, this.opt, options));
});
},
setScore: function(score) {
return this.each(function() {
if ($(this).data('readonly') !== true) {
score = methods._adjustedScore.call(this, score);
methods._apply.call(this, score);
methods._target.call(this, score);
}
});
}
};
$.fn.raty = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist!');
}
};
$.fn.raty.defaults = {
cancel : false,
cancelClass : 'raty-cancel',
cancelHint : 'Cancel this rating!',
cancelOff : 'cancel-off.png',
cancelOn : 'cancel-on.png',
cancelPlace : 'left',
click : undefined,
half : false,
halfShow : true,
hints : ['bad', 'poor', 'regular', 'good', 'gorgeous'],
iconRange : undefined,
mouseout : undefined,
mouseover : undefined,
noRatedMsg : 'Not rated yet!',
number : 5,
numberMax : 20,
path : undefined,
precision : false,
readOnly : false,
round : { down: 0.25, full: 0.6, up: 0.76 },
score : undefined,
scoreName : 'score',
single : false,
space : true,
starHalf : 'star-half.png',
starOff : 'star-off.png',
starOn : 'star-on.png',
starType : 'img',
target : undefined,
targetFormat : '{score}',
targetKeep : false,
targetScore : undefined,
targetText : '',
targetType : 'hint'
};
})(jQuery);

View file

@ -0,0 +1,5 @@
$( document ).ready(function() {
$(".disabled-rate .star").each(function(){
$(this).raty('readOnly', true);
});
});

View file

@ -0,0 +1,62 @@
$.fn.raty.defaults.half = false;
$.fn.raty.defaults.halfShow = true;
$.fn.raty.defaults.path = "/assets";
$.fn.raty.defaults.cancel = false;
$(function(){
$(".star").each(function() {
var $readonly = ($(this).attr('data-readonly') == 'true');
var $half = ($(this).attr('data-enable-half') == 'true');
var $halfShow = ($(this).attr('data-half-show') == 'true');
var $single = ($(this).attr('data-single') == 'true');
$(this).raty({
score: function() {
return $(this).attr('data-rating')
},
number: function() {
return $(this).attr('data-star-count')
},
half: $half,
halfShow: $halfShow,
single: $single,
path: $(this).attr('data-star-path'),
starOn: $(this).attr('data-star-on'),
starOff: $(this).attr('data-star-off'),
starHalf: $(this).attr('data-star-half'),
cancel: $(this).attr('data-cancel'),
cancelPlace: $(this).attr('data-cancel-place'),
cancelHint: $(this).attr('data-cancel-hint'),
cancelOn: $(this).attr('data-cancel-on'),
cancelOff: $(this).attr('data-cancel-off'),
noRatedMsg: $(this).attr('data-no-rated-message'),
round: $(this).attr('data-round'),
space: $(this).attr('data-space'),
target: $(this).attr('data-target'),
targetText: $(this).attr('data-target-text'),
targetType: $(this).attr('data-target-type'),
targetFormat: $(this).attr('data-target-format'),
targetScoret: $(this).attr('data-target-score'),
readOnly: $readonly,
click: function(score, evt) {
var _this = this;
if (score == null) { score = 0; }
$.post('<%= Rails.application.class.routes.url_helpers.rate_path %>',
{
score: score,
dimension: $(this).attr('data-dimension'),
id: $(this).attr('data-id'),
klass: $(this).attr('data-classname')
},
function(data) {
if(data) {
// success code goes here ...
if ($(_this).attr('data-disable-after-rate') == 'true') {
$(_this).raty('set', { readOnly: true, score: score });
}
}
});
}
});
});
});

View file

@ -3,7 +3,6 @@
*= require formtastic-bootstrap
*= require dataTables/bootstrap/3/jquery.dataTables.bootstrap
*= require osem
*= require osem-rating
*= require osem-schedule
*= require osem-schedule-print
*= require osem-dashboard
@ -18,4 +17,5 @@
*= require osem-navbar
*= require selectize
*= require selectize.bootstrap3
*= require osem-rating
*/

View file

@ -1,19 +1,3 @@
/* Styling for voting on proposals*/
.myrating.bright { background-image: image-url("star-bright.png"); }
.myrating.glow { background-image: image-url("star-glow.png"); }
.othersrating.bright { background-image: image-url("star-bright.png"); }
.avgrating.bright { background-image: image-url("star-bright.png"); }
.avgrating {
background: image-url("star.png") 0 0;
margin-right: -2px;
width: 24px;
height: 24px;
display: inline-block;
img.raty-cancel {
display: none;
}
.myrating, .othersrating {
background: image-url("star.png") 0 0;
width: 24px;
height: 24px;
float: left;
}

View file

@ -47,13 +47,12 @@ module Admin
@event_types = @program.event_types
@comments = @event.root_comments
@comment_count = @event.comment_threads.count
@ratings = @event.votes.includes(:user)
@difficulty_levels = @program.difficulty_levels
@versions = @event.versions |
PaperTrail::Version.where(item_type: 'Commercial').where_object(commercialable_id: @event.id, commercialable_type: 'Event') |
PaperTrail::Version.where(item_type: 'Commercial').where_object_changes(commercialable_id: @event.id, commercialable_type: 'Event') |
PaperTrail::Version.where(item_type: 'Vote').where('object_changes LIKE ?', "%\nevent_id:\n- \n- #{@event.id}\n%") |
PaperTrail::Version.where(item_type: 'Vote').where('object LIKE ?', "%\nevent_id: #{@event.id}\n%")
PaperTrail::Version.where(item_type: 'Commercial').where_object_changes(commercialable_id: @event.id, commercialable_type: 'Event')
@votable_fields = VotableField.where(votable_type: 'Event', conference: @event.program.conference, enabled: true, for_admin: true)
Event.vote(@votable_fields)
end
def edit
@ -142,24 +141,6 @@ module Admin
update_state(:restart, 'Review started!')
end
def vote
@ratings = @event.votes.includes(:user)
if (votes = current_user.votes.find_by_event_id(params[:id]))
votes.update_attributes(rating: params[:rating])
else
@myvote = @event.votes.build
@myvote.user = current_user
@myvote.rating = params[:rating]
@myvote.save
end
respond_to do |format|
format.html { redirect_to admin_conference_program_event_path(@conference.short_title, @event) }
format.js
end
end
def registrations
@event_registrations = @event.events_registrations
end

View file

@ -38,7 +38,7 @@ module Admin
private
def program_params
params.require(:program).permit(:rating, :schedule_public, :schedule_interval, :schedule_fluid, :languages, :blind_voting, :voting_start_date, :voting_end_date, :selected_schedule_id)
params.require(:program).permit(:rating, :rating_enabled, :schedule_public, :schedule_interval, :schedule_fluid, :languages, :blind_voting, :voting_start_date, :voting_end_date, :selected_schedule_id)
end
end
end

View file

@ -0,0 +1,57 @@
module Admin
class VotableFieldsController < ApplicationController
load_and_authorize_resource :conference, find_by: :short_title
load_and_authorize_resource :votable_field
after_action :remove_rates, only: :destroy
def index; end
def edit; end
def new
@votable_field = @conference.votable_fields.new
end
def create
@votable_field = @conference.votable_fields.new(votable_field_params)
if @votable_field.save
redirect_to admin_conference_votable_fields_path(@conference.short_title),
notice: 'Votable field successfully created.'
else
flash[:error] = "Creating votable field failed: #{@votable_field.errors.full_messages.join('. ')}."
redirect_to new_admin_conference_votable_field_path(@conference.short_title)
end
end
def update
if @votable_field.update_attributes(votable_field_params)
flash[:notice] = 'Votable field successfully updated'
redirect_to admin_conference_votable_fields_path(@conference.short_title)
else
flash[:error] = "Votable field update failed: #{@votable_field.errors.full_messages.join('. ')}."
render :edit
end
end
def destroy
if @votable_field.destroy
redirect_to admin_conference_votable_fields_path(@conference.short_title),
notice: 'Votable field successfully destroyed.'
else
redirect_to admin_conference_votable_fields_path(@conference.short_title),
error: 'Votable field could not be destroyed.' \
"#{@votable_field.errors.full_messages.join('. ')}."
end
end
private
def remove_rates
false unless Rate.where(dimension: @votable_field.title).destroy_all && RatingCache.where(dimension: @votable_field.title).destroy_all
end
def votable_field_params
params.require(:votable_field).permit(:title, :enabled, :votable_type, :conference_id, :for_admin, :stars)
end
end
end

View file

@ -15,6 +15,8 @@ class ProposalsController < ApplicationController
def show
@event_schedule = @event.event_schedules.find_by(schedule_id: @program.selected_schedule_id)
@speakers_ordered = @event.speakers_ordered
@votable_fields = VotableField.where(votable_type: 'Event', conference: @event.program.conference, enabled: true, for_admin: false)
Event.vote(@votable_fields)
end
def new

View file

@ -0,0 +1,19 @@
class RaterController < ApplicationController
load_and_authorize_resource :rate
def create
if user_signed_in?
votable_type = ''
VotableField::VALID_VOTABLE_TYPES.each do |valid_votable_type|
votable_type = valid_votable_type
break if params[:klass] == votable_type
end
obj = votable_type.classify.constantize.find(params[:id])
obj.rate params[:score].to_f, current_user, params[:dimension]
render json: true
else
render json: false
end
end
end

View file

@ -510,17 +510,6 @@ module ApplicationHelper
end
end
def vote_change_description(version)
user = current_or_last_object_state(version.item_type, version.item_id).user
if version.event == 'create'
version.previous.nil? ? 'voted on' : "re-added #{user.name}'s vote on"
elsif version.event == 'update'
"updated #{user.name}'s vote on"
else
"deleted #{user.name}'s vote on"
end
end
def user_change_description(version)
if version.event == 'create'
link_to_user(version.item_id) + ' signed up'
@ -597,4 +586,12 @@ module ApplicationHelper
end
concurrent_events
end
def raters(votable_field)
users = []
votable_field.each do |field|
users += User.where(id: Rate.where(dimension: field.title).pluck(:rater_id)).pluck(:name)
end
users.uniq.join(', ')
end
end

View file

@ -138,6 +138,8 @@ class Ability
# ids of all the conferences for which the user has the 'organizer' role
conf_ids_for_organizer = Conference.with_role(:organizer, user).pluck(:id)
can :manage, Rate, conference_id: conf_ids_for_organizer
can :manage, VotableField, conference_id: conf_ids_for_organizer
can :manage, Resource, conference_id: conf_ids_for_organizer
can [:new, :create], Conference if user.has_role?(:organizer, :any)
can :manage, Conference, id: conf_ids_for_organizer
@ -192,6 +194,8 @@ class Ability
# ids of all the conferences for which the user has the 'cfp' role
conf_ids_for_cfp = Conference.with_role(:cfp, user).pluck(:id)
can :manage, Rate, conference_id: conf_ids_for_cfp
can :manage, VotableField, conference_id: conf_ids_for_cfp
can [:index, :show, :update], Resource, conference_id: conf_ids_for_cfp
can :manage, Event, program: { conference_id: conf_ids_for_cfp }
can :manage, EventType, program: { conference_id: conf_ids_for_cfp }
@ -220,7 +224,6 @@ class Ability
end
can [:index, :revert_object, :revert_attribute], PaperTrail::Version, item_type: 'Event', conference_id: conf_ids_for_cfp
can [:index, :revert_object, :revert_attribute], PaperTrail::Version, item_type: 'Vote', conference_id: conf_ids_for_cfp
can [:index, :revert_object, :revert_attribute], PaperTrail::Version do |version|
version.item_type == 'Commercial' && conf_ids_for_cfp.include?(version.conference_id) &&
(version.object.to_s.include?('Event') || version.object_changes.to_s.include?('Event'))

View file

@ -0,0 +1,4 @@
class AverageCache < ActiveRecord::Base
belongs_to :rater, class_name: 'User'
belongs_to :rateable, polymorphic: true
end

View file

@ -22,7 +22,7 @@ class Conference < ActiveRecord::Base
has_many :supporters, through: :ticket_purchases, source: :user
has_many :tickets, dependent: :destroy
has_many :resources, dependent: :destroy
has_many :votable_fields, dependent: :destroy
has_many :lodgings, dependent: :destroy
has_many :registrations, dependent: :destroy
has_many :participants, through: :registrations, source: :user

View file

@ -1,5 +1,8 @@
class Event < ActiveRecord::Base
include ActiveRecord::Transitions
scope :vote, ->(votable_fields) { votable_fields.each { |field| ratyrate_rateable field.title } }
has_paper_trail on: [:create, :update], ignore: [:updated_at, :guid, :week], meta: { conference_id: :conference_id }
acts_as_commentable
@ -15,8 +18,6 @@ class Event < ActiveRecord::Base
has_one :submitter_event_user, -> { where(event_role: 'submitter') }, class_name: 'EventUser'
has_one :submitter, through: :submitter_event_user, source: :user
has_many :votes, dependent: :destroy
has_many :voters, through: :votes, source: :user
has_many :commercials, as: :commercialable, dependent: :destroy
belongs_to :event_type
@ -92,33 +93,9 @@ class Event < ActiveRecord::Base
registrations.count < max_attendees
end
##
# Finds the rating of the user for the event
# ====Returns
# * +integer+ -> the rating of the user for the event
def user_rating(user)
(vote = votes.find_by(user: user)) ? vote.rating : 0
end
##
# Checks if the event has votes
# If a user is provided, it checks if the event has votes by the user
# ====Returns
# * +true+ -> If the event has votes (optionally, by the user)
# * +false+ -> If the event does not have any votes (optionally, by the user)
def voted?(user=nil)
return votes.where(user: user).any? if user
votes.any?
end
def average_rating
@total_rating = 0
votes.each do |vote|
@total_rating = @total_rating + vote.rating
end
@total = votes.size
@total_rating > 0 ? number_with_precision(@total_rating / @total.to_f, precision: 2, strip_insignificant_zeros: true) : 0
def ended?
timezone = program.conference.timezone
Time.now.in_time_zone(timezone) > event_schedules.find_by(schedule: program.selected_schedule).end_time
end
# get event speakers with the event sumbmitter at the first position

View file

@ -0,0 +1,3 @@
class OverallAverage < ActiveRecord::Base
belongs_to :rateable, polymorphic: true
end

View file

@ -53,11 +53,11 @@ class Program < ActiveRecord::Base
accepts_nested_attributes_for :difficulty_levels, allow_destroy: true
# validates :conference_id, presence: true, uniqueness: true
validates :rating, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 10, only_integer: true }
validates :schedule_interval, numericality: { greater_than_or_equal_to: 5, less_than_or_equal_to: 60 }, presence: true
validate :schedule_interval_divisor_60
validate :voting_start_date_before_end_date
validate :voting_dates_exist
validate :voting_dates_exist_for_blind_voting
validate :voting_dates_exist_for_rating_enabled
after_create :create_event_types
after_create :create_difficulty_levels
@ -93,10 +93,10 @@ class Program < ActiveRecord::Base
end
##
# Checks if both voting_start_date and voting_end_date are set
# Checks if both voting_start_date and voting_end_date are set when blind voting is enabled
# ====Returns
# Errors when the condition is not true
def voting_dates_exist
def voting_dates_exist_for_blind_voting
errors.add(:voting_start_date, 'must be set, when blind voting is enabled') if blind_voting && !voting_start_date && !voting_end_date
errors.add(:voting_end_date, 'must be set, when blind voting is enabled') if blind_voting && !voting_start_date && !voting_end_date
@ -106,6 +106,15 @@ class Program < ActiveRecord::Base
errors.add(:voting_start_date, 'must be set, when voting_end_date is set') if voting_end_date && !voting_start_date
end
##
# Checks if both voting_start_date and voting_end_date are set when rating is enabled
# ====Returns
# Errors when the condition is not true
def voting_dates_exist_for_rating_enabled
errors.add(:voting_start_date, 'must be set, when voting is enabled') if rating_enabled && !voting_start_date && !voting_end_date
errors.add(:voting_end_date, 'must be set, when voting is enabled') if rating_enabled && !voting_start_date && !voting_end_date
end
##
# Checks if voting_start_date is before voting_end_date
# ====Returns
@ -115,16 +124,7 @@ class Program < ActiveRecord::Base
end
##
# Checcks if the program has rating enabled
#
# ====Returns
# * +false+ -> If rating is not enabled
# * +true+ -> If rating is enabled
def rating_enabled?
rating && rating > 0
end
##
# Checks if the call for papers for the conference is currently open
#
# ====Returns

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

@ -0,0 +1,4 @@
class Rate < ActiveRecord::Base
belongs_to :rater, class_name: 'User'
belongs_to :rateable, polymorphic: true
end

View file

@ -0,0 +1,3 @@
class RatingCache < ActiveRecord::Base
belongs_to :cacheable, polymorphic: true
end

View file

@ -5,6 +5,7 @@ class UserDisabled < StandardError
end
class User < ActiveRecord::Base
ratyrate_rater
rolify
has_many :users_roles
has_many :roles, through: :users_roles, dependent: :destroy
@ -47,8 +48,6 @@ class User < ActiveRecord::Base
has_many :ticket_purchases, dependent: :destroy
has_many :payments, dependent: :destroy
has_many :tickets, through: :ticket_purchases, source: :ticket
has_many :votes, dependent: :destroy
has_many :voted_events, through: :votes, source: :events
has_many :subscriptions, dependent: :destroy
accepts_nested_attributes_for :roles

View file

@ -0,0 +1,20 @@
class VotableField < ActiveRecord::Base
belongs_to :conference
validates :title, :votable_type, :stars, presence: true
validates :title, uniqueness: { scope: :votable_type, message: 'already exsists for the selected votable type' }
VALID_VOTABLE_TYPES = %w[Event].freeze
# ratyrate does not allow criterias to have spaces in them
validate :no_spaces_in_title
validate :correct_votable_type
private
def no_spaces_in_title
errors.add(:title, 'should not have spaces') unless title.match(/\s/).nil?
end
def correct_votable_type
errors.add(:votable_type, "should be one of the following: #{VALID_VOTABLE_TYPES.join(', ')}") unless VALID_VOTABLE_TYPES.include? votable_type
end
end

View file

@ -1,14 +1,2 @@
class Vote < ActiveRecord::Base
belongs_to :user
belongs_to :event
has_paper_trail ignore: [:updated_at], meta: { conference_id: :conference_id }
delegate :name, to: :user
private
def conference_id
event.program.conference_id
end
end

View file

@ -42,10 +42,6 @@
Yes
- else
No
%dt
Rating Levels
%dd#rating
= @program.rating
.row
.col-md-12.text-right
= link_to(edit_admin_conference_program_cfp_path(@conference.short_title), class: 'btn btn-primary') do

View file

@ -168,8 +168,8 @@
%b Description
%td= simple_format(@event.description)
- if @conference.program && @conference.program.rating && @conference.program.rating > 0
= render partial: 'voting'
- if @votable_fields.present?
= render partial: 'voting'
.row
= link_to "Comments (#{@comment_count})", '#', id: 'event-comment-link'

View file

@ -1,80 +1,33 @@
%table.table#myrating
- if @program.show_voting?
%tr
%td.col-md-2
%b Rating
%td
- if @event.average_rating.to_f > 0
#{@event.average_rating}/#{@program.rating}
- else
Rating: 0/#{@program.rating}
- @program.rating.times do |counter|
- if @event.average_rating.to_f.round == counter + 1
= label_tag 'label_rating', '', class: 'avgrating', avgrate: true
= javascript_tag "$('label[avgrate=true]').prevAll().andSelf().addClass('bright');"
- else
= label_tag 'label_rating', '', class: 'avgrating'
%tr
%td
%b Voters
%td
= @event.voters.length
- if @event.voters.length > 0
(
= @ratings.map {|x| "#{x.name}"}.join ', '
)
%tr
%td.col-md-2
%b Your vote
%td
- if @program.voting_period?
- @program.rating.times do |counter|
- if @event.voted?(current_user) && @event.user_rating(current_user) == counter + 1
= link_to '', vote_admin_conference_program_event_path(@conference.short_title, @event, rating: counter + 1), remote: true, id: "label#{counter + 1}", class: 'myrating', voted: true
- else
= link_to '', vote_admin_conference_program_event_path(@conference.short_title, @event, rating: counter + 1), remote: true, id: "label#{counter + 1}", class: 'myrating'
%br
- else
- @conference.program.rating.times do |counter|
- if @event.voted?(current_user) && @event.user_rating(current_user) == counter + 1
= label_tag "label#{counter + 1}", '', class: 'othersrating', voted: true
= javascript_tag "$('label[voted=true]').prevAll().andSelf().addClass('bright');"
- else
= label_tag "label#{counter + 1}", '', class: 'othersrating'
(#{voting_open_or_close(@program)})
- if @program.show_voting?
- if @ratings.length > 0
- @ratings.each do |rate|
- unless rate.user_id == current_user.id
%hr
- if @program.rating_enabled
- if @program.voting_period?
- if @program.show_voting?
%table.table
%thead
%td.col-md-2
%b Overall Votes
- @votable_fields.each do |field|
%tr
%td.col-md-2
= field.title
%td
%div.disabled-rate
= rating_for @event, field.title, disable: true, star: field.stars
%tr
%td
= rate.name
Voters
%td
- @conference.program.rating.times do |counter|
- if @event.voted?(rate.user) && @event.user_rating(rate.user) == counter + 1
= label_tag "label#{counter + 1}", "", class: 'othersrating', voted: true
= javascript_tag "$('label[voted=true]').prevAll().andSelf().addClass('bright');"
- else
= label_tag "label#{counter + 1}", "", class: 'othersrating'
:javascript
$(function () {
var checkedId = $("a[voted='true']").attr('id');
$('a[id=' + checkedId + ']').prevAll().andSelf().addClass('bright');
});
$(".myrating").hover(
function() { // mouseover
$(this).prevAll().andSelf().addClass('glow');
},
function() { // mouseout
$(this).siblings().andSelf().removeClass('glow');
}
);
$(".myrating").click(function() {
$(this).siblings().removeClass("bright");
$(this).prevAll().andSelf().addClass("bright");
});
= raters(@votable_fields)
%table.table
%thead
%td.col-md-2
%b Your Votes
- @votable_fields.each do |field|
%tr
%td.col-md-2
= field.title
%td
= rating_for_user @event, current_user, field.title, star: field.stars
- else
%b
= voting_open_or_close(@program)

View file

@ -1,19 +0,0 @@
- if @program.show_voting?
#{event.average_rating}/#{@program.rating}
%br
#{pluralize(event.voters.length, 'voter')}
%br
- @program.rating.times do |counter|
- if event.average_rating.to_f.round == counter + 1
= label_tag 'label_rating', '', class: 'avgrating', avgrate: true
= javascript_tag "$('label[avgrate=true]').prevAll().andSelf().addClass('bright');"
- else
= label_tag 'label_rating', '', class: 'avgrating'
%br
- if event.voted?(current_user)
%span.label.label-success
Your rating: #{ event.user_rating(current_user) }
- else
%span.label.label-danger
Not rated

View file

@ -50,9 +50,6 @@
%b ID
%th
%b Title
- if @program.rating_enabled?
%th
%b Rating
%th
%b Submitter
%th
@ -80,11 +77,6 @@
= event.id
%td
= link_to event.title, admin_conference_program_event_path(@conference.short_title, event)
- if @program.rating_enabled?
%td.col-md-1{ 'data-order' => "#{event.average_rating}" }
= render partial: 'voting_index', locals: { event: event }
- if event.submitter && event.submitter.registrations && event.submitter.registrations.count < 1
- bgcolor = '#F7819F'
- else

View file

@ -36,10 +36,6 @@
= event_change_description(version)
= "event #{@event.title}"
- elsif version.item_type == 'Vote'
= vote_change_description(version)
= "event #{@event.title}"
- else
= general_change_description(version)
= link_to 'commercial',

View file

@ -1 +0,0 @@
$('table#myrating').replaceWith("<%= escape_javascript(render :partial => 'voting') %>");

View file

@ -7,11 +7,11 @@
= semantic_form_for(@program, url: admin_conference_program_path(@conference.short_title), html: {multipart: true}) do |f|
= f.input :schedule_public, label: 'Show Schedule on the home and splash page'
= f.input :schedule_fluid, label: 'Allow submitters to change their event after it is scheduled'
= f.input :rating, hint: 'Enter the number of different rating levels you want to have for voting on proposals. Enter 0 if you do not want to vote on proposals.'
= f.input :languages, hint: "Enter the languages allowed for events as values of #{link_to('ISO 639-1', 'http://www.loc.gov/standards/iso639-2/php/code_list.php', target: "_blank")} language codes separated with commas. The first language would be the default language. Leave it blank if you do not want to specify languages.".html_safe
= f.input :schedule_interval, hint: "It is the minimal time interval of your schedule. The value should be 5, 6, 10, 12, 15, 20, 30 or 60. Warning! Some events could be unscheduled when changing this value."
= f.input :blind_voting, hint: 'Enable this feature if you do not want to show voting results and voters prior to user submitting a vote. For the feature to work you need to set the voting dates below as well'
= f.input :voting_start_date, as: :string, input_html: { id: 'datetimepicker-voting_start_date', readonly: true, value: (f.object.voting_start_date.to_formatted_s(:db_without_seconds) unless f.object.voting_start_date.nil?) }
= f.input :voting_end_date, as: :string, input_html: { id: 'datetimepicker-voting_start_date', readonly: true, value: (f.object.voting_end_date.to_formatted_s(:db_without_seconds) unless f.object.voting_end_date.nil?) }
= f.input :rating_enabled, label: 'Enable voting', hint: 'To enable voting you need to set voting dates as well'
= f.input :blind_voting, hint: 'Enable this feature if you do not want to show voting results and voters prior to user submitting a vote. For the feature to work you need to set the voting dates below as well', input_html: { class: 'voting_fields' }
= f.input :voting_start_date, as: :string, input_html: { id: 'datetimepicker-voting_start_date', readonly: true, value: (f.object.voting_start_date.to_formatted_s(:db_without_seconds) unless f.object.voting_start_date.nil?),class: 'voting_fields' }
= f.input :voting_end_date, as: :string, input_html: { id: 'datetimepicker-voting_start_date', readonly: true, value: (f.object.voting_end_date.to_formatted_s(:db_without_seconds) unless f.object.voting_end_date.nil?),class: 'voting_fields' }
%p.text-right
= f.action :submit, as: :button, button_html: {class: 'btn btn-primary'}

View file

@ -57,11 +57,12 @@
%h3 Voting Options
%hr
%dt
Rating Levels
%dd#rating
= @program.rating
%dt Voting enabled?
%dd
- if @program.rating_enabled
Yes
- else
No
%dt Blind Voting
%dd#blind_voting
= @program.blind_voting

View file

@ -11,7 +11,6 @@
%th Title
%th State
%th Type
%th Rating
%th Created At
%tbody
- @user.events.each do |event|
@ -21,12 +20,4 @@
%td= link_to event.title, admin_conference_program_event_path(event.program.conference.short_title, event)
%td= event.state
%td= "#{event.event_type.title} (#{show_time(event.event_type.length)})"
%td
- if event.program && event.program.rating && event.program.rating > 0
- event.program.rating.times do |counter|
- if event.average_rating.to_f.round == counter+1
= label_tag 'label_rating', '', class: 'avgrating', avgrate: true
= javascript_tag "$('label[avgrate=true]').prevAll().andSelf().addClass('bright');"
- else
= label_tag 'label_rating', '', class: 'avgrating'
%td= event.created_at

View file

@ -42,7 +42,7 @@
= link_to 'commercial',
admin_conference_commercials_path(conference_id: Conference.find(version.conference_id).short_title)
- when 'EventsRegistration', 'Comment', 'Vote', 'Event'
- when 'EventsRegistration', 'Comment', 'Event'
= 'event'
- object = current_or_last_object_state(version.item_type, version.item_id)
- event_id = object.try(:event_id) || object.try(:commentable_id) || object.id

View file

@ -35,9 +35,6 @@
- when 'Comment'
= comment_change_description(version)
- when 'Vote'
= vote_change_description(version)
- when 'User'
= user_change_description(version)

View file

@ -0,0 +1,16 @@
.row
.col-md-12
.page-header
%h1
- if @votable_field.new_record?
New
Votable Field
.row
.col-md-8
= semantic_form_for(@votable_field, :url => (@votable_field.new_record? ? admin_conference_votable_fields_path : admin_conference_votable_field_path(@conference.short_title, @votable_field))) do |f|
= f.input :title
= f.input :votable_type, collection: options_for_select(['Event'])
= f.input :stars, label: 'Add the total number of stars for the field'
= f.input :for_admin, label: 'Add this field for voting in admin side only'
%p.text-right
= f.action :submit, as: :button, button_html: { class: 'btn btn-primary' }

View file

@ -0,0 +1,42 @@
.row
.col-md-12
.page-header
%h1 Votable Fields
%p.text-muted
Select the criteria of rating in conference
- if @conference.votable_fields.any?
.row
.col-md-12
%table.table.table-striped.table-bordered.table-hover.datatable
%thead
%th Enabled?
%th Title
%th Votable Type
%th Actions
%tbody
- @conference.votable_fields.each do |votable_field|
%tr
%td
- if can? :update, votable_field
= check_box_tag @conference.short_title, votable_field.id , votable_field.enabled,
method: :patch, url: "/admin/conferences/#{@conference.short_title}/votable_fields/#{votable_field.id}?votable_field[enabled]=",
class: 'switch-checkbox', readonly: false, data: { size: 'small', on_color: 'success', off_color: 'warning', on_text: 'Yes', off_text: 'No' }
- else
= check_box_tag @conference.short_title, votable_field.id , votable_field.enabled,
method: :patch, url: "/admin/conferences/#{@conference.short_title}/votable_fields/#{votable_field.id}?votable_field[enabled]=",
class: 'switch-checkbox', readonly: true,data: { size: 'small', on_color: 'success', off_color: 'warning', on_text: 'Yes', off_text: 'No' }
%td
= votable_field.title
%td
= votable_field.votable_type
%td
.btn-group
= link_to 'Edit', edit_admin_conference_votable_field_path(@conference.short_title, votable_field),
method: :get, class: 'btn btn-primary', disabled: !(can? :update, votable_field)
= link_to 'Delete', admin_conference_votable_field_path(@conference.short_title, votable_field),
method: :delete, class: 'btn btn-danger', disabled: !(can? :destroy, votable_field),
data: { confirm: "Do you really want to delete #{votable_field.title}?" }
.row
.col-md-12
= link_to 'Add Votable Field', new_admin_conference_votable_field_path, class: 'btn btn-success pull-right', disabled: !(can? :create, @conference.votable_fields.new)

View file

@ -0,0 +1 @@
= render 'form'

View file

@ -144,3 +144,8 @@
= link_to admin_conference_resources_path(@conference.short_title) do
%span.fa.fa-pencil-square
Resources
- if can? :index, @conference.votable_fields.new
%li
= link_to admin_conference_votable_fields_path(@conference.short_title) do
%span.fa.fa-star
Votable Fields

View file

@ -119,3 +119,15 @@
%dt Room:
%dd
= event.room.name
.col-md-12
%hr
%table.table
%thead
%td.col-md-2
%b Feedback:
- @votable_fields.each do |field|
%tr
%td.col-md-2
= field.title
%td
= rating_for @event, field.title, star: field.stars

View file

@ -1,5 +1,6 @@
Osem::Application.routes.draw do
post '/rate' => 'rater#create', as: 'rate'
if ENV['OSEM_ICHAIN_ENABLED'] == 'true'
devise_for :users, controllers: { registrations: :registrations }
else
@ -72,6 +73,7 @@ Osem::Application.routes.draw do
end
resources :resources
resources :votable_fields
resources :tickets
resources :sponsors, except: [:show]
resources :lodgings, except: [:show]

View file

@ -0,0 +1,17 @@
class CreateRatingCaches < ActiveRecord::Migration
def self.up
create_table :rating_caches do |t|
t.belongs_to :cacheable, polymorphic: true
t.float :avg, null: false
t.integer :qty, null: false
t.string :dimension
t.timestamps
end
add_index :rating_caches, [:cacheable_id, :cacheable_type]
end
def self.down
drop_table :rating_caches
end
end

View file

@ -0,0 +1,18 @@
class CreateRates < ActiveRecord::Migration
def self.up
create_table :rates do |t|
t.belongs_to :rater
t.belongs_to :rateable, polymorphic: true
t.float :stars, null: false
t.string :dimension
t.timestamps
end
add_index :rates, :rater_id
add_index :rates, [:rateable_id, :rateable_type]
end
def self.down
drop_table :rates
end
end

View file

@ -0,0 +1,14 @@
class CreateAverageCaches < ActiveRecord::Migration
def self.up
create_table :average_caches do |t|
t.belongs_to :rater
t.belongs_to :rateable, polymorphic: true
t.float :avg, null: false
t.timestamps
end
end
def self.down
drop_table :average_caches
end
end

View file

@ -0,0 +1,13 @@
class CreateOverallAverages < ActiveRecord::Migration
def self.up
create_table :overall_averages do |t|
t.belongs_to :rateable, polymorphic: true
t.float :overall_avg, null: false
t.timestamps
end
end
def self.down
drop_table :overall_averages
end
end

View file

@ -0,0 +1,12 @@
class CreateVotableFields < ActiveRecord::Migration
def change
create_table :votable_fields do |t|
t.string :title
t.string :votable_type
t.boolean :enabled, default: true
t.references :conference
t.timestamps null: false
end
end
end

View file

@ -0,0 +1,5 @@
class AddForAdminToVotableField < ActiveRecord::Migration
def change
add_column :votable_fields, :for_admin, :boolean, default: false
end
end

View file

@ -0,0 +1,5 @@
class AddStarsToVotableField < ActiveRecord::Migration
def change
add_column :votable_fields, :stars, :integer, default: 5
end
end

View file

@ -0,0 +1,5 @@
class AddRatingEnabledToProgram < ActiveRecord::Migration
def change
add_column :programs, :rating_enabled, :boolean, default: false
end
end

View file

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170302145716) do
ActiveRecord::Schema.define(version: 20170405004359) do
create_table "ahoy_events", force: :cascade do |t|
t.uuid "visit_id", limit: 16
@ -31,6 +31,15 @@ ActiveRecord::Schema.define(version: 20170302145716) do
t.datetime "updated_at"
end
create_table "average_caches", force: :cascade do |t|
t.integer "rater_id"
t.integer "rateable_id"
t.string "rateable_type"
t.float "avg", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "campaigns", force: :cascade do |t|
t.integer "conference_id"
t.string "name"
@ -266,6 +275,14 @@ ActiveRecord::Schema.define(version: 20170302145716) do
t.datetime "updated_at"
end
create_table "overall_averages", force: :cascade do |t|
t.integer "rateable_id"
t.string "rateable_type"
t.float "overall_avg", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "payments", force: :cascade do |t|
t.string "last4"
t.integer "amount"
@ -290,6 +307,7 @@ ActiveRecord::Schema.define(version: 20170302145716) do
t.datetime "voting_end_date"
t.integer "selected_schedule_id"
t.integer "schedule_interval", default: 15, null: false
t.boolean "rating_enabled", default: false
end
add_index "programs", ["selected_schedule_id"], name: "index_programs_on_selected_schedule_id"
@ -321,6 +339,31 @@ ActiveRecord::Schema.define(version: 20170302145716) do
t.datetime "updated_at"
end
create_table "rates", force: :cascade do |t|
t.integer "rater_id"
t.integer "rateable_id"
t.string "rateable_type"
t.float "stars", null: false
t.string "dimension"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "rates", ["rateable_id", "rateable_type"], name: "index_rates_on_rateable_id_and_rateable_type"
add_index "rates", ["rater_id"], name: "index_rates_on_rater_id"
create_table "rating_caches", force: :cascade do |t|
t.integer "cacheable_id"
t.string "cacheable_type"
t.float "avg", null: false
t.integer "qty", null: false
t.string "dimension"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "rating_caches", ["cacheable_id", "cacheable_type"], name: "index_rating_caches_on_cacheable_id_and_cacheable_type"
create_table "registration_periods", force: :cascade do |t|
t.integer "conference_id"
t.date "start_date"
@ -583,6 +626,17 @@ ActiveRecord::Schema.define(version: 20170302145716) do
add_index "visits", ["user_id"], name: "index_visits_on_user_id"
create_table "votable_fields", force: :cascade do |t|
t.string "title"
t.string "votable_type"
t.boolean "enabled", default: true
t.integer "conference_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "for_admin", default: false
t.integer "stars", default: 5
end
create_table "votes", force: :cascade do |t|
t.integer "event_id"
t.integer "rating"

View file

@ -0,0 +1,20 @@
namespace :votes do
desc 'Migrate old votes to new voting system'
task migrate: :environment do
ActiveRecord::Base.transaction do
Conference.all.each do |conf|
VotableField.create(title: 'Overall', conference_id: conf.id, for_admin: true, stars: conf.program.rating, votable_type: 'Event')
Event.all.each do |event|
votes = Vote.where(event_id: event.id)
break if votes.blank?
avg_votes = votes.pluck(:rating).sum / votes.count
votes.each do |vote|
Rate.create(dimension: 'Overall', rater_id: vote.user_id, rateable_type: 'Event', stars: vote.rating, rateable_id: event.id)
end
RatingCache.create(cacheable_id: event.id, cacheable_type: 'Event', avg: avg_votes, qty: votes.count, dimension: 'Overall')
end
end
end
puts 'All done!'
end
end

View file

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :votable_field do
title { Faker::Lorem.word }
votable_type { 'Event' }
conference
end
end

View file

@ -1,7 +0,0 @@
FactoryGirl.define do
factory :vote do
event
user
rating 1
end
end

View file

@ -12,19 +12,5 @@ feature Program do
sign_in organizer
end
scenario 'changes rating', feature: true, js: true do
visit admin_conference_program_path(conference.short_title)
click_link 'Edit'
fill_in 'program_rating', with: '4'
click_button 'Update Program'
# Validations
expect(flash)
.to eq('The program was successfully updated.')
expect(find('#rating').text).to eq('4')
end
end
end

View file

@ -15,6 +15,7 @@ feature Event do
@event = create(:event, program: conference.program, title: 'Example Proposal')
@event.event_users.create(user: participant, event_role: 'submitter')
@event.event_users.create(user: participant, event_role: 'speaker')
create(:votable_field, conference: conference, for_admin: true)
end
after(:each) do
@ -26,6 +27,38 @@ feature Event do
sign_in organizer
end
scenario 'for program with voting enabled can successfully vote' do
conference.program.update(rating_enabled: true, voting_start_date: Date.current - 1, voting_end_date: Date.current + 1)
visit admin_conference_program_event_path(conference.short_title, @event)
expect(page.has_content?('Overall Votes')).to eq true
expect(page.has_content?('Your Votes')).to eq true
end
scenario 'for program with voting disabled cannot successfuly vote' do
conference.program.update(rating_enabled: false, voting_start_date: Date.today, voting_end_date: Date.today + 1)
visit admin_conference_program_event_path(conference.short_title, @event)
expect(page.has_content?('Overall Votes')).to eq false
expect(page.has_content?('Your Votes')).to eq false
end
scenario 'for program with blind voting enabled cannot see overall votes' do
conference.program.update(rating_enabled: true, voting_start_date: Date.today, voting_end_date: Date.today + 1, blind_voting: true)
visit admin_conference_program_event_path(conference.short_title, @event)
expect(page.has_content?('Overall Votes')).to eq false
expect(page.has_content?('Your Votes')).to eq true
end
scenario 'for program with blind voting disabled can see overall votes' do
conference.program.update(rating_enabled: true, voting_start_date: Date.today, voting_end_date: Date.today + 1, blind_voting: false)
visit admin_conference_program_event_path(conference.short_title, @event)
expect(page.has_content?('Overall Votes')).to eq true
expect(page.has_content?('Your Votes')).to eq true
end
scenario 'rejects a proposal', feature: true, js: true do
visit admin_conference_program_events_path(conference.short_title)
expect(page.has_content?('Example Proposal')).to be true

View file

@ -21,15 +21,6 @@ feature 'Version' do
expect(page).to have_text("#{organizer.name} updated social tag, email, googleplus and sponsor email of contact details in conference #{conference.short_title}")
end
scenario 'display changes in program', feature: true, versioning: true, js: true do
visit edit_admin_conference_program_path(conference.short_title)
fill_in 'program_rating', with: '4'
click_button 'Update Program'
visit admin_revision_history_path
expect(page).to have_text("#{organizer.name} updated rating of program in conference #{conference.short_title}")
end
scenario 'display changes in cfp', feature: true, versioning: true, js: true do
cfp = create(:cfp, program: conference.program)
cfp.update_attributes(start_date: (Date.today + 1).strftime('%d/%m/%Y'), end_date: (Date.today + 3).strftime('%d/%m/%Y'))
@ -346,20 +337,6 @@ feature 'Version' do
expect(page).to have_text("Someone (probably via the console) re-added #{organizer.name}'s comment on event #{event.title} in conference #{conference.short_title}")
end
scenario 'display changes in vote', feature: true, versioning: true, js: true do
conference.program.rating = 1
create(:event, program: conference.program, title: 'My first event')
event = create(:event, program: conference.program, title: 'My second event')
create(:vote, user: organizer, event: event)
Vote.last.destroy
PaperTrail::Version.last.reify.save
visit admin_revision_history_path
expect(page).to have_text("Someone (probably via the console) voted on event My second event in conference #{conference.short_title}")
expect(page).to have_text("Someone (probably via the console) deleted #{organizer.name}'s vote on event #{event.title} in conference #{conference.short_title}")
expect(page).to have_text("Someone (probably via the console) re-added #{organizer.name}'s vote on event #{event.title} in conference #{conference.short_title}")
end
scenario 'display changes in campaign', feature: true, versioning: true, js: true do
campaign = create(:campaign, conference: conference, name: 'Test Campaign', utm_campaign: 'campaign')
campaign.update_attributes(utm_source: 'source', utm_medium: 'medium', utm_term: 'term', utm_content: 'content')

View file

@ -169,70 +169,6 @@ describe Event do
end
end
describe '#user_rating' do
it 'returns 0 if the event has no votes' do
expect(event.user_rating(user)).to eq 0
end
it 'returns 0 if the event has no votes from that user' do
create(:vote, user: another_user, event: event)
expect(event.user_rating(user)).to eq 0
end
it 'returns the rating if the event has votes from that user' do
create(:vote, user: another_user, event: event, rating: 3)
create(:vote, user: user, event: event, rating: 2)
expect(event.user_rating(user)).to eq 2
end
end
describe '#voted?' do
it 'returns false if the event has no votes' do
expect(event.voted?).to eq false
end
it 'returns false if the event has no votes by that user' do
create(:vote, user: another_user, event: event)
expect(event.voted?(user)).to eq false
end
it 'returns true when the event has votes' do
create(:vote, user: another_user, event: event)
expect(event.voted?).to eq true
end
it 'returns true when the event has votes by that user' do
create(:vote, user: user, event: event)
expect(event.voted?(user)).to eq true
end
end
describe '#average_rating' do
context 'returns 0' do
it 'when there are no votes' do
expect(event.average_rating).to eq 0
end
end
context 'returns the average voting' do
before :each do
another_user = create(:user)
create(:vote, user: user, event: event, rating: 1)
create(:vote, user: another_user, event: event, rating: 3)
end
it 'when there are votes and the average is integer' do
expect(event.average_rating).to eq '2'
end
it 'when there are votes and the average is float' do
new_user = create(:user)
create(:vote, user: new_user, event: event, rating: 3)
expect(event.average_rating).to eq '2.33'
end
end
end
describe '#submitter' do
it 'returns the user that submitted the event' do
submitter = create(:user)

View file

@ -27,12 +27,6 @@ describe Program do
expect(build(:program)).to be_valid
end
it 'is valid for rating of 5' do
expect(build(:program, rating: 5)).to be_valid
end
it { is_expected.to validate_numericality_of(:rating).is_greater_than_or_equal_to(0).is_less_than_or_equal_to(10).only_integer }
it { is_expected.to validate_numericality_of(:schedule_interval).is_greater_than_or_equal_to(5).is_less_than_or_equal_to(60) }
describe 'schedule_interval_divisor_60' do
@ -60,6 +54,22 @@ describe Program do
end
describe 'voting_dates_exist' do
it 'is invalid with rating_enabled true, when voting dates does not exist' do
expect(build(:program, rating_enabled: true)).not_to be_valid
end
it 'is valid with rating_enabled true, when voting dates exist' do
expect(build(:program, rating_enabled: true, voting_start_date: Date.today, voting_end_date: Date.today + 1)).to be_valid
end
it 'is valid with blind_voting true, when voting dates exist' do
expect(build(:program, blind_voting: true, voting_start_date: Date.today, voting_end_date: Date.today + 1)).to be_valid
end
it 'is invalid with blind_voting true, when voting dates does not exist' do
expect(build(:program, blind_voting: true)).not_to be_valid
end
it 'is valid, when both voting_start_date and voting_end_date are set' do
expect(build(:program, voting_start_date: Date.today, voting_end_date: Date.today + 1)).to be_valid
end
@ -125,19 +135,6 @@ describe Program do
end
end
describe '#rating_enabled?' do
it 'returns true if proposals can be rated (program.rating > 0)' do
program.rating = 3
expect(program.rating_enabled?).to be true
end
it 'returns false if proposals cannot be rated (program.rating == 0) ' do
program = conference.program
program.rating = 0
expect(program.rating_enabled?).to be false
end
end
describe '#cfp_open?' do
describe 'returns true' do
it 'when there is an open Call for Papers for the conference' do

View file

@ -58,7 +58,6 @@ describe User do
it { is_expected.to have_many(:events_registrations).through(:registrations) }
it { is_expected.to have_many(:ticket_purchases).dependent(:destroy) }
it { is_expected.to have_many(:tickets).through(:ticket_purchases) }
it { is_expected.to have_many(:votes).dependent(:destroy) }
it { is_expected.to have_many(:subscriptions).dependent(:destroy) }
end

View file

@ -0,0 +1,37 @@
require 'spec_helper'
describe VotableField do
subject { create(:votable_field) }
describe 'validations' do
it 'has a valid factory' do
expect(build(:votable_field)).to be_valid
end
it 'is not valid without a title' do
subject.title = ''
expect(subject).to be_invalid
end
it 'is not valid without a votable field' do
should validate_presence_of(:votable_type)
end
it 'is valid with title containing special characters but not spaces' do
should allow_value('example-votable&field').for(:title)
end
it 'is not valid with title containing spaces' do
should_not allow_value('example votable field').for(:title)
end
it 'is not valid with unsupported votable_type' do
should_not allow_value('unsupported').for(:votable_type)
end
it 'is valid for supported votable types' do
should allow_value('Event').for(:votable_type)
end
end
end