diff --git a/app/assets/stylesheets/osem.css.scss b/app/assets/stylesheets/osem.css.scss index 83a921a6..03c221da 100644 --- a/app/assets/stylesheets/osem.css.scss +++ b/app/assets/stylesheets/osem.css.scss @@ -1,13 +1,13 @@ html { - position: relative; - min-height: 100%; + position: relative; + min-height: 100%; } body { - /* Margin bottom by 2 times the footer height */ - margin-bottom: 60px; - /* Margin bottom by navbar height */ - padding-top: 60px; + /* Margin bottom by 2 times the footer height */ + margin-bottom: 60px; + /* Margin bottom by navbar height */ + padding-top: 60px; } #content { @@ -15,23 +15,23 @@ body { } #footer { - position: absolute; - bottom: 0; - width: 100%; - /* Set the fixed height of the footer here */ - height: 60px; - background-color: #f5f5f5; - .container { - padding: 15px; - } + position: absolute; + bottom: 0; + width: 100%; + /* Set the fixed height of the footer here */ + height: 60px; + background-color: #f5f5f5; + .container { + padding: 15px; + } } .nav-tabs { - margin-bottom: 15px; + margin-bottom: 15px; } .img-center { - margin: 0 auto; + margin: 0 auto; } /* centered columns styles */ @@ -71,4 +71,14 @@ p.comment-body { #account-already { font-size: 0.6em; -} \ No newline at end of file +} + +/* comments views pane: use padding-bottom before next comment box */ +.panel.panel-default .panel-body .notifications { + padding-bottom: 20px; +} + +/* comments views pane: use padding-bottom after each comment */ +.panel.panel-default .panel-body .notifications p { + padding-bottom: 20px; +} diff --git a/app/controllers/admin/comments_controller.rb b/app/controllers/admin/comments_controller.rb new file mode 100644 index 00000000..874439df --- /dev/null +++ b/app/controllers/admin/comments_controller.rb @@ -0,0 +1,28 @@ +module Admin + class CommentsController < Admin::BaseController + load_and_authorize_resource + + def index + # All available comments, grouped and sorted + @comments = grouped_comments(accessible_ordered_comments) + + # Grouped, sorted, available comments, posted since current_user last login + @unread_comments = grouped_comments(accessible_ordered_comments.find_since_last_login(current_user)) + + # Grouped, sorted, available comments, posted by current_user + @posted_comments = grouped_comments(accessible_ordered_comments.find_comments_by_user(current_user)) + end + + private + + # Returning all available comments, ordered by created_at: :desc and by event.title + def accessible_ordered_comments + Comment.accessible_by(current_ability).joins('INNER JOIN events ON commentable_id = events.id').order('events.title', 'comments.created_at DESC') + end + +# Grouping all comments by conference, and by event. It returns {:conference => {:event => [{comment_2}, {comment_1 }]}} + def grouped_comments(remarks) + remarks.group_by{ |comment| comment.commentable.conference }.map {|conference, comments| [conference, comments.group_by{|comment| comment.commentable}]}.to_h + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7e9f7652..4c72b3dc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -278,4 +278,8 @@ module ApplicationHelper new_user_registration_path end end + + def unread_notifications(user) + Comment.accessible_by(current_ability).find_since_last_login(user) + end end diff --git a/app/mailers/mailbot.rb b/app/mailers/mailbot.rb index 9cf89b88..e8118382 100644 --- a/app/mailers/mailbot.rb +++ b/app/mailers/mailbot.rb @@ -80,6 +80,22 @@ class Mailbot < ActionMailer::Base end end + def send_notification_email_for_comment(comment) + @comment = comment + @event = @comment.commentable + @conference = @event.conference + recipients = User.comment_notifiable(@conference) # with scope + recipients.each do |user| + @user = user + mail(to: @user.email, + from: @conference.contact.email, + reply_to: @conference.contact.email, + template_path: 'admin/emails', + template_name: 'comment_template', + subject: "New comment has been posted for #{@event.title}") + end + end + def build_email(conference, to, subject, body) mail(to: to, from: conference.contact.email, diff --git a/app/models/ability.rb b/app/models/ability.rb index 0f89057e..bd30a8fd 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -88,12 +88,13 @@ class Ability # Abilities from not_signed_in and signed_in are also inherited signed_in(user) - signed_in_with_organizer_role(user) - signed_in_with_cfp_role(user) - signed_in_with_info_desk_role(user) - signed_in_with_volunteers_coordinator_role(user) + signed_in_with_organizer_role(user) if user.has_role? :organizer, :any + signed_in_with_cfp_role(user) if user.has_role? :cfp, :any + signed_in_with_info_desk_role(user) if user.has_role? :info_desk, :any + signed_in_with_volunteers_coordinator_role(user) if user.has_role? :volunteer_coordinator, :any # for users with any role + can :access, Admin can [:show], Conference can :index, Commercial, commercialable_type: 'Conference' cannot [:edit, :update, :destroy], Question, global: true @@ -137,6 +138,8 @@ class Ability can :manage, Sponsor, conference_id: conf_ids_for_organizer can :manage, SponsorshipLevel, conference_id: conf_ids_for_organizer can :manage, Ticket, conference_id: conf_ids_for_organizer + can :index, Comment, commentable_type: 'Event', + commentable_id: Event.where(conference_id: conf_ids_for_organizer).pluck(:id) end def signed_in_with_cfp_role(user) @@ -155,6 +158,8 @@ class Ability can :manage, CallForPaper, conference_id: conf_ids_for_cfp can :manage, Commercial, commercialable_type: 'Event', commercialable_id: Event.where(conference_id: conf_ids_for_cfp).pluck(:id) + can :index, Comment, commentable_type: 'Event', + commentable_id: Event.where(conference_id: conf_ids_for_cfp).pluck(:id) end def signed_in_with_info_desk_role(user) diff --git a/app/models/comment.rb b/app/models/comment.rb index 6196856e..f668afb3 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -3,6 +3,7 @@ class Comment < ActiveRecord::Base attr_accessible :commentable, :body, :user_id validates_presence_of :body validates_presence_of :user + after_create :send_notification # NOTE: install the acts_as_votable plugin if you # want user to vote on the quality of comments. @@ -40,9 +41,18 @@ class Comment < ActiveRecord::Base where(commentable_type: commentable_str.to_s, commentable_id: commentable_id).order('created_at DESC') } + scope :find_since_last_login, lambda { |user| + where(created_at: (user.last_sign_in_at..Time.now)).order(created_at: :desc) + } # Helper class method to look up a commentable object # given the commentable class name and id def self.find_commentable(commentable_str, commentable_id) commentable_str.constantize.find(commentable_id) end + + private + + def send_notification + Mailbot.delay.send_notification_email_for_comment(self) + end end diff --git a/app/models/email_settings.rb b/app/models/email_settings.rb index 11a7b628..5642b4da 100644 --- a/app/models/email_settings.rb +++ b/app/models/email_settings.rb @@ -49,7 +49,7 @@ class EmailSettings < ActiveRecord::Base h['registration_end_date'] = conference.registration_period.end_date end - if !event.nil? + if event h['eventtitle'] = event.title h['proposalslink'] = Rails.application.routes.url_helpers.conference_proposal_index_url( conference.short_title, host: CONFIG['url_for_emails']) @@ -67,6 +67,8 @@ class EmailSettings < ActiveRecord::Base parse_template(conf_update_template, values) end + private + def parse_template(text, values) values.each do |key, value| if value.kind_of?(Date) diff --git a/app/models/user.rb b/app/models/user.rb index 6214d07a..568a1c89 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,6 +11,9 @@ class User < ActiveRecord::Base before_create :setup_role + # add scope + scope :comment_notifiable, ->(conference) {joins(:roles).where('roles.name IN (?)', [:organizer, :cfp]).where('roles.resource_type = ? AND roles.resource_id = ?', 'Conference', conference.id)} + # Include default devise modules. Others available are: # :token_authenticatable, :confirmable, # :lockable, :timeoutable and :omniauthable diff --git a/app/views/admin/comments/_all_comments.html.haml b/app/views/admin/comments/_all_comments.html.haml new file mode 100644 index 00000000..02ad15e8 --- /dev/null +++ b/app/views/admin/comments/_all_comments.html.haml @@ -0,0 +1,12 @@ +- @comments.each do |conference, events| + .panel.panel-default + .panel-heading + %h4.title.panel-title= conference.title + .panel-body + - events.each do |event, comments| + .notifications + %h4.title= link_to event.title, admin_conference_event_path(event.conference.short_title, event) + %hr + - comments.each do |comment| + %h5.strong Posted by: #{comment.user.name} | Created at: #{comment.created_at} + %p= comment.body diff --git a/app/views/admin/comments/_posted_comments.html.haml b/app/views/admin/comments/_posted_comments.html.haml new file mode 100644 index 00000000..89c90519 --- /dev/null +++ b/app/views/admin/comments/_posted_comments.html.haml @@ -0,0 +1,12 @@ +- @posted_comments.each do |conference, events| + .panel.panel-default + .panel-heading + %h4.title.panel-title= conference.title + .panel-body + - events.each do |event, comments| + .notifications + %h4.title= link_to event.title, admin_conference_event_path(event.conference.short_title, event) + %hr + - comments.each do |comment| + %h5.strong Created at: #{comment.created_at} + %p= comment.body diff --git a/app/views/admin/comments/_unread_comments.html.haml b/app/views/admin/comments/_unread_comments.html.haml new file mode 100644 index 00000000..9291aa83 --- /dev/null +++ b/app/views/admin/comments/_unread_comments.html.haml @@ -0,0 +1,12 @@ +- @unread_comments.each do |conference, events| + .panel.panel-default + .panel-heading + %h4.title.panel-title= conference.title + .panel-body + - events.each do |event, comments| + .notifications + %h4.title= link_to event.title, admin_conference_event_path(event.conference.short_title, event) + %hr + - comments.each do |comment| + %h5.strong Posted by: #{comment.user.name} | Created at: #{comment.created_at} + %p= comment.body diff --git a/app/views/admin/comments/index.html.haml b/app/views/admin/comments/index.html.haml new file mode 100644 index 00000000..9a141c85 --- /dev/null +++ b/app/views/admin/comments/index.html.haml @@ -0,0 +1,24 @@ +%h1 Comments +%br +.row + .col-md-12 + %ul.nav.nav-tabs#commentsTable + %li.active + %a{:href=>"#unread_comments", "data-toggle"=>"tab"} + %span.fa.fa-comment + Unread + %li + %a{:href=>"#posted_comments", "data-toggle"=>"tab"} + %span.fa.fa-pencil + Posted + %li + %a{:href=>"#all_comments", "data-toggle"=>"tab"} + %span.fa.fa-comments-o + All + .tab-content + .tab-pane.active#unread_comments + = render partial: 'unread_comments' + .tab-pane#posted_comments + = render partial: 'posted_comments' + .tab-pane#all_comments + = render partial: 'all_comments' diff --git a/app/views/admin/emails/comment_template.text.erb b/app/views/admin/emails/comment_template.text.erb new file mode 100644 index 00000000..0ebd8ad4 --- /dev/null +++ b/app/views/admin/emails/comment_template.text.erb @@ -0,0 +1,10 @@ +Dear <%= @user.name %>, + +User <%= @comment.user.name %> posted a new comment for event <%= @event.title %> of <%= @conference.short_title %> . + +"<%= @comment.body %>" + +To reply to this comment, please go to <%= h( admin_conference_event_url(@conference.short_title, @event)) %> + +Best wishes, +<%= @conference.short_title %> Team diff --git a/app/views/layouts/_navigation.html.haml b/app/views/layouts/_navigation.html.haml index ea837d2c..cf2b3b44 100644 --- a/app/views/layouts/_navigation.html.haml +++ b/app/views/layouts/_navigation.html.haml @@ -13,17 +13,34 @@ %ul.nav.navbar-nav#splash-nav = content_for :splash_nav -if user_signed_in? - %ul.nav.navbar-nav.navbar-right - %li.dropdown - %a.dropdown-toggle{"data-toggle" => "dropdown", :href => "#", id: "current-user-detail"} - - if not current_user.name.blank? - #{current_user.name} - -else - #{current_user.email} - = image_tag(current_user.gravatar_url(size: '18'), title: "Yo #{current_user.name}!", :alt => '') - %b.caret - %ul.dropdown-menu - = render 'layouts/user_menu' + .btn-group.pull-right + %ul.nav.navbar-nav.navbar-right + %li.dropdown + %a.dropdown-toggle{"data-toggle" => "dropdown", :href => "#", id: "current-user-detail"} + - if not current_user.name.blank? + #{current_user.name} + -else + #{current_user.email} + = image_tag(current_user.gravatar_url(size: '18'), title: "Yo #{current_user.name}!", :alt => '') + %b.caret + %ul.dropdown-menu + = render 'layouts/user_menu' + - if can? :index, Comment + %ul.nav.navbar-nav.navbar-right + %li.dropdown + %a.dropdown-toggle{"data-toggle" => "dropdown", :href => "#"} + - if unread_notifications(current_user) + Notifications (#{unread_notifications(current_user).length}) + %span.fa.fa-comment + %b.caret + %ul.dropdown-menu + - if unread_notifications(current_user).length > 0 + %li.dropdown-header Last 5 Comments for: + - unread_notifications(current_user).limit(5).group_by{ |comment| comment.commentable}.each do |event, comments| + %li= link_to("#{event.title}(#{comments.count})", admin_conference_event_path(event.conference.short_title, event.id)) + %li.divider + %li= link_to "See all unread Comments (#{unread_notifications(current_user).length})", admin_comments_path + %li= link_to 'See all Comments', admin_comments_path(anchor: 'all_comments') - else %ul.nav.navbar-nav.navbar-right - if CONFIG['authentication']['ichain']['enabled'] diff --git a/app/views/layouts/_user_menu.html.haml b/app/views/layouts/_user_menu.html.haml index fe349b3b..1764d9d0 100644 --- a/app/views/layouts/_user_menu.html.haml +++ b/app/views/layouts/_user_menu.html.haml @@ -21,14 +21,15 @@ = link_to(destroy_user_session_path, :method=>'delete') do %span.fa.fa-minus Sign out -- if can? :manage, Conference +- if can? :access, Admin %li.divider %li - = link_to(admin_conference_index_path()) do - - if Conference.any? + - if Conference.any? + = link_to(admin_conference_index_path()) do %span.fa.fa-home - Administration - - else + Administration + - if can? :create, Conference + =link_to(new_admin_conference_path) do %span.fa.fa-plus Create Conference -if @conference and @conference.id and can? :show, @conference diff --git a/config/routes.rb b/config/routes.rb index 5f8a59b6..05766f76 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,6 +15,7 @@ Osem::Application.routes.draw do namespace :admin do resources :users resources :people + resources :comments, only: [:index] resources :conference do member do get :roles diff --git a/spec/controllers/admin/comments_controller_spec.rb b/spec/controllers/admin/comments_controller_spec.rb new file mode 100644 index 00000000..ac75b189 --- /dev/null +++ b/spec/controllers/admin/comments_controller_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Admin::CommentsController, type: :controller do + + # It is necessary to use bang version of let to build roles before user + let(:conference) { create(:conference) } + let!(:first_user) { create(:user) } + let!(:organizer_role) { create(:role, name: 'organizer', resource: conference) } + let(:organizer) { create(:user, role_ids: organizer_role.id, last_sign_in_at: Time.now - 1.day) } + let(:participant) { create(:user) } + let(:event) { create(:event, conference: conference) } + let(:comment) { create(:comment, commentable_type: 'Event', commentable_id: event.id) } + + context 'not logged in user' do + describe 'GET #index' do + it 'renders the :index template' do + comment + get :index + expect(response).to redirect_to(user_session_path) + end + end + end + + context 'logged in as admin, organizer or cfp' do + before :each do + sign_in(organizer) + comment + end + describe 'GET #index' do + it 'populates a hash with comments' do + get :index + expect(assigns(:comments)).to be_a(Hash) + # assigns(:comments).first returns an array of first pair key-value from hash. + # Calling again 'first' returns the key, meaning the Conference object. + expect(assigns(:comments).first.first.title).to eq(comment.commentable.conference.title) + end + it 'has status 200: OK' do + get :index + expect(response).to have_http_status(:ok) + end + it 'renders the :index template' do + get :index + expect(response).to render_template(:index) + end + end + end + + context 'logged in with any other role or normal user' do + describe 'GET#index' do + it 'requires organizer privileges' do + sign_in(participant) + comment + get :index + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to match('You are not authorized to access this area!') + end + end + end +end diff --git a/spec/factories/comments.rb b/spec/factories/comments.rb new file mode 100644 index 00000000..a424e23b --- /dev/null +++ b/spec/factories/comments.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :comment do + body 'Most interresting comment ever, created by a girl.' + user + association :commentable, factory: :event + end +end diff --git a/spec/factories/events.rb b/spec/factories/events.rb index 537dfecb..6dc89b22 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -47,6 +47,7 @@ FactoryGirl.define do event.difficulty_level = build(:difficulty_level, conference: event.conference) event.track = build(:track, conference: event.conference) event.room = build(:room, conference: event.conference) + event.comment_threads << build(:comment, commentable: event) end end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index b3859748..73812dc1 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -141,6 +141,8 @@ describe 'User' do it{ should_not be_able_to(:manage, other_event.difficulty_level) } it{ should be_able_to(:manage, event.commercials.first) } it{ should_not be_able_to(:manage, other_event.commercials.first) } + it{ should be_able_to(:index, event.comment_threads.first) } + it{ should_not be_able_to(:index, other_event.comment_threads.first) } end context 'when user has the role cfp' do @@ -198,6 +200,8 @@ describe 'User' do it{ should_not be_able_to(:manage, other_event.difficulty_level) } it{ should be_able_to(:manage, event.commercials.first) } it{ should_not be_able_to(:manage, other_event.commercials.first) } + it{ should be_able_to(:index, event.comment_threads.first) } + it{ should_not be_able_to(:index, other_event.comment_threads.first) } end context 'when user has the role info_desk' do @@ -255,6 +259,8 @@ describe 'User' do it{ should_not be_able_to(:manage, other_event.difficulty_level) } it{ should_not be_able_to(:manage, event.commercials.first) } it{ should_not be_able_to(:manage, other_event.commercials.first) } + it{ should_not be_able_to(:index, event.comment_threads.first) } + it{ should_not be_able_to(:index, other_event.comment_threads.first) } end context 'when user has the role volunteers_coordinator' do @@ -312,6 +318,8 @@ describe 'User' do it{ should_not be_able_to(:manage, other_event.difficulty_level) } it{ should_not be_able_to(:manage, event.commercials.first) } it{ should_not be_able_to(:manage, other_event.commercials.first) } + it{ should_not be_able_to(:index, event.comment_threads.first) } + it{ should_not be_able_to(:index, other_event.comment_threads.first) } it 'should be_able to :manage Vposition' it 'should be_able to :manage Vday' end