diff --git a/Gemfile b/Gemfile index 1b882aab..10081399 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index 3bc396d4..4c816d66 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/images/big-star.png b/app/assets/images/big-star.png new file mode 100644 index 00000000..9304638a Binary files /dev/null and b/app/assets/images/big-star.png differ diff --git a/app/assets/images/cancel-off.png b/app/assets/images/cancel-off.png new file mode 100644 index 00000000..a3031f05 Binary files /dev/null and b/app/assets/images/cancel-off.png differ diff --git a/app/assets/images/cancel-on.png b/app/assets/images/cancel-on.png new file mode 100644 index 00000000..08f24936 Binary files /dev/null and b/app/assets/images/cancel-on.png differ diff --git a/app/assets/images/mid-star.png b/app/assets/images/mid-star.png new file mode 100644 index 00000000..aebb3ccb Binary files /dev/null and b/app/assets/images/mid-star.png differ diff --git a/app/assets/images/star-half.png b/app/assets/images/star-half.png new file mode 100644 index 00000000..3c19e90a Binary files /dev/null and b/app/assets/images/star-half.png differ diff --git a/app/assets/images/star-off.png b/app/assets/images/star-off.png new file mode 100644 index 00000000..956fa7c6 Binary files /dev/null and b/app/assets/images/star-off.png differ diff --git a/app/assets/images/star-on.png b/app/assets/images/star-on.png new file mode 100644 index 00000000..975fe7f3 Binary files /dev/null and b/app/assets/images/star-on.png differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index b347821b..b7b63dc0 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -47,6 +47,8 @@ //= require unobtrusive_flash_bootstrap //= require countable //= require selectize +//= require jquery.raty +//= require ratyrate $(document).ready(function() { $('a[disabled=disabled]').click(function(event){ diff --git a/app/assets/javascripts/jquery.raty.js b/app/assets/javascripts/jquery.raty.js new file mode 100644 index 00000000..c5836513 --- /dev/null +++ b/app/assets/javascripts/jquery.raty.js @@ -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 $('', { 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); diff --git a/app/assets/javascripts/ratyrate.js.erb b/app/assets/javascripts/ratyrate.js.erb new file mode 100644 index 00000000..bdf21db4 --- /dev/null +++ b/app/assets/javascripts/ratyrate.js.erb @@ -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 }); + } + } + }); + } + }); + }); +}); diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index c95bbb87..7fca341d 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -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 diff --git a/app/assets/stylesheets/osem-rating.css.scss b/app/assets/stylesheets/osem-rating.css.scss deleted file mode 100644 index 7312ac45..00000000 --- a/app/assets/stylesheets/osem-rating.css.scss +++ /dev/null @@ -1,19 +0,0 @@ -/* 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; -} -.myrating, .othersrating { - background: image-url("star.png") 0 0; - width: 24px; - height: 24px; - float: left; -} - diff --git a/app/controllers/admin/events_controller.rb b/app/controllers/admin/events_controller.rb index 6cf31ddb..934c2a70 100644 --- a/app/controllers/admin/events_controller.rb +++ b/app/controllers/admin/events_controller.rb @@ -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 diff --git a/app/controllers/admin/programs_controller.rb b/app/controllers/admin/programs_controller.rb index fe401078..320d1cba 100644 --- a/app/controllers/admin/programs_controller.rb +++ b/app/controllers/admin/programs_controller.rb @@ -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 diff --git a/app/controllers/admin/votable_fields_controller.rb b/app/controllers/admin/votable_fields_controller.rb new file mode 100644 index 00000000..332252c7 --- /dev/null +++ b/app/controllers/admin/votable_fields_controller.rb @@ -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 diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index f940e7ee..9abe2a6b 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -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 diff --git a/app/controllers/rater_controller.rb b/app/controllers/rater_controller.rb new file mode 100644 index 00000000..0a2c150d --- /dev/null +++ b/app/controllers/rater_controller.rb @@ -0,0 +1,12 @@ +class RaterController < ApplicationController + def create + if user_signed_in? + obj = params[:klass].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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c82e6bab..9eb48682 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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 diff --git a/app/models/ability.rb b/app/models/ability.rb index decf8cb8..74f0d56c 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -138,6 +138,7 @@ 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, 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 +193,7 @@ 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, 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 +222,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')) diff --git a/app/models/average_cache.rb b/app/models/average_cache.rb new file mode 100644 index 00000000..49fb96b8 --- /dev/null +++ b/app/models/average_cache.rb @@ -0,0 +1,4 @@ +class AverageCache < ActiveRecord::Base + belongs_to :rater, class_name: 'User' + belongs_to :rateable, polymorphic: true +end diff --git a/app/models/conference.rb b/app/models/conference.rb index 224c1dfb..1257f874 100644 --- a/app/models/conference.rb +++ b/app/models/conference.rb @@ -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 diff --git a/app/models/event.rb b/app/models/event.rb index 2bb526b9..44890ed4 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -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 diff --git a/app/models/overall_average.rb b/app/models/overall_average.rb new file mode 100644 index 00000000..9784ffdd --- /dev/null +++ b/app/models/overall_average.rb @@ -0,0 +1,3 @@ +class OverallAverage < ActiveRecord::Base + belongs_to :rateable, polymorphic: true +end diff --git a/app/models/program.rb b/app/models/program.rb index e8114f99..79a14afe 100644 --- a/app/models/program.rb +++ b/app/models/program.rb @@ -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 diff --git a/app/models/rate.rb b/app/models/rate.rb new file mode 100644 index 00000000..91eb105e --- /dev/null +++ b/app/models/rate.rb @@ -0,0 +1,4 @@ +class Rate < ActiveRecord::Base + belongs_to :rater, class_name: 'User' + belongs_to :rateable, polymorphic: true +end diff --git a/app/models/rating_cache.rb b/app/models/rating_cache.rb new file mode 100644 index 00000000..dd372095 --- /dev/null +++ b/app/models/rating_cache.rb @@ -0,0 +1,3 @@ +class RatingCache < ActiveRecord::Base + belongs_to :cacheable, polymorphic: true +end diff --git a/app/models/user.rb b/app/models/user.rb index 5f41e693..a4b7a285 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/models/votable_field.rb b/app/models/votable_field.rb new file mode 100644 index 00000000..99f8fceb --- /dev/null +++ b/app/models/votable_field.rb @@ -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 diff --git a/app/models/vote.rb b/app/models/vote.rb index 5b0e3fe7..3b2b5a27 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -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 diff --git a/app/views/admin/cfps/show.html.haml b/app/views/admin/cfps/show.html.haml index 09441508..17f21f6f 100644 --- a/app/views/admin/cfps/show.html.haml +++ b/app/views/admin/cfps/show.html.haml @@ -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 diff --git a/app/views/admin/events/_proposal.html.haml b/app/views/admin/events/_proposal.html.haml index 15894f26..76dc3b4e 100644 --- a/app/views/admin/events/_proposal.html.haml +++ b/app/views/admin/events/_proposal.html.haml @@ -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' diff --git a/app/views/admin/events/_voting.html.haml b/app/views/admin/events/_voting.html.haml index b7ae8ee2..55fd6498 100644 --- a/app/views/admin/events/_voting.html.haml +++ b/app/views/admin/events/_voting.html.haml @@ -1,80 +1,32 @@ -%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 + = 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) diff --git a/app/views/admin/events/_voting_index.html.haml b/app/views/admin/events/_voting_index.html.haml deleted file mode 100644 index 18a9e73b..00000000 --- a/app/views/admin/events/_voting_index.html.haml +++ /dev/null @@ -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 diff --git a/app/views/admin/events/index.html.haml b/app/views/admin/events/index.html.haml index 0e204c87..34c020d3 100644 --- a/app/views/admin/events/index.html.haml +++ b/app/views/admin/events/index.html.haml @@ -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 diff --git a/app/views/admin/events/show.html.haml b/app/views/admin/events/show.html.haml index 9969bbaf..813a4514 100644 --- a/app/views/admin/events/show.html.haml +++ b/app/views/admin/events/show.html.haml @@ -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', diff --git a/app/views/admin/events/vote.js.erb b/app/views/admin/events/vote.js.erb deleted file mode 100644 index dc677901..00000000 --- a/app/views/admin/events/vote.js.erb +++ /dev/null @@ -1 +0,0 @@ -$('table#myrating').replaceWith("<%= escape_javascript(render :partial => 'voting') %>"); \ No newline at end of file diff --git a/app/views/admin/programs/_form.html.haml b/app/views/admin/programs/_form.html.haml index 035093f0..5c4c5b12 100644 --- a/app/views/admin/programs/_form.html.haml +++ b/app/views/admin/programs/_form.html.haml @@ -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'} diff --git a/app/views/admin/programs/show.html.haml b/app/views/admin/programs/show.html.haml index 47e50fb6..557d9720 100644 --- a/app/views/admin/programs/show.html.haml +++ b/app/views/admin/programs/show.html.haml @@ -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 diff --git a/app/views/admin/users/_submissions.html.haml b/app/views/admin/users/_submissions.html.haml index c3b05119..c9583df8 100644 --- a/app/views/admin/users/_submissions.html.haml +++ b/app/views/admin/users/_submissions.html.haml @@ -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 diff --git a/app/views/admin/versions/_object_desc_and_link.html.haml b/app/views/admin/versions/_object_desc_and_link.html.haml index 44eedf82..2e280b5d 100644 --- a/app/views/admin/versions/_object_desc_and_link.html.haml +++ b/app/views/admin/versions/_object_desc_and_link.html.haml @@ -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 diff --git a/app/views/admin/versions/index.html.haml b/app/views/admin/versions/index.html.haml index 69af134b..d9219b9a 100644 --- a/app/views/admin/versions/index.html.haml +++ b/app/views/admin/versions/index.html.haml @@ -35,9 +35,6 @@ - when 'Comment' = comment_change_description(version) - - when 'Vote' - = vote_change_description(version) - - when 'User' = user_change_description(version) diff --git a/app/views/admin/votable_fields/_form.html.haml b/app/views/admin/votable_fields/_form.html.haml new file mode 100644 index 00000000..f45e3239 --- /dev/null +++ b/app/views/admin/votable_fields/_form.html.haml @@ -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' } diff --git a/app/views/admin/votable_fields/index.html.haml b/app/views/admin/votable_fields/index.html.haml new file mode 100644 index 00000000..cb99f0e0 --- /dev/null +++ b/app/views/admin/votable_fields/index.html.haml @@ -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) + diff --git a/app/views/admin/votable_fields/new.html.haml b/app/views/admin/votable_fields/new.html.haml new file mode 100644 index 00000000..bcc58327 --- /dev/null +++ b/app/views/admin/votable_fields/new.html.haml @@ -0,0 +1 @@ += render 'form' diff --git a/app/views/layouts/_admin_sidebar.html.haml b/app/views/layouts/_admin_sidebar.html.haml index a9a2a99a..5821c450 100644 --- a/app/views/layouts/_admin_sidebar.html.haml +++ b/app/views/layouts/_admin_sidebar.html.haml @@ -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 diff --git a/app/views/proposals/show.html.haml b/app/views/proposals/show.html.haml index 2d38d3a8..5de37a52 100644 --- a/app/views/proposals/show.html.haml +++ b/app/views/proposals/show.html.haml @@ -45,6 +45,13 @@ .row.speakerbio .col-md-12 = markdown(speaker.biography) + -if @event.scheduled? + %dl.col-md-12 + -if @event.ended? + - @votable_fields.each do |field| + %dt= field.title + %dd= rating_for @event, field.title + %br .col-md-9 .row .col-md-12 diff --git a/config/routes.rb b/config/routes.rb index 9fa1c29a..e4ddd67e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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] diff --git a/db/migrate/20160922205550_create_rating_caches.rb b/db/migrate/20160922205550_create_rating_caches.rb new file mode 100644 index 00000000..0a1fa176 --- /dev/null +++ b/db/migrate/20160922205550_create_rating_caches.rb @@ -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 diff --git a/db/migrate/20160922205551_create_rates.rb b/db/migrate/20160922205551_create_rates.rb new file mode 100644 index 00000000..7e1a79f7 --- /dev/null +++ b/db/migrate/20160922205551_create_rates.rb @@ -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 diff --git a/db/migrate/20160922205552_create_average_caches.rb b/db/migrate/20160922205552_create_average_caches.rb new file mode 100644 index 00000000..7ff567d7 --- /dev/null +++ b/db/migrate/20160922205552_create_average_caches.rb @@ -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 diff --git a/db/migrate/20160922205553_create_overall_averages.rb b/db/migrate/20160922205553_create_overall_averages.rb new file mode 100644 index 00000000..e080b18d --- /dev/null +++ b/db/migrate/20160922205553_create_overall_averages.rb @@ -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 diff --git a/db/migrate/20170319144754_create_votable_fields.rb b/db/migrate/20170319144754_create_votable_fields.rb new file mode 100644 index 00000000..5612511e --- /dev/null +++ b/db/migrate/20170319144754_create_votable_fields.rb @@ -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 diff --git a/db/migrate/20170325130604_add_for_admin_to_votable_field.rb b/db/migrate/20170325130604_add_for_admin_to_votable_field.rb new file mode 100644 index 00000000..358ee4a4 --- /dev/null +++ b/db/migrate/20170325130604_add_for_admin_to_votable_field.rb @@ -0,0 +1,5 @@ +class AddForAdminToVotableField < ActiveRecord::Migration + def change + add_column :votable_fields, :for_admin, :boolean, default: false + end +end diff --git a/db/migrate/20170326084800_add_stars_to_votable_field.rb b/db/migrate/20170326084800_add_stars_to_votable_field.rb new file mode 100644 index 00000000..3abf1aa5 --- /dev/null +++ b/db/migrate/20170326084800_add_stars_to_votable_field.rb @@ -0,0 +1,5 @@ +class AddStarsToVotableField < ActiveRecord::Migration + def change + add_column :votable_fields, :stars, :integer, default: 5 + end +end diff --git a/db/migrate/20170405004359_add_rating_enabled_to_program.rb b/db/migrate/20170405004359_add_rating_enabled_to_program.rb new file mode 100644 index 00000000..c7437dad --- /dev/null +++ b/db/migrate/20170405004359_add_rating_enabled_to_program.rb @@ -0,0 +1,5 @@ +class AddRatingEnabledToProgram < ActiveRecord::Migration + def change + add_column :programs, :rating_enabled, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index ab95b4dc..148be94c 100644 --- a/db/schema.rb +++ b/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" diff --git a/lib/tasks/migrate_votes.rake b/lib/tasks/migrate_votes.rake new file mode 100644 index 00000000..78cb341c --- /dev/null +++ b/lib/tasks/migrate_votes.rake @@ -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 unless votes.present? + 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 diff --git a/spec/factories/votable_fields.rb b/spec/factories/votable_fields.rb new file mode 100644 index 00000000..d9fb80a3 --- /dev/null +++ b/spec/factories/votable_fields.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :votable_field do + title { Faker::Lorem.word } + votable_type { 'Event' } + conference + end +end diff --git a/spec/factories/votes.rb b/spec/factories/votes.rb deleted file mode 100644 index 12ede741..00000000 --- a/spec/factories/votes.rb +++ /dev/null @@ -1,7 +0,0 @@ -FactoryGirl.define do - factory :vote do - event - user - rating 1 - end -end diff --git a/spec/features/program_spec.rb b/spec/features/program_spec.rb index 369a0525..a0184159 100644 --- a/spec/features/program_spec.rb +++ b/spec/features/program_spec.rb @@ -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 diff --git a/spec/features/versions_spec.rb b/spec/features/versions_spec.rb index ba43bc27..f0eadbf3 100644 --- a/spec/features/versions_spec.rb +++ b/spec/features/versions_spec.rb @@ -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') diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index e1c2297c..c0d11cf0 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -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) diff --git a/spec/models/program_spec.rb b/spec/models/program_spec.rb index ac720def..99db7049 100644 --- a/spec/models/program_spec.rb +++ b/spec/models/program_spec.rb @@ -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 @@ -125,19 +119,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 diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index def87319..dda83fdd 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/models/votable_field_spec.rb b/spec/models/votable_field_spec.rb new file mode 100644 index 00000000..bc6eb4bc --- /dev/null +++ b/spec/models/votable_field_spec.rb @@ -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