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 # for observing records
gem 'rails-observers' gem 'rails-observers'
# for rating
gem 'ratyrate', github: 'wazery/ratyrate', branch: 'master'
# for tracking data changes # for tracking data changes
gem 'paper_trail' 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 GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
remote: https://rails-assets.org/ remote: https://rails-assets.org/
@ -611,6 +618,7 @@ DEPENDENCIES
rails-i18n (~> 4.0.0) rails-i18n (~> 4.0.0)
rails-observers rails-observers
rails_12factor rails_12factor
ratyrate!
rdoc-generator-fivefish rdoc-generator-fivefish
redcarpet redcarpet
responders (~> 2.0) 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 unobtrusive_flash_bootstrap
//= require countable //= require countable
//= require selectize //= require selectize
//= require jquery.raty
//= require ratyrate
//= require osem-rating
$(document).ready(function() { $(document).ready(function() {
$('a[disabled=disabled]').click(function(event){ $('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 formtastic-bootstrap
*= require dataTables/bootstrap/3/jquery.dataTables.bootstrap *= require dataTables/bootstrap/3/jquery.dataTables.bootstrap
*= require osem *= require osem
*= require osem-rating
*= require osem-schedule *= require osem-schedule
*= require osem-schedule-print *= require osem-schedule-print
*= require osem-dashboard *= require osem-dashboard
@ -18,4 +17,5 @@
*= require osem-navbar *= require osem-navbar
*= require selectize *= require selectize
*= require selectize.bootstrap3 *= require selectize.bootstrap3
*= require osem-rating
*/ */

View file

@ -1,19 +1,3 @@
/* Styling for voting on proposals*/ img.raty-cancel {
.myrating.bright { background-image: image-url("star-bright.png"); } display: none;
.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;
} }
.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 @event_types = @program.event_types
@comments = @event.root_comments @comments = @event.root_comments
@comment_count = @event.comment_threads.count @comment_count = @event.comment_threads.count
@ratings = @event.votes.includes(:user)
@difficulty_levels = @program.difficulty_levels @difficulty_levels = @program.difficulty_levels
@versions = @event.versions | @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(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: '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%") | @votable_fields = VotableField.where(votable_type: 'Event', conference: @event.program.conference, enabled: true, for_admin: true)
PaperTrail::Version.where(item_type: 'Vote').where('object LIKE ?', "%\nevent_id: #{@event.id}\n%") Event.vote(@votable_fields)
end end
def edit def edit
@ -142,24 +141,6 @@ module Admin
update_state(:restart, 'Review started!') update_state(:restart, 'Review started!')
end 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 def registrations
@event_registrations = @event.events_registrations @event_registrations = @event.events_registrations
end end

View file

@ -38,7 +38,7 @@ module Admin
private private
def program_params 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 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 def show
@event_schedule = @event.event_schedules.find_by(schedule_id: @program.selected_schedule_id) @event_schedule = @event.event_schedules.find_by(schedule_id: @program.selected_schedule_id)
@speakers_ordered = @event.speakers_ordered @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 end
def new 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
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) def user_change_description(version)
if version.event == 'create' if version.event == 'create'
link_to_user(version.item_id) + ' signed up' link_to_user(version.item_id) + ' signed up'
@ -597,4 +586,12 @@ module ApplicationHelper
end end
concurrent_events concurrent_events
end 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 end

View file

@ -138,6 +138,8 @@ class Ability
# ids of all the conferences for which the user has the 'organizer' role # ids of all the conferences for which the user has the 'organizer' role
conf_ids_for_organizer = Conference.with_role(:organizer, user).pluck(:id) 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 :manage, Resource, conference_id: conf_ids_for_organizer
can [:new, :create], Conference if user.has_role?(:organizer, :any) can [:new, :create], Conference if user.has_role?(:organizer, :any)
can :manage, Conference, id: conf_ids_for_organizer 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 # ids of all the conferences for which the user has the 'cfp' role
conf_ids_for_cfp = Conference.with_role(:cfp, user).pluck(:id) 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 [:index, :show, :update], Resource, conference_id: conf_ids_for_cfp
can :manage, Event, program: { 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 } can :manage, EventType, program: { conference_id: conf_ids_for_cfp }
@ -220,7 +224,6 @@ class Ability
end 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: '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| can [:index, :revert_object, :revert_attribute], PaperTrail::Version do |version|
version.item_type == 'Commercial' && conf_ids_for_cfp.include?(version.conference_id) && 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')) (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 :supporters, through: :ticket_purchases, source: :user
has_many :tickets, dependent: :destroy has_many :tickets, dependent: :destroy
has_many :resources, dependent: :destroy has_many :resources, dependent: :destroy
has_many :votable_fields, dependent: :destroy
has_many :lodgings, dependent: :destroy has_many :lodgings, dependent: :destroy
has_many :registrations, dependent: :destroy has_many :registrations, dependent: :destroy
has_many :participants, through: :registrations, source: :user has_many :participants, through: :registrations, source: :user

View file

@ -1,5 +1,8 @@
class Event < ActiveRecord::Base class Event < ActiveRecord::Base
include ActiveRecord::Transitions 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 } has_paper_trail on: [:create, :update], ignore: [:updated_at, :guid, :week], meta: { conference_id: :conference_id }
acts_as_commentable 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_event_user, -> { where(event_role: 'submitter') }, class_name: 'EventUser'
has_one :submitter, through: :submitter_event_user, source: :user 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 has_many :commercials, as: :commercialable, dependent: :destroy
belongs_to :event_type belongs_to :event_type
@ -92,33 +93,9 @@ class Event < ActiveRecord::Base
registrations.count < max_attendees registrations.count < max_attendees
end end
## def ended?
# Finds the rating of the user for the event timezone = program.conference.timezone
# ====Returns Time.now.in_time_zone(timezone) > event_schedules.find_by(schedule: program.selected_schedule).end_time
# * +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
end end
# get event speakers with the event sumbmitter at the first position # 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 accepts_nested_attributes_for :difficulty_levels, allow_destroy: true
# validates :conference_id, presence: true, uniqueness: 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 validates :schedule_interval, numericality: { greater_than_or_equal_to: 5, less_than_or_equal_to: 60 }, presence: true
validate :schedule_interval_divisor_60 validate :schedule_interval_divisor_60
validate :voting_start_date_before_end_date 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_event_types
after_create :create_difficulty_levels after_create :create_difficulty_levels
@ -93,10 +93,10 @@ class Program < ActiveRecord::Base
end 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 # ====Returns
# Errors when the condition is not true # 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_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 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 errors.add(:voting_start_date, 'must be set, when voting_end_date is set') if voting_end_date && !voting_start_date
end 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 # Checks if voting_start_date is before voting_end_date
# ====Returns # ====Returns
@ -115,16 +124,7 @@ class Program < ActiveRecord::Base
end 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 # Checks if the call for papers for the conference is currently open
# #
# ====Returns # ====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 end
class User < ActiveRecord::Base class User < ActiveRecord::Base
ratyrate_rater
rolify rolify
has_many :users_roles has_many :users_roles
has_many :roles, through: :users_roles, dependent: :destroy has_many :roles, through: :users_roles, dependent: :destroy
@ -47,8 +48,6 @@ class User < ActiveRecord::Base
has_many :ticket_purchases, dependent: :destroy has_many :ticket_purchases, dependent: :destroy
has_many :payments, dependent: :destroy has_many :payments, dependent: :destroy
has_many :tickets, through: :ticket_purchases, source: :ticket 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 has_many :subscriptions, dependent: :destroy
accepts_nested_attributes_for :roles 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 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 end

View file

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

View file

@ -168,7 +168,7 @@
%b Description %b Description
%td= simple_format(@event.description) %td= simple_format(@event.description)
- if @conference.program && @conference.program.rating && @conference.program.rating > 0 - if @votable_fields.present?
= render partial: 'voting' = render partial: 'voting'
.row .row

View file

@ -1,80 +1,33 @@
%table.table#myrating %hr
- if @program.show_voting? - if @program.rating_enabled
%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? - 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 @program.show_voting?
- if @ratings.length > 0 %table.table
- @ratings.each do |rate| %thead
- unless rate.user_id == current_user.id %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 %tr
%td %td
= rate.name Voters
%td %td
- @conference.program.rating.times do |counter| = raters(@votable_fields)
%table.table
- if @event.voted?(rate.user) && @event.user_rating(rate.user) == counter + 1 %thead
= label_tag "label#{counter + 1}", "", class: 'othersrating', voted: true %td.col-md-2
= javascript_tag "$('label[voted=true]').prevAll().andSelf().addClass('bright');" %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 - else
= label_tag "label#{counter + 1}", "", class: 'othersrating' %b
:javascript = voting_open_or_close(@program)
$(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");
});

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 %b ID
%th %th
%b Title %b Title
- if @program.rating_enabled?
%th
%b Rating
%th %th
%b Submitter %b Submitter
%th %th
@ -80,11 +77,6 @@
= event.id = event.id
%td %td
= link_to event.title, admin_conference_program_event_path(@conference.short_title, event) = 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 - if event.submitter && event.submitter.registrations && event.submitter.registrations.count < 1
- bgcolor = '#F7819F' - bgcolor = '#F7819F'
- else - else

View file

@ -36,10 +36,6 @@
= event_change_description(version) = event_change_description(version)
= "event #{@event.title}" = "event #{@event.title}"
- elsif version.item_type == 'Vote'
= vote_change_description(version)
= "event #{@event.title}"
- else - else
= general_change_description(version) = general_change_description(version)
= link_to 'commercial', = 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| = 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_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 :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 :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 :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 :rating_enabled, label: 'Enable voting', hint: 'To enable voting you need to set voting dates 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 :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_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 :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 %p.text-right
= f.action :submit, as: :button, button_html: {class: 'btn btn-primary'} = f.action :submit, as: :button, button_html: {class: 'btn btn-primary'}

View file

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

View file

@ -11,7 +11,6 @@
%th Title %th Title
%th State %th State
%th Type %th Type
%th Rating
%th Created At %th Created At
%tbody %tbody
- @user.events.each do |event| - @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= link_to event.title, admin_conference_program_event_path(event.program.conference.short_title, event)
%td= event.state %td= event.state
%td= "#{event.event_type.title} (#{show_time(event.event_type.length)})" %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 %td= event.created_at

View file

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

View file

@ -35,9 +35,6 @@
- when 'Comment' - when 'Comment'
= comment_change_description(version) = comment_change_description(version)
- when 'Vote'
= vote_change_description(version)
- when 'User' - when 'User'
= user_change_description(version) = 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 = link_to admin_conference_resources_path(@conference.short_title) do
%span.fa.fa-pencil-square %span.fa.fa-pencil-square
Resources 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: %dt Room:
%dd %dd
= event.room.name = 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 Osem::Application.routes.draw do
post '/rate' => 'rater#create', as: 'rate'
if ENV['OSEM_ICHAIN_ENABLED'] == 'true' if ENV['OSEM_ICHAIN_ENABLED'] == 'true'
devise_for :users, controllers: { registrations: :registrations } devise_for :users, controllers: { registrations: :registrations }
else else
@ -72,6 +73,7 @@ Osem::Application.routes.draw do
end end
resources :resources resources :resources
resources :votable_fields
resources :tickets resources :tickets
resources :sponsors, except: [:show] resources :sponsors, except: [:show]
resources :lodgings, 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. # 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| create_table "ahoy_events", force: :cascade do |t|
t.uuid "visit_id", limit: 16 t.uuid "visit_id", limit: 16
@ -31,6 +31,15 @@ ActiveRecord::Schema.define(version: 20170302145716) do
t.datetime "updated_at" t.datetime "updated_at"
end 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| create_table "campaigns", force: :cascade do |t|
t.integer "conference_id" t.integer "conference_id"
t.string "name" t.string "name"
@ -266,6 +275,14 @@ ActiveRecord::Schema.define(version: 20170302145716) do
t.datetime "updated_at" t.datetime "updated_at"
end 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| create_table "payments", force: :cascade do |t|
t.string "last4" t.string "last4"
t.integer "amount" t.integer "amount"
@ -290,6 +307,7 @@ ActiveRecord::Schema.define(version: 20170302145716) do
t.datetime "voting_end_date" t.datetime "voting_end_date"
t.integer "selected_schedule_id" t.integer "selected_schedule_id"
t.integer "schedule_interval", default: 15, null: false t.integer "schedule_interval", default: 15, null: false
t.boolean "rating_enabled", default: false
end end
add_index "programs", ["selected_schedule_id"], name: "index_programs_on_selected_schedule_id" 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" t.datetime "updated_at"
end 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| create_table "registration_periods", force: :cascade do |t|
t.integer "conference_id" t.integer "conference_id"
t.date "start_date" 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" 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| create_table "votes", force: :cascade do |t|
t.integer "event_id" t.integer "event_id"
t.integer "rating" 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 sign_in organizer
end 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
end end

View file

@ -15,6 +15,7 @@ feature Event do
@event = create(:event, program: conference.program, title: 'Example Proposal') @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: 'submitter')
@event.event_users.create(user: participant, event_role: 'speaker') @event.event_users.create(user: participant, event_role: 'speaker')
create(:votable_field, conference: conference, for_admin: true)
end end
after(:each) do after(:each) do
@ -26,6 +27,38 @@ feature Event do
sign_in organizer sign_in organizer
end 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 scenario 'rejects a proposal', feature: true, js: true do
visit admin_conference_program_events_path(conference.short_title) visit admin_conference_program_events_path(conference.short_title)
expect(page.has_content?('Example Proposal')).to be true 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}") expect(page).to have_text("#{organizer.name} updated social tag, email, googleplus and sponsor email of contact details in conference #{conference.short_title}")
end 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 scenario 'display changes in cfp', feature: true, versioning: true, js: true do
cfp = create(:cfp, program: conference.program) 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')) 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}") 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 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 scenario 'display changes in campaign', feature: true, versioning: true, js: true do
campaign = create(:campaign, conference: conference, name: 'Test Campaign', utm_campaign: 'campaign') 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') 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
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 describe '#submitter' do
it 'returns the user that submitted the event' do it 'returns the user that submitted the event' do
submitter = create(:user) submitter = create(:user)

View file

@ -27,12 +27,6 @@ describe Program do
expect(build(:program)).to be_valid expect(build(:program)).to be_valid
end 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) } 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 describe 'schedule_interval_divisor_60' do
@ -60,6 +54,22 @@ describe Program do
end end
describe 'voting_dates_exist' do 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 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 expect(build(:program, voting_start_date: Date.today, voting_end_date: Date.today + 1)).to be_valid
end end
@ -125,19 +135,6 @@ describe Program do
end end
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 '#cfp_open?' do
describe 'returns true' do describe 'returns true' do
it 'when there is an open Call for Papers for the conference' 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(:events_registrations).through(:registrations) }
it { is_expected.to have_many(:ticket_purchases).dependent(:destroy) } 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(:tickets).through(:ticket_purchases) }
it { is_expected.to have_many(:votes).dependent(:destroy) }
it { is_expected.to have_many(:subscriptions).dependent(:destroy) } it { is_expected.to have_many(:subscriptions).dependent(:destroy) }
end 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