diff --git a/app/assets/stylesheets/frab.css b/app/assets/stylesheets/frab.css index 2c875bca6..b8a7115c4 100644 --- a/app/assets/stylesheets/frab.css +++ b/app/assets/stylesheets/frab.css @@ -60,6 +60,8 @@ div.rating { white-space: nowrap; } +label.review-scores { width: auto; min-width: 40px; } + table.room { width: 150px; margin: 10px; diff --git a/app/controllers/conferences_controller.rb b/app/controllers/conferences_controller.rb index c74992d70..6fdb30fb4 100644 --- a/app/controllers/conferences_controller.rb +++ b/app/controllers/conferences_controller.rb @@ -105,6 +105,13 @@ def edit_classifiers end end + def edit_review_metrics + authorize @conference, :orga? + respond_to do |format| + format.html + end + end + def send_notification authorize @conference, :orga? SendBulkTicketJob.new.async.perform @conference, params[:notification] @@ -172,7 +179,7 @@ def get_previous_nested_form(parameters) next unless attribs.positive? test = name.gsub('_attributes', '') - next unless %w(rooms days schedule notifications tracks classifiers ticket_server).include?(test) + next unless %w(rooms days schedule notifications tracks review_metrics classifiers ticket_server).include?(test) return "edit_#{test}" } 'edit' @@ -228,7 +235,8 @@ def existing_conference_params if @conference.main_conference? || policy(@conference.parent).manage? allowed += [ - classifiers_attributes: %i(name description _destroy id), + classifiers_attributes: %i(name description _destroy id), + review_metrics_attributes: %i(name description _destroy id), rooms_attributes: %i(name size public rank _destroy id), tracks_attributes: %i(name color _destroy id) ] diff --git a/app/controllers/event_ratings_controller.rb b/app/controllers/event_ratings_controller.rb index 32095f1b2..bd637b311 100644 --- a/app/controllers/event_ratings_controller.rb +++ b/app/controllers/event_ratings_controller.rb @@ -4,6 +4,12 @@ class EventRatingsController < BaseConferenceController def show @rating = @event.event_ratings.find_by(person_id: current_user.person.id) || EventRating.new + + # Add any review_metrics missing from @rating + missing_ids = @conference.review_metrics.pluck(:id) - (@rating.review_scores.pluck(:review_metric_id)) + + @rating.review_scores_attributes = missing_ids.map{ |rmid| { review_metric_id: rmid, score: 0} } + setup_batch_reviews_next_event end @@ -67,6 +73,6 @@ def find_event end def event_rating_params - params.require(:event_rating).permit(:rating, :comment, :text) + params.require(:event_rating).permit(:rating, :comment, :text, review_scores_attributes: [:score, :review_metric_id, :id]) end end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 7e38b461d..2bba339f0 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -84,7 +84,7 @@ def attachments def ratings authorize @conference, :read? - result = search @conference.events + result = search @conference.events_with_review_averages @events = result.paginate page: page_param clean_events_attributes @@ -94,7 +94,7 @@ def ratings @events_no_review_total = @events_total - @events_reviewed_total # current_user rated: - @events_reviewed = @conference.events.joins(:event_ratings).where('event_ratings.person_id' => current_user.person.id).count + @events_reviewed = @conference.events.joins(:event_ratings).where('event_ratings.person_id' => current_user.person.id).where.not('event_ratings.rating' => [nil, 0]).count @events_no_review = @events_total - @events_reviewed end diff --git a/app/models/average_review_score.rb b/app/models/average_review_score.rb new file mode 100644 index 000000000..e231d4f1f --- /dev/null +++ b/app/models/average_review_score.rb @@ -0,0 +1,8 @@ +class AverageReviewScore < ApplicationRecord + belongs_to :event + belongs_to :review_metric + has_one :conference, through: :event + + validates :review_metric, presence: true, uniqueness: { scope: :event } + validates :event, presence: true +end diff --git a/app/models/conference.rb b/app/models/conference.rb index 8cff9ada5..602d9fe28 100644 --- a/app/models/conference.rb +++ b/app/models/conference.rb @@ -5,6 +5,7 @@ class Conference < ApplicationRecord has_many :availabilities, dependent: :destroy has_many :classifiers, dependent: :destroy + has_many :review_metrics, dependent: :destroy has_many :conference_users, dependent: :destroy has_many :days, dependent: :destroy has_many :events, dependent: :destroy @@ -21,6 +22,7 @@ class Conference < ApplicationRecord accepts_nested_attributes_for :rooms, reject_if: proc { |r| r['name'].blank? }, allow_destroy: true accepts_nested_attributes_for :classifiers, reject_if: proc { |r| r['name'].blank? }, allow_destroy: true + accepts_nested_attributes_for :review_metrics, reject_if: proc { |r| r['name'].blank? }, allow_destroy: true accepts_nested_attributes_for :days, reject_if: :all_blank, allow_destroy: true accepts_nested_attributes_for :notifications, allow_destroy: true accepts_nested_attributes_for :tracks, reject_if: :all_blank, allow_destroy: true @@ -210,6 +212,10 @@ def schedule_events .scheduled end + def events_with_review_averages + events.with_review_averages(self) + end + def to_s "#{model_name.human}: #{title} (#{acronym})" end diff --git a/app/models/event.rb b/app/models/event.rb index b7528d7a7..c7c1b5c85 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -14,6 +14,8 @@ class Event < ApplicationRecord has_many :event_feedbacks, dependent: :destroy has_many :event_people, dependent: :destroy has_many :event_ratings, dependent: :destroy + has_many :review_scores, through: :event_ratings + has_many :average_review_scores, dependent: :destroy has_many :event_classifiers, dependent: :destroy has_many :links, as: :linkable, dependent: :destroy has_many :people, through: :event_people @@ -31,6 +33,8 @@ class Event < ApplicationRecord accepts_nested_attributes_for :event_attachments, allow_destroy: true, reject_if: :all_blank accepts_nested_attributes_for :ticket, allow_destroy: true, reject_if: :all_blank accepts_nested_attributes_for :event_classifiers, allow_destroy: true + accepts_nested_attributes_for :event_ratings, allow_destroy: true + accepts_nested_attributes_for :average_review_scores, allow_destroy: true validates_attachment_content_type :logo, content_type: [/jpg/, /jpeg/, /png/, /gif/] @@ -48,16 +52,36 @@ class Event < ApplicationRecord scope :with_speaker, -> { where('speaker_count > 0') } scope :with_more_than_one_speaker, -> { where('speaker_count > 1') } + scope :with_review_averages, ->(conference) { + e = select(column_names, conference.review_metrics.map{|rm| "#{rm.safe_name}.score AS #{rm.safe_name}"}) + conference.review_metrics.each do |rm| + e = e.joins("LEFT OUTER JOIN average_review_scores #{rm.safe_name} ON #{rm.safe_name}.event_id=events.id AND #{rm.safe_name}.review_metric_id=#{rm.id}") + end + e + } + + ReviewMetric.all.each do |rm| + ransacker rm.safe_name do + Arel.sql(rm.safe_name) + end + end + has_paper_trail has_secure_token :invite_token def self.ids_by_least_reviewed(conference, reviewer) - already_reviewed = connection.select_rows("SELECT events.id FROM events JOIN event_ratings ON events.id = event_ratings.event_id WHERE events.conference_id = #{conference.id} AND event_ratings.person_id = #{reviewer.id}").flatten.map(&:to_i) - least_reviewed = connection.select_rows("SELECT events.id FROM events LEFT OUTER JOIN event_ratings ON events.id = event_ratings.event_id WHERE events.conference_id = #{conference.id} GROUP BY events.id ORDER BY COUNT(event_ratings.id) ASC, events.id ASC").flatten.map(&:to_i) + already_reviewed = connection.select_rows("SELECT events.id + FROM events + JOIN event_ratings ON events.id = event_ratings.event_id + WHERE events.conference_id = #{conference.id} + AND event_ratings.person_id = #{reviewer.id} + AND event_ratings.rating IS NOT NULL + AND event_ratings.rating <> 0").flatten.map(&:to_i) + least_reviewed = conference.events.order(event_ratings_count: :asc).pluck(:id) least_reviewed -= already_reviewed least_reviewed end - + def localized_event_type(locale = nil) return '' unless event_type.present? I18n.t(event_type, scope: 'options', locale: locale, default: event_type) @@ -70,7 +94,7 @@ def track_name def end_time start_time.since((time_slots * conference.timeslot_duration).minutes) end - + def people_involved_or_reviewing ids = event_ratings.pluck(:person_id) + event_people.pluck(:person_id) Person.where(id: ids.uniq) @@ -94,7 +118,14 @@ def recalculate_average_feedback! end def recalculate_average_rating! - update_attributes(average_rating: average(:event_ratings)) + update_attributes(average_rating: average_of_nonzeros(event_ratings.pluck(:rating)), event_ratings_count: event_ratings.where.not(rating: [nil, 0]).count ) + end + + def recalculate_review_averages! + conference.review_metrics.each do |review_metric| + avg = average_of_nonzeros(review_scores.where(review_metric: review_metric).pluck(:score)) + average_review_scores.find_or_create_by(review_metric: review_metric).update_attributes(score: avg) + end end def speakers @@ -178,4 +209,11 @@ def average(rating_type) return nil if rating_count.zero? result.to_f / rating_count end + + def average_of_nonzeros(list) + return nil unless list + list=list.select{ |x| x && x>0 } + return nil if list.empty? + list.reduce(:+).to_f / list.size + end end diff --git a/app/models/event_rating.rb b/app/models/event_rating.rb index 835085fd1..d46f61e20 100644 --- a/app/models/event_rating.rb +++ b/app/models/event_rating.rb @@ -1,15 +1,16 @@ class EventRating < ApplicationRecord - belongs_to :event, counter_cache: true + belongs_to :event has_one :conference, through: :event + has_many :review_scores, dependent: :destroy belongs_to :person after_save :update_average after_destroy :update_average - validates :rating, presence: true - validates :event, presence: true validates :person, presence: true + + accepts_nested_attributes_for :review_scores, allow_destroy: true protected diff --git a/app/models/review_metric.rb b/app/models/review_metric.rb new file mode 100644 index 000000000..6eab9fed7 --- /dev/null +++ b/app/models/review_metric.rb @@ -0,0 +1,19 @@ +class ReviewMetric < ApplicationRecord + belongs_to :conference + has_many :review_score, dependent: :destroy + has_many :average_review_score, dependent: :destroy + + has_paper_trail meta: { associated_id: :conference_id, associated_type: 'Conference' } + + def to_s + "#{model_name.human}: #{name}" + end + + def safe_name + # safe_name is used as an sql term, and also as a request parameter. + # So we try to have it similiar to the review metric name. + name.parameterize.gsub(%r{[^a-z0-9]}, '_').presence || "rm#{id}" + end + + validates :name, presence: true, uniqueness: { scope: :conference } +end diff --git a/app/models/review_score.rb b/app/models/review_score.rb new file mode 100644 index 000000000..3c494a8dc --- /dev/null +++ b/app/models/review_score.rb @@ -0,0 +1,19 @@ +class ReviewScore < ApplicationRecord + belongs_to :event_rating + belongs_to :review_metric + has_one :event, through: :event_rating + has_one :conference, through: :event_rating + + after_save :update_average + after_destroy :update_average + + validates :score, inclusion: 0..5 # 0 is N/A + validates :review_metric, presence: true, uniqueness: { scope: :event_rating } + validates :event_rating, presence: true + + protected + + def update_average + event_rating.event.recalculate_review_averages! + end +end \ No newline at end of file diff --git a/app/views/conferences/_form_review_metrics.html.haml b/app/views/conferences/_form_review_metrics.html.haml new file mode 100644 index 000000000..5d74f8c13 --- /dev/null +++ b/app/views/conferences/_form_review_metrics.html.haml @@ -0,0 +1,4 @@ += simple_form_for(@conference, url: conference_path) do |f| + = dynamic_association :review_metrics, t(:review_metrics), f + .actions + = f.button :submit, class: 'primary' diff --git a/app/views/conferences/_review_metric_fields.html.haml b/app/views/conferences/_review_metric_fields.html.haml new file mode 100644 index 000000000..101241f73 --- /dev/null +++ b/app/views/conferences/_review_metric_fields.html.haml @@ -0,0 +1,5 @@ +.nested-fields + %fieldset.inputs + = f.input :name + = f.input :description + = remove_association_link :review_metric, f diff --git a/app/views/conferences/_tabs.html.haml b/app/views/conferences/_tabs.html.haml index 0dea85a70..b1dfe62fc 100644 --- a/app/views/conferences/_tabs.html.haml +++ b/app/views/conferences/_tabs.html.haml @@ -14,6 +14,8 @@ = link_to t(:ticket_server), edit_ticket_server_conference_path - conference_tab(:classifiers, active) do = link_to t(:classifiers), edit_classifiers_conference_path + - conference_tab(:reviewing, active) do + = link_to t(:reviewing), edit_review_metrics_conference_path - if @conference.ticket_type == 'integrated' or @conference.bulk_notification_enabled - conference_tab(:notifications, active) do = link_to t('notifications'), edit_notifications_conference_path diff --git a/app/views/conferences/edit_review_metrics.html.haml b/app/views/conferences/edit_review_metrics.html.haml new file mode 100644 index 000000000..f86fce7ce --- /dev/null +++ b/app/views/conferences/edit_review_metrics.html.haml @@ -0,0 +1,26 @@ +%section + .page-header + %h1= t :edit_conference_review_metrics + = render partial: 'tabs', locals: { active: :reviewing } + + - if @conference.sub_conference? && !policy(@conference).manage? + .row + .span16 + .blank-slate + %p= raw(GitHub.render(t('reviewing_module.modify_review_metric_with_parent', {parent: @conference.parent.title}))) + %p= t('reviewing_module.current_review_metrics') + + %uls + - @conference.review_metrics.each do |review_metric| + %li + ="#{review_metric.name} (description: #{review_metric.description})" + + - else + - if @conference.review_metrics.empty? + .row + .span16 + .blank-slate + %p= t('reviewing_module.empty_review_metrics') + .row + .span16 + = render 'form_review_metrics' diff --git a/app/views/event_ratings/show.html.haml b/app/views/event_ratings/show.html.haml index 92913e3ba..24854240d 100644 --- a/app/views/event_ratings/show.html.haml +++ b/app/views/event_ratings/show.html.haml @@ -75,6 +75,17 @@ %fieldset.inputs = f.input :rating, as: :rating = f.input :comment, input_html: {rows: 3} + = f.simple_fields_for :review_scores do |ff| + = ff.input :score, + as: :radio_buttons, + collection: [[t('events_module.not_applicable'), '0'], ['1', '1'], ['2', '2'], ['3', '3'], ['4', '4'], ['5', '5']], + label: ff.object.review_metric.name, + hint: ff.object.review_metric.description, + item_label_class: 'review-scores' + + = ff.input :review_metric_id, as: :hidden + = ff.input :id, as: :hidden + .actions = f.button :submit, class: 'primary' - if @rating.persisted? @@ -98,6 +109,8 @@ %th %th= t('user') %th= t('rating') + - @conference.review_metrics.each do |review_metric| + %th=review_metric.name %th= t('comment') %tbody - @event_ratings.each do |event_rating| @@ -108,4 +121,7 @@ %td=event_rating.person.full_name %td = raty_for("event_rating_#{event_rating.id}", event_rating.rating) + - @conference.review_metrics.each do |review_metric| + - score=event_rating.review_scores.find_by(review_metric: review_metric)&.score + %td=score if score and score>0 %td=event_rating.comment diff --git a/app/views/events/ratings.html.haml b/app/views/events/ratings.html.haml index 6fbd4fb5d..b46cc09ee 100644 --- a/app/views/events/ratings.html.haml +++ b/app/views/events/ratings.html.haml @@ -1,71 +1,78 @@ -%section - .page-header - .pull-right - = action_button "primary", t('ratings_module.start_reviewing'), start_review_events_path, hint: t('ratings_module.start_reviewing_hint'), disabled: @events_no_review == 0 - %h1= t('titles.event_ratings') - %ul.tabs - %li= link_to t('events_module.all_events'), events_path - %li= link_to t('events_module.my_events'), my_events_path - %li= link_to t('events_module.attachments_overview'), attachments_events_path - %li.active= link_to t('events_module.event_ratings'), ratings_events_path - - if @conference.feedback_enabled - %li= link_to t('events_module.event_feedbacks'), feedbacks_events_path - - if not @conference.events.any? - .row - .span16 - .blank-slate - %p= t('ratings_module.no_event') - - else - = render partial: 'filters', locals: { params: params } - .row - .span16 - %h2= t('ratings_module.statistics') - %p - %b= t('ratings_module.total_number_of_events') - = @events_total - %br/ - %b= t('ratings_module.total_number_of_events_no_review') - = @events_no_review_total - %br/ - %b= t('ratings_module.total_number_of_reviewed') - = @events_reviewed - %br/ - %b= t('ratings_module.total_number_of_not_reviewed') - = @events_no_review - .row - .span16 - - if @events.empty? - %p= t('reports_module.no_search_results') - - else - %table.zebra-striped - %thead - %tr - %th - %th= sort_link @search, :title, term: params[:term] - %th= sort_link @search, :track_name, t('track'), term: params[:term] - %th= sort_link @search, :event_type, term: params[:term] - - if policy(@conference).manage? - %th= sort_link @search, :state, term: params[:term] - %th= sort_link @search, :average_rating, term: params[:term] - %th= sort_link @search, :event_ratings_count, term: params[:term] - %th - %tbody - - @events.includes(:track).each do |event| - %tr - %td= image_box event.logo, :small - %td - = link_to event.title, event - %p.small - = by_speakers(event) - %td= link_to_unless (params[:track_name].present? or event.track.nil?), event.track.try(:name), request.query_parameters.merge(:track_name => event.track.try(:name)) - %td= link_to_unless params[:event_type].present?, event.event_type, request.query_parameters.merge(:event_type => event.event_type) - - if policy(@conference).manage? - %td= link_to_unless params[:event_state].present?, event.state, request.query_parameters.merge(:event_state => event.state) - %td - - if event.average_rating - = raty_for("event_rating_#{event.id}", event.average_rating) - %td= event.event_ratings_count - %td - = link_to t('ratings_module.show_ratings'), event_event_rating_path(event), class: "btn small" - = actions_bar do - = will_paginate @events +%section + .page-header + .pull-right + = action_button "primary", t('ratings_module.start_reviewing'), start_review_events_path, hint: t('ratings_module.start_reviewing_hint'), disabled: @events_no_review == 0 + %h1= t('titles.event_ratings') + %ul.tabs + %li= link_to t('events_module.all_events'), events_path + %li= link_to t('events_module.my_events'), my_events_path + %li= link_to t('events_module.attachments_overview'), attachments_events_path + %li.active= link_to t('events_module.event_ratings'), ratings_events_path + - if @conference.feedback_enabled + %li= link_to t('events_module.event_feedbacks'), feedbacks_events_path + - if not @conference.events.any? + .row + .span16 + .blank-slate + %p= t('ratings_module.no_event') + - else + = render partial: 'filters', locals: { params: params } + .row + .span16 + %h2= t('ratings_module.statistics') + %p + %b= t('ratings_module.total_number_of_events') + = @events_total + %br/ + %b= t('ratings_module.total_number_of_events_no_review') + = @events_no_review_total + %br/ + %b= t('ratings_module.total_number_of_reviewed') + = @events_reviewed + %br/ + %b= t('ratings_module.total_number_of_not_reviewed') + = @events_no_review + .row + .span16 + - if @events.empty? + %p= t('reports_module.no_search_results') + - else + - review_metrics = @conference.review_metrics.all + %table.zebra-striped + %thead + %tr + %th + %th= sort_link @search, :title, term: params[:term] + %th= sort_link @search, :track_name, t('track'), term: params[:term] + %th= sort_link @search, :event_type, term: params[:term] + - if policy(@conference).manage? + %th= sort_link @search, :state, term: params[:term] + %th= sort_link @search, :average_rating, term: params[:term] + %th= sort_link @search, :event_ratings_count, term: params[:term] + - review_metrics.each do |review_metric| + %th= sort_link @search, review_metric.safe_name, review_metric.name, term: params[:term] + %th + %tbody + - @events.includes(:track).each do |event| + %tr + %td= image_box event.logo, :small + %td + = link_to event.title, event + %p.small + = by_speakers(event) + %td= link_to_unless (params[:track_name].present? or event.track.nil?), event.track.try(:name), request.query_parameters.merge(:track_name => event.track.try(:name)) + %td= link_to_unless params[:event_type].present?, event.event_type, request.query_parameters.merge(:event_type => event.event_type) + - if policy(@conference).manage? + %td= link_to_unless params[:event_state].present?, event.state, request.query_parameters.merge(:event_state => event.state) + %td + - if event.average_rating + = raty_for("event_rating_#{event.id}", event.average_rating) + %td= event.event_ratings_count + - review_metrics.each do |review_metric| + - avg = event[review_metric.safe_name] + %td= avg.round(2) unless avg.nil? + %td + = link_to t('ratings_module.show_ratings'), event_event_rating_path(event), class: "btn small" + = actions_bar do + = will_paginate @events + diff --git a/config/locales/de.yml b/config/locales/de.yml index e10d83ca8..598e8af85 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -202,6 +202,7 @@ de: notification: Benachrichtigung person: Profil phone_number: Telefonnummer + review_metric: Metrik überprüfen room: Raum track: Track add_association: "%{name} hinzufügen" @@ -1033,6 +1034,7 @@ de: no_ratings: | Bisher hat noch niemand eine Bewertung für dieses Event abgegeben. Seien Sie der erste und bewerten Sie das Event mit dem obigen Formular. + not_applicable: N/A not_involved: | Sie sind für diese Konferenz in keinem Event involviert. Wenn Sie ein Event einreichen oder einem beitreten, taucht er in dieser Liste auf. @@ -1996,6 +1998,16 @@ de: speakers_without_availabilities: Sprecher ohne Verfügbarkeit unconfirmed_events: Unbestätigte Events resend: Erneut senden + reviewing: Überprüfung + reviewing_module: + current_review_metrics: 'Die derzeit konfigurierten Überprüfungsmetriken sind:' + empty_review_metrics: | + Sie können Prüfer bitten, sich auf bestimmte Themen zu konzentrieren + Aspekte jeder Einreichung während der Überprüfung durch Hinzufügen + Überprüfen Sie die Messdaten in dieser Liste. + modify_review_metric_with_parent: | + Überprüfungsmetriken für diese Konferenz können nur mit geändert werden + Administrationsrechte für die Elternkonferenz %{parent} role: admin: Admin assistant: Assistent diff --git a/config/locales/en.yml b/config/locales/en.yml index 897f5920c..e8867c602 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -175,6 +175,7 @@ en: person: profile phone_number: phone number room: room + review_metric: Review metric track: track add_association: Add %{name} additional_resources: Additional Resources @@ -1024,6 +1025,7 @@ en: no_ratings: | No one has entered a rating for this event yet. Be the first and rate the event using the form above. + not_applicable: N/A not_involved: | You are not yet involved in any event in this conference. Add yourself to an event in whatever @@ -1974,6 +1976,16 @@ en: speakers_without_availabilities: speakers without availabilities unconfirmed_events: events that are unconfirmed resend: Resend + reviewing: Reviewing + reviewing_module: + current_review_metrics: 'The currently configured review metrics are:' + empty_review_metrics: | + You can ask submission reviewers to focus on specific + aspects of each submission while reviewing, by adding + review metrics to this list. + modify_review_metric_with_parent: | + Review metrics for this conference can only be modified with + administration rights for the parent conference %{parent} role: admin: Admin assistant: Assistant diff --git a/config/locales/es.yml b/config/locales/es.yml index 5eb3738ee..78745ec84 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -218,6 +218,7 @@ es: notification: Notificación person: perfil phone_number: número telefónico + review_metric: Revisar métrica room: sala track: área add_association: Agregar %{name} @@ -1072,6 +1073,7 @@ es: no_ratings: | Nadie ha ingresado una calificación para este evento todavía. Sé el primero y califica el evento usando el formulario de arriba. + not_applicable: N/A not_involved: | Todavía no estás involucrado en ningún evento en este conferencia. Sumérgete en un evento en lo que sea @@ -2030,6 +2032,16 @@ es: speakers_without_availabilities: altavoces sin disponibilidad unconfirmed_events: eventos que no están confirmados resend: Reenviar + reviewing: Revisando + reviewing_module: + current_review_metrics: 'Las métricas de revisión configuradas actualmente son:' + empty_review_metrics: | + Puede solicitar a los revisores de envío que se centren en + aspectos de cada presentación durante la revisión, agregando + revise las métricas de esta lista. + modify_review_metric_with_parent: | + Las métricas de revisión para esta conferencia solo se pueden modificar con + derechos de administración para la conferencia de padres %{parent} role: admin: Administración assistant: Asistente diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9ae942984..71fe6cef7 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -174,6 +174,7 @@ fr: notification: Notification person: profil phone_number: numéro de téléphone + review_metric: Critique métrique room: salle track: fil add_association: Ajouter %{name} @@ -1052,6 +1053,7 @@ fr: no_ratings: | Personne n’a noté cet évènement pour le moment. Soyez le premier en utilisant le formulaire ci-dessous. + not_applicable: N/A not_involved: | Vous n’êtes pas encore impliqué dans un évènement pour cette conférence. Ajoutez-vous à un évènement pour le rôle que vous souhaitez tenir, et @@ -2005,6 +2007,16 @@ fr: speakers_without_availabilities: orateurs sans disponibilités unconfirmed_events: événements non confirmés resend: Renvoyer + reviewing: Révision + reviewing_module: + current_review_metrics: 'Les métriques de révision actuellement configurées sont les suivantes:' + empty_review_metrics: | + Vous pouvez demander aux examinateurs de soumission de se concentrer sur des + aspects de chaque soumission tout en examinant, en ajoutant + examiner les métriques de cette liste. + modify_review_metric_with_parent: | + Les métriques d'évaluation de cette conférence ne peuvent être modifiées qu'avec + droits d'administration pour la conférence parente %{parent} role: admin: Administrateur assistant: Assistant diff --git a/config/locales/it.yml b/config/locales/it.yml index eb5bc9614..2aee299ac 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -174,6 +174,7 @@ it: notification: notifica person: profilo phone_number: numero di telefono + review_metric: Revisionare la metrica room: sala track: filo add_association: Aggiungi %{name} @@ -1056,6 +1057,7 @@ it: no_ratings: | Nessuno ha ancora valutato questo evento. Sii il primo a utilizzando il modulo sottostante. + not_applicable: N/A not_involved: | Non sei ancora coinvolto in un evento per questa conferenza. Aggiungi te stesso a un evento per il ruolo che vuoi tenere, e @@ -2009,6 +2011,16 @@ it: speakers_without_availabilities: altoparlanti senza disponibilità unconfirmed_events: eventi non confermati resend: ritorno + reviewing: Revisione + reviewing_module: + current_review_metrics: 'Le metriche di revisione attualmente configurate sono:' + empty_review_metrics: | + Puoi chiedere ai revisori delle presentazioni di concentrarsi su specifici + aspetti di ogni invio durante la revisione, aggiungendo + rivedere le metriche in questo elenco. + modify_review_metric_with_parent: | + Le metriche di revisione per questa conferenza possono essere modificate solo con + diritti di amministrazione per la conferenza principale %{parent} role: admin: amministratore assistant: Assistente diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index d5e92d7a0..79b601c44 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -196,6 +196,7 @@ pt-BR: notification: Notificação person: Perfil phone_number: Número de telefone + review_metric: Métrica de revisão room: Sala track: Trilha add_association: adicionar %{name} @@ -1041,6 +1042,7 @@ pt-BR: no_ratings: | Ninguém inseriu uma classificação para este evento ainda. Seja o primeiro e avalie o evento usando o formulário acima. + not_applicable: N/A not_involved: | Você ainda não está envolvido em nenhum evento neste conferência. Adicione-se a um evento em qualquer @@ -2015,6 +2017,16 @@ pt-BR: speakers_without_availabilities: alto-falantes sem disponibilidade unconfirmed_events: eventos que não são confirmados resend: Reenviar + reviewing: Revendo + reviewing_module: + current_review_metrics: 'As métricas de revisão configuradas atualmente são:' + empty_review_metrics: | + Você pode solicitar que os revisores de envios se concentrem em + aspectos de cada envio durante a revisão, adicionando + revise as métricas para esta lista. + modify_review_metric_with_parent: | + As métricas de revisão desta conferência só podem ser modificadas com + direitos de administração da conferência principal %{parent} role: admin: Admin assistant: Assistente diff --git a/config/locales/ru.yml b/config/locales/ru.yml index fe2f2eca5..c54b4e6a9 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -174,6 +174,7 @@ ru: notification: уведомление person: профиль phone_number: номер телефона + review_metric: Обзор метрики room: комната track: трек add_association: Добавить %{name} @@ -1031,6 +1032,7 @@ ru: no_ratings: | Никто еще не ввел рейтинг для этого мероприятия. Будь первым и оцените событие, используя форму выше. + not_applicable: N/A not_involved: | Вы еще не участвовали ни в одном событии в этом конференция. Добавьте себя к событию в любом @@ -1993,6 +1995,16 @@ ru: speakers_without_availabilities: динамики без оговорок unconfirmed_events: события, неподтвержденные resend: Отправить + reviewing: Обзор + reviewing_module: + current_review_metrics: 'В настоящее время настроены показатели обзора:' + empty_review_metrics: | + Вы можете попросить рецензентов, чтобы сосредоточиться на конкретных + аспекты каждого представления при рассмотрении, добавив + просмотреть метрики к этому списку. + modify_review_metric_with_parent: | + Метрики просмотра для этой конференции могут быть изменены только с + Администрирование прав на родительскую конференцию %{parent} role: admin: Администратор assistant: Ассистент diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 8db4dd1dc..2a75a4667 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -174,6 +174,7 @@ zh: notification: 通知 person: 轮廓 phone_number: 电话号码 + review_metric: 审核指标 room: 房间 track: 跟踪 add_association: 添加%{name} @@ -1031,6 +1032,7 @@ zh: no_ratings: | 尚未有人为此活动输入评分。成为第一个 并使用上面的表格对事件进行评分。 + not_applicable: N/A not_involved: | 您尚未参与此活动 会议。把自己添加到任何事件中 @@ -1981,6 +1983,16 @@ zh: speakers_without_availabilities: 没有可用性的扬声器 unconfirmed_events: 未经证实的事件 resend: 重发 + reviewing: 审核中 + reviewing_module: + current_review_metrics: 当前配置的审阅指标是: + empty_review_metrics: | + 您可以要求提交审稿人专注于特定的 + 审核时,通过添加每个提交的方面 + 查看此列表的指标。 + modify_review_metric_with_parent: | + 此会议的审核指标只能通过 + 父会议%{parent}的管理权限 role: admin: 管理员 assistant: 助理 diff --git a/config/routes.rb b/config/routes.rb index 225970cc6..dda1e0c38 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -92,6 +92,7 @@ get :edit_schedule get :edit_rooms get :edit_classifiers + get :edit_review_metrics get :edit_ticket_server get :edit_notifications post :send_notification diff --git a/db/migrate/20191004000001_create_review_metrics.rb b/db/migrate/20191004000001_create_review_metrics.rb new file mode 100644 index 000000000..68f37d7e9 --- /dev/null +++ b/db/migrate/20191004000001_create_review_metrics.rb @@ -0,0 +1,17 @@ +class CreateReviewMetrics < ActiveRecord::Migration[5.2] + def self.up + create_table :review_metrics do |t| + t.string :name + t.string :description + t.references :conference, index: true, foreign_key: true + + t.timestamps + end + + add_index :review_metrics, [:name, :conference_id], unique: true + end + + def self.down + drop_table :review_metrics + end +end diff --git a/db/migrate/20191004000002__create_review_scores.rb b/db/migrate/20191004000002__create_review_scores.rb new file mode 100644 index 000000000..728674175 --- /dev/null +++ b/db/migrate/20191004000002__create_review_scores.rb @@ -0,0 +1,11 @@ +class CreateReviewScores < ActiveRecord::Migration[5.2] + def change + create_table :review_scores do |t| + t.references :event_rating, foreign_key: true + t.references :review_metric, foreign_key: true + t.integer :score + + t.timestamps + end + end +end diff --git a/db/migrate/20191004000003_create_average_review_scores.rb b/db/migrate/20191004000003_create_average_review_scores.rb new file mode 100644 index 000000000..92b22d7b2 --- /dev/null +++ b/db/migrate/20191004000003_create_average_review_scores.rb @@ -0,0 +1,11 @@ +class CreateAverageReviewScores < ActiveRecord::Migration[5.2] + def change + create_table :average_review_scores do |t| + t.references :event, foreign_key: true + t.references :review_metric, foreign_key: true + t.float :score + end + + add_index :average_review_scores , [:event_id, :review_metric_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index a25002bb4..80bf45f51 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_10_02_000000) do +ActiveRecord::Schema.define(version: 2019_10_04_000003) do create_table "availabilities", force: :cascade do |t| t.integer "person_id" @@ -24,6 +24,15 @@ t.index ["person_id"], name: "index_availabilities_on_person_id" end + create_table "average_review_scores", force: :cascade do |t| + t.integer "event_id" + t.integer "review_metric_id" + t.float "score" + t.index ["event_id", "review_metric_id"], name: "index_average_review_scores_on_event_id_and_review_metric_id", unique: true + t.index ["event_id"], name: "index_average_review_scores_on_event_id" + t.index ["review_metric_id"], name: "index_average_review_scores_on_review_metric_id" + end + create_table "call_for_participations", force: :cascade do |t| t.date "start_date", null: false t.date "end_date", null: false @@ -317,6 +326,26 @@ t.index ["person_id"], name: "index_phone_numbers_on_person_id" end + create_table "review_metrics", force: :cascade do |t| + t.string "name" + t.string "description" + t.integer "conference_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["conference_id"], name: "index_review_metrics_on_conference_id" + t.index ["name", "conference_id"], name: "index_review_metrics_on_name_and_conference_id", unique: true + end + + create_table "review_scores", force: :cascade do |t| + t.integer "event_rating_id" + t.integer "review_metric_id" + t.integer "score" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["event_rating_id"], name: "index_review_scores_on_event_rating_id" + t.index ["review_metric_id"], name: "index_review_scores_on_review_metric_id" + end + create_table "rooms", force: :cascade do |t| t.integer "conference_id", null: false t.string "name", limit: 255, null: false diff --git a/test/features/editing_event_review_test.rb b/test/features/editing_event_review_test.rb new file mode 100644 index 000000000..15aba9a1a --- /dev/null +++ b/test/features/editing_event_review_test.rb @@ -0,0 +1,50 @@ +require 'test_helper' + +class EditingEventReviewTest < FeatureTest + REVIEW_METRIC_NAME = 'innovative חדשני' + setup do + @conference = create(:three_day_conference_with_events) + @event = @conference.events.last + + review_metric = ReviewMetric.create(name: REVIEW_METRIC_NAME, conference: @conference) + @conference.review_metrics << review_metric + + # When test start, three people already reviewed it + [2, 4, 5].each do |score| + reviewer=create(:conference_coordinator, conference: @conference) + @event.event_ratings_attributes = [{ person: reviewer.person, review_scores_attributes: [{review_metric: review_metric, score: score}] }] + end + @event.save + + @coordinator = create(:conference_coordinator, conference: @conference) + @user = @coordinator.user + end + + it 'can edit review metrics and delete, calculate average', js:true do + # Test that when @user updates the review score, the average is updated correctly + sign_in_user(@user) + visit "/#{@conference.acronym}/events/#{@event.id}/event_rating" + find('span', exact_text: '4').find('input').click() + click_on 'Create Event rating' + assert_content page, 'saved successfully' + + visit "/#{@conference.acronym}/events/ratings" + assert_content page, REVIEW_METRIC_NAME + assert_content page, '3.75' # average([2,4,4,5]) + + # Test that when @user deletes the review, the average is updated correctly + visit "/#{@conference.acronym}/events/#{@event.id}/event_rating" + click_on 'Delete Event rating' + assert_content page, 'deleted successfully' + + visit "/#{@conference.acronym}/events/ratings" + assert_content page, REVIEW_METRIC_NAME + assert_content page, '3.67' # average([2,4,5]) + + # Test that sorting by review metric doesn't assert + click_on REVIEW_METRIC_NAME + assert_content page, REVIEW_METRIC_NAME + + + end +end