Merge 360a764cb4 into f057900fb1
3
Gemfile
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
BIN
app/assets/images/big-star.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
app/assets/images/cancel-off.png
Normal file
|
After Width: | Height: | Size: 699 B |
BIN
app/assets/images/cancel-on.png
Normal file
|
After Width: | Height: | Size: 715 B |
BIN
app/assets/images/mid-star.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
app/assets/images/star-half.png
Normal file
|
After Width: | Height: | Size: 667 B |
BIN
app/assets/images/star-off.png
Normal file
|
After Width: | Height: | Size: 685 B |
BIN
app/assets/images/star-on.png
Normal file
|
After Width: | Height: | Size: 631 B |
|
|
@ -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){
|
||||
|
|
|
|||
760
app/assets/javascripts/jquery.raty.js
Normal 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(' ').prepend(cancel);
|
||||
} else {
|
||||
this.self.append(' ').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 ? ' ' : '');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
5
app/assets/javascripts/osem-rating.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
$( document ).ready(function() {
|
||||
$(".disabled-rate .star").each(function(){
|
||||
$(this).raty('readOnly', true);
|
||||
});
|
||||
});
|
||||
62
app/assets/javascripts/ratyrate.js.erb
Normal 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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
57
app/controllers/admin/votable_fields_controller.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
19
app/controllers/rater_controller.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
4
app/models/average_cache.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class AverageCache < ActiveRecord::Base
|
||||
belongs_to :rater, class_name: 'User'
|
||||
belongs_to :rateable, polymorphic: true
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
3
app/models/overall_average.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class OverallAverage < ActiveRecord::Base
|
||||
belongs_to :rateable, polymorphic: true
|
||||
end
|
||||
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
class Rate < ActiveRecord::Base
|
||||
belongs_to :rater, class_name: 'User'
|
||||
belongs_to :rateable, polymorphic: true
|
||||
end
|
||||
3
app/models/rating_cache.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class RatingCache < ActiveRecord::Base
|
||||
belongs_to :cacheable, polymorphic: true
|
||||
end
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
20
app/models/votable_field.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
$('table#myrating').replaceWith("<%= escape_javascript(render :partial => 'voting') %>");
|
||||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -35,9 +35,6 @@
|
|||
- when 'Comment'
|
||||
= comment_change_description(version)
|
||||
|
||||
- when 'Vote'
|
||||
= vote_change_description(version)
|
||||
|
||||
- when 'User'
|
||||
= user_change_description(version)
|
||||
|
||||
|
|
|
|||
16
app/views/admin/votable_fields/_form.html.haml
Normal 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' }
|
||||
42
app/views/admin/votable_fields/index.html.haml
Normal 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)
|
||||
|
||||
1
app/views/admin/votable_fields/new.html.haml
Normal file
|
|
@ -0,0 +1 @@
|
|||
= render 'form'
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
17
db/migrate/20160922205550_create_rating_caches.rb
Normal 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
|
||||
18
db/migrate/20160922205551_create_rates.rb
Normal 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
|
||||
14
db/migrate/20160922205552_create_average_caches.rb
Normal 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
|
||||
13
db/migrate/20160922205553_create_overall_averages.rb
Normal 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
|
||||
12
db/migrate/20170319144754_create_votable_fields.rb
Normal 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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class AddForAdminToVotableField < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :votable_fields, :for_admin, :boolean, default: false
|
||||
end
|
||||
end
|
||||
5
db/migrate/20170326084800_add_stars_to_votable_field.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class AddStarsToVotableField < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :votable_fields, :stars, :integer, default: 5
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class AddRatingEnabledToProgram < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :programs, :rating_enabled, :boolean, default: false
|
||||
end
|
||||
end
|
||||
56
db/schema.rb
|
|
@ -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"
|
||||
|
|
|
|||
20
lib/tasks/migrate_votes.rake
Normal 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
|
||||
7
spec/factories/votable_fields.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
FactoryGirl.define do
|
||||
factory :votable_field do
|
||||
title { Faker::Lorem.word }
|
||||
votable_type { 'Event' }
|
||||
conference
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
FactoryGirl.define do
|
||||
factory :vote do
|
||||
event
|
||||
user
|
||||
rating 1
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
37
spec/models/votable_field_spec.rb
Normal 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
|
||||