diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c5388673..4e809209 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,7 +2,7 @@ class ApplicationController < ActionController::Base include CustomPaths helper CustomPaths - before_action :set_time_zone + before_action :set_time_zone, :set_notifications_for_current_user helper_method :can_edit?, :logged_in?, :admin? @@ -41,4 +41,8 @@ def require_admin def set_time_zone Time.zone = 'Sofia' end + + def set_notifications_for_current_user + @notifications = Notification.unread_for_user(current_user) if user_signed_in? + end end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 00000000..7a366864 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,10 @@ +class NotificationsController < ApplicationController + def index + end + + def show + notification = Notification.find params[:id] + notification.mark_as_read + redirect_to notification.source + end +end diff --git a/app/models/challenge.rb b/app/models/challenge.rb index 9dc3c9be..1992c320 100644 --- a/app/models/challenge.rb +++ b/app/models/challenge.rb @@ -3,6 +3,8 @@ class Challenge < ActiveRecord::Base validates :name, :description, presence: true + after_create :post_notification + class << self def in_chronological_order order('created_at ASC') @@ -16,4 +18,8 @@ def visible def closed? closes_at.past? end -end + + def post_notification + Notification.create_notifications_for self, to: User.all, title: "Новo предизвикателство: #{name}" + end +end \ No newline at end of file diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 00000000..bd8f5655 --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,27 @@ +class Notification < ActiveRecord::Base + belongs_to :user + belongs_to :source, polymorphic: true + + def mark_as_read + self.read = true + save! + end + + class << self + def unread_for_user(user) + where(user_id: user.id, read: false) + end + + def create_notifications_for(source, to: nil, title: nil) + unless to.nil? + to.find_each do |user| + Notification.create do |notification| + notification.title = title + notification.source = source + notification.user = user + end + end + end + end + end +end diff --git a/app/models/reply.rb b/app/models/reply.rb index 838536a9..08e2816f 100644 --- a/app/models/reply.rb +++ b/app/models/reply.rb @@ -1,10 +1,13 @@ class Reply < Post belongs_to :topic + belongs_to :user attr_accessible :body validates :topic_id, presence: true + after_create :post_notification + def topic_title topic.title end @@ -13,4 +16,10 @@ def page_in_topic replies_before_this = topic.replies.where('id < ?', id).count replies_before_this / Reply.per_page + 1 end + + private + + def post_notification + Notification.create_notifications_for topic, to: topic.participants, title: "Нов отговор в тема: #{topic_title}" + end end diff --git a/app/models/task.rb b/app/models/task.rb index 3fda9a47..af24d087 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -8,6 +8,8 @@ class Task < ActiveRecord::Base scope :checked, -> { where checked: true } scope :in_chronological_order, -> { order 'created_at ASC' } + after_create :post_notification + class << self def visible in_chronological_order.where(hidden: false) @@ -45,4 +47,8 @@ def restrictions_must_be_valid_yaml rescue Psych::SyntaxError errors.add :restrictions, :invalid end + + def post_notification + Notification.create_notifications_for self, to: User.all, title: "Нова задача: #{name}" + end end diff --git a/app/models/topic.rb b/app/models/topic.rb index e4ecb7dd..1befdcfa 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1,5 +1,7 @@ class Topic < Post has_many :replies, -> { order 'created_at ASC' } + + has_many :participants, -> { uniq }, through: :replies, source: :user attr_accessible :title, :body diff --git a/app/models/user.rb b/app/models/user.rb index b6cfbd74..39e57137 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,7 @@ class User < ActiveRecord::Base has_many :solutions has_many :tips has_many :attributions + has_many :notifications attr_protected :full_name, :faculty_number, :email, :admin diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 5e249698..226a19b1 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -21,6 +21,7 @@ %ul - if user_signed_in? %li Здрасти, #{link_to current_user.name, edit_profile_path} + %li.notifications-link #{link_to 'Известия', notifications_path} (#{@notifications.count}) %li= link_to 'Изход', destroy_user_session_path - else %li= link_to 'Вход', new_user_session_path diff --git a/app/views/notifications/index.html.haml b/app/views/notifications/index.html.haml new file mode 100644 index 00000000..51217e01 --- /dev/null +++ b/app/views/notifications/index.html.haml @@ -0,0 +1,11 @@ +%h1 Известия + +%p + Общо известия: + %strong= @notifications.count + +%table + %tbody + - @notifications.each do |notification| + %tr + %td.name.notification-name= link_to notification.title, notification \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 1366c7d3..e4b557fc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,8 @@ resources :vouchers, only: %w(index new create) + resources :notifications, only: %w(index show) + resources :announcements, except: %w(destroy) resource :profile, only: %w(edit update) resource :leaderboard, only: :show diff --git a/db/migrate/20150518105230_create_notifications.rb b/db/migrate/20150518105230_create_notifications.rb new file mode 100644 index 00000000..e4d39de9 --- /dev/null +++ b/db/migrate/20150518105230_create_notifications.rb @@ -0,0 +1,19 @@ +class CreateNotifications < ActiveRecord::Migration + include ForeignKeys + + def self.up + create_table(:notifications) do |t| + t.string :title, null: false + t.references :source, polymorphic: true + t.boolean :read, default: false + t.references :user + t.timestamps + end + + foreign_key :notifications, :user_id + end + + def self.down + drop_table :notifications + end +end diff --git a/features/notifications.feature b/features/notifications.feature new file mode 100644 index 00000000..61001a76 --- /dev/null +++ b/features/notifications.feature @@ -0,0 +1,45 @@ +# language: bg +Функционалност: Известия + Студентите получават известия, когато бъде публикувана задача, + предизвикателсвто или отговор във форум тема + + Сценарий: Получаване на известие при публикуване на задача + Дадено че съм влязъл като администратор + Когато създам задача: + | Име | Първа задача | + | Условие | Напишете програма | + То трябва да съществува известие "Нова задача: Първа задача" + + Сценарий: Получаване на известие при публикуване на предизвикателство + Дадено че съм влязъл като администратор + Когато създам предизвикателство "Четене на субтитри" + То трябва да съществува известие "Новo предизвикателство: Четене на субтитри" + + Сценарий: Получаване на известие при публикуване на отговор в тема + Дадено че съм студент + Дадено че съществува тема "Въпрос" + И че темата "Въпрос" има "1" отговора, последния от които на "Петър Петров" + Когато отговоря на "Въпрос" с: + """ + Моят текст + """ + То трябва да съществува известие "Нов отговор в тема: Въпрос" + + Сценарий: Потребителите виждат само непрочетени известия + Дадено че съм студент + Дадено че съществува известие "Прочетено известие" + Дадено че съществува непрочетено известие "Ново непрочетено известие" + Когато отворя страницата с известията + То трябва да виждам известие "Ново непрочетено известие" + И трябва да не виждам известие "Прочетено известие" + И броя на новите известия трябва да е 1 + + Сценарий: Когато избера ново известие съм пренасочен към страницата на първоизточника + Дадено че съм влязъл като администратор + Когато създам задача: + | Име | Първа задача | + | Условие | Напишете програма | + И отворя страницата с известията + И избера известието "Нова задача: Първа задача" + То трябва да съм на задачата "Първа задача" + И броя на новите известия трябва да е 0 \ No newline at end of file diff --git a/features/step_definitions/notifications_steps.rb b/features/step_definitions/notifications_steps.rb new file mode 100644 index 00000000..0aafc010 --- /dev/null +++ b/features/step_definitions/notifications_steps.rb @@ -0,0 +1,39 @@ +Дадено 'че съществува известие "$title"' do |title| + create :notification, title: title, user_id: current_user.id, read: true +end + +Дадено 'че съществува непрочетено известие "$title"' do |title| + create :notification, title: title, user_id: current_user.id, read: false +end + +Когато 'отворя страницата с известията' do + visit notifications_path +end + +Когато 'избера известието "$title"' do |title| + notification = Notification.find_by_title! title + visit notification_path(notification) +end + +То 'трябва да виждам известие "$title"' do |title| + within ".notification-name" do + page.should have_content title + end +end + +То 'трябва да не виждам известие "$title"' do |title| + within ".notification-name" do + page.should_not have_content title + end +end + +То 'броя на новите известия трябва да е $count' do |count| + within ".notifications-link" do + page.should have_content "(#{count})" + end +end + +То 'трябва да съществува известие "$title"' do |title| + notification = Notification.find_by_title! title + notification.should_not be_blank +end diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb new file mode 100644 index 00000000..a71ed5bb --- /dev/null +++ b/spec/controllers/notifications_controller_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe NotificationsController do + describe "GET index" do + it "assigns unread user notifications to @notifications" do + controller.stub_chain(:current_user).and_return(:user) + Notification.should_receive(:unread_for_user).with(:user).and_return('notifications') + get :index + assigns(:notifications).should eq 'notifications' + end + end + + describe "GET show" do + notification = FactoryGirl.build_stubbed(:notification) + + it "marks notification as read and redirects to its source" do + Notification.should_receive(:find).with('3').and_return(notification) + get :show, id: '3' + response.should redirect_to notification.source + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 52517e4e..8dc2de43 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -36,6 +36,12 @@ user end + factory :notification do + title 'Title' + association :source, factory: :topic + user + end + factory :reply do body 'Body' topic diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb new file mode 100644 index 00000000..3796a33e --- /dev/null +++ b/spec/models/notification_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Notification do + it "can be marked as read" do + notification = create(:notification) + notification.mark_as_read + notification.read.should eq true + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 437e3ff3..c1545de4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,5 +44,10 @@ config.include EmailSpec::Helpers, type: :mailer config.include EmailSpec::Matchers, type: :mailer config.include CustomPaths, type: :mailer + config.include Devise::TestHelpers, :type => :controller + + config.before(:each, type: :controller) do + allow(Notification).to receive(:unread_for_user).and_return([]) + end end end