From 20f48534876ed4c0172971c846be509914165c9e Mon Sep 17 00:00:00 2001 From: shlok007 Date: Thu, 22 Sep 2016 20:06:46 -0400 Subject: [PATCH] Implement Event Ratings --- Gemfile | 3 + Gemfile.lock | 8 + app/assets/images/big-star.png | Bin 0 -> 8089 bytes app/assets/images/cancel-off.png | Bin 0 -> 699 bytes app/assets/images/cancel-on.png | Bin 0 -> 715 bytes app/assets/images/mid-star.png | Bin 0 -> 6335 bytes app/assets/images/star-half.png | Bin 0 -> 667 bytes app/assets/images/star-off.png | Bin 0 -> 685 bytes app/assets/images/star-on.png | Bin 0 -> 631 bytes app/assets/javascripts/application.js | 2 + app/assets/javascripts/jquery.raty.js | 760 ++++++++++++++++++ app/assets/javascripts/ratyrate.js.erb | 62 ++ app/assets/stylesheets/application.css | 1 - app/assets/stylesheets/osem-rating.css.scss | 19 - app/controllers/admin/events_controller.rb | 25 +- app/controllers/admin/programs_controller.rb | 2 +- .../admin/votable_fields_controller.rb | 57 ++ app/controllers/proposals_controller.rb | 2 + app/controllers/rater_controller.rb | 12 + app/helpers/application_helper.rb | 19 +- app/models/ability.rb | 3 +- app/models/average_cache.rb | 4 + app/models/conference.rb | 2 +- app/models/event.rb | 35 +- app/models/overall_average.rb | 3 + app/models/program.rb | 26 +- app/models/rate.rb | 4 + app/models/rating_cache.rb | 3 + app/models/user.rb | 3 +- app/models/votable_field.rb | 20 + app/models/vote.rb | 12 - app/views/admin/cfps/show.html.haml | 4 - app/views/admin/events/_proposal.html.haml | 4 +- app/views/admin/events/_voting.html.haml | 106 +-- .../admin/events/_voting_index.html.haml | 19 - app/views/admin/events/index.html.haml | 8 - app/views/admin/events/show.html.haml | 4 - app/views/admin/events/vote.js.erb | 1 - app/views/admin/programs/_form.html.haml | 8 +- app/views/admin/programs/show.html.haml | 11 +- app/views/admin/users/_submissions.html.haml | 9 - .../versions/_object_desc_and_link.html.haml | 2 +- app/views/admin/versions/index.html.haml | 3 - .../admin/votable_fields/_form.html.haml | 16 + .../admin/votable_fields/index.html.haml | 42 + app/views/admin/votable_fields/new.html.haml | 1 + app/views/layouts/_admin_sidebar.html.haml | 5 + app/views/proposals/show.html.haml | 7 + config/routes.rb | 2 + .../20160922205550_create_rating_caches.rb | 17 + db/migrate/20160922205551_create_rates.rb | 18 + .../20160922205552_create_average_caches.rb | 14 + .../20160922205553_create_overall_averages.rb | 13 + .../20170319144754_create_votable_fields.rb | 12 + ...25130604_add_for_admin_to_votable_field.rb | 5 + ...170326084800_add_stars_to_votable_field.rb | 5 + ...405004359_add_rating_enabled_to_program.rb | 5 + db/schema.rb | 56 +- lib/tasks/migrate_votes.rake | 20 + spec/factories/votable_fields.rb | 7 + spec/factories/votes.rb | 7 - spec/features/program_spec.rb | 14 - spec/features/versions_spec.rb | 23 - spec/models/event_spec.rb | 64 -- spec/models/program_spec.rb | 19 - spec/models/user_spec.rb | 1 - spec/models/votable_field_spec.rb | 37 + 67 files changed, 1298 insertions(+), 378 deletions(-) create mode 100644 app/assets/images/big-star.png create mode 100644 app/assets/images/cancel-off.png create mode 100644 app/assets/images/cancel-on.png create mode 100644 app/assets/images/mid-star.png create mode 100644 app/assets/images/star-half.png create mode 100644 app/assets/images/star-off.png create mode 100644 app/assets/images/star-on.png create mode 100644 app/assets/javascripts/jquery.raty.js create mode 100644 app/assets/javascripts/ratyrate.js.erb delete mode 100644 app/assets/stylesheets/osem-rating.css.scss create mode 100644 app/controllers/admin/votable_fields_controller.rb create mode 100644 app/controllers/rater_controller.rb create mode 100644 app/models/average_cache.rb create mode 100644 app/models/overall_average.rb create mode 100644 app/models/rate.rb create mode 100644 app/models/rating_cache.rb create mode 100644 app/models/votable_field.rb delete mode 100644 app/views/admin/events/_voting_index.html.haml delete mode 100644 app/views/admin/events/vote.js.erb create mode 100644 app/views/admin/votable_fields/_form.html.haml create mode 100644 app/views/admin/votable_fields/index.html.haml create mode 100644 app/views/admin/votable_fields/new.html.haml create mode 100644 db/migrate/20160922205550_create_rating_caches.rb create mode 100644 db/migrate/20160922205551_create_rates.rb create mode 100644 db/migrate/20160922205552_create_average_caches.rb create mode 100644 db/migrate/20160922205553_create_overall_averages.rb create mode 100644 db/migrate/20170319144754_create_votable_fields.rb create mode 100644 db/migrate/20170325130604_add_for_admin_to_votable_field.rb create mode 100644 db/migrate/20170326084800_add_stars_to_votable_field.rb create mode 100644 db/migrate/20170405004359_add_rating_enabled_to_program.rb create mode 100644 lib/tasks/migrate_votes.rake create mode 100644 spec/factories/votable_fields.rb delete mode 100644 spec/factories/votes.rb create mode 100644 spec/models/votable_field_spec.rb 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 0000000000000000000000000000000000000000..9304638ad3b533ca1ea2417d3e013808f61c0de1 GIT binary patch literal 8089 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/cancel-off.png b/app/assets/images/cancel-off.png new file mode 100644 index 0000000000000000000000000000000000000000..a3031f055375716b848c29191cabf6253b936379 GIT binary patch literal 699 zcmV;s0!00ZP)LlQBzMVHAd+8!Cx7n4ko2BpKR*l8~R!U*RTd z-KC3@i{RwscE?Or>7D!)-4qlQMGUEfL=(Km2!<>7e((2nscB0J9em*|hwptkhx2@% zbB?#@9f0R~!$uf}AB-`J&bbvyGuB!pb*#0!lD@62t+ig?I_L15bHjiz3>S+9fx3`mlMX0u7HR>SkWlB6Yo%&%RRecz|k z>G1UQM7!Ol*X!~4`1n@=JUl#5tyYn=JiLOW*=#mTx7)=S!|LklKTpX-R`T3bfqw!C(=Xn$g1v;G$#+deS0%Ocx zv)QCjD3GQpV`F0@W@{|~#bS};<71q2dw;I5Z>_z(xVT_`ejb2KCc`T%)><-|4E1`Q z!^1=3IKGv%5BvscXJ=4|#1P83Dl-```crC2O7H8sW2(GjgyYar=!5CjJ! zC~t3XFGvaizgDY}&*uS%qKK1|6XH0&lk_DBf>D%TwY9bNLDHfzX2m%-BMD01y4qo3(5`U!bUg@nP|I3oZ6002ovPDHLkV1f)gL$Ck< literal 0 HcmV?d00001 diff --git a/app/assets/images/cancel-on.png b/app/assets/images/cancel-on.png new file mode 100644 index 0000000000000000000000000000000000000000..08f249365afd29594b51210c6e21ba253897505d GIT binary patch literal 715 zcmV;+0yO=JP)C4}Mrzlg<+1Y8PEBfUp0jJpx4B>@E+cy3`^(Gw`Mf+2&yxZm<$to~Vpgvg&QKNR z_f#1(r6svZt%iF?s+n<8X?B&!h3g9Dbb8_=MX}!;HiQSAh`bp^WMl~Z-44teO7W_Y zV4thSL{h;rJY7!l3%5J4H1!tIzB`Dv+YxO(haWeausGZYkI8^hWj6mzo=L0{%;yxzh{5!Htr?51 zvG|W62MzC8BZ76hRpCyO2zOn<%e)K>NHge!-~)Ap33OdWw6hsLYbCxGNt0%wk_2z7 zfyYvXheSG)5HRK1VB~%mq7Dmurw#bi@hEcOr3&G1ZiF*$M=&9nB#VNf&Q^r$4G5kp zTURh&s)E0%5&hyVD}sp<72~zmAY`Y(9aqO6CXF%=zFHGzO-A&I(pE}v70YQxCPJ{Y z4L+?5-crdLn3ZRPEs!A4ehEY3ZRpL~w9>@aMN+{F4dI@v&>(QDHQum!mG~E^$OS8l z!7?%Uwib*ROP67Hw`ika)gX-(8Ia`-u_IEhxG7U<13kSsMW+$lbb2dUMm5p6pa}cjgA+U$^mJ^AjD?&bdi)8~y+Q002ovPDHLkV1g8IMc@Dc literal 0 HcmV?d00001 diff --git a/app/assets/images/mid-star.png b/app/assets/images/mid-star.png new file mode 100644 index 0000000000000000000000000000000000000000..aebb3ccb5b6c851512227f11ad01c0c106a441b9 GIT binary patch literal 6335 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 BNg@CM literal 0 HcmV?d00001 diff --git a/app/assets/images/star-half.png b/app/assets/images/star-half.png new file mode 100644 index 0000000000000000000000000000000000000000..3c19e90a8a755e004424db205c8433f0f63048b4 GIT binary patch literal 667 zcmV;M0%ZM(P)5kxfVxVHn4M^Ulocu4}uCxq(JO)+Y9GD5Jat zbqNy5lR|3Ugd)g`R}H!Z-82#+xHAej^Uvb`)a1J$lTNk&_XNAE zBfTQ9v_v8Sa5IxmRs3@SYh=d-l+~gM@B>A4P0dc=P9~jd$fQ$sTUl-3B9(2Y;#Z9F ziv79~Bz4_Q3b9eCYwPF+d^xg&l#+bj;nT_rzOS%t8z2j`7`B7!=X2+jXB|TV=B8-| z5I_(F*tU&nN^1AkB4vn~=}AV%Mjv-|U3#ej40jVdS&P?6I*kPtxhy74l2*jJNmZ458vW9PWLT-hGu|=LysO@uBuA(2q79M zrYCoMu8-X7>n;ia!^1;Yo0`tX3%(WvK`H*P`T^?|->M&U?OXr=002ovPDHLkV1gTF BCtCmj literal 0 HcmV?d00001 diff --git a/app/assets/images/star-off.png b/app/assets/images/star-off.png new file mode 100644 index 0000000000000000000000000000000000000000..956fa7c637cddb4db6a091556cc63a0f6a186264 GIT binary patch literal 685 zcmV;e0#f~nP)R5*>Dl0QrnVHn1Ly)Enp;##exZh7()Uk zR1!C3adKe*!lVgta&se2n3x!5BLe|s!Nt*~X-#y7B3eoUik5~%+lD(Tz0ZL^0YjVU zv%Pu#yl=keg9gZ?Q_Q(|Z0g_5!WIoz~V%6X6SCq;x5j%6#A6o&%l(m4W2o+DSkrok{@j zfS9fu_%t7u6-EyR*{?cCDbX|y$Ei}>*}-*d*tQMu1GuLQBnNZ({Kj-B6r%H5Cv9zQ z1OftQ-v&TRiEZ1cs+abw?R0hbV45b`)$HqmfpE80>NxE=6Ci}>F?0hV0tABwPH{}X=zm$k-d TM0>cp00000NkvXXu0mjfLk}+u0K@`S+@BV*_sWrqlwFo8}(kQkaqzD;A z=u{lE-2?}_II1YR)S)U)E*26HCl}q?MW=Ld6)l=U=ui@ZMFoe7jcsaT?tgaXnG9{0ZQ-FLi$F8Gk$pICVsN_6A+-I}LP#xf!zP`KEQ*A|86(eYX?Vrxqg`*m?> zyc17#YQ&Emm{YDS9%UIE2;KXq0p(YRuVBj4qCoke?8ek~ZfK?*SF$toUz&(q^LzVF zc)m+{o=z$CtKX9fo)4nH88Gx=Z0LtIJw+2pF{o}Aa&I@D_|-bv)_zWzpnQfNU|%D| z2ONlib6`yct1JCRlQx>P4J2Q!lkr}!P)`rD5RWn;!ch^BrauSPHXPUh?BDz@Ut;od zmQ4>}ahc!a2^JEAoD+yQ#m?=B%^vZad=f}S8X&Tqm1m)@;e`m!IcA(C>>M0F#2E0tt%Gn#jhcX3%^aLBZN-xDTU?LbBKJ1?Kz|Loh!06cE>#5u?R;17H*+`wdH ROO^lt002ovPDHLkV1mP{6ITEL literal 0 HcmV?d00001 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